using Microsoft.Extensions.Logging.Abstractions; using Omika.Diagnostics.Models; using Omika.Diagnostics.Tests.Fakes; using Omika.Diagnostics.Web.Pages; namespace Omika.Diagnostics.Tests; /// /// Tâche 13-F — Tests des PageModels (Diagnostics / Erreurs / Traces) : transmission /// des filtres de contexte (F11/F17), chargement des options dynamiques (F12), /// conservation de la sélection (F13), pagination (F14), détail (F15), libellé NULL /// historique (F16) et non-confusion du serveur producteur (§38). Les PageModels /// n'accèdent pas à HttpContext pour ces chemins (aucun harnais Web requis). /// public class DiagnosticPageModelTests { private static readonly DateTimeOffset From = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); private static DiagnosticEventRecord Ev( long id, string eventCode = "TEST", string? clientCode = null, string? serverInstance = null, string? environment = null, DateTimeOffset? ts = null, string level = "Info", Guid? operationId = null, string? component = null) { DiagnosticEventRecord record = DiagnosticTestData.Event(Guid.NewGuid(), eventCode, ts ?? From, component: component, level: level); record.Id = id; record.ClientCode = clientCode; record.ServerInstance = serverInstance; record.Environment = environment; record.OperationId = operationId; return record; } // --------------------------- F11 : DiagnosticsModel transmet les filtres ----- [Fact] public async Task F11_DiagnosticsModel_PassesDeploymentFiltersToQuery() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(1, clientCode: "CLIENT-A", serverInstance: "API-SRV-01", environment: "Production"), Ev(2, clientCode: "CLIENT-B", serverInstance: "WKR-SRV-01", environment: "Staging") }); var model = new DiagnosticsModel(repo) { ClientCode = "CLIENT-A", ServerInstance = "API-SRV-01", Environment = "Production" }; await model.OnGetAsync(); DiagnosticEventRecord record = Assert.Single(model.Items); Assert.Equal("CLIENT-A", record.ClientCode); Assert.Equal("API-SRV-01", record.ServerInstance); Assert.Equal("Production", record.Environment); } // --------------------------- F12 : options dynamiques chargées --------------- [Fact] public async Task F12_DiagnosticsModel_LoadsDeploymentFilterOptions() { var repo = new FakeDiagnosticEventRepository(); // Les options de filtre sont bornées par une fenêtre glissante (60 jours) : // les événements doivent être RÉCENTS pour être observés (cf. OptionsWindowDays). await repo.InsertBatchAsync(new[] { Ev(1, clientCode: "CLIENT-A", serverInstance: "API-SRV-01", environment: "Production", ts: DateTimeOffset.UtcNow.AddMinutes(-5)), Ev(2, clientCode: "CLIENT-B", serverInstance: "WKR-SRV-01", environment: "Staging", ts: DateTimeOffset.UtcNow.AddMinutes(-4)) }); var model = new DiagnosticsModel(repo); await model.OnGetAsync(); Assert.Contains("CLIENT-A", model.ClientOptions); Assert.Contains("CLIENT-B", model.ClientOptions); Assert.Contains("API-SRV-01", model.ServerOptions); Assert.Contains("WKR-SRV-01", model.ServerOptions); Assert.Contains("Production", model.EnvironmentOptions); Assert.Contains("Staging", model.EnvironmentOptions); } // --------------------------- F13 : sélection conservée ----------------------- [Fact] public async Task F13_DiagnosticsModel_SelectedValueRemainsSelected() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(1, clientCode: "CLIENT-A", serverInstance: "SRV-1", environment: "Production") }); var model = new DiagnosticsModel(repo) { ClientCode = "CLIENT-Z", ServerInstance = "SRV-Z", Environment = "Custom" }; await model.OnGetAsync(); // La valeur active reste la valeur du modèle (jamais réécrite)… Assert.Equal("CLIENT-Z", model.ClientCode); Assert.Equal("SRV-Z", model.ServerInstance); Assert.Equal("Custom", model.Environment); // …et elle est injectée en tête des options (pagination/conservation §24). Assert.Equal("CLIENT-Z", model.ClientOptions[0]); Assert.Equal("SRV-Z", model.ServerOptions[0]); Assert.Equal("Custom", model.EnvironmentOptions[0]); Assert.True(DiagnosticsModel.IsSelected("CLIENT-Z", model.ClientCode)); } // --------------------------- F14 : pagination conserve les filtres ---------- [Fact] public void F14_DiagnosticsModel_RoutePreservesDeploymentFilters() { var model = new DiagnosticsModel(new FakeDiagnosticEventRepository()) { ClientCode = "CLIENT-A", ServerInstance = "WKR-SRV-01", Environment = "Production" }; Dictionary route = model.Route(2); Assert.Equal("2", route["pageNumber"]); Assert.Equal("CLIENT-A", route["clientCode"]); Assert.Equal("WKR-SRV-01", route["serverInstance"]); Assert.Equal("Production", route["environment"]); } // --------------------------- F15 : détail affiche les trois valeurs --------- [Fact] public async Task F15_DiagnosticsDetail_LoadsDeploymentContext() { var repo = new FakeDiagnosticEventRepository(); DiagnosticEventRecord record = Ev(42, clientCode: "CLIENT-A", serverInstance: "WKR-SRV-01", environment: "Production"); await repo.InsertBatchAsync(new[] { record }); var model = new DiagnosticsDetailModel(repo); Microsoft.AspNetCore.Mvc.IActionResult result = await model.OnGetAsync(42); Assert.IsType(result); Assert.NotNull(model.Event); Assert.Equal("CLIENT-A", model.Event!.ClientCode); Assert.Equal("WKR-SRV-01", model.Event.ServerInstance); Assert.Equal("Production", model.Event.Environment); } // --------------------------- F16 : NULL historique → « Non renseigné » ------- [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void F16_NullContext_RendersNonRenseigne(string? value) { Assert.Equal("Non renseigné", UiDeploymentContext.Value(value)); } [Fact] public void F16_NonNullContext_IsPreserved() { Assert.Equal("CLIENT-A", UiDeploymentContext.Value("CLIENT-A")); } // --------------------------- F17/F18 : ErreursModel transmet + pré-agrégation - [Fact] public async Task F17_F18_ErreursModel_AppliesFiltersBeforeAggregation() { var repo = new FakeDiagnosticEventRepository(); DateTimeOffset now = DateTimeOffset.UtcNow; await repo.InsertBatchAsync(new[] { Ev(1, "ERR", "CLIENT-A", "SRV-1", "Production", now.AddMinutes(-5), level: "Error", component: "Omika.Api"), Ev(2, "ERR", "CLIENT-A", "SRV-1", "Production", now.AddMinutes(-4), level: "Error", component: "Omika.Api"), Ev(3, "ERR", "CLIENT-B", "SRV-1", "Staging", now.AddMinutes(-3), level: "Error", component: "Omika.Api"), Ev(4, "ERR", "CLIENT-A", "SRV-1", "Staging", now.AddMinutes(-2), level: "Error", component: "Omika.Api") }); // Sans filtre : un seul groupe logique, 4 occurrences. var allModel = new ErreursModel(repo, new FakeDiagnosticTaskRepository(), NullLogger.Instance); await allModel.OnGetAsync(); Omika.Diagnostics.Queries.DiagnosticErrorGroup allGroup = Assert.Single(allModel.Groups); Assert.Equal(4, allGroup.OccurrenceCount); // Avec filtre Client + Environnement : le regroupement est recalculé sur le périmètre. var filteredModel = new ErreursModel(repo, new FakeDiagnosticTaskRepository(), NullLogger.Instance) { ClientCode = "CLIENT-A", Environment = "Production" }; await filteredModel.OnGetAsync(); Omika.Diagnostics.Queries.DiagnosticErrorGroup filteredGroup = Assert.Single(filteredModel.Groups); Assert.Equal(2, filteredGroup.OccurrenceCount); } // --------------------------- §38 : Traces sans confusion serveur ------------- [Fact] public async Task Section38_Traces_KeepProducerServerInstanceDistinct() { var repo = new FakeDiagnosticEventRepository(); Guid operationId = Guid.NewGuid(); await repo.InsertBatchAsync(new[] { Ev(1, "OMK-API-HTTP-001", "CLIENT-A", "API-SRV-01", "Production", operationId: operationId, component: "Omika.Api"), Ev(2, "OMK-WKR-SYNC-001", "CLIENT-A", "WKR-SRV-01", "Production", operationId: operationId, component: "Omika.Worker") }); var model = new TracesModel(repo) { OperationId = operationId.ToString() }; await model.OnGetAsync(); Assert.Equal(2, model.Timeline.Count); DiagnosticEventRecord api = model.Timeline.Single(e => e.Component == "Omika.Api"); DiagnosticEventRecord worker = model.Timeline.Single(e => e.Component == "Omika.Worker"); // Le serveur affiché est le serveur PRODUCTEUR de chaque événement, jamais mélangé. Assert.Equal("API-SRV-01", api.ServerInstance); Assert.Equal("WKR-SRV-01", worker.ServerInstance); Assert.Equal("CLIENT-A", worker.ClientCode); Assert.Equal("Production", worker.Environment); } }