using Omika.Core.Observability; using Omika.Core.Observability.Outbox; namespace Omika.Tests.Observability.Outbox; /// Tests du worker d'upload : succès/suppression, retry, auth, poison, split, anti-loop. public class DiagnosticUploadWorkerTests { private static (DiagnosticUploadWorker Worker, InMemoryDiagnosticOutboxStore Store, DiagnosticCounters Counters, FakeUploadClient Client) Create( DiagnosticUploadOptions? uploadOptions = null, string? token = "token-1", Func? handler = null) { var store = new InMemoryDiagnosticOutboxStore(); var counters = new DiagnosticCounters(); var outbox = new DiagnosticOutbox(store, new DiagnosticOutboxOptions(), counters); var client = new FakeUploadClient { Handler = handler ?? (_ => new DiagnosticUploadResult(DiagnosticUploadOutcome.Success)) }; var worker = new DiagnosticUploadWorker( outbox, client, uploadOptions ?? new DiagnosticUploadOptions(), counters, () => token); return (worker, store, counters, client); } [Fact] public async Task Success_DeletesRows_AndCounts() { (DiagnosticUploadWorker worker, InMemoryDiagnosticOutboxStore store, DiagnosticCounters counters, _) = Create(); await SeedAsync(store, 1); bool attempted = await worker.FlushAsync(); Assert.True(attempted); Assert.Equal(0, await store.CountAsync()); Assert.Equal(1, counters.UploadSuccess); Assert.Equal(1, counters.UploadEventsSent); } [Fact] public async Task TransientFailure_ConservesRows_IncrementsAttempt() { (DiagnosticUploadWorker worker, InMemoryDiagnosticOutboxStore store, DiagnosticCounters counters, _) = Create(handler: _ => new DiagnosticUploadResult(DiagnosticUploadOutcome.Transient)); await SeedAsync(store, 1); await worker.FlushAsync(); Assert.Equal(1, await store.CountAsync()); Assert.Equal(1, counters.UploadFailure); DiagnosticOutboxRow row = store.Snapshot().Single(); Assert.Equal(1, row.AttemptCount); Assert.True(row.NextAttemptUtc > DateTimeOffset.UtcNow); } [Fact] public async Task Unauthorized_ConservesRows_NoDelete() { (DiagnosticUploadWorker worker, InMemoryDiagnosticOutboxStore store, DiagnosticCounters counters, _) = Create(handler: _ => new DiagnosticUploadResult(DiagnosticUploadOutcome.Unauthorized)); await SeedAsync(store, 1); await worker.FlushAsync(); Assert.Equal(1, await store.CountAsync()); Assert.Equal(1, counters.UploadUnauthorized); Assert.Equal(0, counters.UploadSuccess); } [Fact] public async Task RejectedSingleton_IsDroppedAsPoison() { (DiagnosticUploadWorker worker, InMemoryDiagnosticOutboxStore store, DiagnosticCounters counters, _) = Create(handler: _ => new DiagnosticUploadResult(DiagnosticUploadOutcome.Rejected)); await SeedAsync(store, 1); await worker.FlushAsync(); Assert.Equal(0, await store.CountAsync()); Assert.Equal(1, counters.UploadPoisonDropped); } [Fact] public async Task RejectedLargeBatch_Splits_UntilSingletonsSucceed() { // Le serveur rejette tout lot de plus d'un événement (400) : le worker divise // jusqu'aux singletons, qui sont acceptés. Aucun poison, tout est envoyé. (DiagnosticUploadWorker worker, InMemoryDiagnosticOutboxStore store, DiagnosticCounters counters, _) = Create(handler: batch => batch.Events.Count > 1 ? new DiagnosticUploadResult(DiagnosticUploadOutcome.Rejected) : new DiagnosticUploadResult(DiagnosticUploadOutcome.Success)); await SeedAsync(store, 4); await worker.FlushAsync(); Assert.Equal(0, await store.CountAsync()); Assert.Equal(4, counters.UploadEventsSent); Assert.Equal(0, counters.UploadPoisonDropped); } [Fact] public async Task NoToken_DoesNotUpload_ConservesRows() { (DiagnosticUploadWorker worker, InMemoryDiagnosticOutboxStore store, _, FakeUploadClient client) = Create(token: null); await SeedAsync(store, 1); await worker.FlushAsync(); Assert.Equal(1, await store.CountAsync()); Assert.Equal(0, client.Calls); } [Fact] public async Task Disabled_LoopDoesNotUpload() { var options = new DiagnosticUploadOptions { Enabled = false, Interval = TimeSpan.FromMilliseconds(10) }; (DiagnosticUploadWorker worker, InMemoryDiagnosticOutboxStore store, _, FakeUploadClient client) = Create(uploadOptions: options); await SeedAsync(store, 1); worker.Start(); await Task.Delay(50); await worker.StopAsync(); Assert.Equal(1, await store.CountAsync()); Assert.Equal(0, client.Calls); } private static async Task SeedAsync(InMemoryDiagnosticOutboxStore store, int count) { // Insère des événements mappés dans le store pour isoler le comportement d'upload. var outbox = new DiagnosticOutbox(store, new DiagnosticOutboxOptions(), new DiagnosticCounters()); await outbox.StoreAsync(Enumerable.Range(0, count) .Select(_ => OutboxTestHarness.NewEvent(TelemetryPriority.P3)) .ToList()); } private sealed class FakeUploadClient : IDiagnosticUploadClient { public Func? Handler { get; set; } public int Calls { get; private set; } public Task UploadAsync(DiagnosticBatchDto batch, CancellationToken cancellationToken = default) { Calls++; return Task.FromResult(Handler!(batch)); } } }