Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management. - Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management. - Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support. - Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for CheckpointSignatureVerifier.
|
||||
/// SPRINT_3000_0001_0001 - T3: Checkpoint signature verification tests
|
||||
/// </summary>
|
||||
public sealed class CheckpointSignatureVerifierTests
|
||||
{
|
||||
// Sample checkpoint format (Rekor production format)
|
||||
private const string ValidCheckpointBody = """
|
||||
rekor.sigstore.dev - 2605736670972794746
|
||||
123456789
|
||||
abc123def456ghi789jkl012mno345pqr678stu901vwx234=
|
||||
1702345678
|
||||
""";
|
||||
|
||||
private const string InvalidFormatCheckpoint = "not a valid checkpoint";
|
||||
|
||||
[Fact]
|
||||
public void ParseCheckpoint_ValidFormat_ExtractsFields()
|
||||
{
|
||||
// Act
|
||||
var result = CheckpointSignatureVerifier.ParseCheckpoint(ValidCheckpointBody);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.Origin);
|
||||
Assert.Contains("rekor.sigstore.dev", result.Origin);
|
||||
Assert.Equal(123456789L, result.TreeSize);
|
||||
Assert.NotNull(result.RootHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCheckpoint_InvalidFormat_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = CheckpointSignatureVerifier.ParseCheckpoint(InvalidFormatCheckpoint);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Verified);
|
||||
Assert.Contains("Invalid", result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCheckpoint_EmptyString_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = CheckpointSignatureVerifier.ParseCheckpoint("");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Verified);
|
||||
Assert.NotNull(result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCheckpoint_MinimalValidFormat_ExtractsFields()
|
||||
{
|
||||
// Arrange - minimal checkpoint without timestamp
|
||||
var checkpoint = """
|
||||
origin-name
|
||||
42
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = CheckpointSignatureVerifier.ParseCheckpoint(checkpoint);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("origin-name", result.Origin);
|
||||
Assert.Equal(42L, result.TreeSize);
|
||||
Assert.NotNull(result.RootHash);
|
||||
Assert.Equal(32, result.RootHash!.Length); // SHA-256 hash
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCheckpoint_InvalidBase64Root_ReturnsFailure()
|
||||
{
|
||||
// Arrange - invalid base64 in root hash
|
||||
var checkpoint = """
|
||||
origin-name
|
||||
42
|
||||
not-valid-base64!!!
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = CheckpointSignatureVerifier.ParseCheckpoint(checkpoint);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Verified);
|
||||
Assert.Contains("Invalid root hash", result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCheckpoint_InvalidTreeSize_ReturnsFailure()
|
||||
{
|
||||
// Arrange - non-numeric tree size
|
||||
var checkpoint = """
|
||||
origin-name
|
||||
not-a-number
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = CheckpointSignatureVerifier.ParseCheckpoint(checkpoint);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Verified);
|
||||
Assert.Contains("Invalid tree size", result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_NullCheckpoint_ThrowsArgumentNull()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
CheckpointSignatureVerifier.VerifyCheckpoint(null!, [], []));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_NullSignature_ThrowsArgumentNull()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
CheckpointSignatureVerifier.VerifyCheckpoint("checkpoint", null!, []));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_NullPublicKey_ThrowsArgumentNull()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
CheckpointSignatureVerifier.VerifyCheckpoint("checkpoint", [], null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_InvalidFormat_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var signature = new byte[64];
|
||||
var publicKey = new byte[65]; // P-256 uncompressed
|
||||
|
||||
// Act
|
||||
var result = CheckpointSignatureVerifier.VerifyCheckpoint(
|
||||
InvalidFormatCheckpoint,
|
||||
signature,
|
||||
publicKey);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Verified);
|
||||
Assert.Contains("Invalid checkpoint format", result.FailureReason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for Rekor inclusion proof verification.
|
||||
/// SPRINT_3000_0001_0001 - T10: Integration tests with mock Rekor responses
|
||||
/// </summary>
|
||||
public sealed class RekorInclusionVerificationIntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Golden test fixture: a valid inclusion proof from Rekor production.
|
||||
/// This is a simplified representation of a real Rekor entry.
|
||||
/// </summary>
|
||||
private static readonly MockRekorEntry ValidEntry = new()
|
||||
{
|
||||
LogIndex = 12345678,
|
||||
TreeSize = 20000000,
|
||||
LeafHash = Convert.FromBase64String("n4bQgYhMfWWaL-qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="),
|
||||
ProofHashes =
|
||||
[
|
||||
Convert.FromBase64String("1B2M2Y8AsgTpgAmY7PhCfg=="),
|
||||
Convert.FromBase64String("47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU="),
|
||||
Convert.FromBase64String("fRjPxJ7P6CcH_HiMzOZz3rkbwsC4HbTYP8Qe7L9j1Po="),
|
||||
],
|
||||
RootHash = Convert.FromBase64String("rMj3G9LfM9C6Xt0qpV3pHbM2q5lPvKjS0mOmV8jXwAk="),
|
||||
Checkpoint = """
|
||||
rekor.sigstore.dev - 2605736670972794746
|
||||
20000000
|
||||
rMj3G9LfM9C6Xt0qpV3pHbM2q5lPvKjS0mOmV8jXwAk=
|
||||
1702345678
|
||||
""",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_SingleLeafTree_Succeeds()
|
||||
{
|
||||
// Arrange - single leaf tree (tree size = 1)
|
||||
var leafHash = new byte[32];
|
||||
Random.Shared.NextBytes(leafHash);
|
||||
|
||||
// Act
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
leafHash,
|
||||
leafIndex: 0,
|
||||
treeSize: 1,
|
||||
proofHashes: [],
|
||||
expectedRootHash: leafHash); // Root equals leaf for single node
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_LeftLeaf_Succeeds()
|
||||
{
|
||||
// Arrange - two-leaf tree, verify left leaf
|
||||
var leftLeaf = new byte[32];
|
||||
var rightLeaf = new byte[32];
|
||||
Random.Shared.NextBytes(leftLeaf);
|
||||
Random.Shared.NextBytes(rightLeaf);
|
||||
|
||||
// Compute expected root
|
||||
var expectedRoot = ComputeInteriorHash(leftLeaf, rightLeaf);
|
||||
|
||||
// Act - verify left leaf (index 0)
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
leftLeaf,
|
||||
leafIndex: 0,
|
||||
treeSize: 2,
|
||||
proofHashes: [rightLeaf],
|
||||
expectedRootHash: expectedRoot);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_RightLeaf_Succeeds()
|
||||
{
|
||||
// Arrange - two-leaf tree, verify right leaf
|
||||
var leftLeaf = new byte[32];
|
||||
var rightLeaf = new byte[32];
|
||||
Random.Shared.NextBytes(leftLeaf);
|
||||
Random.Shared.NextBytes(rightLeaf);
|
||||
|
||||
// Compute expected root
|
||||
var expectedRoot = ComputeInteriorHash(leftLeaf, rightLeaf);
|
||||
|
||||
// Act - verify right leaf (index 1)
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
rightLeaf,
|
||||
leafIndex: 1,
|
||||
treeSize: 2,
|
||||
proofHashes: [leftLeaf],
|
||||
expectedRootHash: expectedRoot);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_FourLeafTree_AllPositions_Succeed()
|
||||
{
|
||||
// Arrange - four-leaf balanced tree
|
||||
var leaves = new byte[4][];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
leaves[i] = new byte[32];
|
||||
Random.Shared.NextBytes(leaves[i]);
|
||||
}
|
||||
|
||||
// Build tree:
|
||||
// root
|
||||
// / \
|
||||
// h01 h23
|
||||
// / \ / \
|
||||
// L0 L1 L2 L3
|
||||
var h01 = ComputeInteriorHash(leaves[0], leaves[1]);
|
||||
var h23 = ComputeInteriorHash(leaves[2], leaves[3]);
|
||||
var root = ComputeInteriorHash(h01, h23);
|
||||
|
||||
// Test each leaf position
|
||||
var testCases = new (int index, byte[][] proof)[]
|
||||
{
|
||||
(0, [leaves[1], h23]), // L0: sibling is L1, then h23
|
||||
(1, [leaves[0], h23]), // L1: sibling is L0, then h23
|
||||
(2, [leaves[3], h01]), // L2: sibling is L3, then h01
|
||||
(3, [leaves[2], h01]), // L3: sibling is L2, then h01
|
||||
};
|
||||
|
||||
foreach (var (index, proof) in testCases)
|
||||
{
|
||||
// Act
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
leaves[index],
|
||||
leafIndex: index,
|
||||
treeSize: 4,
|
||||
proofHashes: proof,
|
||||
expectedRootHash: root);
|
||||
|
||||
// Assert
|
||||
Assert.True(result, $"Verification failed for leaf index {index}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_WrongLeafHash_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var correctLeaf = new byte[32];
|
||||
var wrongLeaf = new byte[32];
|
||||
var sibling = new byte[32];
|
||||
Random.Shared.NextBytes(correctLeaf);
|
||||
Random.Shared.NextBytes(wrongLeaf);
|
||||
Random.Shared.NextBytes(sibling);
|
||||
|
||||
var root = ComputeInteriorHash(correctLeaf, sibling);
|
||||
|
||||
// Act - try to verify with wrong leaf
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
wrongLeaf,
|
||||
leafIndex: 0,
|
||||
treeSize: 2,
|
||||
proofHashes: [sibling],
|
||||
expectedRootHash: root);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_WrongRootHash_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var leaf = new byte[32];
|
||||
var sibling = new byte[32];
|
||||
var wrongRoot = new byte[32];
|
||||
Random.Shared.NextBytes(leaf);
|
||||
Random.Shared.NextBytes(sibling);
|
||||
Random.Shared.NextBytes(wrongRoot);
|
||||
|
||||
// Act
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
leaf,
|
||||
leafIndex: 0,
|
||||
treeSize: 2,
|
||||
proofHashes: [sibling],
|
||||
expectedRootHash: wrongRoot);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_InvalidLeafIndex_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var leaf = new byte[32];
|
||||
Random.Shared.NextBytes(leaf);
|
||||
|
||||
// Act - index >= tree size
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
leaf,
|
||||
leafIndex: 5,
|
||||
treeSize: 4,
|
||||
proofHashes: [],
|
||||
expectedRootHash: leaf);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_NegativeLeafIndex_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var leaf = new byte[32];
|
||||
Random.Shared.NextBytes(leaf);
|
||||
|
||||
// Act
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
leaf,
|
||||
leafIndex: -1,
|
||||
treeSize: 4,
|
||||
proofHashes: [],
|
||||
expectedRootHash: leaf);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_ZeroTreeSize_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var leaf = new byte[32];
|
||||
Random.Shared.NextBytes(leaf);
|
||||
|
||||
// Act
|
||||
var result = MerkleProofVerifier.VerifyInclusion(
|
||||
leaf,
|
||||
leafIndex: 0,
|
||||
treeSize: 0,
|
||||
proofHashes: [],
|
||||
expectedRootHash: leaf);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_EmptyProof_SingleLeaf_ReturnsLeafHash()
|
||||
{
|
||||
// Arrange
|
||||
var leaf = new byte[32];
|
||||
Random.Shared.NextBytes(leaf);
|
||||
|
||||
// Act
|
||||
var result = MerkleProofVerifier.ComputeRootFromPath(
|
||||
leaf,
|
||||
leafIndex: 0,
|
||||
treeSize: 1,
|
||||
proofHashes: []);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(leaf, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_EmptyProof_MultiLeaf_ReturnsNull()
|
||||
{
|
||||
// Arrange - empty proof for multi-leaf tree is invalid
|
||||
var leaf = new byte[32];
|
||||
Random.Shared.NextBytes(leaf);
|
||||
|
||||
// Act
|
||||
var result = MerkleProofVerifier.ComputeRootFromPath(
|
||||
leaf,
|
||||
leafIndex: 0,
|
||||
treeSize: 4,
|
||||
proofHashes: []);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes an interior node hash per RFC 6962.
|
||||
/// H(0x01 || left || right)
|
||||
/// </summary>
|
||||
private static byte[] ComputeInteriorHash(byte[] left, byte[] right)
|
||||
{
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var combined = new byte[1 + left.Length + right.Length];
|
||||
combined[0] = 0x01; // Interior node prefix
|
||||
left.CopyTo(combined, 1);
|
||||
right.CopyTo(combined, 1 + left.Length);
|
||||
return sha256.ComputeHash(combined);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock Rekor entry for testing.
|
||||
/// </summary>
|
||||
private sealed class MockRekorEntry
|
||||
{
|
||||
public long LogIndex { get; init; }
|
||||
public long TreeSize { get; init; }
|
||||
public byte[] LeafHash { get; init; } = [];
|
||||
public byte[][] ProofHashes { get; init; } = [];
|
||||
public byte[] RootHash { get; init; } = [];
|
||||
public string Checkpoint { get; init; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public class TimeSkewValidatorTests
|
||||
{
|
||||
private readonly TimeSkewOptions _defaultOptions = new()
|
||||
{
|
||||
Enabled = true,
|
||||
WarnThresholdSeconds = 60,
|
||||
RejectThresholdSeconds = 300,
|
||||
MaxFutureSkewSeconds = 60,
|
||||
FailOnReject = true
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenDisabled_ReturnsSkipped()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TimeSkewOptions { Enabled = false };
|
||||
var validator = new TimeSkewValidator(options);
|
||||
var integratedTime = DateTimeOffset.UtcNow.AddSeconds(-10);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(TimeSkewStatus.Skipped, result.Status);
|
||||
Assert.Contains("disabled", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenNoIntegratedTime_ReturnsSkipped()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime: null);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(TimeSkewStatus.Skipped, result.Status);
|
||||
Assert.Contains("No integrated time", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)] // No skew
|
||||
[InlineData(5)] // 5 seconds ago
|
||||
[InlineData(30)] // 30 seconds ago
|
||||
[InlineData(59)] // Just under warn threshold
|
||||
public void Validate_WhenSkewBelowWarnThreshold_ReturnsOk(int secondsAgo)
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
var localTime = DateTimeOffset.UtcNow;
|
||||
var integratedTime = localTime.AddSeconds(-secondsAgo);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime, localTime);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(TimeSkewStatus.Ok, result.Status);
|
||||
Assert.InRange(result.SkewSeconds, secondsAgo - 1, secondsAgo + 1);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(60)] // At warn threshold
|
||||
[InlineData(120)] // 2 minutes
|
||||
[InlineData(299)] // Just under reject threshold
|
||||
public void Validate_WhenSkewBetweenWarnAndReject_ReturnsWarning(int secondsAgo)
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
var localTime = DateTimeOffset.UtcNow;
|
||||
var integratedTime = localTime.AddSeconds(-secondsAgo);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime, localTime);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid); // Warning still passes
|
||||
Assert.Equal(TimeSkewStatus.Warning, result.Status);
|
||||
Assert.Contains("warning threshold", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(300)] // At reject threshold
|
||||
[InlineData(600)] // 10 minutes
|
||||
[InlineData(3600)] // 1 hour
|
||||
public void Validate_WhenSkewExceedsRejectThreshold_ReturnsRejected(int secondsAgo)
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
var localTime = DateTimeOffset.UtcNow;
|
||||
var integratedTime = localTime.AddSeconds(-secondsAgo);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime, localTime);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(TimeSkewStatus.Rejected, result.Status);
|
||||
Assert.Contains("rejection threshold", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5)] // 5 seconds in future (OK)
|
||||
[InlineData(30)] // 30 seconds in future (OK)
|
||||
[InlineData(60)] // At max future threshold (OK)
|
||||
public void Validate_WhenSmallFutureSkew_ReturnsOk(int secondsInFuture)
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
var localTime = DateTimeOffset.UtcNow;
|
||||
var integratedTime = localTime.AddSeconds(secondsInFuture);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime, localTime);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(TimeSkewStatus.Ok, result.Status);
|
||||
Assert.True(result.SkewSeconds < 0); // Negative means future
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(61)] // Just over max future
|
||||
[InlineData(120)] // 2 minutes in future
|
||||
[InlineData(3600)] // 1 hour in future
|
||||
public void Validate_WhenLargeFutureSkew_ReturnsFutureTimestamp(int secondsInFuture)
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
var localTime = DateTimeOffset.UtcNow;
|
||||
var integratedTime = localTime.AddSeconds(secondsInFuture);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime, localTime);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(TimeSkewStatus.FutureTimestamp, result.Status);
|
||||
Assert.Contains("Future timestamp", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_UsesCurrentTimeWhenLocalTimeNotProvided()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
var integratedTime = DateTimeOffset.UtcNow.AddSeconds(-10);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.InRange(result.SkewSeconds, 9, 12); // Allow for test execution time
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CustomThresholds_AreRespected()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TimeSkewOptions
|
||||
{
|
||||
Enabled = true,
|
||||
WarnThresholdSeconds = 10,
|
||||
RejectThresholdSeconds = 30,
|
||||
MaxFutureSkewSeconds = 5
|
||||
};
|
||||
var validator = new TimeSkewValidator(options);
|
||||
var localTime = DateTimeOffset.UtcNow;
|
||||
|
||||
// Act - 15 seconds should warn with custom thresholds
|
||||
var result = validator.Validate(localTime.AddSeconds(-15), localTime);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(TimeSkewStatus.Warning, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReturnsCorrectTimestamps()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new TimeSkewValidator(_defaultOptions);
|
||||
var localTime = new DateTimeOffset(2025, 12, 16, 12, 0, 0, TimeSpan.Zero);
|
||||
var integratedTime = new DateTimeOffset(2025, 12, 16, 11, 59, 30, TimeSpan.Zero);
|
||||
|
||||
// Act
|
||||
var result = validator.Validate(integratedTime, localTime);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(integratedTime, result.IntegratedTime);
|
||||
Assert.Equal(localTime, result.LocalTime);
|
||||
Assert.Equal(30, result.SkewSeconds, precision: 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsOnNullOptions()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new TimeSkewValidator(null!));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user