using Omika.App.Local.Diagnostics; using Omika.Core.Observability; using Omika.Core.Observability.Outbox; namespace Omika.Tests.Observability.Outbox; /// Tests du store SQLite séparé (omika-diagnostics.db) : persistance, rétention, éviction, redémarrage. public class DiagnosticOutboxSqliteStoreTests : IAsyncLifetime { private readonly string _dir = Path.Combine(Path.GetTempPath(), "omika-diag-sqlite-tests", Guid.NewGuid().ToString("N")); private string DbPath => Path.Combine(_dir, "omika-diagnostics.db"); public Task InitializeAsync() { Directory.CreateDirectory(_dir); return Task.CompletedTask; } public Task DisposeAsync() { try { if (Directory.Exists(_dir)) { Directory.Delete(_dir, recursive: true); } } catch { // Best-effort. } return Task.CompletedTask; } private static DiagnosticOutboxRow NewRow(TelemetryPriority priority = TelemetryPriority.P3, string payload = "{}") => new() { EventId = Guid.NewGuid(), Priority = priority, CreatedAtUtc = DateTimeOffset.UtcNow, ExpiresAtUtc = DateTimeOffset.UtcNow.AddDays(7), PayloadVersion = 1, PayloadJson = payload, AttemptCount = 0, NextAttemptUtc = DateTimeOffset.UtcNow }; [Fact] public async Task StoreAndRetrieve_RoundTrip() { await using var store = new DiagnosticOutboxSqliteStore(new DiagnosticOutboxOptions { DatabasePath = DbPath }); DiagnosticOutboxRow row = NewRow(payload: """{"eventId":"00000000-0000-0000-0000-000000000001"}"""); await store.StoreAsync([row]); IReadOnlyList batch = await store.GetNextBatchAsync(10, 1024 * 1024, DateTimeOffset.UtcNow); DiagnosticOutboxRow retrieved = Assert.Single(batch); Assert.Equal(row.EventId, retrieved.EventId); Assert.Equal(row.PayloadJson, retrieved.PayloadJson); Assert.Equal(row.Priority, retrieved.Priority); } [Fact] public async Task GetNextBatch_RespectsNextAttemptUtc() { await using var store = new DiagnosticOutboxSqliteStore(new DiagnosticOutboxOptions { DatabasePath = DbPath }); DiagnosticOutboxRow future = NewRow(); future = new DiagnosticOutboxRow { EventId = future.EventId, Priority = future.Priority, CreatedAtUtc = future.CreatedAtUtc, ExpiresAtUtc = future.ExpiresAtUtc, PayloadVersion = future.PayloadVersion, PayloadJson = future.PayloadJson, NextAttemptUtc = DateTimeOffset.UtcNow.AddMinutes(5) }; await store.StoreAsync([future]); IReadOnlyList batch = await store.GetNextBatchAsync(10, 1024 * 1024, DateTimeOffset.UtcNow); Assert.Empty(batch); IReadOnlyList later = await store.GetNextBatchAsync(10, 1024 * 1024, DateTimeOffset.UtcNow.AddMinutes(10)); Assert.Single(later); } [Fact] public async Task DeleteExpired_RemovesOnlyExpired() { await using var store = new DiagnosticOutboxSqliteStore(new DiagnosticOutboxOptions { DatabasePath = DbPath }); DiagnosticOutboxRow expired = NewRow(); expired = new DiagnosticOutboxRow { EventId = expired.EventId, Priority = expired.Priority, CreatedAtUtc = expired.CreatedAtUtc, ExpiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(-1), PayloadVersion = expired.PayloadVersion, PayloadJson = expired.PayloadJson }; await store.StoreAsync([expired, NewRow()]); int removed = await store.DeleteExpiredAsync(DateTimeOffset.UtcNow); Assert.Equal(1, removed); Assert.Equal(1, await store.CountAsync()); } [Fact] public async Task DeleteOldest_RemovesOldestOfPriority() { await using var store = new DiagnosticOutboxSqliteStore(new DiagnosticOutboxOptions { DatabasePath = DbPath }); DiagnosticOutboxRow old = NewRow(TelemetryPriority.P3); await store.StoreAsync([old]); await Task.Delay(10); DiagnosticOutboxRow newer = NewRow(TelemetryPriority.P3); await store.StoreAsync([newer]); int removed = await store.DeleteOldestAsync(TelemetryPriority.P3, 1); Assert.Equal(1, removed); Assert.False((await store.GetNextBatchAsync(10, 1024 * 1024, DateTimeOffset.UtcNow)).Any(r => r.EventId == old.EventId)); } [Fact] public async Task Restart_PersistsRowsAcrossSessions() { DiagnosticOutboxRow row = NewRow(payload: """{"eventId":"00000000-0000-0000-0000-00000000000A"}"""); await using (var store = new DiagnosticOutboxSqliteStore(new DiagnosticOutboxOptions { DatabasePath = DbPath })) { await store.StoreAsync([row]); } // « Redémarrage » : nouvelle connexion sur le même fichier. await using (var store = new DiagnosticOutboxSqliteStore(new DiagnosticOutboxOptions { DatabasePath = DbPath })) { IReadOnlyList batch = await store.GetNextBatchAsync(10, 1024 * 1024, DateTimeOffset.UtcNow); Assert.Equal(row.EventId, Assert.Single(batch).EventId); } } }