using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.ApprovalFlow; /// /// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1 源码契约测试。 /// /// 背景:FlowEngineService 全程依赖 SqlSugarRepository<T>, /// 裸 xUnit 进程无法实例化(无参构造触发 Furion.App 静态构造,在无宿主进程中抛 /// TypeInitializationException),因此本仓对这一层历来采用源码契约测试 /// (grep 断言代码形状),而非行为级 DB 集成测试——本文件延续该既有惯例。 /// /// 覆盖范围:验证 §14 六个场景要求的代码形状是否落地,而非跑真实数据库。 /// 真正的行为级验证在运行态验收(Scheduler 真跑 GWR-S6-WO-DELAY)里完成。 /// public class S8ExceptionFlowTenantContextContractTests { private static string FlowEngine() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "FlowEngineService.cs")); private static string S8ManualReportService() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Service", "S8", "S8ManualReportService.cs")); private static string S8TaskFlowService() => File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Service", "S8", "S8TaskFlowService.cs")); private static string FindFile(params string[] parts) { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) { var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray()); if (File.Exists(candidate)) return candidate; dir = dir.Parent; } throw new FileNotFoundException(string.Join("/", parts)); } // ── Case 1 / Case 16:HTTP 人工路径必须原样保留,不接受客户端可提交的 TenantId ── [Fact] public void PublicStartFlow_StillUsesUserManagerTenantId_NoRegressionForHttpCallers() { Assert.Contains( "public Task StartFlow(StartFlowInput input) => StartFlowCore(input, _userManager.TenantId);", FlowEngine()); } [Fact] public void StartFlowInput_Dto_HasNoClientSettableTenantField() { var dto = File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "Dto", "FlowEngineDtos.cs")); var startInputBlock = dto[dto.IndexOf("public class StartFlowInput")..]; startInputBlock = startInputBlock[..startInputBlock.IndexOf("\n}\n")]; Assert.DoesNotContain("TenantId", startInputBlock); } // ── Case 2:受信任重载存在,供后台/系统调用方使用 ── [Fact] public void TrustedStartFlowOverload_Exists() { Assert.Contains( "public Task StartFlow(StartFlowInput input, long trustedTenantId) => StartFlowCore(input, trustedTenantId);", FlowEngine()); } // ── Case 3:租户隔离——Flow Definition 选择必须硬过滤,不能只在 ORDER BY 里降权 ── [Fact] public void FlowSelection_HardFiltersTenant_NotJustOrdersByIt() { var src = FlowEngine(); Assert.Contains("u.TenantId == effectiveTenantId || u.TenantId == null", src); // 回归防护:旧的「只在 ORDER BY 里区分租户、WHERE 不过滤」写法不得再出现。 Assert.DoesNotContain("u.TenantId == tenantId ? 1 : 0", src); } // ── Case 4:Global Fallback——WHERE 允许 TenantId IS NULL,UAT 没有专属流程时能落到全局流程 ── [Fact] public void FlowSelection_AllowsGlobalFlowAsFallback() { Assert.Contains("u.TenantId == effectiveTenantId || u.TenantId == null", FlowEngine()); } // ── Case 5:ResolveApprovers 的 Role Code / RoleId 两条分支都必须用 effectiveTenantId,禁止再读 _userManager.TenantId ── [Fact] public void ResolveApprovers_Signature_TakesExplicitEffectiveTenantId() { Assert.Contains( "private async Task> ResolveApprovers(FlowProperties? props, long initiatorId, long effectiveTenantId)", FlowEngine()); } [Fact] public void ResolveApprovers_Body_DoesNotReadUserManagerTenantId() { var src = FlowEngine(); var start = src.IndexOf("private async Task> ResolveApprovers("); Assert.True(start >= 0, "ResolveApprovers method not found"); // 方法体到下一个同级 private 方法(EvaluateGateway)之前 var end = src.IndexOf("private string EvaluateGateway(", start); Assert.True(end > start, "Could not bound ResolveApprovers method body"); var body = src[start..end]; Assert.DoesNotContain("_userManager.TenantId", body); Assert.Contains("r.TenantId == effectiveTenantId", body); Assert.Contains("u.TenantId == effectiveTenantId", body); } // ── Case 6:角色缺失必须显式失败,不得回落默认租户角色(既有 throw 逻辑未被改动) ── [Fact] public void CreateTasksForNode_StillThrowsOnZeroApprovers_NoSilentFallback() { Assert.Contains( "throw Oops.Oh($\"节点 [{node.Properties?.NodeName ?? nodeId}] 未配置审批人或审批人列表为空\");", FlowEngine()); } // ── 既有推进路径(Approve/超时自动通过/手动升级)不得被本次改动波及行为 ── [Fact] public void ManualEscalate_StillPassesUserManagerTenantId_UnchangedBehavior() { var src = FlowEngine(); var idx = src.IndexOf("public async Task Escalate(long taskId, string? comment)"); Assert.True(idx >= 0); var nextMethod = src.IndexOf("public async Task Urge(long instanceId)", idx); var body = src[idx..nextMethod]; Assert.Contains("_userManager.TenantId", body); } [Fact] public void AutoEscalateTask_StillPassesUserManagerTenantId_PreExistingGapUnchangedNotFixed() { var src = FlowEngine(); var idx = src.IndexOf("private async Task AutoEscalateTask("); Assert.True(idx >= 0); var body = src[idx..(idx + 1500)]; Assert.Contains("_userManager.TenantId", body); } // ── §13 调用点迁移:S8 三处后台/受信任路径必须改走受信任重载,不得依赖 _userManager.TenantId ── [Fact] public void S8ManualReportService_TryStartIntakeFlowAsync_UsesTrustedOverloadWithEntityTenantId() { var src = S8ManualReportService(); var idx = src.IndexOf("private async Task TryStartIntakeFlowAsync(AdoS8Exception entity)"); Assert.True(idx >= 0); var body = src[idx..(idx + 1200)]; Assert.Contains("}, entity.TenantId);", body); } [Fact] public void S8TaskFlowService_UpgradeAsync_UsesTrustedOverloadWithExplicitTenantId() { var src = S8TaskFlowService(); var idx = src.IndexOf("public async Task UpgradeAsync(long id, long tenantId, long factoryId, string? remark)"); Assert.True(idx >= 0); var body = src[idx..(idx + 1200)]; Assert.Contains("}, tenantId);", body); } // ── §13 HTTP 分类:其余 6 个调用点保持不变,未被误改为受信任重载 ── [Theory] [InlineData("FinishedWarehouse", "FqcTaskEntryService.cs")] [InlineData("MaterialWarehouse", "IqcInspBillFlowService.cs")] [InlineData("Manufacturing", "IpqcInspectionFlowService.cs")] [InlineData("Manufacturing", "S6ProcessInspectionReviewService.cs")] [InlineData("FinishedWarehouse", "FqcInspBillFlowService.cs")] public void HttpTriggeredCallSites_StillUsePublicOverload_NoRegression(string subDir, string file) { var src = File.ReadAllText(FindFile( "server", "Plugins", "Admin.NET.Plugin.AiDOP", subDir, file)); Assert.Contains("_flowEngine.StartFlow(new StartFlowInput", src); // 不应出现受信任重载的两参数调用形态在这些 HTTP 触发的文件里 Assert.DoesNotContain("}, tenantId);", src); } }