216 lines
7.5 KiB
C#
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;
|
|
}
|
|
}
|