Here is my problem. I can't seem to manage to create an integration test that requires an authenticated user. I use Microsoft.AspNetCore.Mvc.Testing
for testing. Here is my test:
As seen the client has the role "Patient" and a UserId. Here are my helpers:
public class TestClaimsProvider
{
public IList<Claim> Claims { get; }
public TestClaimsProvider(IList<Claim> claims)
{
Claims = claims;
}
public TestClaimsProvider()
{
Claims = new List<Claim>();
}
public static TestClaimsProvider WithAdminClaims()
{
var provider = new TestClaimsProvider();
provider.Claims.Add(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()));
provider.Claims.Add(new Claim(ClaimTypes.Name, "Admin user"));
provider.Claims.Add(new Claim(ClaimTypes.Role, "Administrator"));
return provider;
}
public static TestClaimsProvider WithUserClaims()
{
var provider = new TestClaimsProvider();
provider.Claims.Add(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()));
provider.Claims.Add(new Claim(ClaimTypes.Name, "Patient"));
provider.Claims.Add(new Claim(ClaimTypes.Role, "Patient"));
return provider;
}
}
This also:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IList<Claim> _claims;
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock, TestClaimsProvider claimsProvider) : base(options, logger, encoder, clock)
{
_claims = claimsProvider.Claims;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
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);
}
}
And:
public static class WebApplicationFactoryExtensions
{
public static WebApplicationFactory<T> WithAuthentication<T>(this WebApplicationFactory<T> factory, TestClaimsProvider claimsProvider) where T : class
{
return factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", op => { });
services.AddScoped<TestClaimsProvider>(_ => claimsProvider);
});
});
}
public static HttpClient CreateClientWithTestAuth<T>(this WebApplicationFactory<T> factory, TestClaimsProvider claimsProvider) where T : class
{
var client = factory.WithAuthentication(claimsProvider).CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");
return client;
}
These are based on this subject https://gunnarpeipman.com/aspnet-core-integration-tests-users-roles/. Although I am not using the FakeStartup
class that he has pointed in previous threads. Also I have tried the authentication from the docs here https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#customize-the-client-with-withwebhostbuilder but it is the same.
Here is my action method in the controller:
[Authorize(Roles = PatientRoleName)]
public async Task<IActionResult> MakePatientAppointment()
{
var patient = await this.patientService.GetPatientByUserIdAsync(this.User.GetId());
if (string.IsNullOrWhiteSpace(patient.FirstName) ||
string.IsNullOrWhiteSpace(patient.LastName) ||
string.IsNullOrWhiteSpace(patient.Phone))
{
this.TempData["Message"] = PatientProfileIsNotFinishedMsg;
return RedirectToAction("Finish", "Patient", new { area = "" });
}
var viewModel = new PatientAppointmentCreateModel
{
DoctorId = await this.doctorService.GetDoctorId(),
AppointmentCauses = await this.appointmentCauseService.GetAllCauses()
};
return View(viewModel);
}
From debugging the test the response redirect is to /Identity/Login
, so from what I am understanding the user is not logged in. How can I refactor the code to manage to get the user authenticated?
The top-level statements new feature of C# 10, provided the unification of the Startup
and Program
classes into a single Program
class, and because of that (among other things like the new minimal hosting model), the creation of the WebApplicationFactory
has changed, starting with the way to change the visibility of the Program
class to the TestFixture (since it now doesn't contain a namespace due to the top-level statements feature)...
And the documentation has been updated as such, however, the way we registered the AuthenticationHandler
to create an impersonated client for authorization has also changed - but the documentation (at least so far) doesn't address this.
So if we register the AuthenticationHandler
as we did until ASP.net 5, now in ASP.net 6, we get a 401 Unauthorized (because it's not working anymore).
After a lot of research I found the solution:
Now, it's necessary to make explicit the AuthenticationOptions
settings in the WebApplicationFactory
according to the settings you have in your WebApi, in my case:
{
o.DefaultAuthenticateScheme = "Test";
o.DefaultChallengeScheme = "Test";
}