| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S8;
- /// <summary>
- /// S8-LEGACY-NOTIFY-DECOMMISSION-1 · 收尾:旧通知分层<b>数据</b>的退役脚本契约。
- ///
- /// <para><b>为什么用静态契约测试而不是跑真库</b>:这条迁移只有一句 DELETE,
- /// 而它的风险恰恰不在「能不能删掉」,在「会不会顺手删错表」。
- /// 一条 DELETE 写错表名的代价是把当前唯一在用的通知配置或规则责任池清空 ——
- /// 那种损失没有回滚点。因此这里守的是<b>脚本边界</b>:只准碰一张表、只准 DELETE、
- /// 不准出现任何 DDL。</para>
- ///
- /// <para>真库执行由 <c>AutoVersionUpdate</c> 在部署时完成,verify 脚本会当场断言残留为 0。
- /// 本批<b>没有</b>、也不应该为了"证明删得掉"去动 shared DB。</para>
- /// </summary>
- 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);
- }
- /// <summary>去掉 `--` 注释行 —— 注释里写了某个词不等于脚本会执行它。</summary>
- private static string SqlOnly(string fileName) =>
- string.Join('\n', Read(fileName).Split('\n')
- .Where(l => !l.TrimStart().StartsWith("--", StringComparison.Ordinal)));
- /// <summary>迁移与 verify 两个文件都必须存在(项目既有规范)。</summary>
- [Fact]
- public void MigrationAndVerifyScriptsExist()
- {
- Assert.False(string.IsNullOrWhiteSpace(Read($"{Version}.sql")));
- Assert.False(string.IsNullOrWhiteSpace(Read($"{Version}.verify.sql")));
- }
- /// <summary>
- /// 迁移<b>只删一张表</b>,且是纯数据操作。
- /// <para>DDL 一律禁止:本批的边界是「清数据不删表」,表 / Entity / CRUD 留给 P1-C3。</para>
- /// </summary>
- [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"));
- }
- /// <summary>
- /// <b>不得触碰任何其它表。</b>
- /// <para>逐一点名而不是只看语句数:将来若有人在这里追加一条,
- /// 这份名单会立刻指出他碰了哪张不该碰的表。</para>
- /// </summary>
- [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);
- }
- /// <summary>
- /// 幂等:重跑就是再 DELETE 一次空表,影响 0 行、不报错。
- /// <para>判据是「语句本身天然幂等」——不依赖任何 IF EXISTS 包装,
- /// 也不依赖执行框架的去重(框架按 version + hash 记录,本就不会重复执行成功过的版本)。</para>
- /// </summary>
- [Fact]
- public void Migration_IsNaturallyIdempotent()
- {
- var sql = SqlOnly($"{Version}.sql").Trim();
- Assert.StartsWith("DELETE FROM", sql, StringComparison.OrdinalIgnoreCase);
- Assert.DoesNotContain("WHERE", sql, StringComparison.OrdinalIgnoreCase);
- }
- /// <summary>
- /// verify 断言残留为 0,并顺带守住「表还在、别的表也还在」。
- /// <para>遵循仓库既有 verify 契约:每条语句自包含、返回布尔标量、禁止 <c>SET @var</c>。</para>
- /// </summary>
- [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);
- }
- /// <summary>
- /// csproj 必须同时满足:版本号 ≥ 本脚本编号,且两个脚本都注册了 Copy 条目。
- /// <para>少了 Copy 条目脚本不会被复制到输出目录,<c>AutoVersionUpdate</c> 扫不到它 ——
- /// 那是一种「文件在仓库里、但永远不会执行」的静默失效。</para>
- /// </summary>
- [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($@"<None Update=""UpdateScripts\{Version}.sql"">", csproj);
- Assert.Contains($@"<None Update=""UpdateScripts\{Version}.verify.sql"">", csproj);
- var m = System.Text.RegularExpressions.Regex.Match(csproj, @"<Version>([\d.]+)</Version>");
- Assert.True(m.Success, "csproj 未找到 <Version>");
- 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}</{tag}>", csproj);
- }
- }
|