Files
2026-10-23 18:46:50 +02:00

252 lines
8.7 KiB
C#

using Omika.Diagnostics.Models;
using Omika.Diagnostics.Queries;
using Omika.Diagnostics.Tests.Fakes;
namespace Omika.Diagnostics.Tests;
/// <summary>
/// Tâche 6 — Télémétrie : nouvelles dimensions de groupement (Platform/Level normalisé/
/// Priority/Source) et statistiques de durée (AVG/MIN/MAX en SQL, NULL ignorés). Tests
/// sur <see cref="FakeDiagnosticEventRepository"/> ; la production exécute les mêmes
/// contrats via GROUP BY / INITCAP(LOWER(...)) / AVG/MAX.
/// </summary>
public class DiagnosticTelemetryTests
{
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 eventCode = "TEST",
DateTimeOffset? ts = null,
string? component = null,
string level = "Info",
string priority = "P2",
string? platform = null,
string? source = null,
long? durationMs = null) =>
new()
{
EventId = Guid.NewGuid(),
EventCode = eventCode,
TimestampUtc = ts ?? From.AddMinutes(1),
Component = component,
Level = level,
Priority = priority,
Platform = platform,
Source = source ?? DiagnosticSources.Mobile,
DurationMs = durationMs,
IngestedAtUtc = DateTimeOffset.UtcNow
};
private static DiagnosticAggregateQuery All(int limit = DiagnosticAggregateDefaults.MaxLimit) =>
new() { FromUtc = From, ToUtc = To, Limit = limit };
// ------------------------------------------------- T1 : groupement Platform ----
[Fact]
public async Task CountGrouped_Platform_ReturnsGroups()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", platform: "Android"),
Ev("B", platform: "Android"),
Ev("C", platform: "iOS")
});
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
DiagnosticGroupDimension.Platform, All());
Assert.Equal(2, result.Count);
Assert.Contains(result, g => g.Key == "Android" && g.Count == 2);
Assert.Contains(result, g => g.Key == "iOS" && g.Count == 1);
}
// ---------------------------------- T2 : groupement Level normalisé (casse) ----
[Fact]
public async Task CountGrouped_Level_IsCaseInsensitive()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", level: "Error"),
Ev("B", level: "error"), // minuscule (ServerDiagnosticMiddleware)
Ev("C", level: "ERROR"),
Ev("D", level: "Info")
});
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
DiagnosticGroupDimension.Level, All());
Assert.Equal(2, result.Count);
Assert.Contains(result, g => g.Key == "Error" && g.Count == 3);
Assert.Contains(result, g => g.Key == "Info" && g.Count == 1);
}
// ----------------------------------------------- T3 : groupement Priority ----
[Fact]
public async Task CountGrouped_Priority_ReturnsGroups()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", priority: "P0"),
Ev("B", priority: "P0"),
Ev("C", priority: "P1")
});
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
DiagnosticGroupDimension.Priority, All());
Assert.Equal(2, result.Count);
Assert.Contains(result, g => g.Key == "P0" && g.Count == 2);
Assert.Contains(result, g => g.Key == "P1" && g.Count == 1);
}
// ---------------------------------------------- T4 : durée moyenne ----
[Fact]
public async Task GetDurationStats_Average_IsCorrect()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", durationMs: 100),
Ev("A", durationMs: 200)
});
DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All());
Assert.NotNull(stats);
Assert.Equal(2, stats.Count);
Assert.Equal(150, stats.AverageMs);
}
// ---------------------------------------------- T5 : durée max ----
[Fact]
public async Task GetDurationStats_Max_IsCorrect()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", durationMs: 100),
Ev("A", durationMs: 350)
});
DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All());
Assert.NotNull(stats);
Assert.Equal(350, stats.MaxMs);
Assert.Equal(100, stats.MinMs);
}
// ---------------------------------------------- T6 : durée NULL ignorée ----
[Fact]
public async Task GetDurationStats_NullDurations_AreIgnored()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", durationMs: 100),
Ev("B", durationMs: null), // sans durée
Ev("C", durationMs: 200)
});
DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All());
Assert.NotNull(stats);
Assert.Equal(2, stats.Count);
Assert.Equal(150, stats.AverageMs);
Assert.Equal(100, stats.MinMs);
Assert.Equal(200, stats.MaxMs);
}
// ---------------------------------------------- T7 : aucune durée → null ----
[Fact]
public async Task GetDurationStats_NoDurations_ReturnsNull()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", durationMs: null),
Ev("B", durationMs: null)
});
DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All());
Assert.Null(stats);
}
// ---------------------------------------------- T8 : période appliquée ----
[Fact]
public async Task GetDurationStats_RespectsPeriod()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", From.AddMinutes(-5), durationMs: 999), // avant → exclu
Ev("A", From.AddMinutes(5), durationMs: 100), // dans
Ev("A", To.AddMinutes(5), durationMs: 999) // après → exclu
});
DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(All());
Assert.NotNull(stats);
Assert.Equal(1, stats.Count);
Assert.Equal(100, stats.AverageMs);
}
// ---------------------------------------------- T9 : filtre EventCode ----
[Fact]
public async Task GetDurationStats_RespectsEventCodeFilter()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", durationMs: 100),
Ev("B", durationMs: 900)
});
DiagnosticDurationStats? stats = await repo.GetDurationStatsAsync(
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, EventCode = "A" });
Assert.NotNull(stats);
Assert.Equal(1, stats.Count);
Assert.Equal(100, stats.AverageMs);
}
// ---------------------------------------------- T10 : top lentes limité ----
[Fact]
public async Task GetTopDurationByEventCode_RespectsLimitAndSort()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("FAST", durationMs: 10),
Ev("SLOW", durationMs: 500),
Ev("MEDIUM", durationMs: 250)
});
IReadOnlyList<DiagnosticDurationByEventCode> result = await repo.GetTopDurationByEventCodeAsync(
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, Limit = 2 });
Assert.Equal(2, result.Count);
Assert.Equal("SLOW", result[0].EventCode); // moyenne décroissante
Assert.Equal("MEDIUM", result[1].EventCode);
}
[Fact]
public async Task GetTopDurationByEventCode_ExcludesNullDurations()
{
var repo = new FakeDiagnosticEventRepository();
await repo.InsertBatchAsync(new[]
{
Ev("A", durationMs: 100),
Ev("B", durationMs: null) // sans durée → ne doit pas apparaître
});
IReadOnlyList<DiagnosticDurationByEventCode> result = await repo.GetTopDurationByEventCodeAsync(All());
DiagnosticDurationByEventCode item = Assert.Single(result);
Assert.Equal("A", item.EventCode);
}
}