using StellaOps.AirGap.Time.Models; using StellaOps.AirGap.Time.Services; using StellaOps.AirGap.Time.Stores; using StellaOps.TestKit; namespace StellaOps.AirGap.Time.Tests; public class SealedStartupValidatorTests { [Trait("Category", TestCategories.Unit)] [Fact] public async Task FailsWhenAnchorMissing() { var validator = Build(out var statusService, DateTimeOffset.UnixEpoch); var result = await validator.ValidateAsync("t1", StalenessBudget.Default, default); Assert.False(result.IsValid); Assert.Equal("time-anchor-missing", result.Reason); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task FailsWhenBreach() { var now = DateTimeOffset.UnixEpoch.AddSeconds(25); var validator = Build(out var statusService, now); var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "src", "fmt", "fp", "digest"); await statusService.SetAnchorAsync("t1", anchor, new StalenessBudget(10, 20)); var status = await statusService.GetStatusAsync("t1", now); var result = status.Staleness.IsBreach; Assert.True(result); var validation = await validator.ValidateAsync("t1", new StalenessBudget(10, 20), default); Assert.False(validation.IsValid); Assert.Equal("time-anchor-stale", validation.Reason); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task SucceedsWhenFresh() { var now = DateTimeOffset.UnixEpoch.AddSeconds(5); var validator = Build(out var statusService, now); var anchor = new TimeAnchor(now, "src", "fmt", "fp", "digest"); await statusService.SetAnchorAsync("t1", anchor, new StalenessBudget(10, 20)); var validation = await validator.ValidateAsync("t1", new StalenessBudget(10, 20), default); Assert.True(validation.IsValid); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task FailsOnBudgetMismatch() { var now = DateTimeOffset.UnixEpoch.AddSeconds(5); var validator = Build(out var statusService, now); var anchor = new TimeAnchor(now, "src", "fmt", "fp", "digest"); await statusService.SetAnchorAsync("t1", anchor, new StalenessBudget(10, 20)); var validation = await validator.ValidateAsync("t1", new StalenessBudget(5, 15), default); Assert.False(validation.IsValid); Assert.Equal("time-anchor-budget-mismatch", validation.Reason); } private static SealedStartupValidator Build(out TimeStatusService statusService, DateTimeOffset now) { var store = new InMemoryTimeAnchorStore(); statusService = new TimeStatusService(store, new StalenessCalculator(), new TimeTelemetry(), new TestOptionsMonitor(new AirGapOptions())); return new SealedStartupValidator(statusService, new FixedTimeProvider(now)); } private sealed class FixedTimeProvider : TimeProvider { private readonly DateTimeOffset _now; public FixedTimeProvider(DateTimeOffset now) => _now = now; public override DateTimeOffset GetUtcNow() => _now; } }