Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Omika.Core.Observability.Identity;
|
||||
using Omika.Core.Observability.Outbox;
|
||||
using Omika.Diagnostics.Import;
|
||||
using Omika.Diagnostics.Models;
|
||||
using Omika.Diagnostics.Queries;
|
||||
using Omika.Diagnostics.Tests.Fakes;
|
||||
|
||||
namespace Omika.Diagnostics.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests d'intégration ActorKey (§19/§20) : identité authentifiée → ActorKey →
|
||||
/// événement canonique → JSONL → importeur → repository → filtre par ActorKey.
|
||||
/// Vérifie aussi qu'aucun identifiant brut n'est persisté dans les données de
|
||||
/// diagnostic (hors fixtures strictement internes au test).
|
||||
/// </summary>
|
||||
public class DiagnosticActorKeyIntegrationTests : IDisposable
|
||||
{
|
||||
private static string TestKey() => Convert.ToBase64String(new byte[32]);
|
||||
|
||||
private readonly string _dir = CreateTempDirectory();
|
||||
|
||||
public void Dispose() => DeleteDirectory(_dir);
|
||||
|
||||
[Fact]
|
||||
public async Task FullFlow_ComputesActorKey_AndPersistsIt_WithoutRawIdentity()
|
||||
{
|
||||
// Contexte : Tenant A = "BATIK.01", Collaborateur 42 (identité authentifiée).
|
||||
const string tenant = "BATIK.01";
|
||||
const string collaborateur = "42";
|
||||
|
||||
DiagnosticActorKey actorKey = new DiagnosticActorKeyProvider(TestKey()).Compute(tenant, collaborateur);
|
||||
Assert.True(actorKey.HasValue);
|
||||
|
||||
Guid eventId = Guid.NewGuid();
|
||||
var record = DiagnosticTestData.Record(
|
||||
eventId,
|
||||
"OMK-MOB-APP-001",
|
||||
actorKey: actorKey.Value,
|
||||
actorKeyVersion: actorKey.Version);
|
||||
|
||||
var source = new FakeDiagnosticJsonlSource(new[] { record });
|
||||
var repository = new FakeDiagnosticEventRepository();
|
||||
var importer = new DiagnosticImporter(source, repository, Options.Create(new DiagnosticImportOptions { BatchSize = 100 }));
|
||||
|
||||
DiagnosticImportResult result = await importer.ImportOnceAsync();
|
||||
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.Equal(1, result.ImportedCount);
|
||||
|
||||
DiagnosticEventRecord persisted = (await repository.QueryAsync(new DiagnosticEventQuery())).Items.Single();
|
||||
Assert.Equal(actorKey.Value, persisted.ActorKey);
|
||||
Assert.Equal(actorKey.Version, persisted.ActorKeyVersion);
|
||||
|
||||
// Aucun identifiant brut dans les champs persistés (le JSONL/MetadataJson
|
||||
// n'est pas scruté ici : il n'y en a pas dans ce cas de figure).
|
||||
Assert.DoesNotContain(collaborateur, persisted.EventCode, StringComparison.Ordinal);
|
||||
Assert.Null(persisted.MetadataJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Repository_FiltersByActorKey()
|
||||
{
|
||||
const string actorA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
const string actorB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
|
||||
var repository = new FakeDiagnosticEventRepository();
|
||||
await repository.InsertBatchAsync(new[]
|
||||
{
|
||||
DiagnosticTestData.Event(Guid.NewGuid(), actorKey: actorA),
|
||||
DiagnosticTestData.Event(Guid.NewGuid(), actorKey: actorB),
|
||||
DiagnosticTestData.Event(Guid.NewGuid(), actorKey: null) // système / sans utilisateur
|
||||
});
|
||||
|
||||
DiagnosticQueryResult<DiagnosticEventRecord> byA = await repository.QueryAsync(new DiagnosticEventQuery { ActorKey = actorA });
|
||||
|
||||
Assert.Single(byA.Items);
|
||||
Assert.Equal(actorA, byA.Items[0].ActorKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Store_PersistsActorKey_AndStripsRawIdentity()
|
||||
{
|
||||
var store = new RemoteDiagnosticIngestionStore(new RemoteDiagnosticOptions { Directory = _dir });
|
||||
DiagnosticActorKey actorKey = new DiagnosticActorKeyProvider(TestKey()).Compute("BATIK.01", "42");
|
||||
|
||||
var batch = new DiagnosticBatchDto
|
||||
{
|
||||
BatchId = Guid.NewGuid(),
|
||||
Events =
|
||||
[
|
||||
new DiagnosticEventDto
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventCode = "OMK-MOB-APP-001",
|
||||
TimestampUtc = DateTimeOffset.UtcNow,
|
||||
Level = "Info",
|
||||
Priority = "P3",
|
||||
// Identifiants bruts fournis par le mobile (transport) :
|
||||
DeviceId = "device-1",
|
||||
ClientId = "BATIK.01",
|
||||
UserId = "user-1",
|
||||
CollaborateurId = "42",
|
||||
Component = "télémétrie"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await store.StoreAsync(batch, actorKey, "trace-id", DateTimeOffset.UtcNow);
|
||||
|
||||
string content = ReadAllTextShared(Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl")));
|
||||
|
||||
// ActorKey + version présents dans l'enregistrement canonique.
|
||||
using (JsonDocument doc = JsonDocument.Parse(content))
|
||||
{
|
||||
JsonElement root = doc.RootElement;
|
||||
Assert.Equal(actorKey.Value, root.GetProperty("actorKey").GetString());
|
||||
Assert.Equal(actorKey.Version, root.GetProperty("actorKeyVersion").GetInt32());
|
||||
}
|
||||
|
||||
// Les identifiants bruts ne sont PAS persistés dans le JSONL (§9).
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Store_WithoutActorKey_WritesNullActorKey()
|
||||
{
|
||||
var store = new RemoteDiagnosticIngestionStore(new RemoteDiagnosticOptions { Directory = _dir });
|
||||
|
||||
await store.StoreAsync(
|
||||
new DiagnosticBatchDto
|
||||
{
|
||||
Events = [new DiagnosticEventDto { EventId = Guid.NewGuid(), EventCode = "OMK-MOB-APP-001" }]
|
||||
},
|
||||
DiagnosticActorKey.Empty,
|
||||
"trace-id",
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
string content = ReadAllTextShared(Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl")));
|
||||
using JsonDocument doc = JsonDocument.Parse(content);
|
||||
// Champs null omis (WhenWritingNull) : ActorKey absent → propriété absente.
|
||||
Assert.False(doc.RootElement.TryGetProperty("actorKey", out _));
|
||||
Assert.False(doc.RootElement.TryGetProperty("actorKeyVersion", out _));
|
||||
}
|
||||
|
||||
private static string CreateTempDirectory()
|
||||
{
|
||||
string dir = Path.Combine(Path.GetTempPath(), "omika-actor-key-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