using Microsoft.Data.Sqlite; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Persistence; /// /// Pins as the single owner of the site config DDL. /// The Host applies this to a LocalDb-managed connection before /// RegisterReplicated installs the capture triggers, so the shape it produces /// is the shape that replicates. /// public class SiteStorageSchemaTests { [Fact] public void Apply_IsIdempotent_AndCreatesEveryTable() { var path = Path.Combine(Path.GetTempPath(), $"site-schema-{Guid.NewGuid():N}.db"); try { using var connection = new SqliteConnection($"Data Source={path}"); connection.Open(); SiteStorageSchema.Apply(connection); SiteStorageSchema.Apply(connection); // second run must not throw // Every table is named explicitly on purpose — a loop asserting "9 tables" // would still pass if one were renamed. foreach (var table in new[] { "deployed_configurations", "static_attribute_overrides", "shared_scripts", "external_systems", "database_connections", "notification_lists", "data_connection_definitions", "smtp_configurations", "native_alarm_state", }) { Assert.True(TableExists(connection, table), $"missing table: {table}"); } } finally { SqliteConnection.ClearAllPools(); File.Delete(path); } } /// /// The migration columns exist to upgrade a database created by an older build in /// place. Asserting only against a freshly-created table would pass even if every /// ALTER were deleted, because CREATE TABLE already lists them — so /// this starts from the pre-migration shape and proves the migration path runs. /// [Fact] public void Apply_AddsMigrationColumns_ToALegacyDatabase() { var path = Path.Combine(Path.GetTempPath(), $"site-schema-legacy-{Guid.NewGuid():N}.db"); try { using var connection = new SqliteConnection($"Data Source={path}"); connection.Open(); // The shapes as they existed before backup_configuration / // failover_retry_count / metadata_json / timeout_seconds were added. using (var legacy = connection.CreateCommand()) { legacy.CommandText = """ CREATE TABLE data_connection_definitions ( name TEXT PRIMARY KEY, protocol TEXT NOT NULL, configuration TEXT, updated_at TEXT NOT NULL ); CREATE TABLE native_alarm_state ( instance_unique_name TEXT NOT NULL, source_canonical_name TEXT NOT NULL, source_reference TEXT NOT NULL, condition_json TEXT NOT NULL, last_transition_at TEXT NOT NULL, PRIMARY KEY (instance_unique_name, source_canonical_name, source_reference) ); CREATE TABLE external_systems ( name TEXT PRIMARY KEY, endpoint_url TEXT NOT NULL, auth_type TEXT NOT NULL, auth_configuration TEXT, method_definitions TEXT, updated_at TEXT NOT NULL ); INSERT INTO external_systems (name, endpoint_url, auth_type, updated_at) VALUES ('legacy-system', 'http://example.invalid', 'None', '2026-01-01T00:00:00Z'); """; legacy.ExecuteNonQuery(); } SiteStorageSchema.Apply(connection); Assert.True(ColumnExists(connection, "data_connection_definitions", "backup_configuration")); Assert.True(ColumnExists(connection, "data_connection_definitions", "failover_retry_count")); Assert.True(ColumnExists(connection, "native_alarm_state", "metadata_json")); Assert.True(ColumnExists(connection, "external_systems", "timeout_seconds")); // The pre-existing row survives the upgrade and reads back the NOT NULL // column's default rather than failing. using var check = connection.CreateCommand(); check.CommandText = "SELECT timeout_seconds FROM external_systems WHERE name = 'legacy-system'"; Assert.Equal(0, Convert.ToInt32(check.ExecuteScalar())); } finally { SqliteConnection.ClearAllPools(); File.Delete(path); } } private static bool TableExists(SqliteConnection connection, string table) { using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = $name"; cmd.Parameters.AddWithValue("$name", table); return Convert.ToInt32(cmd.ExecuteScalar()) > 0; } private static bool ColumnExists(SqliteConnection connection, string table, string column) { using var cmd = connection.CreateCommand(); cmd.CommandText = $"SELECT COUNT(*) FROM pragma_table_info('{table}') WHERE name = $name"; cmd.Parameters.AddWithValue("$name", column); return Convert.ToInt32(cmd.ExecuteScalar()) > 0; } }