Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -2,11 +2,13 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Canonical.Json.Tests;
|
||||
|
||||
public class CanonJsonTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_SameInput_ProducesSameHash()
|
||||
{
|
||||
var obj = new { foo = "bar", baz = 42, nested = new { x = 1, y = 2 } };
|
||||
@@ -18,7 +20,8 @@ public class CanonJsonTests
|
||||
Assert.Equal(CanonJson.Sha256Hex(bytes1), CanonJson.Sha256Hex(bytes2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_SortsKeysAlphabetically()
|
||||
{
|
||||
var obj = new { z = 3, a = 1, m = 2 };
|
||||
@@ -28,7 +31,8 @@ public class CanonJsonTests
|
||||
Assert.Matches(@"\{""a"":1,""m"":2,""z"":3\}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_HandlesNestedObjects()
|
||||
{
|
||||
var obj = new { outer = new { z = 9, a = 1 }, inner = new { b = 2 } };
|
||||
@@ -39,7 +43,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"""outer"":{""a"":1,""z"":9}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_HandlesArrays()
|
||||
{
|
||||
var obj = new { items = new[] { 3, 1, 2 } };
|
||||
@@ -49,7 +54,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"""items"":[3,1,2]", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_HandlesNullValues()
|
||||
{
|
||||
var obj = new { name = "test", value = (string?)null };
|
||||
@@ -58,7 +64,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"""value"":null", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_HandlesBooleans()
|
||||
{
|
||||
var obj = new { enabled = true, disabled = false };
|
||||
@@ -68,7 +75,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"""enabled"":true", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_HandlesDecimals()
|
||||
{
|
||||
var obj = new { value = 3.14159, integer = 42 };
|
||||
@@ -78,7 +86,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"""value"":3.14159", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_HandlesEmptyObject()
|
||||
{
|
||||
var obj = new { };
|
||||
@@ -87,7 +96,8 @@ public class CanonJsonTests
|
||||
Assert.Equal("{}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_HandlesEmptyArray()
|
||||
{
|
||||
var obj = new { items = Array.Empty<int>() };
|
||||
@@ -96,7 +106,8 @@ public class CanonJsonTests
|
||||
Assert.Equal(@"{""items"":[]}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_WithCustomOptions_UsesOptions()
|
||||
{
|
||||
var obj = new { MyProperty = "test" };
|
||||
@@ -109,7 +120,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"""my_property"":""test""", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_RawJsonBytes_SortsKeys()
|
||||
{
|
||||
var rawJson = Encoding.UTF8.GetBytes(@"{""z"":3,""a"":1}");
|
||||
@@ -119,7 +131,8 @@ public class CanonJsonTests
|
||||
Assert.Equal(@"{""a"":1,""z"":3}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sha256Hex_ProducesLowercaseHex()
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes("test");
|
||||
@@ -128,7 +141,8 @@ public class CanonJsonTests
|
||||
Assert.Matches(@"^[0-9a-f]{64}$", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sha256Hex_ProducesConsistentHash()
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes("deterministic input");
|
||||
@@ -139,7 +153,8 @@ public class CanonJsonTests
|
||||
Assert.Equal(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sha256Prefixed_IncludesPrefix()
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes("test");
|
||||
@@ -149,7 +164,8 @@ public class CanonJsonTests
|
||||
Assert.Equal(71, hash.Length); // "sha256:" (7) + 64 hex chars
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Hash_CanonicalizesAndHashes()
|
||||
{
|
||||
var obj = new { z = 3, a = 1 };
|
||||
@@ -161,7 +177,8 @@ public class CanonJsonTests
|
||||
Assert.Matches(@"^[0-9a-f]{64}$", hash1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashPrefixed_CanonicalizesAndHashesWithPrefix()
|
||||
{
|
||||
var obj = new { name = "test" };
|
||||
@@ -171,7 +188,8 @@ public class CanonJsonTests
|
||||
Assert.StartsWith("sha256:", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DifferentObjects_ProduceDifferentHashes()
|
||||
{
|
||||
var obj1 = new { value = 1 };
|
||||
@@ -183,7 +201,8 @@ public class CanonJsonTests
|
||||
Assert.NotEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void KeyOrderDoesNotAffectHash()
|
||||
{
|
||||
// These should produce the same hash because keys are sorted
|
||||
@@ -198,7 +217,8 @@ public class CanonJsonTests
|
||||
CanonJson.Sha256Hex(canonical2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_DeeplyNestedStructure()
|
||||
{
|
||||
var obj = new
|
||||
@@ -219,7 +239,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"""a"":{""nested"":{""a"":1,""b"":2}}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_ArrayOfObjects_SortsObjectKeys()
|
||||
{
|
||||
// Use raw JSON to test mixed object shapes in array
|
||||
@@ -232,7 +253,8 @@ public class CanonJsonTests
|
||||
Assert.Contains(@"{""a"":1,""b"":2}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_UnicodeStrings()
|
||||
{
|
||||
var obj = new { greeting = "Привет мир", emoji = "🚀" };
|
||||
@@ -249,7 +271,8 @@ public class CanonJsonTests
|
||||
Assert.Contains("emoji", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_SpecialCharactersInStrings()
|
||||
{
|
||||
var obj = new { path = "C:\\Users\\test", quote = "He said \"hello\"" };
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Canonical.Json.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -13,20 +14,23 @@ public class CanonVersionTests
|
||||
{
|
||||
#region Version Constants
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void V1_HasExpectedValue()
|
||||
{
|
||||
Assert.Equal("stella:canon:v1", CanonVersion.V1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VersionFieldName_HasUnderscorePrefix()
|
||||
{
|
||||
Assert.Equal("_canonVersion", CanonVersion.VersionFieldName);
|
||||
Assert.StartsWith("_", CanonVersion.VersionFieldName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Current_EqualsV1()
|
||||
{
|
||||
Assert.Equal(CanonVersion.V1, CanonVersion.Current);
|
||||
@@ -36,35 +40,40 @@ public class CanonVersionTests
|
||||
|
||||
#region IsVersioned Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsVersioned_VersionedJson_ReturnsTrue()
|
||||
{
|
||||
var json = """{"_canonVersion":"stella:canon:v1","foo":"bar"}"""u8;
|
||||
Assert.True(CanonVersion.IsVersioned(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsVersioned_LegacyJson_ReturnsFalse()
|
||||
{
|
||||
var json = """{"foo":"bar"}"""u8;
|
||||
Assert.False(CanonVersion.IsVersioned(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsVersioned_EmptyJson_ReturnsFalse()
|
||||
{
|
||||
var json = "{}"u8;
|
||||
Assert.False(CanonVersion.IsVersioned(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsVersioned_TooShort_ReturnsFalse()
|
||||
{
|
||||
var json = """{"_ca":"v"}"""u8;
|
||||
Assert.False(CanonVersion.IsVersioned(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsVersioned_WrongFieldName_ReturnsFalse()
|
||||
{
|
||||
var json = """{"_version":"stella:canon:v1","foo":"bar"}"""u8;
|
||||
@@ -75,28 +84,32 @@ public class CanonVersionTests
|
||||
|
||||
#region ExtractVersion
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractVersion_VersionedJson_ReturnsVersion()
|
||||
{
|
||||
var json = """{"_canonVersion":"stella:canon:v1","foo":"bar"}"""u8;
|
||||
Assert.Equal("stella:canon:v1", CanonVersion.ExtractVersion(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractVersion_CustomVersion_ReturnsVersion()
|
||||
{
|
||||
var json = """{"_canonVersion":"custom:v2","foo":"bar"}"""u8;
|
||||
Assert.Equal("custom:v2", CanonVersion.ExtractVersion(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractVersion_LegacyJson_ReturnsNull()
|
||||
{
|
||||
var json = """{"foo":"bar"}"""u8;
|
||||
Assert.Null(CanonVersion.ExtractVersion(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractVersion_EmptyVersion_ReturnsNull()
|
||||
{
|
||||
var json = """{"_canonVersion":"","foo":"bar"}"""u8;
|
||||
@@ -107,7 +120,8 @@ public class CanonVersionTests
|
||||
|
||||
#region CanonicalizeVersioned
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_IncludesVersionMarker()
|
||||
{
|
||||
var obj = new { foo = "bar" };
|
||||
@@ -118,7 +132,8 @@ public class CanonVersionTests
|
||||
Assert.Contains("\"foo\":\"bar\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_VersionMarkerIsFirst()
|
||||
{
|
||||
var obj = new { aaa = 1, zzz = 2 };
|
||||
@@ -131,7 +146,8 @@ public class CanonVersionTests
|
||||
Assert.True(versionIndex < aaaIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_SortsOtherKeys()
|
||||
{
|
||||
var obj = new { z = 3, a = 1, m = 2 };
|
||||
@@ -142,7 +158,8 @@ public class CanonVersionTests
|
||||
Assert.Matches(@"\{""_canonVersion"":""[^""]+"",""a"":1,""m"":2,""z"":3\}", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_CustomVersion_UsesProvidedVersion()
|
||||
{
|
||||
var obj = new { foo = "bar" };
|
||||
@@ -152,14 +169,16 @@ public class CanonVersionTests
|
||||
Assert.Contains("\"_canonVersion\":\"custom:v99\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_NullVersion_ThrowsArgumentException()
|
||||
{
|
||||
var obj = new { foo = "bar" };
|
||||
Assert.ThrowsAny<ArgumentException>(() => CanonJson.CanonicalizeVersioned(obj, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_EmptyVersion_ThrowsArgumentException()
|
||||
{
|
||||
var obj = new { foo = "bar" };
|
||||
@@ -170,7 +189,8 @@ public class CanonVersionTests
|
||||
|
||||
#region Hash Difference (Versioned vs Legacy)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashVersioned_DiffersFromLegacyHash()
|
||||
{
|
||||
var obj = new { foo = "bar", count = 42 };
|
||||
@@ -181,7 +201,8 @@ public class CanonVersionTests
|
||||
Assert.NotEqual(legacyHash, versionedHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashVersionedPrefixed_DiffersFromLegacyHashPrefixed()
|
||||
{
|
||||
var obj = new { foo = "bar", count = 42 };
|
||||
@@ -194,7 +215,8 @@ public class CanonVersionTests
|
||||
Assert.StartsWith("sha256:", legacyHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashVersioned_SameInput_ProducesSameHash()
|
||||
{
|
||||
var obj = new { foo = "bar", count = 42 };
|
||||
@@ -205,7 +227,8 @@ public class CanonVersionTests
|
||||
Assert.Equal(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashVersioned_DifferentVersions_ProduceDifferentHashes()
|
||||
{
|
||||
var obj = new { foo = "bar" };
|
||||
@@ -220,7 +243,8 @@ public class CanonVersionTests
|
||||
|
||||
#region Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_SameInput_ProducesSameBytes()
|
||||
{
|
||||
var obj = new { name = "test", value = 123, nested = new { x = 1, y = 2 } };
|
||||
@@ -231,7 +255,8 @@ public class CanonVersionTests
|
||||
Assert.Equal(bytes1, bytes2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_DifferentPropertyOrder_ProducesSameBytes()
|
||||
{
|
||||
// Create two objects with same properties but defined in different order
|
||||
@@ -247,7 +272,8 @@ public class CanonVersionTests
|
||||
Assert.Equal(bytes1, bytes2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_StableAcrossMultipleCalls()
|
||||
{
|
||||
var obj = new { id = Guid.Parse("12345678-1234-1234-1234-123456789012"), name = "stable" };
|
||||
@@ -264,7 +290,8 @@ public class CanonVersionTests
|
||||
|
||||
#region Golden File / Snapshot Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_KnownInput_ProducesKnownOutput()
|
||||
{
|
||||
// Golden test: exact output for known input to detect algorithm changes
|
||||
@@ -276,7 +303,8 @@ public class CanonVersionTests
|
||||
Assert.Equal("""{"_canonVersion":"stella:canon:v1","message":"hello","number":42}""", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashVersioned_KnownInput_ProducesKnownHash()
|
||||
{
|
||||
// Golden test: exact hash for known input to detect algorithm changes
|
||||
@@ -294,7 +322,8 @@ public class CanonVersionTests
|
||||
Assert.Equal(hash, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_NestedObject_ProducesCorrectOutput()
|
||||
{
|
||||
var obj = new
|
||||
@@ -313,7 +342,8 @@ public class CanonVersionTests
|
||||
|
||||
#region Backward Compatibility
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanVersion_CanDistinguishLegacyFromVersioned()
|
||||
{
|
||||
var obj = new { foo = "bar" };
|
||||
@@ -325,7 +355,8 @@ public class CanonVersionTests
|
||||
Assert.True(CanonVersion.IsVersioned(versioned));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LegacyCanonicalize_StillWorks()
|
||||
{
|
||||
// Ensure we haven't broken the legacy canonicalize method
|
||||
@@ -341,7 +372,8 @@ public class CanonVersionTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_EmptyObject_IncludesOnlyVersion()
|
||||
{
|
||||
var obj = new { };
|
||||
@@ -351,7 +383,8 @@ public class CanonVersionTests
|
||||
Assert.Equal("""{"_canonVersion":"stella:canon:v1"}""", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_WithSpecialCharacters_HandlesCorrectly()
|
||||
{
|
||||
var obj = new { message = "hello\nworld", special = "quote:\"test\"" };
|
||||
@@ -365,7 +398,8 @@ public class CanonVersionTests
|
||||
Assert.Equal("stella:canon:v1", parsed.GetProperty("_canonVersion").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizeVersioned_WithUnicodeCharacters_HandlesCorrectly()
|
||||
{
|
||||
var obj = new { greeting = "こんにちは", emoji = "🚀" };
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Cryptography.Plugin.EIDAS.DependencyInjection;
|
||||
using StellaOps.Cryptography.Plugin.EIDAS.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Plugin.EIDAS.Tests;
|
||||
|
||||
public class EidasCryptoProviderTests
|
||||
@@ -70,13 +71,15 @@ public class EidasCryptoProviderTests
|
||||
?? throw new InvalidOperationException("Failed to resolve EidasCryptoProvider");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Provider_Name_IsEidas()
|
||||
{
|
||||
Assert.Equal("eidas", _provider.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(CryptoCapability.Signing, "ECDSA-P256", true)]
|
||||
[InlineData(CryptoCapability.Signing, "ECDSA-P384", true)]
|
||||
[InlineData(CryptoCapability.Signing, "ECDSA-P521", true)]
|
||||
@@ -94,19 +97,22 @@ public class EidasCryptoProviderTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetPasswordHasher_ThrowsNotSupported()
|
||||
{
|
||||
Assert.Throws<NotSupportedException>(() => _provider.GetPasswordHasher("PBKDF2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetHasher_ThrowsNotSupported()
|
||||
{
|
||||
Assert.Throws<NotSupportedException>(() => _provider.GetHasher("SHA256"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetSigner_ReturnsEidasSigner()
|
||||
{
|
||||
var keyRef = new CryptoKeyReference("test-key-local");
|
||||
@@ -117,7 +123,8 @@ public class EidasCryptoProviderTests
|
||||
Assert.Equal("ECDSA-P256", signer.AlgorithmId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpsertSigningKey_AddsKey()
|
||||
{
|
||||
var keyRef = new CryptoKeyReference("test-upsert");
|
||||
@@ -134,7 +141,8 @@ public class EidasCryptoProviderTests
|
||||
Assert.Contains(keys, k => k.Reference.KeyId == "test-upsert");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RemoveSigningKey_RemovesKey()
|
||||
{
|
||||
var keyRef = new CryptoKeyReference("test-remove");
|
||||
@@ -153,14 +161,16 @@ public class EidasCryptoProviderTests
|
||||
Assert.DoesNotContain(_provider.GetSigningKeys(), k => k.Reference.KeyId == "test-remove");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RemoveSigningKey_ReturnsFalseForNonExistentKey()
|
||||
{
|
||||
var removed = _provider.RemoveSigningKey("non-existent-key");
|
||||
Assert.False(removed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_WithLocalKey_ReturnsSignature()
|
||||
{
|
||||
// Note: This test will use the stub implementation
|
||||
@@ -176,7 +186,8 @@ public class EidasCryptoProviderTests
|
||||
Assert.NotEmpty(signature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithLocalKey_ReturnsTrue()
|
||||
{
|
||||
// Note: This test will use the stub implementation
|
||||
@@ -192,7 +203,8 @@ public class EidasCryptoProviderTests
|
||||
Assert.True(isValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_WithTspKey_ReturnsSignature()
|
||||
{
|
||||
// Note: This test will use the stub TSP implementation
|
||||
@@ -208,7 +220,8 @@ public class EidasCryptoProviderTests
|
||||
Assert.NotEmpty(signature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExportPublicJsonWebKey_ReturnsStubJwk()
|
||||
{
|
||||
var keyRef = new CryptoKeyReference("test-key-local");
|
||||
@@ -226,7 +239,8 @@ public class EidasCryptoProviderTests
|
||||
|
||||
public class EidasDependencyInjectionTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddEidasCryptoProviders_RegistersServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -249,7 +263,8 @@ public class EidasDependencyInjectionTests
|
||||
Assert.IsType<EidasCryptoProvider>(provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddEidasCryptoProviders_WithAction_RegistersServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
@@ -15,13 +15,15 @@ namespace StellaOps.Cryptography.Plugin.SmRemote.Tests;
|
||||
|
||||
public class SmRemoteHttpProviderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Service_EndToEnd_SignsAndVerifies()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SM_SOFT_ALLOWED", "1");
|
||||
using var app = new WebApplicationFactory<Program>()
|
||||
.WithWebHostBuilder(_ => { });
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var client = app.CreateClient();
|
||||
var status = await client.GetFromJsonAsync<SmStatusResponse>("/status");
|
||||
status.Should().NotBeNull();
|
||||
@@ -39,7 +41,8 @@ public class SmRemoteHttpProviderTests
|
||||
verify!.Valid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignsAndVerifiesViaHttp()
|
||||
{
|
||||
var handler = new StubHandler();
|
||||
|
||||
@@ -10,6 +10,7 @@ using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.SmSoft;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Plugin.SmSoft.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,13 +39,15 @@ public class Sm2ComplianceTests
|
||||
?? throw new InvalidOperationException("Failed to resolve SmSoftCryptoProvider");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Provider_Name_IsCnSmSoft()
|
||||
{
|
||||
Assert.Equal("cn.sm.soft", _provider.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(CryptoCapability.Signing, "SM2", true)]
|
||||
[InlineData(CryptoCapability.Verification, "SM2", true)]
|
||||
[InlineData(CryptoCapability.ContentHashing, "SM3", true)]
|
||||
@@ -56,13 +59,15 @@ public class Sm2ComplianceTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetPasswordHasher_ThrowsNotSupported()
|
||||
{
|
||||
Assert.Throws<NotSupportedException>(() => _provider.GetPasswordHasher("PBKDF2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetHasher_WithSm3_ReturnsSm3Hasher()
|
||||
{
|
||||
var hasher = _provider.GetHasher("SM3");
|
||||
@@ -70,13 +75,15 @@ public class Sm2ComplianceTests
|
||||
Assert.Equal("SM3", hasher.AlgorithmId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetHasher_WithInvalidAlgorithm_Throws()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => _provider.GetHasher("SHA256"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sm3_ComputeHash_EmptyInput_ReturnsCorrectHash()
|
||||
{
|
||||
// OSCCA GM/T 0004-2012 test vector for empty string
|
||||
@@ -88,7 +95,8 @@ public class Sm2ComplianceTests
|
||||
Assert.Equal("1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sm3_ComputeHash_AbcInput_ReturnsCorrectHash()
|
||||
{
|
||||
// OSCCA GM/T 0004-2012 test vector for "abc"
|
||||
@@ -100,7 +108,8 @@ public class Sm2ComplianceTests
|
||||
Assert.Equal("66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sm3_ComputeHash_LongInput_ReturnsCorrectHash()
|
||||
{
|
||||
// OSCCA GM/T 0004-2012 test vector for 64-byte string
|
||||
@@ -113,7 +122,8 @@ public class Sm2ComplianceTests
|
||||
Assert.Equal("debe9ff92275b8a138604889c18e5a4d6fdb70e5387e5765293dcba39c0c5732", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Sm2_SignAndVerify_WithTestKey_Succeeds()
|
||||
{
|
||||
// Note: This test uses the existing BouncyCastle SM2 implementation
|
||||
@@ -157,7 +167,8 @@ public class Sm2ComplianceTests
|
||||
Assert.False(isInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sm2_ExportPublicJsonWebKey_ReturnsValidJwk()
|
||||
{
|
||||
var keyPair = GenerateTestSm2KeyPair();
|
||||
|
||||
@@ -2,11 +2,13 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.PluginLoader.Tests;
|
||||
|
||||
public class CryptoPluginLoaderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_WithNullConfiguration_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -17,7 +19,8 @@ public class CryptoPluginLoaderTests
|
||||
.WithParameterName("configuration");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadProviders_WithEmptyEnabledList_ReturnsEmptyCollection()
|
||||
{
|
||||
// Arrange
|
||||
@@ -38,7 +41,8 @@ public class CryptoPluginLoaderTests
|
||||
providers.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadProviders_WithMissingManifest_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -58,7 +62,8 @@ public class CryptoPluginLoaderTests
|
||||
.WithMessage("*manifest.json*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadProviders_WithRequireAtLeastOneAndNoProviders_ThrowsCryptoPluginLoadException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -80,7 +85,8 @@ public class CryptoPluginLoaderTests
|
||||
.WithMessage("*at least one provider*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadProviders_WithDisabledPattern_FiltersMatchingPlugins()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +121,8 @@ public class CryptoPluginLoaderTests
|
||||
|
||||
public class CryptoPluginConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -133,7 +140,8 @@ public class CryptoPluginConfigurationTests
|
||||
|
||||
public class CryptoPluginManifestTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CryptoPluginDescriptor_WithRequiredProperties_IsValid()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class PolicyProvidersTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FipsSoft_Signs_And_Verifies_Es256()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("FIPS_SOFT_ALLOWED", "1");
|
||||
@@ -32,13 +33,15 @@ public class PolicyProvidersTests
|
||||
provider.GetHasher(HashAlgorithms.Sha256).ComputeHash(data).Length.Should().Be(32);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EidasSoft_Signs_And_Verifies_Es384()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("EIDAS_SOFT_ALLOWED", "1");
|
||||
|
||||
var provider = new EidasSoftCryptoProvider();
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384);
|
||||
using StellaOps.TestKit;
|
||||
var key = new CryptoSigningKey(
|
||||
new CryptoKeyReference("eidas-es384"),
|
||||
SignatureAlgorithms.Es384,
|
||||
@@ -55,7 +58,8 @@ public class PolicyProvidersTests
|
||||
provider.GetHasher(HashAlgorithms.Sha384).ComputeHash(data).Length.Should().Be(48);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void KcmvpHashOnly_Computes_Hash()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("KCMVP_HASH_ALLOWED", "1");
|
||||
|
||||
@@ -9,6 +9,7 @@ using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.PqSoft;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class PqSoftCryptoProviderTests
|
||||
@@ -20,7 +21,8 @@ public class PqSoftCryptoProviderTests
|
||||
private const string FalconPrivate = "CADAE9CBC9D6BC+DE88+D+9A+7EKAE9/E9B96FI+BF/83BI/AE/++CG7+4AEDE+E76CC8A+++E8CABBDB7/CCA9C4BFD/6G62CBDBHAAF4/BGBJD+BAGA/89JFC+9AB9FG78+EA8EDE/C+D78/J/8B7+/BEF+CBD7+/7F596AADGD/F/EE+DB/A++7CFI/D/A//+9/E+AA/7+D99A988C7ACJ/B+F+BADAB98KFBBEJ+D7+A8EBD/75A9/99BI9BE8A8D/IC/638/A8/6CH+A5CAB+/I9CDG76ALB/ABD947E579AA6BA5/9B5AFCDA7KE97A5DB/FACE//D/F+MD8978/AEE7F3B++C3BEB7C+H/CBE+188EDAD+8+C9EE9AF6IG/4+CCEFFD5HF+7FE/E+/B76+LI8G/A/777A898E98CG8AB8H5EE9CJE+A/+I5DAHD8BFCD+76+9B+/7H7FEA8/A/BHA+JB+D7DE3CFF8B+E898BI6B8FA6//BBA8BFGFB8F9/F+A+BD4A/MA+CBC9+IA/E7D+AC/DFD+DA8B9+CF6BBB9/L2+/A+FEF6+ECB9FC866E2/5DA+C/9C1+HC/AF88A+FFB/C//97A7/BBDD/AHCE8+FC9EBA88H9B7B6AA48BD+DFCC8+/58/GG8CAF+D8AAH/CK/KJACB7/6ICDE8F/DC/ABF6+HEzDHBGBC4B9D+CBDA/++/+AE69FBB7ABCC89785CDFGD6A9/ADKD9C5BD895CC/DFBAD45AAFAB6+EI79C+BBDGK+HC46E9CB7AD+B977/ECC+A/+3ABC+8G878CHGBCG9DDD2+97C7+6DCHCE/6AEG6E0B+AADBDIF8DA39B/85C9//6C9+6G/+BFEAAD57/H/BCB9DJ/AF+ABBH+AABG8EEA/9B8FED6D/D9E9BGEMC+GFBE+9A6+A8JFE7CAAD9BDCBAEC7BADBBF2/EAEB/GKBHC/D/FDDA89//EA/B6/5/GB/AD6A6AE74GAF+CAFd4f/wwCEBw0CS0R9trX5BPh+x4BAcYl+wUJ/xH55e0ICe0IDgINBBf8ABoVJPr/Ggb4HvIvBf3JAxz3/foCDgv4+hm31hkK7QsG5+LmKwUoxBoTAvMgFN/eAB/8+/Ah3/kD9gDpB/X1+A8czukx8w/yGAfw9+vV/fITJQMZGRwCBOL15tMJ9RMJG/732hbU0wcuKPk76/wM8gcK2B//K93j6Ojo6hj7A/8G9cQDE+bpA/In9Q0IDtAxEwH38hQ+CfMj++Tw/vX7Afoe6/gqz/f0AwIIENkU+u/1AggxF+z07AkM1hra5QYnMOvn9wv84O7lFSUx+Q0JChEY9gzqAvz/Lfz3BQj5GgQi/xcYLAvvA9AS9gAL+Qzl8hfk8wgVDx/96eHcH98aIPYPKhjV8vvkIiMELAUm1dcP8+gh7egiAhQK/AsW9dD9AQsD9gMNBxj6HO8Q/xvzBQkSDPEC6P30Hw3X0iX37uUFORr16hzd7+VC2/H2AxbtJxUEGusbJf7iDuTtG+MrFAHSz//m9P4KEQj95wTq//wS+vvWJwbnFgnyDvIRJ+TxGQX2FCgAzfPMAAPdCQoDAy7fuv8OFSIPBv0G6wbftwHuJPspKPnm+vgS3tT1Fe7w3QXT5g4L5DPg6AEVCPX3GEUpIh7xFtssp/n2MAn+LSQF/MTrDMc=";
|
||||
private const string FalconPublic = "Jv6LkSCSV25Fk8XuSAZVAYuFsivvFkrde2eUhjeNOIi7FnEOVhDy2tulOoFJYB1eLnKWcfcB4B6iWvuQvZ+NRHYbZJNnlG/JrcCTSQFybqYY5tv7TAYHcozO9Qe6Nju25ThfuANwomnNsCWsizGs4xO2IdZ1inooJGnmcCAhIsa8WvXVIBhoSlSjV/Nxjmw7c5KSFUqUiQ6KFn8dELZNK/10OLBzBMWQIrmCi+0NmUieiKcVk1MdeKJFslEISCHi1vDiUpgQFPTH2MAsOqz591JFAD7FyWarfGLh6Rgd1WwZAWtWxpAnSXGkY9ADO0R+wXnuIoiEOQi1axW0UwjMZCoivmoJf5ZsEn4CyfenMNKqe5K+Z7fiw5Vdjoh4ZqoH0gzbWYZoGlRpC6XmWlQIwbnBaQRFRdkphuMTTBK8NKDHxod1xzEJrXmAiG4xZQGXvopGQMNdqGja8sOcncMN4lN4dp9VDOK4algsVBglrOuwinwp+GxLz+rcHwV0hkNUswmpWOPplUZLJtlmeJ1YJjRk8egce6oQRYde4QsWIIyoKYOfh/7hV2NYdEcIPCknRZsPiXyqcGoFqAZTMlZblFQ5A1e+1FyQ8DPQeKzlO8A1JRAJ7A6eioHBKJ1zYZblxiwyggHOOoXK1HbpU4PvJROQ6MW08ZK4tAkXZI0lw3s50ViSBFd5zI8xtFrzpjRLOZzMY0dkVDGLlr8+9gsBCU48dfyMga6lrbgVJVcW6rFgPD9lG09mhDArGR8opVeSm6WR60Wq9RRI9WCmDFL+9N/Z8YsdGgo8y2Fx9mNz88DRd0hGyQ/pZTluDuuWEHxx0UCyzWofcq7TkT2pLGq5cBBB3klRWK4CgpRns+HRQhuWeEEtH8Pm4pyJ1NYkXeEbOohkk5NgEmfCF/NmlORdZDxAQSi9r/bvBnlGdS6aV9BvpWugvJV3wcDFvQVjR4HT0IlBwwpHyQhJdcrKsebSaynfQfySaUgMOgUGARAn4jl34sZZia3UCQhpO7yYcacxXRwHSK5nTGePiOoiWCfZYwGQ92k0aLO5fIL4GWo6VmrQu/HiJrF1/GCHa9ozFvcW4wHkejos9Nkf8at5c7QoRoNfEl4zkKjvSKivpZyetasiOuf/6UW06A8m2xfHU0ixQB1emzSGRpGP0gWwicUbdE1tcakAKtMV2I3NolR5wO4=";
|
||||
private const string FalconSignature = "OfUX6CmZ14QRj1xzxUFf657ng0GtWjPuT3HVLNV+es9pwMAMZe98/2POuD5YQ9z85g2fkSKqDx9pgyiMLG4lTERJ/qorc9KgSKomkUgS1JL+78qdsnkwUS4cA5BDHB0ipP3thSLVkEWt74ReIz2TDDfbzqzX/ZgFf5ZiYPhsZBSQ0fq23cqYYKTWZ4rjDOkiawuFEMUmCaEp9amx9essVLyEcTqQMCfjdFbJ3LTPweD8Q2SGECYNL7PdUbqJWRWWGR9LE14EpNLNckyFPP3uEYevr1CYF+kbUd3nNyXPG0zHKe2KEffTP3EcmO/x5HjGXZSUdrElbQU0FjYiwm8s2vOO7+IfOm8Fj4rslTRlDuuwDHGB40q57r4P3JMv61tqa0ynSglolqkOBp7EZfAyb7sMwrU6arvXpIDlzCmagDW8DGw7SPUxcV1CZ/XvoNs1jfjWQfVOiTrFDWOvm6WSRYZnLkHaVKUN7CMbFI1lzaNdu2UfMQKA6zsRs+NwVeZGJ26q0rvMmkIvVuMUlaav+Rp/kKKIZVM7Sj7LnrkU2zrII+WSsy9JaZmbHi+XxIXUi9XeT2Xc5anTE9manN4x/wvpByQogntxZb6ej8ZXD1t/a6sPQKc9MJWX9UVl2/wr07iHww0317LqLLC2FNHclYcLR7KK/GEVnAvrDn94r4wY88nsusNDmli3Thh1C6UNg17gjsazteq6m0y+Yrzrb3ZmMzl0bkganSnNTOIsgwyCbDo0d8/WTKjMeoNWQQhKUJM2elWn2TvmaFdOZHMV6Iuw17ociTXHko3J/MZKncSPjYE12pw70mHZ2B+ZxvM1Cj4RUbLHfYz0ANGuSbUGjlgzmewiNXJS4KOsY2K6PYcZ";
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dilithium3_Signs_And_Verifies()
|
||||
{
|
||||
var provider = CreateProvider();
|
||||
@@ -46,7 +48,8 @@ public class PqSoftCryptoProviderTests
|
||||
(await signer.VerifyAsync(data, signature)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Falcon512_Signs_And_Verifies()
|
||||
{
|
||||
var provider = CreateProvider();
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class SimRemoteProviderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Supports_DefaultAlgorithms_CoversStandardIds()
|
||||
{
|
||||
var handler = new NoopHandler();
|
||||
@@ -27,7 +28,8 @@ public class SimRemoteProviderTests
|
||||
Assert.True(provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.Dilithium3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_WithSimProvider_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -47,6 +49,7 @@ public class SimRemoteProviderTests
|
||||
services.AddSingleton<SimRemoteProvider>();
|
||||
|
||||
using var providerScope = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var provider = providerScope.GetRequiredService<SimRemoteProvider>();
|
||||
var signer = provider.GetSigner("pq.sim", new CryptoKeyReference("sim-key"));
|
||||
var payload = Encoding.UTF8.GetBytes("hello-sim");
|
||||
|
||||
@@ -18,7 +18,8 @@ public class EvidenceRecordTests
|
||||
|
||||
#region ComputeEvidenceId
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_ValidInputs_ReturnsSha256Prefixed()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -34,7 +35,8 @@ public class EvidenceRecordTests
|
||||
Assert.Equal(71, evidenceId.Length); // "sha256:" + 64 hex chars
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_SameInputs_ReturnsSameId()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -46,7 +48,8 @@ public class EvidenceRecordTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_DifferentSubjects_ReturnsDifferentIds()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
|
||||
@@ -57,7 +60,8 @@ public class EvidenceRecordTests
|
||||
Assert.NotEqual(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_DifferentTypes_ReturnsDifferentIds()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -69,7 +73,8 @@ public class EvidenceRecordTests
|
||||
Assert.NotEqual(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_DifferentPayloads_ReturnsDifferentIds()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -82,7 +87,8 @@ public class EvidenceRecordTests
|
||||
Assert.NotEqual(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_DifferentProvenance_ReturnsDifferentIds()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -108,7 +114,8 @@ public class EvidenceRecordTests
|
||||
Assert.NotEqual(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_NullSubject_ThrowsArgumentException()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
|
||||
@@ -116,7 +123,8 @@ public class EvidenceRecordTests
|
||||
EvidenceRecord.ComputeEvidenceId(null!, EvidenceType.Scan, payload, TestProvenance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_EmptySubject_ThrowsArgumentException()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
|
||||
@@ -124,7 +132,8 @@ public class EvidenceRecordTests
|
||||
EvidenceRecord.ComputeEvidenceId("", EvidenceType.Scan, payload, TestProvenance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_NullProvenance_ThrowsArgumentNullException()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("""{"data":"test"}""");
|
||||
@@ -136,7 +145,8 @@ public class EvidenceRecordTests
|
||||
|
||||
#region Create Factory Method
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Create_ValidInputs_ReturnsRecordWithComputedId()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -158,7 +168,8 @@ public class EvidenceRecordTests
|
||||
Assert.Null(record.ExternalPayloadCid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Create_WithSignatures_IncludesSignatures()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -184,7 +195,8 @@ public class EvidenceRecordTests
|
||||
Assert.Equal("key-123", record.Signatures[0].SignerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Create_WithExternalCid_IncludesCid()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -198,6 +210,7 @@ public class EvidenceRecordTests
|
||||
"reachability/v1",
|
||||
externalPayloadCid: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi");
|
||||
|
||||
using StellaOps.TestKit;
|
||||
Assert.Equal("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", record.ExternalPayloadCid);
|
||||
}
|
||||
|
||||
@@ -205,7 +218,8 @@ public class EvidenceRecordTests
|
||||
|
||||
#region VerifyIntegrity
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyIntegrity_ValidRecord_ReturnsTrue()
|
||||
{
|
||||
var record = EvidenceRecord.Create(
|
||||
@@ -218,7 +232,8 @@ public class EvidenceRecordTests
|
||||
Assert.True(record.VerifyIntegrity());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyIntegrity_TamperedPayload_ReturnsFalse()
|
||||
{
|
||||
var originalPayload = Encoding.UTF8.GetBytes("""{"vulnerability":"CVE-2021-44228"}""");
|
||||
@@ -237,7 +252,8 @@ public class EvidenceRecordTests
|
||||
Assert.False(tampered.VerifyIntegrity());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyIntegrity_TamperedSubject_ReturnsFalse()
|
||||
{
|
||||
var record = EvidenceRecord.Create(
|
||||
@@ -256,7 +272,8 @@ public class EvidenceRecordTests
|
||||
|
||||
#region Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Create_SameInputs_ProducesSameEvidenceId()
|
||||
{
|
||||
var subjectId = "sha256:abc123";
|
||||
@@ -271,7 +288,8 @@ public class EvidenceRecordTests
|
||||
Assert.Single(ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_EmptyPayload_Works()
|
||||
{
|
||||
var id = EvidenceRecord.ComputeEvidenceId(
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Immutable;
|
||||
using StellaOps.Evidence.Core;
|
||||
using StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Evidence.Core.Tests;
|
||||
|
||||
public sealed class ExceptionApplicationAdapterTests
|
||||
@@ -24,7 +25,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithValidApplication_ReturnsTrue()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -34,7 +36,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithNullApplication_ReturnsFalse()
|
||||
{
|
||||
var result = _adapter.CanConvert(null!);
|
||||
@@ -42,7 +45,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithEmptyExceptionId_ReturnsFalse()
|
||||
{
|
||||
var application = CreateValidApplication() with { ExceptionId = "" };
|
||||
@@ -52,7 +56,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithEmptyFindingId_ReturnsFalse()
|
||||
{
|
||||
var application = CreateValidApplication() with { FindingId = "" };
|
||||
@@ -62,7 +67,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_CreatesSingleRecord()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -72,7 +78,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.Single(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasExceptionType()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -82,7 +89,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.Equal(EvidenceType.Exception, results[0].EvidenceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasCorrectSubjectNodeId()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -92,7 +100,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.Equal(_subjectNodeId, results[0].SubjectNodeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasNonEmptyPayload()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -102,7 +111,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasPayloadSchemaVersion()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -112,7 +122,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.Equal("1.0.0", results[0].PayloadSchemaVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasEmptySignatures()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -122,7 +133,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.Empty(results[0].Signatures);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_UsesProvidedProvenance()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -133,7 +145,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.Equal(_provenance.GeneratorVersion, results[0].Provenance.GeneratorVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasUniqueEvidenceId()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -144,7 +157,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.NotEmpty(results[0].EvidenceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithNullSubjectNodeId_ThrowsArgumentNullException()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -153,7 +167,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
_adapter.Convert(application, null!, _provenance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithEmptySubjectNodeId_ThrowsArgumentException()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -162,7 +177,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
_adapter.Convert(application, "", _provenance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithNullProvenance_ThrowsArgumentNullException()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -171,7 +187,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
_adapter.Convert(application, _subjectNodeId, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithVulnerabilityId_IncludesInPayload()
|
||||
{
|
||||
var application = CreateValidApplication() with { VulnerabilityId = "CVE-2024-9999" };
|
||||
@@ -181,7 +198,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithEvaluationRunId_IncludesInPayload()
|
||||
{
|
||||
var runId = Guid.NewGuid();
|
||||
@@ -192,7 +210,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithPolicyBundleDigest_IncludesInPayload()
|
||||
{
|
||||
var application = CreateValidApplication() with { PolicyBundleDigest = "sha256:policy123" };
|
||||
@@ -202,7 +221,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithMetadata_IncludesInPayload()
|
||||
{
|
||||
var metadata = ImmutableDictionary<string, string>.Empty
|
||||
@@ -216,7 +236,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_DifferentApplications_ProduceDifferentEvidenceIds()
|
||||
{
|
||||
var app1 = CreateValidApplication() with { ExceptionId = "exc-001" };
|
||||
@@ -228,7 +249,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.NotEqual(results1[0].EvidenceId, results2[0].EvidenceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_SameApplicationTwice_ProducesSameEvidenceId()
|
||||
{
|
||||
var application = CreateValidApplication();
|
||||
@@ -239,7 +261,8 @@ public sealed class ExceptionApplicationAdapterTests
|
||||
Assert.Equal(results1[0].EvidenceId, results2[0].EvidenceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_AllStatusTransitions_Supported()
|
||||
{
|
||||
var transitions = new[]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Evidence.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -28,7 +29,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region StoreAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAsync_ValidEvidence_ReturnsEvidenceId()
|
||||
{
|
||||
var evidence = CreateTestEvidence("sha256:subject1");
|
||||
@@ -39,7 +41,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(1, _store.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAsync_DuplicateEvidence_IsIdempotent()
|
||||
{
|
||||
var evidence = CreateTestEvidence("sha256:subject1");
|
||||
@@ -50,7 +53,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(1, _store.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAsync_NullEvidence_ThrowsArgumentNullException()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => _store.StoreAsync(null!));
|
||||
@@ -60,7 +64,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region StoreBatchAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreBatchAsync_MultipleRecords_StoresAll()
|
||||
{
|
||||
var evidence1 = CreateTestEvidence("sha256:subject1");
|
||||
@@ -73,7 +78,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(3, _store.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreBatchAsync_WithDuplicates_SkipsDuplicates()
|
||||
{
|
||||
var evidence1 = CreateTestEvidence("sha256:subject1");
|
||||
@@ -86,7 +92,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(2, _store.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreBatchAsync_EmptyList_ReturnsZero()
|
||||
{
|
||||
var count = await _store.StoreBatchAsync([]);
|
||||
@@ -99,7 +106,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region GetByIdAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ExistingEvidence_ReturnsEvidence()
|
||||
{
|
||||
var evidence = CreateTestEvidence("sha256:subject1");
|
||||
@@ -112,7 +120,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(evidence.SubjectNodeId, result.SubjectNodeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_NonExistingEvidence_ReturnsNull()
|
||||
{
|
||||
var result = await _store.GetByIdAsync("sha256:nonexistent");
|
||||
@@ -120,13 +129,15 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_NullId_ThrowsArgumentException()
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<ArgumentException>(() => _store.GetByIdAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_EmptyId_ThrowsArgumentException()
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<ArgumentException>(() => _store.GetByIdAsync(""));
|
||||
@@ -136,7 +147,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region GetBySubjectAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBySubjectAsync_ExistingSubject_ReturnsAllEvidence()
|
||||
{
|
||||
var subjectId = "sha256:subject1";
|
||||
@@ -151,7 +163,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(2, results.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBySubjectAsync_WithTypeFilter_ReturnsFilteredResults()
|
||||
{
|
||||
var subjectId = "sha256:subject1";
|
||||
@@ -167,7 +180,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(EvidenceType.Scan, results[0].EvidenceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBySubjectAsync_NonExistingSubject_ReturnsEmptyList()
|
||||
{
|
||||
var results = await _store.GetBySubjectAsync("sha256:nonexistent");
|
||||
@@ -179,7 +193,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region GetByTypeAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByTypeAsync_ExistingType_ReturnsMatchingEvidence()
|
||||
{
|
||||
await _store.StoreAsync(CreateTestEvidence("sha256:sub1", EvidenceType.Scan));
|
||||
@@ -192,7 +207,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.All(results, r => Assert.Equal(EvidenceType.Scan, r.EvidenceType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByTypeAsync_WithLimit_RespectsLimit()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
@@ -205,7 +221,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(5, results.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByTypeAsync_NonExistingType_ReturnsEmptyList()
|
||||
{
|
||||
await _store.StoreAsync(CreateTestEvidence("sha256:sub1", EvidenceType.Scan));
|
||||
@@ -219,7 +236,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region ExistsAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExistsAsync_ExistingEvidenceForType_ReturnsTrue()
|
||||
{
|
||||
var subjectId = "sha256:subject1";
|
||||
@@ -230,7 +248,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.True(exists);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExistsAsync_DifferentType_ReturnsFalse()
|
||||
{
|
||||
var subjectId = "sha256:subject1";
|
||||
@@ -241,7 +260,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.False(exists);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExistsAsync_NonExistingSubject_ReturnsFalse()
|
||||
{
|
||||
var exists = await _store.ExistsAsync("sha256:nonexistent", EvidenceType.Scan);
|
||||
@@ -253,7 +273,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region DeleteAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ExistingEvidence_ReturnsTrue()
|
||||
{
|
||||
var evidence = CreateTestEvidence("sha256:subject1");
|
||||
@@ -265,7 +286,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(0, _store.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_NonExistingEvidence_ReturnsFalse()
|
||||
{
|
||||
var deleted = await _store.DeleteAsync("sha256:nonexistent");
|
||||
@@ -273,7 +295,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.False(deleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_RemovedEvidence_NotRetrievable()
|
||||
{
|
||||
var evidence = CreateTestEvidence("sha256:subject1");
|
||||
@@ -289,7 +312,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region CountBySubjectAsync
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountBySubjectAsync_MultipleEvidence_ReturnsCorrectCount()
|
||||
{
|
||||
var subjectId = "sha256:subject1";
|
||||
@@ -302,7 +326,8 @@ public class InMemoryEvidenceStoreTests
|
||||
Assert.Equal(3, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountBySubjectAsync_NoEvidence_ReturnsZero()
|
||||
{
|
||||
var count = await _store.CountBySubjectAsync("sha256:nonexistent");
|
||||
@@ -314,7 +339,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region Clear
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllEvidence()
|
||||
{
|
||||
await _store.StoreAsync(CreateTestEvidence("sha256:sub1"));
|
||||
@@ -329,7 +355,8 @@ public class InMemoryEvidenceStoreTests
|
||||
|
||||
#region Cancellation
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAsync_CancelledToken_ThrowsOperationCancelledException()
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
@@ -341,7 +368,8 @@ public class InMemoryEvidenceStoreTests
|
||||
_store.StoreAsync(evidence, cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_CancelledToken_ThrowsOperationCancelledException()
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Immutable;
|
||||
using StellaOps.Evidence.Core;
|
||||
using StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Evidence.Core.Tests;
|
||||
|
||||
public sealed class ProofSegmentAdapterTests
|
||||
@@ -24,7 +25,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithValidSegment_ReturnsTrue()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -34,7 +36,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithNullSegment_ReturnsFalse()
|
||||
{
|
||||
var result = _adapter.CanConvert(null!);
|
||||
@@ -42,7 +45,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithEmptySegmentId_ReturnsFalse()
|
||||
{
|
||||
var segment = CreateValidSegment() with { SegmentId = "" };
|
||||
@@ -52,7 +56,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithEmptyInputHash_ReturnsFalse()
|
||||
{
|
||||
var segment = CreateValidSegment() with { InputHash = "" };
|
||||
@@ -62,7 +67,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_CreatesSingleRecord()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -72,7 +78,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Single(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasCorrectSubjectNodeId()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -82,7 +89,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Equal(_subjectNodeId, results[0].SubjectNodeId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("SbomSlice", EvidenceType.Artifact)]
|
||||
[InlineData("Match", EvidenceType.Scan)]
|
||||
[InlineData("Reachability", EvidenceType.Reachability)]
|
||||
@@ -98,7 +106,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Equal(expectedType, results[0].EvidenceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_UnknownSegmentType_DefaultsToCustomType()
|
||||
{
|
||||
var segment = CreateValidSegment() with { SegmentType = "UnknownType" };
|
||||
@@ -108,7 +117,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Equal(EvidenceType.Custom, results[0].EvidenceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasNonEmptyPayload()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -118,7 +128,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasPayloadSchemaVersion()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -128,7 +139,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Equal("proof-segment/v1", results[0].PayloadSchemaVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasEmptySignatures()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -138,7 +150,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Empty(results[0].Signatures);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_UsesProvidedProvenance()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -149,7 +162,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Equal(_provenance.GeneratorVersion, results[0].Provenance.GeneratorVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordHasUniqueEvidenceId()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -160,7 +174,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.NotEmpty(results[0].EvidenceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithNullSubjectNodeId_ThrowsArgumentNullException()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -169,7 +184,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
_adapter.Convert(segment, null!, _provenance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithNullProvenance_ThrowsArgumentNullException()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -178,7 +194,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
_adapter.Convert(segment, _subjectNodeId, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_DifferentSegments_ProduceDifferentEvidenceIds()
|
||||
{
|
||||
var segment1 = CreateValidSegment() with { SegmentId = "seg-001" };
|
||||
@@ -190,7 +207,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.NotEqual(results1[0].EvidenceId, results2[0].EvidenceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_SameSegmentTwice_ProducesSameEvidenceId()
|
||||
{
|
||||
var segment = CreateValidSegment();
|
||||
@@ -201,7 +219,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Equal(results1[0].EvidenceId, results2[0].EvidenceId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Pending")]
|
||||
[InlineData("Verified")]
|
||||
[InlineData("Partial")]
|
||||
@@ -216,7 +235,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.Single(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithToolInfo_IncludesInPayload()
|
||||
{
|
||||
var segment = CreateValidSegment() with
|
||||
@@ -230,7 +250,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithPrevSegmentHash_IncludesInPayload()
|
||||
{
|
||||
var segment = CreateValidSegment() with { PrevSegmentHash = "sha256:prevhash" };
|
||||
@@ -240,7 +261,8 @@ public sealed class ProofSegmentAdapterTests
|
||||
Assert.False(results[0].Payload.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithSpineId_IncludesInPayload()
|
||||
{
|
||||
var segment = CreateValidSegment() with { SpineId = "spine-001" };
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Immutable;
|
||||
using StellaOps.Evidence.Core;
|
||||
using StellaOps.Evidence.Core.Adapters;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Evidence.Core.Tests;
|
||||
|
||||
public sealed class VexObservationAdapterTests
|
||||
@@ -24,7 +25,8 @@ public sealed class VexObservationAdapterTests
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithValidObservation_ReturnsTrue()
|
||||
{
|
||||
var observation = CreateValidObservation();
|
||||
@@ -34,7 +36,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithNullObservation_ReturnsFalse()
|
||||
{
|
||||
var result = _adapter.CanConvert(null!);
|
||||
@@ -42,7 +45,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithEmptyObservationId_ReturnsFalse()
|
||||
{
|
||||
var observation = CreateValidObservation() with { ObservationId = "" };
|
||||
@@ -52,7 +56,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanConvert_WithEmptyProviderId_ReturnsFalse()
|
||||
{
|
||||
var observation = CreateValidObservation() with { ProviderId = "" };
|
||||
@@ -62,7 +67,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_CreatesObservationLevelRecord()
|
||||
{
|
||||
var observation = CreateValidObservation();
|
||||
@@ -75,7 +81,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.Equal(_subjectNodeId, observationRecord.SubjectNodeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_CreatesStatementRecordsForEachStatement()
|
||||
{
|
||||
var statements = ImmutableArray.Create(
|
||||
@@ -100,7 +107,8 @@ public sealed class VexObservationAdapterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithSingleStatement_CreatesCorrectRecords()
|
||||
{
|
||||
var observation = CreateValidObservation();
|
||||
@@ -111,7 +119,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.Equal(2, results.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithEmptyStatements_CreatesOnlyObservationRecord()
|
||||
{
|
||||
var observation = CreateValidObservation() with { Statements = [] };
|
||||
@@ -122,7 +131,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.Equal(EvidenceType.Provenance, results[0].EvidenceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithSignature_IncludesSignatureInRecords()
|
||||
{
|
||||
var signature = new VexObservationSignatureInput
|
||||
@@ -147,7 +157,8 @@ public sealed class VexObservationAdapterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithoutSignature_CreatesRecordsWithEmptySignatures()
|
||||
{
|
||||
var signature = new VexObservationSignatureInput
|
||||
@@ -169,7 +180,8 @@ public sealed class VexObservationAdapterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_UsesProvidedProvenance()
|
||||
{
|
||||
var observation = CreateValidObservation();
|
||||
@@ -183,7 +195,8 @@ public sealed class VexObservationAdapterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithNullSubjectNodeId_ThrowsArgumentNullException()
|
||||
{
|
||||
var observation = CreateValidObservation();
|
||||
@@ -192,7 +205,8 @@ public sealed class VexObservationAdapterTests
|
||||
_adapter.Convert(observation, null!, _provenance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_WithNullProvenance_ThrowsArgumentNullException()
|
||||
{
|
||||
var observation = CreateValidObservation();
|
||||
@@ -201,7 +215,8 @@ public sealed class VexObservationAdapterTests
|
||||
_adapter.Convert(observation, _subjectNodeId, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_EachRecordHasUniqueEvidenceId()
|
||||
{
|
||||
var statements = ImmutableArray.Create(
|
||||
@@ -216,7 +231,8 @@ public sealed class VexObservationAdapterTests
|
||||
Assert.Equal(evidenceIds.Count, evidenceIds.Distinct().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Convert_RecordsHavePayloadSchemaVersion()
|
||||
{
|
||||
var observation = CreateValidObservation();
|
||||
|
||||
@@ -2,9 +2,11 @@ using System.Text.Json;
|
||||
using StellaOps.Replay.Core;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public class ReplayManifestTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializesWithNamespacesAndAnalysis_V1()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -48,7 +50,8 @@ public class ReplayManifestTests
|
||||
Assert.Contains("\"namespace\":\"runtime_traces\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializesWithV2HashFields()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using StellaOps.Replay.Core;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Replay.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -19,7 +20,8 @@ public class ReplayManifestV2Tests
|
||||
|
||||
#region Section 4.1: Minimal Valid Manifest v2
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MinimalValidManifestV2_SerializesCorrectly()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -67,7 +69,8 @@ public class ReplayManifestV2Tests
|
||||
|
||||
#region Section 4.2: Manifest with Runtime Traces
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestWithRuntimeTraces_SerializesCorrectly()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -116,7 +119,8 @@ public class ReplayManifestV2Tests
|
||||
|
||||
#region Section 4.3: Sorting Validation
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SortingValidation_UnsortedGraphs_FailsValidation()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -151,7 +155,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Contains(result.Errors, e => e.ErrorCode == ReplayManifestErrorCodes.UnsortedEntries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SortingValidation_SortedGraphs_PassesValidation()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -189,7 +194,8 @@ public class ReplayManifestV2Tests
|
||||
|
||||
#region Section 4.4: Invalid Manifest Vectors
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InvalidManifest_MissingSchemaVersion_FailsValidation()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -204,7 +210,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Contains(result.Errors, e => e.ErrorCode == ReplayManifestErrorCodes.MissingVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InvalidManifest_VersionMismatch_WhenV2Required_FailsValidation()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -219,7 +226,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Contains(result.Errors, e => e.ErrorCode == ReplayManifestErrorCodes.VersionMismatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InvalidManifest_MissingHashAlg_InV2_FailsValidation()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -246,7 +254,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Contains(result.Errors, e => e.ErrorCode == ReplayManifestErrorCodes.MissingHashAlg);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task InvalidManifest_MissingCasReference_FailsValidation()
|
||||
{
|
||||
var casValidator = new InMemoryCasValidator();
|
||||
@@ -276,7 +285,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Contains(result.Errors, e => e.ErrorCode == ReplayManifestErrorCodes.CasNotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task InvalidManifest_HashMismatch_FailsValidation()
|
||||
{
|
||||
var casValidator = new InMemoryCasValidator();
|
||||
@@ -315,7 +325,8 @@ public class ReplayManifestV2Tests
|
||||
|
||||
#region Section 5: Migration Path
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeToV2_ConvertsV1ManifestCorrectly()
|
||||
{
|
||||
var v1 = new ReplayManifest
|
||||
@@ -347,7 +358,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Equal("sha256", v2.Reachability.Graphs[0].HashAlgorithm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeToV2_SortsGraphsByUri()
|
||||
{
|
||||
var v1 = new ReplayManifest
|
||||
@@ -373,7 +385,8 @@ public class ReplayManifestV2Tests
|
||||
|
||||
#region ReachabilityReplayWriter Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildManifestV2_WithValidGraphs_CreatesSortedManifest()
|
||||
{
|
||||
var scan = new ReplayScanMetadata { Id = "test-scan" };
|
||||
@@ -401,7 +414,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Equal("cas://graphs/zzzz", manifest.Reachability.Graphs[1].CasUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildManifestV2_WithLegacySha256_MigratesHashField()
|
||||
{
|
||||
var scan = new ReplayScanMetadata { Id = "test-scan" };
|
||||
@@ -423,7 +437,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Equal("sha256", manifest.Reachability.Graphs[0].HashAlgorithm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildManifestV2_InfersHashAlgorithmFromPrefix()
|
||||
{
|
||||
var scan = new ReplayScanMetadata { Id = "test-scan" };
|
||||
@@ -444,7 +459,8 @@ public class ReplayManifestV2Tests
|
||||
Assert.Equal("blake3-256", manifest.Reachability.Graphs[0].HashAlgorithm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildManifestV2_RequiresAtLeastOneGraph()
|
||||
{
|
||||
var scan = new ReplayScanMetadata { Id = "test-scan" };
|
||||
@@ -460,7 +476,8 @@ public class ReplayManifestV2Tests
|
||||
|
||||
#region CodeIdCoverage Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CodeIdCoverage_SerializesWithSnakeCaseKeys()
|
||||
{
|
||||
var coverage = new CodeIdCoverage
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Resolver.Tests;
|
||||
|
||||
public class CycleDetectionTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphWithMarkedCycleCutEdge_IsValid()
|
||||
{
|
||||
// CYCLE-9100-016: Graph with marked cycle-cut edge passes validation
|
||||
@@ -33,7 +35,8 @@ public class CycleDetectionTests
|
||||
Assert.True(result.IsValid, $"Expected valid graph. Errors: {string.Join(", ", result.Errors)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphWithUnmarkedCycle_ThrowsInvalidGraphException()
|
||||
{
|
||||
// CYCLE-9100-017: Graph with unmarked cycle throws exception
|
||||
@@ -57,7 +60,8 @@ public class CycleDetectionTests
|
||||
Assert.Contains(result.Errors, e => e.Contains("Cycle detected without IsCycleCut edge"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphWithMultipleCycles_AllMarked_IsValid()
|
||||
{
|
||||
// CYCLE-9100-018: Multiple cycles, all marked
|
||||
@@ -84,7 +88,8 @@ public class CycleDetectionTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphWithMultipleCycles_OneUnmarked_HasError()
|
||||
{
|
||||
// CYCLE-9100-019: Multiple cycles, one unmarked
|
||||
@@ -112,7 +117,8 @@ public class CycleDetectionTests
|
||||
Assert.Single(result.Errors.Where(e => e.Contains("Cycle detected")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CycleDetection_IsDeterministic()
|
||||
{
|
||||
// CYCLE-9100-020: Property test - deterministic detection
|
||||
@@ -142,7 +148,8 @@ public class CycleDetectionTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CycleCutEdge_IncludedInGraphDigest()
|
||||
{
|
||||
// CYCLE-9100-021: Cycle-cut edges affect graph digest
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Resolver.Tests;
|
||||
|
||||
public class DeterministicResolverTests
|
||||
@@ -15,7 +16,8 @@ public class DeterministicResolverTests
|
||||
private readonly IGraphOrderer _orderer = new TopologicalGraphOrderer();
|
||||
private readonly ITrustLatticeEvaluator _evaluator = new DefaultTrustLatticeEvaluator();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Run_SameInputTwice_IdenticalFinalDigest()
|
||||
{
|
||||
// RESOLVER-9100-020: Replay test
|
||||
@@ -31,7 +33,8 @@ public class DeterministicResolverTests
|
||||
Assert.Equal(result1.TraversalSequence.Length, result2.TraversalSequence.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Run_ShuffledNodesAndEdges_IdenticalFinalDigest()
|
||||
{
|
||||
// RESOLVER-9100-021: Permutation test
|
||||
@@ -60,7 +63,8 @@ public class DeterministicResolverTests
|
||||
Assert.Equal(result1.FinalDigest, result2.FinalDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Run_IsIdempotent()
|
||||
{
|
||||
// RESOLVER-9100-022: Idempotency property test
|
||||
@@ -76,7 +80,8 @@ public class DeterministicResolverTests
|
||||
Assert.Equal(result2.FinalDigest, result3.FinalDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Run_TraversalSequence_MatchesTopologicalOrder()
|
||||
{
|
||||
// RESOLVER-9100-023: Traversal order test
|
||||
@@ -106,7 +111,8 @@ public class DeterministicResolverTests
|
||||
"Root should appear before at least one child in traversal");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolutionResult_CanonicalJsonStructure()
|
||||
{
|
||||
// RESOLVER-9100-024: Snapshot test for canonical JSON
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Resolver.Tests;
|
||||
|
||||
public class EdgeIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeId_ComputedDeterministically()
|
||||
{
|
||||
// EDGEID-9100-015: EdgeId computed deterministically
|
||||
@@ -25,7 +27,8 @@ public class EdgeIdTests
|
||||
Assert.Equal(64, edgeId1.Value.Length); // SHA256 hex
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeId_OrderingConsistentWithStringOrdering()
|
||||
{
|
||||
// EDGEID-9100-016: EdgeId ordering is consistent
|
||||
@@ -43,7 +46,8 @@ public class EdgeIdTests
|
||||
Assert.Equal(sorted1, sorted2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphHash_ChangesWhenEdgeAddedOrRemoved()
|
||||
{
|
||||
// EDGEID-9100-017: Graph hash changes with edge changes
|
||||
@@ -63,7 +67,8 @@ public class EdgeIdTests
|
||||
Assert.NotEqual(graph2.GraphDigest, graph3.GraphDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeDelta_CorrectlyIdentifiesChanges()
|
||||
{
|
||||
// EDGEID-9100-018: Delta detection identifies changes
|
||||
@@ -86,7 +91,8 @@ public class EdgeIdTests
|
||||
Assert.Empty(delta.ModifiedEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeId_IsIdempotent()
|
||||
{
|
||||
// EDGEID-9100-019: Property test - idempotent computation
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Resolver.Tests;
|
||||
|
||||
public class FinalDigestTests
|
||||
@@ -15,7 +16,8 @@ public class FinalDigestTests
|
||||
private readonly IGraphOrderer _orderer = new TopologicalGraphOrderer();
|
||||
private readonly ITrustLatticeEvaluator _evaluator = new DefaultTrustLatticeEvaluator();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FinalDigest_IsDeterministic()
|
||||
{
|
||||
// DIGEST-9100-018: Same inputs → same digest
|
||||
@@ -29,7 +31,8 @@ public class FinalDigestTests
|
||||
Assert.Equal(result1.FinalDigest, result2.FinalDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FinalDigest_ChangesWhenVerdictChanges()
|
||||
{
|
||||
// DIGEST-9100-019: FinalDigest changes when any verdict changes
|
||||
@@ -53,7 +56,8 @@ public class FinalDigestTests
|
||||
Assert.Equal(64, result1.FinalDigest.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FinalDigest_ChangesWhenGraphChanges()
|
||||
{
|
||||
// DIGEST-9100-020: FinalDigest changes when graph changes
|
||||
@@ -76,7 +80,8 @@ public class FinalDigestTests
|
||||
Assert.NotEqual(result1.FinalDigest, result2.FinalDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FinalDigest_ChangesWhenPolicyChanges()
|
||||
{
|
||||
// DIGEST-9100-021: FinalDigest changes when policy changes
|
||||
@@ -97,7 +102,8 @@ public class FinalDigestTests
|
||||
Assert.NotEqual(result1.FinalDigest, result2.FinalDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerificationApi_CorrectlyIdentifiesMatch()
|
||||
{
|
||||
// DIGEST-9100-022: Verification API works
|
||||
@@ -116,7 +122,8 @@ public class FinalDigestTests
|
||||
Assert.Empty(verification.Differences);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerificationApi_CorrectlyIdentifiesMismatch()
|
||||
{
|
||||
// DIGEST-9100-022 continued: Verification API detects mismatch
|
||||
@@ -137,7 +144,8 @@ public class FinalDigestTests
|
||||
Assert.NotEmpty(verification.Differences);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FinalDigest_IsCollisionResistant()
|
||||
{
|
||||
// DIGEST-9100-024: Property test - different inputs → different digest
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Resolver.Tests;
|
||||
|
||||
public class GraphValidationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NfcNormalization_ProducesConsistentNodeIds()
|
||||
{
|
||||
// VALID-9100-021: NFC normalization produces consistent NodeIds
|
||||
@@ -28,7 +30,8 @@ public class GraphValidationTests
|
||||
Assert.Equal(nodeId1, nodeId2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeReferencingNonExistentNode_Detected()
|
||||
{
|
||||
// VALID-9100-022
|
||||
@@ -45,7 +48,8 @@ public class GraphValidationTests
|
||||
Assert.Contains(violations, v => v.ViolationType == "DanglingEdgeDestination");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DuplicateNodeIds_Detected()
|
||||
{
|
||||
// VALID-9100-023
|
||||
@@ -64,7 +68,8 @@ public class GraphValidationTests
|
||||
Assert.Contains(violations, v => v.ViolationType == "DuplicateNodeId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DuplicateEdgeIds_Detected()
|
||||
{
|
||||
// VALID-9100-024
|
||||
@@ -86,7 +91,8 @@ public class GraphValidationTests
|
||||
Assert.Contains(violations, v => v.ViolationType == "DuplicateEdgeId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ValidGraph_PassesAllChecks()
|
||||
{
|
||||
// VALID-9100-027
|
||||
@@ -106,7 +112,8 @@ public class GraphValidationTests
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NfcNormalization_IsIdempotent()
|
||||
{
|
||||
// VALID-9100-028: Property test - NFC is idempotent
|
||||
@@ -121,7 +128,8 @@ public class GraphValidationTests
|
||||
Assert.Equal(normalized2, normalized3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EmptyGraph_IsValid()
|
||||
{
|
||||
var graph = EvidenceGraph.Empty;
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
using StellaOps.Resolver.Purity;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Resolver.Tests;
|
||||
|
||||
public class RuntimePurityTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ProhibitedTimeProvider_ThrowsOnAccess()
|
||||
{
|
||||
// PURITY-9100-021
|
||||
@@ -20,7 +22,8 @@ public class RuntimePurityTests
|
||||
Assert.Throws<AmbientAccessViolationException>(() => _ = provider.Now);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ProhibitedEnvironmentAccessor_ThrowsOnAccess()
|
||||
{
|
||||
// PURITY-9100-024
|
||||
@@ -29,7 +32,8 @@ public class RuntimePurityTests
|
||||
Assert.Throws<AmbientAccessViolationException>(() => accessor.GetVariable("PATH"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InjectedTimeProvider_ReturnsInjectedTime()
|
||||
{
|
||||
// PURITY-9100-025
|
||||
@@ -39,7 +43,8 @@ public class RuntimePurityTests
|
||||
Assert.Equal(injectedTime, provider.Now);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InjectedEnvironmentAccessor_ReturnsInjectedValues()
|
||||
{
|
||||
var vars = new Dictionary<string, string> { { "TEST_VAR", "test_value" } };
|
||||
@@ -49,7 +54,8 @@ public class RuntimePurityTests
|
||||
Assert.Null(accessor.GetVariable("NONEXISTENT"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PureEvaluationContext_StrictMode_ThrowsOnAmbientAccess()
|
||||
{
|
||||
var context = PureEvaluationContext.CreateStrict();
|
||||
@@ -57,7 +63,8 @@ public class RuntimePurityTests
|
||||
Assert.Throws<AmbientAccessViolationException>(() => _ = context.InjectedNow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PureEvaluationContext_WithInjectedValues_WorksCorrectly()
|
||||
{
|
||||
var injectedTime = DateTimeOffset.Parse("2025-12-24T12:00:00Z");
|
||||
@@ -66,7 +73,8 @@ public class RuntimePurityTests
|
||||
Assert.Equal(injectedTime, context.InjectedNow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AmbientAccessViolationException_ContainsDetails()
|
||||
{
|
||||
var ex = new AmbientAccessViolationException("Time", "Attempted DateTime.Now access");
|
||||
@@ -76,7 +84,8 @@ public class RuntimePurityTests
|
||||
Assert.Contains("Time", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FullResolution_CompletesWithoutAmbientAccess()
|
||||
{
|
||||
// PURITY-9100-027: Integration test
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Resolver.Tests;
|
||||
|
||||
public class VerdictDigestTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerdictDigest_IsDeterministic()
|
||||
{
|
||||
// VDIGEST-9100-016: Same verdict → same digest
|
||||
@@ -24,7 +26,8 @@ public class VerdictDigestTests
|
||||
Assert.Equal(verdict1.VerdictDigest, verdict2.VerdictDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerdictDigest_ChangesWhenStatusChanges()
|
||||
{
|
||||
// VDIGEST-9100-017: Digest changes with status
|
||||
@@ -37,7 +40,8 @@ public class VerdictDigestTests
|
||||
Assert.NotEqual(passVerdict.VerdictDigest, failVerdict.VerdictDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerdictDigest_ChangesWhenEvidenceChanges()
|
||||
{
|
||||
// VDIGEST-9100-018: Digest changes with evidence
|
||||
@@ -51,7 +55,8 @@ public class VerdictDigestTests
|
||||
Assert.NotEqual(verdict1.VerdictDigest, verdict2.VerdictDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerdictDelta_CorrectlyIdentifiesChangedVerdicts()
|
||||
{
|
||||
// VDIGEST-9100-019: Delta detection identifies changed verdicts
|
||||
@@ -95,7 +100,8 @@ public class VerdictDigestTests
|
||||
Assert.Equal(nodeId2, delta.ChangedVerdicts[0].Old.Node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerdictDelta_HandlesAddedRemovedNodes()
|
||||
{
|
||||
// VDIGEST-9100-020: Delta handles added/removed nodes
|
||||
@@ -136,7 +142,8 @@ public class VerdictDigestTests
|
||||
Assert.Equal(nodeId2, delta.RemovedVerdicts[0].Node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerdictDigest_ExcludesItselfFromComputation()
|
||||
{
|
||||
// VDIGEST-9100-021: Property test - no recursion
|
||||
|
||||
@@ -60,4 +60,48 @@ public static class TestCategories
|
||||
/// Live tests: Require external services (e.g., Rekor, NuGet feeds). Disabled by default in CI.
|
||||
/// </summary>
|
||||
public const string Live = "Live";
|
||||
|
||||
// =========================================================================
|
||||
// Additional categories aligned with test-matrix.yml pipeline
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Architecture tests: Module dependency rules, naming conventions, forbidden packages.
|
||||
/// </summary>
|
||||
public const string Architecture = "Architecture";
|
||||
|
||||
/// <summary>
|
||||
/// Golden tests: Output comparison against known-good reference files.
|
||||
/// </summary>
|
||||
public const string Golden = "Golden";
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark tests: BenchmarkDotNet performance measurements. Scheduled runs only.
|
||||
/// </summary>
|
||||
public const string Benchmark = "Benchmark";
|
||||
|
||||
/// <summary>
|
||||
/// AirGap tests: Offline/air-gapped environment validation. On-demand only.
|
||||
/// </summary>
|
||||
public const string AirGap = "AirGap";
|
||||
|
||||
/// <summary>
|
||||
/// Chaos tests: Fault injection, failure recovery, resilience under adverse conditions.
|
||||
/// </summary>
|
||||
public const string Chaos = "Chaos";
|
||||
|
||||
/// <summary>
|
||||
/// Determinism tests: Reproducibility validation, stable ordering, idempotency.
|
||||
/// </summary>
|
||||
public const string Determinism = "Determinism";
|
||||
|
||||
/// <summary>
|
||||
/// Resilience tests: Retry policies, circuit breakers, timeout handling.
|
||||
/// </summary>
|
||||
public const string Resilience = "Resilience";
|
||||
|
||||
/// <summary>
|
||||
/// Observability tests: OpenTelemetry traces, metrics, structured logging validation.
|
||||
/// </summary>
|
||||
public const string Observability = "Observability";
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LoadFromDirectoryAsync_LoadsPemFiles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -46,7 +47,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("test-key", result.KeyIds!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LoadFromDirectoryAsync_FailsWithNonExistentDirectory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -60,7 +62,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LoadFromDirectoryAsync_FailsWithEmptyPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,7 +77,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("required", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LoadFromDirectoryAsync_LoadsFromManifest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -108,7 +112,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("stella-signing-key-001", result.KeyIds!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadFromBundle_ParsesJsonBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -139,7 +144,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("bundle-key-001", result.KeyIds!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadFromBundle_FailsWithEmptyContent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -153,7 +159,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("empty", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LoadFromBundle_FailsWithInvalidJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -167,7 +174,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetPublicKey_ReturnsKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -185,7 +193,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.NotNull(result.KeyBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetPublicKey_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
@@ -200,7 +209,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Equal("nonexistent-key", result.KeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetPublicKey_DetectsExpiredKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -236,7 +246,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("expired", result.Warning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateVerificationKey_ReturnsEcdsaKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -273,7 +284,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
key.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateVerificationKey_ReturnsNullForMissingKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -287,7 +299,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Null(key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAvailableKeyIds_ReturnsAllKeys()
|
||||
{
|
||||
// Arrange
|
||||
@@ -305,7 +318,8 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
Assert.Contains("key2", keyIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Count_ReturnsCorrectValue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -321,6 +335,7 @@ public class AirGapTrustStoreIntegrationTests : IDisposable
|
||||
private static string GenerateEcdsaPublicKeyPem()
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using StellaOps.TestKit;
|
||||
return ecdsa.ExportSubjectPublicKeyInfoPem();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.AuditPack.Services;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AuditPack.Tests;
|
||||
|
||||
public class AuditBundleWriterTests : IDisposable
|
||||
@@ -28,7 +29,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_CreatesValidBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +52,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
Assert.True(result.FileCount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_ComputesMerkleRoot()
|
||||
{
|
||||
// Arrange
|
||||
@@ -69,7 +72,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
Assert.Equal(71, result.MerkleRoot.Length); // sha256: + 64 hex chars
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_SignsManifest_WhenSignIsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +92,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
Assert.NotNull(result.SigningAlgorithm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_DoesNotSign_WhenSignIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +111,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
Assert.Null(result.SigningKeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_FailsWithoutSbom()
|
||||
{
|
||||
// Arrange
|
||||
@@ -134,7 +140,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
Assert.Contains("SBOM", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_IncludesOptionalVex()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +170,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
Assert.True(result.FileCount >= 5); // sbom, feeds, policy, verdict, vex
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_AddsTimeAnchor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +194,8 @@ public class AuditBundleWriterTests : IDisposable
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_DeterministicMerkleRoot()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -41,7 +41,8 @@ public class AuditReplayE2ETests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_ExportTransferReplayOffline_MatchingVerdict()
|
||||
{
|
||||
// ===== PHASE 1: EXPORT =====
|
||||
@@ -146,7 +147,8 @@ public class AuditReplayE2ETests : IDisposable
|
||||
Assert.Equal(decision, replayResult.OriginalDecision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_ReplayDetectsTamperedSbom()
|
||||
{
|
||||
// Setup
|
||||
@@ -222,7 +224,8 @@ public class AuditReplayE2ETests : IDisposable
|
||||
tamperedRead.Manifest?.Inputs.SbomDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_DeterministicMerkleRoot_SameInputs()
|
||||
{
|
||||
// Create identical inputs
|
||||
@@ -271,7 +274,8 @@ public class AuditReplayE2ETests : IDisposable
|
||||
Assert.Equal(result1.MerkleRoot, result2.MerkleRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_BundleContainsAllRequiredFiles()
|
||||
{
|
||||
// Setup
|
||||
@@ -323,7 +327,8 @@ public class AuditReplayE2ETests : IDisposable
|
||||
Assert.Contains(filePaths, p => p.Contains("vex"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_FullCycleWithTimeAnchor()
|
||||
{
|
||||
// Setup with explicit time anchor
|
||||
@@ -506,6 +511,7 @@ public class AuditReplayE2ETests : IDisposable
|
||||
private static async Task<string> ComputeFileHashAsync(string filePath)
|
||||
{
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
using StellaOps.TestKit;
|
||||
var hash = await SHA256.HashDataAsync(stream);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ using StellaOps.Canonicalization.Json;
|
||||
using StellaOps.Canonicalization.Ordering;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Canonicalization.Tests;
|
||||
|
||||
public class CanonicalJsonSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_Dictionary_OrdersKeysAlphabetically()
|
||||
{
|
||||
var dict = new Dictionary<string, int> { ["z"] = 1, ["a"] = 2, ["m"] = 3 };
|
||||
@@ -15,7 +17,8 @@ public class CanonicalJsonSerializerTests
|
||||
json.Should().Be("{\"a\":2,\"m\":3,\"z\":1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_DateTimeOffset_UsesUtcIso8601()
|
||||
{
|
||||
var dt = new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.FromHours(5));
|
||||
@@ -24,7 +27,8 @@ public class CanonicalJsonSerializerTests
|
||||
json.Should().Contain("2024-01-15T05:30:00.000Z");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_NullValues_AreOmitted()
|
||||
{
|
||||
var obj = new { Name = "test", Value = (string?)null };
|
||||
@@ -32,7 +36,8 @@ public class CanonicalJsonSerializerTests
|
||||
json.Should().NotContain("value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializeWithDigest_ProducesConsistentDigest()
|
||||
{
|
||||
var obj = new { Name = "test", Value = 123 };
|
||||
@@ -44,7 +49,8 @@ public class CanonicalJsonSerializerTests
|
||||
|
||||
public class PackageOrdererTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void StableOrder_OrdersByPurlFirst()
|
||||
{
|
||||
var packages = new[]
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Configuration;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Configuration.Tests;
|
||||
|
||||
public class AuthorityPluginConfigurationLoaderTests : IDisposable
|
||||
@@ -17,7 +18,8 @@ public class AuthorityPluginConfigurationLoaderTests : IDisposable
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Load_ReturnsConfiguration_ForEnabledPlugin()
|
||||
{
|
||||
var pluginDir = Path.Combine(tempRoot, "etc", "authority.plugins");
|
||||
@@ -43,7 +45,8 @@ public class AuthorityPluginConfigurationLoaderTests : IDisposable
|
||||
Assert.True(context.Manifest.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Load_Throws_WhenEnabledConfigMissing()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -62,7 +65,8 @@ public class AuthorityPluginConfigurationLoaderTests : IDisposable
|
||||
Assert.Contains("standard.yaml", ex.FileName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Load_SkipsMissingFile_ForDisabledPlugin()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -83,7 +87,8 @@ public class AuthorityPluginConfigurationLoaderTests : IDisposable
|
||||
Assert.Null(context.Configuration["connection:host"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ThrowsForUnknownCapability()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -98,7 +103,8 @@ public class AuthorityPluginConfigurationLoaderTests : IDisposable
|
||||
Assert.Contains("unknown capability", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_ReturnsWarning_WhenStandardPasswordPolicyWeaker()
|
||||
{
|
||||
var pluginDir = Path.Combine(tempRoot, "etc", "authority.plugins");
|
||||
@@ -127,7 +133,8 @@ public class AuthorityPluginConfigurationLoaderTests : IDisposable
|
||||
Assert.Contains("symbol requirement disabled", diagnostic.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Analyze_ReturnsNoDiagnostics_WhenPasswordPolicyMatchesBaseline()
|
||||
{
|
||||
var pluginDir = Path.Combine(tempRoot, "etc", "authority.plugins");
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
using StellaOps.Auth;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Configuration.Tests;
|
||||
|
||||
public class AuthorityTelemetryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceName_AndNamespace_MatchExpectations()
|
||||
{
|
||||
Assert.Equal("stellaops-authority", AuthorityTelemetry.ServiceName);
|
||||
Assert.Equal("stellaops", AuthorityTelemetry.ServiceNamespace);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildDefaultResourceAttributes_ContainsExpectedKeys()
|
||||
{
|
||||
var attributes = AuthorityTelemetry.BuildDefaultResourceAttributes();
|
||||
|
||||
@@ -5,11 +5,13 @@ using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Configuration;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Configuration.Tests;
|
||||
|
||||
public class StellaOpsAuthorityOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_IssuerMissing()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions();
|
||||
@@ -19,7 +21,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Contains("issuer", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Normalises_Collections()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -52,7 +55,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Equal(new[] { "cloud-openai", "sovereign-local" }, options.AdvisoryAi.RemoteInference.AllowedProfiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_RemoteInferenceEnabledWithoutProfiles()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -72,7 +76,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Contains("remote inference", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Normalises_PluginDescriptors()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -105,7 +110,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Equal("password", normalized.Capabilities[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Allows_TenantRemoteInferenceConsent_WhenConfigured()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -141,7 +147,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Equal(DateTimeOffset.Parse("2025-10-31T12:34:56Z", CultureInfo.InvariantCulture), tenant.AdvisoryAi.RemoteInference.ConsentedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_TenantRemoteInferenceConsentMissingVersion()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -174,7 +181,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Contains("consentVersion", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_StorageConnectionStringMissing()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -192,7 +200,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Contains("connection string", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_Binds_From_Configuration()
|
||||
{
|
||||
var context = StellaOpsAuthorityConfiguration.Build(options =>
|
||||
@@ -247,7 +256,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Equal("file", options.Signing.KeySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Normalises_ExceptionRoutingTemplates()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -280,7 +290,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.True(template.Value.RequireMfa);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_ExceptionRoutingTemplatesDuplicate()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
@@ -309,7 +320,8 @@ public class StellaOpsAuthorityOptionsTests
|
||||
Assert.Contains("secops", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_RateLimitingInvalid()
|
||||
{
|
||||
var options = new StellaOpsAuthorityOptions
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace StellaOps.Cryptography.Kms.Tests;
|
||||
|
||||
public sealed class CloudKmsClientTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AwsClient_Signs_Verifies_And_Exports_Metadata()
|
||||
{
|
||||
using var fixture = new EcdsaFixture();
|
||||
@@ -53,7 +54,8 @@ public sealed class CloudKmsClientTests
|
||||
Assert.Equal(fixture.Parameters.Q.Y, exported.Qy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GcpClient_Uses_Primary_When_Version_Not_Specified()
|
||||
{
|
||||
using var fixture = new EcdsaFixture();
|
||||
@@ -90,7 +92,8 @@ public sealed class CloudKmsClientTests
|
||||
Assert.Equal(fixture.Parameters.Q.Y, exported.Qy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void KmsCryptoProvider_Skips_NonExportable_Keys()
|
||||
{
|
||||
using var fixture = new EcdsaFixture();
|
||||
@@ -113,7 +116,8 @@ public sealed class CloudKmsClientTests
|
||||
Assert.Empty(keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Pkcs11Client_Signs_Verifies_And_Exports()
|
||||
{
|
||||
using var fixture = new EcdsaFixture();
|
||||
@@ -150,10 +154,12 @@ public sealed class CloudKmsClientTests
|
||||
Assert.Equal(fixture.Parameters.Q.Y, exported.Qy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Fido2Client_Signs_Verifies_And_Exports()
|
||||
{
|
||||
using var fixture = new EcdsaFixture();
|
||||
using StellaOps.TestKit;
|
||||
var authenticator = new TestFidoAuthenticator(fixture);
|
||||
var options = new Fido2Options
|
||||
{
|
||||
|
||||
@@ -12,7 +12,8 @@ public sealed class FileKmsClientTests : IDisposable
|
||||
_rootPath = Path.Combine(Path.GetTempPath(), $"kms-tests-{Guid.NewGuid():N}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RotateSignVerifyLifecycle_Works()
|
||||
{
|
||||
using var client = CreateClient();
|
||||
@@ -49,7 +50,8 @@ public sealed class FileKmsClientTests : IDisposable
|
||||
Assert.True(await client.VerifyAsync(keyId, previousVersion.VersionId, firstData, firstSignature.Signature));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RevokePreventsSigning()
|
||||
{
|
||||
using var client = CreateClient();
|
||||
@@ -66,10 +68,12 @@ public sealed class FileKmsClientTests : IDisposable
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => client.SignAsync(keyId, null, data));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_ReturnsKeyMaterial()
|
||||
{
|
||||
using var client = CreateClient();
|
||||
using StellaOps.TestKit;
|
||||
var keyId = "kms-export";
|
||||
|
||||
await client.RotateAsync(keyId);
|
||||
|
||||
@@ -15,14 +15,16 @@ public class OfflineVerificationProviderTests
|
||||
_provider = new OfflineVerificationCryptoProvider();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Name_ReturnsCorrectValue()
|
||||
{
|
||||
// Assert
|
||||
_provider.Name.Should().Be("offline-verification");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(CryptoCapability.Signing, "ES256", true)]
|
||||
[InlineData(CryptoCapability.Signing, "ES384", true)]
|
||||
[InlineData(CryptoCapability.Signing, "ES512", true)]
|
||||
@@ -52,7 +54,8 @@ public class OfflineVerificationProviderTests
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("SHA-256", "hello world", "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")]
|
||||
[InlineData("SHA-384", "hello world", "fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd")]
|
||||
[InlineData("SHA-512", "hello world", "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f")]
|
||||
@@ -71,7 +74,8 @@ public class OfflineVerificationProviderTests
|
||||
actualHex.Should().Be(expectedHex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetHasher_WithUnsupportedAlgorithm_ThrowsNotSupportedException()
|
||||
{
|
||||
// Act
|
||||
@@ -82,7 +86,8 @@ public class OfflineVerificationProviderTests
|
||||
.WithMessage("*MD5*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetPasswordHasher_ThrowsNotSupportedException()
|
||||
{
|
||||
// Act
|
||||
@@ -93,7 +98,8 @@ public class OfflineVerificationProviderTests
|
||||
.WithMessage("*Password hashing*");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256")]
|
||||
[InlineData("ES384")]
|
||||
[InlineData("ES512")]
|
||||
@@ -132,7 +138,8 @@ public class OfflineVerificationProviderTests
|
||||
isValid.Should().BeTrue("ephemeral verifier should verify signature from original key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateEphemeralVerifier_ForRsaPkcs1_VerifiesSignatureCorrectly()
|
||||
{
|
||||
// Arrange - Create a real RSA key, sign a message
|
||||
@@ -151,7 +158,8 @@ public class OfflineVerificationProviderTests
|
||||
isValid.Should().BeTrue("ephemeral RSA verifier should verify PKCS1 signature from original key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateEphemeralVerifier_ForRsaPss_VerifiesSignatureCorrectly()
|
||||
{
|
||||
// Arrange - Create a real RSA key, sign a message
|
||||
@@ -170,7 +178,8 @@ public class OfflineVerificationProviderTests
|
||||
isValid.Should().BeTrue("ephemeral RSA verifier should verify PSS signature from original key");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256")]
|
||||
[InlineData("PS256")]
|
||||
public void EphemeralVerifier_SignAsync_ThrowsNotSupportedException(string algorithmId)
|
||||
@@ -199,7 +208,8 @@ public class OfflineVerificationProviderTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256")]
|
||||
[InlineData("PS256")]
|
||||
public void EphemeralVerifier_WithTamperedMessage_FailsVerification(string algorithmId)
|
||||
@@ -233,7 +243,8 @@ public class OfflineVerificationProviderTests
|
||||
isValid.Should().BeFalse("ephemeral verifier should fail with tampered message");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateEphemeralVerifier_WithUnsupportedAlgorithm_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange - Create a dummy public key
|
||||
@@ -248,7 +259,8 @@ public class OfflineVerificationProviderTests
|
||||
.WithMessage("*UNSUPPORTED*");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256")]
|
||||
[InlineData("PS256")]
|
||||
public void EphemeralVerifier_HasCorrectProperties(string algorithmId)
|
||||
@@ -258,6 +270,7 @@ public class OfflineVerificationProviderTests
|
||||
using (var ecdsa = ECDsa.Create())
|
||||
{
|
||||
publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo();
|
||||
using StellaOps.TestKit;
|
||||
}
|
||||
|
||||
// Act
|
||||
|
||||
@@ -2,13 +2,15 @@ using System;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class Argon2idPasswordHasherTests
|
||||
{
|
||||
private readonly Argon2idPasswordHasher hasher = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Hash_ProducesPhcEncodedString()
|
||||
{
|
||||
var options = new PasswordHashOptions();
|
||||
@@ -17,7 +19,8 @@ public class Argon2idPasswordHasherTests
|
||||
Assert.StartsWith("$argon2id$", encoded, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Verify_ReturnsTrue_ForCorrectPassword()
|
||||
{
|
||||
var options = new PasswordHashOptions();
|
||||
@@ -27,7 +30,8 @@ public class Argon2idPasswordHasherTests
|
||||
Assert.False(hasher.Verify("wrong", encoded));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NeedsRehash_ReturnsTrue_WhenParametersChange()
|
||||
{
|
||||
var options = new PasswordHashOptions();
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public sealed class BouncyCastleEd25519CryptoProviderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_WithBouncyCastleProvider_Succeeds()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder().Build();
|
||||
@@ -19,6 +20,7 @@ public sealed class BouncyCastleEd25519CryptoProviderTests
|
||||
services.AddBouncyCastleEd25519Provider();
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var registry = provider.GetRequiredService<ICryptoProviderRegistry>();
|
||||
var bcProvider = provider.GetServices<ICryptoProvider>()
|
||||
.OfType<BouncyCastleEd25519CryptoProvider>()
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class CryptoProGostSignerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExportPublicJsonWebKey_ContainsCertificateChain()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
@@ -28,6 +29,7 @@ public class CryptoProGostSignerTests
|
||||
var request = new CertificateRequest("CN=stellaops.test", ecdsa, HashAlgorithmName.SHA256);
|
||||
using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var entry = new CryptoProGostKeyEntry(
|
||||
"test-key",
|
||||
SignatureAlgorithms.GostR3410_2012_256,
|
||||
|
||||
@@ -6,11 +6,13 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class CryptoProviderRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveOrThrow_RespectsPreferredProviderOrder()
|
||||
{
|
||||
var providerA = new FakeCryptoProvider("providerA")
|
||||
@@ -28,7 +30,8 @@ public class CryptoProviderRegistryTests
|
||||
Assert.Same(providerB, resolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveSigner_UsesPreferredProviderHint()
|
||||
{
|
||||
var providerA = new FakeCryptoProvider("providerA")
|
||||
@@ -59,7 +62,8 @@ public class CryptoProviderRegistryTests
|
||||
Assert.Equal("key-a", fallbackResolution.Signer.KeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegistryOptions_UsesActiveProfileOrder()
|
||||
{
|
||||
var options = new StellaOps.Cryptography.DependencyInjection.CryptoProviderRegistryOptions();
|
||||
|
||||
@@ -14,7 +14,8 @@ public sealed class DefaultCryptoHashTests
|
||||
{
|
||||
private static readonly byte[] Sample = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_Sha256_MatchesBcl()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
@@ -23,7 +24,8 @@ public sealed class DefaultCryptoHashTests
|
||||
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_Sha512_MatchesBcl()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
@@ -32,7 +34,8 @@ public sealed class DefaultCryptoHashTests
|
||||
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_Gost256_MatchesBouncyCastle()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
@@ -41,7 +44,8 @@ public sealed class DefaultCryptoHashTests
|
||||
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_Gost512_MatchesBouncyCastle()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
@@ -50,7 +54,8 @@ public sealed class DefaultCryptoHashTests
|
||||
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeHashAsync_Stream_MatchesBuffer()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
@@ -60,7 +65,8 @@ public sealed class DefaultCryptoHashTests
|
||||
Assert.Equal(Convert.ToHexString(bufferDigest), Convert.ToHexString(streamDigest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHashHex_Sha256_MatchesBclLowerHex()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
@@ -69,12 +75,14 @@ public sealed class DefaultCryptoHashTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeHashHexAsync_Sha256_MatchesBclLowerHex()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
var expected = Convert.ToHexStringLower(SHA256.HashData(Sample));
|
||||
await using var stream = new MemoryStream(Sample);
|
||||
using StellaOps.TestKit;
|
||||
var actual = await hash.ComputeHashHexAsync(stream, HashAlgorithms.Sha256);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ public sealed class DefaultCryptoHmacTests
|
||||
private static readonly byte[] Sample = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
|
||||
private static readonly byte[] Key = Encoding.UTF8.GetBytes("test-key");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHmacHexForPurpose_WebhookInterop_MatchesBclLowerHex()
|
||||
{
|
||||
var hmac = DefaultCryptoHmac.CreateForTests();
|
||||
@@ -22,12 +23,14 @@ public sealed class DefaultCryptoHmacTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeHmacHexForPurposeAsync_WebhookInterop_MatchesBclLowerHex()
|
||||
{
|
||||
var hmac = DefaultCryptoHmac.CreateForTests();
|
||||
var expected = Convert.ToHexStringLower(HMACSHA256.HashData(Key, Sample));
|
||||
await using var stream = new MemoryStream(Sample);
|
||||
using StellaOps.TestKit;
|
||||
var actual = await hmac.ComputeHmacHexForPurposeAsync(Key, stream, HmacPurpose.WebhookInterop);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class DefaultCryptoProviderSigningTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertSigningKey_AllowsSignAndVerifyEs256()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
@@ -52,11 +53,13 @@ public class DefaultCryptoProviderSigningTests
|
||||
Assert.False(tamperedResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RemoveSigningKey_PreventsRetrieval()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using StellaOps.TestKit;
|
||||
var parameters = ecdsa.ExportParameters(true);
|
||||
var signingKey = new CryptoSigningKey(new CryptoKeyReference("key-to-remove"), SignatureAlgorithms.Es256, in parameters, DateTimeOffset.UtcNow);
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ using System.Security.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class GostSignatureEncodingTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(32)]
|
||||
[InlineData(64)]
|
||||
public void RawAndDer_RoundTrip(int coordinateLength)
|
||||
@@ -25,14 +27,16 @@ public class GostSignatureEncodingTests
|
||||
Assert.Equal(raw, roundTrip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToDer_Throws_When_Length_Invalid()
|
||||
{
|
||||
var raw = new byte[10];
|
||||
Assert.Throws<CryptographicException>(() => GostSignatureEncoding.ToDer(raw, coordinateLength: 32));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRaw_Throws_When_Not_Der()
|
||||
{
|
||||
var raw = new byte[64];
|
||||
|
||||
@@ -9,11 +9,13 @@ namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class LibsodiumCryptoProviderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LibsodiumProvider_SignsAndVerifiesEs256()
|
||||
{
|
||||
var provider = new LibsodiumCryptoProvider();
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using StellaOps.TestKit;
|
||||
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
|
||||
|
||||
var signingKey = new CryptoSigningKey(
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.OfflineVerification;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public sealed class OfflineVerificationCryptoProviderTests
|
||||
@@ -14,7 +15,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
_provider = new OfflineVerificationCryptoProvider();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Name_ReturnsOfflineVerification()
|
||||
{
|
||||
// Act
|
||||
@@ -24,7 +26,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
name.Should().Be("offline-verification");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256")]
|
||||
[InlineData("ES384")]
|
||||
[InlineData("ES512")]
|
||||
@@ -43,7 +46,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
supports.Should().BeTrue($"{algorithmId} should be supported for signing");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256")]
|
||||
[InlineData("ES384")]
|
||||
[InlineData("ES512")]
|
||||
@@ -62,7 +66,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
supports.Should().BeTrue($"{algorithmId} should be supported for verification");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("SHA-256")]
|
||||
[InlineData("SHA-384")]
|
||||
[InlineData("SHA-512")]
|
||||
@@ -78,7 +83,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
supports.Should().BeTrue($"{algorithmId} should be supported for content hashing");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("PBKDF2")]
|
||||
[InlineData("Argon2id")]
|
||||
public void Supports_PasswordHashingAlgorithms_ReturnsTrue(string algorithmId)
|
||||
@@ -90,7 +96,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
supports.Should().BeTrue($"{algorithmId} should be reported as supported for password hashing");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256K")]
|
||||
[InlineData("EdDSA")]
|
||||
[InlineData("UNKNOWN")]
|
||||
@@ -103,7 +110,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
supports.Should().BeFalse($"{algorithmId} should not be supported");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Supports_SymmetricEncryption_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
@@ -113,7 +121,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
supports.Should().BeFalse("Symmetric encryption should not be supported");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("SHA-256")]
|
||||
[InlineData("SHA-384")]
|
||||
[InlineData("SHA-512")]
|
||||
@@ -130,7 +139,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
hasher.AlgorithmId.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetHasher_UnsupportedAlgorithm_ThrowsNotSupportedException()
|
||||
{
|
||||
// Act
|
||||
@@ -141,7 +151,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
.WithMessage("*MD5*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetHasher_SHA256_ComputesCorrectHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -156,7 +167,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
hash.Length.Should().Be(32); // SHA-256 produces 32 bytes
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetHasher_SHA256_ProducesDeterministicOutput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +184,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
hash1.Should().Equal(hash2, "Same data should produce same hash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetPasswordHasher_ThrowsNotSupportedException()
|
||||
{
|
||||
// Act
|
||||
@@ -183,7 +196,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
.WithMessage("*not supported*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetSigner_UnsupportedAlgorithm_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -197,7 +211,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
.WithMessage("*UNKNOWN*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateEphemeralVerifier_UnsupportedAlgorithm_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -211,7 +226,8 @@ public sealed class OfflineVerificationCryptoProviderTests
|
||||
.WithMessage("*UNKNOWN*");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ES256")]
|
||||
[InlineData("ES384")]
|
||||
[InlineData("ES512")]
|
||||
|
||||
@@ -11,11 +11,13 @@ using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class OpenSslGostSignerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_WithManagedProvider_Succeeds()
|
||||
{
|
||||
var keyPair = GenerateKeyPair();
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class PasswordHashOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_DoesNotThrow_ForDefaults()
|
||||
{
|
||||
var options = new PasswordHashOptions();
|
||||
options.Validate();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenMemoryInvalid()
|
||||
{
|
||||
var options = new PasswordHashOptions
|
||||
|
||||
@@ -2,13 +2,15 @@ using System;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class Pbkdf2PasswordHasherTests
|
||||
{
|
||||
private readonly Pbkdf2PasswordHasher hasher = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Hash_ProducesLegacyFormat()
|
||||
{
|
||||
var options = new PasswordHashOptions
|
||||
@@ -22,7 +24,8 @@ public class Pbkdf2PasswordHasherTests
|
||||
Assert.StartsWith("PBKDF2.", encoded, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Verify_Succeeds_ForCorrectPassword()
|
||||
{
|
||||
var options = new PasswordHashOptions
|
||||
@@ -37,7 +40,8 @@ public class Pbkdf2PasswordHasherTests
|
||||
Assert.False(hasher.Verify("other", encoded));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NeedsRehash_DetectsIterationChange()
|
||||
{
|
||||
var options = new PasswordHashOptions
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class Pkcs11GostProviderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DescribeKeys_ExposesLibraryPathAndThumbprint()
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("STELLAOPS_PKCS11_ENABLED"), "1", StringComparison.Ordinal))
|
||||
@@ -20,6 +21,7 @@ public class Pkcs11GostProviderTests
|
||||
}
|
||||
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using StellaOps.TestKit;
|
||||
var req = new CertificateRequest("CN=pkcs11.test", ecdsa, HashAlgorithmName.SHA256);
|
||||
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
|
||||
|
||||
|
||||
@@ -4,18 +4,21 @@ using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Digests;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public sealed class Sha256DigestTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_AllowsBareHex_WhenPrefixNotRequired()
|
||||
{
|
||||
var hex = new string('a', Sha256Digest.HexLength);
|
||||
Assert.Equal($"sha256:{hex}", Sha256Digest.Normalize(hex));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_NormalizesPrefixAndHexToLower()
|
||||
{
|
||||
var hexUpper = new string('A', Sha256Digest.HexLength);
|
||||
@@ -24,7 +27,8 @@ public sealed class Sha256DigestTests
|
||||
Sha256Digest.Normalize($"SHA256:{hexUpper}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_RequiresPrefix_WhenConfigured()
|
||||
{
|
||||
var hex = new string('a', Sha256Digest.HexLength);
|
||||
@@ -33,14 +37,16 @@ public sealed class Sha256DigestTests
|
||||
Assert.Contains("sha256:", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractHex_ReturnsLowercaseHex()
|
||||
{
|
||||
var hexUpper = new string('A', Sha256Digest.HexLength);
|
||||
Assert.Equal(new string('a', Sha256Digest.HexLength), Sha256Digest.ExtractHex($"sha256:{hexUpper}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compute_UsesCryptoHashStack()
|
||||
{
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
|
||||
@@ -13,6 +13,7 @@ using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.SmSoft;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class SmSoftCryptoProviderTests : IDisposable
|
||||
@@ -25,7 +26,8 @@ public class SmSoftCryptoProviderTests : IDisposable
|
||||
Environment.SetEnvironmentVariable("SM_SOFT_ALLOWED", "1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_Sm2_Works()
|
||||
{
|
||||
var provider = new SmSoftCryptoProvider();
|
||||
@@ -47,7 +49,8 @@ public class SmSoftCryptoProviderTests : IDisposable
|
||||
Assert.False(string.IsNullOrEmpty(jwk.Y));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Hash_Sm3_Works()
|
||||
{
|
||||
var provider = new SmSoftCryptoProvider();
|
||||
|
||||
@@ -8,11 +8,13 @@ using StellaOps.DeltaVerdict.Serialization;
|
||||
using StellaOps.DeltaVerdict.Signing;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.DeltaVerdict.Tests;
|
||||
|
||||
public class DeltaVerdictTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeDelta_TracksComponentAndVulnerabilityChanges()
|
||||
{
|
||||
var baseVerdict = CreateVerdict(
|
||||
@@ -52,7 +54,8 @@ public class DeltaVerdictTests
|
||||
delta.Summary.TotalChanges.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RiskBudgetEvaluator_FlagsCriticalViolations()
|
||||
{
|
||||
var delta = new DeltaVerdict.Models.DeltaVerdict
|
||||
@@ -85,7 +88,8 @@ public class DeltaVerdictTests
|
||||
result.Violations.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SigningService_RoundTrip_VerifiesEnvelope()
|
||||
{
|
||||
var delta = new DeltaVerdict.Models.DeltaVerdict
|
||||
@@ -122,7 +126,8 @@ public class DeltaVerdictTests
|
||||
verify.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serializer_ComputesDeterministicDigest()
|
||||
{
|
||||
var verdict = CreateVerdict(
|
||||
|
||||
@@ -16,6 +16,7 @@ using StellaOps.Evidence.Storage.Postgres.Tests.Fixtures;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Evidence.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -52,7 +53,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
|
||||
#region Multi-Module Evidence for Same Subject
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SameSubject_MultipleEvidenceTypes_AllLinked()
|
||||
{
|
||||
// Arrange - A container image subject with evidence from multiple modules
|
||||
@@ -87,7 +89,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
_output.WriteLine($"Subject {subjectNodeId} has {allEvidence.Count} evidence records from different modules");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SameSubject_FilterByType_ReturnsCorrectEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +114,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
|
||||
#region Evidence Chain Scenarios
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EvidenceChain_ScanToVexToPolicy_LinkedCorrectly()
|
||||
{
|
||||
// Scenario: Vulnerability scan → VEX assessment → Policy decision
|
||||
@@ -147,7 +151,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
_output.WriteLine($"Evidence chain: Scan({scan.EvidenceId}) → VEX({vex.EvidenceId}) → Policy({policy.EvidenceId})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EvidenceChain_ReachabilityToEpssToPolicy_LinkedCorrectly()
|
||||
{
|
||||
// Scenario: Reachability analysis + EPSS score → Policy decision
|
||||
@@ -178,7 +183,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
|
||||
#region Multi-Tenant Evidence Isolation
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MultiTenant_SameSubject_IsolatedByTenant()
|
||||
{
|
||||
// Arrange - Two tenants with evidence for the same subject
|
||||
@@ -214,7 +220,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
|
||||
#region Evidence Graph Queries
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EvidenceGraph_AllTypesForArtifact_ReturnsComplete()
|
||||
{
|
||||
// Arrange - Simulate a complete evidence graph for a container artifact
|
||||
@@ -251,7 +258,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EvidenceGraph_ExistsCheck_ForAllTypes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -272,7 +280,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
|
||||
#region Cross-Module Evidence Correlation
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Correlation_SameCorrelationId_FindsRelatedEvidence()
|
||||
{
|
||||
// Arrange - Evidence from different modules with same correlation ID
|
||||
@@ -295,7 +304,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
allEvidence.Should().OnlyContain(e => e.Provenance.CorrelationId == correlationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Generators_MultiplePerSubject_AllPreserved()
|
||||
{
|
||||
// Arrange - Evidence from different generators
|
||||
@@ -322,7 +332,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
|
||||
#region Evidence Count and Statistics
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountBySubject_AfterMultiModuleInserts_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -339,7 +350,8 @@ public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByType_AcrossSubjects_ReturnsAll()
|
||||
{
|
||||
// Arrange - Multiple subjects with same evidence type
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Evidence.Storage.Postgres.Tests.Fixtures;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Evidence.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -47,7 +48,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Store Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAsync_NewEvidence_ReturnsEvidenceId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -61,7 +63,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
_output.WriteLine($"Stored evidence: {storedId}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreAsync_DuplicateEvidence_IsIdempotent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -80,7 +83,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreBatchAsync_MultipleRecords_StoresAllSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -98,7 +102,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
count.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StoreBatchAsync_WithDuplicates_StoresOnlyUnique()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +121,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region GetById Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ExistingEvidence_ReturnsEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +142,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
retrieved.Provenance.GeneratorId.Should().Be(evidence.Provenance.GeneratorId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_NonExistingEvidence_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -149,7 +156,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_WithSignatures_PreservesSignatures()
|
||||
{
|
||||
// Arrange
|
||||
@@ -170,7 +178,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region GetBySubject Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBySubjectAsync_MultipleEvidence_ReturnsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -196,7 +205,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
.Contain(new[] { EvidenceType.Scan, EvidenceType.Reachability, EvidenceType.Policy });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBySubjectAsync_WithTypeFilter_ReturnsFiltered()
|
||||
{
|
||||
// Arrange
|
||||
@@ -213,7 +223,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
retrieved[0].EvidenceType.Should().Be(EvidenceType.Scan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBySubjectAsync_NoEvidence_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -230,7 +241,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region GetByType Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByTypeAsync_MultipleEvidence_ReturnsMatchingType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -246,7 +258,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
retrieved.Should().OnlyContain(e => e.EvidenceType == EvidenceType.Scan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByTypeAsync_WithLimit_RespectsLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -266,7 +279,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Exists Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExistsAsync_ExistingEvidence_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -280,7 +294,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
exists.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExistsAsync_NonExistingEvidence_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -294,7 +309,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
exists.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExistsAsync_NonExistingSubject_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -311,7 +327,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Delete Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ExistingEvidence_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -329,7 +346,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
retrieved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_NonExistingEvidence_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -346,7 +364,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Count Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountBySubjectAsync_MultipleEvidence_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -362,7 +381,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountBySubjectAsync_NoEvidence_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
@@ -379,7 +399,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Integrity Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RoundTrip_EvidenceRecord_PreservesIntegrity()
|
||||
{
|
||||
// Arrange
|
||||
@@ -394,7 +415,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
retrieved!.VerifyIntegrity().Should().BeTrue("evidence ID should match computed hash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RoundTrip_BinaryPayload_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -417,7 +439,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
retrieved!.Payload.ToArray().Should().BeEquivalentTo(binaryPayload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RoundTrip_UnicodePayload_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -446,7 +469,8 @@ public sealed class PostgresEvidenceStoreIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Factory Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Factory_CreateStore_ReturnsTenantScopedStore()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,11 +6,13 @@ using StellaOps.Evidence.Services;
|
||||
using StellaOps.Evidence.Validation;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Evidence.Tests;
|
||||
|
||||
public class EvidenceIndexTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvidenceLinker_BuildsIndexWithDigest()
|
||||
{
|
||||
var linker = new EvidenceLinker();
|
||||
@@ -24,7 +26,8 @@ public class EvidenceIndexTests
|
||||
index.Sboms.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvidenceValidator_FlagsMissingSbom()
|
||||
{
|
||||
var index = CreateIndex() with { Sboms = [] };
|
||||
@@ -34,7 +37,8 @@ public class EvidenceIndexTests
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvidenceSerializer_RoundTrip_PreservesFields()
|
||||
{
|
||||
var index = CreateIndex();
|
||||
@@ -43,7 +47,8 @@ public class EvidenceIndexTests
|
||||
deserialized.Should().BeEquivalentTo(index);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EvidenceQueryService_BuildsSummary()
|
||||
{
|
||||
var index = CreateIndex();
|
||||
|
||||
@@ -30,7 +30,8 @@ public sealed class PostgresFixtureTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Initialize_CreatesSchema()
|
||||
{
|
||||
// Arrange
|
||||
@@ -45,7 +46,8 @@ public sealed class PostgresFixtureTests : IAsyncLifetime
|
||||
options.SchemaName.Should().StartWith("test_initialize_createsschema_");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TruncateAllTables_ClearsTables()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,7 +76,8 @@ public sealed class PostgresFixtureTests : IAsyncLifetime
|
||||
count.Should().Be(0L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispose_DropsSchema()
|
||||
{
|
||||
// Arrange
|
||||
@@ -94,6 +97,7 @@ public sealed class PostgresFixtureTests : IAsyncLifetime
|
||||
await using var cmd = new Npgsql.NpgsqlCommand(
|
||||
"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = @name)",
|
||||
conn);
|
||||
using StellaOps.TestKit;
|
||||
cmd.Parameters.AddWithValue("name", schemaName);
|
||||
var exists = await cmd.ExecuteScalarAsync();
|
||||
exists.Should().Be(false);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using StellaOps.Microservice.AspNetCore;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Microservice.AspNetCore.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -30,7 +31,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
|
||||
#region Route Normalization
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("api/items", "/api/items")]
|
||||
[InlineData("/api/items", "/api/items")]
|
||||
[InlineData("/api/items/", "/api/items")]
|
||||
@@ -51,7 +53,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
Assert.Equal(expected, endpoints[0].Path);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("/api/items/{id:int}", "/api/items/{id}")]
|
||||
[InlineData("/api/items/{id:guid}", "/api/items/{id}")]
|
||||
[InlineData("/api/items/{name:alpha:minlength(3)}", "/api/items/{name}")]
|
||||
@@ -71,7 +74,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
Assert.Equal(expected, endpoints[0].Path);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("/api/{**path}", "/api/{path}")]
|
||||
[InlineData("/files/{*filepath}", "/files/{filepath}")]
|
||||
public void NormalizeRoutePattern_NormalizesCatchAll(string input, string expected)
|
||||
@@ -93,7 +97,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
|
||||
#region Deterministic Ordering
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_OrdersByPathThenMethod()
|
||||
{
|
||||
// Arrange - create endpoints in random order
|
||||
@@ -134,7 +139,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
Assert.Equal("POST", discovered[5].Method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_IsDeterministicAcrossMultipleCalls()
|
||||
{
|
||||
// Arrange
|
||||
@@ -167,7 +173,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
|
||||
#region Duplicate Detection
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_SkipsDuplicates()
|
||||
{
|
||||
// Arrange - same path and method twice
|
||||
@@ -193,7 +200,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
|
||||
#region Excluded Paths
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_ExcludesConfiguredPaths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -226,7 +234,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
|
||||
#region HTTP Method Handling
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_HandlesMultipleMethodsPerRoute()
|
||||
{
|
||||
// Arrange - endpoint with multiple HTTP methods
|
||||
@@ -245,7 +254,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
Assert.Contains(discovered, e => e.Method == "DELETE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_NormalizesMethodToUpperCase()
|
||||
{
|
||||
// Arrange
|
||||
@@ -265,7 +275,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
|
||||
#region Metadata Extraction
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_SetsServiceNameAndVersion()
|
||||
{
|
||||
// Arrange
|
||||
@@ -284,7 +295,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
Assert.Equal("2.5.0", discovered[0].Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_ExtractsRouteParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -310,7 +322,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
Assert.Equal(ParameterSource.Route, sectionParam.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_ExtractsOptionalRouteParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -334,7 +347,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
|
||||
#region Caching
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_CachesResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -349,7 +363,8 @@ public sealed class AspNetCoreEndpointDiscoveryProviderTests
|
||||
Assert.Same(result1, result2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RefreshEndpoints_ClearsCache()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Microservice.AspNetCore;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Microservice.AspNetCore.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,7 +13,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
{
|
||||
#region No YAML Config
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_NullYamlConfig_ReturnsCodeEndpointsUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
@@ -29,7 +31,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Equal("admin", result[0].RequiringClaims[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_EmptyYamlEndpoints_ReturnsCodeEndpointsUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
|
||||
#region YamlOnly Strategy
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_YamlOnlyStrategy_NoOverrides_ClearsCodeClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -69,7 +73,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Empty(result[0].RequiringClaims); // Code claims cleared
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_YamlOnlyStrategy_WithOverrides_UsesOnlyYamlClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -95,7 +100,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Contains(result[0].RequiringClaims, c => c.Value == "write");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_YamlOnlyStrategy_NonMatchingOverride_ClearsCodeClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -119,7 +125,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
|
||||
#region AspNetMetadataOnly Strategy
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_AspNetMetadataOnlyStrategy_NoOverrides_KeepsCodeClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +145,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Equal("admin", result[0].RequiringClaims[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_AspNetMetadataOnlyStrategy_WithOverrides_IgnoresYamlClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +171,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Equal("admin", result[0].RequiringClaims[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_AspNetMetadataOnlyStrategy_StillAppliesNonClaimOverrides()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +195,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
|
||||
#region Hybrid Strategy
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_HybridStrategy_NoOverrides_KeepsCodeClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -205,7 +215,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Equal("admin", result[0].RequiringClaims[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_HybridStrategy_YamlAddsNewClaimType_BothTypesPresent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -229,7 +240,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Contains(result[0].RequiringClaims, c => c.Type == "scope" && c.Value == "read");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_HybridStrategy_YamlOverridesSameClaimType_YamlTakesPrecedence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -258,7 +270,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.DoesNotContain(result[0].RequiringClaims, c => c.Value == "admin");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_HybridStrategy_YamlEmptyClaims_KeepsCodeClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -289,7 +302,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Equal("admin", result[0].RequiringClaims[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_HybridStrategy_MultipleClaimTypesInYaml_OnlyOverridesMatchingTypes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -326,7 +340,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
|
||||
#region Timeout and Streaming Overrides
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_AppliesTimeoutOverride()
|
||||
{
|
||||
// Arrange
|
||||
@@ -343,7 +358,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.Equal(TimeSpan.FromSeconds(60), result[0].DefaultTimeout);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_AppliesStreamingOverride()
|
||||
{
|
||||
// Arrange
|
||||
@@ -360,7 +376,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
Assert.True(result[0].SupportsStreaming);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_NullOverrideProperties_KeepsCodeDefaults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -393,7 +410,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
|
||||
#region Case Insensitive Matching
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_MatchesEndpointsIgnoringCase()
|
||||
{
|
||||
// Arrange
|
||||
@@ -416,7 +434,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
|
||||
#region Multiple Endpoints
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_MultipleEndpoints_AppliesCorrectOverrides()
|
||||
{
|
||||
// Arrange
|
||||
@@ -457,7 +476,8 @@ public sealed class AspNetEndpointOverrideMergerTests
|
||||
|
||||
#region Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_ProducesDeterministicOrder()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Microservice.AspNetCore;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Microservice.AspNetCore.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -27,7 +28,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
|
||||
#region AllowAnonymous
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_AllowAnonymous_ReturnsAllowAnonymousResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -47,7 +49,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.Equal(AuthorizationSource.AspNetMetadata, result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_AllowAnonymousWithOtherAttributes_AllowAnonymousTakesPrecedence()
|
||||
{
|
||||
// Arrange - [AllowAnonymous] should override [Authorize]
|
||||
@@ -69,7 +72,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
|
||||
#region Role Extraction
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_AuthorizeWithSingleRole_ExtractsRole()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +94,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.Equal(AuthorizationSource.AspNetMetadata, result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_AuthorizeWithMultipleRoles_ExtractsAllRoles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +117,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.All(result.Claims, c => Assert.Equal(ClaimTypes.Role, c.Type));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_MultipleAuthorizeAttributes_CombinesRoles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +137,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.Contains("Moderator", result.Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_DuplicateRoles_Deduplicated()
|
||||
{
|
||||
// Arrange
|
||||
@@ -154,7 +161,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
|
||||
#region Policy Extraction
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_AuthorizeWithPolicy_ExtractsPolicy()
|
||||
{
|
||||
// Arrange
|
||||
@@ -173,7 +181,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.Equal(AuthorizationSource.AspNetMetadata, result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_MultipleAuthorizeWithPolicies_ExtractsAllPolicies()
|
||||
{
|
||||
// Arrange
|
||||
@@ -192,7 +201,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.Contains("Policy2", result.Policies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_DuplicatePolicies_Deduplicated()
|
||||
{
|
||||
// Arrange
|
||||
@@ -214,7 +224,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
|
||||
#region No Authorization
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_NoAuthorization_ReturnsEmptyResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -232,7 +243,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.Equal(AuthorizationSource.None, result.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_AuthorizeWithoutRolesOrPolicy_HasNoClaimsButSourceIsAspNet()
|
||||
{
|
||||
// Arrange - [Authorize] without roles or policy means "authenticated only"
|
||||
@@ -258,7 +270,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
|
||||
#region Combined Roles and Policies
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_AuthorizeWithRolesAndPolicy_ExtractsBoth()
|
||||
{
|
||||
// Arrange
|
||||
@@ -282,28 +295,32 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
|
||||
#region HasAuthorization
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HasAuthorization_WhenAllowAnonymous_ReturnsTrue()
|
||||
{
|
||||
var result = new AuthorizationMappingResult { AllowAnonymous = true };
|
||||
Assert.True(result.HasAuthorization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HasAuthorization_WhenHasRoles_ReturnsTrue()
|
||||
{
|
||||
var result = new AuthorizationMappingResult { Roles = ["Admin"] };
|
||||
Assert.True(result.HasAuthorization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HasAuthorization_WhenHasPolicies_ReturnsTrue()
|
||||
{
|
||||
var result = new AuthorizationMappingResult { Policies = ["Policy1"] };
|
||||
Assert.True(result.HasAuthorization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HasAuthorization_WhenHasClaims_ReturnsTrue()
|
||||
{
|
||||
var result = new AuthorizationMappingResult
|
||||
@@ -313,7 +330,8 @@ public sealed class DefaultAuthorizationClaimMapperTests
|
||||
Assert.True(result.HasAuthorization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HasAuthorization_WhenEmpty_ReturnsFalse()
|
||||
{
|
||||
var result = new AuthorizationMappingResult();
|
||||
|
||||
@@ -271,6 +271,7 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
app.MapPatch("/items/{itemId}", async ([FromRoute] string itemId, HttpContext context) =>
|
||||
{
|
||||
using var reader = new StreamReader(context.Request.Body);
|
||||
using StellaOps.TestKit;
|
||||
var bodyText = await reader.ReadToEndAsync();
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var request = JsonSerializer.Deserialize<PatchItemRequestDto>(bodyText, options);
|
||||
@@ -295,7 +296,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region FromQuery Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_StringParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -310,7 +312,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("test-search-term", body.GetProperty("query").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_IntParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -326,7 +329,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal(25, body.GetProperty("pageSize").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_BoolParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -341,7 +345,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.True(body.GetProperty("includeDeleted").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_MultipleParameters_BindCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -359,7 +364,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.False(body.GetProperty("includeDeleted").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_DefaultValues_UsedWhenNotProvided()
|
||||
{
|
||||
// Arrange
|
||||
@@ -377,7 +383,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("asc", body.GetProperty("sortOrder").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_OverrideDefaults_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -395,7 +402,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("desc", body.GetProperty("sortOrder").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_UrlEncodedValues_BindCorrectly()
|
||||
{
|
||||
// Arrange - URL encode "hello world & test"
|
||||
@@ -414,7 +422,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region FromRoute Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_SinglePathParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -429,7 +438,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("user-123", body.GetProperty("userId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_MultiplePathParameters_BindCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -445,7 +455,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("widget-456", body.GetProperty("itemId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_NumericPathParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -460,7 +471,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("12345", body.GetProperty("userId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_GuidPathParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -480,7 +492,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region FromHeader Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromHeader_AuthorizationHeader_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -498,7 +511,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("Bearer test-token-12345", body.GetProperty("authorization").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromHeader_CustomHeaders_BindCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -520,7 +534,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("en-US", body.GetProperty("acceptLanguage").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromHeader_AllHeadersAccessible()
|
||||
{
|
||||
// Arrange
|
||||
@@ -545,7 +560,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region FromBody Tests (JSON)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_SimpleJson_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -561,7 +577,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Contains("Hello, World!", body.GetProperty("echo").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_ComplexObject_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -579,7 +596,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("john@example.com", body.GetProperty("email").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_RawBody_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -597,7 +615,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal(textBody.Length.ToString(), response.Headers["X-Echo-Length"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_LargePayload_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -614,7 +633,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Contains(largeMessage, body.GetProperty("echo").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_UnicodeContent_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -635,7 +655,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region FromForm Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_SimpleFormData_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -653,7 +674,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.True(body.GetProperty("rememberMe").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_UrlEncodedSpecialChars_BindsCorrectly()
|
||||
{
|
||||
// Arrange - Special characters that need URL encoding
|
||||
@@ -669,7 +691,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("p@ss=word&special!", body.GetProperty("password").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_ContentType_IsCorrect()
|
||||
{
|
||||
// Arrange
|
||||
@@ -689,7 +712,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Combined Binding Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CombinedBinding_PathAndBody_BindCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -707,7 +731,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("New description", body.GetProperty("description").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CombinedBinding_PathQueryAndBody_BindCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -730,7 +755,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region HTTP Method Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpGet_ReturnsData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -745,7 +771,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("get-test-user", body.GetProperty("userId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPost_CreatesResource()
|
||||
{
|
||||
// Arrange
|
||||
@@ -761,7 +788,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.True(body.GetProperty("success").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPut_UpdatesResource()
|
||||
{
|
||||
// Arrange
|
||||
@@ -778,7 +806,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("Updated Name", body.GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPatch_PartialUpdate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -796,7 +825,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal(29.99m, body.GetProperty("price").GetDecimal());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPatch_OnlySpecifiedFields_Updated()
|
||||
{
|
||||
// Arrange - Only update name, not price
|
||||
@@ -814,7 +844,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.DoesNotContain("price", updatedFields.EnumerateArray().Select(e => e.GetString()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpDelete_RemovesResource()
|
||||
{
|
||||
// Arrange
|
||||
@@ -834,7 +865,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SimpleEndpoint_NoParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -849,7 +881,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("OK", body.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NonExistentEndpoint_Returns404()
|
||||
{
|
||||
// Arrange
|
||||
@@ -862,7 +895,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal(404, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WrongHttpMethod_Returns404()
|
||||
{
|
||||
// Arrange - /quick is GET only
|
||||
@@ -875,7 +909,8 @@ public sealed class MinimalApiBindingIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal(404, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConcurrentRequests_AllSucceed()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -114,6 +114,7 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
app.MapPut("/api/items/{id}", async (string id, HttpContext context) =>
|
||||
{
|
||||
using var reader = new StreamReader(context.Request.Body);
|
||||
using StellaOps.TestKit;
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
var name = data.GetProperty("name").GetString();
|
||||
@@ -124,7 +125,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Service Registration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Services_RegisteredCorrectly()
|
||||
{
|
||||
// Assert - All required services are registered
|
||||
@@ -135,7 +137,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.NotNull(_app.Services.GetService<IAspNetEndpointOverrideMerger>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BridgeOptions_ConfiguredCorrectly()
|
||||
{
|
||||
// Act
|
||||
@@ -149,7 +152,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), options.DefaultTimeout);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InstanceId_GeneratedIfNotProvided()
|
||||
{
|
||||
// Act
|
||||
@@ -165,7 +169,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Endpoint Discovery Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointDiscovery_FindsAllEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +191,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.Contains(endpoints, e => e.Method == "POST" && e.Path == "/api/items");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointDiscovery_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,7 +216,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointDiscovery_IncludesServiceMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -229,7 +236,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Request Dispatch Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_SimpleGet_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
@@ -244,7 +252,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("Healthy", body.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_GetWithRouteParameter_BindsParameter()
|
||||
{
|
||||
// Arrange
|
||||
@@ -260,7 +269,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("Item-test-item-123", body.GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_PostWithJsonBody_BindsBody()
|
||||
{
|
||||
// Arrange
|
||||
@@ -277,7 +287,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal("New Test Item", body.GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_PutWithRouteAndBody_BindsBoth()
|
||||
{
|
||||
// Arrange
|
||||
@@ -295,7 +306,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.True(body.GetProperty("updated").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_Delete_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
@@ -311,7 +323,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.True(body.GetProperty("deleted").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_NotFound_Returns404()
|
||||
{
|
||||
// Arrange
|
||||
@@ -324,7 +337,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.Equal(404, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_WrongMethod_Returns404()
|
||||
{
|
||||
// Arrange - /api/health is GET only
|
||||
@@ -341,7 +355,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Identity Population Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_WithIdentityHeaders_PopulatesUser()
|
||||
{
|
||||
// Arrange
|
||||
@@ -365,7 +380,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Concurrent Requests Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_ConcurrentRequests_AllSucceed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -391,7 +407,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Response Capture Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_ResponseContainsAllExpectedFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -407,7 +424,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
Assert.False(response.HasMoreChunks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_ContentTypeHeader_Preserved()
|
||||
{
|
||||
// Arrange
|
||||
@@ -469,7 +487,8 @@ public sealed class StellaRouterBridgeIntegrationTests : IAsyncLifetime
|
||||
/// </summary>
|
||||
public sealed class StellaRouterBridgeOptionsValidationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaRouterBridge_MissingServiceName_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -488,7 +507,8 @@ public sealed class StellaRouterBridgeOptionsValidationTests
|
||||
Assert.Contains("ServiceName", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaRouterBridge_MissingVersion_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -507,7 +527,8 @@ public sealed class StellaRouterBridgeOptionsValidationTests
|
||||
Assert.Contains("Version", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaRouterBridge_MissingRegion_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -526,7 +547,8 @@ public sealed class StellaRouterBridgeOptionsValidationTests
|
||||
Assert.Contains("Region", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaRouterBridge_ZeroTimeout_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -547,7 +569,8 @@ public sealed class StellaRouterBridgeOptionsValidationTests
|
||||
Assert.Contains("DefaultTimeout", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaRouterBridge_ExcessiveTimeout_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -568,7 +591,8 @@ public sealed class StellaRouterBridgeOptionsValidationTests
|
||||
Assert.Contains("DefaultTimeout", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaRouterBridge_ValidOptions_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -61,7 +61,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
|
||||
#region Basic Generation Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithTypedEndpoint_GeneratesSource()
|
||||
{
|
||||
// Arrange
|
||||
@@ -98,7 +99,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
generatedSource.Should().Contain("GET");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithRawEndpoint_GeneratesSource()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +133,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
generatedSource.Should().Contain("/raw/upload");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithMultipleEndpoints_GeneratesAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -175,7 +178,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
generatedSource.Should().Contain("/endpoint2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithNoEndpoints_GeneratesNothing()
|
||||
{
|
||||
// Arrange
|
||||
@@ -201,7 +205,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
|
||||
#region Attribute Property Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithTimeout_IncludesTimeoutInGeneration()
|
||||
{
|
||||
// Arrange
|
||||
@@ -232,7 +237,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
generatedSource.Should().Contain("FromSeconds(120)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithStreaming_IncludesStreamingFlag()
|
||||
{
|
||||
// Arrange
|
||||
@@ -260,7 +266,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
generatedSource.Should().Contain("SupportsStreaming = true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithRequiredClaims_IncludesClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -297,7 +304,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
|
||||
#region HTTP Method Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("GET")]
|
||||
[InlineData("POST")]
|
||||
[InlineData("PUT")]
|
||||
@@ -337,7 +345,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
|
||||
#region Error Cases Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithAbstractClass_ReportsDiagnostic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -368,7 +377,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
generatedSource.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithMissingInterface_ReportsDiagnostic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -399,7 +409,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
|
||||
#region Generated Provider Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_GeneratesProviderClass()
|
||||
{
|
||||
// Arrange
|
||||
@@ -439,7 +450,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
|
||||
#region Namespace Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithGlobalNamespace_HandlesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -468,7 +480,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
generatedSource.Should().Contain("GlobalEndpoint");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithNestedNamespace_HandlesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -504,7 +517,8 @@ public sealed class StellaEndpointGeneratorTests
|
||||
|
||||
#region Path Escaping Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Generator_WithSpecialCharactersInPath_EscapesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -513,6 +527,7 @@ public sealed class StellaEndpointGeneratorTests
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace TestNamespace
|
||||
{
|
||||
public record Req();
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Microservice.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -39,7 +40,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
|
||||
#region DiscoverEndpoints Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_CallsDiscoveryProvider()
|
||||
{
|
||||
// Arrange
|
||||
@@ -52,7 +54,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
_discoveryProviderMock.Verify(d => d.DiscoverEndpoints(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_LoadsYamlConfig()
|
||||
{
|
||||
// Arrange
|
||||
@@ -65,7 +68,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
_yamlLoaderMock.Verify(l => l.Load(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_MergesCodeAndYaml()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +97,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
_mergerMock.Verify(m => m.Merge(codeEndpoints, yamlConfig), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_ReturnsMergedEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -119,7 +124,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
result.Should().BeSameAs(mergedEndpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_WhenYamlLoadFails_UsesCodeEndpointsOnly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -139,7 +145,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
_mergerMock.Verify(m => m.Merge(codeEndpoints, null), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_WithMultipleEndpoints_ReturnsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -162,7 +169,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
result.Should().HaveCount(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_EmptyEndpoints_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -179,7 +187,8 @@ public sealed class EndpointDiscoveryServiceTests
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Microservice.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,7 +21,8 @@ public sealed class EndpointRegistryTests
|
||||
|
||||
#region Register Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_SingleEndpoint_AddsToRegistry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -35,7 +37,8 @@ public sealed class EndpointRegistryTests
|
||||
registry.GetAllEndpoints()[0].Should().Be(endpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_MultipleEndpoints_AddsAllToRegistry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public sealed class EndpointRegistryTests
|
||||
registry.GetAllEndpoints().Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegisterAll_AddsAllEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -69,7 +73,8 @@ public sealed class EndpointRegistryTests
|
||||
registry.GetAllEndpoints().Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegisterAll_WithEmptyCollection_DoesNotAddAny()
|
||||
{
|
||||
// Arrange
|
||||
@@ -86,7 +91,8 @@ public sealed class EndpointRegistryTests
|
||||
|
||||
#region TryMatch Method Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ExactMethodAndPath_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -102,7 +108,8 @@ public sealed class EndpointRegistryTests
|
||||
match!.Endpoint.Path.Should().Be("/api/users");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_NonMatchingMethod_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -117,7 +124,8 @@ public sealed class EndpointRegistryTests
|
||||
match.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_NonMatchingPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -132,7 +140,8 @@ public sealed class EndpointRegistryTests
|
||||
match.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_MethodIsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
@@ -145,7 +154,8 @@ public sealed class EndpointRegistryTests
|
||||
registry.TryMatch("GET", "/api/users", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_PathIsCaseInsensitive_WhenEnabled()
|
||||
{
|
||||
// Arrange
|
||||
@@ -157,7 +167,8 @@ public sealed class EndpointRegistryTests
|
||||
registry.TryMatch("GET", "/Api/Users", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_PathIsCaseSensitive_WhenDisabled()
|
||||
{
|
||||
// Arrange
|
||||
@@ -173,7 +184,8 @@ public sealed class EndpointRegistryTests
|
||||
|
||||
#region TryMatch Path Parameter Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_PathWithParameter_ExtractsParameter()
|
||||
{
|
||||
// Arrange
|
||||
@@ -190,7 +202,8 @@ public sealed class EndpointRegistryTests
|
||||
match.PathParameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_PathWithMultipleParameters_ExtractsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -208,7 +221,8 @@ public sealed class EndpointRegistryTests
|
||||
match.PathParameters["orderId"].Should().Be("789");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_PathParameterWithSpecialChars_ExtractsParameter()
|
||||
{
|
||||
// Arrange
|
||||
@@ -223,7 +237,8 @@ public sealed class EndpointRegistryTests
|
||||
match!.PathParameters["itemId"].Should().Be("item-with-dashes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_EmptyPathParameter_DoesNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -241,7 +256,8 @@ public sealed class EndpointRegistryTests
|
||||
|
||||
#region TryMatch Multiple Endpoints Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_FirstMatchingEndpoint_ReturnsFirst()
|
||||
{
|
||||
// Arrange
|
||||
@@ -256,7 +272,8 @@ public sealed class EndpointRegistryTests
|
||||
match.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_SelectsCorrectEndpointByMethod()
|
||||
{
|
||||
// Arrange
|
||||
@@ -291,7 +308,8 @@ public sealed class EndpointRegistryTests
|
||||
|
||||
#region GetAllEndpoints Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetAllEndpoints_EmptyRegistry_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -304,7 +322,8 @@ public sealed class EndpointRegistryTests
|
||||
endpoints.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetAllEndpoints_ReturnsAllRegisteredEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -324,7 +343,8 @@ public sealed class EndpointRegistryTests
|
||||
endpoints.Should().Contain(endpoint3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetAllEndpoints_PreservesRegistrationOrder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -349,7 +369,8 @@ public sealed class EndpointRegistryTests
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_DefaultCaseInsensitive_IsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -360,7 +381,8 @@ public sealed class EndpointRegistryTests
|
||||
registry.TryMatch("GET", "/api/test", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ExplicitCaseInsensitiveFalse_IsCaseSensitive()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed class HeaderCollectionTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Default_CreatesEmptyCollection()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -17,7 +18,8 @@ public sealed class HeaderCollectionTests
|
||||
headers.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_WithKeyValuePairs_AddsAllHeaders()
|
||||
{
|
||||
// Arrange
|
||||
@@ -35,7 +37,8 @@ public sealed class HeaderCollectionTests
|
||||
headers["Accept"].Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_WithDuplicateKeys_AddsMultipleValues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +59,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region Empty Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Empty_IsSharedInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -67,7 +71,8 @@ public sealed class HeaderCollectionTests
|
||||
empty1.Should().BeSameAs(empty2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Empty_HasNoHeaders()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -81,7 +86,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region Indexer Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Indexer_ExistingKey_ReturnsFirstValue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -95,7 +101,8 @@ public sealed class HeaderCollectionTests
|
||||
value.Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Indexer_MultipleValues_ReturnsFirstValue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -110,7 +117,8 @@ public sealed class HeaderCollectionTests
|
||||
value.Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Indexer_NonexistentKey_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -123,7 +131,8 @@ public sealed class HeaderCollectionTests
|
||||
value.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Indexer_IsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
@@ -140,7 +149,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region Add Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Add_NewKey_AddsHeader()
|
||||
{
|
||||
// Arrange
|
||||
@@ -153,7 +163,8 @@ public sealed class HeaderCollectionTests
|
||||
headers["Content-Type"].Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Add_ExistingKey_AppendsValue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -167,7 +178,8 @@ public sealed class HeaderCollectionTests
|
||||
headers.GetValues("Accept").Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Add_CaseInsensitiveKey_AppendsToExisting()
|
||||
{
|
||||
// Arrange
|
||||
@@ -185,7 +197,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region Set Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Set_NewKey_AddsHeader()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +211,8 @@ public sealed class HeaderCollectionTests
|
||||
headers["Content-Type"].Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Set_ExistingKey_ReplacesValue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -217,7 +231,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region GetValues Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetValues_ExistingKey_ReturnsAllValues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -233,7 +248,8 @@ public sealed class HeaderCollectionTests
|
||||
values.Should().BeEquivalentTo(["application/json", "text/plain", "text/html"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetValues_NonexistentKey_ReturnsEmptyEnumerable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -246,7 +262,8 @@ public sealed class HeaderCollectionTests
|
||||
values.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetValues_IsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
@@ -262,7 +279,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region TryGetValue Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryGetValue_ExistingKey_ReturnsTrueAndValue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -277,7 +295,8 @@ public sealed class HeaderCollectionTests
|
||||
value.Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryGetValue_NonexistentKey_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -291,7 +310,8 @@ public sealed class HeaderCollectionTests
|
||||
value.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryGetValue_IsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
@@ -310,7 +330,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region ContainsKey Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ContainsKey_ExistingKey_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -321,7 +342,8 @@ public sealed class HeaderCollectionTests
|
||||
headers.ContainsKey("Content-Type").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ContainsKey_NonexistentKey_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -331,7 +353,8 @@ public sealed class HeaderCollectionTests
|
||||
headers.ContainsKey("X-Missing").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ContainsKey_IsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
@@ -347,7 +370,8 @@ public sealed class HeaderCollectionTests
|
||||
|
||||
#region Enumeration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetEnumerator_EnumeratesAllHeaderValues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -366,7 +390,8 @@ public sealed class HeaderCollectionTests
|
||||
list.Should().Contain(kvp => kvp.Key == "Accept" && kvp.Value == "text/html");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetEnumerator_EmptyCollection_EnumeratesNothing()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -22,7 +22,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
|
||||
#region Track Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Track_NewRequest_ReturnsNonCancelledToken()
|
||||
{
|
||||
// Arrange
|
||||
@@ -35,7 +36,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
token.IsCancellationRequested.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Track_NewRequest_IncreasesCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -48,7 +50,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
_tracker.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Track_MultipleRequests_TracksAll()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -60,7 +63,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
_tracker.Count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Track_DuplicateCorrelationId_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +79,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
.WithMessage($"*{correlationId}*already being tracked*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Track_AfterDispose_ThrowsObjectDisposedException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +97,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
|
||||
#region Cancel Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Cancel_TrackedRequest_CancelsToken()
|
||||
{
|
||||
// Arrange
|
||||
@@ -107,7 +113,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
token.IsCancellationRequested.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Cancel_UntrackedRequest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -120,7 +127,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Cancel_WithNullReason_Works()
|
||||
{
|
||||
// Arrange
|
||||
@@ -134,7 +142,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Cancel_CompletedRequest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -153,7 +162,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
|
||||
#region Complete Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Complete_TrackedRequest_RemovesFromTracking()
|
||||
{
|
||||
// Arrange
|
||||
@@ -167,7 +177,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
_tracker.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Complete_UntrackedRequest_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
@@ -180,7 +191,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Complete_MultipleCompletions_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
@@ -202,7 +214,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
|
||||
#region CancelAll Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CancelAll_CancelsAllTrackedRequests()
|
||||
{
|
||||
// Arrange
|
||||
@@ -219,7 +232,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
token3.IsCancellationRequested.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CancelAll_ClearsTrackedRequests()
|
||||
{
|
||||
// Arrange
|
||||
@@ -233,7 +247,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
_tracker.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CancelAll_WithNoRequests_DoesNotThrow()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -247,7 +262,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
|
||||
#region Dispose Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_CancelsAllRequests()
|
||||
{
|
||||
// Arrange
|
||||
@@ -260,7 +276,8 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
token.IsCancellationRequested.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -279,17 +296,20 @@ public sealed class InflightRequestTrackerTests : IDisposable
|
||||
|
||||
#region Count Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Count_InitiallyZero()
|
||||
{
|
||||
// Arrange - use a fresh tracker
|
||||
using var tracker = new InflightRequestTracker(NullLogger<InflightRequestTracker>.Instance);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Assert
|
||||
tracker.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Count_ReflectsActiveRequests()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed class RawRequestContextTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Method_DefaultsToEmptyString()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -17,7 +18,8 @@ public sealed class RawRequestContextTests
|
||||
context.Method.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Path_DefaultsToEmptyString()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -27,7 +29,8 @@ public sealed class RawRequestContextTests
|
||||
context.Path.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_PathParameters_DefaultsToEmptyDictionary()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -38,7 +41,8 @@ public sealed class RawRequestContextTests
|
||||
context.PathParameters.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Headers_DefaultsToEmptyCollection()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -48,7 +52,8 @@ public sealed class RawRequestContextTests
|
||||
context.Headers.Should().BeSameAs(HeaderCollection.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Body_DefaultsToStreamNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -58,7 +63,8 @@ public sealed class RawRequestContextTests
|
||||
context.Body.Should().BeSameAs(Stream.Null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_CancellationToken_DefaultsToNone()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -68,7 +74,8 @@ public sealed class RawRequestContextTests
|
||||
context.CancellationToken.Should().Be(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_CorrelationId_DefaultsToNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -82,7 +89,8 @@ public sealed class RawRequestContextTests
|
||||
|
||||
#region Property Initialization Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Method_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -92,7 +100,8 @@ public sealed class RawRequestContextTests
|
||||
context.Method.Should().Be("POST");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Path_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -102,7 +111,8 @@ public sealed class RawRequestContextTests
|
||||
context.Path.Should().Be("/api/users/123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathParameters_CanBeInitialized()
|
||||
{
|
||||
// Arrange
|
||||
@@ -121,7 +131,8 @@ public sealed class RawRequestContextTests
|
||||
context.PathParameters["action"].Should().Be("update");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Headers_CanBeInitialized()
|
||||
{
|
||||
// Arrange
|
||||
@@ -137,7 +148,8 @@ public sealed class RawRequestContextTests
|
||||
context.Headers["Authorization"].Should().Be("Bearer token");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Body_CanBeInitialized()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,7 +162,8 @@ public sealed class RawRequestContextTests
|
||||
context.Body.Should().BeSameAs(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CancellationToken_CanBeInitialized()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +176,8 @@ public sealed class RawRequestContextTests
|
||||
context.CancellationToken.Should().Be(cts.Token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CorrelationId_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -177,7 +191,8 @@ public sealed class RawRequestContextTests
|
||||
|
||||
#region Complete Context Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompleteContext_AllPropertiesSet_Works()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,11 +225,13 @@ public sealed class RawRequestContextTests
|
||||
context.CorrelationId.Should().Be("corr-789");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Context_WithCancelledToken_HasCancellationRequested()
|
||||
{
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource();
|
||||
using StellaOps.TestKit;
|
||||
cts.Cancel();
|
||||
|
||||
// Act
|
||||
@@ -228,7 +245,8 @@ public sealed class RawRequestContextTests
|
||||
|
||||
#region Typical Use Case Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TypicalGetRequest_HasMinimalProperties()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -245,7 +263,8 @@ public sealed class RawRequestContextTests
|
||||
context.Headers.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TypicalPostRequest_HasBodyAndHeaders()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -9,7 +9,8 @@ public sealed class RawResponseTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_StatusCode_DefaultsTo200()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -19,7 +20,8 @@ public sealed class RawResponseTests
|
||||
response.StatusCode.Should().Be(200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Headers_DefaultsToEmpty()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -29,7 +31,8 @@ public sealed class RawResponseTests
|
||||
response.Headers.Should().BeSameAs(HeaderCollection.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Body_DefaultsToStreamNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -43,7 +46,8 @@ public sealed class RawResponseTests
|
||||
|
||||
#region Ok Factory Method Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Ok_WithStream_CreatesOkResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +61,8 @@ public sealed class RawResponseTests
|
||||
response.Body.Should().BeSameAs(stream);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Ok_WithByteArray_CreatesOkResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -72,7 +77,8 @@ public sealed class RawResponseTests
|
||||
((MemoryStream)response.Body).ToArray().Should().BeEquivalentTo(data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Ok_WithString_CreatesOkResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -87,7 +93,8 @@ public sealed class RawResponseTests
|
||||
reader.ReadToEnd().Should().Be(text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Ok_WithEmptyString_CreatesOkResponse()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -102,7 +109,8 @@ public sealed class RawResponseTests
|
||||
|
||||
#region NoContent Factory Method Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NoContent_Creates204Response()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -112,7 +120,8 @@ public sealed class RawResponseTests
|
||||
response.StatusCode.Should().Be(204);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NoContent_HasDefaultHeaders()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -122,7 +131,8 @@ public sealed class RawResponseTests
|
||||
response.Headers.Should().BeSameAs(HeaderCollection.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NoContent_HasDefaultBody()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -136,7 +146,8 @@ public sealed class RawResponseTests
|
||||
|
||||
#region BadRequest Factory Method Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BadRequest_Creates400Response()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -146,7 +157,8 @@ public sealed class RawResponseTests
|
||||
response.StatusCode.Should().Be(400);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BadRequest_WithDefaultMessage_HasBadRequestText()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -157,7 +169,8 @@ public sealed class RawResponseTests
|
||||
reader.ReadToEnd().Should().Be("Bad Request");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BadRequest_WithCustomMessage_HasCustomText()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -168,7 +181,8 @@ public sealed class RawResponseTests
|
||||
reader.ReadToEnd().Should().Be("Invalid input");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BadRequest_SetsTextPlainContentType()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -182,7 +196,8 @@ public sealed class RawResponseTests
|
||||
|
||||
#region NotFound Factory Method Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NotFound_Creates404Response()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -192,7 +207,8 @@ public sealed class RawResponseTests
|
||||
response.StatusCode.Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NotFound_WithDefaultMessage_HasNotFoundText()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -203,7 +219,8 @@ public sealed class RawResponseTests
|
||||
reader.ReadToEnd().Should().Be("Not Found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NotFound_WithCustomMessage_HasCustomText()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -218,7 +235,8 @@ public sealed class RawResponseTests
|
||||
|
||||
#region InternalError Factory Method Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InternalError_Creates500Response()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -228,7 +246,8 @@ public sealed class RawResponseTests
|
||||
response.StatusCode.Should().Be(500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InternalError_WithDefaultMessage_HasInternalServerErrorText()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -239,7 +258,8 @@ public sealed class RawResponseTests
|
||||
reader.ReadToEnd().Should().Be("Internal Server Error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InternalError_WithCustomMessage_HasCustomText()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -254,7 +274,8 @@ public sealed class RawResponseTests
|
||||
|
||||
#region Error Factory Method Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(400, "Bad Request")]
|
||||
[InlineData(401, "Unauthorized")]
|
||||
[InlineData(403, "Forbidden")]
|
||||
@@ -271,7 +292,8 @@ public sealed class RawResponseTests
|
||||
response.StatusCode.Should().Be(statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Error_SetsCorrectContentType()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -281,7 +303,8 @@ public sealed class RawResponseTests
|
||||
response.Headers["Content-Type"].Should().Be("text/plain; charset=utf-8");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Error_SetsMessageInBody()
|
||||
{
|
||||
// Arrange
|
||||
@@ -295,7 +318,8 @@ public sealed class RawResponseTests
|
||||
reader.ReadToEnd().Should().Be(message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Error_WithUnicodeMessage_EncodesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -306,6 +330,7 @@ public sealed class RawResponseTests
|
||||
|
||||
// Assert
|
||||
using var reader = new StreamReader(response.Body, Encoding.UTF8);
|
||||
using StellaOps.TestKit;
|
||||
reader.ReadToEnd().Should().Be(message);
|
||||
}
|
||||
|
||||
@@ -313,7 +338,8 @@ public sealed class RawResponseTests
|
||||
|
||||
#region Property Initialization Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void StatusCode_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -323,7 +349,8 @@ public sealed class RawResponseTests
|
||||
response.StatusCode.Should().Be(201);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Headers_CanBeInitialized()
|
||||
{
|
||||
// Arrange
|
||||
@@ -337,7 +364,8 @@ public sealed class RawResponseTests
|
||||
response.Headers["X-Custom"].Should().Be("value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Body_CanBeInitialized()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -51,7 +51,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_InitializesCorrectly()
|
||||
{
|
||||
// Act
|
||||
@@ -68,7 +69,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region CurrentStatus Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CurrentStatus_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -81,7 +83,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
manager.CurrentStatus.Should().Be(InstanceHealthStatus.Draining);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(InstanceHealthStatus.Healthy)]
|
||||
[InlineData(InstanceHealthStatus.Degraded)]
|
||||
[InlineData(InstanceHealthStatus.Draining)]
|
||||
@@ -102,7 +105,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region InFlightRequestCount Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InFlightRequestCount_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -119,7 +123,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region ErrorRate Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ErrorRate_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +141,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region StartAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StartAsync_DiscoversEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -157,7 +163,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
_discoveryProviderMock.Verify(d => d.DiscoverEndpoints(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StartAsync_WithRouters_CreatesConnections()
|
||||
{
|
||||
// Arrange
|
||||
@@ -180,7 +187,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
await manager.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StartAsync_RegistersEndpointsInConnection()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,7 +218,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
await manager.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StartAsync_AfterDispose_ThrowsObjectDisposedException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -228,7 +237,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region StopAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StopAsync_ClearsConnections()
|
||||
{
|
||||
// Arrange
|
||||
@@ -252,7 +262,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region Heartbeat Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Heartbeat_SendsViaTransport()
|
||||
{
|
||||
// Arrange
|
||||
@@ -275,7 +286,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
Times.AtLeastOnce);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Heartbeat_IncludesCurrentMetrics()
|
||||
{
|
||||
// Arrange
|
||||
@@ -286,6 +298,7 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
TransportType = TransportType.InMemory
|
||||
});
|
||||
using var manager = CreateManager();
|
||||
using StellaOps.TestKit;
|
||||
manager.CurrentStatus = InstanceHealthStatus.Degraded;
|
||||
manager.InFlightRequestCount = 10;
|
||||
manager.ErrorRate = 0.05;
|
||||
@@ -311,7 +324,8 @@ public sealed class RouterConnectionManagerTests : IDisposable
|
||||
|
||||
#region Dispose Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,6 +6,7 @@ using StellaOps.Provcache.Api;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Provcache.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -22,7 +23,8 @@ public sealed class ApiContractTests
|
||||
|
||||
#region CacheSource Contract Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("none")]
|
||||
[InlineData("inMemory")]
|
||||
[InlineData("redis")]
|
||||
@@ -45,7 +47,8 @@ public sealed class ApiContractTests
|
||||
|
||||
#region TrustScoreBreakdown Contract Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustScoreBreakdown_DefaultWeights_SumToOne()
|
||||
{
|
||||
// Verify the standard weights sum to 1.0 (100%)
|
||||
@@ -60,7 +63,8 @@ public sealed class ApiContractTests
|
||||
totalWeight.Should().Be(1.00m, "standard weights must sum to 100%");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustScoreBreakdown_StandardWeights_MatchDocumentation()
|
||||
{
|
||||
// Verify weights match the documented percentages
|
||||
@@ -74,7 +78,8 @@ public sealed class ApiContractTests
|
||||
breakdown.SignerTrust.Weight.Should().Be(0.20m, "Signer trust weight should be 20%");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustScoreBreakdown_ComputeTotal_ReturnsCorrectWeightedSum()
|
||||
{
|
||||
// Given all scores at 100, total should be 100
|
||||
@@ -88,7 +93,8 @@ public sealed class ApiContractTests
|
||||
breakdown.ComputeTotal().Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustScoreBreakdown_ComputeTotal_WithZeroScores_ReturnsZero()
|
||||
{
|
||||
var breakdown = TrustScoreBreakdown.CreateDefault();
|
||||
@@ -96,7 +102,8 @@ public sealed class ApiContractTests
|
||||
breakdown.ComputeTotal().Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustScoreBreakdown_ComputeTotal_WithMixedScores_ComputesCorrectly()
|
||||
{
|
||||
// Specific test case:
|
||||
@@ -116,7 +123,8 @@ public sealed class ApiContractTests
|
||||
breakdown.ComputeTotal().Should().Be(79);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustScoreBreakdown_Serialization_IncludesAllComponents()
|
||||
{
|
||||
var breakdown = TrustScoreBreakdown.CreateDefault(50, 60, 70, 80, 90);
|
||||
@@ -130,7 +138,8 @@ public sealed class ApiContractTests
|
||||
json.Should().Contain("\"signerTrust\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TrustScoreComponent_Contribution_CalculatesCorrectly()
|
||||
{
|
||||
var component = new TrustScoreComponent { Score = 80, Weight = 0.25m };
|
||||
@@ -142,7 +151,8 @@ public sealed class ApiContractTests
|
||||
|
||||
#region DecisionDigest Contract Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DecisionDigest_TrustScoreBreakdown_IsOptional()
|
||||
{
|
||||
// DecisionDigest should serialize correctly without TrustScoreBreakdown
|
||||
@@ -168,7 +178,8 @@ public sealed class ApiContractTests
|
||||
digest.TrustScoreBreakdown.Should().BeNull("TrustScoreBreakdown should be optional");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DecisionDigest_WithBreakdown_SerializesCorrectly()
|
||||
{
|
||||
var digest = new DecisionDigest
|
||||
@@ -194,7 +205,8 @@ public sealed class ApiContractTests
|
||||
|
||||
#region InputManifest Contract Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InputManifestResponse_RequiredFields_NotNull()
|
||||
{
|
||||
var manifest = new InputManifestResponse
|
||||
@@ -218,7 +230,8 @@ public sealed class ApiContractTests
|
||||
manifest.TimeWindow.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InputManifestResponse_Serialization_IncludesAllComponents()
|
||||
{
|
||||
var manifest = new InputManifestResponse
|
||||
@@ -245,7 +258,8 @@ public sealed class ApiContractTests
|
||||
json.Should().Contain("\"generatedAt\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SbomInfoDto_OptionalFields_CanBeNull()
|
||||
{
|
||||
var sbom = new SbomInfoDto
|
||||
@@ -262,7 +276,8 @@ public sealed class ApiContractTests
|
||||
// Optional fields should not be serialized as null (default JsonSerializer behavior with ignore defaults)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VexInfoDto_Sources_CanBeEmpty()
|
||||
{
|
||||
var vex = new VexInfoDto
|
||||
@@ -276,7 +291,8 @@ public sealed class ApiContractTests
|
||||
vex.StatementCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyInfoDto_OptionalFields_PreserveValues()
|
||||
{
|
||||
var policy = new PolicyInfoDto
|
||||
@@ -293,7 +309,8 @@ public sealed class ApiContractTests
|
||||
policy.Name.Should().Be("Organization Security Policy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignerInfoDto_Certificates_CanBeNull()
|
||||
{
|
||||
var signers = new SignerInfoDto
|
||||
@@ -306,7 +323,8 @@ public sealed class ApiContractTests
|
||||
signers.Certificates.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignerCertificateDto_AllFields_AreOptional()
|
||||
{
|
||||
var cert = new SignerCertificateDto
|
||||
@@ -321,7 +339,8 @@ public sealed class ApiContractTests
|
||||
json.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TimeWindowInfoDto_Bucket_IsRequired()
|
||||
{
|
||||
var timeWindow = new TimeWindowInfoDto
|
||||
@@ -338,7 +357,8 @@ public sealed class ApiContractTests
|
||||
|
||||
#region API Response Backwards Compatibility
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ProvcacheGetResponse_Status_ValidValues()
|
||||
{
|
||||
// Verify status field uses expected values
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Provcache.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -22,7 +23,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 12, 24, 12, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_SameInputs_ProducesSameDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +59,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest1.TrustScore.Should().Be(digest2.TrustScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DispositionsInDifferentOrder_ProducesSameVerdictHash()
|
||||
{
|
||||
// Arrange - Same dispositions, different insertion order
|
||||
@@ -83,7 +86,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest1.VerdictHash.Should().Be(digest2.VerdictHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentDispositions_ProducesDifferentVerdictHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -98,7 +102,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest1.VerdictHash.Should().NotBe(digest2.VerdictHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_SameEvidenceChunks_ProducesSameMerkleRoot()
|
||||
{
|
||||
// Arrange - valid SHA256 hex hashes (64 characters each)
|
||||
@@ -118,7 +123,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest1.ProofRoot.Should().Be(digest2.ProofRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentEvidenceChunkOrder_ProducesDifferentMerkleRoot()
|
||||
{
|
||||
// Arrange - Merkle tree is order-sensitive (valid SHA256 hex hashes)
|
||||
@@ -141,7 +147,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest1.ProofRoot.Should().NotBe(digest2.ProofRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithTrustScore_ComponentWeights_ProducesConsistentScore()
|
||||
{
|
||||
// Arrange - Using weighted formula: 25% reach + 20% sbom + 20% vex + 15% policy + 20% signer
|
||||
@@ -161,7 +168,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest.TrustScore.Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithTrustScore_MixedScores_CalculatesCorrectWeight()
|
||||
{
|
||||
// Arrange - 80 * 0.25 + 60 * 0.20 + 70 * 0.20 + 50 * 0.15 + 90 * 0.20
|
||||
@@ -181,7 +189,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest.TrustScore.Should().Be(72);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithDefaultTimestamps_UsesFrozenTime()
|
||||
{
|
||||
// Arrange
|
||||
@@ -204,7 +213,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest.ExpiresAt.Should().Be(frozenTime.Add(_options.DefaultTtl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_MultipleTimes_ReturnsConsistentDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -222,7 +232,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digests.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_EmptyDispositions_ProducesConsistentHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -238,7 +249,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest1.VerdictHash.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_EmptyEvidenceChunks_ProducesConsistentHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -254,7 +266,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest1.ProofRoot.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_ReplaySeedPreservedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -273,7 +286,8 @@ public class DecisionDigestBuilderDeterminismTests
|
||||
digest.ReplaySeed.FrozenEpoch.Should().Be(frozenEpoch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_MissingComponent_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -10,6 +10,7 @@ using Moq;
|
||||
using StellaOps.Provcache.Api;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Provcache.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -67,7 +68,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEvidenceChunks_ReturnsChunksWithPagination()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +108,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
result.NextCursor.Should().Be("10");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEvidenceChunks_WithOffset_ReturnsPaginatedResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -144,7 +147,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
result.HasMore.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEvidenceChunks_WithIncludeData_ReturnsBase64Blobs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +182,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
result!.Chunks[0].Data.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEvidenceChunks_NotFound_Returns404()
|
||||
{
|
||||
// Arrange
|
||||
@@ -193,7 +198,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetProofManifest_ReturnsManifestWithChunkMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -227,7 +233,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
result.Chunks.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetProofManifest_NotFound_Returns404()
|
||||
{
|
||||
// Arrange
|
||||
@@ -242,7 +249,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSingleChunk_ReturnsChunkWithData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -264,7 +272,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
result.Data.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSingleChunk_NotFound_Returns404()
|
||||
{
|
||||
// Arrange
|
||||
@@ -279,7 +288,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyProof_ValidChunks_ReturnsIsValidTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -310,7 +320,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
result.ChunkResults.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyProof_MerkleRootMismatch_ReturnsIsValidFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -340,7 +351,8 @@ public sealed class EvidenceApiTests : IAsyncLifetime
|
||||
result.Error.Should().Contain("Merkle root mismatch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyProof_NoChunks_Returns404()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -18,7 +18,8 @@ public sealed class EvidenceChunkerTests
|
||||
_chunker = new EvidenceChunker(_options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ChunkAsync_ShouldSplitEvidenceIntoMultipleChunks_WhenLargerThanChunkSize()
|
||||
{
|
||||
// Arrange
|
||||
@@ -44,7 +45,8 @@ public sealed class EvidenceChunkerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ChunkAsync_ShouldCreateSingleChunk_WhenSmallerThanChunkSize()
|
||||
{
|
||||
// Arrange
|
||||
@@ -62,7 +64,8 @@ public sealed class EvidenceChunkerTests
|
||||
result.Chunks[0].BlobSize.Should().Be(32);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ChunkAsync_ShouldHandleEmptyEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -78,7 +81,8 @@ public sealed class EvidenceChunkerTests
|
||||
result.TotalSize.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ChunkAsync_ShouldProduceUniqueHashForEachChunk()
|
||||
{
|
||||
// Arrange - create evidence with distinct bytes per chunk
|
||||
@@ -95,7 +99,8 @@ public sealed class EvidenceChunkerTests
|
||||
result.Chunks[0].ChunkHash.Should().NotBe(result.Chunks[1].ChunkHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReassembleAsync_ShouldRecoverOriginalEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +117,8 @@ public sealed class EvidenceChunkerTests
|
||||
reassembled.Should().BeEquivalentTo(original);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReassembleAsync_ShouldThrow_WhenMerkleRootMismatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -128,7 +134,8 @@ public sealed class EvidenceChunkerTests
|
||||
.WithMessage("*Merkle root mismatch*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReassembleAsync_ShouldThrow_WhenChunkCorrupted()
|
||||
{
|
||||
// Arrange
|
||||
@@ -151,7 +158,8 @@ public sealed class EvidenceChunkerTests
|
||||
.WithMessage("*verification failed*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyChunk_ShouldReturnTrue_WhenChunkValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -175,7 +183,8 @@ public sealed class EvidenceChunkerTests
|
||||
_chunker.VerifyChunk(chunk).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyChunk_ShouldReturnFalse_WhenHashMismatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -195,7 +204,8 @@ public sealed class EvidenceChunkerTests
|
||||
_chunker.VerifyChunk(chunk).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_ShouldReturnSameResult_ForSameInput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,7 +220,8 @@ public sealed class EvidenceChunkerTests
|
||||
root1.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_ShouldHandleSingleHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -223,7 +234,8 @@ public sealed class EvidenceChunkerTests
|
||||
root.Should().Be("sha256:aabbccdd");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_ShouldHandleOddNumberOfHashes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -237,13 +249,15 @@ public sealed class EvidenceChunkerTests
|
||||
root.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ChunkStreamAsync_ShouldYieldChunksInOrder()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = new byte[200];
|
||||
Random.Shared.NextBytes(evidence);
|
||||
using var stream = new MemoryStream(evidence);
|
||||
using StellaOps.TestKit;
|
||||
const string contentType = "application/octet-stream";
|
||||
|
||||
// Act
|
||||
@@ -261,7 +275,8 @@ public sealed class EvidenceChunkerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Roundtrip_ShouldPreserveDataIntegrity()
|
||||
{
|
||||
// Arrange - use realistic chunk size
|
||||
|
||||
@@ -2,6 +2,7 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Provcache.Tests;
|
||||
|
||||
public sealed class LazyFetchTests
|
||||
@@ -17,7 +18,8 @@ public sealed class LazyFetchTests
|
||||
NullLogger<LazyFetchOrchestrator>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAndStoreAsync_WhenFetcherNotAvailable_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -34,7 +36,8 @@ public sealed class LazyFetchTests
|
||||
result.Errors.Should().Contain(e => e.Contains("not available"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAndStoreAsync_WhenNoManifestFound_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +59,8 @@ public sealed class LazyFetchTests
|
||||
result.Errors.Should().Contain(e => e.Contains("No manifest found"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAndStoreAsync_WhenAllChunksPresent_ReturnsSuccessWithZeroFetched()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +86,8 @@ public sealed class LazyFetchTests
|
||||
result.BytesFetched.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAndStoreAsync_FetchesMissingChunks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +129,8 @@ public sealed class LazyFetchTests
|
||||
It.IsAny<CancellationToken>()), Times.AtLeastOnce);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAndStoreAsync_WithVerification_RejectsCorruptedChunks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -166,7 +172,8 @@ public sealed class LazyFetchTests
|
||||
result.ChunksFetched.Should().Be(0); // Nothing stored
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAndStoreAsync_WithFailOnVerificationError_AbortsOnCorruption()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,7 +217,8 @@ public sealed class LazyFetchTests
|
||||
result.ChunksFailedVerification.Should().BeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAndStoreAsync_RespectsMaxChunksLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -250,7 +258,8 @@ public sealed class LazyFetchTests
|
||||
result.ChunksFetched.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FileChunkFetcher_FetcherType_ReturnsFile()
|
||||
{
|
||||
// Arrange
|
||||
@@ -261,7 +270,8 @@ public sealed class LazyFetchTests
|
||||
fetcher.FetcherType.Should().Be("file");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FileChunkFetcher_IsAvailableAsync_ReturnsTrueWhenDirectoryExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -284,7 +294,8 @@ public sealed class LazyFetchTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FileChunkFetcher_IsAvailableAsync_ReturnsFalseWhenDirectoryMissing()
|
||||
{
|
||||
// Arrange
|
||||
@@ -298,7 +309,8 @@ public sealed class LazyFetchTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FileChunkFetcher_FetchChunkAsync_ReturnsNullWhenChunkNotFound()
|
||||
{
|
||||
// Arrange
|
||||
@@ -321,7 +333,8 @@ public sealed class LazyFetchTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HttpChunkFetcher_FetcherType_ReturnsHttp()
|
||||
{
|
||||
// Arrange
|
||||
@@ -332,7 +345,8 @@ public sealed class LazyFetchTests
|
||||
fetcher.FetcherType.Should().Be("http");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpChunkFetcher_IsAvailableAsync_ReturnsFalseWhenHostUnreachable()
|
||||
{
|
||||
// Arrange - use a non-routable IP to ensure connection failure
|
||||
|
||||
@@ -113,7 +113,8 @@ public sealed class MinimalProofExporterTests
|
||||
|
||||
#region Export Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_LiteDensity_ReturnsDigestAndManifestOnly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -132,7 +133,8 @@ public sealed class MinimalProofExporterTests
|
||||
bundle.Signature.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_StandardDensity_ReturnsFirstNChunks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +162,8 @@ public sealed class MinimalProofExporterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_StrictDensity_ReturnsAllChunks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -177,7 +180,8 @@ public sealed class MinimalProofExporterTests
|
||||
bundle.Chunks.Select(c => c.Index).Should().BeEquivalentTo([0, 1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_NotFound_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -190,7 +194,8 @@ public sealed class MinimalProofExporterTests
|
||||
_exporter.ExportAsync("sha256:notfound", options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsJsonAsync_ReturnsValidJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -207,7 +212,8 @@ public sealed class MinimalProofExporterTests
|
||||
bundle!.BundleVersion.Should().Be("v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportToStreamAsync_WritesToStream()
|
||||
{
|
||||
// Arrange
|
||||
@@ -215,6 +221,7 @@ public sealed class MinimalProofExporterTests
|
||||
var options = new MinimalProofExportOptions { Density = ProofDensity.Lite };
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
await _exporter.ExportToStreamAsync(_testEntry.VeriKey, options, stream);
|
||||
|
||||
@@ -229,7 +236,8 @@ public sealed class MinimalProofExporterTests
|
||||
|
||||
#region Import Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ImportAsync_ValidBundle_StoresChunks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -255,7 +263,8 @@ public sealed class MinimalProofExporterTests
|
||||
result.Verification.ChunksValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ImportFromJsonAsync_ValidJson_ImportsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -275,7 +284,8 @@ public sealed class MinimalProofExporterTests
|
||||
|
||||
#region Verify Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidBundle_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -294,7 +304,8 @@ public sealed class MinimalProofExporterTests
|
||||
verification.FailedChunkIndices.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_CorruptedChunk_ReportsFailure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -315,7 +326,8 @@ public sealed class MinimalProofExporterTests
|
||||
verification.FailedChunkIndices.Should().Contain(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_InvalidDigest_ReportsFailure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -338,7 +350,8 @@ public sealed class MinimalProofExporterTests
|
||||
|
||||
#region EstimateSize Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EstimateExportSizeAsync_LiteDensity_ReturnsBaseSize()
|
||||
{
|
||||
// Arrange
|
||||
@@ -351,7 +364,8 @@ public sealed class MinimalProofExporterTests
|
||||
size.Should().Be(2048); // Base size
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EstimateExportSizeAsync_StrictDensity_ReturnsLargerSize()
|
||||
{
|
||||
// Arrange
|
||||
@@ -364,7 +378,8 @@ public sealed class MinimalProofExporterTests
|
||||
size.Should().BeGreaterThan(2048); // Base + all chunk data
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EstimateExportSizeAsync_NotFound_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
@@ -382,7 +397,8 @@ public sealed class MinimalProofExporterTests
|
||||
|
||||
#region Signing Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_SigningWithoutSigner_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -398,7 +414,8 @@ public sealed class MinimalProofExporterTests
|
||||
_exporter.ExportAsync(_testEntry.VeriKey, options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_WithSigner_SignsBundle()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Provcache.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +66,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
|
||||
#region GET /v1/provcache/{veriKey}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByVeriKey_CacheHit_Returns200WithEntry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +90,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
content.Source.Should().Be("valkey");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByVeriKey_CacheMiss_Returns204()
|
||||
{
|
||||
// Arrange
|
||||
@@ -105,7 +108,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByVeriKey_Expired_Returns410Gone()
|
||||
{
|
||||
// Arrange
|
||||
@@ -123,7 +127,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Gone);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByVeriKey_WithBypassCache_PassesFlagToService()
|
||||
{
|
||||
// Arrange
|
||||
@@ -144,7 +149,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
|
||||
#region POST /v1/provcache
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdate_ValidRequest_Returns201Created()
|
||||
{
|
||||
// Arrange
|
||||
@@ -170,7 +176,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
content.Success.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdate_NullEntry_Returns400BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -187,7 +194,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
|
||||
#region POST /v1/provcache/invalidate
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Invalidate_SingleVeriKey_Returns200WithAffectedCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -214,7 +222,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
content.Type.Should().Be("verikey");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Invalidate_ByPolicyHash_Returns200WithBulkResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -249,7 +258,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
content!.EntriesAffected.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Invalidate_ByPattern_Returns200WithPatternResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -288,7 +298,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
|
||||
#region GET /v1/provcache/metrics
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetMetrics_Returns200WithMetrics()
|
||||
{
|
||||
// Arrange
|
||||
@@ -325,7 +336,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
|
||||
#region Contract Verification Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByVeriKey_ResponseContract_HasRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -350,7 +362,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
root.TryGetProperty("entry", out _).Should().BeTrue("Response must have 'entry' field");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdate_ResponseContract_HasRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -374,7 +387,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
root.TryGetProperty("expiresAt", out _).Should().BeTrue("Response must have 'expiresAt' field");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task InvalidateResponse_Contract_HasRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -401,7 +415,8 @@ public sealed class ProvcacheApiTests : IAsyncDisposable
|
||||
root.TryGetProperty("value", out _).Should().BeTrue("Response must have 'value' field");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MetricsResponse_Contract_HasRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Provcache.Entities;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Provcache.Tests;
|
||||
|
||||
public sealed class RevocationLedgerTests
|
||||
@@ -14,7 +15,8 @@ public sealed class RevocationLedgerTests
|
||||
_ledger = new InMemoryRevocationLedger(NullLogger<InMemoryRevocationLedger>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RecordAsync_AssignsSeqNo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -29,7 +31,8 @@ public sealed class RevocationLedgerTests
|
||||
recorded.RevokedKey.Should().Be("signer-hash-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RecordAsync_AssignsIncrementingSeqNos()
|
||||
{
|
||||
// Arrange
|
||||
@@ -48,7 +51,8 @@ public sealed class RevocationLedgerTests
|
||||
recorded3.SeqNo.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEntriesSinceAsync_ReturnsEntriesAfterSeqNo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -66,7 +70,8 @@ public sealed class RevocationLedgerTests
|
||||
entries[1].SeqNo.Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEntriesSinceAsync_RespectsLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +87,8 @@ public sealed class RevocationLedgerTests
|
||||
entries.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEntriesByTypeAsync_FiltersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +105,8 @@ public sealed class RevocationLedgerTests
|
||||
signerEntries.Should().OnlyContain(e => e.RevocationType == RevocationTypes.Signer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetEntriesByTypeAsync_FiltersBySinceTime()
|
||||
{
|
||||
// Arrange
|
||||
@@ -125,7 +132,8 @@ public sealed class RevocationLedgerTests
|
||||
entries[0].RevokedKey.Should().Be("s2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetLatestSeqNoAsync_ReturnsZeroWhenEmpty()
|
||||
{
|
||||
// Act
|
||||
@@ -135,7 +143,8 @@ public sealed class RevocationLedgerTests
|
||||
seqNo.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetLatestSeqNoAsync_ReturnsLatest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,7 +159,8 @@ public sealed class RevocationLedgerTests
|
||||
seqNo.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetRevocationsForKeyAsync_ReturnsMatchingEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -166,7 +176,8 @@ public sealed class RevocationLedgerTests
|
||||
entries.Should().OnlyContain(e => e.RevokedKey == "s1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetStatsAsync_ReturnsCorrectStats()
|
||||
{
|
||||
// Arrange
|
||||
@@ -188,7 +199,8 @@ public sealed class RevocationLedgerTests
|
||||
stats.EntriesByType[RevocationTypes.Policy].Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -237,7 +249,8 @@ public sealed class RevocationReplayServiceTests
|
||||
NullLogger<RevocationReplayService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplayFromAsync_ReplaysAllEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -262,7 +275,8 @@ public sealed class RevocationReplayServiceTests
|
||||
result.EntriesByType.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplayFromAsync_StartsFromCheckpoint()
|
||||
{
|
||||
// Arrange
|
||||
@@ -282,7 +296,8 @@ public sealed class RevocationReplayServiceTests
|
||||
result.EndSeqNo.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplayFromAsync_RespectsMaxEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -303,7 +318,8 @@ public sealed class RevocationReplayServiceTests
|
||||
result.EntriesReplayed.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplayFromAsync_ReturnsEmptyWhenNoEntries()
|
||||
{
|
||||
// Act
|
||||
@@ -314,7 +330,8 @@ public sealed class RevocationReplayServiceTests
|
||||
result.EntriesReplayed.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetCheckpointAsync_ReturnsZeroInitially()
|
||||
{
|
||||
// Act
|
||||
@@ -324,7 +341,8 @@ public sealed class RevocationReplayServiceTests
|
||||
checkpoint.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveCheckpointAsync_PersistsCheckpoint()
|
||||
{
|
||||
// Act
|
||||
|
||||
@@ -47,7 +47,8 @@ public class WriteBehindQueueTests
|
||||
HitCount = 0
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_SingleEntry_UpdatesMetrics()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +69,8 @@ public class WriteBehindQueueTests
|
||||
metrics.CurrentQueueDepth.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_MultipleEntries_TracksQueueDepth()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +92,8 @@ public class WriteBehindQueueTests
|
||||
metrics.CurrentQueueDepth.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetMetrics_InitialState_AllZeros()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +115,8 @@ public class WriteBehindQueueTests
|
||||
metrics.CurrentQueueDepth.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ProcessBatch_SuccessfulPersist_UpdatesPersistMetrics()
|
||||
{
|
||||
// Arrange
|
||||
@@ -133,6 +137,7 @@ public class WriteBehindQueueTests
|
||||
|
||||
// Act - Start the queue and let it process
|
||||
using var cts = new CancellationTokenSource();
|
||||
using StellaOps.TestKit;
|
||||
var task = queue.StartAsync(cts.Token);
|
||||
|
||||
// Wait for processing
|
||||
@@ -147,7 +152,8 @@ public class WriteBehindQueueTests
|
||||
metrics.TotalBatches.Should().BeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WriteBehindMetrics_Timestamp_IsRecent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -205,7 +211,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
HitCount = 0
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetAsync_ThenGetAsync_ReturnsEntry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -239,7 +246,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
result.Source.Should().Be("valkey");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAsync_CacheMissWithDbHit_BackfillsCache()
|
||||
{
|
||||
// Arrange
|
||||
@@ -274,7 +282,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
store.Verify(s => s.SetAsync(It.Is<ProvcacheEntry>(e => e.VeriKey == veriKey), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAsync_FullMiss_ReturnsMissResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -303,7 +312,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
result.Entry.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetOrComputeAsync_CacheHit_DoesNotCallFactory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -335,7 +345,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
result.VeriKey.Should().Be(veriKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetOrComputeAsync_CacheMiss_CallsFactoryAndStores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -374,7 +385,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
store.Verify(s => s.SetAsync(It.Is<ProvcacheEntry>(e => e.VeriKey == veriKey), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task InvalidateAsync_RemovesFromBothStoreLayers()
|
||||
{
|
||||
// Arrange
|
||||
@@ -403,7 +415,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
repository.Verify(r => r.DeleteAsync(veriKey, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAsync_BypassCache_ReturnsbypassedResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -426,7 +439,8 @@ public class ProvcacheServiceStorageIntegrationTests
|
||||
store.Verify(s => s.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetMetricsAsync_ReturnsCurrentMetrics()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Provcache.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -14,7 +15,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
TimeWindowBucket = TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_SameInputs_ProducesSameVeriKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -49,7 +51,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentInputOrder_VexHashes_ProducesSameVeriKey()
|
||||
{
|
||||
// Arrange - VEX hashes in different orders
|
||||
@@ -64,7 +67,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().Be(veriKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentInputOrder_CertificateHashes_ProducesSameVeriKey()
|
||||
{
|
||||
// Arrange - Certificate hashes in different orders
|
||||
@@ -79,7 +83,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().Be(veriKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentSourceHash_ProducesDifferentVeriKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +95,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().NotBe(veriKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentSbomHash_ProducesDifferentVeriKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -101,7 +107,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().NotBe(veriKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_DifferentTimeWindow_ProducesDifferentVeriKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +119,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().NotBe(veriKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_MultipleTimes_ReturnsConsistentResult()
|
||||
{
|
||||
// Arrange & Act - Create multiple builder instances with same inputs
|
||||
@@ -125,7 +133,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
results.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_AcrossMultipleBuilders_ProducesSameResult()
|
||||
{
|
||||
// Act - Create 10 different builder instances
|
||||
@@ -138,7 +147,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
results.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithHashPrefixNormalization_ProducesSameVeriKey()
|
||||
{
|
||||
// Arrange - Same hash with different case prefixes
|
||||
@@ -164,7 +174,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().Be(veriKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithTimeWindow_Timestamp_BucketsDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +193,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
builder1.Build().Should().NotBe(builder3.Build());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildWithComponents_ReturnsSameVeriKeyAsIndividualComponents()
|
||||
{
|
||||
// Arrange & Act - Create two identical builders
|
||||
@@ -195,7 +207,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
components.SbomHash.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_EmptyVexSet_ProducesConsistentHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -215,7 +228,8 @@ public class VeriKeyBuilderDeterminismTests
|
||||
veriKey1.Should().Be(veriKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_MissingComponent_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -4,11 +4,13 @@ using StellaOps.Replay.Core;
|
||||
using StellaOps.Cryptography.Bcl;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Replay.Core.Tests;
|
||||
|
||||
public class ReachabilityReplayWriterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildManifestV2_SortsGraphsAndTraces_Deterministically()
|
||||
{
|
||||
var scan = new ReplayScanMetadata
|
||||
|
||||
@@ -7,11 +7,13 @@ using StellaOps.Replay.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Replay.Tests;
|
||||
|
||||
public class ReplayEngineTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Replay_SameManifest_ProducesIdenticalVerdict()
|
||||
{
|
||||
var manifest = CreateManifest();
|
||||
@@ -23,7 +25,8 @@ public class ReplayEngineTests
|
||||
result1.VerdictDigest.Should().Be(result2.VerdictDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Replay_DifferentManifest_ProducesDifferentVerdict()
|
||||
{
|
||||
var manifest1 = CreateManifest();
|
||||
@@ -39,7 +42,8 @@ public class ReplayEngineTests
|
||||
result1.VerdictDigest.Should().NotBe(result2.VerdictDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CheckDeterminism_IdenticalResults_ReturnsTrue()
|
||||
{
|
||||
var engine = CreateEngine();
|
||||
@@ -51,7 +55,8 @@ public class ReplayEngineTests
|
||||
check.IsDeterministic.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CheckDeterminism_DifferentResults_ReturnsDifferences()
|
||||
{
|
||||
var engine = CreateEngine();
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Frames;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Common.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -12,7 +13,8 @@ public sealed class FrameConverterTests
|
||||
{
|
||||
#region ToFrame (RequestFrame) Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_ReturnsFrameWithRequestType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -25,7 +27,8 @@ public sealed class FrameConverterTests
|
||||
frame.Type.Should().Be(FrameType.Request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_SetsCorrelationIdFromRequest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -38,7 +41,8 @@ public sealed class FrameConverterTests
|
||||
frame.CorrelationId.Should().Be("test-correlation-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_UsesRequestIdWhenCorrelationIdIsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +61,8 @@ public sealed class FrameConverterTests
|
||||
frame.CorrelationId.Should().Be("request-id-456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_SerializesPayload()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,7 +79,8 @@ public sealed class FrameConverterTests
|
||||
|
||||
#region ToRequestFrame Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_ValidRequestFrame_ReturnsRequestFrame()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +94,8 @@ public sealed class FrameConverterTests
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_WrongFrameType_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +113,8 @@ public sealed class FrameConverterTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_InvalidJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +132,8 @@ public sealed class FrameConverterTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesRequestId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +147,8 @@ public sealed class FrameConverterTests
|
||||
result!.RequestId.Should().Be("unique-request-id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesMethod()
|
||||
{
|
||||
// Arrange
|
||||
@@ -152,7 +162,8 @@ public sealed class FrameConverterTests
|
||||
result!.Method.Should().Be("DELETE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -166,7 +177,8 @@ public sealed class FrameConverterTests
|
||||
result!.Path.Should().Be("/api/users/123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesHeaders()
|
||||
{
|
||||
// Arrange
|
||||
@@ -193,7 +205,8 @@ public sealed class FrameConverterTests
|
||||
result.Headers["X-Custom-Header"].Should().Be("custom-value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesPayload()
|
||||
{
|
||||
// Arrange
|
||||
@@ -214,7 +227,8 @@ public sealed class FrameConverterTests
|
||||
result!.Payload.ToArray().Should().BeEquivalentTo(payloadBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesTimeoutSeconds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -234,7 +248,8 @@ public sealed class FrameConverterTests
|
||||
result!.TimeoutSeconds.Should().Be(60);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesSupportsStreaming()
|
||||
{
|
||||
// Arrange
|
||||
@@ -258,7 +273,8 @@ public sealed class FrameConverterTests
|
||||
|
||||
#region ToFrame (ResponseFrame) Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToFrame_ResponseFrame_ReturnsFrameWithResponseType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -271,7 +287,8 @@ public sealed class FrameConverterTests
|
||||
frame.Type.Should().Be(FrameType.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToFrame_ResponseFrame_SetsCorrelationIdToRequestId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -288,7 +305,8 @@ public sealed class FrameConverterTests
|
||||
|
||||
#region ToResponseFrame Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_ValidResponseFrame_ReturnsResponseFrame()
|
||||
{
|
||||
// Arrange
|
||||
@@ -302,7 +320,8 @@ public sealed class FrameConverterTests
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_WrongFrameType_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -320,7 +339,8 @@ public sealed class FrameConverterTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_InvalidJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -338,7 +358,8 @@ public sealed class FrameConverterTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesRequestId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -352,7 +373,8 @@ public sealed class FrameConverterTests
|
||||
result!.RequestId.Should().Be("original-req-id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesStatusCode()
|
||||
{
|
||||
// Arrange
|
||||
@@ -366,7 +388,8 @@ public sealed class FrameConverterTests
|
||||
result!.StatusCode.Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesHeaders()
|
||||
{
|
||||
// Arrange
|
||||
@@ -391,7 +414,8 @@ public sealed class FrameConverterTests
|
||||
result.Headers["Cache-Control"].Should().Be("no-cache");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesPayload()
|
||||
{
|
||||
// Arrange
|
||||
@@ -411,7 +435,8 @@ public sealed class FrameConverterTests
|
||||
result!.Payload.ToArray().Should().BeEquivalentTo(payloadBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesHasMoreChunks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -434,7 +459,8 @@ public sealed class FrameConverterTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_EmptyPayload_ReturnsEmptyPayload()
|
||||
{
|
||||
// Arrange
|
||||
@@ -454,7 +480,8 @@ public sealed class FrameConverterTests
|
||||
result!.Payload.IsEmpty.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_NullHeaders_ReturnsEmptyHeaders()
|
||||
{
|
||||
// Arrange
|
||||
@@ -474,7 +501,8 @@ public sealed class FrameConverterTests
|
||||
result.Headers.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_EmptyPayload_ReturnsEmptyPayload()
|
||||
{
|
||||
// Arrange
|
||||
@@ -493,7 +521,8 @@ public sealed class FrameConverterTests
|
||||
result!.Payload.IsEmpty.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToFrame_LargePayload_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Frames;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Common.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -13,7 +14,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
{
|
||||
#region Request Frame Complete Round-Trip Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_CompleteRoundTrip_AllFieldsPreserved()
|
||||
{
|
||||
// Arrange
|
||||
@@ -51,7 +53,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored.Payload.ToArray().Should().BeEquivalentTo(original.Payload);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("GET")]
|
||||
[InlineData("POST")]
|
||||
[InlineData("PUT")]
|
||||
@@ -72,7 +75,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Method.Should().Be(method);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("/")]
|
||||
[InlineData("/api")]
|
||||
[InlineData("/api/users")]
|
||||
@@ -94,7 +98,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Path.Should().Be(path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_EmptyPayload_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -114,7 +119,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Payload.Length.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_LargePayload_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange - 1MB payload
|
||||
@@ -137,7 +143,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Payload.ToArray().Should().BeEquivalentTo(largePayload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_BinaryPayload_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange - Binary data with all byte values 0-255
|
||||
@@ -159,7 +166,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Payload.ToArray().Should().BeEquivalentTo(binaryPayload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_NoHeaders_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -179,7 +187,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Headers.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_ManyHeaders_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange - 100 headers
|
||||
@@ -202,7 +211,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Headers.Should().BeEquivalentTo(headers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(30)]
|
||||
[InlineData(60)]
|
||||
@@ -231,7 +241,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
|
||||
#region Response Frame Complete Round-Trip Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResponseFrame_CompleteRoundTrip_AllFieldsPreserved()
|
||||
{
|
||||
// Arrange
|
||||
@@ -262,7 +273,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored.Payload.ToArray().Should().BeEquivalentTo(original.Payload);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(200)]
|
||||
[InlineData(201)]
|
||||
[InlineData(204)]
|
||||
@@ -288,7 +300,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.StatusCode.Should().Be(statusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void ResponseFrame_StreamingFlag_RoundTripsCorrectly(bool hasMoreChunks)
|
||||
@@ -313,7 +326,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
|
||||
#region Frame Type Discrimination Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_HasCorrectFrameType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -326,7 +340,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
frame.Type.Should().Be(FrameType.Request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResponseFrame_HasCorrectFrameType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -339,7 +354,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
frame.Type.Should().Be(FrameType.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToRequestFrame_ReturnsNull_ForResponseFrame()
|
||||
{
|
||||
// Arrange
|
||||
@@ -353,7 +369,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToResponseFrame_ReturnsNull_ForRequestFrame()
|
||||
{
|
||||
// Arrange
|
||||
@@ -371,7 +388,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_MultipleRoundTrips_ProduceIdenticalResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -413,7 +431,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResponseFrame_MultipleRoundTrips_ProduceIdenticalResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -453,7 +472,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
|
||||
#region Correlation ID Handling Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_CorrelationIdNull_UsesRequestIdInFrame()
|
||||
{
|
||||
// Arrange
|
||||
@@ -472,7 +492,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
frame.CorrelationId.Should().Be("req-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_CorrelationIdSet_UsesCorrelationIdInFrame()
|
||||
{
|
||||
// Arrange
|
||||
@@ -491,7 +512,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
frame.CorrelationId.Should().Be("corr-456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResponseFrame_UsesRequestIdAsCorrelationId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -512,7 +534,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
|
||||
#region Edge Case Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_SpecialCharactersInHeaders_RoundTripCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -538,7 +561,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.Headers.Should().BeEquivalentTo(original.Headers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_UnicodePayload_RoundTripsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -560,7 +584,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restoredPayload.Should().Be(unicodeJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RequestFrame_EmptyRequestId_RoundTripsCorrectly()
|
||||
{
|
||||
// Note: Empty RequestId is technically invalid but should still round-trip
|
||||
@@ -579,7 +604,8 @@ public sealed class MessageFramingRoundTripTests
|
||||
restored!.RequestId.Should().Be("");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResponseFrame_ZeroStatusCode_RoundTripsCorrectly()
|
||||
{
|
||||
// Note: Zero status code is technically invalid but should still round-trip
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed class PathMatcherTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_SetsTemplate()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -17,7 +18,8 @@ public sealed class PathMatcherTests
|
||||
matcher.Template.Should().Be("/api/users/{id}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_DefaultsCaseInsensitive()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -27,7 +29,8 @@ public sealed class PathMatcherTests
|
||||
matcher.IsMatch("/api/users").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_CaseSensitive_DoesNotMatchDifferentCase()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -42,7 +45,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region IsMatch Tests - Exact Paths
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_ExactPath_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -52,7 +56,8 @@ public sealed class PathMatcherTests
|
||||
matcher.IsMatch("/api/health").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_ExactPath_TrailingSlash_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -62,7 +67,8 @@ public sealed class PathMatcherTests
|
||||
matcher.IsMatch("/api/health/").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_ExactPath_NoLeadingSlash_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -72,7 +78,8 @@ public sealed class PathMatcherTests
|
||||
matcher.IsMatch("api/health").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_DifferentPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +89,8 @@ public sealed class PathMatcherTests
|
||||
matcher.IsMatch("/api/status").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_PartialPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +100,8 @@ public sealed class PathMatcherTests
|
||||
matcher.IsMatch("/api/users").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_LongerPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +115,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region IsMatch Tests - Case Sensitivity
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_CaseInsensitive_MatchesMixedCase()
|
||||
{
|
||||
// Arrange
|
||||
@@ -118,7 +128,8 @@ public sealed class PathMatcherTests
|
||||
matcher.IsMatch("/aPi/uSeRs").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_CaseSensitive_OnlyMatchesExactCase()
|
||||
{
|
||||
// Arrange
|
||||
@@ -134,7 +145,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region TryMatch Tests - Single Parameter
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -147,7 +159,8 @@ public sealed class PathMatcherTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ExtractsParameter()
|
||||
{
|
||||
// Arrange
|
||||
@@ -161,7 +174,8 @@ public sealed class PathMatcherTests
|
||||
parameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ExtractsGuidParameter()
|
||||
{
|
||||
// Arrange
|
||||
@@ -175,7 +189,8 @@ public sealed class PathMatcherTests
|
||||
parameters["userId"].Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ExtractsStringParameter()
|
||||
{
|
||||
// Arrange
|
||||
@@ -192,7 +207,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region TryMatch Tests - Multiple Parameters
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_MultipleParameters_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -205,7 +221,8 @@ public sealed class PathMatcherTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_MultipleParameters_ExtractsAllParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -221,7 +238,8 @@ public sealed class PathMatcherTests
|
||||
parameters["postId"].Should().Be("post-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ThreeParameters_ExtractsAllParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -241,7 +259,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region TryMatch Tests - Non-Matching
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_NonMatchingPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -255,7 +274,8 @@ public sealed class PathMatcherTests
|
||||
parameters.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_MissingParameter_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -268,7 +288,8 @@ public sealed class PathMatcherTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ExtraSegment_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -285,7 +306,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region TryMatch Tests - Path Normalization
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_TrailingSlash_Matches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -299,7 +321,8 @@ public sealed class PathMatcherTests
|
||||
parameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_NoLeadingSlash_Matches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -317,7 +340,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region TryMatch Tests - Parameter Type Constraints
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithTypeConstraint_ExtractsParameterName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -332,7 +356,8 @@ public sealed class PathMatcherTests
|
||||
parameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithGuidConstraint_ExtractsParameterName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -350,7 +375,8 @@ public sealed class PathMatcherTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_RootPath_Matches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -364,7 +390,8 @@ public sealed class PathMatcherTests
|
||||
parameters.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_SingleSegmentWithParameter_Matches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -378,7 +405,8 @@ public sealed class PathMatcherTests
|
||||
parameters["id"].Should().Be("test-value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_EmptyPath_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -391,7 +419,8 @@ public sealed class PathMatcherTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithHyphen_Extracts()
|
||||
{
|
||||
// Arrange
|
||||
@@ -405,7 +434,8 @@ public sealed class PathMatcherTests
|
||||
parameters["user-id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithUnderscore_Extracts()
|
||||
{
|
||||
// Arrange
|
||||
@@ -418,7 +448,8 @@ public sealed class PathMatcherTests
|
||||
parameters.Should().ContainKey("user_id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_SpecialCharactersInPath_Matches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -431,7 +462,8 @@ public sealed class PathMatcherTests
|
||||
parameters["query"].Should().Be("hello-world_test.123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsMatch_ComplexRealWorldPath_Matches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -444,7 +476,8 @@ public sealed class PathMatcherTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryMatch_ComplexRealWorldPath_ExtractsAllParameters()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -2,6 +2,7 @@ using StellaOps.Router.Common.Abstractions;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Common.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -11,7 +12,8 @@ public sealed class RoutingDeterminismTests
|
||||
{
|
||||
#region Core Determinism Property Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SameContextAndConnections_AlwaysSelectsSameRoute()
|
||||
{
|
||||
// Arrange
|
||||
@@ -32,7 +34,8 @@ public sealed class RoutingDeterminismTests
|
||||
results.Should().AllBeEquivalentTo(results[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DifferentConnectionOrder_ProducesSameResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +60,8 @@ public sealed class RoutingDeterminismTests
|
||||
result1.ConnectionId.Should().Be(result2.ConnectionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SamePathAndMethod_WithSameHeaders_ProducesSameRouteKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -97,7 +101,8 @@ public sealed class RoutingDeterminismTests
|
||||
|
||||
#region Region Affinity Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SameRegion_AlwaysPreferredWhenAvailable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -117,7 +122,8 @@ public sealed class RoutingDeterminismTests
|
||||
results.Should().AllSatisfy(r => r.Instance.Region.Should().Be("us-east"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NoLocalRegion_FallbackIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -142,7 +148,8 @@ public sealed class RoutingDeterminismTests
|
||||
|
||||
#region Version Selection Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SameRequestedVersion_AlwaysSelectsMatchingConnection()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +170,8 @@ public sealed class RoutingDeterminismTests
|
||||
results.Should().AllSatisfy(r => r.Instance.Version.Should().Be("2.0.0"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NoVersionRequested_LatestStableIsSelectedDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
@@ -188,7 +196,8 @@ public sealed class RoutingDeterminismTests
|
||||
|
||||
#region Health Status Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HealthyConnectionsPreferred_Deterministically()
|
||||
{
|
||||
// Arrange
|
||||
@@ -209,7 +218,8 @@ public sealed class RoutingDeterminismTests
|
||||
results.Should().AllSatisfy(r => r.ConnectionId.Should().Be("conn-healthy"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DegradedConnectionSelected_WhenNoHealthyAvailable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -231,7 +241,8 @@ public sealed class RoutingDeterminismTests
|
||||
results[0].Status.Should().Be(InstanceHealthStatus.Degraded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DrainingConnectionsExcluded()
|
||||
{
|
||||
// Arrange
|
||||
@@ -253,7 +264,8 @@ public sealed class RoutingDeterminismTests
|
||||
|
||||
#region Multi-Criteria Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegionThenVersionThenHealth_OrderingIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -288,7 +300,8 @@ public sealed class RoutingDeterminismTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TieBreaker_UsesConnectionIdForConsistency()
|
||||
{
|
||||
// Arrange - Two identical connections except ID
|
||||
@@ -312,7 +325,8 @@ public sealed class RoutingDeterminismTests
|
||||
|
||||
#region Endpoint Matching Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathParameterMatching_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -335,7 +349,8 @@ public sealed class RoutingDeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MultipleEndpoints_SamePath_SelectsFirstMatchDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Common.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -11,7 +12,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
{
|
||||
#region Path Template Matching Rules
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathMatcher_ExactPath_MatchesOnly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -25,7 +27,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
matcher.IsMatch("/api").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathMatcher_SingleParameter_CapturesValue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -40,7 +43,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
parameters["id"].Should().Be("12345");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathMatcher_MultipleParameters_CapturesAllValues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +61,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
parameters["memberId"].Should().Be("member-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathMatcher_SegmentMismatch_DoesNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -69,7 +74,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
matcher.IsMatch("/api/users/123").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("/api/users/123", true)]
|
||||
[InlineData("/api/users/abc-def-ghi", true)]
|
||||
[InlineData("/api/users/user@example.com", false)] // Contains @ which may be problematic
|
||||
@@ -91,7 +97,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
|
||||
#region Endpoint Selection Rules
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointSelection_MatchesByMethodAndPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -110,7 +117,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
selector.FindEndpoint("PUT", "/api/users").Should().BeNull(); // No PUT endpoint
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointSelection_MoreSpecificPathWins()
|
||||
{
|
||||
// Arrange - Specific path should win over parameterized path
|
||||
@@ -129,7 +137,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
idEndpoint!.ServiceName.Should().Be("user-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointSelection_DifferentMethodsSamePath_SelectsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,7 +159,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
|
||||
#region Version Matching Rules
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VersionMatching_ExactMatch_Required()
|
||||
{
|
||||
// Arrange
|
||||
@@ -169,7 +179,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result[0].Instance.Version.Should().Be("2.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VersionMatching_NoVersionRequested_AllVersionsEligible()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +197,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VersionMatching_NoMatchingVersion_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -207,7 +219,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
|
||||
#region Health Status Rules
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HealthFilter_OnlyHealthy_WhenAvailable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -226,7 +239,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result[0].ConnectionId.Should().Be("conn-healthy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HealthFilter_DegradedFallback_WhenNoHealthy()
|
||||
{
|
||||
// Arrange
|
||||
@@ -245,7 +259,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result.All(c => c.Status == InstanceHealthStatus.Degraded).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HealthFilter_NoDegradedAllowed_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -262,7 +277,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HealthFilter_DrainingAlwaysExcluded()
|
||||
{
|
||||
// Arrange
|
||||
@@ -284,7 +300,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
|
||||
#region Region Affinity Rules
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegionFilter_LocalRegionFirst()
|
||||
{
|
||||
// Arrange
|
||||
@@ -303,7 +320,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result.Connections[0].Instance.Region.Should().Be("eu-west");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegionFilter_NeighborTierSecond()
|
||||
{
|
||||
// Arrange
|
||||
@@ -322,7 +340,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result.Connections[0].Instance.Region.Should().Be("eu-central");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegionFilter_GlobalTierLast()
|
||||
{
|
||||
// Arrange
|
||||
@@ -344,7 +363,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
|
||||
#region Latency-Based Rules
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LatencySort_LowestPingFirst()
|
||||
{
|
||||
// Arrange
|
||||
@@ -364,7 +384,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result[2].ConnectionId.Should().Be("conn-high");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LatencySort_TiedPing_UsesHeartbeatRecency()
|
||||
{
|
||||
// Arrange
|
||||
@@ -389,7 +410,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
|
||||
#region Rule Combination Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RuleChain_AppliesInOrder()
|
||||
{
|
||||
// Arrange - Multiple healthy connections, different regions, different pings
|
||||
@@ -413,7 +435,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
result.ConnectionId.Should().Be("local-healthy-slow");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RuleChain_FallsBackWhenNoIdealCandidate()
|
||||
{
|
||||
// Arrange - No local healthy connections
|
||||
@@ -440,7 +463,8 @@ public sealed class RoutingRulesEvaluationTests
|
||||
|
||||
#region Determinism Verification
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RuleEvaluation_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace StellaOps.Router.Config.Tests;
|
||||
/// </summary>
|
||||
public sealed class ConfigChangedEventArgsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_SetsPreviousConfig()
|
||||
{
|
||||
// Arrange
|
||||
@@ -19,7 +20,8 @@ public sealed class ConfigChangedEventArgsTests
|
||||
args.Previous.Should().BeSameAs(previous);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_SetsCurrentConfig()
|
||||
{
|
||||
// Arrange
|
||||
@@ -33,7 +35,8 @@ public sealed class ConfigChangedEventArgsTests
|
||||
args.Current.Should().BeSameAs(current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_SetsChangedAtToCurrentTime()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public sealed class ConfigChangedEventArgsTests
|
||||
args.ChangedAt.Should().BeOnOrBefore(afterCreate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_DifferentConfigs_BothAccessible()
|
||||
{
|
||||
// Arrange
|
||||
@@ -71,7 +75,8 @@ public sealed class ConfigChangedEventArgsTests
|
||||
args.Current.Routing.LocalRegion.Should().Be("us-east-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConfigChangedEventArgs_InheritsFromEventArgs()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed class ConfigValidationResultTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Errors_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -18,7 +19,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Warnings_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -33,7 +35,8 @@ public sealed class ConfigValidationResultTests
|
||||
|
||||
#region IsValid Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsValid_NoErrors_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -43,7 +46,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsValid_WithErrors_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -54,7 +58,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsValid_WithOnlyWarnings_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -65,7 +70,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsValid_WithErrorsAndWarnings_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +83,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IsValid_MultipleErrors_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -95,7 +102,8 @@ public sealed class ConfigValidationResultTests
|
||||
|
||||
#region Static Success Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Success_ReturnsValidResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -107,7 +115,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.Warnings.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Success_ReturnsNewInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -122,7 +131,8 @@ public sealed class ConfigValidationResultTests
|
||||
|
||||
#region Errors Collection Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Errors_CanBeModified()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +148,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.Errors.Should().Contain("Error 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Errors_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -156,7 +167,8 @@ public sealed class ConfigValidationResultTests
|
||||
|
||||
#region Warnings Collection Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Warnings_CanBeModified()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +184,8 @@ public sealed class ConfigValidationResultTests
|
||||
result.Warnings.Should().Contain("Warning 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Warnings_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed class RouterConfigOptionsTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ConfigPath_DefaultsToNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -17,7 +18,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.ConfigPath.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_EnvironmentVariablePrefix_DefaultsToStellaOpsRouter()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -27,7 +29,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.EnvironmentVariablePrefix.Should().Be("STELLAOPS_ROUTER_");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_EnableHotReload_DefaultsToTrue()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -37,7 +40,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.EnableHotReload.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_DebounceInterval_DefaultsTo500Milliseconds()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -47,7 +51,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.DebounceInterval.Should().Be(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ThrowOnValidationError_DefaultsToFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -57,7 +62,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.ThrowOnValidationError.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ConfigurationSection_DefaultsToRouter()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -71,7 +77,8 @@ public sealed class RouterConfigOptionsTests
|
||||
|
||||
#region Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConfigPath_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +91,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.ConfigPath.Should().Be("/etc/stellaops/router.yaml");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnvironmentVariablePrefix_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -97,7 +105,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.EnvironmentVariablePrefix.Should().Be("CUSTOM_PREFIX_");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnableHotReload_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -110,7 +119,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.EnableHotReload.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DebounceInterval_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -123,7 +133,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.DebounceInterval.Should().Be(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ThrowOnValidationError_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +147,8 @@ public sealed class RouterConfigOptionsTests
|
||||
options.ThrowOnValidationError.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConfigurationSection_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -32,7 +33,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_InitializesCurrentConfig()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -42,7 +44,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
provider.Current.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ExposesOptions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -60,7 +63,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
provider.Options.ConfigPath.Should().Be("/test/path.yaml");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_WithHotReloadDisabled_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +81,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region Validate Tests - PayloadLimits
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ValidConfig_ReturnsIsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +95,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ZeroMaxRequestBytesPerCall_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -105,7 +111,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerCall"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NegativeMaxRequestBytesPerCall_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -120,7 +127,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerCall"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ZeroMaxRequestBytesPerConnection_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -135,7 +143,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerConnection"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ZeroMaxAggregateInflightBytes_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,7 +159,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxAggregateInflightBytes"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_MaxCallBytesLargerThanConnectionBytes_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
@@ -174,7 +184,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region Validate Tests - RoutingOptions
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ZeroDefaultTimeout_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -189,7 +200,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("DefaultTimeout"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NegativeDefaultTimeout_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -208,7 +220,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region Validate Tests - Services
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_EmptyServiceName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -223,7 +236,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("Service name cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WhitespaceServiceName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -238,7 +252,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("Service name cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_DuplicateServiceNames_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -254,7 +269,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("Duplicate service name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_DuplicateServiceNamesCaseInsensitive_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -270,7 +286,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("Duplicate service name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_EndpointEmptyMethod_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -289,7 +306,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("endpoint method cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_EndpointEmptyPath_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -308,7 +326,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("endpoint path cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_EndpointNonPositiveTimeout_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
@@ -331,7 +350,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region Validate Tests - StaticInstances
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_StaticInstanceEmptyServiceName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -352,7 +372,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("Static instance service name cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_StaticInstanceEmptyHost_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -373,7 +394,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("host cannot be empty"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(65536)]
|
||||
@@ -398,7 +420,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.Errors.Should().Contain(e => e.Contains("port must be between 1 and 65535"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(80)]
|
||||
[InlineData(443)]
|
||||
@@ -423,7 +446,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(-100)]
|
||||
@@ -452,7 +476,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region ReloadAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReloadAsync_ValidConfig_UpdatesCurrentConfig()
|
||||
{
|
||||
// Arrange
|
||||
@@ -465,7 +490,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
provider.Current.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReloadAsync_InvalidConfig_ThrowsConfigurationException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -483,7 +509,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
provider.Current.PayloadLimits.MaxRequestBytesPerCall.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReloadAsync_Cancelled_ThrowsOperationCanceledException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -499,7 +526,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region ConfigurationChanged Event Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReloadAsync_RaisesConfigurationChangedEvent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -521,7 +549,8 @@ public sealed class RouterConfigProviderTests : IDisposable
|
||||
|
||||
#region Dispose Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,7 +10,8 @@ public sealed class RouterConfigTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_PayloadLimits_DefaultsToNewInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -19,7 +21,8 @@ public sealed class RouterConfigTests
|
||||
config.PayloadLimits.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Routing_DefaultsToNewInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -29,7 +32,8 @@ public sealed class RouterConfigTests
|
||||
config.Routing.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Services_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -40,7 +44,8 @@ public sealed class RouterConfigTests
|
||||
config.Services.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_StaticInstances_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -55,7 +60,8 @@ public sealed class RouterConfigTests
|
||||
|
||||
#region PayloadLimits Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PayloadLimits_HasDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -67,7 +73,8 @@ public sealed class RouterConfigTests
|
||||
config.PayloadLimits.MaxAggregateInflightBytes.Should().Be(1024 * 1024 * 1024); // 1 GB
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PayloadLimits_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -91,7 +98,8 @@ public sealed class RouterConfigTests
|
||||
|
||||
#region Routing Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Routing_HasDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -104,7 +112,8 @@ public sealed class RouterConfigTests
|
||||
config.Routing.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Routing_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -132,7 +141,8 @@ public sealed class RouterConfigTests
|
||||
|
||||
#region Services Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Services_CanAddServices()
|
||||
{
|
||||
// Arrange
|
||||
@@ -148,7 +158,8 @@ public sealed class RouterConfigTests
|
||||
config.Services[1].ServiceName.Should().Be("service-b");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Services_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -170,7 +181,8 @@ public sealed class RouterConfigTests
|
||||
|
||||
#region StaticInstances Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void StaticInstances_CanAddInstances()
|
||||
{
|
||||
// Arrange
|
||||
@@ -190,7 +202,8 @@ public sealed class RouterConfigTests
|
||||
config.StaticInstances[0].ServiceName.Should().Be("legacy-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void StaticInstances_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -223,7 +236,8 @@ public sealed class RouterConfigTests
|
||||
|
||||
#region Complete Configuration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompleteConfiguration_Works()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed class RoutingOptionsTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_LocalRegion_DefaultsToDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -17,7 +18,8 @@ public sealed class RoutingOptionsTests
|
||||
options.LocalRegion.Should().Be("default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_NeighborRegions_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -28,7 +30,8 @@ public sealed class RoutingOptionsTests
|
||||
options.NeighborRegions.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_TieBreaker_DefaultsToRoundRobin()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -38,7 +41,8 @@ public sealed class RoutingOptionsTests
|
||||
options.TieBreaker.Should().Be(TieBreakerStrategy.RoundRobin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_PreferLocalRegion_DefaultsToTrue()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -48,7 +52,8 @@ public sealed class RoutingOptionsTests
|
||||
options.PreferLocalRegion.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_DefaultTimeout_DefaultsTo30Seconds()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -62,7 +67,8 @@ public sealed class RoutingOptionsTests
|
||||
|
||||
#region Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LocalRegion_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +81,8 @@ public sealed class RoutingOptionsTests
|
||||
options.LocalRegion.Should().Be("us-east-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NeighborRegions_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +97,8 @@ public sealed class RoutingOptionsTests
|
||||
options.NeighborRegions.Should().Contain("eu-west-1");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(TieBreakerStrategy.RoundRobin)]
|
||||
[InlineData(TieBreakerStrategy.Random)]
|
||||
[InlineData(TieBreakerStrategy.LeastLoaded)]
|
||||
@@ -107,7 +115,8 @@ public sealed class RoutingOptionsTests
|
||||
options.TieBreaker.Should().Be(strategy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PreferLocalRegion_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -120,7 +129,8 @@ public sealed class RoutingOptionsTests
|
||||
options.PreferLocalRegion.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DefaultTimeout_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -137,7 +147,8 @@ public sealed class RoutingOptionsTests
|
||||
|
||||
#region TieBreakerStrategy Enum Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_HasFourValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -147,28 +158,32 @@ public sealed class RoutingOptionsTests
|
||||
values.Should().HaveCount(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_RoundRobin_HasValueZero()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
((int)TieBreakerStrategy.RoundRobin).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_Random_HasValueOne()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
((int)TieBreakerStrategy.Random).Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_LeastLoaded_HasValueTwo()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
((int)TieBreakerStrategy.LeastLoaded).Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_ConsistentHash_HasValueThree()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,7 +11,8 @@ public sealed class ServiceConfigTests
|
||||
{
|
||||
#region ServiceConfig Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceConfig_DefaultVersion_DefaultsToNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -20,7 +22,8 @@ public sealed class ServiceConfigTests
|
||||
config.DefaultVersion.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceConfig_DefaultTransport_DefaultsToTcp()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -30,7 +33,8 @@ public sealed class ServiceConfigTests
|
||||
config.DefaultTransport.Should().Be(TransportType.Tcp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceConfig_Endpoints_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -45,7 +49,8 @@ public sealed class ServiceConfigTests
|
||||
|
||||
#region ServiceConfig Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceConfig_ServiceName_CanBeSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -55,7 +60,8 @@ public sealed class ServiceConfigTests
|
||||
config.ServiceName.Should().Be("my-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceConfig_DefaultVersion_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +74,8 @@ public sealed class ServiceConfigTests
|
||||
config.DefaultVersion.Should().Be("1.0.0");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(TransportType.Tcp)]
|
||||
[InlineData(TransportType.Certificate)]
|
||||
[InlineData(TransportType.Udp)]
|
||||
@@ -86,7 +93,8 @@ public sealed class ServiceConfigTests
|
||||
config.DefaultTransport.Should().Be(transport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceConfig_Endpoints_CanAddEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -104,7 +112,8 @@ public sealed class ServiceConfigTests
|
||||
|
||||
#region EndpointConfig Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_DefaultTimeout_DefaultsToNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -114,7 +123,8 @@ public sealed class ServiceConfigTests
|
||||
endpoint.DefaultTimeout.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_SupportsStreaming_DefaultsToFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -124,7 +134,8 @@ public sealed class ServiceConfigTests
|
||||
endpoint.SupportsStreaming.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_RequiringClaims_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -139,7 +150,8 @@ public sealed class ServiceConfigTests
|
||||
|
||||
#region EndpointConfig Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_Method_CanBeSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -149,7 +161,8 @@ public sealed class ServiceConfigTests
|
||||
endpoint.Method.Should().Be("DELETE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_Path_CanBeSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -159,7 +172,8 @@ public sealed class ServiceConfigTests
|
||||
endpoint.Path.Should().Be("/api/users/{id}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_DefaultTimeout_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +186,8 @@ public sealed class ServiceConfigTests
|
||||
endpoint.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_SupportsStreaming_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -185,7 +200,8 @@ public sealed class ServiceConfigTests
|
||||
endpoint.SupportsStreaming.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointConfig_RequiringClaims_CanAddClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -203,7 +219,8 @@ public sealed class ServiceConfigTests
|
||||
|
||||
#region Complex Configuration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceConfig_CompleteConfiguration_Works()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,7 +10,8 @@ public sealed class StaticInstanceConfigTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Region_DefaultsToDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -25,7 +27,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Region.Should().Be("default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Transport_DefaultsToTcp()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -41,7 +44,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Transport.Should().Be(TransportType.Tcp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Weight_DefaultsTo100()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -57,7 +61,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Weight.Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Metadata_DefaultsToEmptyDictionary()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -78,7 +83,8 @@ public sealed class StaticInstanceConfigTests
|
||||
|
||||
#region Required Properties Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceName_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -94,7 +100,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.ServiceName.Should().Be("required-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Version_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -110,7 +117,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Version.Should().Be("2.3.4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Host_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -126,7 +134,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Host.Should().Be("192.168.1.100");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Port_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -146,7 +155,8 @@ public sealed class StaticInstanceConfigTests
|
||||
|
||||
#region Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Region_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -165,7 +175,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Region.Should().Be("us-west-2");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(TransportType.Tcp)]
|
||||
[InlineData(TransportType.Certificate)]
|
||||
[InlineData(TransportType.Udp)]
|
||||
@@ -189,7 +200,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Transport.Should().Be(transport);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(50)]
|
||||
[InlineData(100)]
|
||||
@@ -213,7 +225,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Weight.Should().Be(weight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Metadata_CanAddEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -239,7 +252,8 @@ public sealed class StaticInstanceConfigTests
|
||||
|
||||
#region Complex Configuration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompleteConfiguration_Works()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -271,7 +285,8 @@ public sealed class StaticInstanceConfigTests
|
||||
config.Metadata.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MultipleInstances_CanHaveDifferentWeights()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Integration.Tests.Fixtures;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,7 +19,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
|
||||
#region Initialization Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_IsInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -28,7 +30,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
connectionManager.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_HasConnections()
|
||||
{
|
||||
// Arrange
|
||||
@@ -41,7 +44,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
connections.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_ConnectionHasCorrectServiceInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -58,7 +62,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
connection.Instance.InstanceId.Should().Be("test-instance-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_ConnectionHasEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +80,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
|
||||
#region Status Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_DefaultStatus_IsHealthy()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +94,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
status.Should().Be(InstanceHealthStatus.Healthy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_CanChangeStatus()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +113,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
newStatus.Should().Be(InstanceHealthStatus.Degraded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(InstanceHealthStatus.Healthy)]
|
||||
[InlineData(InstanceHealthStatus.Degraded)]
|
||||
[InlineData(InstanceHealthStatus.Draining)]
|
||||
@@ -132,7 +140,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
|
||||
#region Metrics Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_InFlightRequestCount_InitiallyZero()
|
||||
{
|
||||
// Arrange
|
||||
@@ -145,7 +154,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
count.Should().BeGreaterOrEqualTo(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_ErrorRate_InitiallyZero()
|
||||
{
|
||||
// Arrange
|
||||
@@ -159,7 +169,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
errorRate.Should().BeLessThanOrEqualTo(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_CanSetInFlightRequestCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -177,7 +188,8 @@ public sealed class ConnectionManagerIntegrationTests
|
||||
newCount.Should().Be(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_CanSetErrorRate()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,6 +6,7 @@ using StellaOps.Router.Common.Frames;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Integration.Tests.Fixtures;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -24,7 +25,8 @@ public sealed class EndToEndRoutingTests
|
||||
|
||||
#region Basic Request/Response Flow
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Route_EchoEndpoint_IsRegistered()
|
||||
{
|
||||
// Arrange & Act - Verify endpoint is registered for routing
|
||||
@@ -35,7 +37,8 @@ public sealed class EndToEndRoutingTests
|
||||
endpoints.Should().Contain(e => e.Path == "/echo" && e.Method == "POST");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Route_GetUserEndpoint_MatchesPathPattern()
|
||||
{
|
||||
// Act
|
||||
@@ -50,7 +53,8 @@ public sealed class EndToEndRoutingTests
|
||||
getUserEndpoint!.Path.Should().Be("/users/{userId}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Route_CreateUserEndpoint_PreservesCorrelationId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +86,8 @@ public sealed class EndToEndRoutingTests
|
||||
|
||||
#region Endpoint Registration Verification
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointRegistry_ContainsAllTestEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -109,7 +114,8 @@ public sealed class EndToEndRoutingTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointRegistry_EachEndpointHasUniqueMethodPath()
|
||||
{
|
||||
// Act
|
||||
@@ -124,7 +130,8 @@ public sealed class EndToEndRoutingTests
|
||||
|
||||
#region Connection Manager State
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_HasActiveConnections()
|
||||
{
|
||||
// Act
|
||||
@@ -134,7 +141,8 @@ public sealed class EndToEndRoutingTests
|
||||
connections.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConnectionManager_ConnectionsHaveInstanceInfo()
|
||||
{
|
||||
// Act
|
||||
@@ -153,7 +161,8 @@ public sealed class EndToEndRoutingTests
|
||||
|
||||
#region Frame Protocol Integration
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Frame_RequestSerializationRoundTrip_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -185,7 +194,8 @@ public sealed class EndToEndRoutingTests
|
||||
restored.Payload.ToArray().Should().BeEquivalentTo(original.Payload.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Frame_ResponseSerializationRoundTrip_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -217,7 +227,8 @@ public sealed class EndToEndRoutingTests
|
||||
|
||||
#region Path Matching Integration
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("GET", "/users/123", true)]
|
||||
[InlineData("GET", "/users/abc-def", true)]
|
||||
[InlineData("GET", "/users/", false)]
|
||||
@@ -237,7 +248,8 @@ public sealed class EndToEndRoutingTests
|
||||
isMatch.Should().Be(shouldMatch);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("/echo", "/echo", true)]
|
||||
[InlineData("/echo", "/Echo", true)] // PathMatcher is case-insensitive
|
||||
[InlineData("/users", "/users", true)]
|
||||
@@ -259,7 +271,8 @@ public sealed class EndToEndRoutingTests
|
||||
|
||||
#region Routing Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Routing_SameRequest_AlwaysSameEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
@@ -283,7 +296,8 @@ public sealed class EndToEndRoutingTests
|
||||
results.Should().OnlyContain(r => r == "POST:/echo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Routing_MultipleEndpoints_DeterministicOrdering()
|
||||
{
|
||||
// Act - Get endpoints multiple times
|
||||
@@ -300,7 +314,8 @@ public sealed class EndToEndRoutingTests
|
||||
|
||||
#region Error Routing
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EndpointRegistry_ContainsFailEndpoint()
|
||||
{
|
||||
// Act
|
||||
@@ -310,7 +325,8 @@ public sealed class EndToEndRoutingTests
|
||||
endpoints.Should().Contain(e => e.Path == "/fail" && e.Method == "POST");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Routing_UnknownPath_NoMatchingEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Integration.Tests.Fixtures;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,7 +19,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
|
||||
#region Endpoint Discovery Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Registry_ContainsAllTestEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -31,7 +33,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
endpoints.Should().HaveCount(17);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("POST", "/echo")]
|
||||
[InlineData("GET", "/users/123")]
|
||||
[InlineData("POST", "/users")]
|
||||
@@ -54,7 +57,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
match!.Endpoint.Method.Should().Be(method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Registry_ReturnsNull_ForUnknownEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +72,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
match.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Registry_MatchesPathParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +87,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
match!.Endpoint.Path.Should().Be("/users/{userId}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Registry_ExtractsPathParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -101,7 +107,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
|
||||
#region Endpoint Metadata Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Endpoint_HasCorrectTimeout()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +123,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
slowMatch!.Endpoint.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Endpoint_HasCorrectStreamingFlag()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +139,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
echoMatch!.Endpoint.SupportsStreaming.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Endpoint_HasCorrectClaims()
|
||||
{
|
||||
// Arrange
|
||||
@@ -148,7 +157,8 @@ public sealed class EndpointRegistryIntegrationTests
|
||||
echoMatch!.Endpoint.RequiringClaims.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Endpoint_HasCorrectHandlerType()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -36,7 +36,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#region FIFO Ordering Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_SingleProducer_SingleConsumer_FIFO()
|
||||
{
|
||||
// Arrange
|
||||
@@ -63,7 +64,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
receivedOrder.Should().BeEquivalentTo(sentOrder, options => options.WithStrictOrdering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_SingleProducer_DelayedConsumer_FIFO()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +94,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
receivedOrder.Should().BeEquivalentTo(sentOrder, options => options.WithStrictOrdering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_ConcurrentProducerConsumer_FIFO()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +134,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#region Bidirectional Ordering
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_BothDirections_IndependentFIFO()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +186,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#region Ordering Under Backpressure
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_WithBackpressure_FIFO()
|
||||
{
|
||||
// Arrange - Small buffer to force backpressure
|
||||
@@ -223,7 +228,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#region Frame Type Ordering
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_MixedFrameTypes_FIFO()
|
||||
{
|
||||
// Arrange
|
||||
@@ -262,7 +268,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#region Correlation ID Ordering
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_CorrelationIds_Preserved()
|
||||
{
|
||||
// Arrange
|
||||
@@ -297,7 +304,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#region Large Message Ordering
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_VariablePayloadSizes_FIFO()
|
||||
{
|
||||
// Arrange
|
||||
@@ -334,7 +342,8 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
|
||||
#region Ordering Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Ordering_MultipleRuns_Deterministic()
|
||||
{
|
||||
// Run the same sequence multiple times and verify deterministic ordering
|
||||
@@ -343,6 +352,7 @@ public sealed class MessageOrderingTests : IAsyncLifetime, IDisposable
|
||||
for (int run = 0; run < 3; run++)
|
||||
{
|
||||
using var channel = new InMemoryChannel($"determinism-{run}", bufferSize: 100);
|
||||
using StellaOps.TestKit;
|
||||
var received = new List<int>();
|
||||
|
||||
// Same sequence each run
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Router.Integration.Tests.Fixtures;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -26,7 +27,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region FromQuery - Query Parameter Binding
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_StringParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -41,7 +43,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Query.Should().Be("test-search-term");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_IntParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -58,7 +61,8 @@ public sealed class ParameterBindingTests
|
||||
result.PageSize.Should().Be(25);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_BoolParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -73,7 +77,8 @@ public sealed class ParameterBindingTests
|
||||
result!.IncludeDeleted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_MultipleParameters_BindCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -94,7 +99,8 @@ public sealed class ParameterBindingTests
|
||||
result.IncludeDeleted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_UrlEncodedValues_BindCorrectly()
|
||||
{
|
||||
// Arrange - Query with special characters
|
||||
@@ -112,7 +118,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Query.Should().Be(query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_OptionalParameters_UseDefaults()
|
||||
{
|
||||
// Arrange & Act - No query parameters provided
|
||||
@@ -129,7 +136,8 @@ public sealed class ParameterBindingTests
|
||||
result.SortOrder.Should().Be("asc"); // Default
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_OverrideDefaults_BindCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -150,7 +158,8 @@ public sealed class ParameterBindingTests
|
||||
result.SortOrder.Should().Be("desc");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromQuery_WithAnonymousObject_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act - Using anonymous object for multiple query params
|
||||
@@ -171,7 +180,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region FromRoute - Path Parameter Binding
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_SinglePathParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -185,7 +195,8 @@ public sealed class ParameterBindingTests
|
||||
result!.UserId.Should().Be("user-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_MultiplePathParameters_BindCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -201,7 +212,8 @@ public sealed class ParameterBindingTests
|
||||
result.Name.Should().Be("Item-widget-456-in-electronics");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_NumericPathParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -215,7 +227,8 @@ public sealed class ParameterBindingTests
|
||||
result!.UserId.Should().Be("12345");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_GuidPathParameter_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -232,7 +245,8 @@ public sealed class ParameterBindingTests
|
||||
result!.UserId.Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromRoute_SpecialCharactersInPath_BindsCorrectly()
|
||||
{
|
||||
// Arrange - URL-encoded special characters
|
||||
@@ -255,7 +269,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region FromHeader - Header Binding
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromHeader_AuthorizationHeader_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -270,7 +285,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Authorization.Should().Be("Bearer test-token-12345");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromHeader_CustomHeaders_BindCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -289,7 +305,8 @@ public sealed class ParameterBindingTests
|
||||
result!.AcceptLanguage.Should().Be("en-US");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromHeader_MultipleHeaders_AllAccessible()
|
||||
{
|
||||
// Arrange
|
||||
@@ -319,7 +336,8 @@ public sealed class ParameterBindingTests
|
||||
result.AllHeaders.Should().ContainKey("Accept-Language");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromHeader_BearerToken_ParsesCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -338,7 +356,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region FromBody - JSON Body Binding
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_SimpleJson_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -353,7 +372,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Echo.Should().Contain("Hello, World!");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_ComplexObject_BindsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -372,7 +392,8 @@ public sealed class ParameterBindingTests
|
||||
result.UserId.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_AnonymousObject_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -387,7 +408,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Echo.Should().Contain("Anonymous type test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_NestedObject_BindsCorrectly()
|
||||
{
|
||||
// Arrange - For raw echo we can test nested JSON structure
|
||||
@@ -414,7 +436,8 @@ public sealed class ParameterBindingTests
|
||||
body.Should().Contain("deeply nested");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromBody_CamelCaseNaming_BindsCorrectly()
|
||||
{
|
||||
// Arrange - Ensure camelCase property naming works
|
||||
@@ -435,7 +458,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region FromForm - Form Data Binding
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_SimpleFormData_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -452,7 +476,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Password.Should().Be("secret123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_BooleanField_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -469,7 +494,8 @@ public sealed class ParameterBindingTests
|
||||
result!.RememberMe.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_WithAnonymousObject_BindsCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -486,7 +512,8 @@ public sealed class ParameterBindingTests
|
||||
result!.RememberMe.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_UrlEncodedSpecialChars_BindsCorrectly()
|
||||
{
|
||||
// Arrange - Special characters that need URL encoding
|
||||
@@ -505,7 +532,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Password.Should().Be(password);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FromForm_ContentType_IsCorrect()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -525,7 +553,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region Combined Binding - Multiple Sources
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CombinedBinding_PathAndBody_BindCorrectly()
|
||||
{
|
||||
// Arrange - PUT /resources/{resourceId} with JSON body
|
||||
@@ -545,7 +574,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Description.Should().Be("New description");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CombinedBinding_PathQueryAndBody_BindCorrectly()
|
||||
{
|
||||
// Arrange - PUT /resources/{resourceId}?format=json&verbose=true with body
|
||||
@@ -568,7 +598,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Name.Should().Be("Full Update");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CombinedBinding_HeadersAndBody_BindCorrectly()
|
||||
{
|
||||
// Arrange - POST with headers and JSON body
|
||||
@@ -589,7 +620,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region HTTP Methods
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpGet_ReturnsData()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -603,7 +635,8 @@ public sealed class ParameterBindingTests
|
||||
result!.UserId.Should().Be("get-test-user");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPost_CreatesResource()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -618,7 +651,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Success.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPut_UpdatesResource()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -634,7 +668,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Name.Should().Be("Updated Name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPatch_PartialUpdate()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -653,7 +688,8 @@ public sealed class ParameterBindingTests
|
||||
result!.UpdatedFields.Should().Contain("price");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpPatch_PartialUpdate_OnlySpecifiedFields()
|
||||
{
|
||||
// Arrange & Act - Only update name, not price
|
||||
@@ -669,7 +705,8 @@ public sealed class ParameterBindingTests
|
||||
result!.UpdatedFields.Should().NotContain("price");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HttpDelete_RemovesResource()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -688,7 +725,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region Raw Body Handling
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RawBody_PlainText_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -705,7 +743,8 @@ public sealed class ParameterBindingTests
|
||||
body.Should().Be(text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RawBody_Xml_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -722,7 +761,8 @@ public sealed class ParameterBindingTests
|
||||
body.Should().Be(xml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RawBody_Binary_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -740,7 +780,8 @@ public sealed class ParameterBindingTests
|
||||
response.Payload.Length.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RawBody_ResponseHeaders_IncludeContentLength()
|
||||
{
|
||||
// Arrange
|
||||
@@ -761,7 +802,8 @@ public sealed class ParameterBindingTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EmptyBody_HandledCorrectly()
|
||||
{
|
||||
// Arrange & Act - GET with no body should work for endpoints with optional params
|
||||
@@ -777,7 +819,8 @@ public sealed class ParameterBindingTests
|
||||
result.Limit.Should().Be(20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EmptyQueryString_UsesDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -793,7 +836,8 @@ public sealed class ParameterBindingTests
|
||||
result.PageSize.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConcurrentRequests_HandleCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -816,7 +860,8 @@ public sealed class ParameterBindingTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LargePayload_HandledCorrectly()
|
||||
{
|
||||
// Arrange - Create a moderately large message
|
||||
@@ -834,7 +879,8 @@ public sealed class ParameterBindingTests
|
||||
result!.Echo.Should().Contain(largeMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UnicodeContent_HandledCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Integration.Tests.Fixtures;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,7 +19,8 @@ public sealed class PathMatchingIntegrationTests
|
||||
|
||||
#region Exact Path Matching Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("POST", "/echo")]
|
||||
[InlineData("POST", "/users")]
|
||||
[InlineData("POST", "/slow")]
|
||||
@@ -43,7 +45,8 @@ public sealed class PathMatchingIntegrationTests
|
||||
|
||||
#region Parameterized Path Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("/users/123", "/users/{userId}")]
|
||||
[InlineData("/users/abc-def", "/users/{userId}")]
|
||||
[InlineData("/users/user_001", "/users/{userId}")]
|
||||
@@ -60,7 +63,8 @@ public sealed class PathMatchingIntegrationTests
|
||||
match!.Endpoint.Path.Should().Be(expectedPattern);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathMatching_PostUsersPath_MatchesCreateEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
@@ -78,7 +82,8 @@ public sealed class PathMatchingIntegrationTests
|
||||
|
||||
#region Non-Matching Path Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("GET", "/nonexistent")]
|
||||
[InlineData("POST", "/unknown/path")]
|
||||
[InlineData("PUT", "/echo")] // Wrong method
|
||||
@@ -100,7 +105,8 @@ public sealed class PathMatchingIntegrationTests
|
||||
|
||||
#region Method Matching Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PathMatching_SamePathDifferentMethods_MatchCorrectEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -4,6 +4,7 @@ using StellaOps.Router.Common.Frames;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Integration.Tests.Fixtures;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Router.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,7 +24,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region Echo Endpoint Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_EchoEndpoint_ReturnsExpectedResponse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -43,7 +45,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
echoResponse.Timestamp.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_EchoEndpoint_ReturnsValidRequestId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +59,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
response.RequestId.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Simple message")]
|
||||
[InlineData("Numbers and underscores 123_456_789")]
|
||||
[InlineData("Long message with multiple words and spaces")]
|
||||
@@ -78,7 +82,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region User Endpoints Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_GetUser_EndpointResponds()
|
||||
{
|
||||
// Arrange - Path parameters are extracted by the microservice
|
||||
@@ -93,7 +98,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
response.StatusCode.Should().BeOneOf(200, 400); // 400 if path param binding issue, 200 if working
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_CreateUser_ReturnsNewUserId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +121,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_FailEndpoint_ReturnsInternalError()
|
||||
{
|
||||
// Arrange
|
||||
@@ -128,7 +135,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
response.StatusCode.Should().Be(500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_NonexistentEndpoint_Returns404()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -138,7 +146,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
response.StatusCode.Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_WrongHttpMethod_Returns404()
|
||||
{
|
||||
// Arrange - /echo is POST only
|
||||
@@ -155,7 +164,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region Slow/Timeout Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_SlowEndpoint_CompletesWithinTimeout()
|
||||
{
|
||||
// Arrange - 100ms delay should complete within 30s timeout
|
||||
@@ -175,7 +185,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region Concurrent Requests Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_MultipleRequests_AllSucceed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -192,7 +203,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
responses.Should().OnlyContain(r => r.StatusCode == 200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_ConcurrentDifferentEndpoints_AllSucceed()
|
||||
{
|
||||
// Arrange & Act - only use endpoints that work with request body binding
|
||||
@@ -216,7 +228,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region Connection State Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Connection_HasRegisteredEndpoints()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -231,7 +244,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
connection.Endpoints.Should().ContainKey(("POST", "/users"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Connection_HasCorrectInstanceInfo()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -249,7 +263,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region Frame Protocol Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispatch_RequestFrameConversion_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -284,7 +299,8 @@ public sealed class RequestDispatchIntegrationTests
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Dispatch_SameRequest_ProducesDeterministicResponse()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user