| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- 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;
- /// <summary>
- /// S8-CFG-NOTIFY-NONDEFAULT-ROLE-OPTIONS-FIX-1 的回归锁。
- ///
- /// <para>缺陷本体:<c>GetRoleOptionsAsync</c> 曾按**默认租户的物理 SysRole.Id** 段筛选,
- /// 于是任何非默认租户的角色候选恒为空。修复后必须按 **canonical Role Code** 在可信租户
- /// 作用域内筛选。本文件把「Code 是逻辑身份、Id 是租户本地物理身份」这条契约钉死。</para>
- /// </summary>
- public class S8CanonicalRoleOptionsTests
- {
- // 真实数据形态:同一 Code 在每个租户各有一行,Id 互不相同。
- private const long DefaultTenantOrderPlannerId = 1329908000101L; // 默认租户
- private const long UatTenantOrderPlannerId = 838257188253765L; // UAT 租户
- private const long ThirdTenantOrderPlannerId = 797403760988229L; // 第三租户
- // ── canonical 定义本身 ────────────────────────────────────────────────
- /// <summary>canonical 集合必须正好是这 7 个业务角色码,顺序即产品展示序。</summary>
- [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);
- }
- /// <summary>
- /// <c>sys_admin</c> 是平台系统角色,不在 S8 业务角色集内——
- /// 它不被任何一条生效通知层引用,不得混进租户的角色候选。
- /// </summary>
- [Fact]
- public void Canonical_DoesNotContainPlatformSysAdmin()
- {
- Assert.DoesNotContain("sys_admin", S8CanonicalRole.BusinessRoleCodes);
- }
- /// <summary>canonical 集合只能承载**逻辑身份**:任何 Code 都不得长得像物理 Id。</summary>
- [Fact]
- public void Canonical_CarriesNoTenantSpecificIdentity()
- {
- foreach (var code in S8CanonicalRole.BusinessRoleCodes)
- Assert.False(long.TryParse(code, out _), $"canonical code '{code}' 不得是数字物理 Id");
- }
- /// <summary>两个既有定义必须与 canonical 同源,否则又会人工分叉。</summary>
- [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);
- }
- // ── 核心回归:非默认租户 ──────────────────────────────────────────────
- /// <summary>
- /// **本缺陷的核心回归锁**:同一个 Code 在默认租户是 Id=A、在非默认租户是 Id=B。
- /// 可信作用域过滤后只剩 B,整形必须原样保留 B——绝不能出现 A。
- /// </summary>
- [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);
- }
- /// <summary>
- /// 跨租户负测:即便三个租户各有同 Code 的行,可信作用域只会交出当前租户那一行;
- /// 整形不得把其它租户的 Id 混进来。
- /// </summary>
- [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);
- }
- /// <summary>
- /// 筛选依据必须是 Code,不是物理 Id:候选行的 Id 与默认租户 Id 段毫无关系时,
- /// 依然必须被完整选出(旧实现在这里会返回空)。
- /// </summary>
- [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"));
- }
- // ── 排序 / 缺失 / 重复 ────────────────────────────────────────────────
- /// <summary>排序由 canonical OrderNo 决定,不由物理 Id 决定——即便 Id 顺序完全颠倒。</summary>
- [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);
- }
- /// <summary>
- /// 缺失行为:canonical 白名单里某个 Code 在当前租户不存在时,
- /// 只是**不出现这一项**——不回落默认租户、不自动建角色、不抛异常。
- /// </summary>
- [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");
- }
- /// <summary>空作用域(该租户一个 canonical 角色都没有)返回空列表,而不是异常。</summary>
- [Fact]
- public void EmptyScope_ReturnsEmpty_NotThrow()
- {
- Assert.Empty(S8NotificationLayerService.ShapeRoleOptions(
- Array.Empty<(long, string, string)>()));
- }
- /// <summary>
- /// 重复 Code(DB 无唯一索引,属已登记技术债)必须**确定性**收敛为一项,
- /// 且取 Id 最小者——保证同一份数据每次给出同一个候选。
- /// </summary>
- [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);
- }
- /// <summary>整形对同一输入必须幂等且顺序稳定(跑两次结果逐项相同)。</summary>
- [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));
- }
- }
|