Belldandy
Un blog? Que es esto, 2004? Mi nombre es Andrea, y hace muchos años que trabajo en sistemas.
Logo

Integration Tests con autenticacion en .NET

Publicado el 22 ene 2025, 15:53:56 —  Categorias: Backend, Tests

"Otro articulo mas hablando de las bondades de los integration tests?" Bueno, un poco si, pero este articulo va a ser 100% tecnico.

En el trabajo tuve que armar el proyecto de tests de integracion de una API existente (solo tenian tests de unidad), y me encontre con algunas particularidades que me hicieron renegar un poco. Asi que nada mejor que compartir conocimiento, y aca les cuento un poco como lo encare. Por supuesto si tienen sugerencias o comentarios, son mas que bienvenidos!

Empezamos por lo basico: la arquitectura de lo que tenemos que probar. Como esta armado, que usa, que hay que tener en cuenta, etc.

En mi caso, es un proyecto hecho en Angular con una API armada con MVC en .NET 6, la cual esta detras de autenticacion FedAuth con cookies (si, el proyecto es bastante viejo) con ciertos roles y claims a tener en cuenta.

El test de integracion debe llamar a la API, ejecutar realmente la informacion (no mocks, sino serian unit tests) y devolver un cierto valor. No nos enfocamos en la base de datos (para tests de integracion deberian ser efimeras, pero bueno, un pasito a la vez).

Bueno, empezamos!

  1. Primero, necesitamos las librerias basicas para hacer nuestro test. Voy a usar MSTest para los ejemplos, porque es lo que usamos (a mi tambien me gusta mas xUnit), pero es basicamente lo mismo
Microsoft.Extensions.Configuration
Microsoft.Extensions.DependencyInjection
Microsoft.NET.Test.Sdk
MSTest.TestAdapter
MSTest.TestFramework
coverlet.collector
Microsoft.AspnetCore.Mvc.Testing
  1. Tenemos que agregar una linea al Program.cs de la API, si estas usando esa forma de inicializar la API. De esta forma podemos crear nuestro WebApplicationFactory.
public partial class Program { }
  1. Despues, agregamos nuestra clase para el test. Para simplificar, pongo todo en la misma clase, pero obviamente dividanlo porque los handlers lo van a usar en todas las clases.
[TestClass]
public class IntegrationTests
{
    private static WebApplicationFactory<Program> _factory;
    private static HttpClient _client;

    private static ServiceProvider _serviceProvider;

    [ClassInitialize]
    public static void ClassInitialize(TestContext context)
    {
        _factory = new CustomWebApplicationFactory();
        _client = _factory.CreateClient();
    }

    private class CustomWebApplicationFactory : WebApplicationFactory<Program>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                //Options
                var configuration = new ConfigurationBuilder()
                    .AddUserSecrets<IntegrationTests>()
                    .AddJsonFile("appSettings.json")
                    .Build();

                services.AddSingleton<IConfiguration>(configuration);

                //Authentication
                services.AddTransient<IAuthenticationSchemeProvider, MockSchemeProvider>();
                services.AddAuthorization(options =>
                {
                    options.AddPolicy("MyPolicy", policy =>
                    {
                        policy.Requirements.Add(
                            new AppDesignationRequirement("MyPolicy"));
                        policy.RequireAuthenticatedUser();
                    });
                });
            });
        }
    }

    [ClassCleanup]
    public static void TestCleanup()
    {
        _serviceProvider?.Dispose();
        _client?.Dispose();
    }

Usando el WebApplicationFactory podemos sobre-escribir el Configure para configurar los servicios que nosotros querramos, entonces, lo que le decimos en este caso es que en vez de usar nuestro schema provider para autorizacion y autenticacion, vamos a usar el MockSchemeProvider

  1. En mi caso, los controllers tenian especificado el authorize y el policy como atributos del controller:
    [Route("api/[controller]")]
    [ApiController]
    [Authorize(AuthenticationSchemes = "MyScheme", Policy = "MyPolicy")]
    public class SampleController : ControllerBase {...}
  1. Finalmente, las dos estrellas de todo esto: primero el MockSchemeProvider que basicamente reemplaza toda nuestra logica de autenticacion, y devuelve un principal con los claims que necesitamos.
public class MockSchemeProvider : AuthenticationSchemeProvider
{
    public MockSchemeProvider(IOptions<AuthenticationOptions> options)
        : base(options) { }

    protected MockSchemeProvider(
        IOptions<AuthenticationOptions> options,
        IDictionary<string, AuthenticationScheme> schemes
    )
        : base(options, schemes) { }

    public override Task<AuthenticationScheme?> GetSchemeAsync(string name)
    {
        if (name != "MyScheme") return base.GetSchemeAsync(name);

        var scheme = new AuthenticationScheme(
            "MyScheme",
            "MyScheme",
            typeof(MockAuthenticationHandler)
        );
        return Task.FromResult(scheme);
    }
}

Y el MockAuthenticationHandler que siempre devuelve un principal con los claims que necesitamos, como si el usuario hubiera pasado por un proceso de autenticacion "real":

public class MockAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public MockAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock
    )
        : base(options, logger, encoder, clock) { }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        const string ALLOWED_ROLE = "RoleA";

        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "test@example.com"),
            new Claim(ClaimTypes.NameIdentifier, "123"),
            new Claim("Role", ALLOWED_ROLE)
            // Add any claims your app needs
        };
        var identity = new ClaimsIdentity(claims, "MySchema");
        var principal = new ClaimsPrincipal(identity);

        var ticket = new AuthenticationTicket(principal, Scheme.Name);

        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}

De esta forma, cuando el unit test le mande un pedido para la API (por ej, /api/cliente/1), lo que termina pasando es que se inyecta este handler "mentiroso", y vas a tener un Principal disponible con los claims y roles que necesites. Despues si queres podes hacer un test de integracion de tu proceso de login (en mi casi, el login es un Identity provider, esta recontra testeado en otro proyecto, y no tenia sentido volver a testearlo aca)

    [TestMethod]
    [DataRow(1)]
    public async Task Get_Cliente_Should_Return_OK(int Id)
    {
        var results = await _client.GetAsync($"/api/cliente/{Id}");

        Assert.IsNotNull(results);
        Assert.IsTrue(results.IsSuccessStatusCode);

        var answer = await results.Content.ReadAsStringAsync();
        var clienteResult = JsonSerializer.Deserialize<ClientModel>(answer,
            new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
        Assert.IsNotNull(clienteResult);

        Assert.AreEqual(clienteResult.ID, Id);
    }

Haciendo esto logramos hacer un test de integracion completo hacia la API, pero "mockeando" la autenticacion, y sin tener que poner IF ni headers "peligrosos" que pueden llegar a causar problemas.

Volver

Comentarios Recientes

No hay comentarios, porque no dejás alguno?

¡Comentario agregado con éxito!
Angel

Deja un comentario

(no se publica)