| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.ApprovalFlow;
- /// <summary>
- /// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1 源码契约测试。
- ///
- /// <para>背景:<c>FlowEngineService</c> 全程依赖 <c>SqlSugarRepository<T></c>,
- /// 裸 xUnit 进程无法实例化(无参构造触发 Furion.App 静态构造,在无宿主进程中抛
- /// <c>TypeInitializationException</c>),因此本仓对这一层历来采用源码契约测试
- /// (grep 断言代码形状),而非行为级 DB 集成测试——本文件延续该既有惯例。</para>
- ///
- /// <para>覆盖范围:验证 §14 六个场景要求的代码形状是否落地,而非跑真实数据库。
- /// 真正的行为级验证在运行态验收(Scheduler 真跑 GWR-S6-WO-DELAY)里完成。</para>
- /// </summary>
- 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<long> 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<long> 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<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId, long effectiveTenantId)",
- FlowEngine());
- }
- [Fact]
- public void ResolveApprovers_Body_DoesNotReadUserManagerTenantId()
- {
- var src = FlowEngine();
- var start = src.IndexOf("private async Task<List<(long userId, string userName)>> 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<AdoS8Exception> 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);
- }
- }
|