using System.Reflection; using System.Runtime.CompilerServices; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Infrastructure; using Admin.NET.Plugin.AiDOP.Infrastructure.S8; using Admin.NET.Plugin.AiDOP.Service.S8; using Admin.NET.Plugin.AiDOP.Service.S8.Rules; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S8; /// /// S8-STEP6E-CFG-WATCH-FIX-AND-SAFE-CERT-1:钉住 CFG_WATCH 的 /// 「整实体 PUT 退役 + 新建 canonical 词表」契约。 /// /// 不触任何数据库。 退役守卫在**任何 DB 访问之前**抛出,词表校验是纯静态方法, /// 所以都不需要可用的仓储。用 绕开构造函数 /// (其 SqlSugarRepository<T> 参数的无参构造会触发 Furion.App 静态初始化, /// 在无宿主的 xunit 进程内必抛 TypeInitializationException)。 /// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 失败—— /// 这正是我们要的信号:守卫必须前置 /// public class S8WatchRuleWriteGuardTests { private static S8WatchRuleService Svc() => (S8WatchRuleService)RuntimeHelpers.GetUninitializedObject(typeof(S8WatchRuleService)); private static readonly S8TrustedScope Scope = new(838257186181189L); private static AdoS8WatchRule Body() => new() { RuleCode = "TEST-WATCH-X", SceneCode = "S1", RuleType = S8TimeoutRuleEvaluator.RuleTypeCode, Severity = S8SeverityCode.Follow, DatasetCode = "PURCHASE_DELIVERY", WatchObjectType = "ORDER", PollIntervalSeconds = 300, }; // ---------------- P0:整实体 PUT 退役 ---------------- [Fact] public void UpdateAsync_IsRetired_WithWatchSpecificMessage() { var ex = Assert.Throws( () => { _ = Svc().UpdateAsync(1L, Body(), Scope); }); Assert.Equal(S8WriteRetiredException.WatchRuleUpdateMessage, ex.Message); // 不能沿用 CFG_ALERT 的文案:那会让用户误以为「监控规则整体只读了」。 Assert.NotEqual(S8WriteRetiredException.UserMessage, ex.Message); Assert.Contains("专用配置接口", ex.Message); } /// 越权 / 不存在的 id 同样 410,不得因 id 不同回落 404 分支(避免存在性探测)。 [Theory] [InlineData(0L)] [InlineData(187L)] // UAT 真实存在的 stale 行 [InlineData(1329909460001L)] // Golden watch rule [InlineData(999999999L)] // 不存在 public void UpdateAsync_IsRetired_RegardlessOfId(long id) { Assert.Throws(() => { _ = Svc().UpdateAsync(id, Body(), Scope); }); } /// 退役异常必须继承 S8BizException,使只捕获基类的旧调用方安全降级为 400 而非 500。 [Fact] public void RetiredException_DerivesFromBizException_AndIsNotNotFound() { Assert.True(typeof(S8BizException).IsAssignableFrom(typeof(S8WriteRetiredException))); Assert.False(typeof(S8NotFoundException).IsAssignableFrom(typeof(S8WriteRetiredException))); } /// 窄写入口与读路径必须保留——退役的只是整实体 PUT。 [Fact] public void NarrowWritePathsAndReadPath_ArePreserved() { var t = typeof(S8WatchRuleService); // S8-TENANT-ONLY-BATCH5:列表按租户取,不再按 (租户, 工厂)。 Assert.NotNull(t.GetMethod("ListAsync", new[] { typeof(long) })); Assert.Null(t.GetMethod("ListAsync", new[] { typeof(long), typeof(long) })); // S8-RULE-GOVERNANCE-BATCH1:整块 params 入口已被强类型白名单入口取代(G1 修复)。 // 断言旧签名**不存在**,否则那条"缺字段即置 NULL"的路径就还活着。 Assert.Null(t.GetMethods().FirstOrDefault(m => m.Name == "UpdateParamsAsync")); Assert.NotNull(t.GetMethod("UpdateParametersAsync", new[] { typeof(long), typeof(S8RuleParametersPayload), typeof(S8TrustedScope) })); Assert.NotNull(t.GetMethod("EnableAsync", new[] { typeof(long), typeof(S8TrustedScope) })); Assert.NotNull(t.GetMethod("DisableAsync", new[] { typeof(long), typeof(S8TrustedScope) })); Assert.NotNull(t.GetMethod("UpdateScheduleAsync", new[] { typeof(long), typeof(S8WatchRuleSchedulePayload), typeof(S8TrustedScope) })); Assert.NotNull(t.GetMethod("PauseAsync", new[] { typeof(long), typeof(S8TrustedScope) })); Assert.NotNull(t.GetMethod("ResumeAsync", new[] { typeof(long), typeof(S8TrustedScope) })); Assert.NotNull(t.GetMethod("RunNowAsync", new[] { typeof(long), typeof(S8TrustedScope) })); } // ================================================================================ // S8-RULE-GOVERNANCE-BATCH3:原「P1:新建 canonical 词表」整段已删除。 // // 那一段守护的是 ValidateVocabularyForCreate —— 即"调用方提交的 rule_type / scene_code / // severity 是否在词表内"。业务侧建规则能力已退役,该私有方法随之删除, // 词表校验的职责整体上移到 S8RuleCatalog 的构造期校验(且更严格:rule_type 改为 ordinal // 精确匹配,"timeout" 这类大小写错误在那里会直接让服务起不来,而不是存下来再运行期报错)。 // // 对应覆盖去向: // · 词表 / 类型合法性 → S8RuleDefinitionContractTests.T2_* // · severity 不得静默降级 → S8RuleParameterContractTests.T6_NonWhitelistedSeverity_IsRejected // + S8TimeoutEvaluatorTests.RuntimeParameters_InvalidSeverity_* // · 业务不能建 / 删规则 → S8RuleCreationRetiredTests // ================================================================================ }