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