| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Service.S8;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S8;
- /// <summary>
- /// S8-STEP6C-CFG-NOTIFY-FIX-AND-CERT-1 · N-1 回归。
- ///
- /// 覆盖 <see cref="S8NotificationLayerMerge"/> 的逐层覆盖语义。这是纯函数测试,不触 DB、不触 pusher。
- /// runtime resolver 与配置页 ListAsync 都调用同一个 Effective(),因此本文件同时守住两处。
- ///
- /// 被守住的缺陷(修正前真实存在):工厂只为某一层建行,平台默认在该 scene+severity 下的
- /// **其余层级会整体消失** —— 工厂本意「补一层」,实际「换掉整组」,且页面上不可见。
- /// </summary>
- public class S8NotificationLayerMergeTests
- {
- private const long GlobalTenant = 0L;
- private const long GlobalFactory = 0L;
- private const long FactoryTenant = 838257186181189L;
- private const long FactoryFactory = 838257186320453L;
- private static AdoS8NotificationLayer Row(
- long id, long tenantId, long factoryId, string scene, string severity, string level) =>
- new()
- {
- Id = id,
- TenantId = tenantId,
- FactoryId = factoryId,
- SceneCode = scene,
- Severity = severity,
- LevelCode = level,
- TargetRoleIds = "ROLE_X",
- NotifyChannel = "log,SignalR"
- };
- private static AdoS8NotificationLayer Global(long id, string scene, string sev, string level) =>
- Row(id, GlobalTenant, GlobalFactory, scene, sev, level);
- private static AdoS8NotificationLayer Factory(long id, string scene, string sev, string level) =>
- Row(id, FactoryTenant, FactoryFactory, scene, sev, level);
- /// <summary>
- /// N-1 的核心回归:工厂只覆盖 L2,平台默认的 L1 / L3 必须**继续生效**。
- /// 修正前此处会只剩 1 行(工厂的 L2),L1/L3 静默消失。
- /// </summary>
- [Fact]
- public void FactoryOverrideOnOneLevel_KeepsPlatformDefaultsOnOtherLevels()
- {
- var candidates = new[]
- {
- Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
- Global(2, "S1", "FOLLOW", "L2_MANAGER"),
- Global(3, "S1", "FOLLOW", "L3_DIRECTOR"),
- Factory(100, "S1", "FOLLOW", "L2_MANAGER"),
- };
- var effective = S8NotificationLayerMerge.Effective(candidates);
- Assert.Equal(3, effective.Count);
- Assert.Equal(new[] { "L1_OPERATOR", "L2_MANAGER", "L3_DIRECTOR" }, effective.Select(x => x.LevelCode));
- // L1 / L3 仍来自平台默认,L2 被工厂接管
- Assert.Equal(GlobalTenant, effective[0].TenantId);
- Assert.Equal(FactoryTenant, effective[1].TenantId);
- Assert.Equal(100, effective[1].Id);
- Assert.Equal(GlobalTenant, effective[2].TenantId);
- }
- /// <summary>同层级有工厂行时,该层级的平台默认必须被替换掉(不能两行都发)。</summary>
- [Fact]
- public void FactoryRow_ReplacesPlatformDefault_OnSameLevel()
- {
- var candidates = new[]
- {
- Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
- Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
- };
- var effective = S8NotificationLayerMerge.Effective(candidates);
- Assert.Single(effective);
- Assert.Equal(100, effective[0].Id);
- }
- /// <summary>覆盖不跨 scene / severity:S1 的工厂行不得影响 S2,FOLLOW 的不得影响 SERIOUS。</summary>
- [Fact]
- public void Override_DoesNotLeakAcrossSceneOrSeverity()
- {
- var candidates = new[]
- {
- Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
- Global(2, "S1", "SERIOUS", "L1_OPERATOR"),
- Global(3, "S2", "FOLLOW", "L1_OPERATOR"),
- Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
- };
- var effective = S8NotificationLayerMerge.Effective(candidates);
- Assert.Equal(3, effective.Count);
- Assert.Equal(100, effective.Single(x => x.SceneCode == "S1" && x.Severity == "FOLLOW").Id);
- Assert.Equal(2, effective.Single(x => x.SceneCode == "S1" && x.Severity == "SERIOUS").Id);
- Assert.Equal(3, effective.Single(x => x.SceneCode == "S2").Id);
- }
- /// <summary>
- /// severity 参与分组前必须归一:平台默认写 FOLLOW、工厂行写 legacy 的 LOW,
- /// 二者是同一档,必须归到同一组由工厂接管,而不是各发一条。
- /// </summary>
- [Fact]
- public void SeverityIsNormalizedBeforeGrouping()
- {
- var candidates = new[]
- {
- Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
- Factory(100, "S1", "LOW", "L1_OPERATOR"),
- };
- var effective = S8NotificationLayerMerge.Effective(candidates);
- Assert.Single(effective);
- Assert.Equal(100, effective[0].Id);
- }
- /// <summary>level_code 归一:大小写 / 空白差异不得让工厂行与平台默认分到两组。</summary>
- [Fact]
- public void LevelIsNormalizedBeforeGrouping()
- {
- var candidates = new[]
- {
- Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
- Factory(100, "S1", "FOLLOW", " l1_operator "),
- };
- var effective = S8NotificationLayerMerge.Effective(candidates);
- Assert.Single(effective);
- Assert.Equal(100, effective[0].Id);
- }
- /// <summary>
- /// 历史脏数据:level_code = '' 的行自成一组,既不吞掉合法层级、也不被合法层级吞掉。
- /// (库中实测存在这样的行。)
- /// </summary>
- [Fact]
- public void EmptyLevelCode_FormsItsOwnGroup()
- {
- var candidates = new[]
- {
- Global(1, "S1", "SERIOUS", "L1_OPERATOR"),
- Factory(100, "S1", "SERIOUS", ""),
- };
- var effective = S8NotificationLayerMerge.Effective(candidates);
- Assert.Equal(2, effective.Count);
- Assert.Contains(effective, x => x.Id == 1);
- Assert.Contains(effective, x => x.Id == 100);
- }
- /// <summary>
- /// 无唯一索引,同键可能重复行:必须全部保留(丢弃会让 runtime 与页面不一致),
- /// 且顺序按 Id 稳定,不依赖 DB 返回序。
- /// </summary>
- [Fact]
- public void DuplicateRowsOnSameKey_AreAllKept_InStableIdOrder()
- {
- var candidates = new[]
- {
- Factory(300, "S1", "FOLLOW", "L1_OPERATOR"),
- Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
- Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
- };
- var effective = S8NotificationLayerMerge.Effective(candidates);
- Assert.Equal(new[] { 100L, 300L }, effective.Select(x => x.Id));
- }
- /// <summary>输入顺序不影响输出:稳定确定序是 scene → severity → level → id。</summary>
- [Fact]
- public void OrderingIsDeterministic_RegardlessOfInputOrder()
- {
- var rows = new[]
- {
- Global(3, "S2", "FOLLOW", "L1_OPERATOR"),
- Global(2, "S1", "SERIOUS", "L1_OPERATOR"),
- Global(1, "S1", "FOLLOW", "L2_MANAGER"),
- Global(4, "S1", "FOLLOW", "L1_OPERATOR"),
- };
- var forward = S8NotificationLayerMerge.Effective(rows).Select(x => x.Id).ToList();
- var reversed = S8NotificationLayerMerge.Effective(rows.Reverse().ToList()).Select(x => x.Id).ToList();
- Assert.Equal(new[] { 4L, 1L, 2L, 3L }, forward);
- Assert.Equal(forward, reversed);
- }
- /// <summary>只有平台默认时全部生效;只有工厂行时同理。</summary>
- [Fact]
- public void SingleSidedCandidates_PassThrough()
- {
- var globalsOnly = S8NotificationLayerMerge.Effective(new[]
- {
- Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
- Global(2, "S1", "FOLLOW", "L2_MANAGER"),
- });
- Assert.Equal(2, globalsOnly.Count);
- var factoryOnly = S8NotificationLayerMerge.Effective(new[]
- {
- Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
- });
- Assert.Single(factoryOnly);
- Assert.Empty(S8NotificationLayerMerge.Effective(Array.Empty<AdoS8NotificationLayer>()));
- }
- }
|