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();
}
}