182 lines
7.2 KiB
C#
182 lines
7.2 KiB
C#
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.
|
|
}
|
|
}
|
|
}
|