This commit is contained in:
master
2026-02-04 19:59:20 +02:00
parent 557feefdc3
commit 5548cf83bf
1479 changed files with 53557 additions and 40339 deletions

View File

@@ -0,0 +1,70 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using StellaOps.AuditPack.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.AuditPack.Tests;
public sealed class ArchiveUtilitiesTests : IDisposable
{
private static int _tempCounter;
private readonly string _tempDir;
public ArchiveUtilitiesTests()
{
var suffix = Interlocked.Increment(ref _tempCounter);
_tempDir = Path.Combine(Path.GetTempPath(), $"audit-archive-{suffix:0000}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractTarGzAsync_WritesFilesAsync()
{
var payload = Encoding.UTF8.GetBytes("{\"ok\":true}");
var archivePath = Path.Combine(_tempDir, "bundle.tar.gz");
var entries = new[]
{
new ArchiveEntry("manifest.json", payload)
};
await ArchiveUtilities.WriteTarGzAsync(archivePath, entries, CancellationToken.None);
var extractDir = Path.Combine(_tempDir, "extract");
await ArchiveUtilities.ExtractTarGzAsync(archivePath, extractDir, overwriteFiles: false, CancellationToken.None);
var extractedPath = Path.Combine(extractDir, "manifest.json");
Assert.True(File.Exists(extractedPath));
var actual = await File.ReadAllBytesAsync(extractedPath);
Assert.Equal(payload, actual);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractTarGzAsync_RejectsParentTraversalAsync()
{
var payload = Encoding.UTF8.GetBytes("escape");
var archivePath = Path.Combine(_tempDir, "escape.tar.gz");
var entries = new[]
{
new ArchiveEntry("../escape.txt", payload)
};
await ArchiveUtilities.WriteTarGzAsync(archivePath, entries, CancellationToken.None);
var extractDir = Path.Combine(_tempDir, "escape");
await Assert.ThrowsAsync<InvalidOperationException>(() =>
ArchiveUtilities.ExtractTarGzAsync(archivePath, extractDir, overwriteFiles: true, CancellationToken.None));
}
}

View File

@@ -13,3 +13,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0076-A | DONE | Waived (test project; revalidated 2026-01-06). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-08 | DONE | Split tests <= 100 lines; deterministic fixtures/time/IDs; async naming; replay/attestation coverage expanded; ConfigureAwait(false) skipped per xUnit1030; dotnet test passed 2026-02-02 (46 tests). |
| REMED-05 | DONE | Added ArchiveUtilities extraction tests; dotnet test passed 2026-02-04 (52 tests). |

View File

@@ -0,0 +1,70 @@
using System;
using StellaOps.Auth.Security.Dpop;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Auth.Security.Tests;
public sealed class DpopValidationOptionsTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsWhenProofLifetimeNonPositive()
{
var options = new DpopValidationOptions
{
ProofLifetime = TimeSpan.Zero
};
Assert.Throws<InvalidOperationException>(() => options.Validate());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsWhenClockSkewOutOfRange()
{
var options = new DpopValidationOptions
{
AllowedClockSkew = TimeSpan.FromMinutes(6)
};
Assert.Throws<InvalidOperationException>(() => options.Validate());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsWhenReplayWindowNegative()
{
var options = new DpopValidationOptions
{
ReplayWindow = TimeSpan.FromSeconds(-1)
};
Assert.Throws<InvalidOperationException>(() => options.Validate());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsWhenAllowedAlgorithmsEmpty()
{
var options = new DpopValidationOptions();
options.AllowedAlgorithms.Clear();
Assert.Throws<InvalidOperationException>(() => options.Validate());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_NormalizesAlgorithms()
{
var options = new DpopValidationOptions();
options.AllowedAlgorithms.Clear();
options.AllowedAlgorithms.Add(" es256 ");
options.AllowedAlgorithms.Add("Es384");
options.Validate();
Assert.Contains("ES256", options.NormalizedAlgorithms);
Assert.Contains("ES384", options.NormalizedAlgorithms);
}
}

View File

@@ -13,3 +13,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0785-A | DONE | Waived (test project; revalidated 2026-01-07). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-08 | DONE | Split tests <= 100 lines; deterministic time/IDs; async naming; helper types separated; ConfigureAwait(false) skipped per xUnit1030; dotnet test passed 2026-02-02 (12 tests). |
| REMED-05 | DONE | Added DpopValidationOptions unit coverage; dotnet test passed 2026-02-04 (20 tests). |

View File

@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.TestKit;
@@ -9,7 +11,7 @@ public sealed partial class CloudKmsClientTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void KmsCryptoProvider_Skips_NonExportable_Keys()
public void KmsCryptoProvider_Returns_VerificationOnly_Keys_When_PublicMaterial_Available()
{
using var fixture = new EcdsaFixture();
var parameters = fixture.Parameters;
@@ -28,6 +30,36 @@ public sealed partial class CloudKmsClientTests
provider.UpsertSigningKey(signingKey);
var keys = provider.GetSigningKeys();
var key = Assert.Single(keys);
Assert.Equal(signingKey.Reference.KeyId, key.Reference.KeyId);
Assert.Null(key.PrivateParameters.D);
Assert.NotNull(key.PublicParameters.Q.X);
Assert.NotNull(key.PublicParameters.Q.Y);
Assert.Equal(signingKey.Metadata["kms.version"], key.Metadata["kms.version"]);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void KmsCryptoProvider_Skips_Keys_Without_PublicMaterial()
{
using var fixture = new EcdsaFixture();
var parameters = fixture.Parameters;
var kmsClient = new NonExportingKmsClient(parameters, FixedNow);
var provider = new KmsCryptoProvider(kmsClient);
var signingKey = new CryptoSigningKey(
new CryptoKeyReference("kms-key-no-public", "kms"),
KmsAlgorithms.Es256,
new byte[32],
FixedNow,
metadata: new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["kms.version"] = "kms-key-no-public",
});
provider.UpsertSigningKey(signingKey);
var keys = provider.GetSigningKeys();
Assert.Empty(keys);
}

View File

@@ -0,0 +1,89 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using System;
using System.Linq;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class BouncyCastleKeyNormalizationTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
[Fact]
public void UpsertSigningKey_With64BytePrivateKey_NormalizesTo32Bytes()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var privateKey = Enumerable.Range(1, 64).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference("key-64", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKey,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
var stored = provider.GetSigningKeys().Single();
stored.PrivateKey.Length.Should().Be(32);
stored.PrivateKey.ToArray().Should().Equal(privateKey.Take(32).ToArray());
}
[Fact]
public void UpsertSigningKey_EmptyPublicKey_DerivesPublicKey()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var privateKey = Enumerable.Range(10, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference("key-derived-public", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKey,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
var stored = provider.GetSigningKeys().Single();
stored.PublicKey.Length.Should().Be(32);
stored.PublicKey.ToArray().Should().NotBeEmpty();
}
[Fact]
public void UpsertSigningKey_InvalidPublicKeyLength_Throws()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var privateKey = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var publicKey = new byte[31];
var keyReference = new CryptoKeyReference("key-invalid-public", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKey,
createdAt: FixedNow,
publicKey: publicKey);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*public key must be 32 bytes*");
}
[Fact]
public void UpsertSigningKey_EdDsaAlgorithm_NormalizesToEd25519()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var privateKey = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference("key-eddsa", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.EdDsa,
privateKey,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
var stored = provider.GetSigningKeys().Single();
stored.AlgorithmId.Should().Be(SignatureAlgorithms.Ed25519);
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Tests;
internal sealed class OrderedTestCryptoProvider : ICryptoProvider
{
internal const string Algorithm = "test-hash";
private readonly ICryptoHasher _hasher;
public OrderedTestCryptoProvider(string name)
{
Name = name;
_hasher = new OrderedTestHasher(Algorithm);
}
public string Name { get; }
public bool Supports(CryptoCapability capability, string algorithmId)
=> capability == CryptoCapability.ContentHashing &&
string.Equals(algorithmId, Algorithm, StringComparison.Ordinal);
public IPasswordHasher GetPasswordHasher(string algorithmId)
=> throw new NotSupportedException();
public ICryptoHasher GetHasher(string algorithmId)
=> Supports(CryptoCapability.ContentHashing, algorithmId)
? _hasher
: throw new InvalidOperationException("Unsupported hash algorithm.");
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
=> throw new NotSupportedException();
public void UpsertSigningKey(CryptoSigningKey signingKey)
=> throw new NotSupportedException();
public bool RemoveSigningKey(string keyId) => false;
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
=> Array.Empty<CryptoSigningKey>();
private sealed class OrderedTestHasher : ICryptoHasher
{
public OrderedTestHasher(string algorithmId)
{
AlgorithmId = algorithmId;
}
public string AlgorithmId { get; }
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
public string ComputeHashHex(ReadOnlySpan<byte> data) => string.Empty;
}
}

View File

@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class CryptoDependencyInjectionTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddStellaOpsCrypto_ResolvesPreferredProviderOrder()
{
var services = new ServiceCollection();
services.AddSingleton<ICryptoProvider>(new OrderedTestCryptoProvider("alpha"));
services.AddSingleton<ICryptoProvider>(new OrderedTestCryptoProvider("beta"));
services.AddStellaOpsCrypto(options =>
{
options.PreferredProviders.Add("beta");
options.PreferredProviders.Add("alpha");
});
using var provider = services.BuildServiceProvider();
var registry = provider.GetRequiredService<ICryptoProviderRegistry>();
var resolved = registry.ResolveOrThrow(CryptoCapability.ContentHashing, OrderedTestCryptoProvider.Algorithm);
Assert.Equal("beta", resolved.Name);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddStellaOpsCryptoFromConfiguration_LoadsPluginProvidersByPriority()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-crypto-tests", $"crypto-di-{Environment.ProcessId}");
Directory.CreateDirectory(tempRoot);
try
{
var assemblyPath = typeof(TestPluginAlphaProvider).Assembly.Location;
var assemblyFileName = Path.GetFileName(assemblyPath);
File.Copy(assemblyPath, Path.Combine(tempRoot, assemblyFileName), overwrite: true);
var manifestPath = Path.Combine(tempRoot, "crypto-plugins-manifest.json");
WriteManifest(manifestPath, assemblyFileName, GetCurrentPlatform());
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["StellaOps:Crypto:Plugins:ManifestPath"] = manifestPath,
["StellaOps:Crypto:Plugins:DiscoveryMode"] = "explicit",
["StellaOps:Crypto:Plugins:Enabled:0:Id"] = "test.plugin.alpha",
["StellaOps:Crypto:Plugins:Enabled:0:Priority"] = "10",
["StellaOps:Crypto:Plugins:Enabled:1:Id"] = "test.plugin.beta",
["StellaOps:Crypto:Plugins:Enabled:1:Priority"] = "90",
})
.Build();
var services = new ServiceCollection();
services.AddStellaOpsCryptoFromConfiguration(configuration, tempRoot);
using var provider = services.BuildServiceProvider();
var registry = provider.GetRequiredService<ICryptoProviderRegistry>();
var resolved = registry.ResolveOrThrow(CryptoCapability.ContentHashing, TestPluginCryptoProviderBase.TestAlgorithm);
Assert.Equal("test.plugin.beta", resolved.Name);
}
finally
{
// Cleanup is best-effort because the plugin assembly can remain locked on Windows.
try
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
private static void WriteManifest(string manifestPath, string assemblyFileName, string platform)
{
var manifest = new
{
version = "1.0",
plugins = new[]
{
new
{
id = "test.plugin.alpha",
name = "Test Plugin Alpha",
assembly = assemblyFileName,
type = typeof(TestPluginAlphaProvider).FullName!,
platforms = new[] { platform },
},
new
{
id = "test.plugin.beta",
name = "Test Plugin Beta",
assembly = assemblyFileName,
type = typeof(TestPluginBetaProvider).FullName!,
platforms = new[] { platform },
},
},
};
var json = JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(manifestPath, json);
}
private static string GetCurrentPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "windows";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "osx";
}
return "unknown";
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Tests;
public abstract class TestPluginCryptoProviderBase : ICryptoProvider
{
public const string TestAlgorithm = "plugin-hash";
private static readonly ICryptoHasher Hasher = new TestPluginHasher();
protected TestPluginCryptoProviderBase(string name)
{
Name = name;
}
public string Name { get; }
public bool Supports(CryptoCapability capability, string algorithmId)
=> capability == CryptoCapability.ContentHashing &&
string.Equals(algorithmId, TestAlgorithm, StringComparison.Ordinal);
public IPasswordHasher GetPasswordHasher(string algorithmId)
=> throw new NotSupportedException();
public ICryptoHasher GetHasher(string algorithmId)
=> Supports(CryptoCapability.ContentHashing, algorithmId)
? Hasher
: throw new InvalidOperationException("Unsupported hash algorithm.");
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
=> throw new NotSupportedException();
public void UpsertSigningKey(CryptoSigningKey signingKey)
=> throw new NotSupportedException();
public bool RemoveSigningKey(string keyId) => false;
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
=> Array.Empty<CryptoSigningKey>();
private sealed class TestPluginHasher : ICryptoHasher
{
public string AlgorithmId => TestAlgorithm;
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
public string ComputeHashHex(ReadOnlySpan<byte> data) => string.Empty;
}
}
public sealed class TestPluginAlphaProvider : TestPluginCryptoProviderBase
{
public TestPluginAlphaProvider()
: base("test.plugin.alpha")
{
}
}
public sealed class TestPluginBetaProvider : TestPluginCryptoProviderBase
{
public TestPluginBetaProvider()
: base("test.plugin.beta")
{
}
}

View File

@@ -13,3 +13,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0271-A | DONE | Waived (test project; revalidated 2026-01-07). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-08 | DONE | Remediated (deterministic fixtures, async naming, file split <= 100 lines); `dotnet test` passed (312 tests). |
| REMED-05 | DONE | Added DI ordering and plugin-loading tests for crypto registration paths; `dotnet test src/__Libraries/__Tests/StellaOps.Cryptography.Tests/StellaOps.Cryptography.Tests.csproj -p:BuildInParallel=false -p:UseSharedCompilation=false` passed (326 tests). |

View File

@@ -67,4 +67,13 @@ public sealed partial class PostgresEvidenceStoreIntegrationTests
storedCount.Should().Be(1);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task StoreBatchAsync_WithEmptyInput_ReturnsZeroAsync()
{
var storedCount = await _store.StoreBatchAsync(Array.Empty<IEvidence>());
storedCount.Should().Be(0);
}
}

View File

@@ -13,3 +13,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0285-A | DONE | Waived (test project; revalidated 2026-01-07). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-2026-02-03 | DONE | Split integration tests into partials, normalized async naming, deterministic fixtures; tests passed 2026-02-03. |
| REMED-07 | DONE | Added StoreBatch empty-input coverage; tests passed 2026-02-04 (35 tests). |

View File

@@ -0,0 +1,56 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Evidence.Budgets;
using StellaOps.Evidence.Retention;
using StellaOps.Evidence.Tests.TestUtilities;
using Xunit;
namespace StellaOps.Evidence.Tests.Retention;
public partial class RetentionTierManagerTests
{
[Fact]
public void GetCurrentTier_UsesRetentionBoundaries()
{
var budget = new EvidenceBudget
{
MaxScanSizeBytes = 100,
RetentionPolicies = new Dictionary<RetentionTier, RetentionPolicy>
{
[RetentionTier.Hot] = new RetentionPolicy { Duration = TimeSpan.FromDays(1) },
[RetentionTier.Warm] = new RetentionPolicy { Duration = TimeSpan.FromDays(3) },
[RetentionTier.Cold] = new RetentionPolicy { Duration = TimeSpan.FromDays(5) },
[RetentionTier.Archive] = new RetentionPolicy { Duration = TimeSpan.FromDays(7) }
}
};
var options = new Mock<IOptionsMonitor<EvidenceBudget>>();
options.Setup(o => o.CurrentValue).Returns(budget);
var repository = new Mock<IEvidenceRepository>();
var archiveStorage = new Mock<IArchiveStorage>();
var manager = new RetentionTierManager(
repository.Object,
archiveStorage.Object,
options.Object,
new FixedTimeProvider(_fixedNow));
var hotItem = new EvidenceItem
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000310"),
ScanId = _defaultScanId,
Type = EvidenceType.CallGraph,
SizeBytes = 1,
Tier = RetentionTier.Hot,
CreatedAt = _fixedNow.AddHours(-12)
};
var warmItem = hotItem with { Id = Guid.Parse("00000000-0000-0000-0000-000000000311"), CreatedAt = _fixedNow.AddDays(-2) };
var coldItem = hotItem with { Id = Guid.Parse("00000000-0000-0000-0000-000000000312"), CreatedAt = _fixedNow.AddDays(-4) };
var archiveItem = hotItem with { Id = Guid.Parse("00000000-0000-0000-0000-000000000313"), CreatedAt = _fixedNow.AddDays(-10) };
manager.GetCurrentTier(hotItem).Should().Be(RetentionTier.Hot);
manager.GetCurrentTier(warmItem).Should().Be(RetentionTier.Warm);
manager.GetCurrentTier(coldItem).Should().Be(RetentionTier.Cold);
manager.GetCurrentTier(archiveItem).Should().Be(RetentionTier.Archive);
}
}

View File

@@ -10,3 +10,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0026-A | DONE | Waived (test project; revalidated 2026-01-08). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-2026-02-02 | DONE | Remediated csproj audit findings; tests passed 2026-02-02. |
| REMED-07 | DONE | Added retention tier boundary coverage; tests passed 2026-02-04 (24 tests). |

View File

@@ -1,8 +1,8 @@
using System;
using System.Linq;
using StellaOps.Cryptography;
using StellaOps.Replay.Core;
using StellaOps.TestKit;
using System.Linq;
using System;
using Xunit;
namespace StellaOps.Replay.Core.Tests;
@@ -10,7 +10,7 @@ namespace StellaOps.Replay.Core.Tests;
public class ReachabilityReplayWriterTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void BuildManifestV2_SortsGraphsAndTraces_Deterministically()
{
var scan = new ReplayScanMetadata

View File

@@ -8,4 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0035-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
| AUDIT-0035-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
| AUDIT-0035-A | DONE | Waived (test project; revalidated 2026-01-08). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| REMED-06 | DONE | SOLID review notes refreshed 2026-02-04; dotnet test passed (1). |

View File

@@ -0,0 +1,48 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Replay.Loaders;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Replay.Tests;
public sealed class FeedSnapshotLoaderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadByDigestAsync_InvalidDigest_ThrowsFormatException()
{
var loader = new FeedSnapshotLoader(new FeedStorageStub(null), NullLogger<FeedSnapshotLoader>.Instance);
var digest = SnapshotTestData.CreateInvalidDigest('a');
var action = () => loader.LoadByDigestAsync(digest);
await action.Should().ThrowAsync<FormatException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadByDigestAsync_ShortDigest_ThrowsFormatException()
{
var loader = new FeedSnapshotLoader(new FeedStorageStub(null), NullLogger<FeedSnapshotLoader>.Instance);
var digest = new string('a', 63);
var action = () => loader.LoadByDigestAsync(digest);
await action.Should().ThrowAsync<FormatException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadByDigestAsync_DigestMismatch_ThrowsException()
{
var snapshot = SnapshotTestData.CreateFeedSnapshot(SnapshotTestData.CreateValidDigest('b'));
var actualDigest = SnapshotTestData.ComputeDigest(snapshot);
var expectedDigest = SnapshotTestData.CreateDifferentDigest(actualDigest);
var loader = new FeedSnapshotLoader(new FeedStorageStub(snapshot), NullLogger<FeedSnapshotLoader>.Instance);
var action = () => loader.LoadByDigestAsync(expectedDigest);
await action.Should().ThrowAsync<DigestMismatchException>();
}
}

View File

@@ -0,0 +1,10 @@
namespace StellaOps.Replay.Tests;
internal sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _now;
public FixedTimeProvider(DateTimeOffset now) => _now = now;
public override DateTimeOffset GetUtcNow() => _now;
}

View File

@@ -0,0 +1,48 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Replay.Loaders;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Replay.Tests;
public sealed class PolicySnapshotLoaderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadByDigestAsync_InvalidDigest_ThrowsFormatException()
{
var loader = new PolicySnapshotLoader(new PolicyStorageStub(null), NullLogger<PolicySnapshotLoader>.Instance);
var digest = SnapshotTestData.CreateInvalidDigest('c');
var action = () => loader.LoadByDigestAsync(digest);
await action.Should().ThrowAsync<FormatException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadByDigestAsync_ShortDigest_ThrowsFormatException()
{
var loader = new PolicySnapshotLoader(new PolicyStorageStub(null), NullLogger<PolicySnapshotLoader>.Instance);
var digest = new string('c', 10);
var action = () => loader.LoadByDigestAsync(digest);
await action.Should().ThrowAsync<FormatException>();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LoadByDigestAsync_DigestMismatch_ThrowsException()
{
var snapshot = SnapshotTestData.CreatePolicySnapshot(SnapshotTestData.CreateValidDigest('d'));
var actualDigest = SnapshotTestData.ComputeDigest(snapshot);
var expectedDigest = SnapshotTestData.CreateDifferentDigest(actualDigest);
var loader = new PolicySnapshotLoader(new PolicyStorageStub(snapshot), NullLogger<PolicySnapshotLoader>.Instance);
var action = () => loader.LoadByDigestAsync(expectedDigest);
await action.Should().ThrowAsync<DigestMismatchException>();
}
}

View File

@@ -16,13 +16,17 @@ internal static class ReplayEngineTestFixtures
new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
internal static readonly DateTimeOffset FixedFeedSnapshotAt = FixedTimestamp.AddHours(-1);
internal static ReplayEngine CreateEngine()
internal static ReplayEngine CreateEngine() =>
CreateEngine(new FixedTimeProvider(FixedTimestamp));
internal static ReplayEngine CreateEngine(TimeProvider timeProvider)
{
return new ReplayEngine(
new FakeFeedLoader(),
new FakePolicyLoader(),
new FakeScannerFactory(),
NullLogger<ReplayEngine>.Instance);
NullLogger<ReplayEngine>.Instance,
timeProvider);
}
internal static RunManifest CreateManifest()

View File

@@ -0,0 +1,63 @@
using FluentAssertions;
using StellaOps.Replay.Engine;
using StellaOps.Replay.Models;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Replay.Tests;
public partial class ReplayEngineTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CheckDeterminism_IdenticalResults_ReturnsTrue()
{
var engine = ReplayEngineTestFixtures.CreateEngine();
var result1 = new ReplayResult
{
RunId = "1",
VerdictDigest = "abc123",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var result2 = new ReplayResult
{
RunId = "1",
VerdictDigest = "abc123",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var check = engine.CheckDeterminism(result1, result2);
check.IsDeterministic.Should().BeTrue();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CheckDeterminism_DifferentResults_ReturnsDifferences()
{
var engine = ReplayEngineTestFixtures.CreateEngine();
var result1 = new ReplayResult
{
RunId = "1",
VerdictJson = "{\"score\":100}",
VerdictDigest = "abc123",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var result2 = new ReplayResult
{
RunId = "1",
VerdictJson = "{\"score\":99}",
VerdictDigest = "def456",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var check = engine.CheckDeterminism(result1, result2);
check.IsDeterministic.Should().BeFalse();
check.Differences.Should().NotBeEmpty();
}
}

View File

@@ -0,0 +1,23 @@
using FluentAssertions;
using StellaOps.Replay.Models;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Replay.Tests;
public partial class ReplayEngineTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Replay_InvalidManifest_UsesTimeProviderAsync()
{
var fixedTime = new DateTimeOffset(2026, 1, 2, 0, 0, 0, TimeSpan.Zero);
var engine = ReplayEngineTestFixtures.CreateEngine(new FixedTimeProvider(fixedTime));
var manifest = ReplayEngineTestFixtures.CreateManifest() with { RunId = "" };
var result = await engine.ReplayAsync(manifest, new ReplayOptions());
result.Success.Should().BeFalse();
result.ExecutedAt.Should().Be(fixedTime);
}
}

View File

@@ -6,7 +6,7 @@ using Xunit;
namespace StellaOps.Replay.Tests;
public class ReplayEngineTests
public partial class ReplayEngineTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
@@ -38,57 +38,4 @@ public class ReplayEngineTests
result1.VerdictDigest.Should().NotBe(result2.VerdictDigest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CheckDeterminism_IdenticalResults_ReturnsTrue()
{
var engine = ReplayEngineTestFixtures.CreateEngine();
var result1 = new ReplayResult
{
RunId = "1",
VerdictDigest = "abc123",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var result2 = new ReplayResult
{
RunId = "1",
VerdictDigest = "abc123",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var check = engine.CheckDeterminism(result1, result2);
check.IsDeterministic.Should().BeTrue();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CheckDeterminism_DifferentResults_ReturnsDifferences()
{
var engine = ReplayEngineTestFixtures.CreateEngine();
var result1 = new ReplayResult
{
RunId = "1",
VerdictJson = "{\"score\":100}",
VerdictDigest = "abc123",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var result2 = new ReplayResult
{
RunId = "1",
VerdictJson = "{\"score\":99}",
VerdictDigest = "def456",
Success = true,
ExecutedAt = ReplayEngineTestFixtures.FixedTimestamp
};
var check = engine.CheckDeterminism(result1, result2);
check.IsDeterministic.Should().BeFalse();
check.Differences.Should().NotBeEmpty();
}
}

View File

@@ -0,0 +1,24 @@
using StellaOps.Replay.Loaders;
using StellaOps.Testing.Manifests.Models;
namespace StellaOps.Replay.Tests;
internal sealed class FeedStorageStub : IFeedStorage
{
private readonly FeedSnapshot? _snapshot;
public FeedStorageStub(FeedSnapshot? snapshot) => _snapshot = snapshot;
public Task<FeedSnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default)
=> Task.FromResult(_snapshot);
}
internal sealed class PolicyStorageStub : IPolicyStorage
{
private readonly PolicySnapshot? _snapshot;
public PolicyStorageStub(PolicySnapshot? snapshot) => _snapshot = snapshot;
public Task<PolicySnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default)
=> Task.FromResult(_snapshot);
}

View File

@@ -0,0 +1,36 @@
using StellaOps.Canonicalization.Json;
using StellaOps.Testing.Manifests.Models;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
namespace StellaOps.Replay.Tests;
internal static class SnapshotTestData
{
internal static readonly DateTimeOffset FixedSnapshotAt =
new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
internal static FeedSnapshot CreateFeedSnapshot(string digest) =>
new FeedSnapshot("nvd", "v1", digest, FixedSnapshotAt);
internal static PolicySnapshot CreatePolicySnapshot(string digest) =>
new PolicySnapshot("1.0.0", digest, ImmutableArray<string>.Empty);
internal static string ComputeDigest<T>(T value)
{
var json = CanonicalJsonSerializer.Serialize(value);
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json))).ToLowerInvariant();
}
internal static string CreateValidDigest(char fill) => new string(fill, 64);
internal static string CreateInvalidDigest(char fill) => new string(fill, 63) + "g";
internal static string CreateDifferentDigest(string digest)
{
var last = digest[^1];
var replacement = last == 'a' ? 'b' : 'a';
return digest[..^1] + replacement;
}
}

View File

@@ -12,3 +12,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| REMED-03 | DONE | Tier 0 remediation (usings sorted, deterministic test data, warnings as errors); dotnet test passed 2026-02-02. |
| REMED-04 | DONE | Async naming updates; ConfigureAwait(false) skipped in tests per xUnit1030; dotnet test passed 2026-02-02. |
| REMED-05 | DONE | File split to keep tests <= 100 lines; dotnet test passed 2026-02-02. |
| REMED-07 | DONE | 2026-02-05; replay tests split into partials, loader validation/digest mismatch coverage + failure timestamp test added; dotnet test src/__Libraries/__Tests/StellaOps.Replay.Tests/StellaOps.Replay.Tests.csproj passed (11 tests). |