using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Components.Authorization; using Prefab.Handler; using Prefab.Web.Data; namespace Prefab.Tests.Infrastructure; /// /// Spins up Aspire infrastructure plus the Prefab web and catalog apps in-process. /// public class PrefabCompositeFixture : IAsyncLifetime { public const int DebugPrefabPort = 5173; public const int DebugCatalogPort = 5174; private readonly PrefabHarnessOptions _options; private readonly TestAuthenticationStateProvider _authenticationProvider = new(); protected PrefabCompositeFixture(PrefabHarnessOptions? options = null) { _options = options ?? ResolveDefaultOptions(); Infra = new AspireInfraHost(_options, "prefab-db"); } public AspireInfraHost Infra { get; } public KestrelInProcApp PrefabWeb { get; private set; } = default!; public KestrelInProcApp? CatalogApi { get; private set; } public TestAuthenticationStateProvider AuthenticationStateProvider => _authenticationProvider; public Uri PrefabBaseAddress => PrefabWeb.HttpAddress; public Uri CatalogBaseAddress => CatalogApi?.HttpAddress ?? throw new InvalidOperationException("Catalog API has not been started."); protected virtual bool ShouldStartCatalog => true; public async ValueTask InitializeAsync() { await Infra.InitializeAsync(); await EnsureDatabaseInitialized(CancellationToken.None); var prefabProject = Infra.GetProject("prefab-web"); var useFixedPorts = _options.Mode == RunMode.DebugPersistent && _options.DisablePortRandomization; Uri? catalogAddress = null; if (ShouldStartCatalog) { var catalogProject = Infra.GetProject("prefab-catalog"); var catalogPort = useFixedPorts ? DebugCatalogPort : (int?)null; CatalogApi = new KestrelInProcApp( Infra, catalogProject, catalogPort, overrides: null, configureServices: ConfigureTestAuthentication); await CatalogApi.InitializeAsync(); catalogAddress = CatalogApi.HttpAddress; } else { CatalogApi = null; } var overrides = GetWebOverrides(catalogAddress); var prefabPort = useFixedPorts ? DebugPrefabPort : (int?)null; PrefabWeb = new KestrelInProcApp( Infra, prefabProject, prefabPort, overrides, ConfigureTestAuthentication); await PrefabWeb.InitializeAsync(); } public async ValueTask DisposeAsync() { await Infra.DisposeAsync(); } public HttpClient CreateHttpClientForWeb() => PrefabWeb.CreateClient(); public IServiceScope CreateWebScope() => PrefabWeb.AppServices.CreateScope(); public async Task GetConnectionStringAsync(string resourceName, CancellationToken cancellationToken = default) => await Infra.GetConnectionStringAsync(resourceName, cancellationToken); public async Task SeedAsync(Func seed, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(seed); var connectionString = await GetConnectionStringAsync("prefab-db", cancellationToken); var optionsBuilder = new DbContextOptionsBuilder().UseSqlServer(connectionString); await using var db = new AppDb(optionsBuilder.Options, new HandlerContextAccessor()); await db.Database.MigrateAsync(cancellationToken); await seed(db, cancellationToken); } protected virtual async ValueTask EnsureDatabaseInitialized(CancellationToken cancellationToken) { var connectionString = await GetConnectionStringAsync("prefab-db", cancellationToken); var optionsBuilder = new DbContextOptionsBuilder().UseSqlServer(connectionString); await using var db = new AppDb(optionsBuilder.Options, new HandlerContextAccessor()); await db.Database.MigrateAsync(cancellationToken); } protected virtual IDictionary GetWebOverrides(Uri? catalogAddress) => new Dictionary { ["Prefab__Catalog__Client__Transport"] = "Http", ["Prefab__Catalog__Client__BaseAddress"] = catalogAddress?.ToString().TrimEnd('/') ?? string.Empty }; private void ConfigureTestAuthentication(IServiceCollection services) { var descriptors = services.Where(d => d.ServiceType == typeof(AuthenticationStateProvider)).ToList(); foreach (var descriptor in descriptors) { services.Remove(descriptor); } services.AddSingleton(_authenticationProvider); services.AddSingleton(_ => _authenticationProvider); } private static PrefabHarnessOptions ResolveDefaultOptions() { var modeEnv = Environment.GetEnvironmentVariable("PREFAB_TEST_MODE")?.Trim().ToLowerInvariant(); var mode = modeEnv switch { "debug" => RunMode.DebugPersistent, "ephemeral" => RunMode.EphemeralIsolated, _ => RunMode.DebugPersistent }; return mode switch { RunMode.EphemeralIsolated => new PrefabHarnessOptions( RunMode.EphemeralIsolated, EnableAspireDashboard: false, DisablePortRandomization: false), _ => new PrefabHarnessOptions( RunMode.DebugPersistent, EnableAspireDashboard: true, DisablePortRandomization: true) }; } } public sealed class PrefabCompositeFixture_Debug : PrefabCompositeFixture { public PrefabCompositeFixture_Debug() : base(new PrefabHarnessOptions( RunMode.DebugPersistent, EnableAspireDashboard: true, DisablePortRandomization: true)) { } } public sealed class PrefabCompositeFixture_Ephemeral : PrefabCompositeFixture { public PrefabCompositeFixture_Ephemeral() : base(new PrefabHarnessOptions( RunMode.EphemeralIsolated, EnableAspireDashboard: false, DisablePortRandomization: false)) { } } public sealed class PrefabCompositeFixture_InProcess : PrefabCompositeFixture { public PrefabCompositeFixture_InProcess() : base(new PrefabHarnessOptions( RunMode.DebugPersistent, EnableAspireDashboard: true, DisablePortRandomization: true)) { } protected override bool ShouldStartCatalog => false; protected override IDictionary GetWebOverrides(Uri? catalogAddress) => new Dictionary { ["Prefab__Catalog__Client__Transport"] = "InProcess" }; } [CollectionDefinition("Prefab.Debug")] public sealed class PrefabDebugCollection : ICollectionFixture { } [CollectionDefinition("Prefab.Ephemeral")] public sealed class PrefabEphemeralCollection : ICollectionFixture { } [CollectionDefinition("Prefab.InProcess")] public sealed class PrefabInProcessCollection : ICollectionFixture { }