using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Omika.Diagnostics.Configuration;
using Omika.Diagnostics.Models;
using Omika.Diagnostics.Repositories;
using Omika.Diagnostics.Tests.Fakes;
using Omika.Diagnostics.Worker;
namespace Omika.Diagnostics.Tests;
/// Rétention télémétrie (telemetry_metric_hourly) : 6 mois, lots bornés, fail-open.
public class DiagnosticTelemetryRetentionTests
{
private static TelemetryMetricHourly At(DateTimeOffset bucket) =>
new()
{
BucketUtc = bucket,
MetricName = "test.metric",
Component = string.Empty,
Value = 1,
Count = 1,
MinValue = 1,
MaxValue = 1,
TagsJson = "{}"
};
// ---------------------------------------------------------------- repo ----
[Fact]
public async Task DeleteOlderThan_RemovesOnlyOlderThanCutoff()
{
var repo = new FakeTelemetryMetricRepository();
DateTimeOffset cutoff = DateTimeOffset.UtcNow.AddDays(-180); // 6 mois
await repo.UpsertHourlyAsync([
At(cutoff.AddDays(-10)),
At(cutoff.AddDays(-1)),
At(DateTimeOffset.UtcNow)
]);
int deleted = await repo.DeleteOlderThanAsync(cutoff, batchSize: 100);
Assert.Equal(2, deleted);
Assert.Single(repo.All);
}
[Fact]
public async Task DeleteOlderThan_RespectsBatchSize()
{
var repo = new FakeTelemetryMetricRepository();
DateTimeOffset cutoff = DateTimeOffset.UtcNow.AddDays(-180);
var old = new List();
for (int i = 0; i < 5; i++)
{
old.Add(At(cutoff.AddDays(-i - 1)));
}
await repo.UpsertHourlyAsync(old);
int first = await repo.DeleteOlderThanAsync(cutoff, batchSize: 2);
int second = await repo.DeleteOlderThanAsync(cutoff, batchSize: 2);
int third = await repo.DeleteOlderThanAsync(cutoff, batchSize: 2);
Assert.Equal(2, first);
Assert.Equal(2, second);
Assert.Equal(1, third);
}
[Fact]
public async Task DeleteOlderThan_NonPositiveBatchSize_ReturnsZero()
{
var repo = new FakeTelemetryMetricRepository();
await repo.UpsertHourlyAsync([At(DateTimeOffset.UtcNow.AddDays(-200))]);
int deleted = await repo.DeleteOlderThanAsync(DateTimeOffset.UtcNow.AddDays(-180), batchSize: 0);
Assert.Equal(0, deleted);
Assert.Single(repo.All);
}
// -------------------------------------------------------------- worker ----
private static DiagnosticTelemetryRetentionWorker CreateWorker(
ITelemetryMetricRepository repo,
DiagnosticStoreOptions options) =>
new(repo, Options.Create(options), NullLogger.Instance);
[Fact]
public async Task TelemetryRetentionWorker_PurgesOlderAggregates()
{
var repo = new FakeTelemetryMetricRepository();
DateTimeOffset cutoff = DateTimeOffset.UtcNow.AddDays(-180);
await repo.UpsertHourlyAsync([
At(cutoff.AddDays(-5)),
At(cutoff.AddDays(-1)),
At(DateTimeOffset.UtcNow)
]);
var worker = CreateWorker(repo, new DiagnosticStoreOptions
{
TelemetryRetentionDays = 180,
RetentionBatchSize = 100,
RetentionIntervalHours = 1
});
using var cts = new CancellationTokenSource();
await worker.StartAsync(cts.Token);
int remaining = 3;
for (int i = 0; i < 50; i++)
{
remaining = repo.All.Count;
if (remaining == 1)
{
break;
}
await Task.Delay(100);
}
cts.Cancel();
await worker.StopAsync(cts.Token);
Assert.Equal(1, remaining); // seul l'agrégat récent subsiste
}
[Fact]
public async Task TelemetryRetentionWorker_Disabled_DoesNotPurge()
{
var repo = new FakeTelemetryMetricRepository();
await repo.UpsertHourlyAsync([At(DateTimeOffset.UtcNow.AddDays(-200))]);
var worker = CreateWorker(repo, new DiagnosticStoreOptions
{
TelemetryRetentionDays = 0, // désactivé
RetentionBatchSize = 100,
RetentionIntervalHours = 1
});
using var cts = new CancellationTokenSource();
await worker.StartAsync(cts.Token);
await Task.Delay(300);
cts.Cancel();
await worker.StopAsync(cts.Token);
Assert.Single(repo.All); // rien de purgé
}
[Fact]
public async Task TelemetryRetentionWorker_DbDown_DoesNotCrash()
{
var worker = CreateWorker(new ThrowingTelemetryMetricRepository(), new DiagnosticStoreOptions
{
TelemetryRetentionDays = 180,
RetentionBatchSize = 100,
RetentionIntervalHours = 1
});
using var cts = new CancellationTokenSource();
await worker.StartAsync(cts.Token); // ne doit pas lever
await Task.Delay(300);
cts.Cancel();
await worker.StopAsync(cts.Token); // arrêt propre
}
}