Mocking IPrincipal dans ASP.NET Core

89

J'ai une application ASP.NET MVC Core pour laquelle j'écris des tests unitaires. L'une des méthodes d'action utilise le nom d'utilisateur pour certaines fonctionnalités:

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

qui échoue évidemment dans le test unitaire. J'ai regardé autour de moi et toutes les suggestions proviennent de .NET 4.5 pour simuler HttpContext. Je suis sûr qu'il existe une meilleure façon de faire cela. J'ai essayé d'injecter IPrincipal, mais cela a généré une erreur; et j'ai même essayé ceci (par désespoir, je suppose):

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

mais cela a également jeté une erreur. Je n'ai rien trouvé dans la documentation non plus ...

Félix
la source

Réponses:

179

Le contrôleur est accessible via le contrôleur . Ce dernier est stocké dans le fichier .User HttpContextControllerContext

Le moyen le plus simple de définir l'utilisateur consiste à attribuer un HttpContext différent à un utilisateur construit. Nous pouvons utiliser DefaultHttpContextà cette fin, de cette façon nous n'avons pas à se moquer de tout. Ensuite, nous utilisons simplement ce HttpContext dans un contexte de contrôleur et le transmettons à l'instance de contrôleur:

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 }
};

Lorsque vous créez le vôtre ClaimsIdentity, assurez-vous de transmettre unauthenticationType au constructeur. Cela garantit que cela IsAuthenticatedfonctionnera correctement (au cas où vous l'utiliseriez dans votre code pour déterminer si un utilisateur est authentifié).

poussée
la source
7
Dans mon cas, c'était new Claim(ClaimTypes.Name, "1")pour correspondre à l'utilisation du contrôleur user.Identity.Name; mais sinon c'est exactement ce que j'essayais de réaliser ... Danke schon!
Felix
Après d'innombrables heures de recherche, c'est le poste qui m'a finalement permis de m'éloigner. Dans ma méthode de contrôleur de projet core 2.0, j'utilisais User.FindFirstValue(ClaimTypes.NameIdentifier);pour définir userId sur un objet que je créais et échouait car le principal était nul. Cela a résolu cela pour moi. Merci pour la bonne réponse!
Timothy Randall
Je cherchais également d'innombrables heures pour faire fonctionner UserManager.GetUserAsync et c'est le seul endroit où j'ai trouvé le lien manquant. Merci! Besoin de configurer un ClaimsIdentity contenant une Claim, pas d'utiliser un GenericIdentity.
Etienne Charland
17

Dans les versions précédentes, vous auriez pu définir Userdirectement sur le contrôleur, ce qui permettait des tests unitaires très faciles.

Si vous regardez le code source de ControllerBase, vous remarquerez que le Userest extrait de HttpContext.

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

et le contrôleur accède au HttpContextviaControllerContext

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

Vous remarquerez que ces deux propriétés sont en lecture seule. La bonne nouvelle est que la ControllerContextpropriété permet de définir sa valeur afin que ce soit votre chemin.

Donc, l'objectif est d'atteindre cet objet. In Core HttpContextest abstrait, il est donc beaucoup plus facile de se moquer.

En supposant un contrôleur comme

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 
}

En utilisant Moq, un test pourrait ressembler à ceci

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);
}
Nkosi
la source
3

Il est également possible d'utiliser les classes existantes et de se moquer uniquement lorsque cela est nécessaire.

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

Dans mon cas, je devais utiliser Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Nameet une logique d'affaires assis à l' extérieur du contrôleur. J'ai pu utiliser une combinaison des réponses de Nkosi, Calin et Poke pour cela:

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();
Luke
la source
2

Je chercherais à implémenter un modèle de fabrique abstraite.

Créez une interface pour une fabrique spécifiquement pour fournir des noms d'utilisateur.

Ensuite, fournissez des classes concrètes, une qui fournit User.Identity.Name et une qui fournit une autre valeur codée en dur qui fonctionne pour vos tests.

Vous pouvez ensuite utiliser la classe concrète appropriée en fonction de la production par rapport au code de test. Vous cherchez peut-être à transmettre l'usine en tant que paramètre ou à passer à l'usine appropriée en fonction d'une valeur de configuration.

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());
James Wood
la source
Merci. Je fais quelque chose de similaire pour mes objets. J'espérais juste que pour une chose aussi courante que IPrinicpal, il y aurait quelque chose de "hors de la boîte". Mais apparemment, non!
Felix
En outre, l'utilisateur est une variable membre de ControllerBase. C'est pourquoi dans les versions précédentes d'ASP.NET, les gens se moquaient de HttpContext et obtenaient IPrincipal à partir de là. On ne peut pas simplement obtenir User d'une classe autonome, comme ProductionFactory
Felix
1

Je veux frapper directement mes contrôleurs et utiliser simplement DI comme AutoFac. Pour ce faire, je m'inscris d'abord 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);

Ensuite, j'active l'injection de propriété lorsque j'enregistre les contrôleurs.

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

Puis User.Identity.Nameest rempli et je n'ai rien de spécial à faire lors de l'appel d'une méthode sur mon Controller.

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
Devgig
la source