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; namespace Omika.Diagnostics.Tests; /// /// Tâche 17-B — Mapping Worker + transport JSONL des dimensions appareil/licence : /// /// Le mapper ne perd plus OperatingSystem/OSVersion/DeviceModel/DeviceManufacturer. /// LicenseNumber/DeviceKey/DeviceKeyVersion + AppBuild + Screen*/DisplayScale sont mappés. /// Le store écrit les champs serveur (licenseNumber/deviceKey/deviceKeyVersion) en JSONL /// et continue d'épurer le DeviceId brut. /// Toutes les valeurs null restent null (compatibilité anciennes lignes). /// /// Aucun PostgreSQL réel requis. /// public class DiagnosticDeviceLicenseMapperTests : IDisposable { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); private readonly string _dir = CreateTempDirectory(); public void Dispose() => DeleteDirectory(_dir); // --------------------------- M1 : mapper conserve toutes les dimensions ------ [Fact] public void M1_Mapper_PreservesDeviceAndLicenseDimensions() { RemoteDiagnosticRecord record = NewRecord( Guid.NewGuid(), licenseNumber: "LIC-123", deviceKey: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", deviceKeyVersion: 1, manufacturer: "Xiaomi", model: "POCO Demo", os: "Android", osVersion: "16", appBuild: "1410", screenWidth: 1080, screenHeight: 2400, logicalWidth: 360, logicalHeight: 800, displayScale: 3.0); DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile"); Assert.NotNull(mapped); Assert.Equal("LIC-123", mapped!.LicenseNumber); Assert.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", mapped.DeviceKey); Assert.Equal(1, mapped.DeviceKeyVersion); Assert.Equal("Xiaomi", mapped.DeviceManufacturer); Assert.Equal("POCO Demo", mapped.DeviceModel); Assert.Equal("Android", mapped.OperatingSystem); Assert.Equal("16", mapped.OperatingSystemVersion); Assert.Equal("1410", mapped.AppBuild); Assert.Equal(1080, mapped.ScreenWidth); Assert.Equal(2400, mapped.ScreenHeight); Assert.Equal(360, mapped.LogicalWidth); Assert.Equal(800, mapped.LogicalHeight); Assert.Equal(3.0, mapped.DisplayScale); } // --------------------------- M2 : toutes valeurs null → null ---------------- [Fact] public void M2_Mapper_AllNull_LeavesAllDimensionsNull() { RemoteDiagnosticRecord record = NewRecord(Guid.NewGuid()); DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile"); Assert.NotNull(mapped); Assert.Null(mapped!.LicenseNumber); Assert.Null(mapped.DeviceKey); Assert.Null(mapped.DeviceKeyVersion); Assert.Null(mapped.DeviceManufacturer); Assert.Null(mapped.DeviceModel); Assert.Null(mapped.OperatingSystem); Assert.Null(mapped.OperatingSystemVersion); Assert.Null(mapped.AppBuild); Assert.Null(mapped.ScreenWidth); Assert.Null(mapped.ScreenHeight); Assert.Null(mapped.LogicalWidth); Assert.Null(mapped.LogicalHeight); Assert.Null(mapped.DisplayScale); } // --------------------------- M3 : JSONL conserve les champs serveur --------- [Fact] public async Task M3_Store_WritesServerIdentityFields_AndPreservesOnReadBack() { var deviceKey = new DiagnosticDeviceKey("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 1); DiagnosticEventDto evt = NewEvent(Guid.NewGuid(), deviceId: "device-raw-secret"); string content = await WriteAsync(evt, deviceKey, licenseNumber: "LIC-456"); using JsonDocument doc = JsonDocument.Parse(content); Assert.Equal("LIC-456", doc.RootElement.GetProperty("licenseNumber").GetString()); Assert.Equal(deviceKey.Value, doc.RootElement.GetProperty("deviceKey").GetString()); Assert.Equal(1, doc.RootElement.GetProperty("deviceKeyVersion").GetInt32()); RemoteDiagnosticRecord? record = JsonSerializer.Deserialize(content, JsonOptions); Assert.NotNull(record); DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile"); Assert.NotNull(mapped); Assert.Equal("LIC-456", mapped!.LicenseNumber); Assert.Equal(deviceKey.Value, mapped.DeviceKey); Assert.Equal(1, mapped.DeviceKeyVersion); } // --------------------------- M4 : DeviceId brut jamais persisté --------------- [Fact] public async Task M4_Store_NeverWritesRawDeviceId() { DiagnosticEventDto evt = NewEvent(Guid.NewGuid(), deviceId: "device-raw-secret"); string content = await WriteAsync(evt, DiagnosticDeviceKey.Empty, licenseNumber: null); Assert.DoesNotContain("\"deviceId\"", content, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("device-raw-secret", content, StringComparison.Ordinal); } // --------------------------- M5 : absence serveur → champs omis -------------- [Fact] public async Task M5_Store_AbsentIdentity_OmitsServerFields() { string content = await WriteAsync(NewEvent(Guid.NewGuid()), DiagnosticDeviceKey.Empty, licenseNumber: null); using JsonDocument doc = JsonDocument.Parse(content); Assert.False(doc.RootElement.TryGetProperty("licenseNumber", out _)); Assert.False(doc.RootElement.TryGetProperty("deviceKey", out _)); Assert.False(doc.RootElement.TryGetProperty("deviceKeyVersion", out _)); } // --------------------------- M6 : dimensions écran null → transport null ----- [Fact] public void M6_Mapper_NullScreenDimensions_RemainNull() { RemoteDiagnosticRecord record = NewRecord(Guid.NewGuid(), screenWidth: null, displayScale: null); DiagnosticEventRecord? mapped = DiagnosticEventRecordMapper.ToRecord(record, "mobile"); Assert.NotNull(mapped); Assert.Null(mapped!.ScreenWidth); Assert.Null(mapped.DisplayScale); } // --------------------------- Helpers ------------------------------------------ private static RemoteDiagnosticRecord NewRecord( Guid eventId, string? licenseNumber = null, string? deviceKey = null, int? deviceKeyVersion = null, string? manufacturer = null, string? model = null, string? os = null, string? osVersion = null, string? appBuild = null, int? screenWidth = null, int? screenHeight = null, int? logicalWidth = null, int? logicalHeight = null, double? displayScale = null) { DateTimeOffset ts = DateTimeOffset.UtcNow; return new RemoteDiagnosticRecord { ReceivedAtUtc = ts, BatchId = Guid.NewGuid(), IngestionTraceId = Guid.NewGuid().ToString("D"), LicenseNumber = licenseNumber, DeviceKey = deviceKey, DeviceKeyVersion = deviceKeyVersion, Event = new DiagnosticEventDto { EventId = eventId, EventCode = "OMK-MOB-APP-001", TimestampUtc = ts, Level = "Info", Priority = "P3", DeviceManufacturer = manufacturer, DeviceModel = model, OperatingSystem = os, OperatingSystemVersion = osVersion, AppBuild = appBuild, ScreenWidth = screenWidth, ScreenHeight = screenHeight, LogicalWidth = logicalWidth, LogicalHeight = logicalHeight, DisplayScale = displayScale } }; } private static DiagnosticEventDto NewEvent(Guid eventId, string? deviceId = null) => new() { EventId = eventId, EventCode = "OMK-MOB-APP-001", TimestampUtc = DateTimeOffset.UtcNow, Level = "Info", Priority = "P3", DeviceId = deviceId }; private async Task WriteAsync( DiagnosticEventDto evt, DiagnosticDeviceKey deviceKey, string? licenseNumber) { var store = new RemoteDiagnosticIngestionStore(new RemoteDiagnosticOptions { Directory = _dir }); var batch = new DiagnosticBatchDto { Events = [evt] }; await store.StoreAsync( batch, DiagnosticActorKey.Empty, "trace-id", DateTimeOffset.UtcNow, CancellationToken.None, deploymentContext: null, deviceKey: deviceKey, licenseNumber: licenseNumber); return ReadAllTextShared(Assert.Single(Directory.EnumerateFiles(_dir, "*.jsonl"))); } private static string CreateTempDirectory() { string dir = Path.Combine(Path.GetTempPath(), "omika-device-license-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. } } }