using System.Security.Cryptography; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using SIGCM2.Application.Abstractions.Security; using SIGCM2.Infrastructure.Persistence; using SIGCM2.Infrastructure.Security; using Xunit; namespace SIGCM2.TestSupport; /// /// WebApplicationFactory for integration tests against SIGCM2.Api. /// Uses SIGCM2_Test_Api database (isolated from Application.Tests which uses SIGCM2_Test_App). /// public sealed class TestWebAppFactory : WebApplicationFactory, IAsyncLifetime { private const string TestConnectionString = TestConnectionStrings.ApiTestDb; // Resolved once — absolute paths independent of working directory private static readonly string RepoRoot = ResolveRepoRoot(); private static readonly string PrivateKeyPath = Path.Combine(RepoRoot, "src", "api", "SIGCM2.Api", "keys", "private.pem"); private static readonly string PublicKeyPath = Path.Combine(RepoRoot, "src", "api", "SIGCM2.Api", "keys", "public.pem"); private readonly SqlTestFixture _dbFixture = new(TestConnectionString); protected override void ConfigureWebHost(IWebHostBuilder builder) { // Step 1: Override configuration BEFORE services are built builder.ConfigureAppConfiguration((ctx, config) => { // Clear all existing sources and rebuild with test values // This ensures our paths win over appsettings.json config.AddInMemoryCollection(new Dictionary { ["ConnectionStrings:SqlServer"] = TestConnectionString, ["Jwt:Issuer"] = "sigcm2.api", ["Jwt:Audience"] = "sigcm2.web", ["Jwt:AccessTokenMinutes"] = "60", ["Jwt:RefreshTokenDays"] = "7", ["Jwt:PrivateKeyPath"] = PrivateKeyPath, ["Jwt:PublicKeyPath"] = PublicKeyPath, ["Jwt:PrivateKey"] = null, ["Jwt:PublicKey"] = null, ["Cors:AllowedOrigins:0"] = "http://localhost:5173", ["Serilog:MinimumLevel:Default"] = "Warning", }); }); builder.UseEnvironment("Testing"); // Step 2: Replace SqlConnectionFactory singleton with the correct test connection. // ConfigureAppConfiguration alone is insufficient because WebApplication.CreateBuilder // evaluates configuration for singleton construction before overrides apply. // ConfigureTestServices runs AFTER all services are registered, so it wins. builder.ConfigureTestServices(services => { // Remove the existing SqlConnectionFactory singleton registered by AddInfrastructure var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(SqlConnectionFactory)); if (descriptor is not null) services.Remove(descriptor); // Re-register with the test connection string services.AddSingleton(new SqlConnectionFactory(TestConnectionString)); }); } public async Task InitializeAsync() { await _dbFixture.InitializeAsync(); } public new async Task DisposeAsync() { await _dbFixture.DisposeAsync(); await base.DisposeAsync(); } private static string ResolveRepoRoot() { // Walk up from AppContext.BaseDirectory looking for SIGCM2.slnx var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null) { if (dir.GetFiles("SIGCM2.slnx").Length > 0) return dir.FullName; dir = dir.Parent; } // Walk up from assembly location var assemblyLocation = typeof(TestWebAppFactory).Assembly.Location; dir = new DirectoryInfo(Path.GetDirectoryName(assemblyLocation)!); while (dir is not null) { if (dir.GetFiles("SIGCM2.slnx").Length > 0) return dir.FullName; dir = dir.Parent; } // Known absolute path (last resort for this machine) const string knownPath = @"E:\SIG-CM2.0"; if (Directory.Exists(knownPath) && File.Exists(Path.Combine(knownPath, "SIGCM2.slnx"))) return knownPath; throw new InvalidOperationException( $"Could not find repo root containing SIGCM2.slnx. " + $"AppContext.BaseDirectory: {AppContext.BaseDirectory}"); } }