using Omika.Diagnostics.Models; using Omika.Diagnostics.Queries; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// Tâche 8 — Synchronisation : résumé sync et filtres par liste fermée d'EventCodes. /// Tests sur ; la production exécute la même /// sémantique via COUNT/COUNT(DISTINCT)/FILTER/AVG côté PostgreSQL. /// public class DiagnosticSyncTests { 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, DateTimeOffset? ts = null, Guid? operationId = null, string level = "Info", long? durationMs = null) => new() { EventId = Guid.NewGuid(), EventCode = eventCode, TimestampUtc = ts ?? From.AddMinutes(1), OperationId = operationId, Level = level, Priority = "P2", DurationMs = durationMs, Source = DiagnosticSources.Mobile, IngestedAtUtc = DateTimeOffset.UtcNow }; private static DiagnosticAggregateQuery All() => new() { FromUtc = From, ToUtc = To }; // ------------------------------------------------- S1 : summary multi ---- [Fact] public async Task GetSyncSummary_MultipleEvents_ReturnsSummary() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001", operationId: Guid.NewGuid()), Ev("OMK-WKR-SYNC-001", operationId: Guid.NewGuid()) }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(2, summary.EventCount); } // ------------------------------------------------- S2 : EventCount ---- [Fact] public async Task GetSyncSummary_EventCount_CountsOnlySync() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001"), Ev("OMK-WKR-SYNC-001"), Ev("NOT-SYNC") }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(2, summary.EventCount); } // ------------------------------------------------- S3 : OperationCount distinct ---- [Fact] public async Task GetSyncSummary_OperationCount_IsDistinct() { Guid opA = Guid.NewGuid(); Guid opB = Guid.NewGuid(); var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001", operationId: opA), Ev("OMK-MOB-SYNC-001", operationId: opA), // même opération Ev("OMK-WKR-SYNC-001", operationId: opB) }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(2, summary.OperationCount); } // ------------------------------------------------- S4 : OperationId null exclu ---- [Fact] public async Task GetSyncSummary_NullOperationId_NotCounted() { Guid opA = Guid.NewGuid(); var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001", operationId: opA), Ev("OMK-MOB-SYNC-001", operationId: null) // sans OperationId }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(1, summary.OperationCount); Assert.Equal(2, summary.EventCount); // l'événement sans OperationId reste compté } // ------------------------------------------------- S5 : ErrorCount casse mixte ---- [Fact] public async Task GetSyncSummary_ErrorCount_MixedCase() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-WKR-SYNC-002", level: "Error"), Ev("OMK-WKR-SYNC-002", level: "error"), Ev("OMK-WKR-SYNC-002", level: "Critical"), Ev("OMK-WKR-SYNC-001", level: "Info") }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(3, summary.ErrorCount); } // ------------------------------------------------- S6 : durée avg/min/max ---- [Fact] public async Task GetSyncSummary_DurationStats_AreCorrect() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-WKR-SYNC-001", durationMs: 100), Ev("OMK-WKR-SYNC-001", durationMs: 300) }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(2, summary.DurationCount); Assert.Equal(200, summary.AverageMs); Assert.Equal(100, summary.MinMs); Assert.Equal(300, summary.MaxMs); } // ------------------------------------------------- S7 : durée NULL ignorée ---- [Fact] public async Task GetSyncSummary_NullDurations_Ignored() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-WKR-SYNC-001", durationMs: 100), Ev("OMK-WKR-SYNC-001", durationMs: null) }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(1, summary.DurationCount); Assert.Equal(100, summary.AverageMs); } [Fact] public async Task GetSyncSummary_NoDurations_NullStats() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001", durationMs: null) }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(0, summary.DurationCount); Assert.Null(summary.AverageMs); Assert.Null(summary.MinMs); Assert.Null(summary.MaxMs); } // ------------------------------------------------- S8 : période ---- [Fact] public async Task GetSyncSummary_RespectsPeriod() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001", From.AddMinutes(-5)), // avant Ev("OMK-MOB-SYNC-001", From.AddMinutes(5)), // dans Ev("OMK-MOB-SYNC-001", To.AddMinutes(5)) // après }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(1, summary.EventCount); } // ------------------------------------------------- S9 : non-sync exclu ---- [Fact] public async Task GetSyncSummary_NonSyncExcluded() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001"), Ev("OMK-MOB-APP-001") // non-sync }); DiagnosticSyncSummary summary = await repo.GetSyncSummaryAsync(All()); Assert.Equal(1, summary.EventCount); } // ------------------------------------------------- S10 : reconnaissance ---- [Theory] [InlineData("OMK-MOB-SYNC-001", true)] [InlineData("OMK-WKR-SYNC-001", true)] [InlineData("OMK-WKR-SYNC-002", true)] [InlineData("OMK-MOB-APP-001", false)] [InlineData(null, false)] public void SyncEventCodes_Recognition(string? code, bool expected) { Assert.Equal(expected, DiagnosticSyncEventCodes.IsSyncEventCode(code)); } // ------------------------------------------------- S11 : distribution EventCode ---- [Fact] public async Task CountGrouped_EventCode_SyncOnly_ReturnsOnlySyncCodes() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev("OMK-MOB-SYNC-001"), Ev("OMK-MOB-SYNC-001"), Ev("OMK-WKR-SYNC-001"), Ev("OMK-MOB-APP-001") // non-sync → exclu }); IReadOnlyList result = await repo.CountGroupedAsync( DiagnosticGroupDimension.EventCode, new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, SyncOnly = true }); Assert.Equal(2, result.Count); Assert.DoesNotContain(result, g => g.Key == "OMK-MOB-APP-001"); Assert.Contains(result, g => g.Key == "OMK-MOB-SYNC-001" && g.Count == 2); } }