Files
Omika/tests/Omika.Diagnostics.Tests/DiagnosticErrorGroupOccurrenceTests.cs
2026-10-23 18:46:50 +02:00

274 lines
10 KiB
C#

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);
}
}