c# - net - exam 70-486
Burlándose de IPrincipal en ASP.NET Core (6)
Tengo una aplicación ASP.NET MVC Core para la que estoy escribiendo pruebas unitarias. Uno de los métodos de acción utiliza el nombre de usuario para algunas funciones:
SettingsViewModel svm = _context.MySettings(User.Identity.Name);
que obviamente falla en la prueba unitaria. Miré a mi alrededor y todas las sugerencias son de .NET 4.5 para burlarse de HttpContext. Estoy seguro de que hay una mejor manera de hacerlo. Traté de inyectar IPrincipal, pero arrojó un error; e incluso intenté esto (por desesperación, supongo):
public IActionResult Index(IPrincipal principal = null) {
IPrincipal user = principal ?? User;
SettingsViewModel svm = _context.MySettings(user.Identity.Name);
return View(svm);
}
pero esto arrojó un error también. No pude encontrar nada en los documentos tampoco ...
En mi caso, necesitaba hacer uso de
Request.HttpContext.User.Identity.IsAuthenticated
,
Request.HttpContext.User.Identity.Name
y algo de lógica de negocios fuera del controlador.
Pude usar una combinación de la respuesta de Nkosi, Calin y Poke para esto:
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();
En versiones anteriores, podría haber configurado
User
directamente en el controlador, lo que hizo algunas pruebas unitarias muy fáciles.
Si observa el código fuente de
ControllerBase
, notará que el
User
se extrae 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;
}
}
y el controlador accede al
HttpContext
través de
ControllerContext
/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext
{
get
{
return ControllerContext.HttpContext;
}
}
Notará que estas dos son propiedades de solo lectura.
La buena noticia es que la propiedad
ControllerContext
permite establecer su valor para que sea su camino.
Entonces el objetivo es llegar a ese objeto.
En Core
HttpContext
es abstracto, por lo que es mucho más fácil burlarse de él.
Asumiendo un 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 Moq, una prueba podría verse así
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);
}
Me gustaría implementar un patrón abstracto de fábrica.
Cree una interfaz para una fábrica específicamente para proporcionar nombres de usuario.
Luego proporcione clases concretas, una que proporcione
User.Identity.Name
y otra que proporcione algún otro valor codificado que funcione para sus pruebas.
Luego puede usar la clase concreta apropiada dependiendo de la producción versus el código de prueba. Quizás busque pasar la fábrica como parámetro, o cambiar a la fábrica correcta en función de algún valor de configuración.
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());
Puede burlarse de HttpContext en Net Core usando IHttpContextAccessor, de esta manera:
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);
}
}
Esto está tomado de esta página: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-2.2
Se
accede
al
User
del controlador
a
través del
HttpContext
del controlador
.
Este último
se almacena
en el
ControllerContext
.
La forma más fácil de configurar al usuario es asignando un HttpContext diferente con un usuario construido.
Podemos usar
DefaultHttpContext
para este propósito, de esa manera no tenemos que burlarnos de todo.
Luego solo usamos ese HttpContext dentro de un contexto de controlador y lo pasamos a la instancia del 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 }
};
Al crear su propia
ClaimsIdentity
, asegúrese de pasar un tipo de
authenticationType
explícito al constructor.
Esto asegura que
IsAuthenticated
funcionará correctamente (en caso de que use eso en su código para determinar si un usuario está autenticado).
También existe la posibilidad de usar las clases existentes, y simulacros solo cuando sea necesario.
var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = user.Object
}
};