using Omika.Diagnostics.Models; using Omika.Diagnostics.Queries; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// Tâche 5 — Occurrences d'un groupe d'erreurs. La récupération des occurrences doit /// reproduire EXACTEMENT la normalisation du regroupement (NormalizeKey = /// LOWER(NULLIF(TRIM(col),''))). Tests sur ; /// la production exécute la même sémantique via IS NOT DISTINCT FROM. /// public class DiagnosticErrorGroupOccurrenceTests { 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 eventCode = "ERR", DateTimeOffset? ts = null, string? errorCode = null, string? exceptionType = null, string? component = null, string level = "Error") => new() { EventId = Guid.NewGuid(), EventCode = eventCode, TimestampUtc = ts ?? From.AddMinutes(1), ErrorCode = errorCode, ExceptionType = exceptionType, Component = component, Level = level, Priority = "P2", Source = DiagnosticSources.Mobile, IngestedAtUtc = DateTimeOffset.UtcNow }; private static DiagnosticErrorGroupOccurrenceQuery Query( string eventCode = "ERR", string? normalizedErrorCode = null, string? normalizedExceptionType = null, string? normalizedComponent = null, int page = 1, int pageSize = DiagnosticErrorGroupOccurrenceQuery.DefaultPageSize) => new() { FromUtc = From, ToUtc = To, EventCode = eventCode, NormalizedErrorCode = normalizedErrorCode, NormalizedExceptionType = normalizedExceptionType, NormalizedComponent = normalizedComponent, Page = page, PageSize = pageSize }; // --------------------------------------------------- E1 : 3 → 3 occurrences ---- [Fact] public async Task Occurrences_ThreeEvents_ReturnThreeOccurrences() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.TimeoutException"), Ev("ERR", From.AddMinutes(2), exceptionType: "System.TimeoutException"), Ev("ERR", From.AddMinutes(3), exceptionType: "System.TimeoutException") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedExceptionType: "system.timeoutexception")); Assert.Equal(3, result.Count); } // --------------------------------------- E2 : casse différente → même groupe ---- [Fact] public async Task Occurrences_CaseInsensitiveExceptionType_SameGroup() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.TimeoutException"), Ev("ERR", From.AddMinutes(2), exceptionType: "system.timeoutexception") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedExceptionType: "system.timeoutexception")); Assert.Equal(2, result.Count); } // --------------------------------------- E3 : espaces autour → même groupe ---- [Fact] public async Task Occurrences_WhitespaceAroundComponent_SameGroup() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), component: "Omika.Api"), Ev("ERR", From.AddMinutes(2), component: " omika.api ") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedComponent: "omika.api")); Assert.Equal(2, result.Count); } // --------------------------------------- E4 : NULL ErrorCode → retrouvé ---- [Fact] public async Task Occurrences_NullErrorCode_AreFound() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), errorCode: null), Ev("ERR", From.AddMinutes(2), errorCode: string.Empty), Ev("ERR", From.AddMinutes(3), errorCode: " ") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedErrorCode: null)); Assert.Equal(3, result.Count); // null / '' / espaces → tous normalisés en NULL } // --------------------------------------- E5 : EventCode différent → exclu ---- [Fact] public async Task Occurrences_DifferentEventCode_AreExcluded() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"), Ev("OTHER", From.AddMinutes(2), exceptionType: "System.Exception") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(eventCode: "ERR", normalizedExceptionType: "system.exception")); DiagnosticEventRecord item = Assert.Single(result); Assert.Equal("ERR", item.EventCode); } // --------------------------------------- E6 : hors période → exclu ---- [Fact] public async Task Occurrences_OutOfPeriod_AreExcluded() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(-5), exceptionType: "System.Exception"), // avant Ev("ERR", From.AddMinutes(5), exceptionType: "System.Exception"), // dans Ev("ERR", To.AddMinutes(5), exceptionType: "System.Exception") // après (borne exclusive) }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedExceptionType: "system.exception")); DiagnosticEventRecord item = Assert.Single(result); } // --------------------------------------- E7 : limite respectée ---- [Fact] public async Task Occurrences_RespectsPageSize() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"), Ev("ERR", From.AddMinutes(2), exceptionType: "System.Exception"), Ev("ERR", From.AddMinutes(3), exceptionType: "System.Exception") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedExceptionType: "system.exception", pageSize: 2)); Assert.Equal(2, result.Count); } // --------------------------------------- E8 : pagination ---- [Fact] public async Task Occurrences_Pagination_ReturnsSecondPage() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"), Ev("ERR", From.AddMinutes(2), exceptionType: "System.Exception"), Ev("ERR", From.AddMinutes(3), exceptionType: "System.Exception") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedExceptionType: "system.exception", page: 2, pageSize: 1)); DiagnosticEventRecord item = Assert.Single(result); Assert.Equal(From.AddMinutes(2), item.TimestampUtc); // 2e plus récent } // --------------------------------------- E9 : tri timestamp DESC ---- [Fact] public async Task Occurrences_ReturnsDescendingOrder() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(2), exceptionType: "System.Exception"), Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"), Ev("ERR", From.AddMinutes(3), exceptionType: "System.Exception") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedExceptionType: "system.exception")); Assert.Equal( new[] { From.AddMinutes(3), From.AddMinutes(2), From.AddMinutes(1) }, result.Select(e => e.TimestampUtc).ToArray()); } // --------------------------------------- E10 : groupe inexistant → vide ---- [Fact] public async Task Occurrences_NonexistentGroup_ReturnsEmpty() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception") }); IReadOnlyList result = await repo.GetErrorGroupOccurrencesAsync( Query(normalizedExceptionType: "system.nullreferenceexception")); Assert.Empty(result); } // ----------------------------------- GetErrorGroupAsync : résumé ---- [Fact] public async Task GetErrorGroup_ReturnsSummary() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.TimeoutException", component: "Omika.Api"), Ev("ERR", From.AddMinutes(10), exceptionType: "System.TimeoutException", component: "Omika.Api") }); DiagnosticErrorGroup? group = await repo.GetErrorGroupAsync( Query(normalizedExceptionType: "system.timeoutexception", normalizedComponent: "omika.api")); Assert.NotNull(group); Assert.Equal(2, group.OccurrenceCount); Assert.Equal(From.AddMinutes(1), group.FirstSeenUtc); Assert.Equal(From.AddMinutes(10), group.LastSeenUtc); } [Fact] public async Task GetErrorGroup_Nonexistent_ReturnsNull() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception") }); DiagnosticErrorGroup? group = await repo.GetErrorGroupAsync( Query(normalizedExceptionType: "system.nullreferenceexception")); Assert.Null(group); } }