110 lines
4.2 KiB
C#
110 lines
4.2 KiB
C#
using Microsoft.Data.SqlClient;
|
|
using Shouldly;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
|
|
|
|
/// <summary>
|
|
/// Pins <see cref="SqlTestDatabaseReaper"/> against the real shared SQL Server: an aged orphan is
|
|
/// dropped, a fresh sibling is not.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A <b>pin, not a red-first test</b>. 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 <c>LIKE</c>, or the timestamp parse. It therefore drives
|
|
/// the internal core with an explicit cutoff rather than waiting 24 hours.
|
|
/// </remarks>
|
|
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);
|
|
}
|
|
}
|