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;
}
}