Mocking IPrincipal in ASP.NET Core


91

Ho un'applicazione ASP.NET MVC Core per cui sto scrivendo unit test. Uno dei metodi di azione utilizza il nome utente per alcune funzionalità:

SettingsViewModel svm = _context.MySettings(User.Identity.Name);

che ovviamente fallisce nello unit test. Mi sono guardato intorno e tutti i suggerimenti provengono da .NET 4.5 per simulare HttpContext. Sono sicuro che ci sia un modo migliore per farlo. Ho provato a iniettare IPrincipal, ma ha generato un errore; e ho anche provato questo (per disperazione, suppongo):

public IActionResult Index(IPrincipal principal = null) {
    IPrincipal user = principal ?? User;
    SettingsViewModel svm = _context.MySettings(user.Identity.Name);
    return View(svm);
}

ma anche questo ha generato un errore. Non ho trovato nulla nemmeno nei documenti ...

Risposte:


182

Il controller è accessibile tramite il del controller . Quest'ultimo è memorizzato all'interno del file .User HttpContextControllerContext

Il modo più semplice per impostare l'utente consiste nell'assegnare un diverso HttpContext a un utente costruito. Possiamo usare DefaultHttpContextper questo scopo, in questo modo non dobbiamo prendere in giro tutto. Quindi usiamo semplicemente quell'HttpContext all'interno di un contesto del controller e lo passiamo all'istanza del controller:

var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
    new Claim(ClaimTypes.Name, "example name"),
    new Claim(ClaimTypes.NameIdentifier, "1"),
    new Claim("custom-claim", "example claim value"),
}, "mock"));

var controller = new SomeController(dependencies…);
controller.ControllerContext = new ControllerContext()
{
    HttpContext = new DefaultHttpContext() { User = user }
};

Quando crei il tuo ClaimsIdentity, assicurati di passare un esplicito authenticationTypeal costruttore. Questo assicura che IsAuthenticatedfunzionerà correttamente (nel caso in cui lo utilizzi nel codice per determinare se un utente è autenticato).


7
Nel mio caso era new Claim(ClaimTypes.Name, "1")per abbinare l'uso del controller user.Identity.Name; ma per il resto è esattamente quello che stavo cercando di ottenere ... Danke schon!
Felix

Dopo innumerevoli ore di ricerca, questo è stato il post che finalmente mi ha fatto quadrare. Nel mio metodo di controller di progetto 2.0 di base stavo utilizzando User.FindFirstValue(ClaimTypes.NameIdentifier);per impostare userId su un oggetto che stavo creando e non funzionava perché principal era null. Questo ha risolto il problema per me. Grazie per la magnifica risposta!
Timothy Randall

Stavo anche cercando innumerevoli ore per far funzionare UserManager.GetUserAsync e questo è l'unico posto in cui ho trovato il collegamento mancante. Grazie! È necessario impostare una ClaimsIdentity contenente una Claim, non utilizzare GenericIdentity.
Etienne Charland

17

Nelle versioni precedenti avresti potuto impostare Userdirettamente sul controller, il che ha reso alcuni test unitari molto semplici.

Se guardi il codice sorgente di ControllerBase noterai che Userviene estratto da HttpContext.

/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User => HttpContext?.User;

e il controller accede al HttpContextviaControllerContext

/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext => ControllerContext.HttpContext;

Noterai che queste due sono proprietà di sola lettura. La buona notizia è che la ControllerContextproprietà consente di impostare il suo valore in modo che sia la tua strada.

Quindi l'obiettivo è raggiungere quell'oggetto. In Core HttpContextè astratto, quindi è molto più facile da deridere.

Supponendo che un controller come

public class MyController : Controller {
    IMyContext _context;

    public MyController(IMyContext context) {
        _context = context;
    }

    public IActionResult Index() {
        SettingsViewModel svm = _context.MySettings(User.Identity.Name);
        return View(svm);
    }

    //...other code removed for brevity 
}

Utilizzando Moq, un test potrebbe essere simile a questo

public void Given_User_Index_Should_Return_ViewResult_With_Model() {
    //Arrange 
    var username = "FakeUserName";
    var identity = new GenericIdentity(username, "");

    var mockPrincipal = new Mock<ClaimsPrincipal>();
    mockPrincipal.Setup(x => x.Identity).Returns(identity);
    mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);

    var mockHttpContext = new Mock<HttpContext>();
    mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

    var model = new SettingsViewModel() {
        //...other code removed for brevity
    };

    var mockContext = new Mock<IMyContext>();
    mockContext.Setup(m => m.MySettings(username)).Returns(model);

    var controller = new MyController(mockContext.Object) {
        ControllerContext = new ControllerContext {
            HttpContext = mockHttpContext.Object
        }
    };

    //Act
    var viewResult = controller.Index() as ViewResult;

    //Assert
    Assert.IsNotNull(viewResult);
    Assert.IsNotNull(viewResult.Model);
    Assert.AreEqual(model, viewResult.Model);
}

3

C'è anche la possibilità di utilizzare le classi esistenti e deridere solo quando necessario.

var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext
    {
        User = user.Object
    }
};

3

Nel mio caso, avevo bisogno di fare uso di Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Namee un po 'la logica di business seduta al di fuori del controller. Sono stato in grado di utilizzare una combinazione delle risposte di Nkosi, Calin e Poke per questo:

var identity = new Mock<IIdentity>();
identity.SetupGet(i => i.IsAuthenticated).Returns(true);
identity.SetupGet(i => i.Name).Returns("FakeUserName");

var mockPrincipal = new Mock<ClaimsPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity.Object);

var mockAuthHandler = new Mock<ICustomAuthorizationHandler>();
mockAuthHandler.Setup(x => x.CustomAuth(It.IsAny<ClaimsPrincipal>(), ...)).Returns(true).Verifiable();

var controller = new MyController(...);

var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext()
{
    User = mockPrincipal.Object
};

var result = controller.Get() as OkObjectResult;
//Assert results

mockAuthHandler.Verify();

2

Cercherei di implementare un Abstract Factory Pattern.

Creare un'interfaccia per una fabbrica specifica per fornire i nomi utente.

Quindi fornisci classi concrete, una che fornisce User.Identity.Namee una che fornisce qualche altro valore hardcoded che funziona per i tuoi test.

È quindi possibile utilizzare la classe concreta appropriata a seconda della produzione rispetto al codice di test. Forse cercando di passare la fabbrica come parametro o passare alla fabbrica corretta in base a un valore di configurazione.

interface IUserNameFactory
{
    string BuildUserName();
}

class ProductionFactory : IUserNameFactory
{
    public BuildUserName() { return User.Identity.Name; }
}

class MockFactory : IUserNameFactory
{
    public BuildUserName() { return "James"; }
}

IUserNameFactory factory;

if(inProductionMode)
{
    factory = new ProductionFactory();
}
else
{
    factory = new MockFactory();
}

SettingsViewModel svm = _context.MySettings(factory.BuildUserName());

Grazie. Sto facendo qualcosa di simile per i miei oggetti. Speravo solo che per una cosa comune come IPrinicpal, ci sarebbe stato qualcosa di "fuori dagli schemi". Ma a quanto pare no!
Felix

Inoltre, l'utente è una variabile membro di ControllerBase. Ecco perché nelle versioni precedenti di ASP.NET le persone deridevano HttpContext e da lì ricavavano IPrincipal. Non si può semplicemente ottenere User da una classe autonoma, come ProductionFactory
Felix

1

Voglio colpire direttamente i miei controller e usare solo DI come AutoFac. Per fare questo mi registro prima ContextController.

var identity = new GenericIdentity("Test User");
var httpContext = new DefaultHttpContext()
{
    User = new GenericPrincipal(identity, null)
};

var context = new ControllerContext { HttpContext = httpContext};
builder.RegisterInstance(context);

Successivamente abilito l'inserimento di proprietà quando registro i controller.

  builder.RegisterAssemblyTypes(assembly)
                    .Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();

Quindi User.Identity.Nameviene popolato e non devo fare nulla di speciale quando chiamo un metodo sul mio controller.

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.