using Omika.Core.Observability; using Omika.Diagnostics.Models; using Omika.Diagnostics.Queries; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// T18 : statistiques d'USAGE (événementiel) — vues/écrans, appareils distincts, /// sessions distinctes, fonctionnalités. Unité = événements ScreenView/FeatureUse, /// jamais la dernière observation d'un appareil (§54). /// public class DiagnosticUsageTests { private static readonly string ScreenView = TelemetryEventCodes.MobileScreenView.Value!; private static readonly string FeatureUse = TelemetryEventCodes.MobileFeatureUse.Value!; private static readonly DateTimeOffset From = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); private static readonly DateTimeOffset To = new(2025, 2, 1, 0, 0, 0, TimeSpan.Zero); [Fact] public async Task GetScreenUsage_CountsViewsDistinctDevicesDistinctSessions() { var repo = new FakeDiagnosticEventRepository(); Guid s1 = Guid.NewGuid(); Guid s2 = Guid.NewGuid(); Guid s3 = Guid.NewGuid(); await repo.InsertBatchAsync( [ Screen("AGENDA", "D-A", s1, 1), Screen("AGENDA", "D-A", s1, 2), Screen("AGENDA", "D-A", s2, 3), Screen("AGENDA", "D-B", s1, 4), Screen("AGENDA", "D-B", s3, 5), Screen("AGENDA", "D-B", s3, 6), Screen("AGENDA", "D-B", s3, 7), Screen("AGENDA", "D-B", s3, 8), Screen("AGENDA", "D-B", s3, 9), Screen("AGENDA", "D-B", s3, 10) ]); IReadOnlyList result = await repo.GetScreenUsageAsync(Query()); ScreenUsageRecord item = Assert.Single(result); Assert.Equal("AGENDA", item.ScreenCode); Assert.Equal(10, item.Views); Assert.Equal(2, item.DistinctDevices); Assert.Equal(3, item.DistinctSessions); } [Fact] public async Task GetScreenUsage_DeviceKeyNull_IgnoredInDistinctDevices() { var repo = new FakeDiagnosticEventRepository(); Guid session = Guid.NewGuid(); var events = new List(); for (int i = 0; i < 5; i++) { events.Add(Screen("AGENDA", null, session, 1 + i)); } events.Add(Screen("AGENDA", "D-A", session, 10)); events.Add(Screen("AGENDA", "D-A", session, 11)); events.Add(Screen("AGENDA", "D-A", session, 12)); events.Add(Screen("AGENDA", "D-A", session, 13)); events.Add(Screen("AGENDA", "D-A", session, 14)); await repo.InsertBatchAsync(events); ScreenUsageRecord item = Assert.Single(await repo.GetScreenUsageAsync(Query())); Assert.Equal(10, item.Views); Assert.Equal(1, item.DistinctDevices); } [Fact] public async Task GetScreenUsage_SessionNull_IgnoredInDistinctSessions() { var repo = new FakeDiagnosticEventRepository(); Guid session = Guid.NewGuid(); await repo.InsertBatchAsync( [ Screen("AGENDA", "D-A", null, 1), Screen("AGENDA", "D-A", null, 2), Screen("AGENDA", "D-A", session, 3) ]); ScreenUsageRecord item = Assert.Single(await repo.GetScreenUsageAsync(Query())); Assert.Equal(3, item.Views); Assert.Equal(1, item.DistinctSessions); } [Fact] public async Task GetScreenUsage_NormalizesCaseAndTrims() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync( [ Screen("AGENDA", "D-A", Guid.NewGuid(), 1), Screen("agenda", "D-A", Guid.NewGuid(), 2), Screen(" AGENDA ", "D-A", Guid.NewGuid(), 3), Screen("SITE_DETAIL", "D-A", Guid.NewGuid(), 4) ]); IReadOnlyList result = await repo.GetScreenUsageAsync(Query()); Assert.Equal(2, result.Count); Assert.Equal("AGENDA", result[0].ScreenCode); Assert.Equal(3, result[0].Views); Assert.Equal("SITE_DETAIL", result[1].ScreenCode); } [Fact] public async Task GetScreenUsage_ExcludesNullOrEmptyScreenCode() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync( [ Screen(null, "D-A", Guid.NewGuid(), 1), Screen("", "D-A", Guid.NewGuid(), 2), Screen("AGENDA", "D-A", Guid.NewGuid(), 3) ]); ScreenUsageRecord item = Assert.Single(await repo.GetScreenUsageAsync(Query())); Assert.Equal("AGENDA", item.ScreenCode); Assert.Equal(1, item.Views); } [Fact] public async Task GetScreenUsage_AppliesFilters() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync( [ Screen("AGENDA", "D-A", Guid.NewGuid(), 1, client: "CLIENT-A", license: "LIC-1", platform: "Android", os: "Android", version: "1.4.0"), Screen("AGENDA", "D-B", Guid.NewGuid(), 2, client: "CLIENT-B", license: "LIC-2", platform: "iOS", os: "iOS", version: "1.4.1"), Screen("SITE_DETAIL", "D-A", Guid.NewGuid(), 3, client: "CLIENT-A", license: "LIC-1", platform: "Android", os: "Android", version: "1.4.0") ]); ScreenUsageRecord byClient = Assert.Single(await repo.GetScreenUsageAsync(Query(clientCode: "CLIENT-B"))); Assert.Equal("AGENDA", byClient.ScreenCode); // LIC-1 est observée sur AGENDA ET SITE_DETAIL (même DeviceKey, deux écrans). IReadOnlyList byLicense = await repo.GetScreenUsageAsync(Query(licenseNumber: "LIC-1")); Assert.Equal(2, byLicense.Count); Assert.Contains(byLicense, s => s.ScreenCode == "AGENDA" && s.Views == 1); Assert.Contains(byLicense, s => s.ScreenCode == "SITE_DETAIL" && s.Views == 1); ScreenUsageRecord byPlatform = Assert.Single(await repo.GetScreenUsageAsync(Query(platform: "iOS"))); Assert.Equal("AGENDA", byPlatform.ScreenCode); ScreenUsageRecord byScreen = Assert.Single(await repo.GetScreenUsageAsync(Query(screenCode: "SITE_DETAIL"))); Assert.Equal("SITE_DETAIL", byScreen.ScreenCode); Assert.Equal(1, byScreen.Views); } [Fact] public async Task GetScreenUsage_LicenseReassigned_CountsBothDevicesWithoutMerging() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync( [ Screen("AGENDA", "D-A", Guid.NewGuid(), 1, license: "LIC-REASSIGNED"), Screen("AGENDA", "D-B", Guid.NewGuid(), 2, license: "LIC-REASSIGNED") ]); IReadOnlyList result = await repo.GetScreenUsageAsync(Query(licenseNumber: "LIC-REASSIGNED")); ScreenUsageRecord item = Assert.Single(result); Assert.Equal("AGENDA", item.ScreenCode); Assert.Equal(2, item.Views); Assert.Equal(2, item.DistinctDevices); // jamais fusionné en 1 appareil } [Fact] public async Task GetFeatureUsage_CountsUsesAndDistinctDevices() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync( [ Feature("EMAIL_SEND", "D-A", 1), Feature("EMAIL_SEND", "D-A", 2), Feature("EMAIL_SEND", "D-B", 3), Feature("REPORT_GENERATE", "D-A", 4) ]); IReadOnlyList result = await repo.GetFeatureUsageAsync(Query()); Assert.Equal(2, result.Count); Assert.Equal("EMAIL_SEND", result[0].FeatureCode); Assert.Equal(3, result[0].Uses); Assert.Equal(2, result[0].DistinctDevices); Assert.Equal("REPORT_GENERATE", result[1].FeatureCode); Assert.Equal(1, result[1].Uses); } [Fact] public async Task GetFeatureUsage_FiltersByFeatureCode() { var repo = new FakeDiagnosticEventRepository(); await repo.InsertBatchAsync( [ Feature("EMAIL_SEND", "D-A", 1), Feature("REPORT_GENERATE", "D-A", 2) ]); FeatureUsageRecord item = Assert.Single(await repo.GetFeatureUsageAsync(Query(featureCode: "REPORT_GENERATE"))); Assert.Equal("REPORT_GENERATE", item.FeatureCode); Assert.Equal(1, item.Uses); } [Fact] public void UsageTelemetryQuery_Validate_RequiresPeriod() { Assert.Throws(() => new UsageTelemetryQuery().Validate()); Assert.Throws(() => new UsageTelemetryQuery { FromUtc = To, ToUtc = From }.Validate()); } private static DiagnosticEventRecord Screen( string? screenCode, string? deviceKey, Guid? sessionId, long id, string? client = null, string? license = null, string? platform = null, string? os = null, string? version = null) => new() { Id = id, EventId = Guid.NewGuid(), EventCode = ScreenView, TimestampUtc = From.AddMinutes(id), ScreenCode = screenCode, DeviceKey = deviceKey, SessionId = sessionId, ClientCode = client, LicenseNumber = license, Platform = platform, OperatingSystem = os, AppVersion = version, Source = "mobile", IngestedAtUtc = DateTimeOffset.UtcNow }; private static DiagnosticEventRecord Feature(string featureCode, string deviceKey, long id) => new() { Id = id, EventId = Guid.NewGuid(), EventCode = FeatureUse, TimestampUtc = From.AddMinutes(id), FeatureCode = featureCode, DeviceKey = deviceKey, Source = "mobile", IngestedAtUtc = DateTimeOffset.UtcNow }; private static UsageTelemetryQuery Query( string? clientCode = null, string? licenseNumber = null, string? platform = null, string? screenCode = null, string? featureCode = null) => new() { FromUtc = From, ToUtc = To, ClientCode = clientCode, LicenseNumber = licenseNumber, Platform = platform, ScreenCode = screenCode, FeatureCode = featureCode, Limit = 50 }; }