save progress
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken.Tests;
|
||||
|
||||
public sealed class DecisionReplayTokenExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateForDecision_MatchesManualRequest()
|
||||
{
|
||||
var generator = CreateGenerator();
|
||||
|
||||
var token = generator.GenerateForDecision(
|
||||
alertId: "alert-1",
|
||||
actorId: "actor-9",
|
||||
decisionStatus: "approved",
|
||||
evidenceHashes: new[] { "sha256:evidence1" },
|
||||
policyContext: "ctx",
|
||||
rulesVersion: "rules-v1");
|
||||
|
||||
var request = new ReplayTokenRequest
|
||||
{
|
||||
InputHashes = new[] { "alert-1" },
|
||||
EvidenceHashes = new[] { "sha256:evidence1" },
|
||||
RulesVersion = "rules-v1",
|
||||
AdditionalContext = new Dictionary<string, string>
|
||||
{
|
||||
["actor_id"] = "actor-9",
|
||||
["decision_status"] = "approved",
|
||||
["policy_context"] = "ctx"
|
||||
}
|
||||
};
|
||||
|
||||
var expected = generator.Generate(request);
|
||||
|
||||
Assert.Equal(expected.Value, token.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateForScoring_MatchesManualRequest()
|
||||
{
|
||||
var generator = CreateGenerator();
|
||||
|
||||
var token = generator.GenerateForScoring(
|
||||
subjectKey: "subject-1",
|
||||
feedManifests: new[] { "sha256:feed1", "sha256:feed2" },
|
||||
scoringConfigVersion: "score-v1",
|
||||
inputHashes: new[] { "sha256:input1" });
|
||||
|
||||
var request = new ReplayTokenRequest
|
||||
{
|
||||
FeedManifests = new[] { "sha256:feed1", "sha256:feed2" },
|
||||
ScoringConfigVersion = "score-v1",
|
||||
InputHashes = new[] { "sha256:input1" },
|
||||
AdditionalContext = new Dictionary<string, string>
|
||||
{
|
||||
["subject_key"] = "subject-1"
|
||||
}
|
||||
};
|
||||
|
||||
var expected = generator.Generate(request);
|
||||
|
||||
Assert.Equal(expected.Value, token.Value);
|
||||
}
|
||||
|
||||
private static Sha256ReplayTokenGenerator CreateGenerator()
|
||||
{
|
||||
var cryptoHash = DefaultCryptoHash.CreateForTests();
|
||||
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
return new Sha256ReplayTokenGenerator(cryptoHash, timeProvider);
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider(DateTimeOffset now) : TimeProvider
|
||||
{
|
||||
public override DateTimeOffset GetUtcNow() => now;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using FluentAssertions;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken.Tests;
|
||||
|
||||
public sealed class ReplayCliSnippetGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateDecisionReplay_QuotesValuesAndOmitsPlus()
|
||||
{
|
||||
var generator = new ReplayCliSnippetGenerator();
|
||||
var token = new ReplayToken("abc123", DateTimeOffset.UnixEpoch);
|
||||
|
||||
var output = generator.GenerateDecisionReplay(
|
||||
token,
|
||||
"alert 1",
|
||||
"file:///tmp/with space",
|
||||
"policy v1");
|
||||
|
||||
output.Should().Contain("--token 'abc123'");
|
||||
output.Should().Contain("--alert-id 'alert 1'");
|
||||
output.Should().Contain("--feed-manifest 'file:///tmp/with space'");
|
||||
output.Should().Contain("--policy-version 'policy v1'");
|
||||
output.Should().NotContain("\n+");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateScoringReplay_EscapesSingleQuotes()
|
||||
{
|
||||
var generator = new ReplayCliSnippetGenerator();
|
||||
var token = new ReplayToken("abc123", DateTimeOffset.UnixEpoch);
|
||||
|
||||
var output = generator.GenerateScoringReplay(token, "subject'key", "config'v1");
|
||||
|
||||
output.Should().Contain("--subject 'subject'\"'\"'key'");
|
||||
output.Should().Contain("--config-version 'config'\"'\"'v1'");
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,96 @@ public sealed class ReplayTokenGeneratorTests
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user