using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.S7;
///
/// S7-FQC-INITIATOR-TENANT-GUARD-1 契约测试:阻止 S7 FQC 再产生跨租户 jyfzr / Initiator。
///
/// 要守住的不变量:
/// BusinessTenant == jyfzr 用户.TenantId == Instance.Initiator 用户.TenantId。
/// S7 契约是三者恒等(SubmitResult 的负责人门 + GenerateInspBill 的认领人门保证
/// jyfzr == 当前用户 == N1 Assignee),故只要 jyfzr 恒同租户,Initiator 就恒同租户。
///
/// 三道防线:Assign → Claim → 起流边界(GenerateInspBill / SubmitResult)。
///
/// 沿用本仓既有做法——源码契约断言而非行为级集成测试:这些服务依赖
/// ISqlSugarClient / UserManager / FlowEngineService,裸 xUnit 进程无法实例化。
/// 真实 ALLOW/DENY 由本批 Runtime R1–R7 覆盖。
///
public class S7FqcInitiatorTenantGuardTests
{
private static string Guard() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.AiDOP", "FinishedWarehouse", "S7FqcInitiatorTenantGuard.cs"));
private static string TaskEntry() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.AiDOP", "FinishedWarehouse", "FqcTaskEntryService.cs"));
private static string FlowService() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.AiDOP", "FinishedWarehouse", "FqcInspBillFlowService.cs"));
private static string Engine() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "FlowEngineService.cs"));
private static string FlowTaskService() => File.ReadAllText(FindFile(
"server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowTask", "FlowTaskService.cs"));
// ───────────── 守卫本体 ─────────────
/// §十七:不得只判「用户存在」,必须显式比较 SysUser.TenantId 与单据租户。
[Fact]
public void Guard_ComparesUserTenantAgainstBusinessTenant()
{
var g = Guard();
Assert.Contains("if (user.TenantId != businessTenantId)", g);
Assert.Contains("不属于当前单据租户", g);
}
/// 存在性与启用状态也要判 —— 三者缺一都不能当负责人。
[Theory]
[InlineData("if (user == null)")]
[InlineData("if (user.Status != StatusEnum.Enable)")]
[InlineData("if (userId <= 0)")]
public void Guard_ChecksExistenceAndEnabled(string expected)
{
Assert.Contains(expected, Guard());
}
///
/// 必须跨租户查用户(ClearFilter<ITenantIdFilter>)—— 否则跨租户用户表现为「查不到」,
/// 无法把「不存在」与「存在但不属于本租户」区分开,只能给出误导性文案。
///
[Fact]
public void Guard_LooksUpUserAcrossTenantsOnPurpose()
{
var g = Guard();
Assert.Contains("ClearFilter()", g);
Assert.Contains("db.Queryable()", g);
}
/// §二十七:文案不得泄漏 TenantId / RoleId / 内部过滤器。
[Theory]
[InlineData("TenantId=")]
[InlineData("RoleId")]
[InlineData("ITenantIdFilter}")]
public void Guard_MessageDoesNotLeakInternals(string leak)
{
var messages = string.Join("\n", Guard().Split('\n').Where(l => l.Contains("Oops.Oh(", StringComparison.Ordinal)));
Assert.DoesNotContain(leak, messages);
}
/// 存量脏数据:jyfzr 可能根本不是用户 Id,必须显式拒绝而不是当 0 处理。
[Fact]
public void Guard_RejectsNonNumericLegacyOwnerValue()
{
var body = Slice(Guard(), "public static async Task EnsureBillOwnerTenantSafeAsync", "private sealed class GuardUser");
Assert.Contains("!long.TryParse(raw, out var ownerId) || ownerId <= 0", body);
Assert.Contains("检验负责人记录无效", body);
Assert.Contains("尚未指定检验负责人", body);
}
/// 守卫只读不写 —— 任何 blocked 路径都不能产生副作用。
[Theory]
[InlineData("AsInsertable")]
[InlineData("AsUpdateable")]
[InlineData("ExecuteCommandAsync")]
[InlineData("Deleteable")]
public void Guard_PerformsNoMutation(string write)
{
Assert.DoesNotContain(write, CsCode(Guard()));
}
// ───────────── §二十九 Assign ─────────────
/// Assign 必须在写 jyfzr **之前**校验目标用户。
[Fact]
public void Assign_GuardsBeforeWritingOwner()
{
var body = Slice(TaskEntry(), "public async Task