Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
// <copyright file="AuthorityConfigDiffTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
||||
// Task: CCUT-021
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.ConfigDiff;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.ConfigDiff.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Config-diff tests for the Authority module.
|
||||
/// Verifies that configuration changes produce only expected behavioral deltas.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.ConfigDiff)]
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Trait("BlastRadius", TestCategories.BlastRadius.Auth)]
|
||||
public class AuthorityConfigDiffTests : ConfigDiffTestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorityConfigDiffTests"/> class.
|
||||
/// </summary>
|
||||
public AuthorityConfigDiffTests()
|
||||
: base(
|
||||
new ConfigDiffTestConfig(StrictMode: true),
|
||||
NullLogger.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that changing token lifetime only affects token behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangingTokenLifetime_OnlyAffectsTokenBehavior()
|
||||
{
|
||||
// Arrange
|
||||
var baselineConfig = new AuthorityTestConfig
|
||||
{
|
||||
AccessTokenLifetimeMinutes = 15,
|
||||
RefreshTokenLifetimeHours = 24,
|
||||
MaxConcurrentSessions = 5
|
||||
};
|
||||
|
||||
var changedConfig = baselineConfig with
|
||||
{
|
||||
AccessTokenLifetimeMinutes = 30
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await TestConfigIsolationAsync(
|
||||
baselineConfig,
|
||||
changedConfig,
|
||||
changedSetting: "AccessTokenLifetimeMinutes",
|
||||
unrelatedBehaviors:
|
||||
[
|
||||
async config => await GetSessionBehaviorAsync(config),
|
||||
async config => await GetRefreshBehaviorAsync(config),
|
||||
async config => await GetAuthenticationBehaviorAsync(config)
|
||||
]);
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue(
|
||||
because: "changing token lifetime should not affect sessions or authentication");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that changing max sessions produces expected behavioral delta.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangingMaxSessions_ProducesExpectedDelta()
|
||||
{
|
||||
// Arrange
|
||||
var baselineConfig = new AuthorityTestConfig { MaxConcurrentSessions = 3 };
|
||||
var changedConfig = new AuthorityTestConfig { MaxConcurrentSessions = 10 };
|
||||
|
||||
var expectedDelta = new ConfigDelta(
|
||||
ChangedBehaviors: ["SessionLimit", "ConcurrencyPolicy"],
|
||||
BehaviorDeltas:
|
||||
[
|
||||
new BehaviorDelta("SessionLimit", "3", "10", null),
|
||||
new BehaviorDelta("ConcurrencyPolicy", "restrictive", "permissive",
|
||||
"More sessions allowed")
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = await TestConfigBehavioralDeltaAsync(
|
||||
baselineConfig,
|
||||
changedConfig,
|
||||
getBehavior: async config => await CaptureSessionBehaviorAsync(config),
|
||||
computeDelta: ComputeBehaviorSnapshotDelta,
|
||||
expectedDelta: expectedDelta);
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue(
|
||||
because: "session limit change should produce expected behavioral delta");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that enabling DPoP only affects token binding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EnablingDPoP_OnlyAffectsTokenBinding()
|
||||
{
|
||||
// Arrange
|
||||
var baselineConfig = new AuthorityTestConfig { EnableDPoP = false };
|
||||
var changedConfig = new AuthorityTestConfig { EnableDPoP = true };
|
||||
|
||||
// Act
|
||||
var result = await TestConfigIsolationAsync(
|
||||
baselineConfig,
|
||||
changedConfig,
|
||||
changedSetting: "EnableDPoP",
|
||||
unrelatedBehaviors:
|
||||
[
|
||||
async config => await GetSessionBehaviorAsync(config),
|
||||
async config => await GetPasswordPolicyBehaviorAsync(config)
|
||||
]);
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue(
|
||||
because: "DPoP should not affect sessions or password policy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that changing password policy produces expected changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChangingPasswordMinLength_ProducesExpectedDelta()
|
||||
{
|
||||
// Arrange
|
||||
var baselineConfig = new AuthorityTestConfig { MinPasswordLength = 8 };
|
||||
var changedConfig = new AuthorityTestConfig { MinPasswordLength = 12 };
|
||||
|
||||
var expectedDelta = new ConfigDelta(
|
||||
ChangedBehaviors: ["PasswordComplexity", "ValidationRejectionRate"],
|
||||
BehaviorDeltas:
|
||||
[
|
||||
new BehaviorDelta("PasswordComplexity", "standard", "enhanced", null),
|
||||
new BehaviorDelta("ValidationRejectionRate", "increase", null,
|
||||
"Stricter requirements reject more passwords")
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = await TestConfigBehavioralDeltaAsync(
|
||||
baselineConfig,
|
||||
changedConfig,
|
||||
getBehavior: async config => await CapturePasswordPolicyBehaviorAsync(config),
|
||||
computeDelta: ComputeBehaviorSnapshotDelta,
|
||||
expectedDelta: expectedDelta);
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that enabling MFA only affects authentication flow.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EnablingMFA_OnlyAffectsAuthentication()
|
||||
{
|
||||
// Arrange
|
||||
var baselineConfig = new AuthorityTestConfig { RequireMFA = false };
|
||||
var changedConfig = new AuthorityTestConfig { RequireMFA = true };
|
||||
|
||||
// Act
|
||||
var result = await TestConfigIsolationAsync(
|
||||
baselineConfig,
|
||||
changedConfig,
|
||||
changedSetting: "RequireMFA",
|
||||
unrelatedBehaviors:
|
||||
[
|
||||
async config => await GetTokenBehaviorAsync(config),
|
||||
async config => await GetSessionBehaviorAsync(config)
|
||||
]);
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue(
|
||||
because: "MFA should not affect token issuance or session management");
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private static Task<object> GetSessionBehaviorAsync(AuthorityTestConfig config)
|
||||
{
|
||||
return Task.FromResult<object>(new { MaxSessions = config.MaxConcurrentSessions });
|
||||
}
|
||||
|
||||
private static Task<object> GetRefreshBehaviorAsync(AuthorityTestConfig config)
|
||||
{
|
||||
return Task.FromResult<object>(new { RefreshLifetime = config.RefreshTokenLifetimeHours });
|
||||
}
|
||||
|
||||
private static Task<object> GetAuthenticationBehaviorAsync(AuthorityTestConfig config)
|
||||
{
|
||||
return Task.FromResult<object>(new { MfaRequired = config.RequireMFA });
|
||||
}
|
||||
|
||||
private static Task<object> GetPasswordPolicyBehaviorAsync(AuthorityTestConfig config)
|
||||
{
|
||||
return Task.FromResult<object>(new { MinLength = config.MinPasswordLength });
|
||||
}
|
||||
|
||||
private static Task<object> GetTokenBehaviorAsync(AuthorityTestConfig config)
|
||||
{
|
||||
return Task.FromResult<object>(new { Lifetime = config.AccessTokenLifetimeMinutes });
|
||||
}
|
||||
|
||||
private static Task<BehaviorSnapshot> CaptureSessionBehaviorAsync(AuthorityTestConfig config)
|
||||
{
|
||||
var snapshot = new BehaviorSnapshot(
|
||||
ConfigurationId: $"sessions-{config.MaxConcurrentSessions}",
|
||||
Behaviors:
|
||||
[
|
||||
new CapturedBehavior("SessionLimit", config.MaxConcurrentSessions.ToString(), DateTimeOffset.UtcNow),
|
||||
new CapturedBehavior("ConcurrencyPolicy",
|
||||
config.MaxConcurrentSessions > 5 ? "permissive" : "restrictive", DateTimeOffset.UtcNow)
|
||||
],
|
||||
CapturedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
return Task.FromResult(snapshot);
|
||||
}
|
||||
|
||||
private static Task<BehaviorSnapshot> CapturePasswordPolicyBehaviorAsync(AuthorityTestConfig config)
|
||||
{
|
||||
var snapshot = new BehaviorSnapshot(
|
||||
ConfigurationId: $"password-{config.MinPasswordLength}",
|
||||
Behaviors:
|
||||
[
|
||||
new CapturedBehavior("PasswordComplexity",
|
||||
config.MinPasswordLength >= 12 ? "enhanced" : "standard", DateTimeOffset.UtcNow),
|
||||
new CapturedBehavior("ValidationRejectionRate",
|
||||
config.MinPasswordLength >= 12 ? "increase" : "standard", DateTimeOffset.UtcNow)
|
||||
],
|
||||
CapturedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
return Task.FromResult(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test configuration for Authority module.
|
||||
/// </summary>
|
||||
public sealed record AuthorityTestConfig
|
||||
{
|
||||
public int AccessTokenLifetimeMinutes { get; init; } = 15;
|
||||
public int RefreshTokenLifetimeHours { get; init; } = 24;
|
||||
public int MaxConcurrentSessions { get; init; } = 5;
|
||||
public bool EnableDPoP { get; init; } = false;
|
||||
public int MinPasswordLength { get; init; } = 8;
|
||||
public bool RequireMFA { get; init; } = false;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Description>Config-diff tests for Authority module</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../__Tests/__Libraries/StellaOps.Testing.ConfigDiff/StellaOps.Testing.ConfigDiff.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -15,5 +15,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Authority.Core/StellaOps.Authority.Core.csproj" />
|
||||
<ProjectReference Include="../../../__Tests/__Libraries/StellaOps.Testing.Temporal/StellaOps.Testing.Temporal.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,296 @@
|
||||
// <copyright file="TemporalVerdictTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_001_TEST_time_skew_idempotency
|
||||
// Task: TSKW-011
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Authority.Core.Verdicts;
|
||||
using StellaOps.Testing.Temporal;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Core.Tests.Verdicts;
|
||||
|
||||
/// <summary>
|
||||
/// Temporal testing for verdict manifests using the Testing.Temporal library.
|
||||
/// Tests clock cutoff handling, timestamp consistency, and determinism under time skew.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class TemporalVerdictTests
|
||||
{
|
||||
private static readonly DateTimeOffset BaseTime = new(2026, 1, 5, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifest_ClockCutoff_BoundaryPrecision()
|
||||
{
|
||||
// Arrange
|
||||
var ttlProvider = new TtlBoundaryTimeProvider(BaseTime);
|
||||
var ttl = TimeSpan.FromHours(24); // Typical verdict validity window
|
||||
var clockCutoff = BaseTime;
|
||||
|
||||
// Position at various boundaries
|
||||
var testCases = TtlBoundaryTimeProvider.GenerateBoundaryTestCases(clockCutoff, ttl).ToList();
|
||||
|
||||
// Assert - verify all boundary cases are correctly handled
|
||||
foreach (var testCase in testCases)
|
||||
{
|
||||
var isExpired = testCase.Time >= clockCutoff.Add(ttl);
|
||||
isExpired.Should().Be(
|
||||
testCase.ShouldBeExpired,
|
||||
$"Verdict clock cutoff case '{testCase.Name}' should be expired={testCase.ShouldBeExpired}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifestBuilder_IsDeterministic_UnderTimeAdvancement()
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new SimulatedTimeProvider(BaseTime);
|
||||
var results = new List<string>();
|
||||
|
||||
// Act - build multiple manifests while advancing time
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var manifest = BuildTestManifest(BaseTime); // Use fixed clock, not advancing
|
||||
results.Add(manifest.ManifestDigest);
|
||||
timeProvider.Advance(TimeSpan.FromMinutes(5)); // Advance between builds
|
||||
}
|
||||
|
||||
// Assert - all manifests should have same digest (deterministic)
|
||||
results.Distinct().Should().HaveCount(1, "manifests built with same inputs should be deterministic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifestBuilder_Build_IsIdempotent()
|
||||
{
|
||||
// Arrange
|
||||
var stateSnapshotter = () => BuildTestManifest(BaseTime).ManifestDigest;
|
||||
var verifier = new IdempotencyVerifier<string>(stateSnapshotter);
|
||||
|
||||
// Act - verify Build is idempotent
|
||||
var result = verifier.Verify(() => { /* Build is called in snapshotter */ }, repetitions: 5);
|
||||
|
||||
// Assert
|
||||
result.IsIdempotent.Should().BeTrue("VerdictManifestBuilder.Build should be idempotent");
|
||||
result.AllSucceeded.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifest_TimestampOrdering_IsMonotonic()
|
||||
{
|
||||
// Arrange - simulate verdict timestamps
|
||||
var timeProvider = new SimulatedTimeProvider(BaseTime);
|
||||
var timestamps = new List<DateTimeOffset>();
|
||||
|
||||
// Simulate verdict lifecycle: created, processed, signed, stored
|
||||
timestamps.Add(timeProvider.GetUtcNow()); // Created
|
||||
timeProvider.Advance(TimeSpan.FromMilliseconds(50));
|
||||
timestamps.Add(timeProvider.GetUtcNow()); // Processed
|
||||
timeProvider.Advance(TimeSpan.FromMilliseconds(100));
|
||||
timestamps.Add(timeProvider.GetUtcNow()); // Signed
|
||||
timeProvider.Advance(TimeSpan.FromMilliseconds(20));
|
||||
timestamps.Add(timeProvider.GetUtcNow()); // Stored
|
||||
|
||||
// Act & Assert - timestamps should be monotonically increasing
|
||||
ClockSkewAssertions.AssertMonotonicTimestamps(timestamps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifest_HandlesClockSkewForward()
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new SimulatedTimeProvider(BaseTime);
|
||||
var clockCutoff1 = timeProvider.GetUtcNow();
|
||||
|
||||
// Simulate clock jump forward (NTP correction)
|
||||
timeProvider.JumpTo(BaseTime.AddHours(2));
|
||||
var clockCutoff2 = timeProvider.GetUtcNow();
|
||||
|
||||
// Act - build manifests with different clock cutoffs
|
||||
var manifest1 = BuildTestManifest(clockCutoff1);
|
||||
var manifest2 = BuildTestManifest(clockCutoff2);
|
||||
|
||||
// Assert - different clock cutoffs should produce different digests
|
||||
manifest1.ManifestDigest.Should().NotBe(manifest2.ManifestDigest,
|
||||
"different clock cutoffs should produce different manifest digests");
|
||||
|
||||
// Clock cutoff difference should be within expected range
|
||||
ClockSkewAssertions.AssertTimestampsWithinTolerance(
|
||||
clockCutoff1,
|
||||
clockCutoff2,
|
||||
tolerance: TimeSpan.FromHours(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifest_ClockDrift_DoesNotAffectDeterminism()
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new SimulatedTimeProvider(BaseTime);
|
||||
timeProvider.SetDrift(TimeSpan.FromMilliseconds(10)); // 10ms/second drift
|
||||
|
||||
var results = new List<string>();
|
||||
var fixedClock = BaseTime; // Use fixed clock for manifest
|
||||
|
||||
// Act - build manifests while time drifts
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var manifest = BuildTestManifest(fixedClock);
|
||||
results.Add(manifest.ManifestDigest);
|
||||
timeProvider.Advance(TimeSpan.FromSeconds(10)); // Time advances with drift
|
||||
}
|
||||
|
||||
// Assert - all should be identical (fixed clock input)
|
||||
results.Distinct().Should().HaveCount(1,
|
||||
"manifests with fixed clock should be deterministic regardless of system drift");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifest_ClockJumpBackward_IsDetected()
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new SimulatedTimeProvider(BaseTime);
|
||||
var timestamps = new List<DateTimeOffset>();
|
||||
|
||||
// Record timestamps
|
||||
timestamps.Add(timeProvider.GetUtcNow());
|
||||
timeProvider.Advance(TimeSpan.FromMinutes(5));
|
||||
timestamps.Add(timeProvider.GetUtcNow());
|
||||
|
||||
// Simulate clock jump backward
|
||||
timeProvider.JumpBackward(TimeSpan.FromMinutes(3));
|
||||
timestamps.Add(timeProvider.GetUtcNow());
|
||||
|
||||
// Assert - backward jump should be detected
|
||||
timeProvider.HasJumpedBackward().Should().BeTrue();
|
||||
|
||||
// Non-monotonic timestamps should be detected
|
||||
var act = () => ClockSkewAssertions.AssertMonotonicTimestamps(timestamps);
|
||||
act.Should().Throw<ClockSkewAssertionException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.9, VexStatus.NotAffected)]
|
||||
[InlineData(0.7, VexStatus.Affected)]
|
||||
[InlineData(0.5, VexStatus.UnderInvestigation)]
|
||||
public void VerdictManifest_ConfidenceScores_AreIdempotent(double confidence, VexStatus status)
|
||||
{
|
||||
// Arrange
|
||||
var stateSnapshotter = () =>
|
||||
{
|
||||
var manifest = BuildTestManifest(BaseTime, confidence, status);
|
||||
return manifest.Result.Confidence;
|
||||
};
|
||||
var verifier = new IdempotencyVerifier<double>(stateSnapshotter);
|
||||
|
||||
// Act
|
||||
var result = verifier.Verify(() => { }, repetitions: 3);
|
||||
|
||||
// Assert
|
||||
result.IsIdempotent.Should().BeTrue();
|
||||
result.States.Should().AllSatisfy(c => c.Should().Be(confidence));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifest_ExpiryWindow_BoundaryTests()
|
||||
{
|
||||
// Arrange - simulate verdict expiry window (e.g., 7 days)
|
||||
var expiryWindow = TimeSpan.FromDays(7);
|
||||
var createdAt = BaseTime;
|
||||
|
||||
// Generate boundary test cases
|
||||
var testCases = TtlBoundaryTimeProvider.GenerateBoundaryTestCases(createdAt, expiryWindow);
|
||||
|
||||
// Assert
|
||||
foreach (var testCase in testCases)
|
||||
{
|
||||
var isExpired = testCase.Time >= createdAt.Add(expiryWindow);
|
||||
isExpired.Should().Be(testCase.ShouldBeExpired, testCase.Name);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetVerdictExpiryBoundaryData))]
|
||||
public void VerdictManifest_TheoryBoundaryTests(
|
||||
string name,
|
||||
DateTimeOffset testTime,
|
||||
bool shouldBeExpired)
|
||||
{
|
||||
// Arrange
|
||||
var expiryWindow = TimeSpan.FromDays(7);
|
||||
var expiry = BaseTime.Add(expiryWindow);
|
||||
|
||||
// Act
|
||||
var isExpired = testTime >= expiry;
|
||||
|
||||
// Assert
|
||||
isExpired.Should().Be(shouldBeExpired, $"Case '{name}' should be expired={shouldBeExpired}");
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetVerdictExpiryBoundaryData()
|
||||
{
|
||||
var expiryWindow = TimeSpan.FromDays(7);
|
||||
return TtlBoundaryTimeProvider.GenerateTheoryData(BaseTime, expiryWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerdictManifest_LeapSecondScenario_MaintainsDeterminism()
|
||||
{
|
||||
// Arrange
|
||||
var leapDay = new DateOnly(2016, 12, 31);
|
||||
var leapProvider = new LeapSecondTimeProvider(
|
||||
new DateTimeOffset(2016, 12, 31, 23, 0, 0, TimeSpan.Zero),
|
||||
leapDay);
|
||||
|
||||
var results = new List<string>();
|
||||
var fixedClock = new DateTimeOffset(2016, 12, 31, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
// Act - build manifests while advancing through leap second
|
||||
foreach (var moment in leapProvider.AdvanceThroughLeapSecond(leapDay))
|
||||
{
|
||||
var manifest = BuildTestManifest(fixedClock);
|
||||
results.Add(manifest.ManifestDigest);
|
||||
}
|
||||
|
||||
// Assert - all manifests should be identical (fixed clock)
|
||||
results.Distinct().Should().HaveCount(1,
|
||||
"manifests should be deterministic even during leap second transition");
|
||||
}
|
||||
|
||||
private static VerdictManifest BuildTestManifest(
|
||||
DateTimeOffset clockCutoff,
|
||||
double confidence = 0.85,
|
||||
VexStatus status = VexStatus.NotAffected)
|
||||
{
|
||||
return new VerdictManifestBuilder(() => "test-manifest-id")
|
||||
.WithTenant("tenant-1")
|
||||
.WithAsset("sha256:abc123", "CVE-2024-1234")
|
||||
.WithInputs(
|
||||
sbomDigests: new[] { "sha256:sbom1" },
|
||||
vulnFeedSnapshotIds: new[] { "feed-snapshot-1" },
|
||||
vexDocumentDigests: new[] { "sha256:vex1" },
|
||||
clockCutoff: clockCutoff)
|
||||
.WithResult(
|
||||
status: status,
|
||||
confidence: confidence,
|
||||
explanations: new[]
|
||||
{
|
||||
new VerdictExplanation
|
||||
{
|
||||
SourceId = "vendor-a",
|
||||
Reason = "Test explanation",
|
||||
ProvenanceScore = 0.9,
|
||||
CoverageScore = 0.8,
|
||||
ReplayabilityScore = 0.7,
|
||||
StrengthMultiplier = 1.0,
|
||||
FreshnessMultiplier = 0.95,
|
||||
ClaimScore = confidence,
|
||||
AssertedStatus = status,
|
||||
Accepted = true,
|
||||
},
|
||||
})
|
||||
.WithPolicy("sha256:policy123", "1.0.0")
|
||||
.WithClock(clockCutoff)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user