using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ScadaLink.AuditLog.Site; using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.AuditLog.Tests.Site; /// /// Bundle E (M6-T6) tests for . /// Exercises the health-metric surface that SiteAuditBacklogReporter /// polls every 30 s and pushes onto the site health report as /// SiteAuditBacklog. /// public class SqliteAuditWriterBacklogStatsTests : IDisposable { private readonly string _dbPath; public SqliteAuditWriterBacklogStatsTests() { // OnDiskBytes assertions only make sense against a real file — the // shared-cache in-memory mode returns 0 for the file size, so this // suite is opinionated about file-backed storage. Tests in // SqliteAuditWriterWriteTests use in-memory for performance reasons. _dbPath = Path.Combine(Path.GetTempPath(), $"audit-backlog-stats-{Guid.NewGuid():N}.db"); } public void Dispose() { if (File.Exists(_dbPath)) { try { File.Delete(_dbPath); } catch { /* test cleanup best-effort */ } } } private SqliteAuditWriter CreateWriter() { var options = new SqliteAuditWriterOptions { DatabasePath = _dbPath }; return new SqliteAuditWriter( Options.Create(options), NullLogger.Instance); } private static AuditEvent NewEvent(DateTime? occurredAtUtc = null) => new() { EventId = Guid.NewGuid(), OccurredAtUtc = occurredAtUtc ?? DateTime.UtcNow, Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCall, Status = AuditStatus.Delivered, PayloadTruncated = false, }; [Fact] public async Task EmptyDb_Returns_Zero_Null_AndZeroBytes() { // No file exists yet — the writer ctor creates one but no rows are // inserted; the snapshot should report a clean queue. OnDiskBytes is // allowed to be zero (fresh ftruncate) OR small (page header) — the // contract only requires non-negative; we assert >= 0 and exercise // the pending fields strictly. await using var writer = CreateWriter(); var snapshot = await writer.GetBacklogStatsAsync(); Assert.Equal(0, snapshot.PendingCount); Assert.Null(snapshot.OldestPendingUtc); Assert.True(snapshot.OnDiskBytes >= 0, $"OnDiskBytes must be non-negative, got {snapshot.OnDiskBytes}"); } [Fact] public async Task Pending_5_Returns_5() { await using var writer = CreateWriter(); for (var i = 0; i < 5; i++) { await writer.WriteAsync(NewEvent()); } var snapshot = await writer.GetBacklogStatsAsync(); Assert.Equal(5, snapshot.PendingCount); } [Fact] public async Task OldestPending_Is_Earliest_OccurredAtUtc() { await using var writer = CreateWriter(); var t1 = new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc); var t2 = new DateTime(2026, 5, 20, 10, 1, 0, DateTimeKind.Utc); var t3 = new DateTime(2026, 5, 20, 10, 2, 0, DateTimeKind.Utc); // Insert out of order so the snapshot is not "the last write" by // accident — the OldestPendingUtc must come from a column-min, not // an insertion-order proxy. await writer.WriteAsync(NewEvent(t2)); await writer.WriteAsync(NewEvent(t1)); await writer.WriteAsync(NewEvent(t3)); var snapshot = await writer.GetBacklogStatsAsync(); Assert.Equal(3, snapshot.PendingCount); Assert.NotNull(snapshot.OldestPendingUtc); // The DB round-trips OccurredAtUtc through the "o" format which // preserves Kind=Utc — assert tick-equality. Assert.Equal(t1, snapshot.OldestPendingUtc!.Value); } [Fact] public async Task OnDiskBytes_ReturnsFileSize() { await using var writer = CreateWriter(); // Insert enough rows to grow the file past the empty schema baseline. for (var i = 0; i < 100; i++) { await writer.WriteAsync(NewEvent()); } var snapshot = await writer.GetBacklogStatsAsync(); // The exact size depends on SQLite page allocation, but a file-backed // db with 100 inserted rows MUST be larger than the empty schema // (a few pages, ~4 KB). The implementation should return the // FileInfo.Length value verbatim. Assert.True(File.Exists(_dbPath), $"DB file should exist at {_dbPath}"); var expected = new FileInfo(_dbPath).Length; Assert.Equal(expected, snapshot.OnDiskBytes); Assert.True(snapshot.OnDiskBytes > 0, $"after 100 inserts OnDiskBytes must be > 0, got {snapshot.OnDiskBytes}"); } }