S8WatchRuleWriteGuardTests.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. using System.Reflection;
  2. using System.Runtime.CompilerServices;
  3. using Admin.NET.Plugin.AiDOP.Entity.S8;
  4. using Admin.NET.Plugin.AiDOP.Infrastructure;
  5. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  6. using Admin.NET.Plugin.AiDOP.Service.S8;
  7. using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  8. using Xunit;
  9. namespace Admin.NET.Plugin.AiDOP.Tests.S8;
  10. /// <summary>
  11. /// S8-STEP6E-CFG-WATCH-FIX-AND-SAFE-CERT-1:钉住 CFG_WATCH 的
  12. /// 「整实体 PUT 退役 + 新建 canonical 词表」契约。
  13. ///
  14. /// <para><b>不触任何数据库。</b> 退役守卫在**任何 DB 访问之前**抛出,词表校验是纯静态方法,
  15. /// 所以都不需要可用的仓储。用 <see cref="RuntimeHelpers.GetUninitializedObject"/> 绕开构造函数
  16. /// (其 <c>SqlSugarRepository&lt;T&gt;</c> 参数的无参构造会触发 Furion.App 静态初始化,
  17. /// 在无宿主的 xunit 进程内必抛 TypeInitializationException)。
  18. /// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 失败——
  19. /// 这正是我们要的信号:<b>守卫必须前置</b>。</para>
  20. /// </summary>
  21. public class S8WatchRuleWriteGuardTests
  22. {
  23. private static S8WatchRuleService Svc() =>
  24. (S8WatchRuleService)RuntimeHelpers.GetUninitializedObject(typeof(S8WatchRuleService));
  25. private static readonly S8TrustedScope Scope = new(838257186181189L);
  26. private static AdoS8WatchRule Body() => new()
  27. {
  28. RuleCode = "TEST-WATCH-X",
  29. SceneCode = "S1",
  30. RuleType = S8TimeoutRuleEvaluator.RuleTypeCode,
  31. Severity = S8SeverityCode.Follow,
  32. DatasetCode = "PURCHASE_DELIVERY",
  33. WatchObjectType = "ORDER",
  34. PollIntervalSeconds = 300,
  35. };
  36. // ---------------- P0:整实体 PUT 退役 ----------------
  37. [Fact]
  38. public void UpdateAsync_IsRetired_WithWatchSpecificMessage()
  39. {
  40. var ex = Assert.Throws<S8WriteRetiredException>(
  41. () => { _ = Svc().UpdateAsync(1L, Body(), Scope); });
  42. Assert.Equal(S8WriteRetiredException.WatchRuleUpdateMessage, ex.Message);
  43. // 不能沿用 CFG_ALERT 的文案:那会让用户误以为「监控规则整体只读了」。
  44. Assert.NotEqual(S8WriteRetiredException.UserMessage, ex.Message);
  45. Assert.Contains("专用配置接口", ex.Message);
  46. }
  47. /// <summary>越权 / 不存在的 id 同样 410,不得因 id 不同回落 404 分支(避免存在性探测)。</summary>
  48. [Theory]
  49. [InlineData(0L)]
  50. [InlineData(187L)] // UAT 真实存在的 stale 行
  51. [InlineData(1329909460001L)] // Golden watch rule
  52. [InlineData(999999999L)] // 不存在
  53. public void UpdateAsync_IsRetired_RegardlessOfId(long id)
  54. {
  55. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().UpdateAsync(id, Body(), Scope); });
  56. }
  57. /// <summary>退役异常必须继承 S8BizException,使只捕获基类的旧调用方安全降级为 400 而非 500。</summary>
  58. [Fact]
  59. public void RetiredException_DerivesFromBizException_AndIsNotNotFound()
  60. {
  61. Assert.True(typeof(S8BizException).IsAssignableFrom(typeof(S8WriteRetiredException)));
  62. Assert.False(typeof(S8NotFoundException).IsAssignableFrom(typeof(S8WriteRetiredException)));
  63. }
  64. /// <summary>窄写入口与读路径必须保留——退役的只是整实体 PUT。</summary>
  65. [Fact]
  66. public void NarrowWritePathsAndReadPath_ArePreserved()
  67. {
  68. var t = typeof(S8WatchRuleService);
  69. // S8-TENANT-ONLY-BATCH5:列表按租户取,不再按 (租户, 工厂)。
  70. Assert.NotNull(t.GetMethod("ListAsync", new[] { typeof(long) }));
  71. Assert.Null(t.GetMethod("ListAsync", new[] { typeof(long), typeof(long) }));
  72. // S8-RULE-GOVERNANCE-BATCH1:整块 params 入口已被强类型白名单入口取代(G1 修复)。
  73. // 断言旧签名**不存在**,否则那条"缺字段即置 NULL"的路径就还活着。
  74. Assert.Null(t.GetMethods().FirstOrDefault(m => m.Name == "UpdateParamsAsync"));
  75. Assert.NotNull(t.GetMethod("UpdateParametersAsync", new[] { typeof(long), typeof(S8RuleParametersPayload), typeof(S8TrustedScope) }));
  76. Assert.NotNull(t.GetMethod("EnableAsync", new[] { typeof(long), typeof(S8TrustedScope) }));
  77. Assert.NotNull(t.GetMethod("DisableAsync", new[] { typeof(long), typeof(S8TrustedScope) }));
  78. Assert.NotNull(t.GetMethod("UpdateScheduleAsync", new[] { typeof(long), typeof(S8WatchRuleSchedulePayload), typeof(S8TrustedScope) }));
  79. Assert.NotNull(t.GetMethod("PauseAsync", new[] { typeof(long), typeof(S8TrustedScope) }));
  80. Assert.NotNull(t.GetMethod("ResumeAsync", new[] { typeof(long), typeof(S8TrustedScope) }));
  81. Assert.NotNull(t.GetMethod("RunNowAsync", new[] { typeof(long), typeof(S8TrustedScope) }));
  82. }
  83. // ================================================================================
  84. // S8-RULE-GOVERNANCE-BATCH3:原「P1:新建 canonical 词表」整段已删除。
  85. //
  86. // 那一段守护的是 ValidateVocabularyForCreate —— 即"调用方提交的 rule_type / scene_code /
  87. // severity 是否在词表内"。业务侧建规则能力已退役,该私有方法随之删除,
  88. // 词表校验的职责整体上移到 S8RuleCatalog 的构造期校验(且更严格:rule_type 改为 ordinal
  89. // 精确匹配,"timeout" 这类大小写错误在那里会直接让服务起不来,而不是存下来再运行期报错)。
  90. //
  91. // 对应覆盖去向:
  92. // · 词表 / 类型合法性 → S8RuleDefinitionContractTests.T2_*
  93. // · severity 不得静默降级 → S8RuleParameterContractTests.T6_NonWhitelistedSeverity_IsRejected
  94. // + S8TimeoutEvaluatorTests.RuntimeParameters_InvalidSeverity_*
  95. // · 业务不能建 / 删规则 → S8RuleCreationRetiredTests
  96. // ================================================================================
  97. }