using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Prefab.Catalog.Api.Data; using Prefab.Catalog.Data; using Prefab.Catalog.Data.Services; using Prefab.Catalog.Domain.Entities; using Prefab.Catalog.Domain.Services; using Prefab.Data; using Prefab.Handler; using Shouldly; namespace Prefab.Tests.Unit.Catalog; public sealed class CatalogSeederShould { [Fact] public async Task PopulateCatalogDataInSqliteDatabase() { await using var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(TestContext.Current.CancellationToken); var services = new ServiceCollection(); services.AddSingleton(); services.AddDbContext(options => { options.UseSqlite(connection); }); services.AddDbContextFactory(options => { options.UseSqlite(connection); }, ServiceLifetime.Scoped); services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped(); services.AddSingleton>(_ => NullLogger.Instance); services.AddScoped(); await using var provider = services.BuildServiceProvider(); await using (var scope = provider.CreateAsyncScope()) { var db = scope.ServiceProvider.GetRequiredService(); await db.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken); } await using (var scope = provider.CreateAsyncScope()) { var seeder = scope.ServiceProvider.GetRequiredService(); await seeder.Execute(scope.ServiceProvider, TestContext.Current.CancellationToken); } await using (var scope = provider.CreateAsyncScope()) { var db = scope.ServiceProvider.GetRequiredService(); var allCategories = await db.Categories .Where(c => c.DeletedOn == null) .ToListAsync(TestContext.Current.CancellationToken); allCategories.ShouldNotBeEmpty("Catalog seeder should create categories."); var categories = allCategories .Select(c => c.Slug) .Where(slug => !string.IsNullOrWhiteSpace(slug)) .Select(slug => slug!.Trim()) .ToList(); var expectedSlugs = new HashSet(StringComparer.OrdinalIgnoreCase) { "ceiling-supports", "boxes-and-covers", "prefab-assemblies", "rod-strut-hardware" }; categories.ShouldBeSubsetOf(expectedSlugs, "Seeded catalog should only include the expected root categories."); categories.Count.ShouldBe(expectedSlugs.Count, "Seeded catalog should include all expected root categories."); var products = await db.Products .Where(p => p.DeletedOn == null && p.Kind == ProductKind.Model) .Include(p => p.Variants) .Select(p => new { p.Slug, p.Name, VariantCount = p.Variants.Count }) .ToListAsync(TestContext.Current.CancellationToken); products.ShouldNotBeEmpty("Catalog seeder should introduce product models for listing."); products.Any(p => string.Equals(p.Slug, "ceiling-tbar-box-assembly", StringComparison.OrdinalIgnoreCase)).ShouldBeTrue(); products.Any(p => string.Equals(p.Slug, "fan-hanger-assembly", StringComparison.OrdinalIgnoreCase)).ShouldBeTrue(); products.Any(p => p.VariantCount > 0).ShouldBeTrue("Seed should include at least one model with variants."); } } private sealed class TestCatalogDb : AppDb { public TestCatalogDb(DbContextOptions options, IHandlerContextAccessor accessor) : base(options, accessor) { } protected override void PrefabOnModelCreating(ModelBuilder builder) { base.PrefabOnModelCreating(builder); foreach (var entity in builder.Model.GetEntityTypes()) { var rowVersionProperty = entity.FindProperty("RowVersion"); if (rowVersionProperty is not null) { rowVersionProperty.IsNullable = true; } } } public override Task SaveChangesAsync(CancellationToken cancellationToken = default) { foreach (var entry in ChangeTracker.Entries().Where(e => e.State == EntityState.Added)) { var rowVersion = entry.Properties.FirstOrDefault(p => string.Equals(p.Metadata.Name, "RowVersion", StringComparison.Ordinal)); if (rowVersion is not null && rowVersion.CurrentValue is null) { rowVersion.CurrentValue = Guid.NewGuid().ToByteArray(); } } return base.SaveChangesAsync(cancellationToken); } } private sealed class TestCatalogDbFactory(IDbContextFactory dbFactory) : ICatalogDbContextFactory { public async ValueTask CreateWritableAsync(CancellationToken cancellationToken = default) => await dbFactory.CreateDbContextAsync(cancellationToken); public async ValueTask CreateReadOnlyAsync(CancellationToken cancellationToken = default) => await dbFactory.CreateDbContextAsync(cancellationToken); } }