using Omika.Diagnostics.Queries; namespace Omika.Diagnostics.Tests; /// /// Tâche 5 — Identifiant URL-safe du groupe d'erreurs (§T5/12/28). Le codec ne doit /// JAMAIS lever depuis une URL malformée (décodage défensif → null) et doit faire un /// aller-retour fidèle (caractères spéciaux, null, séparateur de contrôle). /// public class DiagnosticErrorGroupLinkTests { [Fact] public void EncodeDecode_RoundTrip_PreservesIdentity() { var identity = new DiagnosticErrorGroupIdentity( "ERR", "timeout", "system.timeoutexception", "omika.api"); string encoded = DiagnosticErrorGroupLink.Encode(identity); DiagnosticErrorGroupIdentity? decoded = DiagnosticErrorGroupLink.TryDecode(encoded); Assert.NotNull(decoded); Assert.Equal(identity, decoded); } [Fact] public void EncodeDecode_WithNulls_RoundTrips() { var identity = new DiagnosticErrorGroupIdentity("ERR", null, null, null); DiagnosticErrorGroupIdentity? decoded = DiagnosticErrorGroupLink.TryDecode( DiagnosticErrorGroupLink.Encode(identity)); Assert.NotNull(decoded); Assert.Null(decoded.NormalizedErrorCode); Assert.Null(decoded.NormalizedExceptionType); Assert.Null(decoded.NormalizedComponent); Assert.Equal("ERR", decoded.EventCode); } [Fact] public void EncodeDecode_SpecialCharacters_RoundTrips() { var identity = new DiagnosticErrorGroupIdentity( "OMK/API-001_2.3", "code+avec=et&des?", "type avec espaces / accents éà", "component\u001Favec-séparateur"); DiagnosticErrorGroupIdentity? decoded = DiagnosticErrorGroupLink.TryDecode( DiagnosticErrorGroupLink.Encode(identity)); Assert.NotNull(decoded); Assert.Equal(identity, decoded); } [Fact] public void Encode_ProducesUrlSafeCharactersOnly() { string encoded = DiagnosticErrorGroupLink.Encode( new DiagnosticErrorGroupIdentity("ERR", "a/b+c=d", "e", "f")); Assert.All(encoded, c => Assert.True( (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_')); } [Fact] public void TryDecode_NullOrEmpty_ReturnsNull() { Assert.Null(DiagnosticErrorGroupLink.TryDecode(null)); Assert.Null(DiagnosticErrorGroupLink.TryDecode(string.Empty)); Assert.Null(DiagnosticErrorGroupLink.TryDecode(" ")); } [Theory] [InlineData("!!!not-base64!!!")] [InlineData("abc")] // base64 invalide (longueur) [InlineData("YWJjZA==")] // base64 standard avec padding '=' (non URL-safe) public void TryDecode_Invalid_ReturnsNull(string encoded) { Assert.Null(DiagnosticErrorGroupLink.TryDecode(encoded)); } [Fact] public void TryDecode_ReNormalizesKeys() { // On encode volontairement des clés NON normalisées ; le décodage re-normalise // (défense en profondeur) pour rester cohérent avec le regroupement. string encoded = DiagnosticErrorGroupLink.Encode( new DiagnosticErrorGroupIdentity("ERR", " TimeOut ", "System.TimeoutException", "Omika.Api")); DiagnosticErrorGroupIdentity? decoded = DiagnosticErrorGroupLink.TryDecode(encoded); Assert.NotNull(decoded); Assert.Equal("timeout", decoded.NormalizedErrorCode); Assert.Equal("system.timeoutexception", decoded.NormalizedExceptionType); Assert.Equal("omika.api", decoded.NormalizedComponent); } [Fact] public void TryDecode_EmptyEventCode_ReturnsNull() { string encoded = DiagnosticErrorGroupLink.Encode( new DiagnosticErrorGroupIdentity("", "a", "b", "c")); Assert.Null(DiagnosticErrorGroupLink.TryDecode(encoded)); } }