web-dev-qa-db-fra.com

Se moquer d'IPrincipal dans ASP.NET Core

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 viennent de .NET 4.5 pour se moquer de HttpContext. Je suis sûr qu'il existe un meilleur moyen de le faire. J'ai essayé d'injecter IPrincipal, mais cela a jeté 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 aussi provoqué une erreur ... Je ne pouvais rien trouver dans la documentation non plus ...

47
Felix

La variable User du contrôleur est accessible via la variable HttpContext du contrôleur. Ce dernier est stocké dans ControllerContext .

Le moyen le plus simple de remplacer l'utilisateur consiste à affecter un autre HttpContext à un utilisateur construit. Nous pouvons utiliser DefaultHttpContext à cette fin, de cette façon vous n’aurez pas à vous moquer de tout:

var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
     new Claim(ClaimTypes.NameIdentifier, "1"),
     new Claim(MyCustomClaim, "example claim value")
}));

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

Dans les versions précédentes, vous pouviez définir User directement sur le contrôleur, ce qui permettait des tests unitaires très simples.

Si vous consultez le code source de ControllerBase , vous remarquerez que la variable User est extraite de HttpContext

/// <summary>
/// Gets or sets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User
{
    get
    {
        return HttpContext?.User;
    }
}

et le contrôleur accède à la HttpContext via ControllerContext

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

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

Donc, l'objectif est d'atteindre cet objet. Dans Core HttpContext est 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<IPrincipal>();
    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);
}
14
Nkosi

Je chercherais à mettre en œuvre un motif abstrait d'usine. 

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

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

Vous pouvez ensuite utiliser la classe de béton appropriée en fonction de la production par rapport au code de test. Peut-être cherchons-nous à passer l'usine en tant que paramètre ou à passer à la bonne usine 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());
2
James Wood

Dans mon cas, je devais utiliser Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Name et une certaine logique métier en dehors du contrôleur. J'ai pu utiliser une combinaison des réponses de Nkosi, Calin et Poke pour ceci:

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();
1
Luke

Vous pouvez vous moquer de HttpContext dans Net Core à l'aide de IHttpContextAccessor - comme ceci:

public class UserRepository : IUserRepository
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserRepository(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void LogCurrentUser()
    {
        var username = _httpContextAccessor.HttpContext.User.Identity.Name;
        service.LogAccessRequest(username);
    }
}

Ceci est pris de cette page: https://docs.Microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-2.2

0
Robert Perry

Il y a aussi la possibilité d'utiliser les classes existantes et de ne se moquer que si nécessaire.

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