using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S8; /// /// S8-LEGACY-NOTIFY-DECOMMISSION-1 · 收尾:旧通知分层数据的退役脚本契约。 /// /// 为什么用静态契约测试而不是跑真库:这条迁移只有一句 DELETE, /// 而它的风险恰恰不在「能不能删掉」,在「会不会顺手删错表」。 /// 一条 DELETE 写错表名的代价是把当前唯一在用的通知配置或规则责任池清空 —— /// 那种损失没有回滚点。因此这里守的是脚本边界:只准碰一张表、只准 DELETE、 /// 不准出现任何 DDL。 /// /// 真库执行由 AutoVersionUpdate 在部署时完成,verify 脚本会当场断言残留为 0。 /// 本批没有、也不应该为了"证明删得掉"去动 shared DB。 /// public class S8LegacyNotifyDataRetirementTests { private const string Version = "1.0.505"; private static string ScriptsDir() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, "Admin.NET.Web.Entry"))) dir = dir.Parent; Assert.NotNull(dir); return Path.Combine(dir!.FullName, "Admin.NET.Web.Entry", "UpdateScripts"); } private static string Read(string fileName) { var path = Path.Combine(ScriptsDir(), fileName); Assert.True(File.Exists(path), $"升级脚本不存在:{path}"); return File.ReadAllText(path); } /// 去掉 `--` 注释行 —— 注释里写了某个词不等于脚本会执行它。 private static string SqlOnly(string fileName) => string.Join('\n', Read(fileName).Split('\n') .Where(l => !l.TrimStart().StartsWith("--", StringComparison.Ordinal))); /// 迁移与 verify 两个文件都必须存在(项目既有规范)。 [Fact] public void MigrationAndVerifyScriptsExist() { Assert.False(string.IsNullOrWhiteSpace(Read($"{Version}.sql"))); Assert.False(string.IsNullOrWhiteSpace(Read($"{Version}.verify.sql"))); } /// /// 迁移只删一张表,且是纯数据操作。 /// DDL 一律禁止:本批的边界是「清数据不删表」,表 / Entity / CRUD 留给 P1-C3。 /// [Fact] public void Migration_OnlyDeletesLegacyNotificationLayer() { var sql = SqlOnly($"{Version}.sql"); Assert.Contains("DELETE FROM `ado_s8_notification_layer`;", sql); // 只有这一条语句 Assert.Equal(1, sql.Split(';').Count(seg => !string.IsNullOrWhiteSpace(seg))); // 任何 DDL 或结构变更都不允许 foreach (var banned in new[] { "DROP TABLE", "DROP ", "ALTER TABLE", "CREATE TABLE", "TRUNCATE", "RENAME", "INSERT INTO", "UPDATE " }) Assert.DoesNotContain(banned, sql, StringComparison.OrdinalIgnoreCase); // AutoVersionUpdate 会整份拒绝含 DELIMITER 的脚本 Assert.DoesNotContain("DELIMITER", Read($"{Version}.sql")); } /// /// 不得触碰任何其它表。 /// 逐一点名而不是只看语句数:将来若有人在这里追加一条, /// 这份名单会立刻指出他碰了哪张不该碰的表。 /// [Fact] public void Migration_TouchesNoOtherTable() { var sql = SqlOnly($"{Version}.sql"); foreach (var protectedTable in new[] { "ado_s8_notification_recipient", "ado_s8_watch_rule", "ado_s8_rule_handler_user", "ado_s8_rule_responsibility_user", "ado_s8_exception", "ado_s8_exception_type", "ado_s8_exception_action_role", "ado_s8_scene_config", "SysRole", "SysUser", "SysUserRole", "mdp_", "dim_", }) Assert.DoesNotContain(protectedTable, sql, StringComparison.OrdinalIgnoreCase); } /// /// 幂等:重跑就是再 DELETE 一次空表,影响 0 行、不报错。 /// 判据是「语句本身天然幂等」——不依赖任何 IF EXISTS 包装, /// 也不依赖执行框架的去重(框架按 version + hash 记录,本就不会重复执行成功过的版本)。 /// [Fact] public void Migration_IsNaturallyIdempotent() { var sql = SqlOnly($"{Version}.sql").Trim(); Assert.StartsWith("DELETE FROM", sql, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("WHERE", sql, StringComparison.OrdinalIgnoreCase); } /// /// verify 断言残留为 0,并顺带守住「表还在、别的表也还在」。 /// 遵循仓库既有 verify 契约:每条语句自包含、返回布尔标量、禁止 SET @var /// [Fact] public void Verify_AssertsZeroResidueAndNoCollateralDamage() { var sql = SqlOnly($"{Version}.verify.sql"); Assert.Contains("(SELECT COUNT(*) FROM `ado_s8_notification_layer`) = 0", sql); // 表本身不得被删(本批只清数据) Assert.Contains("TABLE_NAME = 'ado_s8_notification_layer') = 1", sql); // 不该被动的表仍在 Assert.Contains("ado_s8_notification_recipient", sql); Assert.Contains("ado_s8_rule_responsibility_user", sql); // 契约:不得使用 SET @var(返回 null 会被判失败)。 // 只看去注释后的 SQL —— 契约说明本身就写着「禁止 SET @var」,查原文会自匹配。 Assert.DoesNotContain("SET @", sql, StringComparison.OrdinalIgnoreCase); // verify 只读,不得改数据 foreach (var banned in new[] { "DELETE", "INSERT", "UPDATE", "DROP", "ALTER", "CREATE" }) Assert.DoesNotContain(banned, sql, StringComparison.OrdinalIgnoreCase); } /// /// csproj 必须同时满足:版本号 ≥ 本脚本编号,且两个脚本都注册了 Copy 条目。 /// 少了 Copy 条目脚本不会被复制到输出目录,AutoVersionUpdate 扫不到它 —— /// 那是一种「文件在仓库里、但永远不会执行」的静默失效。 /// [Fact] public void Csproj_RegistersScriptsAndVersionIsAtLeastScriptVersion() { var csprojPath = Path.Combine( new DirectoryInfo(ScriptsDir()).Parent!.FullName, "Admin.NET.Web.Entry.csproj"); var csproj = File.ReadAllText(csprojPath); Assert.Contains($@"", csproj); Assert.Contains($@"", csproj); var m = System.Text.RegularExpressions.Regex.Match(csproj, @"([\d.]+)"); Assert.True(m.Success, "csproj 未找到 "); Assert.True(System.Version.Parse(m.Groups[1].Value) >= System.Version.Parse(Version), $"csproj 版本 {m.Groups[1].Value} 低于脚本编号 {Version},AutoVersionUpdate 不会应用它"); // 三处版本号必须同号 foreach (var tag in new[] { "AssemblyVersion", "FileVersion", "Version" }) Assert.Contains($"<{tag}>{m.Groups[1].Value}", csproj); } }