Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Create fake user for ASP.NET Core integration tests

After getting done with fake users for ASP.NET Core controller unit tests I wanted to make fake users available also in Integration Tests. It took some inventing and hacking but I made it work. This blog post shows you how to create fake users for ASP.NET Core integration tests and write effective extension methods to keep integration tests shorter.

Source code available! Thin and clean demo solution with ASP.NET Core testing examples is available in my Github repository gpeipman/AspNetCoreTests.

Getting started with integration tests

Let’s start with default integration test for home controller. Here is the integration test to test Index and Privacy actions of HomeController.

public class HomeControllerTests : TestBase
{
    public HomeControllerTests(TestApplicationFactoryFakeStartup> factory) : base(factory)
    {
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Home/Privacy")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = Factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
}

If there’s no Authentication and authorization enabled then this test will pass.

Adding authentication to web application

Let’s enable authentication in web application and configure it to use whatever authentication we need to support. My demo application uses cookie authentication and it is enabled in Configure() and ConfigureServices() method of Startup class.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public virtual void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContextDemoDbContext>(options =>
        {
            options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
        });

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
                    options =>
                    {
                        options.LoginPath = new PathString("/auth/login");
                        options.AccessDeniedPath = new PathString("/auth/denied");
                    });

        services.AddAuthorization();
        services.AddControllersWithViews();

        services.AddScopedICustomerService, CustomerService>();
    }

    public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();
        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }

For testing purposes let’s add AuthorizeAttribute to HomeController.

[Authorize]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    public IActionResult Privacy()
    {
        return View();
    }
}

Integration test may succeed because by default it follows redirects and if we are redirected to login page then we get back HTTP response 200. To make sure that client doesn’t follow redirects we have to modify our test.

[Theory]
[InlineData("/")]
[InlineData("/Home/Privacy")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
    // Arrange
    var client = Factory.CreateClient(new WebApplicationFactoryClientOptions
    {
        AllowAutoRedirect = false
    });

    // Act
    var response = await client.GetAsync(url);

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
}

Integration test will now fail.

Adding fake authentication for integration tests

With controller unit tests it was easy to have authenticated user like I demonstrated in my blog post Create fake user for ASP.NET Core controller tests. Integration tests are different beast and it’s not so easy to get under the skin.

Here’s the plan based on official documentation:

  • When application is started in test host add new authentication scheme (let’s call it Test).
  • Configure authentication scheme to use custom authentication handler (TestAuthHandler) that creates fake identity for integration tests.
  • Extend test client to use authentication header with scheme Test.

Let’s start with authentication handler.

public class TestAuthHandler : AuthenticationHandlerAuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitorAuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override TaskAuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "Test user"),
            new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())
        };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

This handler creates fake user when HandleAuthenticateAsync() is called. We don’t need any additional hacks to make ASP.NET Core application use this fake identity. Our integration test needs also some changes because of authentication handler.

[Theory]
[InlineData("/")]
[InlineData("/Home/Privacy")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
    // Arrange
    var factory = Factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication("Test")
                    .AddSchemeAuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });
        });
    });

    var client = factory.CreateClient(new WebApplicationFactoryClientOptions
    {
        AllowAutoRedirect = false
    });

    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");

    // Act
    var response = await client.GetAsync(url);

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
}

Now our integration test will pass.

Moving fake authentication to extension methods

Although our integration test works fine it looks ugly and in long run this approach will end up with huge amount of duplicated code as we need authenticated calls in many other integration tests too. This is why I move all authentication related code to extension methods.

public static class WebApplicationFactoryExtensions
{
    public static WebApplicationFactoryT> WithAuthenticationT>(this WebApplicationFactoryT> factory) where T : class
    {
        return factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                        .AddSchemeAuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });
            });
        });
    }

    public static HttpClient CreateClientWithTestAuthT>(this WebApplicationFactoryT> factory) where T : class
    {
        var client = factory.WithAuthentication().CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");

        return client;
    }
}

Using these extension methods we can make our integration test way shorter.

[Theory]
[InlineData("/")]
[InlineData("/Home/Privacy")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
    // Arrange
    var client = Factory.CreateClientWithTestAuth();

    // Act
    var response = await client.GetAsync(url);

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
}

We can use same approach for authenticated user also in other integration tests.

NB! Solution shown here fits well for simpler applications that doesn’t depend heavily on small details of authentication and authorization. For more complex scenarious it’s possible to use this sample as a base to get started.

Wrapping up

Faking identities for ASP.NET Core integration tests wasn’t easy and straightforward. We had to configure web application through web application factory and custom authentication handler to make get fake user to application. This method is not perfect for applications that depend on small details of authentication or authorization. But as far as only user claims are used this solution works very well. In the end we moved user related code to extension methods and achieved shorter integration test.

The post Create fake user for ASP.NET Core integration tests appeared first on Gunnar Peipman - Programming Blog.



This post first appeared on Gunnar Peipman - Programming, please read the originial post: here

Share the post

Create fake user for ASP.NET Core integration tests

×

Subscribe to Gunnar Peipman - Programming

Get updates delivered right to your inbox!

Thank you for your subscription

×