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}{tag}>", csproj);
}
}