S8ExceptionFlowTenantContextContractTests.cs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. using Xunit;
  2. namespace Admin.NET.Plugin.AiDOP.Tests.ApprovalFlow;
  3. /// <summary>
  4. /// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1 源码契约测试。
  5. ///
  6. /// <para>背景:<c>FlowEngineService</c> 全程依赖 <c>SqlSugarRepository&lt;T&gt;</c>,
  7. /// 裸 xUnit 进程无法实例化(无参构造触发 Furion.App 静态构造,在无宿主进程中抛
  8. /// <c>TypeInitializationException</c>),因此本仓对这一层历来采用源码契约测试
  9. /// (grep 断言代码形状),而非行为级 DB 集成测试——本文件延续该既有惯例。</para>
  10. ///
  11. /// <para>覆盖范围:验证 §14 六个场景要求的代码形状是否落地,而非跑真实数据库。
  12. /// 真正的行为级验证在运行态验收(Scheduler 真跑 GWR-S6-WO-DELAY)里完成。</para>
  13. /// </summary>
  14. public class S8ExceptionFlowTenantContextContractTests
  15. {
  16. private static string FlowEngine() => File.ReadAllText(FindFile(
  17. "server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "FlowEngineService.cs"));
  18. private static string S8ManualReportService() => File.ReadAllText(FindFile(
  19. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Service", "S8", "S8ManualReportService.cs"));
  20. private static string S8TaskFlowService() => File.ReadAllText(FindFile(
  21. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Service", "S8", "S8TaskFlowService.cs"));
  22. private static string FindFile(params string[] parts)
  23. {
  24. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  25. while (dir != null)
  26. {
  27. var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
  28. if (File.Exists(candidate)) return candidate;
  29. dir = dir.Parent;
  30. }
  31. throw new FileNotFoundException(string.Join("/", parts));
  32. }
  33. // ── Case 1 / Case 16:HTTP 人工路径必须原样保留,不接受客户端可提交的 TenantId ──
  34. [Fact]
  35. public void PublicStartFlow_StillUsesUserManagerTenantId_NoRegressionForHttpCallers()
  36. {
  37. Assert.Contains(
  38. "public Task<long> StartFlow(StartFlowInput input) => StartFlowCore(input, _userManager.TenantId);",
  39. FlowEngine());
  40. }
  41. [Fact]
  42. public void StartFlowInput_Dto_HasNoClientSettableTenantField()
  43. {
  44. var dto = File.ReadAllText(FindFile(
  45. "server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "Dto", "FlowEngineDtos.cs"));
  46. var startInputBlock = dto[dto.IndexOf("public class StartFlowInput")..];
  47. startInputBlock = startInputBlock[..startInputBlock.IndexOf("\n}\n")];
  48. Assert.DoesNotContain("TenantId", startInputBlock);
  49. }
  50. // ── Case 2:受信任重载存在,供后台/系统调用方使用 ──
  51. [Fact]
  52. public void TrustedStartFlowOverload_Exists()
  53. {
  54. Assert.Contains(
  55. "public Task<long> StartFlow(StartFlowInput input, long trustedTenantId) => StartFlowCore(input, trustedTenantId);",
  56. FlowEngine());
  57. }
  58. // ── Case 3:租户隔离——Flow Definition 选择必须硬过滤,不能只在 ORDER BY 里降权 ──
  59. [Fact]
  60. public void FlowSelection_HardFiltersTenant_NotJustOrdersByIt()
  61. {
  62. var src = FlowEngine();
  63. Assert.Contains("u.TenantId == effectiveTenantId || u.TenantId == null", src);
  64. // 回归防护:旧的「只在 ORDER BY 里区分租户、WHERE 不过滤」写法不得再出现。
  65. Assert.DoesNotContain("u.TenantId == tenantId ? 1 : 0", src);
  66. }
  67. // ── Case 4:Global Fallback——WHERE 允许 TenantId IS NULL,UAT 没有专属流程时能落到全局流程 ──
  68. [Fact]
  69. public void FlowSelection_AllowsGlobalFlowAsFallback()
  70. {
  71. Assert.Contains("u.TenantId == effectiveTenantId || u.TenantId == null", FlowEngine());
  72. }
  73. // ── Case 5:ResolveApprovers 的 Role Code / RoleId 两条分支都必须用 effectiveTenantId,禁止再读 _userManager.TenantId ──
  74. [Fact]
  75. public void ResolveApprovers_Signature_TakesExplicitEffectiveTenantId()
  76. {
  77. Assert.Contains(
  78. "private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId, long effectiveTenantId)",
  79. FlowEngine());
  80. }
  81. [Fact]
  82. public void ResolveApprovers_Body_DoesNotReadUserManagerTenantId()
  83. {
  84. var src = FlowEngine();
  85. var start = src.IndexOf("private async Task<List<(long userId, string userName)>> ResolveApprovers(");
  86. Assert.True(start >= 0, "ResolveApprovers method not found");
  87. // 方法体到下一个同级 private 方法(EvaluateGateway)之前
  88. var end = src.IndexOf("private string EvaluateGateway(", start);
  89. Assert.True(end > start, "Could not bound ResolveApprovers method body");
  90. var body = src[start..end];
  91. Assert.DoesNotContain("_userManager.TenantId", body);
  92. Assert.Contains("r.TenantId == effectiveTenantId", body);
  93. Assert.Contains("u.TenantId == effectiveTenantId", body);
  94. }
  95. // ── Case 6:角色缺失必须显式失败,不得回落默认租户角色(既有 throw 逻辑未被改动) ──
  96. [Fact]
  97. public void CreateTasksForNode_StillThrowsOnZeroApprovers_NoSilentFallback()
  98. {
  99. Assert.Contains(
  100. "throw Oops.Oh($\"节点 [{node.Properties?.NodeName ?? nodeId}] 未配置审批人或审批人列表为空\");",
  101. FlowEngine());
  102. }
  103. // ── 既有推进路径(Approve/超时自动通过/手动升级)不得被本次改动波及行为 ──
  104. [Fact]
  105. public void ManualEscalate_StillPassesUserManagerTenantId_UnchangedBehavior()
  106. {
  107. var src = FlowEngine();
  108. var idx = src.IndexOf("public async Task Escalate(long taskId, string? comment)");
  109. Assert.True(idx >= 0);
  110. var nextMethod = src.IndexOf("public async Task Urge(long instanceId)", idx);
  111. var body = src[idx..nextMethod];
  112. Assert.Contains("_userManager.TenantId", body);
  113. }
  114. [Fact]
  115. public void AutoEscalateTask_StillPassesUserManagerTenantId_PreExistingGapUnchangedNotFixed()
  116. {
  117. var src = FlowEngine();
  118. var idx = src.IndexOf("private async Task AutoEscalateTask(");
  119. Assert.True(idx >= 0);
  120. var body = src[idx..(idx + 1500)];
  121. Assert.Contains("_userManager.TenantId", body);
  122. }
  123. // ── §13 调用点迁移:S8 三处后台/受信任路径必须改走受信任重载,不得依赖 _userManager.TenantId ──
  124. [Fact]
  125. public void S8ManualReportService_TryStartIntakeFlowAsync_UsesTrustedOverloadWithEntityTenantId()
  126. {
  127. var src = S8ManualReportService();
  128. var idx = src.IndexOf("private async Task TryStartIntakeFlowAsync(AdoS8Exception entity)");
  129. Assert.True(idx >= 0);
  130. var body = src[idx..(idx + 1200)];
  131. Assert.Contains("}, entity.TenantId);", body);
  132. }
  133. [Fact]
  134. public void S8TaskFlowService_UpgradeAsync_UsesTrustedOverloadWithExplicitTenantId()
  135. {
  136. var src = S8TaskFlowService();
  137. var idx = src.IndexOf("public async Task<AdoS8Exception> UpgradeAsync(long id, long tenantId, long factoryId, string? remark)");
  138. Assert.True(idx >= 0);
  139. var body = src[idx..(idx + 1200)];
  140. Assert.Contains("}, tenantId);", body);
  141. }
  142. // ── §13 HTTP 分类:其余 6 个调用点保持不变,未被误改为受信任重载 ──
  143. [Theory]
  144. [InlineData("FinishedWarehouse", "FqcTaskEntryService.cs")]
  145. [InlineData("MaterialWarehouse", "IqcInspBillFlowService.cs")]
  146. [InlineData("Manufacturing", "IpqcInspectionFlowService.cs")]
  147. [InlineData("Manufacturing", "S6ProcessInspectionReviewService.cs")]
  148. [InlineData("FinishedWarehouse", "FqcInspBillFlowService.cs")]
  149. public void HttpTriggeredCallSites_StillUsePublicOverload_NoRegression(string subDir, string file)
  150. {
  151. var src = File.ReadAllText(FindFile(
  152. "server", "Plugins", "Admin.NET.Plugin.AiDOP", subDir, file));
  153. Assert.Contains("_flowEngine.StartFlow(new StartFlowInput", src);
  154. // 不应出现受信任重载的两参数调用形态在这些 HTTP 触发的文件里
  155. Assert.DoesNotContain("}, tenantId);", src);
  156. }
  157. }