using Xunit; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations; /// /// Content guard for the FixAuditPurgerRoleGrants migration (arch-review 04, S3). /// The switch-out dance CREATEs a staging table and the per-channel override runs a bounded /// row DELETE — but the shipped scadabridge_audit_purger role only carried /// SELECT + ALTER ON SCHEMA::dbo, which does NOT confer the database-level /// CREATE TABLE permission nor row-level DELETE. This test asserts the migration /// source grants both, so a segregated maintenance principal can genuinely execute the purge. /// A pure source-content scan (no MSSQL required) — the schema-application sanity check lives /// in the MSSQL migration suite. /// public class FixAuditPurgerRoleGrantsTests { private static string ReadMigrationSource() { var migrationsDir = Path.Combine( GetConfigurationDatabaseSourceDirectory(), "Migrations"); // Timestamp-prefixed filename; match the non-Designer partial only. var file = Directory.GetFiles(migrationsDir, "*_FixAuditPurgerRoleGrants.cs") .FirstOrDefault(f => !f.EndsWith(".Designer.cs", StringComparison.OrdinalIgnoreCase)); Assert.True(file is not null, "Expected a *_FixAuditPurgerRoleGrants.cs migration under " + migrationsDir + " — scaffold it with `dotnet ef migrations add FixAuditPurgerRoleGrants` (build first, NO --no-build)."); return File.ReadAllText(file!); } [Fact] public void Migration_Grants_CreateTable_To_Purger() { var src = ReadMigrationSource(); Assert.Contains("GRANT CREATE TABLE TO scadabridge_audit_purger", src); } [Fact] public void Migration_Grants_Delete_On_AuditLog_To_Purger() { var src = ReadMigrationSource(); Assert.Contains("GRANT DELETE ON dbo.AuditLog TO scadabridge_audit_purger", src); } [Fact] public void Down_Revokes_Both_Grants() { var src = ReadMigrationSource(); // Down must reverse both grants so the migration is cleanly reversible. Assert.Contains("REVOKE CREATE TABLE", src); Assert.Contains("REVOKE DELETE ON dbo.AuditLog", src); } // Same walk-up anchor pattern the append-only guard uses. private static string GetConfigurationDatabaseSourceDirectory() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) { var candidate = Path.Combine( dir.FullName, "src", "ZB.MOM.WW.ScadaBridge.ConfigurationDatabase", "ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.csproj"); if (File.Exists(candidate)) { return Path.GetDirectoryName(candidate)!; } dir = dir.Parent; } throw new InvalidOperationException( "Could not locate ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.csproj by walking up from the test output directory."); } }