Premiere commit - Omika
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user