Files
prefab-page-detail/Prefab.Tests/Unit/Catalog/CatalogSeederShould.cs
2025-10-27 17:39:18 -04:00

147 lines
5.9 KiB
C#

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<IHandlerContextAccessor, HandlerContextAccessor>();
services.AddDbContext<TestCatalogDb>(options =>
{
options.UseSqlite(connection);
});
services.AddDbContextFactory<TestCatalogDb>(options =>
{
options.UseSqlite(connection);
}, ServiceLifetime.Scoped);
services.AddScoped<AppDb>(sp => sp.GetRequiredService<TestCatalogDb>());
services.AddScoped<IPrefabDb>(sp => sp.GetRequiredService<TestCatalogDb>());
services.AddScoped<IModuleDb>(sp => sp.GetRequiredService<TestCatalogDb>());
services.AddScoped<IModuleDbReadOnly>(sp => sp.GetRequiredService<TestCatalogDb>());
services.AddScoped<ICatalogDbContextFactory, TestCatalogDbFactory>();
services.AddScoped<IUniqueChecker, UniqueChecker>();
services.AddSingleton<ILogger<Seeder>>(_ => NullLogger<Seeder>.Instance);
services.AddScoped<Seeder>();
await using var provider = services.BuildServiceProvider();
await using (var scope = provider.CreateAsyncScope())
{
var db = scope.ServiceProvider.GetRequiredService<TestCatalogDb>();
await db.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken);
}
await using (var scope = provider.CreateAsyncScope())
{
var seeder = scope.ServiceProvider.GetRequiredService<Seeder>();
await seeder.Execute(scope.ServiceProvider, TestContext.Current.CancellationToken);
}
await using (var scope = provider.CreateAsyncScope())
{
var db = scope.ServiceProvider.GetRequiredService<TestCatalogDb>();
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<string>(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<TestCatalogDb> 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<int> 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<TestCatalogDb> dbFactory) : ICatalogDbContextFactory
{
public async ValueTask<IModuleDb> CreateWritableAsync(CancellationToken cancellationToken = default) =>
await dbFactory.CreateDbContextAsync(cancellationToken);
public async ValueTask<IModuleDbReadOnly> CreateReadOnlyAsync(CancellationToken cancellationToken = default) =>
await dbFactory.CreateDbContextAsync(cancellationToken);
}
}