using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Policy.Persistence.Postgres; using StellaOps.Policy.Persistence.Postgres.Models; using StellaOps.Policy.Persistence.Postgres.Repositories; using Xunit; using StellaOps.TestKit; namespace StellaOps.Policy.Persistence.Tests; /// /// Tests for pack versioning workflow scenarios (PG-T4.8.2). /// Validates the complete lifecycle of pack versioning including: /// - Creating pack versions /// - Activating/deactivating versions /// - Rolling back to previous versions /// - Version history preservation /// [Collection(PolicyPostgresCollection.Name)] public sealed class PackVersioningWorkflowTests : IAsyncLifetime { private readonly PolicyPostgresFixture _fixture; private readonly PackRepository _packRepository; private readonly RuleRepository _ruleRepository; private readonly string _tenantId = Guid.NewGuid().ToString(); public PackVersioningWorkflowTests(PolicyPostgresFixture fixture) { _fixture = fixture; var options = fixture.Fixture.CreateOptions(); options.SchemaName = fixture.SchemaName; var dataSource = new PolicyDataSource(Options.Create(options), NullLogger.Instance); _packRepository = new PackRepository(dataSource, NullLogger.Instance); _ruleRepository = new RuleRepository(dataSource, NullLogger.Instance); } public Task InitializeAsync() => _fixture.TruncateAllTablesAsync(); public Task DisposeAsync() => Task.CompletedTask; [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_CreateUpdateActivate_MaintainsVersionIntegrity() { // Arrange - Create initial pack var pack = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "versioned-pack", DisplayName = "Versioned Policy Pack", Description = "Pack for version testing", ActiveVersion = 1, IsBuiltin = false }; await _packRepository.CreateAsync(pack); // Act - Update to version 2 await _packRepository.SetActiveVersionAsync(_tenantId, pack.Id, 2); var afterV2 = await _packRepository.GetByIdAsync(_tenantId, pack.Id); // Assert afterV2.Should().NotBeNull(); afterV2!.ActiveVersion.Should().Be(2); // Act - Update to version 3 await _packRepository.SetActiveVersionAsync(_tenantId, pack.Id, 3); var afterV3 = await _packRepository.GetByIdAsync(_tenantId, pack.Id); // Assert afterV3.Should().NotBeNull(); afterV3!.ActiveVersion.Should().Be(3); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_RollbackVersion_RestoresPreviousVersion() { // Arrange - Create pack at version 3 var pack = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "rollback-pack", ActiveVersion = 3, IsBuiltin = false }; await _packRepository.CreateAsync(pack); // Act - Rollback to version 2 await _packRepository.SetActiveVersionAsync(_tenantId, pack.Id, 2); var afterRollback = await _packRepository.GetByIdAsync(_tenantId, pack.Id); // Assert afterRollback.Should().NotBeNull(); afterRollback!.ActiveVersion.Should().Be(2); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_MultiplePacksDifferentVersions_Isolated() { // Arrange - Create multiple packs with different versions var pack1 = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "pack-a", ActiveVersion = 1 }; var pack2 = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "pack-b", ActiveVersion = 5 }; await _packRepository.CreateAsync(pack1); await _packRepository.CreateAsync(pack2); // Act - Update pack1 only await _packRepository.SetActiveVersionAsync(_tenantId, pack1.Id, 10); // Assert - pack2 should be unaffected var fetchedPack1 = await _packRepository.GetByIdAsync(_tenantId, pack1.Id); var fetchedPack2 = await _packRepository.GetByIdAsync(_tenantId, pack2.Id); fetchedPack1!.ActiveVersion.Should().Be(10); fetchedPack2!.ActiveVersion.Should().Be(5); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_DeprecatedPackVersionStillReadable() { // Arrange - Create and deprecate pack var pack = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "deprecated-version-pack", ActiveVersion = 3, IsDeprecated = false }; await _packRepository.CreateAsync(pack); // Act - Deprecate the pack await _packRepository.DeprecateAsync(_tenantId, pack.Id); var deprecated = await _packRepository.GetByIdAsync(_tenantId, pack.Id); // Assert - Version should still be readable deprecated.Should().NotBeNull(); deprecated!.IsDeprecated.Should().BeTrue(); deprecated.ActiveVersion.Should().Be(3); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_ConcurrentVersionUpdates_LastWriteWins() { // Arrange - Create pack var pack = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "concurrent-version-pack", ActiveVersion = 1 }; await _packRepository.CreateAsync(pack); // Act - Simulate concurrent updates var tasks = new[] { _packRepository.SetActiveVersionAsync(_tenantId, pack.Id, 2), _packRepository.SetActiveVersionAsync(_tenantId, pack.Id, 3), _packRepository.SetActiveVersionAsync(_tenantId, pack.Id, 4) }; await Task.WhenAll(tasks); // Assert - One of the versions should win var final = await _packRepository.GetByIdAsync(_tenantId, pack.Id); final.Should().NotBeNull(); final!.ActiveVersion.Should().BeOneOf(2, 3, 4); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_DeterministicOrdering_VersionsReturnConsistently() { // Arrange - Create multiple packs var packs = Enumerable.Range(1, 5).Select(i => new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = $"ordered-pack-{i}", ActiveVersion = i }).ToList(); foreach (var pack in packs) { await _packRepository.CreateAsync(pack); } // Act - Fetch multiple times var results1 = await _packRepository.GetAllAsync(_tenantId); var results2 = await _packRepository.GetAllAsync(_tenantId); var results3 = await _packRepository.GetAllAsync(_tenantId); // Assert - Order should be deterministic var names1 = results1.Select(p => p.Name).ToList(); var names2 = results2.Select(p => p.Name).ToList(); var names3 = results3.Select(p => p.Name).ToList(); names1.Should().Equal(names2); names2.Should().Equal(names3); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_UpdateTimestampProgresses_OnVersionChange() { // Arrange var pack = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "timestamp-version-pack", ActiveVersion = 1 }; await _packRepository.CreateAsync(pack); var created = await _packRepository.GetByIdAsync(_tenantId, pack.Id); var initialUpdatedAt = created!.UpdatedAt; // Small delay to ensure timestamp difference await Task.Delay(10); // Act - Update version await _packRepository.SetActiveVersionAsync(_tenantId, pack.Id, 2); var updated = await _packRepository.GetByIdAsync(_tenantId, pack.Id); // Assert - UpdatedAt should have progressed updated!.UpdatedAt.Should().BeOnOrAfter(initialUpdatedAt); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_ZeroVersionAllowed_AsInitialState() { // Arrange - Create pack with version 0 (no active version) var pack = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "zero-version-pack", ActiveVersion = 0 }; // Act await _packRepository.CreateAsync(pack); var fetched = await _packRepository.GetByIdAsync(_tenantId, pack.Id); // Assert fetched.Should().NotBeNull(); fetched!.ActiveVersion.Should().Be(0); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VersionWorkflow_BuiltinPackVersioning_WorksLikeCustomPacks() { // Arrange - Create builtin pack var builtinPack = new PackEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "builtin-versioned", ActiveVersion = 1, IsBuiltin = true }; await _packRepository.CreateAsync(builtinPack); // Act - Update version await _packRepository.SetActiveVersionAsync(_tenantId, builtinPack.Id, 2); var updated = await _packRepository.GetByIdAsync(_tenantId, builtinPack.Id); // Assert updated.Should().NotBeNull(); updated!.ActiveVersion.Should().Be(2); updated.IsBuiltin.Should().BeTrue(); } }