How use JWT Bearer Scheme when working with WebApplicationFactory
Asked Answered
G

1

6

I have a working web API that I recently updated to use JWT auth and, while it is working when I run it normally, I can't seem to get my integration tests to work.

I wanted to start working on integrating a token generation option for my integration tests, but I can't even get them to throw a 401.

When I run any of my preexisting integration tests that work without JWT in the project, I'd expect to get a 401 since I don't have any auth info, but am actually getting a System.InvalidOperationException : Scheme already exists: Bearer error.

I'd assume this is happening because of the way that WebApplicationFactory works by running its ConfigureWebHost method runs after the Startup class' ConfigureServices method and when i put a breakpoint on my jwt service, it does indeed get hit twice, but given that this is how WebApplicationFactory is built, I'm not sure what the recommended option here is. Of note, even when I remove one of the services I still get the error:

var serviceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(JwtBearerHandler));
services.Remove(serviceDescriptor);

My WebApplicationFactory is based on the eshopwebapi factory:


    public class CustomWebApplicationFactory : WebApplicationFactory<StartupTesting>
    {
        // checkpoint for respawning to clear the database when spinning up each time
        private static Checkpoint checkpoint = new Checkpoint
        {
            
        };

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.UseEnvironment("Testing");

            builder.ConfigureServices(async services =>
            {
                // Create a new service provider.
                var provider = services
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                // Add a database context (LabDbContext) using an in-memory 
                // database for testing.
                services.AddDbContext<LabDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                    options.UseInternalServiceProvider(provider);
                });

                // Build the service provider.
                var sp = services.BuildServiceProvider();

                // Create a scope to obtain a reference to the database
                // context (ApplicationDbContext).
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<LabDbContext>();

                    // Ensure the database is created.
                    db.Database.EnsureCreated();

                    try
                    {
                        await checkpoint.Reset(db.Database.GetDbConnection());
                    }
                    catch
                    {
                    }
                }
            }).UseStartup<StartupTesting>();
        }

        public HttpClient GetAnonymousClient()
        {
            return CreateClient();
        }
    }

This is my service registration:

    public static class ServiceRegistration
    {
        public static void AddIdentityInfrastructure(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Authority = configuration["JwtSettings:Authority"];
                    options.Audience = configuration["JwtSettings:Audience"];
                });
            
            services.AddAuthorization(options =>
            {
                options.AddPolicy("CanReadPatients", 
                    policy => policy.RequireClaim("scope", "patients.read"));
                options.AddPolicy("CanAddPatients", 
                    policy => policy.RequireClaim("scope", "patients.add"));
                options.AddPolicy("CanDeletePatients", 
                    policy => policy.RequireClaim("scope", "patients.delete"));
                options.AddPolicy("CanUpdatePatients", 
                    policy => policy.RequireClaim("scope", "patients.update"));
            });
        }
    }

And this is my integration test (that I would expect to currently throw a 401):

public class GetPatientIntegrationTests : IClassFixture<CustomWebApplicationFactory>
    { 
        private readonly CustomWebApplicationFactory _factory;

        public GetPatientIntegrationTests(CustomWebApplicationFactory factory)
        {
            _factory = factory;
        }

        
        [Fact]
        public async Task GetPatients_ReturnsSuccessCodeAndResourceWithAccurateFields()
        {
            var fakePatientOne = new FakePatient { }.Generate();
            var fakePatientTwo = new FakePatient { }.Generate();

            var appFactory = _factory;
            using (var scope = appFactory.Services.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<LabDbContext>();
                context.Database.EnsureCreated();

                context.Patients.AddRange(fakePatientOne, fakePatientTwo);
                context.SaveChanges();
            }

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

            var result = await client.GetAsync("api/Patients")
                .ConfigureAwait(false);
            var responseContent = await result.Content.ReadAsStringAsync()
                .ConfigureAwait(false);
            var response = JsonConvert.DeserializeObject<Response<IEnumerable<PatientDto>>>(responseContent).Data;

            // Assert
            result.StatusCode.Should().Be(200);
            response.Should().ContainEquivalentOf(fakePatientOne, options =>
                options.ExcludingMissingMembers());
            response.Should().ContainEquivalentOf(fakePatientTwo, options =>
                options.ExcludingMissingMembers());
        }
    } 
Goggin answered 7/2, 2021 at 1:48 Comment(0)
J
9

Hey I saw your post when I was looking for the same answer. I solved it by putting the following code in the ConfigureWebHost method of my WebApplicationFactory:

    protected override void ConfigureWebHost(
        IWebHostBuilder builder)
    {
        builder.ConfigureServices(serviceCollection =>
        {

        });
   
        // Overwrite registrations from Startup.cs
        builder.ConfigureTestServices(serviceCollection =>
        {
            var authenticationBuilder = serviceCollection.AddAuthentication();
            authenticationBuilder.Services.Configure<AuthenticationOptions>(o =>
            {
                o.SchemeMap.Clear();
                ((IList<AuthenticationSchemeBuilder>) o.Schemes).Clear();
            });
        });
    }

I know I'm four months late, but I hope you still have any use for it.

Joappa answered 14/6, 2021 at 16:11 Comment(2)
Also I used this library for the fake authentication: github.com/webmotions/fake-authentication-jwtbearer. Maybe also some use to you.Joappa
I did the same as @weretigerGoggin

© 2022 - 2024 — McMap. All rights reserved.