Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -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);
}
}