using Microsoft.AspNetCore.Mvc;
using Omika.Diagnostics.Models;
using Omika.Diagnostics.Queries;
using Omika.Diagnostics.Tests.Fakes;
using Omika.Diagnostics.Web.Pages;
namespace Omika.Diagnostics.Tests;
///
/// T17-D — Contexte Client / Licence / Appareil / Application : formatage d'affichage
/// (NULL historique → « — »), détail diagnostic complet, filtres Licence/Appareil,
/// pagination, et non-régression sécurité (aucun identifiant brut exposé).
///
public class DiagnosticDeviceContextUiTests
{
// ------------------------------------------------ formatage -------------------------
[Fact]
public void Value_NullOrEmpty_RendersEmDash()
{
Assert.Equal("—", UiDeviceContext.Value(null));
Assert.Equal("—", UiDeviceContext.Value(""));
Assert.Equal("—", UiDeviceContext.Value(" "));
Assert.Equal("Android", UiDeviceContext.Value(" Android "));
}
[Theory]
[InlineData(1080, 2400, "1080 × 2400 px")]
[InlineData(null, 2400, "—")]
[InlineData(1080, null, "—")]
[InlineData(0, 2400, "—")]
[InlineData(1080, 0, "—")]
public void Resolution_RequiresBothDimensions(int? w, int? h, string expected)
=> Assert.Equal(expected, UiDeviceContext.Resolution(w, h, "px"));
[Fact]
public void Scale_FormatsWithoutFloatingNoise()
{
Assert.Equal("2.75×", UiDeviceContext.Scale(2.75));
Assert.Equal("3×", UiDeviceContext.Scale(3.0));
Assert.Equal("—", UiDeviceContext.Scale(null));
Assert.Equal("—", UiDeviceContext.Scale(0));
}
[Fact]
public void DeviceIdentity_CombinesManufacturerAndModel()
{
Assert.Equal("Xiaomi · 23113RKC6G", UiDeviceContext.DeviceIdentity("Xiaomi", "23113RKC6G"));
Assert.Equal("Apple", UiDeviceContext.DeviceIdentity("Apple", null));
Assert.Equal("—", UiDeviceContext.DeviceIdentity(null, null));
}
[Fact]
public void PlatformOs_CombinesOsAndVersion()
{
Assert.Equal("Android 16", UiDeviceContext.PlatformOs("Android", "16"));
Assert.Equal("iOS 18.6", UiDeviceContext.PlatformOs("iOS", "18.6"));
Assert.Equal("—", UiDeviceContext.PlatformOs(null, "16"));
}
[Fact]
public void VersionBuild_ShowsBuildWhenPresent()
{
Assert.Equal("1.4.1 (10401)", UiDeviceContext.VersionBuild("1.4.1", "10401"));
Assert.Equal("1.4.1", UiDeviceContext.VersionBuild("1.4.1", null));
Assert.Equal("—", UiDeviceContext.VersionBuild(null, "10401"));
}
[Fact]
public void Short_TruncatesAndKeepsFullViaTitle()
{
Assert.Equal("—", UiDeviceContext.Short(null));
Assert.Equal("f71919f4f9974d70…", UiDeviceContext.Short("f71919f4f9974d70967014fbbefabf9d", 16));
Assert.Equal("abc", UiDeviceContext.Short("abc", 16)); // court → intact
}
// ------------------------------------------------ détail complet (§41) -------------
[Fact]
public async Task Detail_LoadsFullDeviceAndLicenseContext()
{
var repo = new FakeDiagnosticEventRepository();
Guid traceId = Guid.NewGuid();
Guid operationId = Guid.NewGuid();
Guid sessionId = Guid.NewGuid();
DiagnosticEventRecord record = DeviceRecord(42, traceId: traceId, operationId: operationId, sessionId: sessionId);
await repo.InsertBatchAsync(new[] { record });
var model = new DiagnosticsDetailModel(repo);
IActionResult result = await model.OnGetAsync(42);
Assert.IsType(result);
Assert.NotNull(model.Event);
Assert.Equal("CLIENT-042", model.Event!.ClientCode);
Assert.Equal("LIC-000842", model.Event.LicenseNumber);
Assert.Equal("hash-test", model.Event.DeviceKey);
Assert.Equal(1, model.Event.DeviceKeyVersion);
Assert.Equal("Android", model.Event.Platform);
Assert.Equal("Android", model.Event.OperatingSystem);
Assert.Equal("16", model.Event.OperatingSystemVersion);
Assert.Equal("Xiaomi", model.Event.DeviceManufacturer);
Assert.Equal("23113RKC6G", model.Event.DeviceModel);
Assert.Equal("1.4.1", model.Event.AppVersion);
Assert.Equal("10401", model.Event.AppBuild);
Assert.Equal(1080, model.Event.ScreenWidth);
Assert.Equal(2400, model.Event.ScreenHeight);
Assert.Equal(393, model.Event.LogicalWidth);
Assert.Equal(873, model.Event.LogicalHeight);
Assert.Equal(2.75, model.Event.DisplayScale);
Assert.Equal(traceId, model.Event.TraceId);
Assert.Equal(operationId, model.Event.OperationId);
Assert.Equal(sessionId, model.Event.SessionId);
}
// ------------------------------------------------ anciennes données NULL (§42) ------
[Fact]
public void OldData_WithAllNulls_FormatsWithoutBreaking()
{
DiagnosticEventRecord legacy = DiagnosticTestData.Event(Guid.NewGuid());
legacy.ClientCode = null;
legacy.LicenseNumber = null;
legacy.DeviceKey = null;
legacy.DeviceManufacturer = null;
legacy.DeviceModel = null;
legacy.OperatingSystem = null;
legacy.ScreenWidth = null;
legacy.ScreenHeight = null;
legacy.LogicalWidth = null;
legacy.LogicalHeight = null;
legacy.DisplayScale = null;
Assert.Equal("Non renseigné", UiDeploymentContext.Value(legacy.ClientCode));
Assert.Equal("—", UiDeviceContext.Value(legacy.LicenseNumber));
Assert.Equal("—", UiDeviceContext.Resolution(legacy.ScreenWidth, legacy.ScreenHeight, "px"));
Assert.Equal("—", UiDeviceContext.Resolution(legacy.LogicalWidth, legacy.LogicalHeight));
Assert.Equal("—", UiDeviceContext.Scale(legacy.DisplayScale));
Assert.Null(UiDeviceContext.CompactDeviceOrNull(legacy));
}
// ------------------------------------------------ filtre licence (§43) -------------
[Fact]
public async Task LicenseFilter_ReturnsOnlyMatchingEvents()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
LicenseRecord(1, "LIC-A"),
LicenseRecord(2, "LIC-A"),
LicenseRecord(3, "LIC-B")
});
var model = new DiagnosticsModel(repo) { LicenseNumber = "LIC-A" };
await model.OnGetAsync();
Assert.Equal(2, model.Items.Count);
Assert.All(model.Items, e => Assert.Equal("LIC-A", e.LicenseNumber));
}
[Fact]
public void Route_PreservesLicenseAndDeviceFilters()
{
var model = new DiagnosticsModel(new FakeDiagnosticEventRepository())
{
LicenseNumber = "LIC-A",
DeviceKey = "hash-test"
};
Dictionary route = model.Route(2);
Assert.Equal("2", route["pageNumber"]);
Assert.Equal("LIC-A", route["licenseNumber"]);
Assert.Equal("hash-test", route["deviceKey"]);
}
// ------------------------------------------------ filtre appareil (§44) ------------
[Fact]
public async Task DeviceKeyFilter_ReturnsOnlyMatchingDevice()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
DeviceKeyRecord(1, "hash-test"),
DeviceKeyRecord(2, "hash-test"),
DeviceKeyRecord(3, "hash-other")
});
var model = new DiagnosticsModel(repo) { DeviceKey = "hash-test" };
await model.OnGetAsync();
Assert.Equal(2, model.Items.Count);
Assert.All(model.Items, e => Assert.Equal("hash-test", e.DeviceKey));
Assert.DoesNotContain(model.Items, e => e.DeviceKey == "hash-other");
}
// ------------------------------------------------ filtres analytiques T17-E -------
[Fact]
public async Task ManufacturerAndOsFilters_ReturnOnlyMatchingEvents()
{
var repo = new FakeDiagnosticEventRepository();
DiagnosticEventRecord samsung = DiagnosticTestData.Event(Guid.NewGuid());
samsung.Id = 1;
samsung.DeviceManufacturer = "Samsung";
samsung.OperatingSystem = "Android";
DiagnosticEventRecord apple = DiagnosticTestData.Event(Guid.NewGuid());
apple.Id = 2;
apple.DeviceManufacturer = "Apple";
apple.OperatingSystem = "iOS";
await repo.InsertBatchAsync(new[] { samsung, apple });
var model = new DiagnosticsModel(repo) { Manufacturer = "Samsung", OperatingSystem = "Android" };
await model.OnGetAsync();
DiagnosticEventRecord record = Assert.Single(model.Items);
Assert.Equal("Samsung", record.DeviceManufacturer);
}
// ------------------------------------------------ sécurité UI (§45) ----------------
[Fact]
public void DiagnosticEventRecord_ExposesNoRawIdentityOrSecrets()
{
string[] names = typeof(DiagnosticEventRecord).GetProperties().Select(p => p.Name).ToArray();
Assert.DoesNotContain("DeviceId", names);
Assert.DoesNotContain("UserId", names);
Assert.DoesNotContain("CollaborateurId", names);
Assert.DoesNotContain("Token", names);
Assert.DoesNotContain("HmacKey", names);
Assert.DoesNotContain("LI_PKID", names);
Assert.DoesNotContain("LI_CleAPI", names);
Assert.DoesNotContain("CL_CleAPIInstall", names);
// DeviceKey (pseudonymisé) est autorisé.
Assert.Contains("DeviceKey", names);
}
// ------------------------------------------------ fabriques ------------------------
private static DiagnosticEventRecord DeviceRecord(
long id,
Guid? traceId = null,
Guid? operationId = null,
Guid? sessionId = null)
{
DiagnosticEventRecord record = DiagnosticTestData.Event(Guid.NewGuid());
record.Id = id;
record.ClientCode = "CLIENT-042";
record.LicenseNumber = "LIC-000842";
record.DeviceKey = "hash-test";
record.DeviceKeyVersion = 1;
record.Platform = "Android";
record.OperatingSystem = "Android";
record.OperatingSystemVersion = "16";
record.DeviceManufacturer = "Xiaomi";
record.DeviceModel = "23113RKC6G";
record.AppVersion = "1.4.1";
record.AppBuild = "10401";
record.ScreenWidth = 1080;
record.ScreenHeight = 2400;
record.LogicalWidth = 393;
record.LogicalHeight = 873;
record.DisplayScale = 2.75;
record.TraceId = traceId;
record.OperationId = operationId;
record.SessionId = sessionId;
return record;
}
private static DiagnosticEventRecord LicenseRecord(long id, string licenseNumber)
{
DiagnosticEventRecord record = DiagnosticTestData.Event(Guid.NewGuid());
record.Id = id;
record.LicenseNumber = licenseNumber;
return record;
}
private static DiagnosticEventRecord DeviceKeyRecord(long id, string deviceKey)
{
DiagnosticEventRecord record = DiagnosticTestData.Event(Guid.NewGuid());
record.Id = id;
record.DeviceKey = deviceKey;
return record;
}
}