using System.Text.Json; using Omika.Core.Observability.External; using Omika.Diagnostics.Import; using Omika.Diagnostics.Models; using Omika.Diagnostics.Queries; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// Tâche 13-A — Socle de stockage ClientCode / ServerInstance / Environment : /// compatibilité additive (NULL), conservation des valeurs, lecture/écriture du /// repository (fake) et filtres backend paramétrés (miroir du repository réel). /// Aucun PostgreSQL réel requis (même convention que les autres tests, §51). /// public class DiagnosticClientContextTests { private static readonly DateTimeOffset From = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); // --------------------------- A1 : ancien record sans contexte -------------- [Fact] public void A1_RecordWithoutContext_AllContextFieldsAreNull() { DiagnosticEventRecord record = DiagnosticTestData.Event(Guid.NewGuid(), "OLD"); Assert.Null(record.ClientCode); Assert.Null(record.ServerInstance); Assert.Null(record.Environment); } // --------------------------- A2/A3/A4 : conservation d'une valeur ---------- [Fact] public void A2_RecordWithClientCode_PreservesValue() { DiagnosticEventRecord record = EventWithContext(Guid.NewGuid(), clientCode: "CLIENT-A"); Assert.Equal("CLIENT-A", record.ClientCode); } [Fact] public void A3_RecordWithServerInstance_PreservesValue() { DiagnosticEventRecord record = EventWithContext(Guid.NewGuid(), serverInstance: "API-SRV-01"); Assert.Equal("API-SRV-01", record.ServerInstance); } [Fact] public void A4_RecordWithEnvironment_PreservesValue() { DiagnosticEventRecord record = EventWithContext(Guid.NewGuid(), environment: "Production"); Assert.Equal("Production", record.Environment); } // --------------------------- A5 : insert avec les trois valeurs ------------- [Fact] public async Task A5_InsertWithAllThreeValues_StoresAndReadsThem() { var repo = new FakeDiagnosticEventRepository(); DiagnosticEventRecord record = EventWithContext(Guid.NewGuid(), "CLIENT-A", "API-SRV-01", "Production"); record.Id = 42; int inserted = await repo.InsertBatchAsync(new[] { record }); Assert.Equal(1, inserted); DiagnosticEventRecord? read = await repo.GetByIdAsync(42); Assert.NotNull(read); Assert.Equal("CLIENT-A", read!.ClientCode); Assert.Equal("API-SRV-01", read.ServerInstance); Assert.Equal("Production", read.Environment); } // --------------------------- A6 : insert NULL compatible -------------------- [Fact] public async Task A6_InsertWithNullContext_IsAcceptedAndReadsNull() { var repo = new FakeDiagnosticEventRepository(); DiagnosticEventRecord record = EventWithContext(Guid.NewGuid(), null, null, null); record.Id = 7; int inserted = await repo.InsertBatchAsync(new[] { record }); Assert.Equal(1, inserted); DiagnosticEventRecord? read = await repo.GetByIdAsync(7); Assert.NotNull(read); Assert.Null(read!.ClientCode); Assert.Null(read.ServerInstance); Assert.Null(read.Environment); } // --------------------------- A7 : lecture repository restitue les valeurs ---- [Fact] public async Task A7_QueryAsync_RestoresContextValues() { var repo = new FakeDiagnosticEventRepository(); DiagnosticEventRecord record = EventWithContext(Guid.NewGuid(), "CLIENT-B", "API-SRV-02", "Staging"); record.Id = 10; await repo.InsertBatchAsync(new[] { record }); DiagnosticQueryResult result = await repo.QueryAsync(new DiagnosticEventQuery()); DiagnosticEventRecord read = Assert.Single(result.Items); Assert.Equal("CLIENT-B", read.ClientCode); Assert.Equal("API-SRV-02", read.ServerInstance); Assert.Equal("Staging", read.Environment); } // --------------------------- A8 : ancien JSONL / source absente → NULL ------ [Fact] public void A8_Mapper_OldRecordWithoutContext_LeavesContextNull() { // RemoteDiagnosticRecord expose désormais ClientCode/ServerInstance/Environment // (T13-B), mais un ancien enregistrement sans contexte porte ces champs à null : // le mapper doit les laisser NULL (aucune valeur inventée). RemoteDiagnosticRecord old = DiagnosticTestData.Record(Guid.NewGuid(), "OLD"); DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(old, "mobile"); Assert.NotNull(mapped); Assert.Null(mapped!.ClientCode); Assert.Null(mapped.ServerInstance); Assert.Null(mapped.Environment); } [Fact] public void A8_OldJsonlLineWithoutContextFields_DeserializesAndMapsToNull() { RemoteDiagnosticRecord old = DiagnosticTestData.Record(Guid.NewGuid(), "OLD"); string line = DiagnosticTestData.SerializeLine(old); // Aucun champ « ClientCode/ServerInstance/Environment » requis : la ligne // ancienne (sans ces champs) doit être acceptée telle quelle (§20). RemoteDiagnosticRecord? parsed = JsonSerializer.Deserialize(line, new JsonSerializerOptions(JsonSerializerDefaults.Web)); Assert.NotNull(parsed); DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(parsed, "mobile"); Assert.NotNull(mapped); Assert.Null(mapped!.ClientCode); Assert.Null(mapped.ServerInstance); Assert.Null(mapped.Environment); } // --------------------------- A9/A10/A11 : filtres backend ------------------- [Fact] public async Task A9_ClientCodeFilter_ReturnsOnlyMatching() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { EventWithContext(Guid.NewGuid(), "CLIENT-A", "SRV-1", "Prod", From.AddMinutes(1)), EventWithContext(Guid.NewGuid(), "CLIENT-B", "SRV-1", "Prod", From.AddMinutes(2)) }); DiagnosticQueryResult result = await repo.QueryAsync(new DiagnosticEventQuery { ClientCode = "CLIENT-A" }); Assert.Single(result.Items); Assert.Equal("CLIENT-A", result.Items[0].ClientCode); } [Fact] public async Task A10_ServerInstanceFilter_ReturnsOnlyMatching() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { EventWithContext(Guid.NewGuid(), "CLIENT-A", "SRV-1", "Prod", From.AddMinutes(1)), EventWithContext(Guid.NewGuid(), "CLIENT-A", "SRV-2", "Prod", From.AddMinutes(2)) }); DiagnosticQueryResult result = await repo.QueryAsync(new DiagnosticEventQuery { ServerInstance = "SRV-2" }); Assert.Single(result.Items); Assert.Equal("SRV-2", result.Items[0].ServerInstance); } [Fact] public async Task A11_EnvironmentFilter_ReturnsOnlyMatching() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync(new[] { EventWithContext(Guid.NewGuid(), "CLIENT-A", "SRV-1", "Production", From.AddMinutes(1)), EventWithContext(Guid.NewGuid(), "CLIENT-A", "SRV-1", "Staging", From.AddMinutes(2)) }); DiagnosticQueryResult result = await repo.QueryAsync(new DiagnosticEventQuery { Environment = "Staging" }); Assert.Single(result.Items); Assert.Equal("Staging", result.Items[0].Environment); } private static DiagnosticEventRecord EventWithContext( Guid id, string? clientCode = null, string? serverInstance = null, string? environment = null, DateTimeOffset? timestamp = null) { DiagnosticEventRecord record = DiagnosticTestData.Event(id, "TEST", timestamp); record.ClientCode = clientCode; record.ServerInstance = serverInstance; record.Environment = environment; return record; } }