280 lines
10 KiB
C#
280 lines
10 KiB
C#
using Omika.Core.Observability;
|
|
using Omika.Diagnostics.Models;
|
|
using Omika.Diagnostics.Queries;
|
|
using Omika.Diagnostics.Tests.Fakes;
|
|
|
|
namespace Omika.Diagnostics.Tests;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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<ScreenUsageRecord> 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<DiagnosticEventRecord>();
|
|
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<ScreenUsageRecord> 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<ScreenUsageRecord> 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<ScreenUsageRecord> 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<FeatureUsageRecord> 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<ArgumentException>(() => new UsageTelemetryQuery().Validate());
|
|
Assert.Throws<ArgumentException>(() => 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
|
|
};
|
|
}
|