Files
git.stella-ops.org/src/__Tests/StellaOps.Audit.ReplayToken.Tests/ReplayTokenGeneratorTests.cs
StellaOps Bot 3f197814c5 save progress
2026-01-02 21:06:27 +02:00

216 lines
7.5 KiB
C#

using StellaOps.Cryptography;
using StellaOps.TestKit;
namespace StellaOps.Audit.ReplayToken.Tests;
public sealed class ReplayTokenGeneratorTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_SameInputs_ReturnsSameValue()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var fixedNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
var timeProvider = new FixedTimeProvider(fixedNow);
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var request = new ReplayTokenRequest
{
FeedManifests = new[] { "sha256:bbb", "sha256:aaa" },
RulesVersion = "rules-v1",
RulesHash = "sha256:rules",
LatticePolicyVersion = "lattice-v1",
LatticePolicyHash = "sha256:lattice",
InputHashes = new[] { "sha256:input2", "sha256:input1" },
ScoringConfigVersion = "score-v1",
EvidenceHashes = new[] { "sha256:e2", "sha256:e1" },
AdditionalContext = new Dictionary<string, string>
{
["b"] = "2",
["a"] = "1"
}
};
var token1 = generator.Generate(request);
var token2 = generator.Generate(request);
Assert.Equal(token1.Value, token2.Value);
Assert.Equal(token1.Canonical, token2.Canonical);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_IgnoresArrayOrdering()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var requestA = new ReplayTokenRequest
{
FeedManifests = new[] { "sha256:aaa", "sha256:bbb" },
InputHashes = new[] { "sha256:input1", "sha256:input2" }
};
var requestB = new ReplayTokenRequest
{
FeedManifests = new[] { "sha256:bbb", "sha256:aaa" },
InputHashes = new[] { "sha256:input2", "sha256:input1" }
};
var tokenA = generator.Generate(requestA);
var tokenB = generator.Generate(requestB);
Assert.Equal(tokenA.Value, tokenB.Value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_MatchingInputs_ReturnsTrue()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var request = new ReplayTokenRequest { InputHashes = new[] { "sha256:input" } };
var token = generator.Generate(request);
Assert.True(generator.Verify(token, request));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_DifferentInputs_ReturnsFalse()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var request = new ReplayTokenRequest { InputHashes = new[] { "sha256:input" } };
var different = new ReplayTokenRequest { InputHashes = new[] { "sha256:other" } };
var token = generator.Generate(request);
Assert.False(generator.Verify(token, different));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_IgnoresAdditionalContextOrdering()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var requestA = new ReplayTokenRequest
{
InputHashes = new[] { "sha256:input" },
AdditionalContext = new Dictionary<string, string>
{
["a"] = "1",
["b"] = "2"
}
};
var requestB = new ReplayTokenRequest
{
InputHashes = new[] { "sha256:input" },
AdditionalContext = new Dictionary<string, string>
{
["b"] = "2",
["a"] = "1"
}
};
var tokenA = generator.Generate(requestA);
var tokenB = generator.Generate(requestB);
Assert.Equal(tokenA.Value, tokenB.Value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_DuplicateAdditionalContextKeys_Throws()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var request = new ReplayTokenRequest
{
InputHashes = new[] { "sha256:input" },
AdditionalContext = new Dictionary<string, string>
{
["key"] = "1",
[" key "] = "2"
}
};
Assert.Throws<ArgumentException>(() => generator.Generate(request));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GenerateWithExpiration_UsesDistinctCanonicalVersion()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var request = new ReplayTokenRequest { InputHashes = new[] { "sha256:input" } };
var v1Token = generator.Generate(request);
var v2Token = generator.GenerateWithExpiration(request, TimeSpan.FromMinutes(5));
Assert.NotEqual(v1Token.Value, v2Token.Value);
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void GenerateWithExpiration_NonPositiveExpiration_Throws(int seconds)
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var generator = new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
var request = new ReplayTokenRequest { InputHashes = new[] { "sha256:input" } };
var expiration = TimeSpan.FromSeconds(seconds);
Assert.Throws<ArgumentOutOfRangeException>(() => generator.GenerateWithExpiration(request, expiration));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReplayToken_Parse_RoundTripsCanonical()
{
var token = new ReplayToken("0123456789abcdef", DateTimeOffset.UnixEpoch);
var parsed = ReplayToken.Parse(token.Canonical);
Assert.Equal(token.Value, parsed.Value);
Assert.Equal(token.Algorithm, parsed.Algorithm);
Assert.Equal(token.Version, parsed.Version);
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("")]
[InlineData("replay")]
[InlineData("replay:v1.0:SHA-256")]
[InlineData("other:v1.0:SHA-256:abc")]
public void ReplayToken_Parse_Invalid_Throws(string canonical)
{
Assert.ThrowsAny<Exception>(() => ReplayToken.Parse(canonical));
}
private sealed class FixedTimeProvider(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
}