Files
Omika/tests/Omika.Diagnostics.Tests/DiagnosticJsonlContextTests.cs
T
2026-10-23 18:46:50 +02:00

314 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using Omika.Core.Observability.External;
using Omika.Core.Observability.Identity;
using Omika.Core.Observability.Outbox;
using Omika.Diagnostics.Import;
using Omika.Diagnostics.Models;
using Omika.Diagnostics.Tests.Fakes;
namespace Omika.Diagnostics.Tests;
/// <summary>
/// Tâche 13-B — Propagation ClientCode / ServerInstance / Environment dans le pipeline
/// JSONL (transport uniquement, aucune résolution réelle) :
/// <list type="bullet">
/// <item>B1–B5 : compatibilité ancien JSONL + round-trip des nouveaux champs.</item>
/// <item>B6/B7 : le mapper (remote ET server via le store) conserve les 3 valeurs.</item>
/// <item>B8/B24 : le ClientId mobile ne devient JAMAIS le ClientCode autoritaire.</item>
/// <item>B9/B10 : StripIdentity épure les identités brutes et conserve l'ActorKey.</item>
/// <item>B11/B12 : le ClientCode autoritaire fourni au store est conservé ; l'absence → null.</item>
/// </list>
/// Aucun PostgreSQL réel requis (fake + store fichier en temp, §51).
/// </summary>
public class DiagnosticJsonlContextTests : IDisposable
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly string _dir = CreateTempDirectory();
public void Dispose() => DeleteDirectory(_dir);
// --------------------------- B1 : ancien JSONL sans nouveaux champs ----------
[Fact]
public void B1_OldJsonlWithoutNewFields_DeserializesToNullContext()
{
const string line = """
{"receivedAtUtc":"2025-01-01T00:00:00+00:00","batchId":"11111111-1111-1111-1111-111111111111","ingestionTraceId":"22222222-2222-2222-2222-222222222222","event":{"eventId":"33333333-3333-3333-3333-333333333333","eventCode":"OMK-MOB-APP-001","timestampUtc":"2025-01-01T00:00:00+00:00","level":"Info"}}
""";
RemoteDiagnosticRecord? parsed = JsonSerializer.Deserialize<RemoteDiagnosticRecord>(line, JsonOptions);
Assert.NotNull(parsed);
Assert.Null(parsed!.ClientCode);
Assert.Null(parsed.ServerInstance);
Assert.Null(parsed.Environment);
}
// --------------------------- B2/B3/B4/B5 : round-trip ------------------------
[Fact]
public void B2_RecordWithClientCode_RoundTrips()
{
RemoteDiagnosticRecord record = DiagnosticTestData.Record(
Guid.NewGuid(), "OMK-MOB-APP-001", clientCode: "CLIENT-TEST-A");
RemoteDiagnosticRecord? back = RoundTrip(record);
Assert.Equal("CLIENT-TEST-A", back!.ClientCode);
}
[Fact]
public void B3_RecordWithServerInstance_RoundTrips()
{
RemoteDiagnosticRecord record = DiagnosticTestData.Record(
Guid.NewGuid(), "OMK-MOB-APP-001", serverInstance: "API-TEST-A1");
RemoteDiagnosticRecord? back = RoundTrip(record);
Assert.Equal("API-TEST-A1", back!.ServerInstance);
}
[Fact]
public void B4_RecordWithEnvironment_RoundTrips()
{
RemoteDiagnosticRecord record = DiagnosticTestData.Record(
Guid.NewGuid(), "OMK-MOB-APP-001", environment: "Development");
RemoteDiagnosticRecord? back = RoundTrip(record);
Assert.Equal("Development", back!.Environment);
}
[Fact]
public void B5_RecordWithAllThreeFields_RoundTrips()
{
RemoteDiagnosticRecord record = DiagnosticTestData.Record(
Guid.NewGuid(),
"OMK-MOB-APP-001",
clientCode: "CLIENT-TEST-A",
serverInstance: "API-TEST-A1",
environment: "Development");
RemoteDiagnosticRecord? back = RoundTrip(record);
Assert.Equal("CLIENT-TEST-A", back!.ClientCode);
Assert.Equal("API-TEST-A1", back!.ServerInstance);
Assert.Equal("Development", back!.Environment);
}
// --------------------------- B6 : mapper remote ------------------------------
[Fact]
public void B6_Mapper_RemoteRecord_PreservesThreeValues()
{
RemoteDiagnosticRecord record = DiagnosticTestData.Record(
Guid.NewGuid(),
"OMK-MOB-APP-001",
clientCode: "CLIENT-TEST-A",
serverInstance: "API-TEST-A1",
environment: "Development");
DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile");
Assert.NotNull(mapped);
Assert.Equal("CLIENT-TEST-A", mapped!.ClientCode);
Assert.Equal("API-TEST-A1", mapped.ServerInstance);
Assert.Equal("Development", mapped.Environment);
}
// --------------------------- B7 : mapper server (via le store) ---------------
[Fact]
public async Task B7_ServerRecordThroughStoreAndMapper_PreservesValues()
{
// ServerDiagnosticRecord (privé) n'est jamais mappé directement : il est écrit
// par le store puis relu comme RemoteDiagnosticRecord. Le cycle complet doit
// conserver les 3 valeurs jusqu'au DiagnosticEventRecord.
var context = new DiagnosticDeploymentContext
{
ClientCode = "CLIENT-TEST-A",
ServerInstance = "API-TEST-A1",
Environment = "Development"
};
Guid eventId = Guid.NewGuid();
string content = await WriteAsync(NewEvent(eventId), DiagnosticActorKey.Empty, context);
RemoteDiagnosticRecord? record = JsonSerializer.Deserialize<RemoteDiagnosticRecord>(content, JsonOptions);
Assert.NotNull(record);
DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile");
Assert.NotNull(mapped);
Assert.Equal("CLIENT-TEST-A", mapped!.ClientCode);
Assert.Equal("API-TEST-A1", mapped.ServerInstance);
Assert.Equal("Development", mapped.Environment);
}
// --------------------------- B8 : ClientId mobile ≠ ClientCode ----------------
[Fact]
public async Task B8_MobileClientId_NeverBecomesClientCodeAutomatically()
{
// Identité transport mobile brute : NE DOIT PAS devenir clientCode.
DiagnosticEventDto evt = NewEvent(Guid.NewGuid(), clientId: "FAUX-CLIENT");
string content = await WriteAsync(evt, DiagnosticActorKey.Empty, context: null);
using JsonDocument doc = JsonDocument.Parse(content);
// Contexte serveur absent → clientCode non écrit (WhenWritingNull), et le
// ClientId mobile est épuré par StripIdentity : "FAUX-CLIENT" n'apparaît pas.
Assert.False(doc.RootElement.TryGetProperty("clientCode", out _));
Assert.DoesNotContain("FAUX-CLIENT", content, StringComparison.Ordinal);
}
// --------------------------- B9 : StripIdentity -------------------------------
[Fact]
public async Task B9_StripIdentity_StillRemovesForbiddenIdentities()
{
DiagnosticEventDto evt = NewEvent(
Guid.NewGuid(),
deviceId: "device-1",
clientId: "BATIK.01",
userId: "user-1",
collaborateurId: "42");
string content = await WriteAsync(evt, DiagnosticActorKey.Empty, context: null);
Assert.DoesNotContain("\"deviceId\"", content, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("\"clientId\"", content, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("\"userId\"", content, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("\"collaborateurId\"", content, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("device-1", content, StringComparison.Ordinal);
Assert.DoesNotContain("user-1", content, StringComparison.Ordinal);
Assert.DoesNotContain("\"42\"", content, StringComparison.Ordinal);
}
// --------------------------- B10 : ActorKey conservé --------------------------
[Fact]
public async Task B10_ActorKey_IsPreserved()
{
const string actorKeyValue = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
var actorKey = new DiagnosticActorKey(actorKeyValue, 1);
string content = await WriteAsync(NewEvent(Guid.NewGuid()), actorKey, context: null);
using JsonDocument doc = JsonDocument.Parse(content);
Assert.Equal(actorKeyValue, doc.RootElement.GetProperty("actorKey").GetString());
Assert.Equal(1, doc.RootElement.GetProperty("actorKeyVersion").GetInt32());
}
// --------------------------- B11 : ClientCode autoritaire conservé ------------
[Fact]
public async Task B11_AuthoritativeClientCode_ProvidedToStore_IsPreserved()
{
var context = new DiagnosticDeploymentContext { ClientCode = "CLIENT-AUTORITAIRE" };
string content = await WriteAsync(NewEvent(Guid.NewGuid()), DiagnosticActorKey.Empty, context);
using JsonDocument doc = JsonDocument.Parse(content);
Assert.Equal("CLIENT-AUTORITAIRE", doc.RootElement.GetProperty("clientCode").GetString());
}
// --------------------------- B12 : valeur absente → null ----------------------
[Fact]
public async Task B12_AbsentContext_WritesNoContextFields_AndDeserializesToNull()
{
string content = await WriteAsync(NewEvent(Guid.NewGuid()), DiagnosticActorKey.Empty, context: null);
using (JsonDocument doc = JsonDocument.Parse(content))
{
// Champs null omis (WhenWritingNull) : compatibilité ancien JSONL.
Assert.False(doc.RootElement.TryGetProperty("clientCode", out _));
Assert.False(doc.RootElement.TryGetProperty("serverInstance", out _));
Assert.False(doc.RootElement.TryGetProperty("environment", out _));
}
RemoteDiagnosticRecord? record = JsonSerializer.Deserialize<RemoteDiagnosticRecord>(content, JsonOptions);
Assert.NotNull(record);
Assert.Null(record!.ClientCode);
Assert.Null(record.ServerInstance);
Assert.Null(record.Environment);
}
// --------------------------- B24 : sécurité ----------------------------------
[Fact]
public async Task B24_MobileClientId_WithAuthoritativeContext_UsesAuthoritativeCode()
{
// DTO mobile : identité transport falsifiable.
DiagnosticEventDto evt = NewEvent(Guid.NewGuid(), clientId: "FAUX-CLIENT");
// Contexte serveur : ClientCode AUTORITAIRE (T13-C le fournira réellement).
var context = new DiagnosticDeploymentContext { ClientCode = "CLIENT-AUTORITAIRE" };
string content = await WriteAsync(evt, DiagnosticActorKey.Empty, context);
using JsonDocument doc = JsonDocument.Parse(content);
Assert.Equal("CLIENT-AUTORITAIRE", doc.RootElement.GetProperty("clientCode").GetString());
Assert.DoesNotContain("FAUX-CLIENT", content, StringComparison.Ordinal);
Assert.DoesNotContain("\"clientId\"", content, StringComparison.OrdinalIgnoreCase);
}
// --------------------------- Helpers ------------------------------------------
private static DiagnosticEventDto NewEvent(
Guid eventId,
string? deviceId = null,
string? clientId = null,
string? userId = null,
string? collaborateurId = null) => new()
{
EventId = eventId,
EventCode = "OMK-MOB-APP-001",
TimestampUtc = DateTimeOffset.UtcNow,
Level = "Info",
Priority = "P3",
DeviceId = deviceId,
ClientId = clientId,
UserId = userId,
CollaborateurId = collaborateurId
};
private static RemoteDiagnosticRecord? RoundTrip(RemoteDiagnosticRecord record)
{
string json = DiagnosticTestData.SerializeLine(record);
return JsonSerializer.Deserialize<RemoteDiagnosticRecord>(json, JsonOptions);
}
private async Task<string> WriteAsync(
DiagnosticEventDto evt,
DiagnosticActorKey actorKey,
DiagnosticDeploymentContext? context)
{
var store = new RemoteDiagnosticIngestionStore(new RemoteDiagnosticOptions { Directory = _dir });
var batch = new DiagnosticBatchDto { Events = [evt] };
await store.StoreAsync(batch, actorKey, "trace-id", DateTimeOffset.UtcNow, CancellationToken.None, context);
return ReadAllTextShared(Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl")));
}
private static string CreateTempDirectory()
{
string dir = Path.Combine(Path.GetTempPath(), "omika-jsonl-context-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
return dir;
}
private static string ReadAllTextShared(string path)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
private static void DeleteDirectory(string dir)
{
try
{
if (Directory.Exists(dir))
{
Directory.Delete(dir, recursive: true);
}
}
catch
{
// Best-effort.
}
}
}