using Omika.Diagnostics.Models; using Omika.Diagnostics.Tests.Fakes; using Omika.Diagnostics.Web.Pages; namespace Omika.Diagnostics.Tests; /// /// Tâche T13-G — Tests PageModel multi-clients (G25-G32) : conservation des filtres /// dans /Diagnostics, pagination, détail, libellé NULL, non-confusion du serveur /// Worker, variation des statistiques Dashboard/Telemetry par client. PageModels /// testés sans HttpContext (constructeur repository uniquement, cf. infra T13-F). /// public class DiagnosticMultiClientPageModelTests { private const string ClientA = "DEMO-CLIENT-A"; private const string ClientB = "DEMO-CLIENT-B"; private const string ClientC = "DEMO-CLIENT-C"; private const string ApiA = "API-A-01"; private const string WkrA = "WORKER-A-01"; private static readonly DateTimeOffset From = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); private static DiagnosticEventRecord Ev( long id, string eventCode = "TEST", string? client = null, string? server = null, string? environment = null, DateTimeOffset? ts = null, string level = "Info", string? component = null, Guid? operationId = null) { DiagnosticEventRecord record = DiagnosticTestData.Event(Guid.NewGuid(), eventCode, ts ?? From, component: component, level: level); record.Id = id; record.ClientCode = client; record.ServerInstance = server; record.Environment = environment; record.OperationId = operationId; return record; } // --------------------------- G25 : filtre Client conservé ------------------ [Fact] public async Task G25_DiagnosticsModel_SelectedClientIsPreserved() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { Ev(1, client: ClientA, server: ApiA, environment: "Production"), Ev(2, client: ClientB, server: "API-B-01", environment: "Production") }); var model = new DiagnosticsModel(repo) { ClientCode = ClientA }; await model.OnGetAsync(); DiagnosticEventRecord record = Assert.Single(model.Items); Assert.Equal(ClientA, record.ClientCode); Assert.Equal(ClientA, model.ClientCode); } // --------------------------- G26 : pagination conserve 3 filtres ----------- [Fact] public void G26_DiagnosticsModel_RoutePreservesAllThreeFilters() { var model = new DiagnosticsModel(new FakeDiagnosticEventRepository()) { ClientCode = ClientA, ServerInstance = WkrA, Environment = "Production" }; Dictionary route = model.Route(3); Assert.Equal("3", route["pageNumber"]); Assert.Equal(ClientA, route["clientCode"]); Assert.Equal(WkrA, route["serverInstance"]); Assert.Equal("Production", route["environment"]); } // --------------------------- G27 : détail affiche les 3 valeurs ------------ [Fact] public async Task G27_DiagnosticsDetail_ShowsDeploymentContext() { var repo = new FakeDiagnosticEventRepository(); DiagnosticEventRecord record = Ev(42, client: ClientA, server: ApiA, 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(ClientA, model.Event!.ClientCode); Assert.Equal(ApiA, model.Event.ServerInstance); Assert.Equal("Production", model.Event.Environment); } // --------------------------- G28 : NULL → « Non renseigné » ---------------- [Fact] public async Task G28_NullContext_RendersNonRenseigneAndLoadsWithoutCrash() { var repo = new FakeDiagnosticEventRepository(); DiagnosticEventRecord record = Ev(7, client: null, server: null, environment: null); await repo.InsertBatchAsync(new[] { record }); var model = new DiagnosticsDetailModel(repo); Microsoft.AspNetCore.Mvc.IActionResult result = await model.OnGetAsync(7); // Le détail se charge sans planter et l'événement NULL reste lisible. Assert.IsType(result); Assert.NotNull(model.Event); Assert.Null(model.Event!.ClientCode); // L'UI affichera « Non renseigné » pour chacune des trois valeurs NULL. Assert.Equal("Non renseigné", UiDeploymentContext.Value(model.Event.ClientCode)); Assert.Equal("Non renseigné", UiDeploymentContext.Value(model.Event.ServerInstance)); Assert.Equal("Non renseigné", UiDeploymentContext.Value(model.Event.Environment)); } // --------------------------- G29 : serveur Worker ≠ serveur API ------------ [Fact] public async Task G29_WorkerOccurrence_ShowsWorkerServerNotApiServer() { var repo = new FakeDiagnosticEventRepository(); Guid operationId = Guid.NewGuid(); await repo.InsertBatchAsync(new[] { Ev(1, "OMK-API-HTTP-001", ClientA, ApiA, "Production", operationId: operationId, component: "Omika.Api"), Ev(2, "OMK-WKR-SYNC-001", ClientA, WkrA, "Production", operationId: operationId, component: "Omika.Worker") }); var model = new TracesModel(repo) { OperationId = operationId.ToString() }; await model.OnGetAsync(); Assert.Equal(2, model.Timeline.Count); DiagnosticEventRecord worker = model.Timeline.Single(e => e.Component == "Omika.Worker"); Assert.Equal(WkrA, worker.ServerInstance); Assert.NotEqual(ApiA, worker.ServerInstance); } // --------------------------- G30 : dashboard varie par client -------------- [Fact] public async Task G30_Dashboard_StatsVaryByClient() { var repo = new FakeDiagnosticEventRepository(); DateTimeOffset now = DateTimeOffset.UtcNow; await repo.InsertBatchAsync(new[] { Ev(1, client: ClientA, server: ApiA, environment: "Production", ts: now.AddMinutes(-30)), Ev(2, client: ClientA, server: ApiA, environment: "Production", ts: now.AddMinutes(-20)), Ev(3, client: ClientA, server: WkrA, environment: "Production", ts: now.AddMinutes(-10)), Ev(4, client: ClientB, server: "API-B-01", environment: "Production", ts: now.AddMinutes(-5)) }); var aModel = new IndexModel(repo) { ClientCode = ClientA }; await aModel.OnGetAsync(); var bModel = new IndexModel(repo) { ClientCode = ClientB }; await bModel.OnGetAsync(); // Les statistiques changent réellement selon le client. Assert.Equal(3, aModel.TotalEvents); Assert.Equal(1, bModel.TotalEvents); Assert.NotEqual(aModel.TotalEvents, bModel.TotalEvents); } // --------------------------- G31 : Telemetry filtre Client ----------------- [Fact] public async Task G31_Telemetry_ClientFilterModifiesDataset() { var repo = new FakeDiagnosticEventRepository(); DateTimeOffset now = DateTimeOffset.UtcNow; await repo.InsertBatchAsync(new[] { Ev(1, client: ClientA, server: ApiA, environment: "Production", ts: now.AddMinutes(-30)), Ev(2, client: ClientA, server: WkrA, environment: "Production", ts: now.AddMinutes(-20)), Ev(3, client: ClientB, server: "API-B-01", environment: "Production", ts: now.AddMinutes(-10)), Ev(4, client: ClientC, server: "API-C-DEV-01", environment: "Development", ts: now.AddMinutes(-5)) }); var aModel = new TelemetryModel(repo) { ClientCode = ClientA }; await aModel.OnGetAsync(); var cModel = new TelemetryModel(repo) { ClientCode = ClientC }; await cModel.OnGetAsync(); Assert.Equal(2, aModel.TotalEvents); Assert.Equal(1, cModel.TotalEvents); Assert.NotEqual(aModel.TotalEvents, cModel.TotalEvents); } // --------------------------- G32 : Telemetry Client + Environment ---------- [Fact] public async Task G32_Telemetry_ClientAndEnvironmentAreCombinable() { var repo = new FakeDiagnosticEventRepository(); DateTimeOffset now = DateTimeOffset.UtcNow; await repo.InsertBatchAsync(new[] { Ev(1, client: ClientB, server: "API-B-01", environment: "Production", ts: now.AddMinutes(-30)), Ev(2, client: ClientB, server: "API-B-01", environment: "Production", ts: now.AddMinutes(-20)), Ev(3, client: ClientB, server: "API-B-STG-01", environment: "Staging", ts: now.AddMinutes(-10)) }); var prodModel = new TelemetryModel(repo) { ClientCode = ClientB, Environment = "Production" }; await prodModel.OnGetAsync(); var stagingModel = new TelemetryModel(repo) { ClientCode = ClientB, Environment = "Staging" }; await stagingModel.OnGetAsync(); Assert.Equal(2, prodModel.TotalEvents); Assert.Equal(1, stagingModel.TotalEvents); } }