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

@@ -5,6 +5,7 @@
// -----------------------------------------------------------------------------
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using FluentAssertions;
using StellaOps.AuditPack.Services;
@@ -25,7 +26,10 @@ public class AuditPackExportServiceIntegrationTests
public AuditPackExportServiceIntegrationTests()
{
var mockWriter = new MockAuditBundleWriter();
_service = new AuditPackExportService(mockWriter);
var repository = new FakeAuditPackRepository();
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero));
var dsseSigner = new FakeDsseSigner();
_service = new AuditPackExportService(mockWriter, repository, timeProvider, dsseSigner);
}
#region ZIP Export Tests
@@ -94,8 +98,7 @@ public class AuditPackExportServiceIntegrationTests
using var memoryStream = new MemoryStream(result.Data!);
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read);
// Note: Attestations entry may be empty without repository
archive.Entries.Should().Contain(e => e.FullName.Contains("manifest.json"));
archive.GetEntry("attestations/attestations.json").Should().NotBeNull();
}
[Fact(DisplayName = "ZIP export includes proof chain when requested")]
@@ -117,6 +120,10 @@ public class AuditPackExportServiceIntegrationTests
// Assert
result.Success.Should().BeTrue();
using var memoryStream = new MemoryStream(result.Data!);
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read);
archive.GetEntry("proof/proof-chain.json").Should().NotBeNull();
}
[Fact(DisplayName = "ZIP manifest contains export metadata")]
@@ -202,11 +209,10 @@ public class AuditPackExportServiceIntegrationTests
{
// Arrange
var request = CreateTestRequest(ExportFormat.Json);
var beforeExport = DateTimeOffset.UtcNow;
var expected = new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero);
// Act
var result = await _service.ExportAsync(request);
var afterExport = DateTimeOffset.UtcNow;
// Assert
result.Success.Should().BeTrue();
@@ -214,8 +220,7 @@ public class AuditPackExportServiceIntegrationTests
var doc = JsonDocument.Parse(result.Data!);
var exportedAt = DateTimeOffset.Parse(doc.RootElement.GetProperty("exportedAt").GetString()!);
exportedAt.Should().BeOnOrAfter(beforeExport);
exportedAt.Should().BeOnOrBefore(afterExport);
exportedAt.Should().Be(expected);
}
#endregion
@@ -409,3 +414,39 @@ internal class MockAuditBundleWriter : Services.IAuditBundleWriter
});
}
}
internal sealed class FakeAuditPackRepository : IAuditPackRepository
{
public Task<byte[]?> GetSegmentDataAsync(string scanId, ExportSegment segment, CancellationToken ct)
{
var payload = new Dictionary<string, object>
{
["segment"] = segment.ToString(),
["scanId"] = scanId
};
return Task.FromResult<byte[]?>(JsonSerializer.SerializeToUtf8Bytes(payload));
}
public Task<IReadOnlyList<object>> GetAttestationsAsync(string scanId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<object>>(new[] { new { attestationId = "att-1", scanId } });
public Task<object?> GetProofChainAsync(string scanId, CancellationToken ct)
=> Task.FromResult<object?>(new { proof = "chain", scanId });
}
internal sealed class FixedTimeProvider(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
internal sealed class FakeDsseSigner : IAuditPackExportSigner
{
public Task<DsseSignature> SignAsync(byte[] payload, CancellationToken ct)
{
return Task.FromResult(new DsseSignature
{
KeyId = "test-key",
Sig = Convert.ToBase64String(payload.Take(4).ToArray())
});
}
}

View File

@@ -0,0 +1,100 @@
using System.Collections.Immutable;
using System.IO.Compression;
using System.Text.Json;
using FluentAssertions;
using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
namespace StellaOps.AuditPack.Tests;
[Trait("Category", "Unit")]
public sealed class AuditPackImporterTests
{
[Fact]
public async Task ImportAsync_DeletesTempDirectory_WhenKeepExtractedIsFalse()
{
var archivePath = CreateArchiveWithManifest();
var importer = new AuditPackImporter(new GuidAuditPackIdGenerator());
var result = await importer.ImportAsync(archivePath, new ImportOptions { KeepExtracted = false });
result.Success.Should().BeTrue();
result.ExtractDirectory.Should().BeNull();
}
[Fact]
public async Task ImportAsync_FailsOnPathTraversalEntries()
{
var archivePath = CreateArchiveWithEntries(
new ArchivePayload("manifest.json", CreateManifestBytes()),
new ArchivePayload("../evil.txt", new byte[] { 1, 2, 3 }));
var importer = new AuditPackImporter(new GuidAuditPackIdGenerator());
var result = await importer.ImportAsync(archivePath, new ImportOptions());
result.Success.Should().BeFalse();
result.Errors.Should().NotBeNull();
}
[Fact]
public async Task ImportAsync_FailsWhenSignaturePresentWithoutTrustRoots()
{
var archivePath = CreateArchiveWithEntries(
new ArchivePayload("manifest.json", CreateManifestBytes()),
new ArchivePayload("manifest.sig", new byte[] { 1, 2, 3 }));
var importer = new AuditPackImporter(new GuidAuditPackIdGenerator());
var result = await importer.ImportAsync(archivePath, new ImportOptions { VerifySignatures = true });
result.Success.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("Signature verification failed", StringComparison.Ordinal));
}
private static string CreateArchiveWithManifest()
=> CreateArchiveWithEntries(new ArchivePayload("manifest.json", CreateManifestBytes()));
private static string CreateArchiveWithEntries(params ArchivePayload[] payloads)
{
var outputPath = Path.Combine(Path.GetTempPath(), $"audit-pack-test-{Guid.NewGuid():N}.tar.gz");
using (var fileStream = File.Create(outputPath))
using (var gzip = new GZipStream(fileStream, CompressionLevel.Optimal, leaveOpen: false))
using (var tarWriter = new System.Formats.Tar.TarWriter(gzip, System.Formats.Tar.TarEntryFormat.Pax, leaveOpen: false))
{
foreach (var payload in payloads)
{
var entry = new System.Formats.Tar.PaxTarEntry(System.Formats.Tar.TarEntryType.RegularFile, payload.Path)
{
DataStream = new MemoryStream(payload.Content, writable: false)
};
tarWriter.WriteEntry(entry);
}
}
return outputPath;
}
private static byte[] CreateManifestBytes()
{
var pack = new StellaOps.AuditPack.Models.AuditPack
{
PackId = "pack-1",
Name = "pack",
CreatedAt = DateTimeOffset.UnixEpoch,
RunManifest = new RunManifest("scan-1", DateTimeOffset.UnixEpoch),
EvidenceIndex = new EvidenceIndex(Array.Empty<string>().ToImmutableArray()),
Verdict = new Verdict("verdict-1", "completed"),
OfflineBundle = new BundleManifest("bundle-1", "1.0"),
Contents = new PackContents
{
Files = Array.Empty<PackFile>().ToImmutableArray(),
TotalSizeBytes = 0,
FileCount = 0
}
};
return JsonSerializer.SerializeToUtf8Bytes(pack);
}
private sealed record ArchivePayload(string Path, byte[] Content);
}

View File

@@ -0,0 +1,130 @@
using System.Text.Json;
using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
namespace StellaOps.AuditPack.Tests;
[Trait("Category", "Unit")]
public sealed class ReplayAttestationServiceTests
{
[Fact]
public async Task VerifyAsync_Fails_WhenEnvelopeHasNoSignatures()
{
var service = new ReplayAttestationService(timeProvider: new FixedTimeProvider(DateTimeOffset.UnixEpoch));
var attestation = await service.GenerateAsync(
new AuditBundleManifest
{
BundleId = "bundle-1",
Name = "bundle",
CreatedAt = DateTimeOffset.UnixEpoch,
ScanId = "scan-1",
ImageRef = "image",
ImageDigest = "sha256:abc",
MerkleRoot = "sha256:root",
Inputs = new InputDigests
{
SbomDigest = "sha256:sbom",
FeedsDigest = "sha256:feeds",
PolicyDigest = "sha256:policy"
},
VerdictDigest = "sha256:verdict",
Decision = "pass",
Files = [],
TotalSizeBytes = 0
},
new ReplayExecutionResult
{
Success = true,
Status = ReplayStatus.Match,
InputsVerified = true,
VerdictMatches = true,
DecisionMatches = true,
OriginalVerdictDigest = "sha256:verdict",
ReplayedVerdictDigest = "sha256:verdict",
OriginalDecision = "pass",
ReplayedDecision = "pass",
Drifts = [],
Errors = [],
DurationMs = 0,
EvaluatedAt = DateTimeOffset.UnixEpoch
});
var result = await service.VerifyAsync(attestation);
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Contains("signatures", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task VerifyAsync_Succeeds_WithVerifierAndValidPayload()
{
var verifier = new AcceptAllVerifier();
var service = new ReplayAttestationService(verifier: verifier);
var payload = CanonicalJson.Serialize(new InTotoStatement
{
Type = "https://in-toto.io/Statement/v1",
Subject = [new InTotoSubject
{
Name = "verdict:bundle-1",
Digest = new Dictionary<string, string> { ["sha256"] = "abc" }
}],
PredicateType = "https://stellaops.io/attestation/verdict-replay/v1",
Predicate = new VerdictReplayAttestation
{
ManifestId = "bundle-1",
ScanId = "scan-1",
ImageRef = "image",
ImageDigest = "sha256:abc",
InputsDigest = "sha256:inputs",
OriginalVerdictDigest = "sha256:verdict",
OriginalDecision = "pass",
Match = true,
Status = "Match",
DriftCount = 0,
EvaluatedAt = DateTimeOffset.UnixEpoch,
ReplayedAt = DateTimeOffset.UnixEpoch,
DurationMs = 0
}
});
var attestation = new ReplayAttestation
{
AttestationId = "att-1",
ManifestId = "bundle-1",
CreatedAt = DateTimeOffset.UnixEpoch,
Statement = JsonSerializer.Deserialize<InTotoStatement>(payload)!,
StatementDigest = "sha256:" + Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(payload)).ToLowerInvariant(),
Envelope = new ReplayDsseEnvelope
{
PayloadType = "application/vnd.in-toto+json",
Payload = Convert.ToBase64String(payload),
Signatures = [new ReplayDsseSignature { KeyId = "key", Sig = "sig" }]
},
Match = true,
ReplayStatus = "Match"
};
var result = await service.VerifyAsync(attestation);
Assert.True(result.IsValid);
Assert.True(result.SignatureVerified);
}
private sealed class AcceptAllVerifier : IReplayAttestationSignatureVerifier
{
public Task<ReplayAttestationSignatureVerification> VerifyAsync(
ReplayDsseEnvelope envelope,
byte[] payload,
CancellationToken cancellationToken = default)
{
return Task.FromResult(new ReplayAttestationSignatureVerification { Verified = true });
}
}
private sealed class FixedTimeProvider(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
}

View File

@@ -0,0 +1,22 @@
# Auth Security Tests AGENTS
## Purpose & Scope
- Working directory: `src/__Libraries/__Tests/StellaOps.Auth.Security.Tests/`.
- Roles: QA automation, backend engineer.
- Focus: DPoP proof validation, nonce/replay caches, and edge-case coverage.
## Required Reading (treat as read before DOING)
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/authority/architecture.md`
- Relevant sprint files.
## Working Agreements
- Keep tests deterministic (fixed time/IDs, stable ordering).
- Avoid live network calls and nondeterministic RNG.
- Update `docs/implplan/SPRINT_*.md` and local `TASKS.md` when starting or completing work.
## Testing
- Use xUnit + FluentAssertions + TestKit.
- Cover validator error paths, nonce stores, and replay cache semantics.

View File

@@ -0,0 +1,17 @@
using StellaOps.Auth.Security.Dpop;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Auth.Security.Tests;
public class DpopNonceUtilitiesTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeStorageKey_NormalizesToLowerInvariant()
{
var key = DpopNonceUtilities.ComputeStorageKey("API", "Client-Id", "ThumbPrint");
Assert.Equal("dpop-nonce:api:client-id:thumbprint", key);
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Security.Dpop;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Auth.Security.Tests;
public class DpopProofValidatorTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_ReturnsFailure_ForNonStringTyp()
{
var proof = BuildUnsignedToken(
new { typ = 123, alg = "ES256" },
new { htm = "GET", htu = "https://api.test/resource", iat = 0, jti = "1" });
var validator = CreateValidator();
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);
Assert.Equal("invalid_header", result.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_ReturnsFailure_ForNonStringAlg()
{
var proof = BuildUnsignedToken(
new { typ = "dpop+jwt", alg = 55 },
new { htm = "GET", htu = "https://api.test/resource", iat = 0, jti = "1" });
var validator = CreateValidator();
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);
Assert.Equal("invalid_header", result.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_ReturnsFailure_ForNonStringHtm()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var (proof, _) = CreateSignedProof(now, payloadMutator: payload => payload["htm"] = 123);
var validator = CreateValidator(now);
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);
Assert.Equal("invalid_payload", result.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_ReturnsFailure_ForNonStringHtu()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var (proof, _) = CreateSignedProof(now, payloadMutator: payload => payload["htu"] = 123);
var validator = CreateValidator(now);
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);
Assert.Equal("invalid_payload", result.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_ReturnsFailure_ForNonStringNonce()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var (proof, _) = CreateSignedProof(now, payloadMutator: payload => payload["nonce"] = 999);
var validator = CreateValidator(now);
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"), nonce: "nonce-1");
Assert.False(result.IsValid);
Assert.Equal("invalid_token", result.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_RejectsProofIssuedInFuture()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var issuedAt = now.AddMinutes(2);
var (proof, _) = CreateSignedProof(issuedAt);
var validator = CreateValidator(now, options => options.AllowedClockSkew = TimeSpan.FromSeconds(5));
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);
Assert.Equal("invalid_token", result.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_RejectsExpiredProofs()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var issuedAt = now.AddMinutes(-10);
var (proof, _) = CreateSignedProof(issuedAt);
var validator = CreateValidator(now, options =>
{
options.ProofLifetime = TimeSpan.FromMinutes(1);
options.AllowedClockSkew = TimeSpan.FromSeconds(5);
});
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);
Assert.Equal("invalid_token", result.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_RejectsReplayTokens()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var jwtId = "jwt-1";
var (proof, _) = CreateSignedProof(now, jti: jwtId);
var timeProvider = new FakeTimeProvider(now);
var replayCache = new InMemoryDpopReplayCache(timeProvider);
var validator = CreateValidator(timeProvider, replayCache);
var first = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
var second = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.True(first.IsValid);
Assert.False(second.IsValid);
Assert.Equal("replay", second.ErrorCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_UsesSnapshotOfOptions()
{
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var (proof, _) = CreateSignedProof(now);
var options = new DpopValidationOptions();
var timeProvider = new FakeTimeProvider(now);
var validator = new DpopProofValidator(Options.Create(options), new InMemoryDpopReplayCache(timeProvider), timeProvider);
options.AllowedAlgorithms.Clear();
options.AllowedAlgorithms.Add("ES512");
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.True(result.IsValid);
}
private static DpopProofValidator CreateValidator(DateTimeOffset now, Action<DpopValidationOptions>? configure = null)
{
var timeProvider = new FakeTimeProvider(now);
return CreateValidator(timeProvider, null, configure);
}
private static DpopProofValidator CreateValidator(TimeProvider timeProvider, IDpopReplayCache? replayCache = null, Action<DpopValidationOptions>? configure = null)
{
var options = new DpopValidationOptions();
configure?.Invoke(options);
return new DpopProofValidator(Options.Create(options), replayCache, timeProvider);
}
private static (string Token, JsonWebKey Jwk) CreateSignedProof(
DateTimeOffset issuedAt,
string? method = "GET",
Uri? httpUri = null,
string? nonce = null,
string? jti = null,
Action<JwtHeader>? headerMutator = null,
Action<JwtPayload>? payloadMutator = null)
{
httpUri ??= new Uri("https://api.test/resource");
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var securityKey = new ECDsaSecurityKey(ecdsa) { KeyId = Guid.NewGuid().ToString("N") };
var jwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(securityKey);
var header = new JwtHeader(new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256))
{
{ "typ", "dpop+jwt" },
{ "jwk", jwk }
};
headerMutator?.Invoke(header);
var payload = new JwtPayload
{
{ "htm", method ?? "GET" },
{ "htu", httpUri.ToString() },
{ "iat", issuedAt.ToUnixTimeSeconds() },
{ "jti", jti ?? Guid.NewGuid().ToString("N") }
};
if (nonce is not null)
{
payload["nonce"] = nonce;
}
payloadMutator?.Invoke(payload);
var token = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
return (handler.WriteToken(token), jwk);
}
private static string BuildUnsignedToken(object header, object payload)
{
var headerJson = JsonSerializer.Serialize(header);
var payloadJson = JsonSerializer.Serialize(payload);
var encodedHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(headerJson));
var encodedPayload = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(payloadJson));
return $"{encodedHeader}.{encodedPayload}.signature";
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Auth.Security.Dpop;
using StellaOps.Messaging;
using StellaOps.Messaging.Abstractions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Auth.Security.Tests;
public class DpopReplayCacheTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task InMemoryReplayCache_RejectsDuplicatesUntilExpiry()
{
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
var cache = new InMemoryDpopReplayCache(timeProvider);
var expiresAt = timeProvider.GetUtcNow().AddMinutes(1);
Assert.True(await cache.TryStoreAsync("jti-1", expiresAt));
Assert.False(await cache.TryStoreAsync("jti-1", expiresAt));
timeProvider.Advance(TimeSpan.FromMinutes(2));
Assert.True(await cache.TryStoreAsync("jti-1", timeProvider.GetUtcNow().AddMinutes(1)));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MessagingReplayCache_RejectsDuplicatesUntilExpiry()
{
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
var factory = new FakeIdempotencyStoreFactory(timeProvider);
var cache = new MessagingDpopReplayCache(factory, timeProvider);
var expiresAt = timeProvider.GetUtcNow().AddMinutes(1);
Assert.True(await cache.TryStoreAsync("jti-1", expiresAt));
Assert.False(await cache.TryStoreAsync("jti-1", expiresAt));
timeProvider.Advance(TimeSpan.FromMinutes(2));
Assert.True(await cache.TryStoreAsync("jti-1", timeProvider.GetUtcNow().AddMinutes(1)));
}
private sealed class FakeIdempotencyStoreFactory : IIdempotencyStoreFactory
{
private readonly FakeIdempotencyStore store;
public FakeIdempotencyStoreFactory(TimeProvider timeProvider)
{
store = new FakeIdempotencyStore(timeProvider);
}
public string ProviderName => "fake";
public IIdempotencyStore Create(string name) => store;
}
private sealed class FakeIdempotencyStore : IIdempotencyStore
{
private readonly Dictionary<string, Entry> entries = new(StringComparer.Ordinal);
private readonly TimeProvider timeProvider;
public FakeIdempotencyStore(TimeProvider timeProvider)
{
this.timeProvider = timeProvider;
}
public string ProviderName => "fake";
public ValueTask<IdempotencyResult> TryClaimAsync(string key, string value, TimeSpan window, CancellationToken cancellationToken = default)
{
var now = timeProvider.GetUtcNow();
if (entries.TryGetValue(key, out var entry) && entry.ExpiresAt > now)
{
return ValueTask.FromResult(IdempotencyResult.Duplicate(entry.Value));
}
entries[key] = new Entry(value, now.Add(window));
return ValueTask.FromResult(IdempotencyResult.Claimed());
}
public ValueTask<bool> ExistsAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(entries.TryGetValue(key, out var entry) && entry.ExpiresAt > timeProvider.GetUtcNow());
public ValueTask<string?> GetAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(entries.TryGetValue(key, out var entry) && entry.ExpiresAt > timeProvider.GetUtcNow() ? entry.Value : null);
public ValueTask<bool> ReleaseAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(entries.Remove(key));
public ValueTask<bool> ExtendAsync(string key, TimeSpan extension, CancellationToken cancellationToken = default)
{
if (!entries.TryGetValue(key, out var entry))
{
return ValueTask.FromResult(false);
}
entries[key] = entry with { ExpiresAt = entry.ExpiresAt.Add(extension) };
return ValueTask.FromResult(true);
}
private readonly record struct Entry(string Value, DateTimeOffset ExpiresAt);
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Auth.Security/StellaOps.Auth.Security.csproj" />
<ProjectReference Include="../../StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
# Auth Security Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0082-A | DONE | Test coverage for DPoP validation, nonce stores, and replay cache. |

View File

@@ -0,0 +1,26 @@
# Infrastructure Postgres Tests Agent Charter
## Mission
- Validate Infrastructure.Postgres migration and fixture behaviors with deterministic integration tests.
## Responsibilities
- Maintain Testcontainers-based coverage for migrations and fixtures.
- Keep tests categorized for CI selection and handle Docker availability gracefully.
- Ensure test data and schema naming remain deterministic and cleaned up.
## Required Reading
- src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md
- src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/AGENTS.md
- docs/modules/platform/architecture-overview.md
## Working Directory & Scope
- Primary: src/__Libraries/__Tests/StellaOps.Infrastructure.Postgres.Tests
- Allowed shared libs/tests: src/__Libraries/StellaOps.Infrastructure.Postgres, src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing
## Testing Expectations
- Integration tests must be tagged as Integration and isolated per schema.
- Skip or gate tests when Docker/Testcontainers is unavailable.
## Working Agreement
- Update sprint status in docs/implplan/SPRINT_*.md and local TASKS.md.
- Keep test artifacts deterministic and clean up schemas after runs.

View File

@@ -0,0 +1,10 @@
# StellaOps.Infrastructure.Postgres.Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0360-M | DONE | Maintainability audit for Infrastructure.Postgres.Tests. |
| AUDIT-0360-T | DONE | Test coverage audit for Infrastructure.Postgres.Tests. |
| AUDIT-0360-A | DONE | Waived (test project). |