Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,279 @@
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
};
}