291 lines
11 KiB
C#
291 lines
11 KiB
C#
using Omika.Diagnostics.Models;
|
|
using Omika.Diagnostics.Queries;
|
|
using Omika.Diagnostics.Tests.Fakes;
|
|
|
|
namespace Omika.Diagnostics.Tests;
|
|
|
|
/// <summary>
|
|
/// Tâche 1 — Agrégations statistiques côté serveur. Les tests s'appuient sur le
|
|
/// <see cref="FakeDiagnosticEventRepository"/> (in-memory) ; l'implémentation
|
|
/// PostgreSQL de production exécute les mêmes contrats via GROUP BY / date_trunc.
|
|
/// </summary>
|
|
public class DiagnosticAggregationTests
|
|
{
|
|
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? errorCode = null,
|
|
string? appVersion = null,
|
|
string level = "Info",
|
|
string priority = "P2") =>
|
|
new()
|
|
{
|
|
EventId = Guid.NewGuid(),
|
|
EventCode = eventCode,
|
|
TimestampUtc = ts ?? From.AddMinutes(1),
|
|
Component = component,
|
|
ErrorCode = errorCode,
|
|
AppVersion = appVersion,
|
|
Level = level,
|
|
Priority = priority,
|
|
Source = DiagnosticSources.Mobile,
|
|
IngestedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
// ---------------------------------------------------- Test 1 : EventCode ----
|
|
[Fact]
|
|
public async Task CountGrouped_EventCode_ReturnsDescendingCounts()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("OMK-API-001", From.AddMinutes(1)),
|
|
Ev("OMK-API-001", From.AddMinutes(2)),
|
|
Ev("OMK-API-001", From.AddMinutes(3)),
|
|
Ev("OMK-API-002", From.AddMinutes(4))
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
|
|
DiagnosticGroupDimension.EventCode,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Equal("OMK-API-001", result[0].Key);
|
|
Assert.Equal(3, result[0].Count);
|
|
Assert.Equal("OMK-API-002", result[1].Key);
|
|
Assert.Equal(1, result[1].Count);
|
|
}
|
|
|
|
// ------------------------------------------------------ Test 2 : période ----
|
|
[Fact]
|
|
public async Task CountGrouped_RespectsTimePeriod()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("BEFORE", From.AddMinutes(-5)), // avant FromUtc
|
|
Ev("IN", From.AddMinutes(10)), // dans la période
|
|
Ev("AFTER", To.AddMinutes(5)) // après ToUtc (borne exclusive)
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
|
|
DiagnosticGroupDimension.EventCode,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
|
|
|
|
DiagnosticGroupCount group = Assert.Single(result);
|
|
Assert.Equal("IN", group.Key);
|
|
Assert.Equal(1, group.Count);
|
|
}
|
|
|
|
// -------------------------------------------------------- Test 3 : limit ----
|
|
[Fact]
|
|
public async Task CountGrouped_RespectsLimit()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("A", From.AddMinutes(1)), Ev("A", From.AddMinutes(2)),
|
|
Ev("B", From.AddMinutes(3)), Ev("B", From.AddMinutes(4)),
|
|
Ev("C", From.AddMinutes(5))
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
|
|
DiagnosticGroupDimension.EventCode,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, Limit = 2 });
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Equal("A", result[0].Key);
|
|
Assert.Equal("B", result[1].Key);
|
|
}
|
|
|
|
// ----------------------------------------------------- Test 4 : NULL keys ----
|
|
[Fact]
|
|
public async Task CountGrouped_NullErrorCode_DoesNotThrow()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("A", From.AddMinutes(1), errorCode: "E1"),
|
|
Ev("B", From.AddMinutes(2)) // ErrorCode null
|
|
});
|
|
|
|
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 == "E1" && g.Count == 1);
|
|
Assert.Contains(result, g => g.Key is null && g.Count == 1);
|
|
}
|
|
|
|
// ------------------------------------------------------ Test 5 : filtres ----
|
|
[Fact]
|
|
public async Task CountGrouped_AppliesComponentFilter()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("A", From.AddMinutes(1), component: "Omika.Api"),
|
|
Ev("A", From.AddMinutes(2), component: "Omika.Api"),
|
|
Ev("A", From.AddMinutes(3), component: "Omika.Worker")
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
|
|
DiagnosticGroupDimension.EventCode,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To, Component = "Omika.Api" });
|
|
|
|
DiagnosticGroupCount group = Assert.Single(result);
|
|
Assert.Equal("A", group.Key);
|
|
Assert.Equal(2, group.Count);
|
|
}
|
|
|
|
// ------------------------------------------------ Test 6 : bucket horaire ----
|
|
[Fact]
|
|
public async Task CountByTimeBucket_Hour_ReturnsChronologicalBuckets()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
var baseTime = new DateTimeOffset(2025, 1, 1, 10, 30, 0, TimeSpan.Zero);
|
|
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("A", baseTime), // 10:30 → bucket 10:00
|
|
Ev("B", baseTime.AddMinutes(15)), // 10:45 → bucket 10:00
|
|
Ev("C", baseTime.AddHours(1)) // 11:30 → bucket 11:00
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticBucketCount> result = await repo.CountByTimeBucketAsync(
|
|
DiagnosticTimeBucket.Hour,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Equal(new DateTimeOffset(2025, 1, 1, 10, 0, 0, TimeSpan.Zero), result[0].BucketUtc);
|
|
Assert.Equal(2, result[0].Count);
|
|
Assert.Equal(new DateTimeOffset(2025, 1, 1, 11, 0, 0, TimeSpan.Zero), result[1].BucketUtc);
|
|
Assert.Equal(1, result[1].Count);
|
|
}
|
|
|
|
// ----------------------------------------------- Test 7 : bucket journalier --
|
|
[Fact]
|
|
public async Task CountByTimeBucket_Day_ReturnsChronologicalBuckets()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
var day1 = new DateTimeOffset(2025, 1, 1, 9, 0, 0, TimeSpan.Zero);
|
|
var day2 = new DateTimeOffset(2025, 1, 2, 9, 0, 0, TimeSpan.Zero);
|
|
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("A", day1),
|
|
Ev("B", day1.AddHours(5)),
|
|
Ev("C", day2)
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticBucketCount> result = await repo.CountByTimeBucketAsync(
|
|
DiagnosticTimeBucket.Day,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Equal(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), result[0].BucketUtc);
|
|
Assert.Equal(2, result[0].Count);
|
|
Assert.Equal(new DateTimeOffset(2025, 1, 2, 0, 0, 0, TimeSpan.Zero), result[1].BucketUtc);
|
|
Assert.Equal(1, result[1].Count);
|
|
}
|
|
|
|
// ---------------------------------------- Test 8 : validation de la période --
|
|
[Fact]
|
|
public void AggregateQuery_Validate_RejectsMissingFrom()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new DiagnosticAggregateQuery { ToUtc = DateTimeOffset.UtcNow }.Validate());
|
|
}
|
|
|
|
[Fact]
|
|
public void AggregateQuery_Validate_RejectsMissingTo()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new DiagnosticAggregateQuery { FromUtc = DateTimeOffset.UtcNow }.Validate());
|
|
}
|
|
|
|
[Fact]
|
|
public void AggregateQuery_Validate_RejectsFromAfterTo()
|
|
{
|
|
DateTimeOffset t = DateTimeOffset.UtcNow;
|
|
Assert.Throws<ArgumentException>(() => new DiagnosticAggregateQuery { FromUtc = t.AddMinutes(1), ToUtc = t }.Validate());
|
|
}
|
|
|
|
[Fact]
|
|
public void AggregateQuery_Validate_RejectsTooLongPeriod()
|
|
{
|
|
DateTimeOffset from = DateTimeOffset.UtcNow;
|
|
DateTimeOffset to = from.AddDays(DiagnosticAggregateDefaults.MaxPeriodDays + 1);
|
|
Assert.Throws<ArgumentException>(() => new DiagnosticAggregateQuery { FromUtc = from, ToUtc = to }.Validate());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CountGrouped_InvalidPeriod_Throws()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
DateTimeOffset t = DateTimeOffset.UtcNow;
|
|
|
|
await Assert.ThrowsAsync<ArgumentException>(() =>
|
|
repo.CountGroupedAsync(
|
|
DiagnosticGroupDimension.EventCode,
|
|
new DiagnosticAggregateQuery { FromUtc = t, ToUtc = t }));
|
|
}
|
|
|
|
// ------------------------------------------- Dimensions complémentaires -----
|
|
[Fact]
|
|
public async Task CountGrouped_AppVersion_ReturnsGroups()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("A", From.AddMinutes(1), appVersion: "1.3.1"),
|
|
Ev("B", From.AddMinutes(2), appVersion: "1.3.1"),
|
|
Ev("C", From.AddMinutes(3), appVersion: "1.3.2")
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
|
|
DiagnosticGroupDimension.AppVersion,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(result, g => g.Key == "1.3.1" && g.Count == 2);
|
|
Assert.Contains(result, g => g.Key == "1.3.2" && g.Count == 1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CountGrouped_Component_ReturnsGroups()
|
|
{
|
|
var repo = new FakeDiagnosticEventRepository();
|
|
await repo.InsertBatchAsync(new[]
|
|
{
|
|
Ev("A", From.AddMinutes(1), component: "Omika.Api"),
|
|
Ev("B", From.AddMinutes(2), component: "Omika.Api"),
|
|
Ev("C", From.AddMinutes(3), component: "Omika.Diagnostics.Worker")
|
|
});
|
|
|
|
IReadOnlyList<DiagnosticGroupCount> result = await repo.CountGroupedAsync(
|
|
DiagnosticGroupDimension.Component,
|
|
new DiagnosticAggregateQuery { FromUtc = From, ToUtc = To });
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(result, g => g.Key == "Omika.Api" && g.Count == 2);
|
|
Assert.Contains(result, g => g.Key == "Omika.Diagnostics.Worker" && g.Count == 1);
|
|
}
|
|
|
|
[Fact]
|
|
public void AggregateQuery_ClampsLimit()
|
|
{
|
|
var tooBig = new DiagnosticAggregateQuery { Limit = 100_000 };
|
|
Assert.Equal(DiagnosticAggregateDefaults.MaxLimit, tooBig.Limit);
|
|
|
|
var tooSmall = new DiagnosticAggregateQuery { Limit = 0 };
|
|
Assert.Equal(1, tooSmall.Limit);
|
|
|
|
var defaultQuery = new DiagnosticAggregateQuery();
|
|
Assert.Equal(DiagnosticAggregateDefaults.DefaultLimit, defaultQuery.Limit);
|
|
}
|
|
}
|