Search code examples
c#unit-testingasp.net-identityclaims-based-identityaspnetboilerplate

ClaimsIdentity user is not recognized as logged in during test


I am trying to get unit testing established in a new project that I'm working on. I'm using ASP.NET Boilerplate (.NET Core, Multi Page Application) with pre-built authentication as my starting point. Since Web.Tests is not included in this template, I am attempting to create my own, pulling from what was provided in the ASP.NET Core template. I've been able to establish a Startup class that uses InMemoryDatabase for testing. However, the most basic of tests does not pass. I'm getting stuck on having a test user who is fully authenticated and recognized as being 'logged in'. The code under test is this:

[AbpMvcAuthorize]
public class HomeController : ProjectControllerBase
{
    public ActionResult Index()
    {
        return View();
    }
}

The test is written as such:

[Fact]
public async Task Index_Test()
{            
    // Added as part of suggestions made by 'https://medium.com/@zbartl/authentication-and-asp-net-core-integration-testing-using-testserver-15d47b03045a'
    Client.DefaultRequestHeaders.Add("my-name", "admin");
    Client.DefaultRequestHeaders.Add("my-id", "2");

    // Act
    var response = await GetResponseAsStringAsync(
        GetUrl<HomeController>(nameof(HomeController.Index))
    );

    // Assert
    response.ShouldNotBeNullOrEmpty();
}

The blog referenced here allowed me to provide a ClaimsPrincipal with ClaimsIdentity using a Middleware class. The middleware class looks like this:

public class TestAuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public TestAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemas)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Headers.Keys.Contains("my-name"))
        {
            if (context.Request.Headers["my-name"].First().Equals("admin"))
            {
                ClaimsIdentity claimsIdentity = new ClaimsIdentity(new List<Claim>
                    {
                        new Claim(ClaimTypes.Name, "admin"),
                        new Claim(ClaimTypes.NameIdentifier, context.Request.Headers["my-id"].First()),
                        new Claim(ClaimTypes.Role, "Admin"),
                        new Claim("http://www.aspnetboilerplate.com/identity/claims/tenantId", "1", "int"),
                        new Claim("AspNet.Identity.SecurityStamp", Guid.NewGuid().ToString())

                    },
                    "Identity.Application");

                ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);
                context.User = principal;
                await context.SignInAsync("Identity.Application", principal);
            }
        }

        await _next(context);
    }
}

So, what I get when I run the test is a failed test.

Shouldly.ShouldAssertException : response.StatusCode
    should be
HttpStatusCode.OK
    but was HttpStatusCode.Redirect

I think what is happening is that we are getting stuck in the [AbpMvcAuthorize] functionality and are getting redirected to the login page. If I remove the AbpMvcAuthorize from the controller, then I get a different fail state. I get a null reference error. The View is attempting to render and is failing in a subsequent view model when GetShownLoginName() is called:

public class SideBarUserAreaViewModel
{
    public GetCurrentLoginInformationsOutput LoginInformations { get; set; }

    public bool IsMultiTenancyEnabled { get; set; }

    public string GetShownLoginName()
    {
        var userName = "<span id=\"HeaderCurrentUserName\">" + LoginInformations.User.UserName + "</span>";

        if (!IsMultiTenancyEnabled)
        {
            return userName;
        }

        return LoginInformations.Tenant == null
            ? ".\\" + userName
            : LoginInformations.Tenant.TenancyName + "\\" + userName;
    }
}

I want to be able to test my controller logic, to ensure that changes to views, changes to view models, and changes to services do not inadvertently cause page load errors. Is there a way, outside of creating instances of UserManager, LogInManager and SignInManager in my TestBase class and programmatically logging in the user?


Solution

  • Problem

    1. Your MyProjectWebTestBase inherits AbpAspNetCoreIntegratedTestBase<TStartup>.
    2. AbpAspNetCoreIntegratedTestBase<TStartup> uses TestAbpSession.
    3. TestAbpSession ignores claims.

    Solution

    1. Implement MyTestAbpSession which falls back on ClaimsAbpSession.

      public class MyTestAbpSession : TestAbpSession
      {
          public ClaimsAbpSession ClaimsAbpSession { get; set; }
      
          public MyTestAbpSession(IMultiTenancyConfig multiTenancy,
              IAmbientScopeProvider<SessionOverride> sessionOverrideScopeProvider,
              ITenantResolver tenantResolver)
              : base(multiTenancy, sessionOverrideScopeProvider, tenantResolver)
          {
          }
      
          public override long? UserId
          {
              get => base.UserId ?? ClaimsAbpSession.UserId; // Fallback
              set => base.UserId = value;
          }
      }
      
    2. Register it in PreInitialize method of your MyProjectWebTestModule.

      public override void PreInitialize()
      {
          // ...
      
          IocManager.IocContainer.Register(
              Component.For<IAbpSession, TestAbpSession>()
                  .ImplementedBy<MyTestAbpSession>()
                  .LifestyleSingleton()
                  .IsDefault()
              );
      }