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