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.PostJsonAsync("/api/auth/register", new
{
userName,
email = $"{userName}@example.test",
password = "TestPassword123",
});
response.EnsureSuccessStatusCode();
var auth = await response.Content.ReadJsonAsync();
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);
}