using Microsoft.AspNetCore.Components.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection.Extensions; 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: ConfigureCatalogServices); 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, ConfigureWebServices); 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 }; protected 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); } protected virtual void ConfigureCatalogServices(IServiceCollection services) => ConfigureTestAuthentication(services); protected virtual void ConfigureWebServices(IServiceCollection services) => ConfigureTestAuthentication(services); 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" }; } public sealed class PrefabCompositeFixture_BffProduct : PrefabCompositeFixture { private Prefab.Shared.Catalog.Products.IProductClient? _productClient; private readonly Prefab.Shared.Catalog.Categories.ICategoryClient _categoryClient = new NullCategoryClient(); private readonly Prefab.Shared.Catalog.Products.IPriceQuoteClient _priceQuoteClient = new NullPriceQuoteClient(); public PrefabCompositeFixture_BffProduct() : base(new PrefabHarnessOptions( RunMode.DebugPersistent, EnableAspireDashboard: true, DisablePortRandomization: true)) { } public void UseProductClient(Prefab.Shared.Catalog.Products.IProductClient client) { _productClient = client ?? throw new ArgumentNullException(nameof(client)); } protected override bool ShouldStartCatalog => false; protected override IDictionary GetWebOverrides(Uri? catalogAddress) => new Dictionary { ["Prefab__Catalog__Client__Transport"] = "InProcess" }; protected override void ConfigureWebServices(IServiceCollection services) { base.ConfigureWebServices(services); services.RemoveAll(); services.RemoveAll(); services.RemoveAll(); services.RemoveAll(); services.AddScoped(_ => _categoryClient); services.AddScoped(_ => _priceQuoteClient); services.AddScoped(_ => _productClient ?? throw new InvalidOperationException("Test product client has not been configured.")); services.AddScoped(sp => new Prefab.Catalog.ModuleClient( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService())); } private sealed class NullCategoryClient : Prefab.Shared.Catalog.Categories.ICategoryClient { public Task GetCategories( CancellationToken cancellationToken) => throw new NotSupportedException("Category retrieval is not configured for this test."); public Task CreateCategory( Prefab.Shared.Catalog.Categories.CreateCategory.Request request, CancellationToken cancellationToken) => throw new NotSupportedException("Category creation is not configured for this test."); } private sealed class NullPriceQuoteClient : Prefab.Shared.Catalog.Products.IPriceQuoteClient { public Task Quote( Prefab.Shared.Catalog.Products.QuotePrice.Request request, CancellationToken cancellationToken) => throw new NotSupportedException("Price quote client is not configured for this test."); public Task QuotePrice( Prefab.Shared.Catalog.Products.QuotePrice.Request request, CancellationToken cancellationToken) => throw new NotSupportedException("Price quote client is not configured for this test."); } } [CollectionDefinition("Prefab.Debug")] public sealed class PrefabDebugCollection : ICollectionFixture { } [CollectionDefinition("Prefab.Ephemeral")] public sealed class PrefabEphemeralCollection : ICollectionFixture { } [CollectionDefinition("Prefab.InProcess")] public sealed class PrefabInProcessCollection : ICollectionFixture { } [CollectionDefinition("Prefab.BffProduct")] public sealed class PrefabBffProductCollection : ICollectionFixture { }