Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user