Premiere commit - Omika
This commit is contained in:
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user