using Omika.Diagnostics.Models; using Omika.Diagnostics.Queries; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// Tâche 2 — Recherche texte bornée. Les tests s'appuient sur le /// (in-memory) ; l'implémentation /// PostgreSQL de production exécute les mêmes contrats via ILIKE paramétré. /// public class DiagnosticSearchTests { 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? message = null, string? exceptionType = null, string? stackTrace = null, string eventCode = "ERR", DateTimeOffset? ts = null, string? component = null, string? errorCode = null, string level = "Error", string priority = "P2", string? appVersion = null, string? platform = null, string? actorKey = null) => new() { EventId = Guid.NewGuid(), EventCode = eventCode, TimestampUtc = ts ?? From.AddMinutes(1), Component = component, ErrorCode = errorCode, Message = message, ExceptionType = exceptionType, StackTrace = stackTrace, Level = level, Priority = priority, AppVersion = appVersion, Platform = platform, ActorKey = actorKey, Source = DiagnosticSources.Mobile, IngestedAtUtc = DateTimeOffset.UtcNow }; // ------------------------------------------------------- R1 : message ---- [Fact] public async Task Search_Message_ContainsTerm_IsFound() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(message: "Connexion serveur impossible") }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = "serveur" }); DiagnosticEventRecord item = Assert.Single(result.Items); Assert.Equal("Connexion serveur impossible", item.Message); Assert.Equal(1, result.TotalCount); } // ------------------------------------------------- R2 : insensible casse ---- [Fact] public async Task Search_IsCaseInsensitive() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(exceptionType: "System.NullReferenceException") }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = "nullreference" }); Assert.Single(result.Items); } // ------------------------------------------------------ R3 : hors période ---- [Fact] public async Task Search_OutOfPeriod_IsExcluded() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(message: "serveur", ts: From.AddMinutes(-5)), // avant FromUtc Ev(message: "serveur", ts: To.AddMinutes(5)) // après ToUtc (borne exclusive) }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = "serveur" }); Assert.Empty(result.Items); Assert.Equal(0, result.TotalCount); } // ----------------------------------------------------- R4 : filtres ---- [Fact] public async Task Search_RespectsOtherFilters() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(message: "serveur", component: "Omika.Api"), Ev(message: "serveur", component: "Omika.Worker") }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = "serveur", Component = "Omika.Api" }); DiagnosticEventRecord item = Assert.Single(result.Items); Assert.Equal("Omika.Api", item.Component); } // -------------------------------------- R5 : recherche vide/trop courte ---- [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData("a")] // < MinSearchLength (2) → recherche désactivée public async Task Search_EmptyOrTooShort_DisablesTextFilter(string? searchText) { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(message: "Connexion serveur impossible"), Ev(message: "Autre message") }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = searchText }); // Pas de filtre texte : tous les événements de la période sont retournés // (jamais un ILIKE '%%'). Assert.Equal(2, result.TotalCount); } // ----------------------------------------- exception_type recherchable ---- [Fact] public async Task Search_MatchesExceptionType() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(message: "échec", exceptionType: "System.TimeoutException") }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = "timeout" }); Assert.Single(result.Items); } // ------------------------------------------------ pagination serveur ---- [Fact] public async Task Search_RespectsPagination() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(message: "serveur A", ts: From.AddMinutes(1)), Ev(message: "serveur B", ts: From.AddMinutes(2)), Ev(message: "serveur C", ts: From.AddMinutes(3)) }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = "serveur", Page = 2, PageSize = 1 }); Assert.Equal(3, result.TotalCount); DiagnosticEventRecord item = Assert.Single(result.Items); Assert.Equal("serveur B", item.Message); // tri timestamp DESC → page 2 = 2e plus récent } // ------------------------------------------ validation période obligatoire ---- [Theory] [InlineData(true, false)] [InlineData(false, true)] public async Task Search_Validate_RejectsMissingPeriod(bool missingFrom, bool missingTo) { var repo = new FakeDiagnosticEventRepository(); var query = new DiagnosticSearchQuery { FromUtc = missingFrom ? null : From, ToUtc = missingTo ? null : To, SearchText = "serveur" }; await Assert.ThrowsAsync(() => repo.SearchAsync(query)); } // ---------------------------------------- joker % littéral (pas un joker) ---- [Fact] public async Task Search_PercentIsLiteral() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(message: "100% réussi"), Ev(message: "sans pourcent") }); DiagnosticQueryResult result = await repo.SearchAsync( new DiagnosticSearchQuery { FromUtc = From, ToUtc = To, SearchText = "100%" }); DiagnosticEventRecord item = Assert.Single(result.Items); Assert.Equal("100% réussi", item.Message); } }