using Microsoft.Data.Sqlite; namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests; /// /// LocalDb Phase 1 (Task 2) — the site_events DDL is extracted out of /// so the Host's AddZbLocalDb onReady callback can /// create it in the consolidated site database. /// /// The extraction also carries the one deliberate schema CHANGE of Phase 1: the primary /// key moves from INTEGER PRIMARY KEY AUTOINCREMENT to a TEXT GUID. Under the /// library's last-writer-wins replication, two site nodes each minting autoincrement id 1 /// would be treated as the same row and silently overwrite one another. GUID keys make /// the event log a pure union across the pair instead. /// /// public class SiteEventLogSchemaTests { private static SqliteConnection OpenConnection() { var conn = new SqliteConnection("Data Source=:memory:"); conn.Open(); return conn; } private static List<(string Name, string Type, bool NotNull, bool Pk)> TableInfo( SqliteConnection conn, string table) { using var cmd = conn.CreateCommand(); cmd.CommandText = $"PRAGMA table_info('{table}')"; using var reader = cmd.ExecuteReader(); var cols = new List<(string, string, bool, bool)>(); while (reader.Read()) { cols.Add((reader.GetString(1), reader.GetString(2), reader.GetInt32(3) == 1, reader.GetInt32(5) > 0)); } return cols; } [Fact] public void Apply_CreatesTableWithExpectedColumns() { using var conn = OpenConnection(); SiteEventLogSchema.Apply(conn); Assert.Equal( ["id", "timestamp", "event_type", "severity", "instance_id", "source", "message", "details"], TableInfo(conn, "site_events").Select(c => c.Name)); } [Fact] public void Apply_IdIsTextPrimaryKey_NotAutoincrement() { using var conn = OpenConnection(); SiteEventLogSchema.Apply(conn); var pk = Assert.Single(TableInfo(conn, "site_events"), c => c.Pk); Assert.Equal("id", pk.Name); Assert.Equal("TEXT", pk.Type); // AUTOINCREMENT creates the sqlite_sequence bookkeeping table. Its absence // is the load-bearing assertion: ids must be application-minted GUIDs, not // server-minted integers that collide across the replicating pair. using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'"; Assert.Equal(0L, Convert.ToInt64(cmd.ExecuteScalar())); } [Fact] public void Apply_RejectsIntegerIdInserts() { using var conn = OpenConnection(); SiteEventLogSchema.Apply(conn); // A TEXT PK with no AUTOINCREMENT means an INSERT omitting id is an error, // which is what forces the writer to mint one (Task 5a). using var cmd = conn.CreateCommand(); cmd.CommandText = """ INSERT INTO site_events (timestamp, event_type, severity, source, message) VALUES ('2026-07-19T00:00:00Z', 'Test', 'Info', 'test', 'no id supplied') """; Assert.Throws(() => cmd.ExecuteNonQuery()); } [Fact] public void Apply_HasNoBlobColumns() { using var conn = OpenConnection(); SiteEventLogSchema.Apply(conn); Assert.DoesNotContain( TableInfo(conn, "site_events"), c => c.Type.Contains("BLOB", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Apply_CreatesExpectedIndexes() { using var conn = OpenConnection(); SiteEventLogSchema.Apply(conn); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'site_events' " + "AND name NOT LIKE 'sqlite_%' ORDER BY name"; using var reader = cmd.ExecuteReader(); var indexes = new List(); while (reader.Read()) indexes.Add(reader.GetString(0)); Assert.Equal( ["idx_events_instance", "idx_events_severity", "idx_events_timestamp", "idx_events_type"], indexes); } [Fact] public void Apply_IsIdempotent() { using var conn = OpenConnection(); SiteEventLogSchema.Apply(conn); SiteEventLogSchema.Apply(conn); Assert.Equal(8, TableInfo(conn, "site_events").Count); } }