Files
prefab-page-detail/Prefab.Tests/Infrastructure/PrefabCompositeFixture.cs
2025-10-27 17:39:18 -04:00

207 lines
7.2 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Components.Authorization;
using Prefab.Handler;
using Prefab.Web.Data;
namespace Prefab.Tests.Infrastructure;
/// <summary>
/// Spins up Aspire infrastructure plus the Prefab web and catalog apps in-process.
/// </summary>
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<Prefab.Web.Module> PrefabWeb { get; private set; } = default!;
public KestrelInProcApp<Prefab.Catalog.Api.Module>? 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<Prefab.Catalog.Api.Module>(
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<Prefab.Web.Module>(
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<string> GetConnectionStringAsync(string resourceName, CancellationToken cancellationToken = default) =>
await Infra.GetConnectionStringAsync(resourceName, cancellationToken);
public async Task SeedAsync(Func<AppDb, CancellationToken, Task> seed, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(seed);
var connectionString = await GetConnectionStringAsync("prefab-db", cancellationToken);
var optionsBuilder = new DbContextOptionsBuilder<AppDb>().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<AppDb>().UseSqlServer(connectionString);
await using var db = new AppDb(optionsBuilder.Options, new HandlerContextAccessor());
await db.Database.MigrateAsync(cancellationToken);
}
protected virtual IDictionary<string, string?> GetWebOverrides(Uri? catalogAddress) =>
new Dictionary<string, string?>
{
["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<AuthenticationStateProvider>(_ => _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<string, string?> GetWebOverrides(Uri? catalogAddress) =>
new Dictionary<string, string?>
{
["Prefab__Catalog__Client__Transport"] = "InProcess"
};
}
[CollectionDefinition("Prefab.Debug")]
public sealed class PrefabDebugCollection : ICollectionFixture<PrefabCompositeFixture_Debug>
{
}
[CollectionDefinition("Prefab.Ephemeral")]
public sealed class PrefabEphemeralCollection : ICollectionFixture<PrefabCompositeFixture_Ephemeral>
{
}
[CollectionDefinition("Prefab.InProcess")]
public sealed class PrefabInProcessCollection : ICollectionFixture<PrefabCompositeFixture_InProcess>
{
}