using System.Net.Http.Headers; using System.Net.Http.Json; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Hosting; namespace LudosData.Api.Tests; /// /// Boots the real application against a throwaway SQLite file and upload folder. /// /// Every collaborator the API uses in production is exercised here — the same /// pipeline, the same Identity configuration, the same JWT validation. Only the /// storage locations and the signing key are swapped, so a test that passes says /// something about the shipped application rather than about a stand-in. /// public class LudosApiFactory : WebApplicationFactory, IAsyncLifetime { private readonly string _root = Path.Combine( Path.GetTempPath(), "ludos-tests", Guid.NewGuid().ToString("N")); public string SigningKey { get; } = "test-signing-key-of-at-least-32-characters-long"; protected override void ConfigureWebHost(IWebHostBuilder builder) { Directory.CreateDirectory(_root); builder.UseEnvironment(Environments.Development); builder.UseSetting("ConnectionStrings:Default", $"Data Source={Path.Combine(_root, "test.db")}"); builder.UseSetting("Uploads:RootPath", Path.Combine(_root, "uploads")); builder.UseSetting("DataProtection:KeysPath", Path.Combine(_root, "keys")); builder.UseSetting("Jwt:Key", SigningKey); builder.UseSetting("Jwt:Issuer", "LudosData"); builder.UseSetting("Jwt:Audience", "LudosData"); // The seeder would otherwise create a user and import 105 games, which // would make "does this user see only their own rows" untestable. builder.UseSetting("Seed:Enabled", "false"); } /// Registers a fresh user and returns a client authenticated as them. public async Task CreateUserClientAsync(string userName) { var client = CreateClient(); var response = await client.PostAsJsonAsync("/api/auth/register", new { userName, email = $"{userName}@example.test", password = "TestPassword123", }); response.EnsureSuccessStatusCode(); var auth = await response.Content.ReadFromJsonAsync(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth!.Token); return client; } public Task InitializeAsync() => Task.CompletedTask; public new async Task DisposeAsync() { await base.DisposeAsync(); try { if (Directory.Exists(_root)) { Directory.Delete(_root, recursive: true); } } catch (IOException) { // A locked SQLite handle on a temp file is not worth failing a run over. } } public record AuthPayload(string Token, DateTimeOffset ExpiresAt, UserPayload User); public record UserPayload(string Id, string UserName, string? Email); }