using Omika.Diagnostics.Models; using Omika.Diagnostics.Queries; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// Tâche T14-C — Normalisation des dimensions d'agrégation (§9/§10) : les variantes de /// casse/espaces de component et error_code ne doivent plus produire de /// dimensions distinctes dans les statistiques. Aucune donnée persistée n'est modifiée : /// seule la clé d'agrégation est normalisée (déterministe, côté requête). /// public class DiagnosticNormalizationTests { private static readonly DateTimeOffset From = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); private static readonly DateTimeOffset To = new(2025, 2, 1, 0, 0, 0, TimeSpan.Zero); private static DiagnosticEventRecord Ev(string? component = null, string? errorCode = null) { return new DiagnosticEventRecord { EventId = Guid.NewGuid(), EventCode = "TEST", TimestampUtc = From.AddMinutes(1), Component = component, ErrorCode = errorCode, Level = "Error", Priority = "P1", Source = DiagnosticSources.Mobile, IngestedAtUtc = DateTimeOffset.UtcNow }; } // -------------------------------------------------- T14-C Test 8 : Component -- [Fact] public async Task Component_NormalizesCaseAndWhitespace() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(component: "Omika.Api"), Ev(component: "OMIKA.API"), Ev(component: "omika.api"), Ev(component: " Omika.Api "), Ev(component: "Omika.Worker") }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.Component, new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To }); // 4 variantes du même composant + 1 autre = 2 dimensions logiques. Assert.Equal(2, result.Count); Assert.Contains(result, g => g.Key == "Omika.Api" && g.Count == 4); Assert.Contains(result, g => g.Key == "Omika.Worker" && g.Count == 1); } // -------------------------------------------------- T14-C Test 9 : ErrorCode -- [Fact] public async Task ErrorCode_NormalizesCaseAndWhitespace() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(errorCode: "timeout"), Ev(errorCode: "TIMEOUT"), Ev(errorCode: "Timeout"), Ev(errorCode: " timeout "), Ev(errorCode: "CRASH") }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.ErrorCode, new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To }); // 4 variantes du même code + 1 autre = 2 codes logiques. Assert.Equal(2, result.Count); Assert.Contains(result, g => g.Key == "TIMEOUT" && g.Count == 4); Assert.Contains(result, g => g.Key == "CRASH" && g.Count == 1); } // ------------------------- Null/vide restent un groupe distinct (compatibilité) -- [Fact] public async Task ErrorCode_NullAndEmpty_RemainGroupedAsNull() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(errorCode: null), Ev(errorCode: " "), Ev(errorCode: "TIMEOUT") }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.ErrorCode, new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To }); Assert.Equal(2, result.Count); Assert.Contains(result, g => g.Key is null && g.Count == 2); Assert.Contains(result, g => g.Key == "TIMEOUT" && g.Count == 1); } // ------------------------- T20 §7 : « Codes d'erreur principaux » ---- [Fact] public async Task ErrorCode_WithHasErrorCode_ExcludesNullAndBlank() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(errorCode: null), Ev(errorCode: " "), Ev(errorCode: ""), Ev(errorCode: "TIMEOUT"), Ev(errorCode: "CRASH"), }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.ErrorCode, new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, HasErrorCode = true }); // Seuls les ErrorCode réels comptent : jamais de groupe « Non renseigné ». Assert.Equal(2, result.Count); Assert.DoesNotContain(result, g => g.Key is null); Assert.Contains(result, g => g.Key == "TIMEOUT" && g.Count == 1); Assert.Contains(result, g => g.Key == "CRASH" && g.Count == 1); } // --------------------- Équivalent C# documenté (unité, sans repository) -------- [Theory] [InlineData("Omika.Api", "Omika.Api")] [InlineData("OMIKA.API", "Omika.Api")] [InlineData("omika.api", "Omika.Api")] [InlineData(" omika.api ", "Omika.Api")] [InlineData("Omika.Diagnostics.Worker", "Omika.Diagnostics.Worker")] [InlineData(null, null)] [InlineData("", null)] [InlineData(" ", null)] public void ComponentHelper_NormalizesDeterministically(string? input, string? expected) { Assert.Equal(expected, DiagnosticDimensionNormalization.Component(input)); } [Theory] [InlineData("timeout", "TIMEOUT")] [InlineData("TIMEOUT", "TIMEOUT")] [InlineData("Timeout", "TIMEOUT")] [InlineData(" timeout ", "TIMEOUT")] [InlineData("UnhandledException", "UNHANDLEDEXCEPTION")] [InlineData(null, null)] [InlineData("", null)] [InlineData(" ", null)] public void ErrorCodeHelper_NormalizesDeterministically(string? input, string? expected) { Assert.Equal(expected, DiagnosticDimensionNormalization.ErrorCode(input)); } }