Files
git.stella-ops.org/tests/AirGap/StellaOps.AirGap.Importer.Tests/Versioning/VersionMonotonicityCheckerTests.cs
master 4391f35d8a Refactor SurfaceCacheValidator to simplify oldest entry calculation
Add global using for Xunit in test project

Enhance ImportValidatorTests with async validation and quarantine checks

Implement FileSystemQuarantineServiceTests for quarantine functionality

Add integration tests for ImportValidator to check monotonicity

Create BundleVersionTests to validate version parsing and comparison logic

Implement VersionMonotonicityCheckerTests for monotonicity checks and activation logic
2025-12-16 10:44:00 +02:00

158 lines
6.1 KiB
C#

using FluentAssertions;
using StellaOps.AirGap.Importer.Versioning;
namespace StellaOps.AirGap.Importer.Tests.Versioning;
public sealed class VersionMonotonicityCheckerTests
{
[Fact]
public async Task CheckAsync_WhenNoCurrent_ShouldBeFirstActivation()
{
var store = new InMemoryBundleVersionStore();
var checker = new VersionMonotonicityChecker(store, new FixedTimeProvider(DateTimeOffset.Parse("2025-12-14T00:00:00Z")));
var incoming = BundleVersion.Parse("1.0.0", DateTimeOffset.Parse("2025-12-14T00:00:00Z"));
var result = await checker.CheckAsync("tenant-a", "offline-kit", incoming);
result.IsMonotonic.Should().BeTrue();
result.ReasonCode.Should().Be("FIRST_ACTIVATION");
result.CurrentVersion.Should().BeNull();
result.CurrentBundleDigest.Should().BeNull();
}
[Fact]
public async Task CheckAsync_WhenOlder_ShouldBeNonMonotonic()
{
var store = new InMemoryBundleVersionStore();
await store.UpsertAsync(new BundleVersionRecord(
TenantId: "tenant-a",
BundleType: "offline-kit",
VersionString: "2.0.0",
Major: 2,
Minor: 0,
Patch: 0,
Prerelease: null,
BundleCreatedAt: DateTimeOffset.Parse("2025-12-14T00:00:00Z"),
BundleDigest: "sha256:current",
ActivatedAt: DateTimeOffset.Parse("2025-12-14T00:00:00Z"),
WasForceActivated: false,
ForceActivateReason: null));
var checker = new VersionMonotonicityChecker(store, new FixedTimeProvider(DateTimeOffset.Parse("2025-12-14T00:00:00Z")));
var incoming = BundleVersion.Parse("1.0.0", DateTimeOffset.Parse("2025-12-14T00:00:00Z"));
var result = await checker.CheckAsync("tenant-a", "offline-kit", incoming);
result.IsMonotonic.Should().BeFalse();
result.ReasonCode.Should().Be("VERSION_NON_MONOTONIC");
result.CurrentVersion.Should().NotBeNull();
result.CurrentVersion!.SemVer.Should().Be("2.0.0");
}
[Fact]
public async Task RecordActivationAsync_WhenNonMonotonicWithoutForce_ShouldThrow()
{
var store = new InMemoryBundleVersionStore();
await store.UpsertAsync(new BundleVersionRecord(
TenantId: "tenant-a",
BundleType: "offline-kit",
VersionString: "2.0.0",
Major: 2,
Minor: 0,
Patch: 0,
Prerelease: null,
BundleCreatedAt: DateTimeOffset.Parse("2025-12-14T00:00:00Z"),
BundleDigest: "sha256:current",
ActivatedAt: DateTimeOffset.Parse("2025-12-14T00:00:00Z"),
WasForceActivated: false,
ForceActivateReason: null));
var checker = new VersionMonotonicityChecker(store, new FixedTimeProvider(DateTimeOffset.Parse("2025-12-15T00:00:00Z")));
var incoming = BundleVersion.Parse("1.0.0", DateTimeOffset.Parse("2025-12-15T00:00:00Z"));
var act = () => checker.RecordActivationAsync("tenant-a", "offline-kit", incoming, "sha256:new");
await act.Should().ThrowAsync<InvalidOperationException>();
}
[Fact]
public async Task RecordActivationAsync_WhenForced_ShouldWriteForceFields()
{
var store = new InMemoryBundleVersionStore();
await store.UpsertAsync(new BundleVersionRecord(
TenantId: "tenant-a",
BundleType: "offline-kit",
VersionString: "2.0.0",
Major: 2,
Minor: 0,
Patch: 0,
Prerelease: null,
BundleCreatedAt: DateTimeOffset.Parse("2025-12-14T00:00:00Z"),
BundleDigest: "sha256:current",
ActivatedAt: DateTimeOffset.Parse("2025-12-14T00:00:00Z"),
WasForceActivated: false,
ForceActivateReason: null));
var checker = new VersionMonotonicityChecker(store, new FixedTimeProvider(DateTimeOffset.Parse("2025-12-15T00:00:00Z")));
var incoming = BundleVersion.Parse("1.0.0", DateTimeOffset.Parse("2025-12-15T00:00:00Z"));
await checker.RecordActivationAsync(
"tenant-a",
"offline-kit",
incoming,
"sha256:new",
wasForceActivated: true,
forceActivateReason: "manual rollback permitted");
var current = await store.GetCurrentAsync("tenant-a", "offline-kit");
current.Should().NotBeNull();
current!.WasForceActivated.Should().BeTrue();
current.ForceActivateReason.Should().Be("manual rollback permitted");
current.BundleDigest.Should().Be("sha256:new");
}
private sealed class InMemoryBundleVersionStore : IBundleVersionStore
{
private BundleVersionRecord? _current;
private readonly List<BundleVersionRecord> _history = new();
public Task<BundleVersionRecord?> GetCurrentAsync(string tenantId, string bundleType, CancellationToken ct = default)
{
return Task.FromResult(_current is not null &&
_current.TenantId.Equals(tenantId, StringComparison.Ordinal) &&
_current.BundleType.Equals(bundleType, StringComparison.Ordinal)
? _current
: null);
}
public Task UpsertAsync(BundleVersionRecord record, CancellationToken ct = default)
{
_current = record;
_history.Insert(0, record);
return Task.CompletedTask;
}
public Task<IReadOnlyList<BundleVersionRecord>> GetHistoryAsync(string tenantId, string bundleType, int limit = 10, CancellationToken ct = default)
{
var items = _history
.Where(r => r.TenantId.Equals(tenantId, StringComparison.Ordinal) && r.BundleType.Equals(bundleType, StringComparison.Ordinal))
.Take(limit)
.ToArray();
return Task.FromResult<IReadOnlyList<BundleVersionRecord>>(items);
}
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FixedTimeProvider(DateTimeOffset utcNow)
{
_utcNow = utcNow;
}
public override DateTimeOffset GetUtcNow() => _utcNow;
}
}