Mocking IPrincipal no ASP.NET Core

91

Eu tenho um aplicativo ASP.NET MVC Core para o qual estou escrevendo testes de unidade. Um dos métodos de ação usa o nome de usuário para alguma funcionalidade:

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

que obviamente falha no teste de unidade. Eu olhei em volta e todas as sugestões são do .NET 4.5 para simular HttpContext. Tenho certeza de que existe uma maneira melhor de fazer isso. Tentei injetar IPrincipal, mas gerou um erro; e eu até tentei isso (por desespero, suponho):

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

mas isso gerou um erro também. Também não foi possível encontrar nada nos documentos ...

Felix
fonte

Respostas:

182

O controlador é acessado por meio do do controlador . O último é armazenado em .User HttpContextControllerContext

A maneira mais fácil de definir o usuário é atribuindo um HttpContext diferente a um usuário construído. Podemos usar DefaultHttpContextpara esse fim, dessa forma não temos que zombar de tudo. Então, apenas usamos esse HttpContext dentro de um contexto de controlador e passamos para a instância do controlador:

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

Ao criar o seu próprio ClaimsIdentity, certifique-se de passar um explícito authenticationTypepara o construtor. Isso garante que IsAuthenticatedfuncionará corretamente (caso você use isso em seu código para determinar se um usuário está autenticado).

cutucar
fonte
7
No meu caso, foi new Claim(ClaimTypes.Name, "1")para corresponder ao uso do controlador user.Identity.Name; mas fora isso é exatamente o que eu estava tentando alcançar ... Danke schon!
Felix
Depois de incontáveis ​​horas procurando, este foi o posto que finalmente me deixou em paz. Em meu método de controlador de projeto core 2.0, eu estava utilizando User.FindFirstValue(ClaimTypes.NameIdentifier);para definir o userId em um objeto que estava criando e estava falhando porque o principal era nulo. Isso consertou isso para mim. Obrigado pela ótima resposta!
Timothy Randall
Eu também estava procurando horas incontáveis ​​para fazer UserManager.GetUserAsync funcionar e este é o único lugar onde encontrei o link que faltava. Obrigado! É necessário configurar um ClaimsIdentity contendo um Claim, não usar um GenericIdentity.
Etienne Charland,
17

Nas versões anteriores, você poderia ter configurado Userdiretamente no controlador, o que tornava alguns testes de unidade muito fáceis.

Se você olhar o código-fonte do ControllerBase , notará que o Userfoi extraído HttpContext.

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

e o controlador acessa a HttpContextviaControllerContext

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

Você notará que essas duas são propriedades somente leitura. A boa notícia é que a ControllerContextpropriedade permite definir seu valor para que seja o seu caminho.

Portanto, o objetivo é chegar a esse objeto. No Core HttpContexté abstrato, então é muito mais fácil de zombar.

Assumindo um controlador como

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 
}

Usando o Moq, um teste poderia ser assim

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
fonte
3

Também existe a possibilidade de usar as classes existentes, e simular apenas quando necessário.

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

No meu caso, eu precisava fazer uso Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Namee alguma lógica de negócios sentado fora do controlador. Consegui usar uma combinação das respostas de Nkosi, Calin e Poke para isso:

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();
Lucas
fonte
2

Eu procuraria implementar um Abstract Factory Pattern.

Crie uma interface para uma fábrica especificamente para fornecer nomes de usuário.

Em seguida, forneça classes concretas, uma que forneça User.Identity.Namee outra que forneça algum outro valor embutido em código que funcione para seus testes.

Você pode então usar a classe concreta apropriada dependendo da produção versus código de teste. Talvez procurando passar a fábrica como um parâmetro ou alternando para a fábrica correta com base em algum valor de configuração.

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
fonte
Obrigado. Estou fazendo algo semelhante para meus objetos. Eu só esperava que para algo tão comum como o IPrinicpal, houvesse algo "fora da caixa". Mas, aparentemente, não!
Felix
Além disso, User é uma variável membro de ControllerBase. É por isso que nas versões anteriores do ASP.NET as pessoas zombavam de HttpContext e estavam obtendo IPrincipal de lá. Não se pode simplesmente obter o usuário de uma classe independente, como ProductionFactory
Felix
1

Eu quero acertar meus controladores diretamente e apenas usar DI como AutoFac. Para fazer isso, primeiro me inscrevo 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);

Em seguida, habilito a injeção de propriedade ao registrar os controladores.

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

Em seguida, User.Identity.Nameé preenchido e não preciso fazer nada de especial ao chamar um método no meu controlador.

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