using Microsoft.Data.SqlClient; using Shouldly; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests; /// /// Pins against the real shared SQL Server: an aged orphan is /// dropped, a fresh sibling is not. /// /// /// A pin, not a red-first test. The reaper is time-gated and the behaviour it protects — "a /// database created by a run that is long over gets dropped, one created by a run that may still be /// going does not" — has no observable failing state to start from; what it needs is a guard against /// someone later widening the age window, the LIKE, or the timestamp parse. It therefore drives /// the internal core with an explicit cutoff rather than waiting 24 hours. /// public sealed class SqlTestDatabaseReaperTests { [SqlServerFact] public void Reap_drops_databases_older_than_the_cutoff_and_leaves_fresh_ones_alone() { var master = AdminUiSqlServer.ConnectionStringFor("master"); // A name that carries a timestamp from 2025 — unambiguously older than any cutoff this suite uses. var aged = $"{AdminUiSqlServer.TestDatabasePrefix}20250101000000_{Guid.NewGuid():N}"; var fresh = $"{AdminUiSqlServer.TestDatabasePrefix}{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}"; CreateDatabase(master, aged); try { CreateDatabase(master, fresh); try { var reaped = SqlTestDatabaseReaper.ReapOrphans( master, AdminUiSqlServer.TestDatabasePrefix, DateTime.UtcNow - TimeSpan.FromHours(24)); reaped.ShouldContain(aged); DatabaseExists(master, aged).ShouldBeFalse("the aged orphan should have been reaped"); reaped.ShouldNotContain(fresh); DatabaseExists(master, fresh).ShouldBeTrue("a database from a possibly-live run must survive"); } finally { SqlTestDatabaseReaper.TryDropDatabase(master, fresh); } } finally { // No-op when the reaper already took it; belt-and-braces if an assertion failed first. SqlTestDatabaseReaper.TryDropDatabase(master, aged); } } [SqlServerFact] public void Reap_leaves_a_matching_name_whose_timestamp_does_not_parse_alone() { var master = AdminUiSqlServer.ConnectionStringFor("master"); var unparseable = $"{AdminUiSqlServer.TestDatabasePrefix}notatimestamp_{Guid.NewGuid():N}"; CreateDatabase(master, unparseable); try { // The cutoff stays at the production 24 hours — deliberately NOT a far-future one, which would // sweep away the live database of an AdminUI fixture running in parallel on this shared server. // It still bites: an unparseable name that fell through to default(DateTime) would read as year // 0001 and be dropped. var reaped = SqlTestDatabaseReaper.ReapOrphans( master, AdminUiSqlServer.TestDatabasePrefix, DateTime.UtcNow - TimeSpan.FromHours(24)); reaped.ShouldNotContain(unparseable); DatabaseExists(master, unparseable) .ShouldBeTrue("a name this code could not have minted is never dropped"); } finally { SqlTestDatabaseReaper.TryDropDatabase(master, unparseable); } } private static void CreateDatabase(string masterConnectionString, string databaseName) { using var conn = new SqlConnection(masterConnectionString); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = $"CREATE DATABASE [{databaseName}];"; cmd.ExecuteNonQuery(); } private static bool DatabaseExists(string masterConnectionString, string databaseName) { using var conn = new SqlConnection(masterConnectionString); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT DB_ID(@name);"; cmd.Parameters.Add(new SqlParameter("@name", databaseName)); return cmd.ExecuteScalar() is not (null or DBNull); } }