Files
Omika/tests/Omika.Diagnostics.Tests/DiagnosticNormalizationTests.cs
2026-10-23 18:46:50 +02:00

157 lines
6.0 KiB
C#

using Omika.Diagnostics.Models;
using Omika.Diagnostics.Queries;
using Omika.Diagnostics.Tests.Fakes;
namespace Omika.Diagnostics.Tests;
/// <summary>
/// Tâche T14-C — Normalisation des dimensions d'agrégation (§9/§10) : les variantes de
/// casse/espaces de <c>component</c> et <c>error_code</c> ne doivent plus produire de
/// dimensions distinctes dans les statistiques. Aucune donnée persistée n'est modifiée :
/// seule la clé d'agrégation est normalisée (déterministe, côté requête).
/// </summary>
public class DiagnosticNormalizationTests
{
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(string? component = null, string? errorCode = null)
{
return new DiagnosticEventRecord
{
EventId = Guid.NewGuid(),
EventCode = "TEST",
TimestampUtc = From.AddMinutes(1),
Component = component,
ErrorCode = errorCode,
Level = "Error",
Priority = "P1",
Source = DiagnosticSources.Mobile,
IngestedAtUtc = DateTimeOffset.UtcNow
};
}
// -------------------------------------------------- T14-C Test 8 : Component --
[Fact]
public async Task Component_NormalizesCaseAndWhitespace()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev(component: "Omika.Api"),
Ev(component: "OMIKA.API"),
Ev(component: "omika.api"),
Ev(component: " Omika.Api "),
Ev(component: "Omika.Worker")
});
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
DiagnosticGroupDimension.Component,
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
// 4 variantes du même composant + 1 autre = 2 dimensions logiques.
Assert.Equal(2, result.Count);
Assert.Contains(result, g => g.Key == "Omika.Api" && g.Count == 4);
Assert.Contains(result, g => g.Key == "Omika.Worker" && g.Count == 1);
}
// -------------------------------------------------- T14-C Test 9 : ErrorCode --
[Fact]
public async Task ErrorCode_NormalizesCaseAndWhitespace()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev(errorCode: "timeout"),
Ev(errorCode: "TIMEOUT"),
Ev(errorCode: "Timeout"),
Ev(errorCode: " timeout "),
Ev(errorCode: "CRASH")
});
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
DiagnosticGroupDimension.ErrorCode,
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
// 4 variantes du même code + 1 autre = 2 codes logiques.
Assert.Equal(2, result.Count);
Assert.Contains(result, g => g.Key == "TIMEOUT" && g.Count == 4);
Assert.Contains(result, g => g.Key == "CRASH" && g.Count == 1);
}
// ------------------------- Null/vide restent un groupe distinct (compatibilité) --
[Fact]
public async Task ErrorCode_NullAndEmpty_RemainGroupedAsNull()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev(errorCode: null),
Ev(errorCode: " "),
Ev(errorCode: "TIMEOUT")
});
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
DiagnosticGroupDimension.ErrorCode,
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
Assert.Equal(2, result.Count);
Assert.Contains(result, g => g.Key is null && g.Count == 2);
Assert.Contains(result, g => g.Key == "TIMEOUT" && g.Count == 1);
}
// ------------------------- T20 §7 : « Codes d'erreur principaux » ----
[Fact]
public async Task ErrorCode_WithHasErrorCode_ExcludesNullAndBlank()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev(errorCode: null),
Ev(errorCode: " "),
Ev(errorCode: ""),
Ev(errorCode: "TIMEOUT"),
Ev(errorCode: "CRASH"),
});
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
DiagnosticGroupDimension.ErrorCode,
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, HasErrorCode = true });
// Seuls les ErrorCode réels comptent : jamais de groupe « Non renseigné ».
Assert.Equal(2, result.Count);
Assert.DoesNotContain(result, g => g.Key is null);
Assert.Contains(result, g => g.Key == "TIMEOUT" && g.Count == 1);
Assert.Contains(result, g => g.Key == "CRASH" && g.Count == 1);
}
// --------------------- Équivalent C# documenté (unité, sans repository) --------
[Theory]
[InlineData("Omika.Api", "Omika.Api")]
[InlineData("OMIKA.API", "Omika.Api")]
[InlineData("omika.api", "Omika.Api")]
[InlineData(" omika.api ", "Omika.Api")]
[InlineData("Omika.Diagnostics.Worker", "Omika.Diagnostics.Worker")]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", null)]
public void ComponentHelper_NormalizesDeterministically(string? input, string? expected)
{
Assert.Equal(expected, DiagnosticDimensionNormalization.Component(input));
}
[Theory]
[InlineData("timeout", "TIMEOUT")]
[InlineData("TIMEOUT", "TIMEOUT")]
[InlineData("Timeout", "TIMEOUT")]
[InlineData(" timeout ", "TIMEOUT")]
[InlineData("UnhandledException", "UNHANDLEDEXCEPTION")]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", null)]
public void ErrorCodeHelper_NormalizesDeterministically(string? input, string? expected)
{
Assert.Equal(expected, DiagnosticDimensionNormalization.ErrorCode(input));
}
}