fix(sql-reaper): validate the GUID suffix and QUOTENAME the drop DDL; short reaper connect timeout; mint names from the prefix constant

This commit is contained in:
Joseph Doherty
2026-08-17 05:52:45 -04:00
parent 98d6a1908e
commit 580b77cecf
3 changed files with 128 additions and 23 deletions
@@ -1,5 +1,6 @@
using Microsoft.Data.SqlClient;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
@@ -14,6 +15,14 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
/// 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>
/// <remarks>
/// Joined to <see cref="AdminUiSqlServerCollection"/> deliberately, though it never touches the
/// fixture: xunit builds a collection fixture before running the collection's tests and serialises them
/// within it, which is what keeps <see cref="AdminUiSqlServerFixture"/>'s own startup reaper from
/// racing this one. Left in its own (parallel) collection, that reaper can drop the aged database this
/// test plants, between the plant and the assertion — observed as an intermittent failure.
/// </remarks>
[Collection(nameof(AdminUiSqlServerCollection))]
public sealed class SqlTestDatabaseReaperTests
{
[SqlServerFact]
@@ -85,6 +94,36 @@ public sealed class SqlTestDatabaseReaperTests
}
}
[SqlServerFact]
public void Reap_leaves_an_aged_name_whose_suffix_is_not_a_guid_alone()
{
var master = AdminUiSqlServer.ConnectionStringFor("master");
// Aged enough to be reaped on the timestamp alone — only the suffix check saves it. That check is
// what bounds the character set of every candidate: without it a database named
// "<prefix><aged timestamp>_" + arbitrary text would be fed to the drop statement, and before the
// statement was parameterised that was a second-order injection running as sa.
var notAGuid = $"{AdminUiSqlServer.TestDatabasePrefix}20250101000000_notaguid";
CreateDatabase(master, notAGuid);
try
{
var reaped = SqlTestDatabaseReaper.ReapOrphans(
master,
AdminUiSqlServer.TestDatabasePrefix,
DateTime.UtcNow - TimeSpan.FromHours(24));
reaped.ShouldNotContain(notAGuid);
DatabaseExists(master, notAGuid)
.ShouldBeTrue("only names shaped exactly as this code mints them are ever dropped");
}
finally
{
SqlTestDatabaseReaper.TryDropDatabase(master, notAGuid);
}
}
private static void CreateDatabase(string masterConnectionString, string databaseName)
{
using var conn = new SqlConnection(masterConnectionString);