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
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.AirGap.Importer.Versioning;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Tests.Versioning;
|
||||
|
||||
public sealed class BundleVersionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_ShouldParseSemVer()
|
||||
{
|
||||
var createdAt = new DateTimeOffset(2025, 12, 14, 0, 0, 0, TimeSpan.Zero);
|
||||
var version = BundleVersion.Parse("1.2.3", createdAt);
|
||||
|
||||
version.Major.Should().Be(1);
|
||||
version.Minor.Should().Be(2);
|
||||
version.Patch.Should().Be(3);
|
||||
version.Prerelease.Should().BeNull();
|
||||
version.CreatedAt.Should().Be(createdAt);
|
||||
version.SemVer.Should().Be("1.2.3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldParsePrerelease()
|
||||
{
|
||||
var createdAt = new DateTimeOffset(2025, 12, 14, 0, 0, 0, TimeSpan.Zero);
|
||||
var version = BundleVersion.Parse("1.2.3-edge.1", createdAt);
|
||||
|
||||
version.SemVer.Should().Be("1.2.3-edge.1");
|
||||
version.Prerelease.Should().Be("edge.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNewerThan_ShouldCompareMajorMinorPatch()
|
||||
{
|
||||
var a = new BundleVersion(1, 2, 3, DateTimeOffset.UnixEpoch);
|
||||
var b = new BundleVersion(2, 0, 0, DateTimeOffset.UnixEpoch);
|
||||
b.IsNewerThan(a).Should().BeTrue();
|
||||
a.IsNewerThan(b).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNewerThan_ShouldTreatReleaseAsNewerThanPrerelease()
|
||||
{
|
||||
var now = new DateTimeOffset(2025, 12, 14, 0, 0, 0, TimeSpan.Zero);
|
||||
var prerelease = new BundleVersion(1, 2, 3, now, "alpha");
|
||||
var release = new BundleVersion(1, 2, 3, now, null);
|
||||
|
||||
release.IsNewerThan(prerelease).Should().BeTrue();
|
||||
prerelease.IsNewerThan(release).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNewerThan_ShouldOrderPrereleaseIdentifiers()
|
||||
{
|
||||
var now = new DateTimeOffset(2025, 12, 14, 0, 0, 0, TimeSpan.Zero);
|
||||
var alpha = new BundleVersion(1, 2, 3, now, "alpha");
|
||||
var beta = new BundleVersion(1, 2, 3, now, "beta");
|
||||
var rc1 = new BundleVersion(1, 2, 3, now, "rc.1");
|
||||
var rc2 = new BundleVersion(1, 2, 3, now, "rc.2");
|
||||
|
||||
beta.IsNewerThan(alpha).Should().BeTrue();
|
||||
rc1.IsNewerThan(beta).Should().BeTrue();
|
||||
rc2.IsNewerThan(rc1).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNewerThan_ShouldUseCreatedAtAsTiebreaker()
|
||||
{
|
||||
var earlier = new DateTimeOffset(2025, 12, 14, 0, 0, 0, TimeSpan.Zero);
|
||||
var later = earlier.AddMinutes(1);
|
||||
|
||||
var a = new BundleVersion(1, 2, 3, earlier, "edge");
|
||||
var b = new BundleVersion(1, 2, 3, later, "edge");
|
||||
|
||||
b.IsNewerThan(a).Should().BeTrue();
|
||||
a.IsNewerThan(b).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user