using StellaOps.AirGap.Controller.Endpoints.Contracts; using StellaOps.AirGap.Controller.Services; using StellaOps.AirGap.Controller.Stores; using StellaOps.AirGap.Importer.Contracts; using StellaOps.AirGap.Importer.Validation; using StellaOps.AirGap.Time.Models; using StellaOps.AirGap.Time.Services; using Xunit; namespace StellaOps.AirGap.Controller.Tests; public class ReplayVerificationServiceTests { private readonly ReplayVerificationService _service; private readonly AirGapStateService _stateService; private readonly StalenessCalculator _staleness = new(); private readonly InMemoryAirGapStateStore _store = new(); public ReplayVerificationServiceTests() { _stateService = new AirGapStateService(_store, _staleness); _service = new ReplayVerificationService(_stateService, new ReplayVerifier()); } [Fact] public async Task Passes_full_recompute_when_hashes_match() { var now = DateTimeOffset.Parse("2025-12-02T01:00:00Z"); await _stateService.SealAsync("tenant-a", "policy-x", TimeAnchor.Unknown, StalenessBudget.Default, now); var request = new VerifyRequest { Depth = ReplayDepth.FullRecompute, ManifestSha256 = new string('a', 64), BundleSha256 = new string('b', 64), ComputedManifestSha256 = new string('a', 64), ComputedBundleSha256 = new string('b', 64), ManifestCreatedAt = now.AddHours(-2), StalenessWindowHours = 24, BundlePolicyHash = "policy-x" }; var result = await _service.VerifyAsync("tenant-a", request, now); Assert.True(result.IsValid); Assert.Equal("full-recompute-passed", result.Reason); } [Fact] public async Task Detects_stale_manifest() { var now = DateTimeOffset.UtcNow; var request = new VerifyRequest { Depth = ReplayDepth.HashOnly, ManifestSha256 = new string('a', 64), BundleSha256 = new string('b', 64), ComputedManifestSha256 = new string('a', 64), ComputedBundleSha256 = new string('b', 64), ManifestCreatedAt = now.AddHours(-30), StalenessWindowHours = 12 }; var result = await _service.VerifyAsync("default", request, now); Assert.False(result.IsValid); Assert.Equal("manifest-stale", result.Reason); } [Fact] public async Task Policy_freeze_requires_matching_policy() { var now = DateTimeOffset.UtcNow; await _stateService.SealAsync("tenant-b", "sealed-policy", TimeAnchor.Unknown, StalenessBudget.Default, now); var request = new VerifyRequest { Depth = ReplayDepth.PolicyFreeze, ManifestSha256 = new string('a', 64), BundleSha256 = new string('b', 64), ComputedManifestSha256 = new string('a', 64), ComputedBundleSha256 = new string('b', 64), ManifestCreatedAt = now, StalenessWindowHours = 48, BundlePolicyHash = "bundle-policy" }; var result = await _service.VerifyAsync("tenant-b", request, now); Assert.False(result.IsValid); Assert.Equal("policy-hash-drift", result.Reason); } }