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);
}
}