save progress

This commit is contained in:
StellaOps Bot
2026-01-02 21:06:27 +02:00
parent f46bde5575
commit 3f197814c5
441 changed files with 21545 additions and 4306 deletions

View File

@@ -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;
}
}

View File

@@ -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'");
}
}

View File

@@ -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()