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,168 @@
using Omika.Diagnostics.Models;
using Omika.Diagnostics.Queries;
using Omika.Diagnostics.Tests.Fakes;
namespace Omika.Diagnostics.Tests;
/// <summary>
/// Tâche 4 (extension) — Séries par client et classement des licences en erreur.
/// Contrats couverts : <c>CountByTimeBucketAndClientAsync</c> (répartition par client,
/// NULL/vides regroupés, ErrorsOnly) et <c>CountLicensesByErrorsAsync</c> (classement
/// par nombre d'erreurs, licences manquantes exclues, limite). Tests sur le
/// <see cref="FakeDiagnosticEventRepository"/> (in-memory) ; l'implémentation PostgreSQL
/// exécute les mêmes contrats via <c>GROUP BY date_trunc(...), NULLIF(TRIM(client_code),'')</c>
/// et <c>GROUP BY NULLIF(TRIM(license_number),'')</c>.
/// </summary>
public class DiagnosticClientSeriesTests
{
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);
private static DiagnosticEventRecord Ev(
DateTimeOffset? ts = null,
string? clientCode = null,
string? licenseNumber = null,
string level = "Info") =>
new()
{
EventId = Guid.NewGuid(),
EventCode = "TEST",
TimestampUtc = ts ?? From.AddMinutes(1),
ClientCode = clientCode,
LicenseNumber = licenseNumber,
Level = level,
Priority = "P2",
Source = DiagnosticSources.Mobile,
IngestedAtUtc = DateTimeOffset.UtcNow
};
private static DiagnosticAggregateQuery All() => new() { FromUtc = From, ToUtc = To };
// --------------------------------------------- série par client : répartition ----
[Fact]
public async Task CountByTimeBucketAndClient_SplitsByClient()
{
var repo = new FakeDiagnosticEventRepository();
var t = new DateTimeOffset(2025, 1, 1, 10, 0, 0, TimeSpan.Zero);
await repo.InsertBatchAsync(new[]
{
Ev(t, clientCode: "BATIK.01"),
Ev(t.AddMinutes(5), clientCode: "BATIK.01"),
Ev(t.AddMinutes(10), clientCode: "AUTRE.02")
});
IReadOnlyList<DiagnosticBucketClientCount> result = await repo.CountByTimeBucketAndClientAsync(
DiagnosticTimeBucket.Hour, All());
Assert.Equal(2, result.Count);
Assert.All(result, r => Assert.Equal(new DateTimeOffset(2025, 1, 1, 10, 0, 0, TimeSpan.Zero), r.BucketUtc));
Assert.Contains(result, r => r.ClientCode == "BATIK.01" && r.Count == 2);
Assert.Contains(result, r => r.ClientCode == "AUTRE.02" && r.Count == 1);
}
// ---------------------------------------- série par client : client manquant ----
[Fact]
public async Task CountByTimeBucketAndClient_NullAndBlankClients_GroupedUnderNull()
{
var repo = new FakeDiagnosticEventRepository();
var t = new DateTimeOffset(2025, 1, 1, 10, 0, 0, TimeSpan.Zero);
await repo.InsertBatchAsync(new[]
{
Ev(t, clientCode: null),
Ev(t.AddMinutes(5), clientCode: " "), // espaces → groupe « non identifié »
Ev(t.AddMinutes(10), clientCode: "BATIK.01")
});
IReadOnlyList<DiagnosticBucketClientCount> result = await repo.CountByTimeBucketAndClientAsync(
DiagnosticTimeBucket.Hour, All());
Assert.Equal(2, result.Count);
Assert.Contains(result, r => r.ClientCode is null && r.Count == 2);
Assert.Contains(result, r => r.ClientCode == "BATIK.01" && r.Count == 1);
}
// ------------------------------------------- série par client : ErrorsOnly ----
[Fact]
public async Task CountByTimeBucketAndClient_ErrorsOnly_OnlyErrorLevels()
{
var repo = new FakeDiagnosticEventRepository();
var t = new DateTimeOffset(2025, 1, 1, 10, 0, 0, TimeSpan.Zero);
await repo.InsertBatchAsync(new[]
{
Ev(t, clientCode: "A", level: "Error"),
Ev(t.AddMinutes(5), clientCode: "A", level: "Info"),
Ev(t.AddMinutes(10), clientCode: "B", level: "critical")
});
IReadOnlyList<DiagnosticBucketClientCount> result = await repo.CountByTimeBucketAndClientAsync(
DiagnosticTimeBucket.Hour,
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, ErrorsOnly = true });
Assert.Equal(2, result.Count);
Assert.Contains(result, r => r.ClientCode == "A" && r.Count == 1);
Assert.Contains(result, r => r.ClientCode == "B" && r.Count == 1);
}
// ------------------------------------------ licences en erreur : classement ----
[Fact]
public async Task CountLicensesByErrors_RanksByErrorCount()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev(From.AddMinutes(1), licenseNumber: "L-1", level: "Error"),
Ev(From.AddMinutes(2), licenseNumber: "L-1", level: "Critical"),
Ev(From.AddMinutes(3), licenseNumber: "L-2", level: "Error"),
Ev(From.AddMinutes(4), licenseNumber: "L-1", level: "Info"), // non-erreur → exclu
Ev(From.AddMinutes(5), licenseNumber: "L-2", level: "Info")
});
IReadOnlyList<DiagnosticLicenseErrorCount> result = await repo.CountLicensesByErrorsAsync(All());
Assert.Equal(2, result.Count);
Assert.Equal("L-1", result[0].LicenseNumber);
Assert.Equal(2, result[0].ErrorCount);
Assert.Equal("L-2", result[1].LicenseNumber);
Assert.Equal(1, result[1].ErrorCount);
}
// ----------------------------------- licences en erreur : licences manquantes ----
[Fact]
public async Task CountLicensesByErrors_ExcludesMissingLicenses()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev(From.AddMinutes(1), licenseNumber: null, level: "Error"),
Ev(From.AddMinutes(2), licenseNumber: " ", level: "Error"),
Ev(From.AddMinutes(3), licenseNumber: "L-1", level: "Error")
});
IReadOnlyList<DiagnosticLicenseErrorCount> result = await repo.CountLicensesByErrorsAsync(All());
DiagnosticLicenseErrorCount row = Assert.Single(result);
Assert.Equal("L-1", row.LicenseNumber);
Assert.Equal(1, row.ErrorCount);
}
// ------------------------------------------- licences en erreur : limite ----
[Fact]
public async Task CountLicensesByErrors_RespectsLimit()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev(From.AddMinutes(1), licenseNumber: "L-1", level: "Error"),
Ev(From.AddMinutes(2), licenseNumber: "L-2", level: "Error"),
Ev(From.AddMinutes(3), licenseNumber: "L-2", level: "Error"),
Ev(From.AddMinutes(4), licenseNumber: "L-3", level: "Error")
});
IReadOnlyList<DiagnosticLicenseErrorCount> result = await repo.CountLicensesByErrorsAsync(
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, Limit = 2 });
Assert.Equal(2, result.Count);
Assert.Equal("L-2", result[0].LicenseNumber);
Assert.Equal("L-1", result[1].LicenseNumber); // ex-aequo 1 erreur : ordre alphabétique
}
}