Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
using Omika.Diagnostics.Models;
|
||||
using Omika.Diagnostics.Queries;
|
||||
using Omika.Diagnostics.Tests.Fakes;
|
||||
|
||||
namespace Omika.Diagnostics.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tâche 5 — Occurrences d'un groupe d'erreurs. La récupération des occurrences doit
|
||||
/// reproduire EXACTEMENT la normalisation du regroupement (NormalizeKey =
|
||||
/// LOWER(NULLIF(TRIM(col),''))). Tests sur <see cref="FakeDiagnosticEventRepository"/> ;
|
||||
/// la production exécute la même sémantique via <c>IS NOT DISTINCT FROM</c>.
|
||||
/// </summary>
|
||||
public class DiagnosticErrorGroupOccurrenceTests
|
||||
{
|
||||
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",
|
||||
DateTimeOffset? ts = null,
|
||||
string? errorCode = null,
|
||||
string? exceptionType = null,
|
||||
string? component = null,
|
||||
string level = "Error") =>
|
||||
new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
EventCode = eventCode,
|
||||
TimestampUtc = ts ?? From.AddMinutes(1),
|
||||
ErrorCode = errorCode,
|
||||
ExceptionType = exceptionType,
|
||||
Component = component,
|
||||
Level = level,
|
||||
Priority = "P2",
|
||||
Source = DiagnosticSources.Mobile,
|
||||
IngestedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
private static DiagnosticErrorGroupOccurrenceQuery Query(
|
||||
string eventCode = "ERR",
|
||||
string? normalizedErrorCode = null,
|
||||
string? normalizedExceptionType = null,
|
||||
string? normalizedComponent = null,
|
||||
int page = 1,
|
||||
int pageSize = DiagnosticErrorGroupOccurrenceQuery.DefaultPageSize) =>
|
||||
new()
|
||||
{
|
||||
FromUtc = From,
|
||||
ToUtc = To,
|
||||
EventCode = eventCode,
|
||||
NormalizedErrorCode = normalizedErrorCode,
|
||||
NormalizedExceptionType = normalizedExceptionType,
|
||||
NormalizedComponent = normalizedComponent,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
// --------------------------------------------------- E1 : 3 → 3 occurrences ----
|
||||
[Fact]
|
||||
public async Task Occurrences_ThreeEvents_ReturnThreeOccurrences()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.TimeoutException"),
|
||||
Ev("ERR", From.AddMinutes(2), exceptionType: "System.TimeoutException"),
|
||||
Ev("ERR", From.AddMinutes(3), exceptionType: "System.TimeoutException")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedExceptionType: "system.timeoutexception"));
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
}
|
||||
|
||||
// --------------------------------------- E2 : casse différente → même groupe ----
|
||||
[Fact]
|
||||
public async Task Occurrences_CaseInsensitiveExceptionType_SameGroup()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.TimeoutException"),
|
||||
Ev("ERR", From.AddMinutes(2), exceptionType: "system.timeoutexception")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedExceptionType: "system.timeoutexception"));
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
// --------------------------------------- E3 : espaces autour → même groupe ----
|
||||
[Fact]
|
||||
public async Task Occurrences_WhitespaceAroundComponent_SameGroup()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), component: "Omika.Api"),
|
||||
Ev("ERR", From.AddMinutes(2), component: " omika.api ")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedComponent: "omika.api"));
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
// --------------------------------------- E4 : NULL ErrorCode → retrouvé ----
|
||||
[Fact]
|
||||
public async Task Occurrences_NullErrorCode_AreFound()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), errorCode: null),
|
||||
Ev("ERR", From.AddMinutes(2), errorCode: string.Empty),
|
||||
Ev("ERR", From.AddMinutes(3), errorCode: " ")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedErrorCode: null));
|
||||
|
||||
Assert.Equal(3, result.Count); // null / '' / espaces → tous normalisés en NULL
|
||||
}
|
||||
|
||||
// --------------------------------------- E5 : EventCode différent → exclu ----
|
||||
[Fact]
|
||||
public async Task Occurrences_DifferentEventCode_AreExcluded()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"),
|
||||
Ev("OTHER", From.AddMinutes(2), exceptionType: "System.Exception")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(eventCode: "ERR", normalizedExceptionType: "system.exception"));
|
||||
|
||||
DiagnosticEventRecord item = Assert.Single(result);
|
||||
Assert.Equal("ERR", item.EventCode);
|
||||
}
|
||||
|
||||
// --------------------------------------- E6 : hors période → exclu ----
|
||||
[Fact]
|
||||
public async Task Occurrences_OutOfPeriod_AreExcluded()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(-5), exceptionType: "System.Exception"), // avant
|
||||
Ev("ERR", From.AddMinutes(5), exceptionType: "System.Exception"), // dans
|
||||
Ev("ERR", To.AddMinutes(5), exceptionType: "System.Exception") // après (borne exclusive)
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedExceptionType: "system.exception"));
|
||||
|
||||
DiagnosticEventRecord item = Assert.Single(result);
|
||||
}
|
||||
|
||||
// --------------------------------------- E7 : limite respectée ----
|
||||
[Fact]
|
||||
public async Task Occurrences_RespectsPageSize()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"),
|
||||
Ev("ERR", From.AddMinutes(2), exceptionType: "System.Exception"),
|
||||
Ev("ERR", From.AddMinutes(3), exceptionType: "System.Exception")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedExceptionType: "system.exception", pageSize: 2));
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
// --------------------------------------- E8 : pagination ----
|
||||
[Fact]
|
||||
public async Task Occurrences_Pagination_ReturnsSecondPage()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"),
|
||||
Ev("ERR", From.AddMinutes(2), exceptionType: "System.Exception"),
|
||||
Ev("ERR", From.AddMinutes(3), exceptionType: "System.Exception")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedExceptionType: "system.exception", page: 2, pageSize: 1));
|
||||
|
||||
DiagnosticEventRecord item = Assert.Single(result);
|
||||
Assert.Equal(From.AddMinutes(2), item.TimestampUtc); // 2e plus récent
|
||||
}
|
||||
|
||||
// --------------------------------------- E9 : tri timestamp DESC ----
|
||||
[Fact]
|
||||
public async Task Occurrences_ReturnsDescendingOrder()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(2), exceptionType: "System.Exception"),
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception"),
|
||||
Ev("ERR", From.AddMinutes(3), exceptionType: "System.Exception")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedExceptionType: "system.exception"));
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
From.AddMinutes(3),
|
||||
From.AddMinutes(2),
|
||||
From.AddMinutes(1)
|
||||
},
|
||||
result.Select(e => e.TimestampUtc).ToArray());
|
||||
}
|
||||
|
||||
// --------------------------------------- E10 : groupe inexistant → vide ----
|
||||
[Fact]
|
||||
public async Task Occurrences_NonexistentGroup_ReturnsEmpty()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception")
|
||||
});
|
||||
|
||||
IReadOnlyList<DiagnosticEventRecord> result = await repo.GetErrorGroupOccurrencesAsync(
|
||||
Query(normalizedExceptionType: "system.nullreferenceexception"));
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
// ----------------------------------- GetErrorGroupAsync : résumé ----
|
||||
[Fact]
|
||||
public async Task GetErrorGroup_ReturnsSummary()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[]
|
||||
{
|
||||
Ev("ERR", From.AddMinutes(1), exceptionType: "System.TimeoutException", component: "Omika.Api"),
|
||||
Ev("ERR", From.AddMinutes(10), exceptionType: "System.TimeoutException", component: "Omika.Api")
|
||||
});
|
||||
|
||||
DiagnosticErrorGroup? group = await repo.GetErrorGroupAsync(
|
||||
Query(normalizedExceptionType: "system.timeoutexception", normalizedComponent: "omika.api"));
|
||||
|
||||
Assert.NotNull(group);
|
||||
Assert.Equal(2, group.OccurrenceCount);
|
||||
Assert.Equal(From.AddMinutes(1), group.FirstSeenUtc);
|
||||
Assert.Equal(From.AddMinutes(10), group.LastSeenUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetErrorGroup_Nonexistent_ReturnsNull()
|
||||
{
|
||||
var repo = new FakeDiagnosticEventRepository();
|
||||
await repo.InsertBatchAsync(new[] { Ev("ERR", From.AddMinutes(1), exceptionType: "System.Exception") });
|
||||
|
||||
DiagnosticErrorGroup? group = await repo.GetErrorGroupAsync(
|
||||
Query(normalizedExceptionType: "system.nullreferenceexception"));
|
||||
|
||||
Assert.Null(group);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user