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
158 lines
6.1 KiB
C#
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;
|
|
}
|
|
}
|
|
|