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,313 @@
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.
}
}
}