using Microsoft.Data.Sqlite; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Tracking; /// /// LocalDb Phase 1 (Task 2) — the OperationTracking DDL is extracted out of /// so it can also be applied from the Host's /// AddZbLocalDb onReady callback, which owns table creation for the consolidated /// site database. These tests pin the extracted schema against the shape the store has /// always created: the move must be behaviour-preserving. /// public class OperationTrackingSchemaTests { 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(); OperationTrackingSchema.Apply(conn); var cols = TableInfo(conn, "OperationTracking"); Assert.Equal( [ "TrackedOperationId", "Kind", "TargetSummary", "Status", "RetryCount", "LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc", "SourceInstanceId", "SourceScript", "SourceNode" ], cols.Select(c => c.Name)); } [Fact] public void Apply_UsesTextPrimaryKey() { using var conn = OpenConnection(); OperationTrackingSchema.Apply(conn); // RegisterReplicated requires an explicit primary key and rejects BLOB // columns; a TEXT PK is what makes this table replicate as-is. var pk = Assert.Single(TableInfo(conn, "OperationTracking"), c => c.Pk); Assert.Equal("TrackedOperationId", pk.Name); Assert.Equal("TEXT", pk.Type); } [Fact] public void Apply_HasNoBlobColumns() { using var conn = OpenConnection(); OperationTrackingSchema.Apply(conn); // ILocalDb.RegisterReplicated throws on any column whose declared type // contains BLOB (json_object cannot capture them). Assert.DoesNotContain( TableInfo(conn, "OperationTracking"), c => c.Type.Contains("BLOB", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Apply_CreatesExpectedIndexes() { using var conn = OpenConnection(); OperationTrackingSchema.Apply(conn); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'OperationTracking' " + "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( ["IX_OperationTracking_Status_Updated", "IX_OperationTracking_UpdatedAt"], indexes); } [Fact] public void Apply_IsIdempotent() { using var conn = OpenConnection(); OperationTrackingSchema.Apply(conn); OperationTrackingSchema.Apply(conn); Assert.Equal(13, TableInfo(conn, "OperationTracking").Count); } [Fact] public void Apply_AddsSourceNodeToLegacyTableMissingIt() { using var conn = OpenConnection(); // A tracking DB created by a pre-SourceNode build. CREATE TABLE IF NOT // EXISTS will not add the column, so Apply must ALTER it in — the // additive-migration behaviour the store has today must survive extraction. using (var legacy = conn.CreateCommand()) { legacy.CommandText = """ CREATE TABLE OperationTracking ( TrackedOperationId TEXT NOT NULL PRIMARY KEY, Kind TEXT NOT NULL, TargetSummary TEXT NULL, Status TEXT NOT NULL, RetryCount INTEGER NOT NULL DEFAULT 0, LastError TEXT NULL, HttpStatus INTEGER NULL, CreatedAtUtc TEXT NOT NULL, UpdatedAtUtc TEXT NOT NULL, TerminalAtUtc TEXT NULL, SourceInstanceId TEXT NULL, SourceScript TEXT NULL ); """; legacy.ExecuteNonQuery(); } OperationTrackingSchema.Apply(conn); Assert.Contains(TableInfo(conn, "OperationTracking"), c => c.Name == "SourceNode"); } }