using System.Net.Http.Headers; using DodoSSH.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Testcontainers.PostgreSql; using Xunit; namespace DodoSSH.Api.Tests; /// /// Hosts the API in-process against a real PostgreSQL container and a stubbed identity provider. /// /// /// One container and one host per assembly. Tests therefore use distinct users and vaults rather /// than assuming an empty database. /// public sealed class ApiFixture : WebApplicationFactory, IAsyncLifetime { private readonly PostgreSqlContainer container = new PostgreSqlBuilder("postgres:18-alpine") .WithDatabase("dodossh") .WithUsername("postgres") .WithPassword("test") .Build(); /// The stubbed identity provider. public StubIdentityProvider IdentityProvider { get; } = new(); /// public async ValueTask InitializeAsync() { await container.StartAsync(); await using var scope = Services.CreateAsyncScope(); var database = scope.ServiceProvider.GetRequiredService(); await database.Database.MigrateAsync(); } /// public override async ValueTask DisposeAsync() { await base.DisposeAsync(); await container.DisposeAsync(); IdentityProvider.Dispose(); } /// protected override void ConfigureWebHost(IWebHostBuilder builder) { ArgumentNullException.ThrowIfNull(builder); builder.UseEnvironment("Testing"); // Environment variables rather than ConfigureAppConfiguration. Under minimal hosting the // application's own configuration sources are added inside Program.cs after this callback // runs, so an appsettings.json value would win over anything registered here. Program.cs // adds AddEnvironmentVariables("DODOSSH_") last, which makes these authoritative. var settings = new Dictionary(StringComparer.Ordinal) { ["DODOSSH_ConnectionStrings__Postgres"] = container.GetConnectionString(), ["DODOSSH_Server__PublicBaseUrl"] = "http://localhost", ["DODOSSH_Oidc__Authority"] = IdentityProvider.Authority, ["DODOSSH_Oidc__Audience"] = StubIdentityProvider.Audience, ["DODOSSH_Oidc__ClientId"] = "dodossh-desktop", // The stub serves plaintext HTTP on a loopback port. ["DODOSSH_Oidc__RequireHttpsMetadata"] = "false", ["DODOSSH_Relay__Enabled"] = "false", // Fixed so cursors stay valid for the lifetime of the test host. ["DODOSSH_Sync__CursorSigningKey"] = Convert.ToBase64String(new byte[32]), }; foreach (var (name, value) in settings) { Environment.SetEnvironmentVariable(name, value); } } /// Creates a client carrying a valid token for the given subject. public HttpClient CreateClientFor(string subject, string? email = null) { var client = CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", IdentityProvider.MintToken(subject, email)); return client; } /// Creates a client carrying the supplied raw token. public HttpClient CreateClientWithToken(string token) { var client = CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } /// Opens a database scope for arranging state and asserting on it. public AsyncServiceScope CreateScope() => Services.CreateAsyncScope(); } /// Shares one host and container across every test class in the assembly. [CollectionDefinition(Name)] public sealed class ApiCollection : ICollectionFixture { /// Collection name. public const string Name = "api"; }