Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,221 @@
using Microsoft.Extensions.Logging.Abstractions;
using Omika.Diagnostics.Models;
using Omika.Diagnostics.Tests.Fakes;
using Omika.Diagnostics.Web.Pages;
namespace Omika.Diagnostics.Tests;
/// <summary>
/// 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).
/// </summary>
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<string, string> 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<Microsoft.AspNetCore.Mvc.RazorPages.PageResult>(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<ErreursModel>.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<ErreursModel>.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);
}
}