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

Felix
fuente

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

dar un toque
fuente
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);
}
Nkosi
fuente
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
    }
};
Calin
fuente
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();
Luke
fuente
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());
James Wood
fuente
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;
    ..................
Devgig
fuente