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; } }