Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
using Omika.Diagnostics.Models;
|
||||
using Omika.Diagnostics.Queries;
|
||||
using Omika.Diagnostics.Tests.Fakes;
|
||||
|
||||
namespace Omika.Diagnostics.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tâche 2 — Regroupement d'erreurs logiquement équivalentes. Les tests s'appuient sur
|
||||
/// le <see cref="FakeDiagnosticEventRepository"/> (in-memory) ; l'implémentation
|
||||
/// PostgreSQL de production exécute le même regroupement via GROUP BY serveur.
|
||||
/// </summary>
|
||||
public class DiagnosticErrorGroupTests
|
||||
{
|
||||
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 = "ERR",
|
||||
string? exceptionType = "System.Exception",
|
||||
DateTimeOffset? ts = null,
|
||||
string? component = null,
|
||||
string? errorCode = null,
|
||||
string? appVersion = null,
|
||||
string? actorKey = null,
|
||||
string level = "Error",
|
||||
string priority = "P2") =>
|
||||
new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventCode = eventCode,
|
||||
TimestampUtc = ts ?? From.AddMinutes(1),
|
||||
Component = component,
|
||||
ErrorCode = errorCode,
|
||||
ExceptionType = exceptionType,
|
||||
Level = level,
|
||||
Priority = priority,
|
||||
AppVersion = appVersion,
|
||||
ActorKey = actorKey,
|
||||
Source = DiagnosticSources.Mobile,
|
||||
IngestedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// --------------------------------- G1 : 500 équivalents → 1 groupe ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_EquivalentEvents_ProduceSingleGroup()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
var events = new List<DiagnosticEventRecord>();
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
events.Add(Ev(
|
||||
eventCode: "OMK-API-001",
|
||||
exceptionType: "System.InvalidOperationException",
|
||||
component: "Omika.Api",
|
||||
ts: From.AddSeconds(i)));
|
||||
}
|
||||
|
||||
await repo.InsertBatchAsync(events);
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
DiagnosticErrorGroup group = Assert.Single(groups);
|
||||
Assert.Equal(500, group.OccurrenceCount);
|
||||
Assert.Equal("OMK-API-001", group.EventCode);
|
||||
}
|
||||
|
||||
// ------------------------------- G2 : ExceptionType différent → séparés ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_DifferentExceptionType_SeparateGroups()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "ERR", exceptionType: "System.A"),
|
||||
Ev(eventCode: "ERR", exceptionType: "System.B")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
Assert.Equal(2, groups.Count);
|
||||
}
|
||||
|
||||
// --------------------------------- G3 : EventCode différent → séparés ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_DifferentEventCode_SeparateGroups()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "A", exceptionType: "System.E"),
|
||||
Ev(eventCode: "B", exceptionType: "System.E")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
Assert.Equal(2, groups.Count);
|
||||
}
|
||||
|
||||
// ----------------------------------------- G4 : hors période → exclus ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_OutOfPeriod_AreExcluded()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "ERR", exceptionType: "System.E", ts: From.AddMinutes(-5)),
|
||||
Ev(eventCode: "ERR", exceptionType: "System.E", ts: From.AddMinutes(5)),
|
||||
Ev(eventCode: "ERR", exceptionType: "System.E", ts: To.AddMinutes(5))
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
DiagnosticErrorGroup group = Assert.Single(groups);
|
||||
Assert.Equal(1, group.OccurrenceCount);
|
||||
}
|
||||
|
||||
// ------------------------------ G5 : NULL ExceptionType → déterministe ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_NullExceptionType_DoesNotThrow()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "ERR", exceptionType: null),
|
||||
Ev(eventCode: "ERR", exceptionType: null),
|
||||
Ev(eventCode: "ERR", exceptionType: "System.E")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
Assert.Equal(2, groups.Count);
|
||||
Assert.Contains(groups, g => g.ExceptionType is null && g.OccurrenceCount == 2);
|
||||
Assert.Contains(groups, g => g.ExceptionType == "System.E" && g.OccurrenceCount == 1);
|
||||
}
|
||||
|
||||
// --------------------------------------- G6 : FirstSeen/LastSeen ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_FirstAndLastSeen_AreCorrect()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
DateTimeOffset first = From.AddMinutes(10);
|
||||
DateTimeOffset last = From.AddMinutes(40);
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "ERR", exceptionType: "System.E", ts: first),
|
||||
Ev(eventCode: "ERR", exceptionType: "System.E", ts: last),
|
||||
Ev(eventCode: "ERR", exceptionType: "System.E", ts: From.AddMinutes(20))
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
DiagnosticErrorGroup group = Assert.Single(groups);
|
||||
Assert.Equal(first, group.FirstSeenUtc);
|
||||
Assert.Equal(last, group.LastSeenUtc);
|
||||
}
|
||||
|
||||
// ----------------------------------------------- G7 : limite respectée ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_RespectsLimit()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "A", exceptionType: "E"),
|
||||
Ev(eventCode: "B", exceptionType: "E"),
|
||||
Ev(eventCode: "C", exceptionType: "E")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To, Limit = 2 });
|
||||
|
||||
Assert.Equal(2, groups.Count);
|
||||
}
|
||||
|
||||
// ---------------------------------------- G8 : tri déterministe ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_Ties_SortedDeterministically()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "B", exceptionType: "E"),
|
||||
Ev(eventCode: "A", exceptionType: "E"),
|
||||
Ev(eventCode: "C", exceptionType: "E")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
// Tri par défaut : occurrence DESC puis clés croissantes (déterministe).
|
||||
Assert.Equal(new[] { "A", "B", "C" }, groups.Select(g => g.EventCode).ToArray());
|
||||
}
|
||||
|
||||
// ------------------------- Normalisation : casse insensible ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_CaseInsensitiveExceptionType_SameGroup()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "ERR", exceptionType: "System.InvalidOperationException"),
|
||||
Ev(eventCode: "ERR", exceptionType: "system.invalidoperationexception")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
DiagnosticErrorGroup group = Assert.Single(groups);
|
||||
Assert.Equal(2, group.OccurrenceCount);
|
||||
// Valeur représentative brute (MIN) conservée pour l'affichage.
|
||||
Assert.Equal("System.InvalidOperationException", group.ExceptionType);
|
||||
}
|
||||
|
||||
// ------------------------- Normalisation : vide/espaces → NULL ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_EmptyExceptionType_TreatedAsNull()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "ERR", exceptionType: ""),
|
||||
Ev(eventCode: "ERR", exceptionType: " "),
|
||||
Ev(eventCode: "ERR", exceptionType: null)
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
// vide, espaces et NULL normalisent tous vers la même clé de groupe.
|
||||
DiagnosticErrorGroup group = Assert.Single(groups);
|
||||
Assert.Equal(3, group.OccurrenceCount);
|
||||
}
|
||||
|
||||
// ------------------------- Compteurs distincts (optionnels §13) ----
|
||||
[Fact]
|
||||
public async Task GroupErrors_AppVersionAndActorCounts()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev(eventCode: "ERR", exceptionType: "E", appVersion: "1.0", actorKey: "actor-1"),
|
||||
Ev(eventCode: "ERR", exceptionType: "E", appVersion: "1.0", actorKey: "actor-1"),
|
||||
Ev(eventCode: "ERR", exceptionType: "E", appVersion: "2.0", actorKey: "actor-2")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticErrorGroup> groups = await repo.GroupErrorsAsync(
|
||||
new DiagnosticErrorGroupQuery { FromUtc = From, ToUtc = To });
|
||||
|
||||
DiagnosticErrorGroup group = Assert.Single(groups);
|
||||
Assert.Equal(3, group.OccurrenceCount);
|
||||
Assert.Equal(2, group.AppVersionCount); // 1.0, 2.0
|
||||
Assert.Equal(2, group.ActorCount); // actor-1, actor-2
|
||||
}
|
||||
|
||||
// ------------------------- Validation période obligatoire ----
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
public async Task GroupErrors_Validate_RejectsMissingPeriod(bool missingFrom, bool missingTo)
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
var query = new DiagnosticErrorGroupQuery
|
||||
{
|
||||
FromUtc = missingFrom ? null : From,
|
||||
ToUtc = missingTo ? null : To
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => repo.GroupErrorsAsync(query));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user