using Microsoft.Data.Sqlite; 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 B (M2-T2) hot-path tests for . Exercise /// the Channel-based enqueue, the background writer's batch INSERTs, duplicate- /// EventId swallowing, ForwardState defaulting, and the /// / /// support surface that /// Bundle D's telemetry actor will call. /// public class SqliteAuditWriterWriteTests { private static (SqliteAuditWriter writer, string dataSource) CreateWriter( string testName, int? channelCapacity = null) { var dataSource = $"file:{testName}-{Guid.NewGuid():N}?mode=memory&cache=shared"; var opts = new SqliteAuditWriterOptions { DatabasePath = dataSource }; if (channelCapacity is int cap) { opts.ChannelCapacity = cap; } var writer = new SqliteAuditWriter( Options.Create(opts), NullLogger.Instance, connectionStringOverride: $"Data Source={dataSource};Cache=Shared"); return (writer, dataSource); } private static SqliteConnection OpenVerifierConnection(string dataSource) { var connection = new SqliteConnection($"Data Source={dataSource};Cache=Shared"); connection.Open(); return connection; } private static AuditEvent NewEvent(Guid? id = null, DateTime? occurredAtUtc = null) { return new AuditEvent { EventId = id ?? Guid.NewGuid(), OccurredAtUtc = occurredAtUtc ?? DateTime.UtcNow, Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCall, Status = AuditStatus.Delivered, PayloadTruncated = false, }; } [Fact] public async Task WriteAsync_FreshEvent_PersistsWithForwardStatePending() { var (writer, dataSource) = CreateWriter(nameof(WriteAsync_FreshEvent_PersistsWithForwardStatePending)); await using var _ = writer; var evt = NewEvent(); await writer.WriteAsync(evt); using var connection = OpenVerifierConnection(dataSource); using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT ForwardState FROM AuditLog WHERE EventId = $id;"; cmd.Parameters.AddWithValue("$id", evt.EventId.ToString()); var actual = cmd.ExecuteScalar() as string; Assert.Equal(AuditForwardState.Pending.ToString(), actual); } [Fact] public async Task WriteAsync_Concurrent_1000Calls_All_Persist_NoExceptions() { var (writer, dataSource) = CreateWriter(nameof(WriteAsync_Concurrent_1000Calls_All_Persist_NoExceptions)); await using var _ = writer; var events = Enumerable.Range(0, 1000).Select(_ => NewEvent()).ToList(); await Parallel.ForEachAsync(events, new ParallelOptions { MaxDegreeOfParallelism = 16 }, async (evt, ct) => await writer.WriteAsync(evt, ct)); using var connection = OpenVerifierConnection(dataSource); using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM AuditLog;"; var count = Convert.ToInt64(cmd.ExecuteScalar()); Assert.Equal(1000, count); } [Fact] public async Task WriteAsync_DuplicateEventId_FirstWriteWins_NoException() { var (writer, dataSource) = CreateWriter(nameof(WriteAsync_DuplicateEventId_FirstWriteWins_NoException)); await using var _ = writer; var sharedId = Guid.NewGuid(); var first = NewEvent(sharedId) with { Target = "first" }; var second = NewEvent(sharedId) with { Target = "second" }; await writer.WriteAsync(first); await writer.WriteAsync(second); using var connection = OpenVerifierConnection(dataSource); using var countCmd = connection.CreateCommand(); countCmd.CommandText = "SELECT COUNT(*) FROM AuditLog WHERE EventId = $id;"; countCmd.Parameters.AddWithValue("$id", sharedId.ToString()); var count = Convert.ToInt64(countCmd.ExecuteScalar()); Assert.Equal(1, count); using var targetCmd = connection.CreateCommand(); targetCmd.CommandText = "SELECT Target FROM AuditLog WHERE EventId = $id;"; targetCmd.Parameters.AddWithValue("$id", sharedId.ToString()); Assert.Equal("first", targetCmd.ExecuteScalar() as string); } [Fact] public async Task WriteAsync_ForcesForwardStatePending_IfNull() { var (writer, dataSource) = CreateWriter(nameof(WriteAsync_ForcesForwardStatePending_IfNull)); await using var _ = writer; var evt = NewEvent() with { ForwardState = null }; await writer.WriteAsync(evt); using var connection = OpenVerifierConnection(dataSource); using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT ForwardState FROM AuditLog WHERE EventId = $id;"; cmd.Parameters.AddWithValue("$id", evt.EventId.ToString()); Assert.Equal(AuditForwardState.Pending.ToString(), cmd.ExecuteScalar() as string); } [Fact] public async Task ReadPendingAsync_Returns_OldestFirst_LimitedToN() { var (writer, _) = CreateWriter(nameof(ReadPendingAsync_Returns_OldestFirst_LimitedToN)); await using var _writer = writer; var baseTime = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc); var evts = new[] { NewEvent(occurredAtUtc: baseTime.AddSeconds(5)), NewEvent(occurredAtUtc: baseTime.AddSeconds(1)), NewEvent(occurredAtUtc: baseTime.AddSeconds(3)), NewEvent(occurredAtUtc: baseTime.AddSeconds(2)), NewEvent(occurredAtUtc: baseTime.AddSeconds(4)), }; foreach (var e in evts) { await writer.WriteAsync(e); } var rows = await writer.ReadPendingAsync(limit: 3); Assert.Equal(3, rows.Count); Assert.Equal(baseTime.AddSeconds(1), rows[0].OccurredAtUtc); Assert.Equal(baseTime.AddSeconds(2), rows[1].OccurredAtUtc); Assert.Equal(baseTime.AddSeconds(3), rows[2].OccurredAtUtc); } [Fact] public async Task MarkForwardedAsync_FlipsRowsToForwarded() { var (writer, dataSource) = CreateWriter(nameof(MarkForwardedAsync_FlipsRowsToForwarded)); await using var _ = writer; var ids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; foreach (var id in ids) { await writer.WriteAsync(NewEvent(id)); } await writer.MarkForwardedAsync(ids); using var connection = OpenVerifierConnection(dataSource); using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT ForwardState, COUNT(*) FROM AuditLog GROUP BY ForwardState;"; using var reader = cmd.ExecuteReader(); var byState = new Dictionary(); while (reader.Read()) { byState[reader.GetString(0)] = reader.GetInt64(1); } Assert.Equal(3, byState[AuditForwardState.Forwarded.ToString()]); Assert.False(byState.ContainsKey(AuditForwardState.Pending.ToString())); } [Fact] public async Task MarkForwardedAsync_NonExistentId_NoThrow() { var (writer, _) = CreateWriter(nameof(MarkForwardedAsync_NonExistentId_NoThrow)); await using var _writer = writer; var phantomIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; await writer.MarkForwardedAsync(phantomIds); // No assertion needed: the call must complete without throwing. } }