Search code examples
.netasp.net-coreintegration-testing.net-7.0asp.net-core-7.0

ASP.NET Core 7 Integration Test with CustomWebApplicationFactory<TProgram> Client always returns 404 not found


I have my asp.net core 7 web api app running using Program.cs and Startup.cs. I have my Integration Tests already written using CustomWebApplicationFactory<TStartup>. All are working as expected.

Now I have decided to move away from Startup.cs. So I have moved all Startup.cs logics inside Program.cs. My Program.cs looks like,

try
{
    //Read Configuration from appSettings
    var config = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .Build();
    //Initialize Logger
    Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(config)
                .CreateLogger();

    Log.Information($"Starting {typeof(Program).Assembly.FullName}");

    var builder = WebApplication.CreateBuilder();

    builder.Host.UseSerilog();//Uses Serilog instead of default .NET Logger
    builder.WebHost.ConfigureKestrel(options =>
    {
        // Set properties and call methods on options
        options.AddServerHeader = false;
    });

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier);
    //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("role", ClaimTypes.Role);

    builder.Services.AddHttpContextAccessor()
                    .AddApiClients(builder.Configuration)
                    .AddApplicationServices(builder.Configuration)
                    .AddMemoryCache()
                    .AddResponseCompression()
                    .AddApiControllersAndBehavior()
                    .AddApiVersion()
                    .AddApiAuthenticationAndAuthorization(builder.Configuration)
                    .AddSwagger(builder.Configuration)
                    .AddApplicationServices()
                    .AddCors(options =>
                    {
                        options.AddPolicy("AppClients", policyBuilder => policyBuilder.WithOrigins(builder.Configuration.GetValue<string>("WebClient"))
                                                                                        .AllowAnyHeader()
                                                                                        .AllowAnyMethod());
                    })
                    //.AddHttpLogging(options =>
                    //{
                    //    options.LoggingFields = HttpLoggingFields.All;
                    //})
                    .AddFeatureManagement()
                    .UseDisabledFeaturesHandler(new DisabledFeatureHandler());

    var consoleLogging = new ConsoleLogging(builder.Configuration.GetValue<bool>("EnableConsoleLogging"));
    builder.Services.AddSingleton(consoleLogging);

    var commandsConnectionString = new CommandConnectionString(builder.Configuration.GetConnectionString("CommandsConnectionString"));
    builder.Services.AddSingleton(commandsConnectionString);

    var queriesConnectionString = new QueryConnectionString(builder.Configuration.GetConnectionString("QueriesConnectionString"));
    builder.Services.AddSingleton(queriesConnectionString);

    if (builder.Environment.IsDevelopment())
    {
        //builder.Services.AddScoped<BaseReadContext, AppInMemoryReadContext>();
        //builder.Services.AddScoped<BaseContext, AppInMemoryContext>();
        builder.Services.AddScoped<BaseReadContext, AppSqlServerReadContext>();
        builder.Services.AddScoped<BaseContext, AppSqlServerContext>();

        builder.Services.AddMiniProfilerServices();
    }
    else
    {
        builder.Services.AddScoped<BaseReadContext, AppSqlServerReadContext>();
        builder.Services.AddScoped<BaseContext, AppSqlServerContext>();
    }

    var app = builder.Build();

    app.UseMiddleware<ExceptionHandler>()
           //.UseHttpLogging()
           .UseSecurityHeaders(SecurityHeadersDefinitions.GetHeaderPolicyCollection(builder.Environment.IsDevelopment()))
           .UseHttpsRedirection()
           .UseResponseCompression();

    if (builder.Environment.IsDevelopment())
    {
        app.UseMiniProfiler()
           .UseSwagger()
           .UseSwaggerUI(options =>
           {
               foreach (var description in app.Services.GetRequiredService<IApiVersionDescriptionProvider>().ApiVersionDescriptions)
               {
                   options.SwaggerEndpoint(
                           $"swagger/AppOpenAPISpecification{description.GroupName}/swagger.json",
                           $"App API - {description.GroupName.ToUpperInvariant()}");
               }

               options.OAuthClientId("appswaggerclient");
               options.OAuthAppName("App API");
               options.OAuthUsePkce();

               options.RoutePrefix = string.Empty;
               options.DefaultModelExpandDepth(2);
               options.DefaultModelRendering(ModelRendering.Model);
               options.DocExpansion(DocExpansion.None);
               options.DisplayRequestDuration();
               options.EnableValidator();
               options.EnableFilter();
               options.EnableDeepLinking();
               options.DisplayOperationId();
           });
    }

    app.UseRouting()
       .UseCors("AppClients")
       .UseAuthentication()
       .UseAuthorization()
       .UseRequestLocalization(options =>
       {
           var supportedCultures = new[] { "en", "en-IN", "en-US" };

           options.SetDefaultCulture("en-IN");
           options.AddSupportedCultures(supportedCultures);
           options.AddSupportedUICultures(supportedCultures);
           options.ApplyCurrentCultureToResponseHeaders = true;
       })
       .UseEndpoints(endpoints =>
       {
           endpoints.MapControllers();
       });

    await app.RunAsync();
}
catch (Exception ex)
{
    Log.Fatal(ex, "The Application failed to start.");
}
finally
{
    Log.CloseAndFlush();
}

/// <summary>
/// Added to Make FunctionalTest Compile
/// </summary>
public partial class Program { }

Here is my CustomWebApplicationFactory<TProgram>,

public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        var projectDir = Directory.GetCurrentDirectory();

        builder.ConfigureAppConfiguration((context, conf) =>
        {
            conf.AddJsonFile(Path.Combine(projectDir, "appsettings.Test.json"));
        });

        builder.UseEnvironment("Testing");

        builder.ConfigureTestServices(async services =>
        {
            services.AddAuthentication("Test")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });

            services.AddScoped(_ => AuthClaimsProvider.WithMasterClaims());

            var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(BaseContext));

            if (descriptor != null)
            {
                services.Remove(descriptor);
            }

            descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(BaseReadContext));

            if (descriptor != null)
            {
                services.Remove(descriptor);
            }

            descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(ITenantService));

            if (descriptor != null)
            {
                services.Remove(descriptor);
                services.AddTransient<ITenantService, TestTenantService>();
            }

            var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" };
            var connection = new SqliteConnection(connectionStringBuilder.ToString());

            var dbContextOptions = new DbContextOptionsBuilder<AppSqliteInMemoryContext>()
                                    .UseSqlite(connection)
                                    .Options;

            services.AddScoped<BaseContext>(options => new AppSqliteInMemoryContext(dbContextOptions));

            var dbContextReadOptions = new DbContextOptionsBuilder<AppSqliteInMemoryReadContext>()
                                    .UseSqlite(connection)
                                    .Options;

            services.AddScoped<BaseReadContext>(options => new AppSqliteInMemoryReadContext(
                dbContextReadOptions, options.GetRequiredService<ITenantService>()));

            await connection.CloseAsync();

            var sp = services.BuildServiceProvider();

            using var scope = sp.CreateScope();
            var scopedServices = scope.ServiceProvider;
            var db = scopedServices.GetRequiredService<BaseContext>();
            var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<Program>>>();

            try
            {
                await db.Database.OpenConnectionAsync();
                await db.Database.EnsureCreatedAsync();
                await DatabaseHelper.InitialiseDbForTests(db);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, $"An error occurred seeding the database with test data. Error: {ex.Message}");
                throw;
            }
        });
    }

}

Here is my Integration Test,

public class GetByIdTests : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public GetByIdTests(CustomWebApplicationFactory<Program> factory)
    {
        //factory.ClientOptions.BaseAddress = new Uri("https://localhost:44367");
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            BaseAddress = new Uri("https://localhost:44367"),
            AllowAutoRedirect = false
        });
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");
        _client.DefaultRequestHeaders.Add("x-api-version", "1.0");
    }

    [Fact]
    public async Task GetById_ReturnsExpectedResponse_ForMasterUser()
    {
        var id = Guid.Parse("6B4DFE8A-2FCB-4716-94ED-4D63BF9351C6");
        using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/branches/{id}");
        var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
    }
}

I have followed all the steps mentioned in official docs. However when I run the test I keep getting 404 not found.

Here is the error screen shot,

404 Error in Integration Tests

Please can you assist me on what I'm doing wrong?


Solution

  • This movement from Startup.cs to Program.cs was in my backlog for long time and every time I attempted I ended up with 404 NotFound in the tests.

    Finally I figured out. Here is how.

    I was comparing my Program.cs line by line with eshoponweb Program.cs and noticed that I was missing args in my CreateBuilder().

    In my Program.cs, I just changed from this

    var builder = WebApplication.CreateBuilder();

    to

    var builder = WebApplication.CreateBuilder(args); // added args here

    and it started working.

    The reason my Program.cs was missing args is that our sonar qube scanner was highlighting a security risk for args and so we removed that when our project was targeting ASP.NET Core 3.1 and now that was making our test to fail when the project is targeting ASP.NET Core 6 or above.