Burlarse de IPrincipal en ASP.NET Core


89

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 simular HttpContext. Estoy seguro de que hay una mejor manera de hacerlo. Intenté inyectar IPrincipal, pero arrojó un error; e incluso probé 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. Tampoco se pudo encontrar nada en los documentos ...

Respuestas:


179

Se accede al controlador a través del controlador . Este último se almacena dentro del .User HttpContextControllerContext

La forma más sencilla de configurar el usuario es asignando un HttpContext diferente con un usuario construido. Podemos usar DefaultHttpContextpara 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 }
};

Cuando cree el suyo ClaimsIdentity, asegúrese de pasar un explícito authenticationTypeal constructor. Esto asegura que IsAuthenticatedfuncionará correctamente (en caso de que lo use en su código para determinar si un usuario está autenticado).


7
En mi caso, fue new Claim(ClaimTypes.Name, "1")para hacer coincidir el uso del controlador de user.Identity.Name; pero por lo demás es exactamente lo que estaba tratando de lograr ... ¡Danke schon!
Felix

Después de innumerables horas de búsqueda, esta fue la publicación que finalmente me ayudó. En mi método de controlador de proyecto core 2.0 que estaba utilizando User.FindFirstValue(ClaimTypes.NameIdentifier);para establecer el userId en un objeto que estaba creando y estaba fallando porque el principal era nulo. Esto arregló eso para mí. ¡Gracias por la gran respuesta!
Timothy Randall

También estuve buscando innumerables horas para que UserManager.GetUserAsync funcionara y este es el único lugar donde encontré el enlace que faltaba. ¡Gracias! Necesita configurar una ClaimsIdentity que contenga una Claim, no usar una GenericIdentity.
Etienne Charland

17

En versiones anteriores, podría haberlo configurado Userdirectamente en el controlador, lo que hizo que algunas pruebas unitarias fueran muy fáciles.

Si observa el código fuente de ControllerBase , notará que Userse extrae de HttpContext.

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

y el controlador accede a la HttpContextvíaControllerContext

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

Notará que estas dos son propiedades de solo lectura. La buena noticia es que la ControllerContextpropiedad permite establecer su valor para que sea su entrada.

Entonces el objetivo es llegar a ese objeto. En Core HttpContextes abstracto, por lo que es mucho más fácil burlarse.

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

También existe la posibilidad de usar las clases existentes y simularlas solo cuando sea necesario.

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

3

En mi caso, necesitaba hacer uso de Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Namey algo de lógica de negocios sentado 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();

2

Buscaría implementar un patrón de fábrica abstracto.

Cree una interfaz para una fábrica específicamente para proporcionar nombres de usuario.

Luego, proporcione clases concretas, una que proporcione User.Identity.Namey otra que proporcione algún otro valor codificado que funcione para sus pruebas.

A continuación, puede utilizar la clase de hormigón adecuada según la producción frente al código de prueba. Quizás esté buscando 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());

Gracias. Estoy haciendo algo similar con mis objetos. Solo esperaba que para algo tan común como IPrinicpal, hubiera algo "fuera de la caja". ¡Pero aparentemente no!
Felix

Además, el usuario es una variable miembro de ControllerBase. Es por eso que en versiones anteriores de ASP.NET la gente se burlaba de HttpContext y obtenían IPrincipal desde allí. No se puede simplemente obtener User de una clase independiente, como ProductionFactory
Felix

1

Quiero tocar mis controladores directamente y usar DI como AutoFac. Para hacer esto primero me registro 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);

A continuación, habilito la inyección de propiedades cuando registro los controladores.

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

Luego User.Identity.Namese completa y no necesito hacer nada especial al llamar a un método en mi controlador.

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.