using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ScadaLink.AuditLog.Site; namespace ScadaLink.AuditLog.Tests.Site; /// /// Bundle B (M2-T1) schema-bootstrap tests for . /// Uses an in-memory shared-cache SQLite database so the same connection name /// reaches the same file-less db across both the writer and the verifier. /// public class SqliteAuditWriterSchemaTests { /// /// Each test uses a unique shared-cache in-memory database. The /// "Mode=Memory;Cache=Shared" syntax lets two SqliteConnections see the same /// in-memory store as long as both use the same Data Source name. /// private static (SqliteAuditWriter writer, string dataSource) CreateWriter(string testName) { var dataSource = $"file:{testName}-{Guid.NewGuid():N}?mode=memory&cache=shared"; var options = new SqliteAuditWriterOptions { DatabasePath = dataSource, }; // The writer uses raw "Data Source={path}" by appending Cache=Shared. Override // by passing the full connection string via the connectionStringOverride hook. var writer = new SqliteAuditWriter( Options.Create(options), 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; } [Fact] public void Opens_Creates_AuditLog_Table_With_21Columns_And_PK_On_EventId() { var (writer, dataSource) = CreateWriter(nameof(Opens_Creates_AuditLog_Table_With_21Columns_And_PK_On_EventId)); using (writer) { using var connection = OpenVerifierConnection(dataSource); using var cmd = connection.CreateCommand(); cmd.CommandText = "PRAGMA table_info(AuditLog);"; using var reader = cmd.ExecuteReader(); var columns = new List<(string Name, int Pk)>(); while (reader.Read()) { columns.Add((reader.GetString(1), reader.GetInt32(5))); } Assert.Equal(21, columns.Count); var expected = new[] { "EventId", "OccurredAtUtc", "Channel", "Kind", "CorrelationId", "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra", "ForwardState", "ExecutionId", }; Assert.Equal(expected.OrderBy(n => n), columns.Select(c => c.Name).OrderBy(n => n)); // PK is EventId only. var pkColumns = columns.Where(c => c.Pk > 0).Select(c => c.Name).ToList(); Assert.Single(pkColumns); Assert.Equal("EventId", pkColumns[0]); } } [Fact] public void Opens_Creates_IX_ForwardState_Occurred_Index() { var (writer, dataSource) = CreateWriter(nameof(Opens_Creates_IX_ForwardState_Occurred_Index)); using (writer) { using var connection = OpenVerifierConnection(dataSource); using var cmd = connection.CreateCommand(); cmd.CommandText = "PRAGMA index_list(AuditLog);"; using var reader = cmd.ExecuteReader(); var indexNames = new List(); while (reader.Read()) { indexNames.Add(reader.GetString(1)); } Assert.Contains("IX_SiteAuditLog_ForwardState_Occurred", indexNames); // Verify the index columns are ForwardState, OccurredAtUtc in that order. using var infoCmd = connection.CreateCommand(); infoCmd.CommandText = "PRAGMA index_info(IX_SiteAuditLog_ForwardState_Occurred);"; using var infoReader = infoCmd.ExecuteReader(); var indexColumns = new List(); while (infoReader.Read()) { indexColumns.Add(infoReader.GetString(2)); } Assert.Equal(new[] { "ForwardState", "OccurredAtUtc" }, indexColumns); } } [Fact] public void PRAGMA_auto_vacuum_Is_INCREMENTAL() { var (writer, dataSource) = CreateWriter(nameof(PRAGMA_auto_vacuum_Is_INCREMENTAL)); using (writer) { using var connection = OpenVerifierConnection(dataSource); using var cmd = connection.CreateCommand(); cmd.CommandText = "PRAGMA auto_vacuum;"; var value = Convert.ToInt32(cmd.ExecuteScalar()); // INCREMENTAL = 2 (0 = NONE, 1 = FULL, 2 = INCREMENTAL). Assert.Equal(2, value); } } }