Files
Omika/tests/Omika.Diagnostics.Tests/DiagnosticDeviceLicenseMapperTests.cs
2026-10-23 18:46:50 +02:00

256 lines
9.8 KiB
C#

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;
/// <summary>
/// Tâche 17-B — Mapping Worker + transport JSONL des dimensions appareil/licence :
/// <list type="bullet">
/// <item>Le mapper ne perd plus OperatingSystem/OSVersion/DeviceModel/DeviceManufacturer.</item>
/// <item>LicenseNumber/DeviceKey/DeviceKeyVersion + AppBuild + Screen*/DisplayScale sont mappés.</item>
/// <item>Le store écrit les champs serveur (licenseNumber/deviceKey/deviceKeyVersion) en JSONL
/// et continue d'épurer le DeviceId brut.</item>
/// <item>Toutes les valeurs null restent null (compatibilité anciennes lignes).</item>
/// </list>
/// Aucun PostgreSQL réel requis.
/// </summary>
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<RemoteDiagnosticRecord>(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<string> 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.
}
}
}