using Omika.Diagnostics.Models; using Omika.Diagnostics.Queries; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// Tâche 6 — Télémétrie : nouvelles dimensions de groupement (Platform/Level normalisé/ /// Priority/Source) et statistiques de durée (AVG/MIN/MAX en SQL, NULL ignorés). Tests /// sur ; la production exécute les mêmes /// contrats via GROUP BY / INITCAP(LOWER(...)) / AVG/MAX. /// public class DiagnosticTelemetryTests { 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 = "TEST", DateTimeOffset? ts = null, string? component = null, string level = "Info", string priority = "P2", string? platform = null, string? source = null, long? durationMs = null) => new() { EventId = Guid.NewGuid(), EventCode = eventCode, TimestampUtc = ts ?? From.AddMinutes(1), Component = component, Level = level, Priority = priority, Platform = platform, Source = source ?? DiagnosticSources.Mobile, DurationMs = durationMs, IngestedAtUtc = DateTimeOffset.UtcNow }; private static DiagnosticAggregateQuery All(int limit = DiagnosticAggregateDefaults.MaxLimit) => new() { FromUtc = From, ToUtc = To, Limit = limit }; // ------------------------------------------------- T1 : groupement Platform ---- [Fact] public async Task CountGrouped_Platform_ReturnsGroups() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", platform: "Android"), Ev("B", platform: "Android"), Ev("C", platform: "iOS") }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.Platform, All()); Assert.Equal(2, result.Count); Assert.Contains(result, g => g.Key == "Android" && g.Count == 2); Assert.Contains(result, g => g.Key == "iOS" && g.Count == 1); } // ---------------------------------- T2 : groupement Level normalisé (casse) ---- [Fact] public async Task CountGrouped_Level_IsCaseInsensitive() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", level: "Error"), Ev("B", level: "error"), // minuscule (ServerDiagnosticMiddleware) Ev("C", level: "ERROR"), Ev("D", level: "Info") }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.Level, All()); Assert.Equal(2, result.Count); Assert.Contains(result, g => g.Key == "Error" && g.Count == 3); Assert.Contains(result, g => g.Key == "Info" && g.Count == 1); } // ----------------------------------------------- T3 : groupement Priority ---- [Fact] public async Task CountGrouped_Priority_ReturnsGroups() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", priority: "P0"), Ev("B", priority: "P0"), Ev("C", priority: "P1") }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.Priority, All()); Assert.Equal(2, result.Count); Assert.Contains(result, g => g.Key == "P0" && g.Count == 2); Assert.Contains(result, g => g.Key == "P1" && g.Count == 1); } // ---------------------------------------------- T4 : durée moyenne ---- [Fact] public async Task GetDurationStats_Average_IsCorrect() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", durationMs: 100), Ev("A", durationMs: 200) }); DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All()); Assert.NotNull(stats); Assert.Equal(2, stats.Count); Assert.Equal(150, stats.AverageMs); } // ---------------------------------------------- T5 : durée max ---- [Fact] public async Task GetDurationStats_Max_IsCorrect() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", durationMs: 100), Ev("A", durationMs: 350) }); DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All()); Assert.NotNull(stats); Assert.Equal(350, stats.MaxMs); Assert.Equal(100, stats.MinMs); } // ---------------------------------------------- T6 : durée NULL ignorée ---- [Fact] public async Task GetDurationStats_NullDurations_AreIgnored() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", durationMs: 100), Ev("B", durationMs: null), // sans durée Ev("C", durationMs: 200) }); DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All()); Assert.NotNull(stats); Assert.Equal(2, stats.Count); Assert.Equal(150, stats.AverageMs); Assert.Equal(100, stats.MinMs); Assert.Equal(200, stats.MaxMs); } // ---------------------------------------------- T7 : aucune durée → null ---- [Fact] public async Task GetDurationStats_NoDurations_ReturnsNull() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", durationMs: null), Ev("B", durationMs: null) }); DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All()); Assert.Null(stats); } // ---------------------------------------------- T8 : période appliquée ---- [Fact] public async Task GetDurationStats_RespectsPeriod() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", From.AddMinutes(-5), durationMs: 999), // avant → exclu Ev("A", From.AddMinutes(5), durationMs: 100), // dans Ev("A", To.AddMinutes(5), durationMs: 999) // après → exclu }); DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All()); Assert.NotNull(stats); Assert.Equal(1, stats.Count); Assert.Equal(100, stats.AverageMs); } // ---------------------------------------------- T9 : filtre EventCode ---- [Fact] public async Task GetDurationStats_RespectsEventCodeFilter() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", durationMs: 100), Ev("B", durationMs: 900) }); DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync( new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, EventCode = "A" }); Assert.NotNull(stats); Assert.Equal(1, stats.Count); Assert.Equal(100, stats.AverageMs); } // ---------------------------------------------- T10 : top lentes limité ---- [Fact] public async Task GetTopDurationByEventCode_RespectsLimitAndSort() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("FAST", durationMs: 10), Ev("SLOW", durationMs: 500), Ev("MEDIUM", durationMs: 250) }); IReadOnlyList result = await repo.GetTopDurationByEventCodeAsync( new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, Limit = 2 }); Assert.Equal(2, result.Count); Assert.Equal("SLOW", result[0].EventCode); // moyenne décroissante Assert.Equal("MEDIUM", result[1].EventCode); } [Fact] public async Task GetTopDurationByEventCode_ExcludesNullDurations() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("A", durationMs: 100), Ev("B", durationMs: null) // sans durée → ne doit pas apparaître }); IReadOnlyList result = await repo.GetTopDurationByEventCodeAsync(All()); DiagnosticDurationByEventCode item = Assert.Single(result); Assert.Equal("A", item.EventCode); } }