using System.Reflection; using Admin.NET.Plugin.AiDOP.Infrastructure.S8; using Admin.NET.Plugin.AiDOP.Service.S8; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S8; /// /// S8-CFG-NOTIFY-NONDEFAULT-ROLE-OPTIONS-FIX-1 的回归锁。 /// /// 缺陷本体:GetRoleOptionsAsync 曾按**默认租户的物理 SysRole.Id** 段筛选, /// 于是任何非默认租户的角色候选恒为空。修复后必须按 **canonical Role Code** 在可信租户 /// 作用域内筛选。本文件把「Code 是逻辑身份、Id 是租户本地物理身份」这条契约钉死。 /// public class S8CanonicalRoleOptionsTests { // 真实数据形态:同一 Code 在每个租户各有一行,Id 互不相同。 private const long DefaultTenantOrderPlannerId = 1329908000101L; // 默认租户 private const long UatTenantOrderPlannerId = 838257188253765L; // UAT 租户 private const long ThirdTenantOrderPlannerId = 797403760988229L; // 第三租户 // ── canonical 定义本身 ──────────────────────────────────────────────── /// canonical 集合必须正好是这 7 个业务角色码,顺序即产品展示序。 [Fact] public void Canonical_IsExactlyTheSevenBusinessRoles() { Assert.Equal( new[] { "ROLE_ORDER_PLANNER", "ROLE_PRODUCTION_PLANNER", "ROLE_PURCHASER", "ROLE_WH_INBOUND", "ROLE_WH_OUTBOUND", "ROLE_QC", "ROLE_EQUIP_MAINT", }, S8CanonicalRole.BusinessRoleCodes); } /// /// sys_admin 是平台系统角色,不在 S8 业务角色集内—— /// 它不被任何一条生效通知层引用,不得混进租户的角色候选。 /// [Fact] public void Canonical_DoesNotContainPlatformSysAdmin() { Assert.DoesNotContain("sys_admin", S8CanonicalRole.BusinessRoleCodes); } /// canonical 集合只能承载**逻辑身份**:任何 Code 都不得长得像物理 Id。 [Fact] public void Canonical_CarriesNoTenantSpecificIdentity() { foreach (var code in S8CanonicalRole.BusinessRoleCodes) Assert.False(long.TryParse(code, out _), $"canonical code '{code}' 不得是数字物理 Id"); } /// 两个既有定义必须与 canonical 同源,否则又会人工分叉。 [Fact] public void Canonical_IsTheSingleSourceForTenantProvisioning() { var field = typeof(Admin.NET.Plugin.AiDOP.Infrastructure.AidopTenantInitService) .GetProperty("S8BusinessRoles", BindingFlags.NonPublic | BindingFlags.Static)!; var roles = ((string Code, string Name, int OrderNo)[])field.GetValue(null)!; Assert.Same(S8CanonicalRole.BusinessRoles, roles); } // ── 核心回归:非默认租户 ────────────────────────────────────────────── /// /// **本缺陷的核心回归锁**:同一个 Code 在默认租户是 Id=A、在非默认租户是 Id=B。 /// 可信作用域过滤后只剩 B,整形必须原样保留 B——绝不能出现 A。 /// [Fact] public void NonDefaultTenant_YieldsTenantLocalRow_NotDefaultTenantRow() { // 可信租户过滤后,仓储只会返回本租户那一行。 var scoped = new[] { (UatTenantOrderPlannerId, "ROLE_ORDER_PLANNER", "订单计划员") }; var result = S8NotificationLayerService.ShapeRoleOptions(scoped); var only = Assert.Single(result); Assert.Equal(UatTenantOrderPlannerId, only.Id); Assert.NotEqual(DefaultTenantOrderPlannerId, only.Id); Assert.Equal("ROLE_ORDER_PLANNER", only.Code); } /// /// 跨租户负测:即便三个租户各有同 Code 的行,可信作用域只会交出当前租户那一行; /// 整形不得把其它租户的 Id 混进来。 /// [Fact] public void CrossTenant_NoLeakage_OnlyCurrentTenantRowSurvives() { var scoped = new[] { (UatTenantOrderPlannerId, "ROLE_ORDER_PLANNER", "订单计划员") }; var ids = S8NotificationLayerService.ShapeRoleOptions(scoped).Select(x => x.Id).ToList(); Assert.DoesNotContain(DefaultTenantOrderPlannerId, ids); Assert.DoesNotContain(ThirdTenantOrderPlannerId, ids); Assert.Equal(new[] { UatTenantOrderPlannerId }, ids); } /// /// 筛选依据必须是 Code,不是物理 Id:候选行的 Id 与默认租户 Id 段毫无关系时, /// 依然必须被完整选出(旧实现在这里会返回空)。 /// [Fact] public void Selection_IsByCode_NotByPhysicalIdRange() { var scoped = S8CanonicalRole.BusinessRoles .Select((r, i) => (Id: 900_000_000_000L + i, r.Code, r.Name)) .ToArray(); var result = S8NotificationLayerService.ShapeRoleOptions(scoped); Assert.Equal(7, result.Count); Assert.All(result, r => Assert.True(r.Id >= 900_000_000_000L)); Assert.All(result, r => Assert.False( r.Id >= 1329908000101L && r.Id <= 1329908000107L, "不得出现默认租户物理 Id")); } // ── 排序 / 缺失 / 重复 ──────────────────────────────────────────────── /// 排序由 canonical OrderNo 决定,不由物理 Id 决定——即便 Id 顺序完全颠倒。 [Fact] public void Ordering_FollowsCanonicalOrder_NotPhysicalId() { var reversed = S8CanonicalRole.BusinessRoles .Select((r, i) => (Id: 999L - i, r.Code, r.Name)) // Id 与 canonical 序相反 .ToArray(); var codes = S8NotificationLayerService.ShapeRoleOptions(reversed).Select(x => x.Code); Assert.Equal(S8CanonicalRole.BusinessRoleCodes, codes); } /// /// 缺失行为:canonical 白名单里某个 Code 在当前租户不存在时, /// 只是**不出现这一项**——不回落默认租户、不自动建角色、不抛异常。 /// [Fact] public void MissingCanonicalRole_IsSimplyOmitted_NoFallbackNoThrow() { var partial = new[] { (10L, "ROLE_QC", "质检员"), (11L, "ROLE_PURCHASER", "采购员"), }; var result = S8NotificationLayerService.ShapeRoleOptions(partial); Assert.Equal(new[] { "ROLE_PURCHASER", "ROLE_QC" }, result.Select(x => x.Code)); // 802 < 805 Assert.DoesNotContain(result, x => x.Code == "ROLE_ORDER_PLANNER"); } /// 空作用域(该租户一个 canonical 角色都没有)返回空列表,而不是异常。 [Fact] public void EmptyScope_ReturnsEmpty_NotThrow() { Assert.Empty(S8NotificationLayerService.ShapeRoleOptions( Array.Empty<(long, string, string)>())); } /// /// 重复 Code(DB 无唯一索引,属已登记技术债)必须**确定性**收敛为一项, /// 且取 Id 最小者——保证同一份数据每次给出同一个候选。 /// [Fact] public void DuplicateCode_CollapsesDeterministically_ToLowestId() { var dup = new[] { (77L, "ROLE_QC", "质检员-B"), (55L, "ROLE_QC", "质检员-A"), }; var only = Assert.Single(S8NotificationLayerService.ShapeRoleOptions(dup)); Assert.Equal(55L, only.Id); Assert.Equal("质检员-A", only.Name); } /// 整形对同一输入必须幂等且顺序稳定(跑两次结果逐项相同)。 [Fact] public void Shaping_IsStableAcrossInvocations() { var rows = S8CanonicalRole.BusinessRoles .Select((r, i) => (Id: 500L + i, r.Code, r.Name)).ToArray(); Assert.Equal( S8NotificationLayerService.ShapeRoleOptions(rows), S8NotificationLayerService.ShapeRoleOptions(rows)); } }