using Admin.NET.Plugin.AiDOP.Service.S8; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S8; /// /// S8-RULE-4TAB-1:审核来源只读呈现。 /// /// 本批刻意不做的事(产品决策已锁定):不新建 Rule 级 Reviewer Pool、 /// 不改 ApproverType、不动审批流节点、不改 owner/escalate 业务语义。 /// 这些"不做"必须被测试守住 —— 否则下一个人很容易顺手"补全"一个 reviewer_user_ids, /// 于是同一个问题又出现两套 authority。 /// /// 为什么要做这个只读视图:规则页此前把审核人显示为「主管」, /// 而系统里根本不存在组织主管关系(SysOrg.DirectorId 与 /// SysUser.ManagerUserId 实测填充率均为 0)。管理员据此以为审核链是通的, /// 实际上 UAT 租户的复核流程审批人在本租户下解析为 0 人、 /// 升级流程三个节点全部指向默认租户的一个演示账号。 /// public class S8ReviewSourceTests { private static readonly string PluginRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../Admin.NET.Plugin.AiDOP")); private static string CodeOnly(string relativePath) { var full = Path.Combine(PluginRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); Assert.True(File.Exists(full), $"源码文件不存在,路径需同步更新:{full}"); return string.Join('\n', File.ReadAllLines(full) .Where(l => { var t = l.TrimStart(); return !t.StartsWith("///", StringComparison.Ordinal) && !t.StartsWith("//", StringComparison.Ordinal); })); } // ══════════════════════════════════════════════════════════════════════ // 节点解析:宽松读取,认不出就说不知道,绝不猜 // ══════════════════════════════════════════════════════════════════════ /// 真实流程定义(UAT 租户 EXCEPTION_CLOSURE 的实际 JSON 形态)能被解出。 [Fact] public void ParseApproverNodes_ReadsRealFlowShape() { const string json = """ {"nodes":[{"nodeKey":"n1","nodeName":"负责人确认","approverType":2,"approverIds":"1300000000101,1300000000888"}]} """; var nodes = S8ReviewSourceService.ParseApproverNodes(json); Assert.Single(nodes); Assert.Equal("负责人确认", nodes[0].NodeName); Assert.Equal(2, nodes[0].Type); // ApproverTypeEnum.Role Assert.Equal("1300000000101,1300000000888", nodes[0].Ids); } /// 嵌套结构同样能找到节点(流程定义的外层结构由 ApprovalFlow 插件决定,不该写死)。 [Fact] public void ParseApproverNodes_WalksNestedStructures() { const string json = """ {"flow":{"children":[{"branches":[[{"nodeName":"厂级审批","approverType":1,"approverIds":"789819678195781"}]]}]}} """; var nodes = S8ReviewSourceService.ParseApproverNodes(json); Assert.Single(nodes); Assert.Equal("厂级审批", nodes[0].NodeName); Assert.Equal(1, nodes[0].Type); // SpecificUser } /// /// 空 / 无审批人的定义解出 0 个节点,不抛异常。 /// 上层会显示「本租户没有该流程」,而不是崩在配置页上。 /// [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData("""{"nodes":[]}""")] [InlineData("""{"nodes":[{"nodeName":"仅抄送"}]}""")] public void ParseApproverNodes_HandlesEmptyGracefully(string? json) { Assert.Empty(S8ReviewSourceService.ParseApproverNodes(json)); } /// 认不出的 approverType 一律跳过,不猜成某个默认值。 [Fact] public void ParseApproverNodes_SkipsUnrecognizedApproverType() { const string json = """ {"nodes":[{"nodeName":"怪节点","approverType":{"weird":true},"approverIds":"x"}, {"nodeName":"正常","approverType":2,"approverIds":"ROLE_X"}]} """; var nodes = S8ReviewSourceService.ParseApproverNodes(json); Assert.Single(nodes); Assert.Equal("正常", nodes[0].NodeName); } /// 字符串形式的 approverType 也接受(不同版本的流程设计器写法不一)。 [Fact] public void ParseApproverNodes_AcceptsStringApproverType() { const string json = """{"nodes":[{"nodeName":"n","approverType":"Role","approverIds":"ROLE_A"}]}"""; var nodes = S8ReviewSourceService.ParseApproverNodes(json); Assert.Single(nodes); Assert.Equal(2, nodes[0].Type); } // ══════════════════════════════════════════════════════════════════════ // 只读边界:本批锁定的"不做"必须被守住 // ══════════════════════════════════════════════════════════════════════ /// /// 审核来源服务绝不写库,也不新建 reviewer 概念。 /// /// 产品决策:不新建 Rule 级 Reviewer Pool、不改 ApproverType、不动流程节点。 /// 这条守卫防的是下一个人"顺手补全"—— 一旦出现 reviewer_user_ids, /// 审核人就有了两个 authority,而现有审批流那套还在跑。 /// [Fact] public void ReviewSource_IsStrictlyReadOnly() { var code = CodeOnly("Service/S8/S8ReviewSourceService.cs"); foreach (var write in new[] { "AsInsertable", "AsUpdateable", "AsDeleteable", "ExecuteCommandAsync", "InsertAsync", "UpdateAsync" }) Assert.DoesNotContain(write, code); foreach (var invented in new[] { "ReviewerPool", "reviewer_user_ids", "ReviewerUserIds", "IS8ReviewerPool" }) Assert.DoesNotContain(invented, code); } /// /// 「主管」这类没有真实实体支持的文案不得出现在审核展示里。 /// /// 系统里 SysOrg.DirectorIdSysUser.ManagerUserId 填充率均为 0, /// 显示「主管」等于告诉管理员一个不存在的东西在负责审核。 /// [Fact] public void ReviewSource_DoesNotShowPhantomSupervisor() { var code = CodeOnly("Service/S8/S8ReviewSourceService.cs"); Assert.DoesNotContain("主管", code); Assert.DoesNotContain("总监", code); var page = File.ReadAllText(Path.GetFullPath(Path.Combine( PluginRoot, "../../../Web/src/views/aidop/s8/config/S8WatchRuleConfigPage.vue"))); // 页面里只允许在"说明为什么不用这个词"的注释中出现,可执行模板不得渲染它 var template = page[page.IndexOf("