using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Application.Content; using Tiku.Domain.Content; using Tiku.Domain.Learning; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; using Tiku.IntegrationTests.Api; namespace Tiku.IntegrationTests.ContentV2; public sealed class PlatformContentPackageProjectionTests { [Fact] public async Task Cell_projection_is_idempotent_and_ignores_out_of_order_packages() { await using var factory = new ApiTestFactory(); var fixture = await SeedPackagesAsync(factory); using var scope = factory.CreateSystemScope("Apply platform package events to a Cell"); var service = scope.ServiceProvider.GetRequiredService(); var applied = await service.ApplyAsync(new ApplyPlatformContentPackageCommand( fixture.OwnerTenantId, fixture.NewPackageVersionId, fixture.PackageCode, "learning-cell-a", 2, fixture.NewHash)); var duplicate = await service.ApplyAsync(new ApplyPlatformContentPackageCommand( fixture.OwnerTenantId, fixture.NewPackageVersionId, fixture.PackageCode, "learning-cell-a", 2, fixture.NewHash)); var old = await service.ApplyAsync(new ApplyPlatformContentPackageCommand( fixture.OwnerTenantId, fixture.OldPackageVersionId, fixture.PackageCode, "learning-cell-a", 1, fixture.OldHash)); Assert.Equal(CellPackageApplyStatus.Applied, applied.Status); Assert.Equal(CellPackageApplyStatus.Duplicate, duplicate.Status); Assert.Equal(CellPackageApplyStatus.IgnoredOutOfOrder, old.Status); var conflict = await Assert.ThrowsAsync(() => service.ApplyAsync( new ApplyPlatformContentPackageCommand( fixture.OwnerTenantId, fixture.OldPackageVersionId, fixture.PackageCode, "learning-cell-a", 2, fixture.OldHash))); Assert.Equal("platform_package_sequence_conflict", conflict.Code); var db = scope.ServiceProvider.GetRequiredService(); var projection = await db.PlatformContentPackageCellProjections.AsNoTracking().SingleAsync(); Assert.Equal(fixture.NewPackageVersionId, projection.PlatformContentPackageVersionId); Assert.Equal(2, projection.EventSequence); Assert.Equal(fixture.NewHash, projection.ContentHash); Assert.Equal(CellProjectionStatus.Ready, projection.Status); } private static async Task SeedPackagesAsync(ApiTestFactory factory) { var ownerTenantId = Guid.NewGuid(); var businessLineId = Guid.NewGuid(); var curriculumId = Guid.NewGuid(); var curriculumVersionId = Guid.NewGuid(); var oldReleaseId = Guid.NewGuid(); var newReleaseId = Guid.NewGuid(); var oldPackageVersionId = Guid.NewGuid(); var newPackageVersionId = Guid.NewGuid(); var packageCode = "national-math"; var oldHash = new string('1', 64); var newHash = new string('2', 64); var now = DateTimeOffset.UtcNow; await factory.SeedAsync( new Tenant { Id = ownerTenantId, Slug = $"platform-{ownerTenantId:N}", Name = "Platform content owner", Mode = TenantMode.PlatformOwned }, new BusinessLine { Id = businessLineId, Code = $"business-{businessLineId:N}", Name = "Generic exam business" }, new Curriculum { Id = curriculumId, TenantId = ownerTenantId, BusinessLineId = businessLineId, Code = $"curriculum-{curriculumId:N}", Name = "Platform curriculum" }); await factory.SeedAsync( new CurriculumVersion { Id = curriculumVersionId, TenantId = ownerTenantId, CurriculumId = curriculumId, Name = "Platform curriculum V1", Status = ContentDefinitionStatus.Published, PublishedAt = now }); await factory.SeedAsync( new ContentRelease { Id = oldReleaseId, TenantId = ownerTenantId, BusinessLineId = businessLineId, CurriculumVersionId = curriculumVersionId, ReleaseNo = 1, Name = "Old release", SourceFingerprint = oldHash, Status = ContentReleaseStatus.Published, PublishedAt = now }, new ContentRelease { Id = newReleaseId, TenantId = ownerTenantId, BusinessLineId = businessLineId, CurriculumVersionId = curriculumVersionId, ReleaseNo = 2, Name = "New release", SourceFingerprint = newHash, Status = ContentReleaseStatus.Published, PublishedAt = now }); await factory.SeedAsync( new PlatformContentPackageVersion { Id = oldPackageVersionId, TenantId = ownerTenantId, PackageCode = packageCode, VersionNo = 1, BusinessLineId = businessLineId, ContentReleaseId = oldReleaseId, ContentHash = oldHash, Status = PlatformContentPackageStatus.Published, PublishedAt = now }, new PlatformContentPackageVersion { Id = newPackageVersionId, TenantId = ownerTenantId, PackageCode = packageCode, VersionNo = 2, BusinessLineId = businessLineId, ContentReleaseId = newReleaseId, ContentHash = newHash, Status = PlatformContentPackageStatus.Published, PublishedAt = now }); return new PackageFixture( ownerTenantId, oldPackageVersionId, newPackageVersionId, packageCode, oldHash, newHash); } private sealed record PackageFixture( Guid OwnerTenantId, Guid OldPackageVersionId, Guid NewPackageVersionId, string PackageCode, string OldHash, string NewHash); }