using Microsoft.EntityFrameworkCore; using Prefab.Handler; using Prefab.Web.Data; namespace Prefab.Tests.Infrastructure.Aspire; /// /// Provides a reusable Aspire application host for integration tests. /// public class AppHostFixture : IAsyncLifetime { private static readonly TimeSpan DefaultStartupTimeout = TimeSpan.FromMinutes(2); private AppHost? _host; /// /// Gets a value indicating whether the Aspire host has been started. /// public bool IsRunning => _host is not null; /// /// Returns the active . /// /// Thrown if the host has not been started. private AppHost Host => _host ?? throw new InvalidOperationException("The Aspire host has not been started."); /// public async ValueTask InitializeAsync() { await StartHostAsync(null, CancellationToken.None); } /// public async ValueTask DisposeAsync() { if (_host is null) { return; } await _host.DisposeAsync(); _host = null; } /// /// Restarts the Aspire host with the supplied environment overrides. /// /// Environment variables to apply when starting the host. /// Cancellation token for the restart operation. public async Task ReinitializeAsync( IReadOnlyDictionary? environmentOverrides, CancellationToken cancellationToken) { await DisposeAsync(); await StartHostAsync(environmentOverrides, cancellationToken); } /// /// Creates an for the specified resource. /// public HttpClient CreateHttpClient(string resourceName) => Host.CreateHttpClient(resourceName); /// /// Retrieves a connection string for the specified resource. /// public Task GetConnectionStringAsync(string resourceName, CancellationToken cancellationToken) => Host.GetSqlConnectionStringAsync(resourceName, cancellationToken); /// /// Provides an configured for write operations so tests can seed data. /// /// Delegate that performs the seed work. /// Token used to cancel the operation. public async Task SeedTestWith(Func seedWork, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(seedWork); var connectionString = await GetConnectionStringAsync("prefab-db", cancellationToken); var options = new DbContextOptionsBuilder() .UseSqlServer(connectionString) .Options; await using var db = new AppDb(options, new HandlerContextAccessor()); await db.Database.MigrateAsync(cancellationToken); await seedWork(db, cancellationToken); } /// /// Starts the Aspire host with the specified environment overrides. /// private async Task StartHostAsync(IReadOnlyDictionary? environmentOverrides, CancellationToken cancellationToken) { if (_host is not null) { throw new InvalidOperationException("The Aspire host has already been started."); } using var cts = cancellationToken.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : new CancellationTokenSource(DefaultStartupTimeout); cts.CancelAfter(DefaultStartupTimeout); var overrides = environmentOverrides ?? BuildEnvironmentOverrides(); _host = await AppHost.StartAsync( overrides is { Count: > 0 } ? overrides : null, cts.Token); } /// /// Provides the default environment overrides used when starting the host. /// protected virtual IReadOnlyDictionary BuildEnvironmentOverrides() => new Dictionary(StringComparer.OrdinalIgnoreCase); }