| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.ApprovalFlow;
- /// <summary>
- /// S6-TENANT-AUTHORITY-1 源码契约测试(Phase E · Resolver 守卫)。
- ///
- /// 与 <see cref="S6TenantAuthorityMigrationContractTests"/> 分文件:迁移(角色 / 成员 / 定义)
- /// 与守卫是**两个可独立发布的阶段** —— 守卫必须在迁移完成并通过 Runtime 验证之后才允许启用,
- /// 否则会把仍靠 legacy 跨租户 RoleId 工作的租户当场打成 0 审批人。拆开后两个提交各自自洽。
- ///
- /// 守卫的范围铁律:只覆盖 IPQC_INSPECTION / S6_PROCESS_INSPECTION。
- /// S5_IQC_INSPBILL / S7_FQC_INSPBILL 等同样写死了默认租户物理 RoleId,但它们当前正靠跨租户
- /// SysUserRole 绑定工作(实测 28 个节点组合),本批不在迁移范围,无条件启用会直接打断它们。
- ///
- /// 沿用本仓既有做法——源码契约断言而非行为级集成测试:FlowEngineService 依赖
- /// SqlSugarRepository<T>,裸 xUnit 进程无法实例化(见 S8ExceptionFlowTenantContextContractTests)。
- /// </summary>
- public class S6TenantAuthorityGuardContractTests
- {
- private static string Engine() => File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "FlowEngineService.cs"));
- // ───────────── Resolver 守卫:S6 专属 fail-closed ─────────────
- /// <summary>守卫必须只覆盖两条 S6 链。</summary>
- [Fact]
- public void Guard_ScopedToTheTwoS6BizTypes()
- {
- var set = Slice(Engine(), "TenantStrictRoleBizTypes = new(StringComparer.Ordinal)", "};");
- Assert.Contains("\"IPQC_INSPECTION\"", set);
- Assert.Contains("\"S6_PROCESS_INSPECTION\"", set);
- }
- /// <summary>S5 / S7 不得被本批守卫波及——它们仍持默认租户物理 RoleId 且当前正靠它工作。</summary>
- [Theory]
- [InlineData("S5_IQC_INSPBILL")]
- [InlineData("S7_FQC_INSPBILL")]
- [InlineData("SMART_OPS_IMPROVEMENT")]
- [InlineData("EXCEPTION_REPORT")]
- public void Guard_DoesNotAffectOtherBizTypes(string bizType)
- {
- var set = Slice(Engine(), "TenantStrictRoleBizTypes = new(StringComparer.Ordinal)", "};");
- Assert.DoesNotContain(bizType, set);
- }
- /// <summary>守卫必须是精确白名单查表,不得退化成前缀/模糊匹配把无关业务一起挡掉。</summary>
- [Fact]
- public void Guard_UsesExactAllowList_NotFuzzyMatch()
- {
- var s = Engine();
- Assert.Contains("TenantStrictRoleBizTypes.Contains(bizType)", s);
- Assert.DoesNotContain("BizType.StartsWith(\"S6", s);
- Assert.DoesNotContain("BizType.Contains(\"IPQC", s);
- }
- /// <summary>守卫必须在 Code 解析之前跑,针对的是 FlowJson 里写死的数字 RoleId。</summary>
- [Fact]
- public void Guard_RunsBeforeRoleCodeResolution()
- {
- var s = Engine();
- var guardAt = s.IndexOf("await EnsureRoleAuthorityTenantScopedAsync(ids, effectiveTenantId, authorityContext);", StringComparison.Ordinal);
- var codeAt = s.IndexOf("codeRoleIds", StringComparison.Ordinal);
- Assert.True(guardAt > 0, "未找到守卫调用点");
- Assert.True(codeAt > guardAt, "守卫必须先于 RoleCode 解析执行");
- }
- /// <summary>跨租户 RoleId 必须 fail-closed;且刻意不做 auto-heal(不按 Code 隐式替换)。</summary>
- [Fact]
- public void Guard_FailsClosedOnCrossTenantRole_AndDoesNotAutoHeal()
- {
- var body = Slice(Engine(),
- "private async Task EnsureRoleAuthorityTenantScopedAsync",
- "/// <summary>\n /// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1");
- Assert.Contains("r.TenantId != effectiveTenantId", body);
- Assert.Contains("throw Oops.Oh(", body);
- // auto-heal 会让坏定义继续成功运行、永远没人去修
- Assert.DoesNotContain("codes.Contains(r.Code)", body);
- Assert.DoesNotContain("roleIds.Add(", body);
- }
- /// <summary>三类失败必须可区分,且日志带全部定位字段。</summary>
- [Theory]
- [InlineData("CROSS_TENANT_ROLE_REFERENCE")]
- [InlineData("INVALID_ROLE")]
- [InlineData("LOCAL_ROLE_HAS_NO_MEMBER")]
- public void Guard_ClassifiesFailures(string classification)
- {
- Assert.Contains(classification, Engine());
- }
- /// <summary>日志必须能定位到具体定义与节点。</summary>
- [Fact]
- public void Guard_LogsDiagnosticFields()
- {
- var s = Engine();
- foreach (var field in new[] { "BizType={BizType}", "FlowId={FlowId}", "NodeId={NodeId}", "RoleRef={RoleRef}", "EffectiveTenantId={TenantId}" })
- Assert.Contains(field, s);
- }
- /// <summary>Branch B 必须保留——历史定义仍可能存 RoleId,本批不是删除 RoleId 支持。</summary>
- [Fact]
- public void Guard_KeepsRoleIdBranch()
- {
- var s = Engine();
- Assert.Contains("var userIds = await _userRoleRep.AsQueryable()", s);
- Assert.Contains("ids.Contains(ur.RoleId)", s);
- }
- /// <summary>RoleCode 分支(目标路径)必须始终按当前租户 + 启用状态解析,本批不得改动它。</summary>
- [Fact]
- public void RoleCodeBranch_StaysTenantScoped()
- {
- var s = Engine();
- Assert.Contains("r.TenantId == effectiveTenantId", s);
- Assert.Contains("r.Status == StatusEnum.Enable", s);
- Assert.Contains("codes.Contains(r.Code)", s);
- }
- /// <summary>成员过滤这道租户收口不得被移除——它是跨租户泄漏的最后一道闸。</summary>
- [Fact]
- public void MemberResolution_KeepsTenantScope()
- {
- Assert.Contains("u.TenantId == effectiveTenantId", Engine());
- }
- private static string Slice(string src, string from, string to)
- {
- var a = src.IndexOf(from, StringComparison.Ordinal);
- Assert.True(a >= 0, $"未找到起点:{from}");
- var b = src.IndexOf(to, a + from.Length, StringComparison.Ordinal);
- return b > a ? src[a..b] : src[a..];
- }
- 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));
- }
- }
|