DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • GDPR Compliance With .NET: Securing Data the Right Way
  • How to Enhance the Performance of .NET Core Applications for Large Responses
  • Developing Minimal APIs Quickly With Open Source ASP.NET Core
  • Revolutionizing Content Management

Trending

  • Integrating Model Context Protocol (MCP) With Microsoft Copilot Studio AI Agents
  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • AI’s Role in Everyday Development
  • Article Moderation: Your Questions, Answered
  1. DZone
  2. Coding
  3. Frameworks
  4. Using ASP.NET Core Identity Users in Integration Tests

Using ASP.NET Core Identity Users in Integration Tests

This blog post shows how to set selectively set authenticated ASP.NET Core user identities for ASP.NET Core integration tests.

By 
Gunnar Peipman user avatar
Gunnar Peipman
·
Apr. 15, 19 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
19.0K Views

Join the DZone community and get the full member experience.

Join For Free

I have an application that uses ASP.NET Core Identity with classic logins and there's a need to cover this application with integration tests. Some tests are for anonymous users and others for authenticated users. This blog post shows how to set selectively set authenticated ASP.NET Core user identities for ASP.NET Core integration tests.

Getting Started

We start with an ASP.NET Core web application where basic authentication is done using ASP.NET Core Identity. There's an integration tests project that uses a fake startup class and custom appsettings.json from my blog post, Using custom startup class with ASP.NET Core integration tests. Take a look at this post as there are some additional classes defined.

Our starting point is simple integration test from my previous blog post.

public class HomeControllerTests : IClassFixture<MediaGalleryFactory<FakeStartup>>
{
    private readonly WebApplicationFactory<FakeStartup> _factory;
 
    public HomeControllerTests(MediaGalleryFactory<FakeStartup> factory)
    {
        var projectDir = Directory.GetCurrentDirectory();
        var configPath = Path.Combine(projectDir, "appsettings.json");
 
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.UseSolutionRelativeContentRoot("MediaGallery");
 
            builder.ConfigureAppConfiguration(conf =>
            {
                conf.AddJsonFile(configPath);
            });
 
            builder.ConfigureTestServices(services =>
            {
                services.AddMvc().AddApplicationPart(typeof(Startup).Assembly);
            });
        });
    }
 
    [Theory]
    [InlineData("/")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();
 
        // Act
        var response = await client.GetAsync(url);
 
        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
}

To work with identity, we need also controller action that has authorize attribute. Here's the action of home controller we will use for this post.

[Authorize(Roles = "Admin")]
[Route("/foradmin")]
public IActionResult ForAdmin()
{
    return Content(string.Format("User: {0}, is admin:  {1}", 
                   User.Identity.Name, User.IsInRole("Admin")));
}

For this action we add test from ASP.NET Core integration testing documentation.

[Fact]public async Task Get_SecurePageRequiresAnAuthenticatedUser(){    // Arrange     var client = _factory.CreateClient(        new WebApplicationFactoryClientOptions         {            AllowAutoRedirect = false         });     // Act     var response = await client.GetAsync("/ForAdmin");     // Assert     Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);    Assert.StartsWith("http://localhost/Identity/Account/Login",                       response.Headers.Location.OriginalString);}[Authorize(Roles = "Admin")]
[Route("/foradmin")]
public IActionResult ForAdmin()
{
    return Content(string.Format("User: {0}, is admin:  {1}", 
                   User.Identity.Name, User.IsInRole("Admin")));
}

This test turns off redirects for HTTP client. If response is 301 or 302 then HTTP client doesn't go to URL given in location header. It stops so we can explore the response. For anonymous users, this test passes and we can see that browser is redirected to login page.

Introducing Fake User Filter

We can use global action filters to "authenticate" users so ASP.NET Core thinks there's real user authenticated. It's a little bit tricky. We have to turn off default user detection as otherwise HTTP client is redirected to login page. We can use AllowAnonymousFilter for this. This filter will open the door for request to pass authentication but we need another filter right next to it that sets authenticated user.

To introduce authenticated user we need custom action filter shown here.

class FakeUserFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, "123"),
            new Claim(ClaimTypes.Name, "Test user"),
            new Claim(ClaimTypes.Email, test@example.com),
            new Claim(ClaimTypes.Role, "Admin")
        }));
 
        await next();
    }
}

It sets dummy claims for user and ASP.NET Core is okay with it.

Here is the new test that expects authenticated user.

[Fact]
public async Task Get_SecurePageIsAvailableForAuthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
 
    // Act
    var response = await client.GetAsync("/ForAdmin");
    var body = await response.Content.ReadAsStringAsync();
 
    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString());
}

Now we have two integration tests that depend on authenticated user and their needs are conflicting. One test needs anonymous user and the other needs authenticated user.

We have to solve this conflict in integration tests file.

Setting Authenticated User for Integration Tests

The easiest solution I found was to modify tests and make presence of authenticated user configurable. We need to move configuring of factory to separate method so we can set if authenticated user is needed or not. I give here full source of integration tests file.

public class HomeControllerTests : IClassFixture<MediaGalleryFactory<FakeStartup>>
{
    private readonly WebApplicationFactory<FakeStartup> _factory;
 
    public HomeControllerTests(MediaGalleryFactory<FakeStartup> factory)
    {
        _factory = factory;
    }
 
    [Fact]
    public async Task Get_SecurePageRequiresAnAuthenticatedUser()
    {
        // Arrange
        var client = GetFactory().CreateClient(
            new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
 
        // Act
        var response = await client.GetAsync("/ForAdmin");
 
        // Assert
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.StartsWith("http://localhost/Identity/Account/Login",
                           response.Headers.Location.OriginalString);
    }
 
    [Fact]
    public async Task Get_SecurePageIsAvailableForAuthenticatedUser()
    {
        // Arrange
        var client = GetFactory(hasUser: true).CreateClient();
 
        // Act
        var response = await client.GetAsync("/ForAdmin");
        var body = await response.Content.ReadAsStringAsync();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("text/plain; charset=utf-8",response.Content.Headers.ContentType.ToString());
    }
 
    [Theory]
    [InlineData("/")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = GetFactory().CreateClient();
 
        // Act
        var response = await client.GetAsync(url);
 
        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
 
    private WebApplicationFactory<FakeStartup> GetFactory(bool hasUser = false)
    {
        var projectDir = Directory.GetCurrentDirectory();
        var configPath = Path.Combine(projectDir, "appsettings.json");
 
        return _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureAppConfiguration(conf =>
            {
                conf.AddJsonFile(configPath);
            });
 
            builder.UseSolutionRelativeContentRoot("MediaGallery");
 
            builder.ConfigureTestServices(services =>
            {
                services.AddMvc(options =>
                {
                    if (hasUser)
                    {
                        options.Filters.Add(new AllowAnonymousFilter());
                        options.Filters.Add(new FakeUserFilter());
                    }
                })
                .AddApplicationPart(typeof(Startup).Assembly);
            });
        });
    }
}

Tests can now specify if the authenticated user is needed or not through the hasUser parameter of the GetFactory() method.

Best of all, our tests pass now.

There's one thing to take care of — controller actions that use current user identity to query data from database or other services.

Using Real ASP.NET Core Identity Accounts

We need now a way to have test user in ASP.NET Core Identity database. If we have users then usually we have data related to these users. To make sure that user identity is always the same through tests I created simple user settings class.

public static class UserSettings
{
    public const string UserId = "47d90476-8de1-4a71-b0f0-9beaf4d89c98";
    public const string Name = "Test User";
    public const string UserEmail = "user@test.com";
}

Our fake user filter must use the same settings.

class FakeUserFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, UserSettings.UserId),
            new Claim(ClaimTypes.Name, UserSettings.Name),
            new Claim(ClaimTypes.Email, UserSettings.UserEmail), 
            new Claim(ClaimTypes.Role, "Admin")
        }));
 
        await next();
    }
}

To get this user to database we can use the fake startup class. My solution supports empty and previously filled databases.

public class FakeStartup : Startup
{
    public FakeStartup(IConfiguration configuration) : base(configuration)
    {
    }
 
    public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        base.Configure(app, env, loggerFactory);
 
        var serviceScopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
        using (var serviceScope = serviceScopeFactory.CreateScope())
        {
            var dbContext = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
 
            if (dbContext.Database.GetDbConnection().ConnectionString.ToLower().Contains("database.windows.net"))
            {
                throw new Exception("LIVE SETTINGS IN TESTS!");
            }
 
            if (!dbContext.Users.Any(u => u.Id == UserSettings.UserId))
            {
 
                var user = new IdentityUser();
                user.ConcurrencyStamp = DateTime.Now.Ticks.ToString();
                user.Email = UserSettings.UserEmail;
                user.EmailConfirmed = true;
                user.Id = UserSettings.UserId;
                user.NormalizedEmail = user.Email;
                user.NormalizedUserName = user.Email;
                user.PasswordHash = Guid.NewGuid().ToString();
                user.UserName = user.Email;
 
                var role = new IdentityRole();
                role.ConcurrencyStamp = DateTime.Now.Ticks.ToString();
                role.Id = "Admin";
                role.Name = "Admin";
 
                var userRole = new IdentityUserRole<string>();
                userRole.RoleId = "Admin";
                userRole.UserId = user.Id;
 
                dbContext.Users.Add(user);
                dbContext.Roles.Add(role);
                dbContext.UserRoles.Add(userRole);
                dbContext.SaveChanges();
            }
        }
    }
}

Now we can also test controller actions where data is queried based on the current user.

Moving the Web Application Factory to the Base Class

It's time to clean up all the mess. When I look at integration tests file, I don't like the fact that there's a GetFactory() method and the _factory attribute equally accessible to tests. Those who know use the GetFactory() method and novices in the project may accidentally use the  _factory attribute instead. Another this is that we don't want to repeat the GetFactory() method to all the integration test classes we have.

To get rid of these issues I create a base class for integration tests.

public class IntegrationTestBase : IClassFixture<MediaGalleryFactory<FakeStartup>>
{
    private readonly WebApplicationFactory<FakeStartup> _factory;
 
    public IntegrationTestBase(MediaGalleryFactory<FakeStartup> factory)
    {
        _factory = factory;
    }
 
    protected WebApplicationFactory<FakeStartup> GetFactory(bool hasUser = false)
    {
        var projectDir = Directory.GetCurrentDirectory();
        var configPath = Path.Combine(projectDir, "appsettings.json");
 
        return _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureAppConfiguration((context,conf) =>
            {
                conf.AddJsonFile(configPath);
            });
 
            builder.UseSolutionRelativeContentRoot("MediaGallery");
 
            builder.ConfigureTestServices(services =>
            {
                services.AddMvc(options =>
                {
                    if (hasUser)
                    {
                        options.Filters.Add(new AllowAnonymousFilter());
                        options.Filters.Add(new FakeUserFilter());
                    }
                })
                .AddApplicationPart(typeof(Startup).Assembly);
            });
        });
    }
}

Our test class gets also smaller and it doesn't expose the injected _factory attribute anymore.

public class HomeControllerTests : IntegrationTestBase
{
    public HomeControllerTests(MediaGalleryFactory<FakeStartup> factory) : base(factory)
    {
    }
 
    [Fact]
    public async Task Get_SecurePageRequiresAnAuthenticatedUser()
    {
        // Arrange
        var client = GetFactory().CreateClient(
            new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
 
        // Act
        var response = await client.GetAsync("/ForAdmin");
 
        // Assert
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.StartsWith("http://localhost/Identity/Account/Login",
                           response.Headers.Location.OriginalString);
    }
 
    [Fact]
    public async Task Get_SecurePageIsAvailableForAuthenticatedUser()
    {
        // Arrange
        var client = GetFactory(hasUser: true).CreateClient();
 
        // Act
        var response = await client.GetAsync("/ForAdmin");
        var body = await response.Content.ReadAsStringAsync();
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("text/plain; charset=utf-8",response.Content.Headers.ContentType.ToString());
    }
 
    [Theory]
    [InlineData("/")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = GetFactory().CreateClient();
 
        // Act
        var response = await client.GetAsync(url);
 
        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
}

Wrapping Up

There are more than one way to get user contexts to ASP.NET Core Identity integration tests. The method introduced here is kind of hacky but effective. If needed, we can easily extend it for more complex scenarios. To support scenarios where there's user related data in the database we have to test the users that are available and that are also in the database — be it empty or previously filled. By moving factory creating logic to base class we ended up with nice and clean integration test classes.

ASP.NET integration test ASP.NET Core

Published at DZone with permission of Gunnar Peipman, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • GDPR Compliance With .NET: Securing Data the Right Way
  • How to Enhance the Performance of .NET Core Applications for Large Responses
  • Developing Minimal APIs Quickly With Open Source ASP.NET Core
  • Revolutionizing Content Management

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!