293 lines
9.9 KiB
C#
293 lines
9.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
[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<PolicyDataSource>.Instance);
|
|
_packRepository = new PackRepository(dataSource, NullLogger<PackRepository>.Instance);
|
|
_ruleRepository = new RuleRepository(dataSource, NullLogger<RuleRepository>.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();
|
|
}
|
|
}
|