169 lines
7.2 KiB
C#
169 lines
7.2 KiB
C#
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
|
|
}
|
|
}
|