stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalBytes_WithValidManifest_ProducesDeterministicOutput()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
var bytes1 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
var bytes2 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
|
||||
bytes1.Should().Equal(bytes2, "Same manifest should produce identical canonical bytes");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_WithValidManifest_ProducesDeterministicString()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
var json1 = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
var json2 = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
|
||||
json1.Should().Be(json2, "Same manifest should produce identical canonical JSON string");
|
||||
json1.Should().NotContain("\n", "Canonical JSON should have no newlines");
|
||||
json1.Should().NotContain(" ", "Canonical JSON should have no indentation");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Testing.Determinism;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
private static DeterminismManifest CreateComplexManifest()
|
||||
{
|
||||
return new DeterminismManifest
|
||||
{
|
||||
SchemaVersion = "1.0",
|
||||
Artifact = new ArtifactInfo
|
||||
{
|
||||
Type = "evidence-bundle",
|
||||
Name = "test-bundle",
|
||||
Version = "2.0.0",
|
||||
Format = "DSSE Envelope",
|
||||
Metadata = new Dictionary<string, object?>
|
||||
{
|
||||
["predicateType"] = "https://in-toto.io/attestation/v1",
|
||||
["subject"] = "pkg:docker/alpine@3.18"
|
||||
}
|
||||
},
|
||||
CanonicalHash = new CanonicalHashInfo
|
||||
{
|
||||
Algorithm = "SHA-256",
|
||||
Value = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
Encoding = "hex"
|
||||
},
|
||||
Inputs = new InputStamps
|
||||
{
|
||||
FeedSnapshotHash = "feed123abc",
|
||||
PolicyManifestHash = "policy456def",
|
||||
SourceCodeHash = "git789ghi",
|
||||
VexDocumentHashes = new[] { "vex123", "vex456" },
|
||||
Custom = new Dictionary<string, string>
|
||||
{
|
||||
["baselineVersion"] = "1.0.0",
|
||||
["environment"] = "production"
|
||||
}
|
||||
},
|
||||
Toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Attestor", Version = "2.0.0", Hash = "hash123" },
|
||||
new ComponentInfo { Name = "StellaOps.Signer", Version = "2.1.0" }
|
||||
},
|
||||
Compiler = new CompilerInfo
|
||||
{
|
||||
Name = "Roslyn",
|
||||
Version = "4.8.0"
|
||||
}
|
||||
},
|
||||
GeneratedAt = new DateTimeOffset(2025, 12, 23, 18, 0, 0, TimeSpan.Zero),
|
||||
Reproducibility = new ReproducibilityMetadata
|
||||
{
|
||||
DeterministicSeed = 12345,
|
||||
ClockFixed = true,
|
||||
OrderingGuarantee = "stable",
|
||||
NormalizationRules = new[] { "UTF-8", "LF line endings", "no trailing whitespace" }
|
||||
},
|
||||
Verification = new VerificationInfo
|
||||
{
|
||||
Command = "dotnet test --verify-determinism",
|
||||
ExpectedHash = "abc123def456",
|
||||
Baseline = "baselines/test-bundle.json"
|
||||
},
|
||||
Signatures = new[]
|
||||
{
|
||||
new SignatureInfo
|
||||
{
|
||||
Algorithm = "ES256",
|
||||
KeyId = "signing-key-1",
|
||||
Signature = "base64encodedSig==",
|
||||
Timestamp = new DateTimeOffset(2025, 12, 23, 18, 0, 30, TimeSpan.Zero)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestSerialization_WithComplexMetadata_PreservesAllFields()
|
||||
{
|
||||
var manifest = CreateComplexManifest();
|
||||
|
||||
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
var deserialized = DeterminismManifestReader.FromString(json);
|
||||
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromBytes_WithValidJson_DeserializesSuccessfully()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
var bytes = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
|
||||
var deserialized = DeterminismManifestReader.FromBytes(bytes);
|
||||
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromString_WithValidJson_DeserializesSuccessfully()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
|
||||
var deserialized = DeterminismManifestReader.FromString(json);
|
||||
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromBytes_WithInvalidSchemaVersion_ThrowsInvalidOperationException()
|
||||
{
|
||||
var manifest = CreateSampleManifest() with { SchemaVersion = "2.0" };
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(manifest);
|
||||
|
||||
Action act = () => DeterminismManifestReader.FromBytes(bytes);
|
||||
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*schema version*2.0*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeCanonicalHash_ProducesDeterministicHash()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
var hash1 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
|
||||
var hash2 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
|
||||
|
||||
hash1.Should().Be(hash2, "Same manifest should produce same hash");
|
||||
hash1.Should().MatchRegex("^[0-9a-f]{64}$", "Hash should be 64-character hex string");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithValidInputs_CreatesManifestWithCorrectHash()
|
||||
{
|
||||
var artifactBytes = "Test artifact content"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test-sbom",
|
||||
Version = "1.0.0",
|
||||
Format = "SPDX 3.0.1"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Scanner", Version = "1.0.0" }
|
||||
}
|
||||
};
|
||||
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain);
|
||||
|
||||
manifest.SchemaVersion.Should().Be("1.0");
|
||||
manifest.Artifact.Should().Be(artifactInfo);
|
||||
manifest.Toolchain.Should().Be(toolchain);
|
||||
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
|
||||
manifest.CanonicalHash.Encoding.Should().Be("hex");
|
||||
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
manifest.GeneratedAt.Should().NotBe(default);
|
||||
manifest.GeneratedAt.Offset.Should().Be(TimeSpan.Zero);
|
||||
|
||||
var expectedHash = CanonJson.Sha256Hex(artifactBytes);
|
||||
manifest.CanonicalHash.Value.Should().Be(expectedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifestForJsonArtifact_WithValidInputs_CreatesManifestWithCanonicalHash()
|
||||
{
|
||||
var artifact = new { Name = "test", Value = 123, Items = new[] { "a", "b", "c" } };
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "verdict",
|
||||
Name = "test-verdict",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Policy.Engine", Version = "1.0.0" }
|
||||
}
|
||||
};
|
||||
|
||||
var manifest = DeterminismManifestWriter.CreateManifestForJsonArtifact(
|
||||
artifact,
|
||||
artifactInfo,
|
||||
toolchain);
|
||||
|
||||
manifest.SchemaVersion.Should().Be("1.0");
|
||||
manifest.Artifact.Should().Be(artifactInfo);
|
||||
manifest.Toolchain.Should().Be(toolchain);
|
||||
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
|
||||
manifest.CanonicalHash.Encoding.Should().Be("hex");
|
||||
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
|
||||
var expectedHash = CanonJson.Hash(artifact);
|
||||
manifest.CanonicalHash.Value.Should().Be(expectedHash);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithInputStamps_IncludesInputStamps()
|
||||
{
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var inputs = new InputStamps
|
||||
{
|
||||
FeedSnapshotHash = "abc123",
|
||||
PolicyManifestHash = "def456",
|
||||
SourceCodeHash = "789abc"
|
||||
};
|
||||
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
inputs: inputs);
|
||||
|
||||
manifest.Inputs.Should().NotBeNull();
|
||||
manifest.Inputs!.FeedSnapshotHash.Should().Be("abc123");
|
||||
manifest.Inputs.PolicyManifestHash.Should().Be("def456");
|
||||
manifest.Inputs.SourceCodeHash.Should().Be("789abc");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithReproducibilityMetadata_IncludesMetadata()
|
||||
{
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var reproducibility = new ReproducibilityMetadata
|
||||
{
|
||||
DeterministicSeed = 42,
|
||||
ClockFixed = true,
|
||||
OrderingGuarantee = "sorted",
|
||||
NormalizationRules = new[] { "UTF-8", "LF line endings", "sorted JSON keys" }
|
||||
};
|
||||
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
reproducibility: reproducibility);
|
||||
|
||||
manifest.Reproducibility.Should().NotBeNull();
|
||||
manifest.Reproducibility!.DeterministicSeed.Should().Be(42);
|
||||
manifest.Reproducibility.ClockFixed.Should().BeTrue();
|
||||
manifest.Reproducibility.OrderingGuarantee.Should().Be("sorted");
|
||||
manifest.Reproducibility.NormalizationRules.Should().ContainInOrder(
|
||||
"UTF-8",
|
||||
"LF line endings",
|
||||
"sorted JSON keys");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithVerificationInfo_IncludesVerification()
|
||||
{
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var verification = new VerificationInfo
|
||||
{
|
||||
Command = "dotnet test --verify-determinism",
|
||||
ExpectedHash = "abc123def456",
|
||||
Baseline = "baselines/test-bundle.json"
|
||||
};
|
||||
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
verification: verification);
|
||||
|
||||
manifest.Verification.Should().NotBeNull();
|
||||
manifest.Verification!.Command.Should().Be("dotnet test --verify-determinism");
|
||||
manifest.Verification.ExpectedHash.Should().Be("abc123def456");
|
||||
manifest.Verification.Baseline.Should().Be("baselines/test-bundle.json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryReadFromFileAsync_WithNonExistentFile_ReturnsNullAsync()
|
||||
{
|
||||
var nonExistentPath = GetMissingFilePath("missing.json");
|
||||
|
||||
var result = await DeterminismManifestReader.TryReadFromFileAsync(nonExistentPath);
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReadFromFile_WithNonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
var nonExistentPath = GetMissingFilePath("missing-sync.json");
|
||||
|
||||
Action act = () => DeterminismManifestReader.ReadFromFile(nonExistentPath);
|
||||
|
||||
act.Should().Throw<FileNotFoundException>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
private static readonly string DeterminismRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"stellaops-tests",
|
||||
"testkit",
|
||||
"determinism-manifest");
|
||||
|
||||
private static string GetFilePath(string fileName)
|
||||
{
|
||||
Directory.CreateDirectory(DeterminismRoot);
|
||||
return Path.Combine(DeterminismRoot, fileName);
|
||||
}
|
||||
|
||||
private static string GetMissingFilePath(string fileName)
|
||||
{
|
||||
var path = GetFilePath(fileName);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.IO;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WriteToFile_AndReadFromFile_RoundTripsSuccessfully()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
var tempFile = GetMissingFilePath("roundtrip.json");
|
||||
|
||||
try
|
||||
{
|
||||
DeterminismManifestWriter.WriteToFile(manifest, tempFile);
|
||||
|
||||
var readManifest = DeterminismManifestReader.ReadFromFile(tempFile);
|
||||
|
||||
readManifest.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteToFileAsync_AndReadFromFileAsync_RoundTripsSuccessfullyAsync()
|
||||
{
|
||||
var manifest = CreateSampleManifest();
|
||||
var tempFile = GetMissingFilePath("roundtrip-async.json");
|
||||
|
||||
try
|
||||
{
|
||||
await DeterminismManifestWriter.WriteToFileAsync(manifest, tempFile);
|
||||
|
||||
var readManifest = await DeterminismManifestReader.ReadFromFileAsync(tempFile);
|
||||
|
||||
readManifest.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using StellaOps.Testing.Determinism;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class DeterminismManifestTests
|
||||
{
|
||||
private static DeterminismManifest CreateSampleManifest()
|
||||
{
|
||||
return new DeterminismManifest
|
||||
{
|
||||
SchemaVersion = "1.0",
|
||||
Artifact = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test-sbom",
|
||||
Version = "1.0.0",
|
||||
Format = "SPDX 3.0.1"
|
||||
},
|
||||
CanonicalHash = new CanonicalHashInfo
|
||||
{
|
||||
Algorithm = "SHA-256",
|
||||
Value = "abc123def4567890123456789012345678901234567890123456789901234",
|
||||
Encoding = "hex"
|
||||
},
|
||||
Toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo
|
||||
{
|
||||
Name = "StellaOps.Scanner",
|
||||
Version = "1.0.0",
|
||||
Hash = "def456abc7890123456789012345678901234567890123455678901234567"
|
||||
}
|
||||
}
|
||||
},
|
||||
GeneratedAt = new DateTimeOffset(2025, 12, 23, 17, 45, 0, TimeSpan.Zero)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,513 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Testing.Determinism;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed class DeterminismManifestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalBytes_WithValidManifest_ProducesDeterministicOutput()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
// Act
|
||||
var bytes1 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
var bytes2 = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
|
||||
// Assert
|
||||
bytes1.Should().Equal(bytes2, "Same manifest should produce identical canonical bytes");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_WithValidManifest_ProducesDeterministicString()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
// Act
|
||||
var json1 = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
var json2 = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
|
||||
// Assert
|
||||
json1.Should().Be(json2, "Same manifest should produce identical canonical JSON string");
|
||||
json1.Should().NotContain("\n", "Canonical JSON should have no newlines");
|
||||
json1.Should().NotContain(" ", "Canonical JSON should have no indentation");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WriteToFile_AndReadFromFile_RoundTripsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
// Act - Write
|
||||
DeterminismManifestWriter.WriteToFile(manifest, tempFile);
|
||||
|
||||
// Act - Read
|
||||
var readManifest = DeterminismManifestReader.ReadFromFile(tempFile);
|
||||
|
||||
// Assert
|
||||
readManifest.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteToFileAsync_AndReadFromFileAsync_RoundTripsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
// Act - Write
|
||||
await DeterminismManifestWriter.WriteToFileAsync(manifest, tempFile);
|
||||
|
||||
// Act - Read
|
||||
var readManifest = await DeterminismManifestReader.ReadFromFileAsync(tempFile);
|
||||
|
||||
// Assert
|
||||
readManifest.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromBytes_WithValidJson_DeserializesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var bytes = DeterminismManifestWriter.ToCanonicalBytes(manifest);
|
||||
|
||||
// Act
|
||||
var deserialized = DeterminismManifestReader.FromBytes(bytes);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromString_WithValidJson_DeserializesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
|
||||
// Act
|
||||
var deserialized = DeterminismManifestReader.FromString(json);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromBytes_WithInvalidSchemaVersion_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest() with { SchemaVersion = "2.0" };
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(manifest);
|
||||
|
||||
// Act
|
||||
Action act = () => DeterminismManifestReader.FromBytes(bytes);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*schema version*2.0*");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryReadFromFileAsync_WithNonExistentFile_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
|
||||
// Act
|
||||
var result = DeterminismManifestReader.TryReadFromFileAsync(nonExistentPath).GetAwaiter().GetResult();
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReadFromFile_WithNonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
|
||||
// Act
|
||||
Action act = () => DeterminismManifestReader.ReadFromFile(nonExistentPath);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<FileNotFoundException>();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeCanonicalHash_ProducesDeterministicHash()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateSampleManifest();
|
||||
|
||||
// Act
|
||||
var hash1 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
|
||||
var hash2 = DeterminismManifestWriter.ComputeCanonicalHash(manifest);
|
||||
|
||||
// Assert
|
||||
hash1.Should().Be(hash2, "Same manifest should produce same hash");
|
||||
hash1.Should().MatchRegex("^[0-9a-f]{64}$", "Hash should be 64-character hex string");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithValidInputs_CreatesManifestWithCorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact content"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test-sbom",
|
||||
Version = "1.0.0",
|
||||
Format = "SPDX 3.0.1"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Scanner", Version = "1.0.0" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain);
|
||||
|
||||
// Assert
|
||||
manifest.SchemaVersion.Should().Be("1.0");
|
||||
manifest.Artifact.Should().Be(artifactInfo);
|
||||
manifest.Toolchain.Should().Be(toolchain);
|
||||
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
|
||||
manifest.CanonicalHash.Encoding.Should().Be("hex");
|
||||
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
manifest.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Verify hash is correct
|
||||
var expectedHash = CanonJson.Sha256Hex(artifactBytes);
|
||||
manifest.CanonicalHash.Value.Should().Be(expectedHash);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifestForJsonArtifact_WithValidInputs_CreatesManifestWithCanonicalHash()
|
||||
{
|
||||
// Arrange
|
||||
var artifact = new { Name = "test", Value = 123, Items = new[] { "a", "b", "c" } };
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "verdict",
|
||||
Name = "test-verdict",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Policy.Engine", Version = "1.0.0" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifestForJsonArtifact(
|
||||
artifact,
|
||||
artifactInfo,
|
||||
toolchain);
|
||||
|
||||
// Assert
|
||||
manifest.SchemaVersion.Should().Be("1.0");
|
||||
manifest.Artifact.Should().Be(artifactInfo);
|
||||
manifest.Toolchain.Should().Be(toolchain);
|
||||
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
|
||||
manifest.CanonicalHash.Encoding.Should().Be("hex");
|
||||
manifest.CanonicalHash.Value.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
|
||||
// Verify hash is correct (should use canonical JSON)
|
||||
var expectedHash = CanonJson.Hash(artifact);
|
||||
manifest.CanonicalHash.Value.Should().Be(expectedHash);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithInputStamps_IncludesInputStamps()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var inputs = new InputStamps
|
||||
{
|
||||
FeedSnapshotHash = "abc123",
|
||||
PolicyManifestHash = "def456",
|
||||
SourceCodeHash = "789abc"
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
inputs: inputs);
|
||||
|
||||
// Assert
|
||||
manifest.Inputs.Should().NotBeNull();
|
||||
manifest.Inputs!.FeedSnapshotHash.Should().Be("abc123");
|
||||
manifest.Inputs.PolicyManifestHash.Should().Be("def456");
|
||||
manifest.Inputs.SourceCodeHash.Should().Be("789abc");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithReproducibilityMetadata_IncludesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var reproducibility = new ReproducibilityMetadata
|
||||
{
|
||||
DeterministicSeed = 42,
|
||||
ClockFixed = true,
|
||||
OrderingGuarantee = "sorted",
|
||||
NormalizationRules = new[] { "UTF-8", "LF line endings", "sorted JSON keys" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
reproducibility: reproducibility);
|
||||
|
||||
// Assert
|
||||
manifest.Reproducibility.Should().NotBeNull();
|
||||
manifest.Reproducibility!.DeterministicSeed.Should().Be(42);
|
||||
manifest.Reproducibility.ClockFixed.Should().BeTrue();
|
||||
manifest.Reproducibility.OrderingGuarantee.Should().Be("sorted");
|
||||
manifest.Reproducibility.NormalizationRules.Should().ContainInOrder("UTF-8", "LF line endings", "sorted JSON keys");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateManifest_WithVerificationInfo_IncludesVerification()
|
||||
{
|
||||
// Arrange
|
||||
var artifactBytes = "Test artifact"u8.ToArray();
|
||||
var artifactInfo = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test",
|
||||
Version = "1.0.0"
|
||||
};
|
||||
var toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[] { new ComponentInfo { Name = "Scanner", Version = "1.0.0" } }
|
||||
};
|
||||
var verification = new VerificationInfo
|
||||
{
|
||||
Command = "dotnet run --project Scanner -- scan container alpine:3.18",
|
||||
ExpectedHash = "abc123def456",
|
||||
Baseline = "tests/baselines/sbom-alpine-3.18.determinism.json"
|
||||
};
|
||||
|
||||
// Act
|
||||
var manifest = DeterminismManifestWriter.CreateManifest(
|
||||
artifactBytes,
|
||||
artifactInfo,
|
||||
toolchain,
|
||||
verification: verification);
|
||||
|
||||
// Assert
|
||||
manifest.Verification.Should().NotBeNull();
|
||||
manifest.Verification!.Command.Should().Be("dotnet run --project Scanner -- scan container alpine:3.18");
|
||||
manifest.Verification.ExpectedHash.Should().Be("abc123def456");
|
||||
manifest.Verification.Baseline.Should().Be("tests/baselines/sbom-alpine-3.18.determinism.json");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestSerialization_WithComplexMetadata_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateComplexManifest();
|
||||
|
||||
// Act
|
||||
var json = DeterminismManifestWriter.ToCanonicalString(manifest);
|
||||
var deserialized = DeterminismManifestReader.FromString(json);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
private static DeterminismManifest CreateSampleManifest()
|
||||
{
|
||||
return new DeterminismManifest
|
||||
{
|
||||
SchemaVersion = "1.0",
|
||||
Artifact = new ArtifactInfo
|
||||
{
|
||||
Type = "sbom",
|
||||
Name = "test-sbom",
|
||||
Version = "1.0.0",
|
||||
Format = "SPDX 3.0.1"
|
||||
},
|
||||
CanonicalHash = new CanonicalHashInfo
|
||||
{
|
||||
Algorithm = "SHA-256",
|
||||
Value = "abc123def456789012345678901234567890123456789012345678901234",
|
||||
Encoding = "hex"
|
||||
},
|
||||
Toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo
|
||||
{
|
||||
Name = "StellaOps.Scanner",
|
||||
Version = "1.0.0",
|
||||
Hash = "def456abc789012345678901234567890123456789012345678901234567"
|
||||
}
|
||||
}
|
||||
},
|
||||
GeneratedAt = new DateTimeOffset(2025, 12, 23, 17, 45, 0, TimeSpan.Zero)
|
||||
};
|
||||
}
|
||||
|
||||
private static DeterminismManifest CreateComplexManifest()
|
||||
{
|
||||
return new DeterminismManifest
|
||||
{
|
||||
SchemaVersion = "1.0",
|
||||
Artifact = new ArtifactInfo
|
||||
{
|
||||
Type = "evidence-bundle",
|
||||
Name = "test-bundle",
|
||||
Version = "2.0.0",
|
||||
Format = "DSSE Envelope",
|
||||
Metadata = new Dictionary<string, object?>
|
||||
{
|
||||
["predicateType"] = "https://in-toto.io/attestation/v1",
|
||||
["subject"] = "pkg:docker/alpine@3.18"
|
||||
}
|
||||
},
|
||||
CanonicalHash = new CanonicalHashInfo
|
||||
{
|
||||
Algorithm = "SHA-256",
|
||||
Value = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
Encoding = "hex"
|
||||
},
|
||||
Inputs = new InputStamps
|
||||
{
|
||||
FeedSnapshotHash = "feed123abc",
|
||||
PolicyManifestHash = "policy456def",
|
||||
SourceCodeHash = "git789ghi",
|
||||
VexDocumentHashes = new[] { "vex123", "vex456" },
|
||||
Custom = new Dictionary<string, string>
|
||||
{
|
||||
["baselineVersion"] = "1.0.0",
|
||||
["environment"] = "production"
|
||||
}
|
||||
},
|
||||
Toolchain = new ToolchainInfo
|
||||
{
|
||||
Platform = ".NET 10.0",
|
||||
Components = new[]
|
||||
{
|
||||
new ComponentInfo { Name = "StellaOps.Attestor", Version = "2.0.0", Hash = "hash123" },
|
||||
new ComponentInfo { Name = "StellaOps.Signer", Version = "2.1.0" }
|
||||
},
|
||||
Compiler = new CompilerInfo
|
||||
{
|
||||
Name = "Roslyn",
|
||||
Version = "4.8.0"
|
||||
}
|
||||
},
|
||||
GeneratedAt = new DateTimeOffset(2025, 12, 23, 18, 0, 0, TimeSpan.Zero),
|
||||
Reproducibility = new ReproducibilityMetadata
|
||||
{
|
||||
DeterministicSeed = 12345,
|
||||
ClockFixed = true,
|
||||
OrderingGuarantee = "stable",
|
||||
NormalizationRules = new[] { "UTF-8", "LF line endings", "no trailing whitespace" }
|
||||
},
|
||||
Verification = new VerificationInfo
|
||||
{
|
||||
Command = "dotnet test --verify-determinism",
|
||||
ExpectedHash = "abc123def456",
|
||||
Baseline = "baselines/test-bundle.json"
|
||||
},
|
||||
Signatures = new[]
|
||||
{
|
||||
new SignatureInfo
|
||||
{
|
||||
Algorithm = "ES256",
|
||||
KeyId = "signing-key-1",
|
||||
Signature = "base64encodedSig==",
|
||||
Timestamp = new DateTimeOffset(2025, 12, 23, 18, 0, 30, TimeSpan.Zero)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Environment;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EnvironmentSkewTests
|
||||
{
|
||||
[Fact]
|
||||
public void EnvironmentProfile_Standard_HasCorrectDefaults()
|
||||
{
|
||||
var profile = EnvironmentProfile.Standard;
|
||||
|
||||
profile.Name.Should().Be("Standard");
|
||||
profile.Cpu.Architecture.Should().Be(CpuArchitecture.X64);
|
||||
profile.Network.Latency.Should().Be(TimeSpan.Zero);
|
||||
profile.Runtime.Should().Be(ContainerRuntime.Docker);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_HighLatency_Has100msLatency()
|
||||
{
|
||||
var profile = EnvironmentProfile.HighLatency;
|
||||
|
||||
profile.Name.Should().Be("HighLatency");
|
||||
profile.Network.Latency.Should().Be(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_LowBandwidth_Has10MbpsLimit()
|
||||
{
|
||||
var profile = EnvironmentProfile.LowBandwidth;
|
||||
|
||||
profile.Name.Should().Be("LowBandwidth");
|
||||
profile.Network.BandwidthMbps.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_PacketLoss_Has1PercentLoss()
|
||||
{
|
||||
var profile = EnvironmentProfile.PacketLoss;
|
||||
|
||||
profile.Name.Should().Be("PacketLoss");
|
||||
profile.Network.PacketLossRate.Should().Be(0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_ArmCpu_HasArm64Architecture()
|
||||
{
|
||||
var profile = EnvironmentProfile.ArmCpu;
|
||||
|
||||
profile.Name.Should().Be("ArmCpu");
|
||||
profile.Cpu.Architecture.Should().Be(CpuArchitecture.Arm64);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_ResourceConstrained_HasLimits()
|
||||
{
|
||||
var profile = EnvironmentProfile.ResourceConstrained;
|
||||
|
||||
profile.Name.Should().Be("ResourceConstrained");
|
||||
profile.ResourceLimits.MemoryMb.Should().Be(256);
|
||||
profile.ResourceLimits.CpuCores.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_All_ContainsExpectedProfiles()
|
||||
{
|
||||
var profiles = EnvironmentProfile.All;
|
||||
|
||||
profiles.Should().HaveCount(5);
|
||||
profiles.Should().Contain(p => p.Name == "Standard");
|
||||
profiles.Should().Contain(p => p.Name == "HighLatency");
|
||||
profiles.Should().Contain(p => p.Name == "LowBandwidth");
|
||||
profiles.Should().Contain(p => p.Name == "PacketLoss");
|
||||
profiles.Should().Contain(p => p.Name == "ResourceConstrained");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NetworkProfile_RequiresNetworkShaping_ReturnsTrueWhenConfigured()
|
||||
{
|
||||
new NetworkProfile { Latency = TimeSpan.FromMilliseconds(50) }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile { PacketLossRate = 0.01 }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile { BandwidthMbps = 10 }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile()
|
||||
.RequiresNetworkShaping.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Environment;
|
||||
using Xunit;
|
||||
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EnvironmentSkewTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SkewReport_ToJson_ProducesValidJsonAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
var json = report.ToJson();
|
||||
|
||||
json.Should().Contain("\"generatedAt\"");
|
||||
json.Should().Contain("\"profileCount\"");
|
||||
json.Should().Contain("\"hasSkew\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewReport_ToMarkdown_ProducesValidMarkdownAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
markdown.Should().Contain("# Environment Skew Report");
|
||||
markdown.Should().Contain("| Profile |");
|
||||
markdown.Should().Contain("Standard");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Environment;
|
||||
using Xunit;
|
||||
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EnvironmentSkewTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestResult_Defaults_AreCorrect()
|
||||
{
|
||||
var result = new TestKitTestResult();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.ProfileName.Should().BeEmpty();
|
||||
result.Metadata.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkewAssertException_SetsMessage()
|
||||
{
|
||||
var ex = new SkewAssertException("Test message");
|
||||
|
||||
ex.Message.Should().Be("Test message");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Environment;
|
||||
using Xunit;
|
||||
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EnvironmentSkewTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_PassesWhenResultsAreEquivalentAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_FailsWhenSkewExceedsToleranceAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var values = new Queue<double>([100.0, 100.0, 100.0, 200.0, 200.0, 200.0]);
|
||||
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = values.Dequeue(), DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().Throw<SkewAssertException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_IgnoresSingleProfileAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Environment;
|
||||
using Xunit;
|
||||
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EnvironmentSkewTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunAcrossProfiles_ExecutesTestForEachProfileAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var executedProfiles = new List<string>();
|
||||
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () =>
|
||||
{
|
||||
executedProfiles.Add("executed");
|
||||
return Task.FromResult(new TestKitTestResult { Value = 1.0, DurationMs = 10 });
|
||||
},
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
report.ProfileCount.Should().Be(2);
|
||||
report.Results.Should().HaveCount(2);
|
||||
executedProfiles.Should().HaveCount(report.Results.Sum(r => r.Results.Count));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_ExecutesMultipleIterationsAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var executionCount = 0;
|
||||
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.FromResult(new TestKitTestResult { Value = executionCount, DurationMs = 10 });
|
||||
},
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 5);
|
||||
|
||||
executionCount.Should().Be(5);
|
||||
result.Results.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_CalculatesAveragesAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var values = new[] { 10.0, 20.0, 30.0 };
|
||||
var index = 0;
|
||||
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () => Task.FromResult(new TestKitTestResult
|
||||
{
|
||||
Value = values[index++],
|
||||
DurationMs = 100
|
||||
}),
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 3);
|
||||
|
||||
result.AverageValue.Should().Be(20.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_HandlesErrorsAsync()
|
||||
{
|
||||
var runner = new SkewTestRunner();
|
||||
var iteration = 0;
|
||||
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () =>
|
||||
{
|
||||
iteration++;
|
||||
if (iteration == 2)
|
||||
{
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.FromResult(new TestKitTestResult { Value = 1.0, Success = true });
|
||||
},
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 3);
|
||||
|
||||
result.Results.Should().HaveCount(3);
|
||||
result.SuccessRate.Should().BeApproximately(2.0 / 3.0, 0.01);
|
||||
}
|
||||
}
|
||||
@@ -1,327 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Environment;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
using TestKitTestResult = StellaOps.TestKit.Environment.TestResult;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for environment skew testing infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class EnvironmentSkewTests
|
||||
public sealed partial class EnvironmentSkewTests
|
||||
{
|
||||
#region EnvironmentProfile Tests
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_Standard_HasCorrectDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.Standard;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("Standard");
|
||||
profile.Cpu.Architecture.Should().Be(CpuArchitecture.X64);
|
||||
profile.Network.Latency.Should().Be(TimeSpan.Zero);
|
||||
profile.Runtime.Should().Be(ContainerRuntime.Docker);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_HighLatency_Has100msLatency()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.HighLatency;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("HighLatency");
|
||||
profile.Network.Latency.Should().Be(TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_LowBandwidth_Has10MbpsLimit()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.LowBandwidth;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("LowBandwidth");
|
||||
profile.Network.BandwidthMbps.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_PacketLoss_Has1PercentLoss()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.PacketLoss;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("PacketLoss");
|
||||
profile.Network.PacketLossRate.Should().Be(0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_ArmCpu_HasArm64Architecture()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.ArmCpu;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("ArmCpu");
|
||||
profile.Cpu.Architecture.Should().Be(CpuArchitecture.Arm64);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_ResourceConstrained_HasLimits()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profile = EnvironmentProfile.ResourceConstrained;
|
||||
|
||||
// Assert
|
||||
profile.Name.Should().Be("ResourceConstrained");
|
||||
profile.ResourceLimits.MemoryMb.Should().Be(256);
|
||||
profile.ResourceLimits.CpuCores.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentProfile_All_ContainsExpectedProfiles()
|
||||
{
|
||||
// Arrange & Act
|
||||
var profiles = EnvironmentProfile.All;
|
||||
|
||||
// Assert
|
||||
profiles.Should().HaveCount(5);
|
||||
profiles.Should().Contain(p => p.Name == "Standard");
|
||||
profiles.Should().Contain(p => p.Name == "HighLatency");
|
||||
profiles.Should().Contain(p => p.Name == "LowBandwidth");
|
||||
profiles.Should().Contain(p => p.Name == "PacketLoss");
|
||||
profiles.Should().Contain(p => p.Name == "ResourceConstrained");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NetworkProfile_RequiresNetworkShaping_ReturnsTrueWhenConfigured()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
new NetworkProfile { Latency = TimeSpan.FromMilliseconds(50) }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile { PacketLossRate = 0.01 }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile { BandwidthMbps = 10 }
|
||||
.RequiresNetworkShaping.Should().BeTrue();
|
||||
|
||||
new NetworkProfile()
|
||||
.RequiresNetworkShaping.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SkewTestRunner Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunAcrossProfiles_ExecutesTestForEachProfile()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var executedProfiles = new List<string>();
|
||||
|
||||
// Act
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () =>
|
||||
{
|
||||
executedProfiles.Add("executed");
|
||||
return Task.FromResult(new TestKitTestResult { Value = 1.0, DurationMs = 10 });
|
||||
},
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
// Assert
|
||||
report.ProfileCount.Should().Be(2);
|
||||
report.Results.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_ExecutesMultipleIterations()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.FromResult(new TestKitTestResult { Value = executionCount, DurationMs = 10 });
|
||||
},
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 5);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(5);
|
||||
result.Results.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_CalculatesAverages()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var values = new[] { 10.0, 20.0, 30.0 };
|
||||
var index = 0;
|
||||
|
||||
// Act
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () => Task.FromResult(new TestKitTestResult
|
||||
{
|
||||
Value = values[index++],
|
||||
DurationMs = 100
|
||||
}),
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 3);
|
||||
|
||||
// Assert
|
||||
result.AverageValue.Should().Be(20.0); // (10 + 20 + 30) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_RunWithProfile_HandlesErrors()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var iteration = 0;
|
||||
|
||||
// Act
|
||||
var result = await runner.RunWithProfile(
|
||||
test: () =>
|
||||
{
|
||||
iteration++;
|
||||
if (iteration == 2)
|
||||
{
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.FromResult(new TestKitTestResult { Value = 1.0, Success = true });
|
||||
},
|
||||
profile: EnvironmentProfile.Standard,
|
||||
iterations: 3);
|
||||
|
||||
// Assert
|
||||
result.Results.Should().HaveCount(3);
|
||||
result.SuccessRate.Should().BeApproximately(2.0 / 3.0, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_PassesWhenResultsAreEquivalent()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_FailsWhenSkewExceedsTolerance()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var values = new Queue<double>([100.0, 100.0, 100.0, 200.0, 200.0, 200.0]); // 100% difference
|
||||
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = values.Dequeue(), DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard, EnvironmentProfile.HighLatency]);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().Throw<SkewAssertException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewTestRunner_AssertEquivalence_IgnoresSingleProfile()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 100.0, DurationMs = 10 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
// Act & Assert - should not throw for single profile
|
||||
var act = () => runner.AssertEquivalence(report, tolerance: 0.05);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SkewReport Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SkewReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"generatedAt\"");
|
||||
json.Should().Contain("\"profileCount\"");
|
||||
json.Should().Contain("\"hasSkew\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SkewReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new SkewTestRunner();
|
||||
var report = await runner.RunAcrossProfiles(
|
||||
test: () => Task.FromResult(new TestKitTestResult { Value = 1.0 }),
|
||||
profiles: [EnvironmentProfile.Standard]);
|
||||
|
||||
// Act
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
// Assert
|
||||
markdown.Should().Contain("# Environment Skew Report");
|
||||
markdown.Should().Contain("| Profile |");
|
||||
markdown.Should().Contain("Standard");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TestResult Tests
|
||||
|
||||
[Fact]
|
||||
public void TestResult_Defaults_AreCorrect()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new TestKitTestResult();
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ProfileName.Should().BeEmpty();
|
||||
result.Metadata.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SkewAssertException Tests
|
||||
|
||||
[Fact]
|
||||
public void SkewAssertException_SetsMessage()
|
||||
{
|
||||
// Arrange & Act
|
||||
var ex = new SkewAssertException("Test message");
|
||||
|
||||
// Assert
|
||||
ex.Message.Should().Be("Test message");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EvidenceChainTests
|
||||
{
|
||||
[Fact]
|
||||
public void ArtifactHashStable_PassesWithCorrectHash()
|
||||
{
|
||||
var content = "test artifact";
|
||||
var expectedHash = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable(content, expectedHash);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_ThrowsWithIncorrectHash()
|
||||
{
|
||||
var content = "test artifact";
|
||||
var wrongHash = new string('0', 64);
|
||||
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable(content, wrongHash);
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact hash mismatch*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_ThrowsOnNullArtifact()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable((byte[])null!, "hash");
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_PassesWithDeterministicGenerator()
|
||||
{
|
||||
var counter = 0;
|
||||
Func<string> generator = () =>
|
||||
{
|
||||
counter++;
|
||||
return "immutable content";
|
||||
};
|
||||
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
|
||||
act.Should().NotThrow();
|
||||
counter.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_ThrowsWithNonDeterministicGenerator()
|
||||
{
|
||||
var counter = 0;
|
||||
Func<string> generator = () =>
|
||||
{
|
||||
counter++;
|
||||
return $"non-deterministic content {counter}";
|
||||
};
|
||||
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact not immutable*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_ThrowsWithLessThanTwoIterations()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(() => "content", iterations: 1);
|
||||
act.Should().Throw<ArgumentOutOfRangeException>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EvidenceChainTests
|
||||
{
|
||||
[Fact]
|
||||
public void EvidenceTraceabilityException_ConstructorWithMessage_SetsMessage()
|
||||
{
|
||||
var ex = new EvidenceTraceabilityException("Test error");
|
||||
|
||||
ex.Message.Should().Be("Test error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceTraceabilityException_ConstructorWithInnerException_SetsInnerException()
|
||||
{
|
||||
var inner = new InvalidOperationException("Inner");
|
||||
|
||||
var ex = new EvidenceTraceabilityException("Outer", inner);
|
||||
|
||||
ex.Message.Should().Be("Outer");
|
||||
ex.InnerException.Should().Be(inner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EvidenceChainTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeSha256_ReturnsLowercaseHex()
|
||||
{
|
||||
var content = "test content";
|
||||
|
||||
var hash = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
hash.Should().NotBeNullOrEmpty();
|
||||
hash.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_IsDeterministic()
|
||||
{
|
||||
var content = "deterministic test";
|
||||
|
||||
var hash1 = EvidenceChainAssert.ComputeSha256(content);
|
||||
var hash2 = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
hash1.Should().Be(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_Bytes_MatchesStringVersion()
|
||||
{
|
||||
var content = "test content";
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
var hashFromString = EvidenceChainAssert.ComputeSha256(content);
|
||||
var hashFromBytes = EvidenceChainAssert.ComputeSha256(bytes);
|
||||
|
||||
hashFromString.Should().Be(hashFromBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EvidenceChainTests
|
||||
{
|
||||
[Fact]
|
||||
public void EvidenceChainReporter_GenerateReport_ReturnsEmptyReportForNoAssemblies()
|
||||
{
|
||||
var reporter = new EvidenceChainReporter();
|
||||
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
report.TotalRequirements.Should().Be(0);
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.AssembliesScanned.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReporter_GenerateReport_ScansAssemblyForRequirements()
|
||||
{
|
||||
var reporter = new EvidenceChainReporter();
|
||||
reporter.AddAssembly(typeof(EvidenceChainTests).Assembly);
|
||||
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
report.AssembliesScanned.Should().Contain("StellaOps.TestKit.Tests");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
var reporter = new EvidenceChainReporter();
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
var json = report.ToJson();
|
||||
|
||||
json.Should().NotBeNullOrEmpty();
|
||||
json.Should().Contain("\"totalRequirements\"");
|
||||
json.Should().Contain("\"totalTests\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
var reporter = new EvidenceChainReporter();
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
markdown.Should().Contain("# Evidence Chain Traceability Report");
|
||||
markdown.Should().Contain("## Traceability Matrix");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EvidenceChainTests
|
||||
{
|
||||
[Fact]
|
||||
public void RequirementAttribute_Constructor_SetsRequirementId()
|
||||
{
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
attr.RequirementId.Should().Be("REQ-TEST-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_Constructor_ThrowsOnNullRequirementId()
|
||||
{
|
||||
var act = () => new RequirementAttribute(null!);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_OptionalProperties_DefaultToEmpty()
|
||||
{
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
attr.SprintTaskId.Should().BeEmpty();
|
||||
attr.ComplianceControl.Should().BeEmpty();
|
||||
attr.SourceDocument.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_ReturnsRequirementTrait()
|
||||
{
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
traits.Should().ContainSingle(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_IncludesSprintTaskWhenSet()
|
||||
{
|
||||
var attr = new RequirementAttribute("REQ-TEST-001") { SprintTaskId = "SPRINT-001" };
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
traits.Should().Contain(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
|
||||
traits.Should().Contain(t => t.Key == "SprintTask" && t.Value == "SPRINT-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_IncludesComplianceControlWhenSet()
|
||||
{
|
||||
var attr = new RequirementAttribute("REQ-TEST-001") { ComplianceControl = "SOC2-CC6.1" };
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
traits.Should().Contain(t => t.Key == "ComplianceControl" && t.Value == "SOC2-CC6.1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class EvidenceChainTests
|
||||
{
|
||||
[Fact]
|
||||
public void RequirementLinked_PassesWithValidRequirementId()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.RequirementLinked("REQ-TEST-001");
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_ThrowsWithEmptyRequirementId()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.RequirementLinked("");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*cannot be empty*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_ThrowsWithWhitespaceRequirementId()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.RequirementLinked(" ");
|
||||
act.Should().Throw<EvidenceTraceabilityException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_PassesWithAllComponents()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
"MyTests.TestMethod",
|
||||
"sha256:abc123");
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingRequirement()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"",
|
||||
"MyTests.TestMethod",
|
||||
"sha256:abc123");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Requirement ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingTestId()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
null!,
|
||||
"sha256:abc123");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Test ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingArtifactId()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
"MyTests.TestMethod",
|
||||
" ");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ReportsAllMissingComponents()
|
||||
{
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete("", "", "");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Requirement ID is missing*")
|
||||
.WithMessage("*Test ID is missing*")
|
||||
.WithMessage("*Artifact ID is missing*");
|
||||
}
|
||||
}
|
||||
@@ -1,400 +1,9 @@
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for evidence chain traceability infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class EvidenceChainTests
|
||||
public sealed partial class EvidenceChainTests
|
||||
{
|
||||
#region RequirementAttribute Tests
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_Constructor_SetsRequirementId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
// Assert
|
||||
attr.RequirementId.Should().Be("REQ-TEST-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_Constructor_ThrowsOnNullRequirementId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var act = () => new RequirementAttribute(null!);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_OptionalProperties_DefaultToEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
// Assert
|
||||
attr.SprintTaskId.Should().BeEmpty();
|
||||
attr.ComplianceControl.Should().BeEmpty();
|
||||
attr.SourceDocument.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_ReturnsRequirementTrait()
|
||||
{
|
||||
// Arrange
|
||||
var attr = new RequirementAttribute("REQ-TEST-001");
|
||||
|
||||
// Act
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
// Assert
|
||||
traits.Should().ContainSingle(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_IncludesSprintTaskWhenSet()
|
||||
{
|
||||
// Arrange
|
||||
var attr = new RequirementAttribute("REQ-TEST-001") { SprintTaskId = "SPRINT-001" };
|
||||
|
||||
// Act
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
// Assert
|
||||
traits.Should().Contain(t => t.Key == "Requirement" && t.Value == "REQ-TEST-001");
|
||||
traits.Should().Contain(t => t.Key == "SprintTask" && t.Value == "SPRINT-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementAttribute_GetTraits_IncludesComplianceControlWhenSet()
|
||||
{
|
||||
// Arrange
|
||||
var attr = new RequirementAttribute("REQ-TEST-001") { ComplianceControl = "SOC2-CC6.1" };
|
||||
|
||||
// Act
|
||||
var traits = attr.GetTraits();
|
||||
|
||||
// Assert
|
||||
traits.Should().Contain(t => t.Key == "ComplianceControl" && t.Value == "SOC2-CC6.1");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EvidenceChainAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_ReturnsLowercaseHex()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content";
|
||||
|
||||
// Act
|
||||
var hash = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
// Assert
|
||||
hash.Should().NotBeNullOrEmpty();
|
||||
hash.Should().MatchRegex("^[0-9a-f]{64}$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var content = "deterministic test";
|
||||
|
||||
// Act
|
||||
var hash1 = EvidenceChainAssert.ComputeSha256(content);
|
||||
var hash2 = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
// Assert
|
||||
hash1.Should().Be(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_Bytes_MatchesStringVersion()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content";
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||
|
||||
// Act
|
||||
var hashFromString = EvidenceChainAssert.ComputeSha256(content);
|
||||
var hashFromBytes = EvidenceChainAssert.ComputeSha256(bytes);
|
||||
|
||||
// Assert
|
||||
hashFromString.Should().Be(hashFromBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_PassesWithCorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test artifact";
|
||||
var expectedHash = EvidenceChainAssert.ComputeSha256(content);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable(content, expectedHash);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_ThrowsWithIncorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test artifact";
|
||||
var wrongHash = new string('0', 64);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable(content, wrongHash);
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact hash mismatch*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactHashStable_ThrowsOnNullArtifact()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactHashStable((byte[])null!, "hash");
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_PassesWithDeterministicGenerator()
|
||||
{
|
||||
// Arrange
|
||||
var counter = 0;
|
||||
Func<string> generator = () =>
|
||||
{
|
||||
counter++;
|
||||
return "immutable content";
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
|
||||
act.Should().NotThrow();
|
||||
counter.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_ThrowsWithNonDeterministicGenerator()
|
||||
{
|
||||
// Arrange
|
||||
var counter = 0;
|
||||
Func<string> generator = () =>
|
||||
{
|
||||
counter++;
|
||||
return $"non-deterministic content {counter}";
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(generator, iterations: 5);
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact not immutable*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactImmutable_ThrowsWithLessThanTwoIterations()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.ArtifactImmutable(() => "content", iterations: 1);
|
||||
act.Should().Throw<ArgumentOutOfRangeException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_PassesWithValidRequirementId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.RequirementLinked("REQ-TEST-001");
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_ThrowsWithEmptyRequirementId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.RequirementLinked("");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*cannot be empty*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirementLinked_ThrowsWithWhitespaceRequirementId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.RequirementLinked(" ");
|
||||
act.Should().Throw<EvidenceTraceabilityException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_PassesWithAllComponents()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
"MyTests.TestMethod",
|
||||
"sha256:abc123");
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingRequirement()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"",
|
||||
"MyTests.TestMethod",
|
||||
"sha256:abc123");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Requirement ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingTestId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
null!,
|
||||
"sha256:abc123");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Test ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ThrowsWithMissingArtifactId()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete(
|
||||
"REQ-001",
|
||||
"MyTests.TestMethod",
|
||||
" ");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Artifact ID is missing*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceabilityComplete_ReportsAllMissingComponents()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
var act = () => EvidenceChainAssert.TraceabilityComplete("", "", "");
|
||||
act.Should().Throw<EvidenceTraceabilityException>()
|
||||
.WithMessage("*Requirement ID is missing*")
|
||||
.WithMessage("*Test ID is missing*")
|
||||
.WithMessage("*Artifact ID is missing*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EvidenceChainReporter Tests
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReporter_GenerateReport_ReturnsEmptyReportForNoAssemblies()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
|
||||
// Act
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.TotalRequirements.Should().Be(0);
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.AssembliesScanned.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReporter_GenerateReport_ScansAssemblyForRequirements()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
reporter.AddAssembly(typeof(EvidenceChainTests).Assembly);
|
||||
|
||||
// Act
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.AssembliesScanned.Should().Contain("StellaOps.TestKit.Tests");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().NotBeNullOrEmpty();
|
||||
json.Should().Contain("\"totalRequirements\"");
|
||||
json.Should().Contain("\"totalTests\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceChainReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
// Arrange
|
||||
var reporter = new EvidenceChainReporter();
|
||||
var report = reporter.GenerateReport();
|
||||
|
||||
// Act
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
// Assert
|
||||
markdown.Should().Contain("# Evidence Chain Traceability Report");
|
||||
markdown.Should().Contain("## Traceability Matrix");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EvidenceTraceabilityException Tests
|
||||
|
||||
[Fact]
|
||||
public void EvidenceTraceabilityException_ConstructorWithMessage_SetsMessage()
|
||||
{
|
||||
// Arrange & Act
|
||||
var ex = new EvidenceTraceabilityException("Test error");
|
||||
|
||||
// Assert
|
||||
ex.Message.Should().Be("Test error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceTraceabilityException_ConstructorWithInnerException_SetsInnerException()
|
||||
{
|
||||
// Arrange
|
||||
var inner = new InvalidOperationException("Inner");
|
||||
|
||||
// Act
|
||||
var ex = new EvidenceTraceabilityException("Outer", inner);
|
||||
|
||||
// Assert
|
||||
ex.Message.Should().Be("Outer");
|
||||
ex.InnerException.Should().Be(inner);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test class with [Requirement] attribute for reporter testing.
|
||||
/// </summary>
|
||||
[Requirement("REQ-REPORTER-TEST-001")]
|
||||
public sealed class RequirementTestFixture
|
||||
{
|
||||
[Fact]
|
||||
[Requirement("REQ-REPORTER-TEST-002", SprintTaskId = "TEST-001")]
|
||||
public void SampleTestWithRequirement()
|
||||
{
|
||||
// This test exists to verify the reporter can scan for [Requirement] attributes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using StellaOps.TestKit.Incident;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IncidentTestGeneratorTests
|
||||
{
|
||||
private static readonly DateTimeOffset SampleOccurredAt =
|
||||
new(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
|
||||
|
||||
private static readonly string _sampleManifestJson = """
|
||||
{
|
||||
"schemaVersion": "2.0",
|
||||
"scan": {
|
||||
"id": "scan-001",
|
||||
"time": "2026-01-15T10:30:00Z",
|
||||
"policyDigest": "sha256:abc123",
|
||||
"scorePolicyDigest": "sha256:def456"
|
||||
},
|
||||
"reachability": {
|
||||
"analysisId": "analysis-001",
|
||||
"graphs": [
|
||||
{
|
||||
"kind": "static",
|
||||
"hash": "sha256:graph123",
|
||||
"analyzer": "java-callgraph"
|
||||
}
|
||||
],
|
||||
"runtimeTraces": []
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static readonly IncidentMetadata _sampleMetadata = new()
|
||||
{
|
||||
IncidentId = "INC-2026-001",
|
||||
OccurredAt = SampleOccurredAt,
|
||||
RootCause = "Race condition in concurrent writes",
|
||||
AffectedModules = ["EvidenceLocker", "Policy"],
|
||||
Severity = IncidentSeverity.P1,
|
||||
Title = "Evidence bundle duplication"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Incident;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IncidentTestGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_CreatesValidScaffold()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
scaffold.Should().NotBeNull();
|
||||
scaffold.Metadata.Should().Be(_sampleMetadata);
|
||||
scaffold.TestClassName.Should().Contain("INC_2026_001");
|
||||
scaffold.TestMethodName.Should().Contain("Validates");
|
||||
scaffold.ReplayManifestHash.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ExtractsInputFixtures()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
scaffold.InputFixtures.Should().ContainKey("scan");
|
||||
scaffold.InputFixtures.Should().ContainKey("reachabilityGraphs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ExtractsExpectedOutputs()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
scaffold.ExpectedOutputs.Should().ContainKey("policyDigest");
|
||||
scaffold.ExpectedOutputs["policyDigest"].Should().Be("sha256:abc123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_GeneratesImplementationNotes()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
scaffold.ImplementationNotes.Should().NotBeEmpty();
|
||||
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("INC-2026-001"));
|
||||
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("Race condition"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_SetsNamespaceFromModule()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
scaffold.Namespace.Should().Contain("EvidenceLocker");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ThrowsOnNullManifest()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var act = () => generator.GenerateFromManifestJson(null!, _sampleMetadata);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ThrowsOnNullMetadata()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var act = () => generator.GenerateFromManifestJson(_sampleManifestJson, null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Incident;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IncidentTestGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void IncidentMetadata_RequiredProperties_AreSet()
|
||||
{
|
||||
var metadata = new IncidentMetadata
|
||||
{
|
||||
IncidentId = "INC-001",
|
||||
OccurredAt = SampleOccurredAt,
|
||||
RootCause = "Test cause",
|
||||
AffectedModules = ["Module1"],
|
||||
Severity = IncidentSeverity.P2
|
||||
};
|
||||
|
||||
metadata.IncidentId.Should().Be("INC-001");
|
||||
metadata.RootCause.Should().Be("Test cause");
|
||||
metadata.Severity.Should().Be(IncidentSeverity.P2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncidentMetadata_OptionalProperties_HaveDefaults()
|
||||
{
|
||||
var metadata = new IncidentMetadata
|
||||
{
|
||||
IncidentId = "INC-001",
|
||||
OccurredAt = SampleOccurredAt,
|
||||
RootCause = "Test",
|
||||
AffectedModules = ["Module1"],
|
||||
Severity = IncidentSeverity.P3
|
||||
};
|
||||
|
||||
metadata.Title.Should().BeEmpty();
|
||||
metadata.ReportUrl.Should().BeEmpty();
|
||||
metadata.ResolvedAt.Should().BeNull();
|
||||
metadata.CorrelationIds.Should().BeEmpty();
|
||||
metadata.FixTaskId.Should().BeEmpty();
|
||||
metadata.Tags.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncidentSeverity_P1_HasCorrectValue()
|
||||
{
|
||||
((int)IncidentSeverity.P1).Should().Be(1);
|
||||
((int)IncidentSeverity.P2).Should().Be(2);
|
||||
((int)IncidentSeverity.P3).Should().Be(3);
|
||||
((int)IncidentSeverity.P4).Should().Be(4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Incident;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IncidentTestGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_AddsToRegistry()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold);
|
||||
|
||||
generator.RegisteredTests.Should().ContainKey("INC-2026-001");
|
||||
generator.RegisteredTests["INC-2026-001"].Should().Be(scaffold);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_OverwritesExisting()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold1 = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
var scaffold2 = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata with { Title = "Updated" });
|
||||
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold1);
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold2);
|
||||
|
||||
generator.RegisteredTests["INC-2026-001"].Metadata.Title.Should().Be("Updated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_ThrowsOnNullId()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
var act = () => generator.RegisterIncidentTest(null!, scaffold);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_ThrowsOnNullScaffold()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var act = () => generator.RegisterIncidentTest("INC-2026-001", null!);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Incident;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IncidentTestGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateReport_ReturnsEmptyForNoTests()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.Tests.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_CountsBySeverity()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var p1Metadata = _sampleMetadata with { IncidentId = "INC-001", Severity = IncidentSeverity.P1 };
|
||||
var p2Metadata = _sampleMetadata with { IncidentId = "INC-002", Severity = IncidentSeverity.P2 };
|
||||
|
||||
generator.RegisterIncidentTest(
|
||||
"INC-001",
|
||||
generator.GenerateFromManifestJson(_sampleManifestJson, p1Metadata));
|
||||
generator.RegisterIncidentTest(
|
||||
"INC-002",
|
||||
generator.GenerateFromManifestJson(_sampleManifestJson, p2Metadata));
|
||||
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
report.TotalTests.Should().Be(2);
|
||||
report.BySeveority.Should().ContainKey(IncidentSeverity.P1);
|
||||
report.BySeveority.Should().ContainKey(IncidentSeverity.P2);
|
||||
report.BySeveority[IncidentSeverity.P1].Should().Be(1);
|
||||
report.BySeveority[IncidentSeverity.P2].Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_CountsByModule()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
generator.RegisterIncidentTest(
|
||||
"INC-001",
|
||||
generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata));
|
||||
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
report.ByModule.Should().ContainKey("EvidenceLocker");
|
||||
report.ByModule.Should().ContainKey("Policy");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Incident;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IncidentTestGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestScaffold_GenerateTestCode_ProducesValidCSharp()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
var code = scaffold.GenerateTestCode();
|
||||
|
||||
code.Should().Contain("namespace StellaOps.EvidenceLocker.Tests.PostIncident");
|
||||
code.Should().Contain($"public sealed class {scaffold.TestClassName}");
|
||||
code.Should().Contain("[Fact]");
|
||||
code.Should().Contain("[Trait(\"Category\", TestCategories.PostIncident)]");
|
||||
code.Should().Contain($"[Trait(\"Incident\", \"{_sampleMetadata.IncidentId}\")]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_GenerateTestCode_IncludesIncidentMetadata()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
var code = scaffold.GenerateTestCode();
|
||||
|
||||
code.Should().Contain("INC-2026-001");
|
||||
code.Should().Contain("Race condition in concurrent writes");
|
||||
code.Should().Contain("IncidentSeverity.P1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_ToJson_ProducesValidJson()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
|
||||
var json = scaffold.ToJson();
|
||||
|
||||
json.Should().Contain("\"incidentId\"");
|
||||
json.Should().Contain("\"testClassName\"");
|
||||
json.Should().Contain("\"inputFixtures\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_FromJson_DeserializesCorrectly()
|
||||
{
|
||||
var generator = new IncidentTestGenerator();
|
||||
var original = generator.GenerateFromManifestJson(_sampleManifestJson, _sampleMetadata);
|
||||
var json = original.ToJson();
|
||||
|
||||
var deserialized = TestScaffold.FromJson(json);
|
||||
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Metadata.IncidentId.Should().Be(original.Metadata.IncidentId);
|
||||
deserialized.TestClassName.Should().Be(original.TestClassName);
|
||||
}
|
||||
}
|
||||
@@ -1,360 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Incident;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for post-incident test generation infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class IncidentTestGeneratorTests
|
||||
public sealed partial class IncidentTestGeneratorTests
|
||||
{
|
||||
private static readonly string SampleManifestJson = """
|
||||
{
|
||||
"schemaVersion": "2.0",
|
||||
"scan": {
|
||||
"id": "scan-001",
|
||||
"time": "2026-01-15T10:30:00Z",
|
||||
"policyDigest": "sha256:abc123",
|
||||
"scorePolicyDigest": "sha256:def456"
|
||||
},
|
||||
"reachability": {
|
||||
"analysisId": "analysis-001",
|
||||
"graphs": [
|
||||
{
|
||||
"kind": "static",
|
||||
"hash": "sha256:graph123",
|
||||
"analyzer": "java-callgraph"
|
||||
}
|
||||
],
|
||||
"runtimeTraces": []
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static readonly IncidentMetadata SampleMetadata = new()
|
||||
{
|
||||
IncidentId = "INC-2026-001",
|
||||
OccurredAt = DateTimeOffset.Parse("2026-01-15T10:30:00Z"),
|
||||
RootCause = "Race condition in concurrent writes",
|
||||
AffectedModules = ["EvidenceLocker", "Policy"],
|
||||
Severity = IncidentSeverity.P1,
|
||||
Title = "Evidence bundle duplication"
|
||||
};
|
||||
|
||||
#region IncidentMetadata Tests
|
||||
|
||||
[Fact]
|
||||
public void IncidentMetadata_RequiredProperties_AreSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var metadata = new IncidentMetadata
|
||||
{
|
||||
IncidentId = "INC-001",
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
RootCause = "Test cause",
|
||||
AffectedModules = ["Module1"],
|
||||
Severity = IncidentSeverity.P2
|
||||
};
|
||||
|
||||
// Assert
|
||||
metadata.IncidentId.Should().Be("INC-001");
|
||||
metadata.RootCause.Should().Be("Test cause");
|
||||
metadata.Severity.Should().Be(IncidentSeverity.P2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncidentMetadata_OptionalProperties_HaveDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
var metadata = new IncidentMetadata
|
||||
{
|
||||
IncidentId = "INC-001",
|
||||
OccurredAt = DateTimeOffset.UtcNow,
|
||||
RootCause = "Test",
|
||||
AffectedModules = ["Module1"],
|
||||
Severity = IncidentSeverity.P3
|
||||
};
|
||||
|
||||
// Assert
|
||||
metadata.Title.Should().BeEmpty();
|
||||
metadata.ReportUrl.Should().BeEmpty();
|
||||
metadata.ResolvedAt.Should().BeNull();
|
||||
metadata.CorrelationIds.Should().BeEmpty();
|
||||
metadata.FixTaskId.Should().BeEmpty();
|
||||
metadata.Tags.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncidentSeverity_P1_HasCorrectValue()
|
||||
{
|
||||
// Assert
|
||||
((int)IncidentSeverity.P1).Should().Be(1);
|
||||
((int)IncidentSeverity.P2).Should().Be(2);
|
||||
((int)IncidentSeverity.P3).Should().Be(3);
|
||||
((int)IncidentSeverity.P4).Should().Be(4);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IncidentTestGenerator Tests
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_CreatesValidScaffold()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.Should().NotBeNull();
|
||||
scaffold.Metadata.Should().Be(SampleMetadata);
|
||||
scaffold.TestClassName.Should().Contain("INC_2026_001");
|
||||
scaffold.TestMethodName.Should().Contain("Validates");
|
||||
scaffold.ReplayManifestHash.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ExtractsInputFixtures()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.InputFixtures.Should().ContainKey("scan");
|
||||
scaffold.InputFixtures.Should().ContainKey("reachabilityGraphs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ExtractsExpectedOutputs()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.ExpectedOutputs.Should().ContainKey("policyDigest");
|
||||
scaffold.ExpectedOutputs["policyDigest"].Should().Be("sha256:abc123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_GeneratesImplementationNotes()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.ImplementationNotes.Should().NotBeEmpty();
|
||||
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("INC-2026-001"));
|
||||
scaffold.ImplementationNotes.Should().Contain(n => n.Contains("Race condition"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_SetsNamespaceFromModule()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Assert
|
||||
scaffold.Namespace.Should().Contain("EvidenceLocker");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ThrowsOnNullManifest()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => generator.GenerateFromManifestJson(null!, SampleMetadata);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateFromManifestJson_ThrowsOnNullMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => generator.GenerateFromManifestJson(SampleManifestJson, null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RegisterIncidentTest Tests
|
||||
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_AddsToRegistry()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold);
|
||||
|
||||
// Assert
|
||||
generator.RegisteredTests.Should().ContainKey("INC-2026-001");
|
||||
generator.RegisteredTests["INC-2026-001"].Should().Be(scaffold);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterIncidentTest_OverwritesExisting()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold1 = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
var scaffold2 = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata with { Title = "Updated" });
|
||||
|
||||
// Act
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold1);
|
||||
generator.RegisterIncidentTest("INC-2026-001", scaffold2);
|
||||
|
||||
// Assert
|
||||
generator.RegisteredTests["INC-2026-001"].Metadata.Title.Should().Be("Updated");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GenerateReport Tests
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_ReturnsEmptyForNoTests()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
|
||||
// Act
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.Tests.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_CountsBySeverity()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var p1Metadata = SampleMetadata with { IncidentId = "INC-001", Severity = IncidentSeverity.P1 };
|
||||
var p2Metadata = SampleMetadata with { IncidentId = "INC-002", Severity = IncidentSeverity.P2 };
|
||||
|
||||
generator.RegisterIncidentTest("INC-001", generator.GenerateFromManifestJson(SampleManifestJson, p1Metadata));
|
||||
generator.RegisterIncidentTest("INC-002", generator.GenerateFromManifestJson(SampleManifestJson, p2Metadata));
|
||||
|
||||
// Act
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.TotalTests.Should().Be(2);
|
||||
report.BySeveority.Should().ContainKey(IncidentSeverity.P1);
|
||||
report.BySeveority.Should().ContainKey(IncidentSeverity.P2);
|
||||
report.BySeveority[IncidentSeverity.P1].Should().Be(1);
|
||||
report.BySeveority[IncidentSeverity.P2].Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateReport_CountsByModule()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
generator.RegisterIncidentTest("INC-001", generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata));
|
||||
|
||||
// Act
|
||||
var report = generator.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.ByModule.Should().ContainKey("EvidenceLocker");
|
||||
report.ByModule.Should().ContainKey("Policy");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TestScaffold Tests
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_GenerateTestCode_ProducesValidCSharp()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
var code = scaffold.GenerateTestCode();
|
||||
|
||||
// Assert
|
||||
code.Should().Contain("namespace StellaOps.EvidenceLocker.Tests.PostIncident");
|
||||
code.Should().Contain($"public sealed class {scaffold.TestClassName}");
|
||||
code.Should().Contain("[Fact]");
|
||||
code.Should().Contain("[Trait(\"Category\", TestCategories.PostIncident)]");
|
||||
code.Should().Contain($"[Trait(\"Incident\", \"{SampleMetadata.IncidentId}\")]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_GenerateTestCode_IncludesIncidentMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
var code = scaffold.GenerateTestCode();
|
||||
|
||||
// Assert
|
||||
code.Should().Contain("INC-2026-001");
|
||||
code.Should().Contain("Race condition in concurrent writes");
|
||||
code.Should().Contain("IncidentSeverity.P1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var scaffold = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
|
||||
// Act
|
||||
var json = scaffold.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"incidentId\"");
|
||||
json.Should().Contain("\"testClassName\"");
|
||||
json.Should().Contain("\"inputFixtures\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestScaffold_FromJson_DeserializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var generator = new IncidentTestGenerator();
|
||||
var original = generator.GenerateFromManifestJson(SampleManifestJson, SampleMetadata);
|
||||
var json = original.ToJson();
|
||||
|
||||
// Act
|
||||
var deserialized = TestScaffold.FromJson(json);
|
||||
|
||||
// Assert
|
||||
deserialized.Should().NotBeNull();
|
||||
deserialized!.Metadata.IncidentId.Should().Be(original.Metadata.IncidentId);
|
||||
deserialized.TestClassName.Should().Be(original.TestClassName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Analysis;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IntentCoverageReportTests
|
||||
{
|
||||
[Fact]
|
||||
public void IntentCoverageReportGenerator_AddAssembly_ThrowsOnNull()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
|
||||
var act = () => generator.AddAssembly(null!);
|
||||
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReportGenerator_EmptyAssemblies_ReturnsEmptyReport()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
|
||||
var report = generator.Generate();
|
||||
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.TaggedTests.Should().Be(0);
|
||||
report.UntaggedTests.Should().Be(0);
|
||||
report.TagCoveragePercent.Should().Be(0);
|
||||
report.ModuleStats.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReportGenerator_ScansSelfAssembly()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
generator.AddAssembly(typeof(IntentCoverageReportTests).Assembly);
|
||||
|
||||
var report = generator.Generate();
|
||||
|
||||
report.TotalTests.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReport_GeneratesWarning_WhenSafetyMissing()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
var report = generator.Generate();
|
||||
|
||||
report.Warnings.Should().Contain("No tests tagged with Safety intent");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Traits;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IntentCoverageReportTests
|
||||
{
|
||||
[Fact]
|
||||
public void IntentAttribute_CreatesTraits()
|
||||
{
|
||||
var attr = new IntentAttribute(TestIntents.Safety, "Security requirement");
|
||||
|
||||
attr.Intent.Should().Be(TestIntents.Safety);
|
||||
attr.Rationale.Should().Be("Security requirement");
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Safety"));
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("IntentRationale", "Security requirement"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentAttribute_WithoutRationale_OnlyIntentTrait()
|
||||
{
|
||||
var attr = new IntentAttribute(TestIntents.Operational);
|
||||
|
||||
attr.Intent.Should().Be(TestIntents.Operational);
|
||||
attr.Rationale.Should().BeEmpty();
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
traits.Should().ContainSingle();
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Operational"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Analysis;
|
||||
using StellaOps.TestKit.Traits;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IntentCoverageReportTests
|
||||
{
|
||||
[Fact]
|
||||
public void IntentCoverageReport_ToMarkdown_GeneratesValidOutput()
|
||||
{
|
||||
var report = new IntentCoverageReport
|
||||
{
|
||||
GeneratedAt = new DateTimeOffset(2026, 1, 27, 12, 0, 0, TimeSpan.Zero),
|
||||
TotalTests = 100,
|
||||
TaggedTests = 60,
|
||||
UntaggedTests = 40,
|
||||
TagCoveragePercent = 60.0,
|
||||
IntentDistribution = new Dictionary<string, int>
|
||||
{
|
||||
[TestIntents.Safety] = 20,
|
||||
[TestIntents.Regulatory] = 15,
|
||||
[TestIntents.Operational] = 25,
|
||||
[TestIntents.Performance] = 0,
|
||||
[TestIntents.Competitive] = 0
|
||||
},
|
||||
ModuleStats = new Dictionary<string, ModuleIntentStatsReadOnly>
|
||||
{
|
||||
["Policy"] = new ModuleIntentStatsReadOnly
|
||||
{
|
||||
ModuleName = "Policy",
|
||||
TotalTests = 50,
|
||||
TaggedTests = 30,
|
||||
TestsWithRationale = 10,
|
||||
TagCoveragePercent = 60.0,
|
||||
IntentCounts = new Dictionary<string, int>
|
||||
{
|
||||
[TestIntents.Safety] = 15,
|
||||
[TestIntents.Regulatory] = 15
|
||||
}
|
||||
}
|
||||
},
|
||||
Warnings = new List<string>
|
||||
{
|
||||
"Low intent coverage: only 60.0% of tests have intent tags"
|
||||
}
|
||||
};
|
||||
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
markdown.Should().Contain("# Intent Coverage Report");
|
||||
markdown.Should().Contain("Total tests: 100");
|
||||
markdown.Should().Contain("Tagged: 60 (60.0%)");
|
||||
markdown.Should().Contain("## Intent Distribution");
|
||||
markdown.Should().Contain("| Safety |");
|
||||
markdown.Should().Contain("## Per-Module Coverage");
|
||||
markdown.Should().Contain("| Policy |");
|
||||
markdown.Should().Contain("## Warnings");
|
||||
markdown.Should().Contain("Low intent coverage");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Traits;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class IntentCoverageReportTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestIntents_All_ContainsAllCategories()
|
||||
{
|
||||
TestIntents.All.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
TestIntents.Regulatory,
|
||||
TestIntents.Safety,
|
||||
TestIntents.Performance,
|
||||
TestIntents.Competitive,
|
||||
TestIntents.Operational
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestIntents_IsValid_ValidatesKnownIntents()
|
||||
{
|
||||
TestIntents.IsValid("Regulatory").Should().BeTrue();
|
||||
TestIntents.IsValid("Safety").Should().BeTrue();
|
||||
TestIntents.IsValid("Performance").Should().BeTrue();
|
||||
TestIntents.IsValid("Competitive").Should().BeTrue();
|
||||
TestIntents.IsValid("Operational").Should().BeTrue();
|
||||
|
||||
TestIntents.IsValid("regulatory").Should().BeTrue();
|
||||
TestIntents.IsValid("SAFETY").Should().BeTrue();
|
||||
|
||||
TestIntents.IsValid("Unknown").Should().BeFalse();
|
||||
TestIntents.IsValid("").Should().BeFalse();
|
||||
TestIntents.IsValid(" ").Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -1,159 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Analysis;
|
||||
using StellaOps.TestKit.Traits;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="IntentCoverageReportGenerator"/> and <see cref="IntentCoverageReport"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class IntentCoverageReportTests
|
||||
public sealed partial class IntentCoverageReportTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestIntents_All_ContainsAllCategories()
|
||||
{
|
||||
TestIntents.All.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
TestIntents.Regulatory,
|
||||
TestIntents.Safety,
|
||||
TestIntents.Performance,
|
||||
TestIntents.Competitive,
|
||||
TestIntents.Operational
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestIntents_IsValid_ValidatesKnownIntents()
|
||||
{
|
||||
TestIntents.IsValid("Regulatory").Should().BeTrue();
|
||||
TestIntents.IsValid("Safety").Should().BeTrue();
|
||||
TestIntents.IsValid("Performance").Should().BeTrue();
|
||||
TestIntents.IsValid("Competitive").Should().BeTrue();
|
||||
TestIntents.IsValid("Operational").Should().BeTrue();
|
||||
|
||||
// Case insensitive
|
||||
TestIntents.IsValid("regulatory").Should().BeTrue();
|
||||
TestIntents.IsValid("SAFETY").Should().BeTrue();
|
||||
|
||||
// Invalid
|
||||
TestIntents.IsValid("Unknown").Should().BeFalse();
|
||||
TestIntents.IsValid("").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentAttribute_CreatesTraits()
|
||||
{
|
||||
var attr = new IntentAttribute(TestIntents.Safety, "Security requirement");
|
||||
|
||||
attr.Intent.Should().Be(TestIntents.Safety);
|
||||
attr.Rationale.Should().Be("Security requirement");
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Safety"));
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("IntentRationale", "Security requirement"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentAttribute_WithoutRationale_OnlyIntentTrait()
|
||||
{
|
||||
var attr = new IntentAttribute(TestIntents.Operational);
|
||||
|
||||
attr.Intent.Should().Be(TestIntents.Operational);
|
||||
attr.Rationale.Should().BeEmpty();
|
||||
|
||||
var traits = attr.GetTraits();
|
||||
traits.Should().ContainSingle();
|
||||
traits.Should().Contain(new KeyValuePair<string, string>("Intent", "Operational"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReportGenerator_EmptyAssemblies_ReturnsEmptyReport()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
|
||||
var report = generator.Generate();
|
||||
|
||||
report.TotalTests.Should().Be(0);
|
||||
report.TaggedTests.Should().Be(0);
|
||||
report.UntaggedTests.Should().Be(0);
|
||||
report.TagCoveragePercent.Should().Be(0);
|
||||
report.ModuleStats.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReportGenerator_ScansSelfAssembly()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
generator.AddAssembly(typeof(IntentCoverageReportTests).Assembly);
|
||||
|
||||
var report = generator.Generate();
|
||||
|
||||
// This test class has tests, so we should find something
|
||||
report.TotalTests.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReport_ToMarkdown_GeneratesValidOutput()
|
||||
{
|
||||
var report = new IntentCoverageReport
|
||||
{
|
||||
GeneratedAt = new DateTimeOffset(2026, 1, 27, 12, 0, 0, TimeSpan.Zero),
|
||||
TotalTests = 100,
|
||||
TaggedTests = 60,
|
||||
UntaggedTests = 40,
|
||||
TagCoveragePercent = 60.0,
|
||||
IntentDistribution = new Dictionary<string, int>
|
||||
{
|
||||
[TestIntents.Safety] = 20,
|
||||
[TestIntents.Regulatory] = 15,
|
||||
[TestIntents.Operational] = 25,
|
||||
[TestIntents.Performance] = 0,
|
||||
[TestIntents.Competitive] = 0
|
||||
},
|
||||
ModuleStats = new Dictionary<string, ModuleIntentStatsReadOnly>
|
||||
{
|
||||
["Policy"] = new ModuleIntentStatsReadOnly
|
||||
{
|
||||
ModuleName = "Policy",
|
||||
TotalTests = 50,
|
||||
TaggedTests = 30,
|
||||
TestsWithRationale = 10,
|
||||
TagCoveragePercent = 60.0,
|
||||
IntentCounts = new Dictionary<string, int>
|
||||
{
|
||||
[TestIntents.Safety] = 15,
|
||||
[TestIntents.Regulatory] = 15
|
||||
}
|
||||
}
|
||||
},
|
||||
Warnings = new List<string>
|
||||
{
|
||||
"Low intent coverage: only 60.0% of tests have intent tags"
|
||||
}
|
||||
};
|
||||
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
markdown.Should().Contain("# Intent Coverage Report");
|
||||
markdown.Should().Contain("Total tests: 100");
|
||||
markdown.Should().Contain("Tagged: 60 (60.0%)");
|
||||
markdown.Should().Contain("## Intent Distribution");
|
||||
markdown.Should().Contain("| Safety |");
|
||||
markdown.Should().Contain("## Per-Module Coverage");
|
||||
markdown.Should().Contain("| Policy |");
|
||||
markdown.Should().Contain("## Warnings");
|
||||
markdown.Should().Contain("Low intent coverage");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntentCoverageReport_GeneratesWarning_WhenSafetyMissing()
|
||||
{
|
||||
var generator = new IntentCoverageReportGenerator();
|
||||
// Empty assemblies means no Safety tests
|
||||
var report = generator.Generate();
|
||||
|
||||
report.Warnings.Should().Contain("No tests tagged with Safety intent");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class InteropTests
|
||||
{
|
||||
[Fact]
|
||||
public void CompatibilityReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
var report = matrix.Analyze();
|
||||
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
markdown.Should().Contain("# Schema Compatibility Report");
|
||||
markdown.Should().Contain("| From | To |");
|
||||
markdown.Should().Contain("1.0");
|
||||
markdown.Should().Contain("2.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
var report = matrix.Analyze();
|
||||
|
||||
var json = report.ToJson();
|
||||
|
||||
json.Should().Contain("\"generatedAt\"");
|
||||
json.Should().Contain("\"versions\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class InteropTests
|
||||
{
|
||||
[Fact]
|
||||
public void ServiceEndpoint_DefaultValues_AreSet()
|
||||
{
|
||||
var endpoint = new ServiceEndpoint();
|
||||
|
||||
endpoint.ServiceName.Should().BeEmpty();
|
||||
endpoint.Version.Should().BeEmpty();
|
||||
endpoint.BaseUrl.Should().BeEmpty();
|
||||
endpoint.IsHealthy.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityResult_DefaultValues_AreSet()
|
||||
{
|
||||
var result = new CompatibilityResult();
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Errors.Should().BeEmpty();
|
||||
result.Warnings.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class InteropTests
|
||||
{
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_Analyze_GeneratesReport()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id", "name"] });
|
||||
|
||||
var report = matrix.Analyze();
|
||||
|
||||
report.Versions.Should().Contain(["1.0", "2.0"]);
|
||||
report.Pairs.Should().HaveCount(2);
|
||||
report.GeneratedAt.Should().NotBe(default);
|
||||
report.GeneratedAt.Offset.Should().Be(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_Analyze_SortsVersions()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
|
||||
var report = matrix.Analyze();
|
||||
|
||||
report.Versions.Should().ContainInOrder("1.0", "2.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_Analyze_DetectsTypeChanges()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id"],
|
||||
FieldTypes = new() { ["id"] = "int" }
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id"],
|
||||
FieldTypes = new() { ["id"] = "string" }
|
||||
});
|
||||
|
||||
var report = matrix.Analyze();
|
||||
|
||||
var pair = report.Pairs.First(p => p.FromVersion == "1.0" && p.ToVersion == "2.0");
|
||||
pair.IsBackwardCompatible.Should().BeFalse();
|
||||
pair.BackwardIssues.Should().Contain(i => i.Contains("Type changed"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class InteropTests
|
||||
{
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_AddVersion_StoresSchema()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
var schema = new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
};
|
||||
|
||||
matrix.AddVersion("1.0", schema);
|
||||
|
||||
matrix.Versions.Should().Contain("1.0");
|
||||
matrix.GetVersion("1.0").Should().Be(schema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsTrueWhenNoFieldsRemoved()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"],
|
||||
OptionalFields = ["description"]
|
||||
});
|
||||
|
||||
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsFalseWhenFieldsRemoved()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "oldField"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
|
||||
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsTrueWhenNewFieldsHaveDefaults()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"],
|
||||
FieldDefaults = new() { ["type"] = "default" }
|
||||
});
|
||||
|
||||
matrix.IsForwardCompatible("1.0", "2.0").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsFalseWhenNewRequiredFieldsHaveNoDefaults()
|
||||
{
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"]
|
||||
});
|
||||
|
||||
matrix.IsForwardCompatible("1.0", "2.0").Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class InteropTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestHandshake_ReturnsSuccessAsync()
|
||||
{
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
var server = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
var result = await fixture.TestHandshake(fixture.CurrentEndpoint!, server);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.ClientVersion.Should().Be(fixture.CurrentEndpoint!.Version);
|
||||
result.ServerVersion.Should().Be("1.0");
|
||||
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestMessageFormat_ReturnsSuccessAsync()
|
||||
{
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
var producer = await fixture.StartVersion("1.0", "Producer");
|
||||
var consumer = await fixture.StartVersion("2.0", "Consumer");
|
||||
|
||||
var result = await fixture.TestMessageFormat(producer, consumer, "EvidenceBundle");
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Message.Should().Contain("EvidenceBundle");
|
||||
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestSchemaMigration_ReturnsSuccessAsync()
|
||||
{
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
var result = await fixture.TestSchemaMigration("1.0", "2.0", new { id = 1 });
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.FromVersion.Should().Be("1.0");
|
||||
result.ToVersion.Should().Be("2.0");
|
||||
result.DataPreserved.Should().BeTrue();
|
||||
result.RollbackSupported.Should().BeTrue();
|
||||
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class InteropTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_Initialize_CreatesCurrentEndpointAsync()
|
||||
{
|
||||
var fixture = new VersionCompatibilityFixture
|
||||
{
|
||||
Config = new VersionCompatibilityConfig { CurrentVersion = "3.0" }
|
||||
};
|
||||
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
fixture.CurrentEndpoint.Should().NotBeNull();
|
||||
fixture.CurrentEndpoint!.Version.Should().Be("3.0");
|
||||
fixture.CurrentEndpoint.IsHealthy.Should().BeTrue();
|
||||
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StartVersion_CreatesEndpointAsync()
|
||||
{
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
var endpoint = await fixture.StartVersion("1.0", "EvidenceLocker");
|
||||
|
||||
endpoint.Should().NotBeNull();
|
||||
endpoint.Version.Should().Be("1.0");
|
||||
endpoint.ServiceName.Should().Be("EvidenceLocker");
|
||||
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StartVersion_ReturnsSameEndpointForSameVersionAsync()
|
||||
{
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
var endpoint1 = await fixture.StartVersion("1.0", "Service");
|
||||
var endpoint2 = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
endpoint1.Should().BeSameAs(endpoint2);
|
||||
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StopVersion_RemovesEndpointAsync()
|
||||
{
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
await fixture.StopVersion("1.0", "Service");
|
||||
var newEndpoint = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
newEndpoint.Should().NotBeNull();
|
||||
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,360 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Interop;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for cross-version interoperability testing infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class InteropTests
|
||||
public sealed partial class InteropTests
|
||||
{
|
||||
#region SchemaVersionMatrix Tests
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_AddVersion_StoresSchema()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
var schema = new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
};
|
||||
|
||||
// Act
|
||||
matrix.AddVersion("1.0", schema);
|
||||
|
||||
// Assert
|
||||
matrix.Versions.Should().Contain("1.0");
|
||||
matrix.GetVersion("1.0").Should().Be(schema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsTrueWhenNoFieldsRemoved()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"], // Added field, none removed
|
||||
OptionalFields = ["description"]
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsBackwardCompatible_ReturnsFalseWhenFieldsRemoved()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "oldField"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"] // oldField removed
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsBackwardCompatible("1.0", "2.0").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsTrueWhenNewFieldsHaveDefaults()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"],
|
||||
FieldDefaults = new() { ["type"] = "default" }
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsForwardCompatible("1.0", "2.0").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_IsForwardCompatible_ReturnsFalseWhenNewRequiredFieldsHaveNoDefaults()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name"]
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id", "name", "type"] // No default for "type"
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
matrix.IsForwardCompatible("1.0", "2.0").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_Analyze_GeneratesReport()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id", "name"] });
|
||||
|
||||
// Act
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Assert
|
||||
report.Versions.Should().Contain(["1.0", "2.0"]);
|
||||
report.Pairs.Should().HaveCount(2); // 1.0->2.0 and 2.0->1.0
|
||||
report.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchemaVersionMatrix_Analyze_DetectsTypeChanges()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id"],
|
||||
FieldTypes = new() { ["id"] = "int" }
|
||||
});
|
||||
matrix.AddVersion("2.0", new SchemaDefinition
|
||||
{
|
||||
RequiredFields = ["id"],
|
||||
FieldTypes = new() { ["id"] = "string" } // Type changed
|
||||
});
|
||||
|
||||
// Act
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Assert
|
||||
var pair = report.Pairs.First(p => p.FromVersion == "1.0" && p.ToVersion == "2.0");
|
||||
pair.IsBackwardCompatible.Should().BeFalse();
|
||||
pair.BackwardIssues.Should().Contain(i => i.Contains("Type changed"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityReport_ToMarkdown_ProducesValidMarkdown()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
matrix.AddVersion("2.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Act
|
||||
var markdown = report.ToMarkdown();
|
||||
|
||||
// Assert
|
||||
markdown.Should().Contain("# Schema Compatibility Report");
|
||||
markdown.Should().Contain("| From | To |");
|
||||
markdown.Should().Contain("1.0");
|
||||
markdown.Should().Contain("2.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var matrix = new SchemaVersionMatrix();
|
||||
matrix.AddVersion("1.0", new SchemaDefinition { RequiredFields = ["id"] });
|
||||
var report = matrix.Analyze();
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"generatedAt\"");
|
||||
json.Should().Contain("\"versions\"");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region VersionCompatibilityFixture Tests
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_Initialize_CreatesCurrentEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture
|
||||
{
|
||||
Config = new VersionCompatibilityConfig { CurrentVersion = "3.0" }
|
||||
};
|
||||
|
||||
// Act
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Assert
|
||||
fixture.CurrentEndpoint.Should().NotBeNull();
|
||||
fixture.CurrentEndpoint!.Version.Should().Be("3.0");
|
||||
fixture.CurrentEndpoint.IsHealthy.Should().BeTrue();
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StartVersion_CreatesEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var endpoint = await fixture.StartVersion("1.0", "EvidenceLocker");
|
||||
|
||||
// Assert
|
||||
endpoint.Should().NotBeNull();
|
||||
endpoint.Version.Should().Be("1.0");
|
||||
endpoint.ServiceName.Should().Be("EvidenceLocker");
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StartVersion_ReturnsSameEndpointForSameVersion()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var endpoint1 = await fixture.StartVersion("1.0", "Service");
|
||||
var endpoint2 = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Assert
|
||||
endpoint1.Should().BeSameAs(endpoint2);
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestHandshake_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
var server = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Act
|
||||
var result = await fixture.TestHandshake(fixture.CurrentEndpoint!, server);
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.ClientVersion.Should().Be(fixture.CurrentEndpoint!.Version);
|
||||
result.ServerVersion.Should().Be("1.0");
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestMessageFormat_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
var producer = await fixture.StartVersion("1.0", "Producer");
|
||||
var consumer = await fixture.StartVersion("2.0", "Consumer");
|
||||
|
||||
// Act
|
||||
var result = await fixture.TestMessageFormat(producer, consumer, "EvidenceBundle");
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Message.Should().Contain("EvidenceBundle");
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_TestSchemaMigration_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
|
||||
// Act
|
||||
var result = await fixture.TestSchemaMigration("1.0", "2.0", new { id = 1 });
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.FromVersion.Should().Be("1.0");
|
||||
result.ToVersion.Should().Be("2.0");
|
||||
result.DataPreserved.Should().BeTrue();
|
||||
result.RollbackSupported.Should().BeTrue();
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionCompatibilityFixture_StopVersion_RemovesEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new VersionCompatibilityFixture();
|
||||
await fixture.InitializeAsync();
|
||||
await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Act
|
||||
await fixture.StopVersion("1.0", "Service");
|
||||
var newEndpoint = await fixture.StartVersion("1.0", "Service");
|
||||
|
||||
// Assert - new endpoint should be created (different base URL due to increment)
|
||||
newEndpoint.Should().NotBeNull();
|
||||
|
||||
// Cleanup
|
||||
await fixture.DisposeAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ServiceEndpoint Tests
|
||||
|
||||
[Fact]
|
||||
public void ServiceEndpoint_DefaultValues_AreSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var endpoint = new ServiceEndpoint();
|
||||
|
||||
// Assert
|
||||
endpoint.ServiceName.Should().BeEmpty();
|
||||
endpoint.Version.Should().BeEmpty();
|
||||
endpoint.BaseUrl.Should().BeEmpty();
|
||||
endpoint.IsHealthy.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CompatibilityResult Tests
|
||||
|
||||
[Fact]
|
||||
public void CompatibilityResult_DefaultValues_AreSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new CompatibilityResult();
|
||||
|
||||
// Assert
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Errors.Should().BeEmpty();
|
||||
result.Warnings.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class LongevityTests
|
||||
{
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasMemoryLeak_ReturnsFalseInitially()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
metrics.HasMemoryLeak().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasConnectionPoolLeak_DetectsLeaks()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.RecordConnectionPool(active: 10, leaked: 2);
|
||||
|
||||
metrics.HasConnectionPoolLeak(maxLeaked: 0).Should().BeTrue();
|
||||
metrics.HasConnectionPoolLeak(maxLeaked: 2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasDrift_DetectsDriftingCounters()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.RecordCounter("counter", 100);
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.RecordCounter("counter", 2000);
|
||||
|
||||
metrics.HasDrift("counter", threshold: 1000).Should().BeTrue();
|
||||
metrics.HasDrift("counter", threshold: 5000).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasDrift_ReturnsFalseWhenCounterMissing()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.CaptureSnapshot();
|
||||
|
||||
metrics.HasDrift("missing", threshold: 1).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_MemoryGrowthRate_CalculatesSlope()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
metrics.CaptureSnapshot();
|
||||
}
|
||||
|
||||
var growthRate = metrics.MemoryGrowthRate;
|
||||
|
||||
double.IsNaN(growthRate).Should().BeFalse();
|
||||
double.IsInfinity(growthRate).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_GenerateReport_CreatesValidReport()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.RecordCounter("test", 42);
|
||||
|
||||
var report = metrics.GenerateReport();
|
||||
|
||||
report.Should().NotBeNull();
|
||||
report.SnapshotCount.Should().Be(2);
|
||||
report.BaselineMemory.Should().BeGreaterThan(0);
|
||||
report.GeneratedAt.Should().NotBe(default);
|
||||
report.GeneratedAt.Offset.Should().Be(TimeSpan.Zero);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class LongevityTests
|
||||
{
|
||||
[Fact]
|
||||
public void StabilityMetrics_CaptureBaseline_SetsBaseline()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
metrics.MemoryBaseline.Should().BeGreaterThan(0);
|
||||
metrics.Snapshots.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_CaptureSnapshot_AddsSnapshot()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.CaptureSnapshot();
|
||||
|
||||
metrics.Snapshots.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_RecordCounter_StoresValue()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
metrics.RecordCounter("requests_total", 100);
|
||||
|
||||
metrics.CounterValues.Should().ContainKey("requests_total");
|
||||
metrics.CounterValues["requests_total"].Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_RecordConnectionPool_StoresValues()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
metrics.RecordConnectionPool(active: 5, leaked: 1);
|
||||
|
||||
metrics.ConnectionPoolActive.Should().Be(5);
|
||||
metrics.ConnectionPoolLeaked.Should().Be(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class LongevityTests
|
||||
{
|
||||
[Fact]
|
||||
public void StabilityReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
var report = metrics.GenerateReport();
|
||||
|
||||
var json = report.ToJson();
|
||||
|
||||
json.Should().Contain("\"snapshotCount\"");
|
||||
json.Should().Contain("\"baselineMemory\"");
|
||||
json.Should().Contain("\"hasMemoryLeak\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_Passed_ReturnsTrueWhenNoIssues()
|
||||
{
|
||||
var report = new StabilityReport
|
||||
{
|
||||
HasMemoryLeak = false,
|
||||
HasConnectionPoolLeak = false,
|
||||
DriftingCounters = []
|
||||
};
|
||||
|
||||
report.Passed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_Passed_ReturnsFalseWhenMemoryLeak()
|
||||
{
|
||||
var report = new StabilityReport
|
||||
{
|
||||
HasMemoryLeak = true,
|
||||
HasConnectionPoolLeak = false,
|
||||
DriftingCounters = []
|
||||
};
|
||||
|
||||
report.Passed.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class LongevityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunExtended_RunsForDurationAsync()
|
||||
{
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 100 }
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
var report = await runner.RunExtended(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
duration: TimeSpan.FromMilliseconds(100));
|
||||
|
||||
executionCount.Should().BeGreaterThan(0);
|
||||
report.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunExtended_RespectsCancellationAsync()
|
||||
{
|
||||
var runner = new StabilityTestRunner();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var executionCount = 0;
|
||||
|
||||
cts.CancelAfter(50);
|
||||
await runner.RunExtended(
|
||||
scenario: async () =>
|
||||
{
|
||||
executionCount++;
|
||||
await Task.Delay(10);
|
||||
},
|
||||
duration: TimeSpan.FromHours(1),
|
||||
cancellationToken: cts.Token);
|
||||
|
||||
executionCount.Should().BeLessThan(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityTestRunner_Metrics_ExposesUnderlyingMetrics()
|
||||
{
|
||||
var runner = new StabilityTestRunner();
|
||||
|
||||
runner.Metrics.Should().NotBeNull();
|
||||
runner.Metrics.Should().BeOfType<StabilityMetrics>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class LongevityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_ContinuesOnErrorIfNotConfiguredAsync()
|
||||
{
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig
|
||||
{
|
||||
StopOnError = false,
|
||||
SnapshotInterval = 10
|
||||
}
|
||||
};
|
||||
var executionCount = 0;
|
||||
var errorCount = 0;
|
||||
|
||||
await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
if (executionCount % 3 == 0)
|
||||
{
|
||||
errorCount++;
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 10);
|
||||
|
||||
executionCount.Should().Be(10);
|
||||
errorCount.Should().BeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class LongevityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_ExecutesScenarioAsync()
|
||||
{
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 5 }
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 10);
|
||||
|
||||
executionCount.Should().Be(10);
|
||||
report.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_CapturesSnapshotsAsync()
|
||||
{
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 2 }
|
||||
};
|
||||
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () => Task.CompletedTask,
|
||||
iterations: 10);
|
||||
|
||||
report.SnapshotCount.Should().BeGreaterThan(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_StopsOnErrorIfConfiguredAsync()
|
||||
{
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig
|
||||
{
|
||||
StopOnError = true,
|
||||
SnapshotInterval = 1
|
||||
}
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
if (executionCount == 5)
|
||||
{
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 100);
|
||||
|
||||
executionCount.Should().Be(5);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,388 +1,9 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Longevity;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for time-extended stability testing infrastructure.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class LongevityTests
|
||||
public sealed partial class LongevityTests
|
||||
{
|
||||
#region StabilityMetrics Tests
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_CaptureBaseline_SetsBaseline()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
|
||||
// Act
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Assert
|
||||
metrics.MemoryBaseline.Should().BeGreaterThan(0);
|
||||
metrics.Snapshots.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_CaptureSnapshot_AddsSnapshot()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.CaptureSnapshot();
|
||||
|
||||
// Assert
|
||||
metrics.Snapshots.Should().HaveCount(3); // Baseline + 2 snapshots
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_RecordCounter_StoresValue()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act
|
||||
metrics.RecordCounter("requests_total", 100);
|
||||
|
||||
// Assert
|
||||
metrics.CounterValues.Should().ContainKey("requests_total");
|
||||
metrics.CounterValues["requests_total"].Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_RecordConnectionPool_StoresValues()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act
|
||||
metrics.RecordConnectionPool(active: 5, leaked: 1);
|
||||
|
||||
// Assert
|
||||
metrics.ConnectionPoolActive.Should().Be(5);
|
||||
metrics.ConnectionPoolLeaked.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasMemoryLeak_ReturnsFalseInitially()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Act & Assert
|
||||
metrics.HasMemoryLeak().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasConnectionPoolLeak_DetectsLeaks()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.RecordConnectionPool(active: 10, leaked: 2);
|
||||
|
||||
// Act & Assert
|
||||
metrics.HasConnectionPoolLeak(maxLeaked: 0).Should().BeTrue();
|
||||
metrics.HasConnectionPoolLeak(maxLeaked: 2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_HasDrift_DetectsDriftingCounters()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.RecordCounter("counter", 100);
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.RecordCounter("counter", 2000);
|
||||
|
||||
// Act & Assert
|
||||
metrics.HasDrift("counter", threshold: 1000).Should().BeTrue();
|
||||
metrics.HasDrift("counter", threshold: 5000).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_MemoryGrowthRate_CalculatesSlope()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
|
||||
// Capture multiple snapshots (growth rate requires at least 2)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
metrics.CaptureSnapshot();
|
||||
}
|
||||
|
||||
// Act
|
||||
var growthRate = metrics.MemoryGrowthRate;
|
||||
|
||||
// Assert - just verify it's calculated and is a valid value
|
||||
double.IsNaN(growthRate).Should().BeFalse();
|
||||
double.IsInfinity(growthRate).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityMetrics_GenerateReport_CreatesValidReport()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
metrics.CaptureSnapshot();
|
||||
metrics.RecordCounter("test", 42);
|
||||
|
||||
// Act
|
||||
var report = metrics.GenerateReport();
|
||||
|
||||
// Assert
|
||||
report.Should().NotBeNull();
|
||||
report.SnapshotCount.Should().Be(2);
|
||||
report.BaselineMemory.Should().BeGreaterThan(0);
|
||||
report.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var metrics = new StabilityMetrics();
|
||||
metrics.CaptureBaseline();
|
||||
var report = metrics.GenerateReport();
|
||||
|
||||
// Act
|
||||
var json = report.ToJson();
|
||||
|
||||
// Assert
|
||||
json.Should().Contain("\"snapshotCount\"");
|
||||
json.Should().Contain("\"baselineMemory\"");
|
||||
json.Should().Contain("\"hasMemoryLeak\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_Passed_ReturnsTrueWhenNoIssues()
|
||||
{
|
||||
// Arrange
|
||||
var report = new StabilityReport
|
||||
{
|
||||
HasMemoryLeak = false,
|
||||
HasConnectionPoolLeak = false,
|
||||
DriftingCounters = []
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
report.Passed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityReport_Passed_ReturnsFalseWhenMemoryLeak()
|
||||
{
|
||||
// Arrange
|
||||
var report = new StabilityReport
|
||||
{
|
||||
HasMemoryLeak = true,
|
||||
HasConnectionPoolLeak = false,
|
||||
DriftingCounters = []
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
report.Passed.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StabilityTestRunner Tests
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_ExecutesScenario()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 5 }
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 10);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(10);
|
||||
report.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_CapturesSnapshots()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 2 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () => Task.CompletedTask,
|
||||
iterations: 10);
|
||||
|
||||
// Assert
|
||||
report.SnapshotCount.Should().BeGreaterThan(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_StopsOnErrorIfConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig
|
||||
{
|
||||
StopOnError = true,
|
||||
SnapshotInterval = 1
|
||||
}
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
if (executionCount == 5)
|
||||
{
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 100);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunIterations_ContinuesOnErrorIfNotConfigured()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig
|
||||
{
|
||||
StopOnError = false,
|
||||
SnapshotInterval = 10
|
||||
}
|
||||
};
|
||||
var executionCount = 0;
|
||||
var errorCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunIterations(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
if (executionCount % 3 == 0)
|
||||
{
|
||||
errorCount++;
|
||||
throw new InvalidOperationException("Test error");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
iterations: 10);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().Be(10);
|
||||
errorCount.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunExtended_RunsForDuration()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner
|
||||
{
|
||||
Config = new StabilityTestConfig { SnapshotInterval = 100 }
|
||||
};
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
var report = await runner.RunExtended(
|
||||
scenario: () =>
|
||||
{
|
||||
executionCount++;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
duration: TimeSpan.FromMilliseconds(100));
|
||||
|
||||
// Assert
|
||||
executionCount.Should().BeGreaterThan(0);
|
||||
report.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StabilityTestRunner_RunExtended_RespectsCancellation()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner();
|
||||
using var cts = new CancellationTokenSource();
|
||||
var executionCount = 0;
|
||||
|
||||
// Act
|
||||
cts.CancelAfter(50);
|
||||
var report = await runner.RunExtended(
|
||||
scenario: async () =>
|
||||
{
|
||||
executionCount++;
|
||||
await Task.Delay(10);
|
||||
},
|
||||
duration: TimeSpan.FromHours(1),
|
||||
cancellationToken: cts.Token);
|
||||
|
||||
// Assert
|
||||
executionCount.Should().BeLessThan(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StabilityTestRunner_Metrics_ExposesUnderlyingMetrics()
|
||||
{
|
||||
// Arrange
|
||||
var runner = new StabilityTestRunner();
|
||||
|
||||
// Act & Assert
|
||||
runner.Metrics.Should().NotBeNull();
|
||||
runner.Metrics.Should().BeOfType<StabilityMetrics>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StabilityTestConfig Tests
|
||||
|
||||
[Fact]
|
||||
public void StabilityTestConfig_Defaults_AreReasonable()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StabilityTestConfig();
|
||||
|
||||
// Assert
|
||||
config.SnapshotInterval.Should().Be(100);
|
||||
config.MemoryLeakThresholdPercent.Should().Be(10);
|
||||
config.MaxConnectionPoolLeaks.Should().Be(0);
|
||||
config.StopOnError.Should().BeFalse();
|
||||
config.IterationDelay.Should().Be(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void ContractViolationException_ContainsMessage()
|
||||
{
|
||||
var ex = new ContractViolationException("Test violation");
|
||||
|
||||
ex.Message.Should().Be("Test violation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContractViolationException_WithInnerException()
|
||||
{
|
||||
var inner = new InvalidOperationException("Inner error");
|
||||
var ex = new ContractViolationException("Outer error", inner);
|
||||
|
||||
ex.Message.Should().Be("Outer error");
|
||||
ex.InnerException.Should().Be(inner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void NoSensitiveData_ContainsEmail_ThrowsContractViolation()
|
||||
{
|
||||
var records = new[]
|
||||
{
|
||||
new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "User test@example.com logged in",
|
||||
StateValues = new Dictionary<string, object?>()
|
||||
}
|
||||
};
|
||||
|
||||
var piiPatterns = new[]
|
||||
{
|
||||
new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*PII*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogLevelAppropriate_WithinRange_NoException()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Warning,
|
||||
Message = "Test warning"
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.LogLevelAppropriate(
|
||||
record,
|
||||
LogLevel.Information,
|
||||
LogLevel.Error);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogLevelAppropriate_OutsideRange_ThrowsContractViolation()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Critical,
|
||||
Message = "Critical error"
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.LogLevelAppropriate(
|
||||
record,
|
||||
LogLevel.Information,
|
||||
LogLevel.Warning);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*Critical*outside*range*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void HasRequiredFields_AllPresent_NoException()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "Test message",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["CorrelationId"] = "abc-123",
|
||||
["TenantId"] = "acme"
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "TenantId");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredFields_Missing_ThrowsContractViolation()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "Test message",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["CorrelationId"] = "abc-123"
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "MissingField");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*MissingField*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoSensitiveData_Clean_NoException()
|
||||
{
|
||||
var records = new[]
|
||||
{
|
||||
new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "User logged in successfully",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["UserId"] = "user-123"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var piiPatterns = new[]
|
||||
{
|
||||
new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void MetricNamesMatchPattern_PassesForSnakeCase()
|
||||
{
|
||||
using var meter = new Meter("TestMeter7");
|
||||
using var capture = new MetricsCapture("TestMeter7");
|
||||
|
||||
var counter = meter.CreateCounter<long>("requests_total");
|
||||
counter.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricNamesMatchPattern(
|
||||
capture,
|
||||
"^[a-z_]+$");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetricNamesMatchPattern_FindsViolations()
|
||||
{
|
||||
using var meter = new Meter("TestMeter8");
|
||||
using var capture = new MetricsCapture("TestMeter8");
|
||||
|
||||
var valid = meter.CreateCounter<long>("requests_total");
|
||||
var invalid = meter.CreateCounter<long>("InvalidMetric");
|
||||
valid.Add(1);
|
||||
invalid.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricNamesMatchPattern(
|
||||
capture,
|
||||
"^[a-z_]+$");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*InvalidMetric*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void CounterMonotonic_AlwaysIncreasing_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter5");
|
||||
using var capture = new MetricsCapture("TestMeter5");
|
||||
|
||||
var counter = meter.CreateCounter<long>("monotonic_counter");
|
||||
counter.Add(1);
|
||||
counter.Add(2);
|
||||
counter.Add(3);
|
||||
|
||||
var act = () => MetricsContractAssert.CounterMonotonic(capture, "monotonic_counter");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GaugeInBounds_WithinRange_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter6");
|
||||
using var capture = new MetricsCapture("TestMeter6");
|
||||
|
||||
var gauge = meter.CreateObservableGauge("memory_usage_bytes", () => 500);
|
||||
_ = capture.GetValues("memory_usage_bytes");
|
||||
|
||||
var act = () => MetricsContractAssert.GaugeInBounds(
|
||||
capture,
|
||||
"memory_usage_bytes",
|
||||
0,
|
||||
1000);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void MetricExists_Present_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter1");
|
||||
using var capture = new MetricsCapture("TestMeter1");
|
||||
|
||||
var counter = meter.CreateCounter<long>("test_requests_total");
|
||||
counter.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricExists(capture, "test_requests_total");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetricExists_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var meter = new Meter("TestMeter2");
|
||||
using var capture = new MetricsCapture("TestMeter2");
|
||||
|
||||
var counter = meter.CreateCounter<long>("some_other_metric");
|
||||
counter.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricExists(capture, "missing_metric");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*missing_metric*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LabelCardinalityBounded_WithinThreshold_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter3");
|
||||
using var capture = new MetricsCapture("TestMeter3");
|
||||
|
||||
var counter = meter.CreateCounter<long>("http_requests_total");
|
||||
counter.Add(1, new KeyValuePair<string, object?>("method", "GET"));
|
||||
counter.Add(1, new KeyValuePair<string, object?>("method", "POST"));
|
||||
|
||||
var act = () => MetricsContractAssert.LabelCardinalityBounded(
|
||||
capture,
|
||||
"http_requests_total",
|
||||
maxLabels: 10);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LabelCardinalityBounded_ExceedsThreshold_ThrowsContractViolation()
|
||||
{
|
||||
using var meter = new Meter("TestMeter4");
|
||||
using var capture = new MetricsCapture("TestMeter4");
|
||||
|
||||
var counter = meter.CreateCounter<long>("requests_by_user");
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
counter.Add(1, new KeyValuePair<string, object?>("user_id", $"user-{i}"));
|
||||
}
|
||||
|
||||
var act = () => MetricsContractAssert.LabelCardinalityBounded(
|
||||
capture,
|
||||
"requests_by_user",
|
||||
maxLabels: 5);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*cardinality*exceeds*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Diagnostics;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void AttributeCardinality_ExceedsThreshold_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource6");
|
||||
using var capture = new OtelCapture("TestSource6");
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
using (var activity = source.StartActivity($"Span{i}"))
|
||||
{
|
||||
activity?.SetTag("request_id", $"id-{i}");
|
||||
}
|
||||
}
|
||||
|
||||
var act = () => OTelContractAssert.AttributeCardinality(capture, "request_id", maxCardinality: 5);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*cardinality*exceeds*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
[Fact]
|
||||
public void HasRequiredSpans_AllPresent_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource");
|
||||
using var capture = new OtelCapture("TestSource");
|
||||
|
||||
using (source.StartActivity("Span1")) { }
|
||||
using (source.StartActivity("Span2")) { }
|
||||
|
||||
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "Span2");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredSpans_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource2");
|
||||
using var capture = new OtelCapture("TestSource2");
|
||||
|
||||
using (source.StartActivity("Span1")) { }
|
||||
|
||||
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "MissingSpan");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*MissingSpan*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanHasAttributes_AllPresent_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource3");
|
||||
using var capture = new OtelCapture("TestSource3");
|
||||
|
||||
using (var activity = source.StartActivity("TestSpan"))
|
||||
{
|
||||
activity?.SetTag("user_id", "123");
|
||||
activity?.SetTag("tenant_id", "acme");
|
||||
}
|
||||
|
||||
var span = capture.CapturedActivities.First();
|
||||
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "tenant_id");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanHasAttributes_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource4");
|
||||
using var capture = new OtelCapture("TestSource4");
|
||||
|
||||
using (var activity = source.StartActivity("TestSpan"))
|
||||
{
|
||||
activity?.SetTag("user_id", "123");
|
||||
}
|
||||
|
||||
var span = capture.CapturedActivities.First();
|
||||
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "missing_attr");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*missing_attr*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeCardinality_WithinThreshold_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource5");
|
||||
using var capture = new OtelCapture("TestSource5");
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
using (var activity = source.StartActivity($"Span{i}"))
|
||||
{
|
||||
activity?.SetTag("status", i % 3 == 0 ? "ok" : "error");
|
||||
}
|
||||
}
|
||||
|
||||
var act = () => OTelContractAssert.AttributeCardinality(capture, "status", maxCardinality: 10);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
@@ -1,360 +1,9 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TestKit.Observability;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for observability contract assertions.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class ObservabilityContractTests
|
||||
public sealed partial class ObservabilityContractTests
|
||||
{
|
||||
#region OTelContractAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredSpans_AllPresent_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource");
|
||||
using var capture = new OtelCapture("TestSource");
|
||||
|
||||
using (source.StartActivity("Span1")) { }
|
||||
using (source.StartActivity("Span2")) { }
|
||||
|
||||
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "Span2");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredSpans_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource2");
|
||||
using var capture = new OtelCapture("TestSource2");
|
||||
|
||||
using (source.StartActivity("Span1")) { }
|
||||
|
||||
var act = () => OTelContractAssert.HasRequiredSpans(capture, "Span1", "MissingSpan");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*MissingSpan*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanHasAttributes_AllPresent_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource3");
|
||||
using var capture = new OtelCapture("TestSource3");
|
||||
|
||||
using (var activity = source.StartActivity("TestSpan"))
|
||||
{
|
||||
activity?.SetTag("user_id", "123");
|
||||
activity?.SetTag("tenant_id", "acme");
|
||||
}
|
||||
|
||||
var span = capture.CapturedActivities.First();
|
||||
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "tenant_id");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanHasAttributes_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource4");
|
||||
using var capture = new OtelCapture("TestSource4");
|
||||
|
||||
using (var activity = source.StartActivity("TestSpan"))
|
||||
{
|
||||
activity?.SetTag("user_id", "123");
|
||||
}
|
||||
|
||||
var span = capture.CapturedActivities.First();
|
||||
var act = () => OTelContractAssert.SpanHasAttributes(span, "user_id", "missing_attr");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*missing_attr*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeCardinality_WithinThreshold_NoException()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource5");
|
||||
using var capture = new OtelCapture("TestSource5");
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
using (var activity = source.StartActivity($"Span{i}"))
|
||||
{
|
||||
activity?.SetTag("status", i % 3 == 0 ? "ok" : "error"); // 2 unique values
|
||||
}
|
||||
}
|
||||
|
||||
var act = () => OTelContractAssert.AttributeCardinality(capture, "status", maxCardinality: 10);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttributeCardinality_ExceedsThreshold_ThrowsContractViolation()
|
||||
{
|
||||
using var source = new ActivitySource("TestSource6");
|
||||
using var capture = new OtelCapture("TestSource6");
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
using (var activity = source.StartActivity($"Span{i}"))
|
||||
{
|
||||
activity?.SetTag("request_id", $"id-{i}"); // 10 unique values
|
||||
}
|
||||
}
|
||||
|
||||
var act = () => OTelContractAssert.AttributeCardinality(capture, "request_id", maxCardinality: 5);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*cardinality*exceeds*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LogContractAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredFields_AllPresent_NoException()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "Test message",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["CorrelationId"] = "abc-123",
|
||||
["TenantId"] = "acme"
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "TenantId");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasRequiredFields_Missing_ThrowsContractViolation()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "Test message",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["CorrelationId"] = "abc-123"
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.HasRequiredFields(record, "CorrelationId", "MissingField");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*MissingField*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoSensitiveData_Clean_NoException()
|
||||
{
|
||||
var records = new[]
|
||||
{
|
||||
new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "User logged in successfully",
|
||||
StateValues = new Dictionary<string, object?>
|
||||
{
|
||||
["UserId"] = "user-123"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var piiPatterns = new[] { new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") };
|
||||
|
||||
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoSensitiveData_ContainsEmail_ThrowsContractViolation()
|
||||
{
|
||||
var records = new[]
|
||||
{
|
||||
new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Information,
|
||||
Message = "User test@example.com logged in",
|
||||
StateValues = new Dictionary<string, object?>()
|
||||
}
|
||||
};
|
||||
|
||||
var piiPatterns = new[] { new Regex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") };
|
||||
|
||||
var act = () => LogContractAssert.NoSensitiveData(records, piiPatterns);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*PII*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogLevelAppropriate_WithinRange_NoException()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Warning,
|
||||
Message = "Test warning"
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.LogLevelAppropriate(record, LogLevel.Information, LogLevel.Error);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogLevelAppropriate_OutsideRange_ThrowsContractViolation()
|
||||
{
|
||||
var record = new CapturedLogRecord
|
||||
{
|
||||
LogLevel = LogLevel.Critical,
|
||||
Message = "Critical error"
|
||||
};
|
||||
|
||||
var act = () => LogContractAssert.LogLevelAppropriate(record, LogLevel.Information, LogLevel.Warning);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*Critical*outside*range*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MetricsContractAssert Tests
|
||||
|
||||
[Fact]
|
||||
public void MetricExists_Present_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter1");
|
||||
using var capture = new MetricsCapture("TestMeter1");
|
||||
|
||||
var counter = meter.CreateCounter<long>("test_requests_total");
|
||||
counter.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricExists(capture, "test_requests_total");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetricExists_Missing_ThrowsContractViolation()
|
||||
{
|
||||
using var meter = new Meter("TestMeter2");
|
||||
using var capture = new MetricsCapture("TestMeter2");
|
||||
|
||||
var counter = meter.CreateCounter<long>("some_other_metric");
|
||||
counter.Add(1);
|
||||
|
||||
var act = () => MetricsContractAssert.MetricExists(capture, "missing_metric");
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*missing_metric*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LabelCardinalityBounded_WithinThreshold_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter3");
|
||||
using var capture = new MetricsCapture("TestMeter3");
|
||||
|
||||
var counter = meter.CreateCounter<long>("http_requests_total");
|
||||
counter.Add(1, new KeyValuePair<string, object?>("method", "GET"));
|
||||
counter.Add(1, new KeyValuePair<string, object?>("method", "POST"));
|
||||
|
||||
var act = () => MetricsContractAssert.LabelCardinalityBounded(capture, "http_requests_total", maxLabels: 10);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LabelCardinalityBounded_ExceedsThreshold_ThrowsContractViolation()
|
||||
{
|
||||
using var meter = new Meter("TestMeter4");
|
||||
using var capture = new MetricsCapture("TestMeter4");
|
||||
|
||||
var counter = meter.CreateCounter<long>("requests_by_user");
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
counter.Add(1, new KeyValuePair<string, object?>("user_id", $"user-{i}"));
|
||||
}
|
||||
|
||||
var act = () => MetricsContractAssert.LabelCardinalityBounded(capture, "requests_by_user", maxLabels: 5);
|
||||
|
||||
act.Should().Throw<ContractViolationException>()
|
||||
.WithMessage("*cardinality*exceeds*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CounterMonotonic_AlwaysIncreasing_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter5");
|
||||
using var capture = new MetricsCapture("TestMeter5");
|
||||
|
||||
var counter = meter.CreateCounter<long>("monotonic_counter");
|
||||
counter.Add(1);
|
||||
counter.Add(2);
|
||||
counter.Add(3);
|
||||
|
||||
var act = () => MetricsContractAssert.CounterMonotonic(capture, "monotonic_counter");
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GaugeInBounds_WithinRange_NoException()
|
||||
{
|
||||
using var meter = new Meter("TestMeter6");
|
||||
using var capture = new MetricsCapture("TestMeter6");
|
||||
|
||||
var gauge = meter.CreateObservableGauge("memory_usage_bytes", () => 500);
|
||||
// Force a measurement
|
||||
capture.GetValues("memory_usage_bytes");
|
||||
|
||||
// This test validates the API structure - actual observable gauge testing
|
||||
// requires meter listener callbacks which are triggered asynchronously
|
||||
var act = () => MetricsContractAssert.GaugeInBounds(capture, "memory_usage_bytes", 0, 1000);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ContractViolationException Tests
|
||||
|
||||
[Fact]
|
||||
public void ContractViolationException_ContainsMessage()
|
||||
{
|
||||
var ex = new ContractViolationException("Test violation");
|
||||
|
||||
ex.Message.Should().Be("Test violation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContractViolationException_WithInnerException()
|
||||
{
|
||||
var inner = new InvalidOperationException("Inner error");
|
||||
var ex = new ContractViolationException("Outer error", inner);
|
||||
|
||||
ex.Message.Should().Be("Outer error");
|
||||
ex.InnerException.Should().Be(inner);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using StellaOps.TestKit.Evidence;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.TestKit.Tests;
|
||||
|
||||
[Requirement("REQ-REPORTER-TEST-001")]
|
||||
public sealed class RequirementTestFixture
|
||||
{
|
||||
[Fact]
|
||||
[Requirement("REQ-REPORTER-TEST-002", SprintTaskId = "TEST-001")]
|
||||
public void SampleTestWithRequirement()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ public sealed class TestKitFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task ConnectorHttpFixture_CreateClient_ReturnsCannedResponse()
|
||||
public async Task ConnectorHttpFixture_CreateClient_ReturnsCannedResponseAsync()
|
||||
{
|
||||
using var fixture = new ConnectorHttpFixture();
|
||||
fixture.AddJsonResponse("https://example.test/api", "{\"status\":\"ok\"}");
|
||||
|
||||
Reference in New Issue
Block a user