| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- 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;
- /// <summary>
- /// S8-STEP6E-CFG-WATCH-FIX-AND-SAFE-CERT-1:钉住 CFG_WATCH 的
- /// 「整实体 PUT 退役 + 新建 canonical 词表」契约。
- ///
- /// <para><b>不触任何数据库。</b> 退役守卫在**任何 DB 访问之前**抛出,词表校验是纯静态方法,
- /// 所以都不需要可用的仓储。用 <see cref="RuntimeHelpers.GetUninitializedObject"/> 绕开构造函数
- /// (其 <c>SqlSugarRepository<T></c> 参数的无参构造会触发 Furion.App 静态初始化,
- /// 在无宿主的 xunit 进程内必抛 TypeInitializationException)。
- /// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 失败——
- /// 这正是我们要的信号:<b>守卫必须前置</b>。</para>
- /// </summary>
- 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<S8WriteRetiredException>(
- () => { _ = Svc().UpdateAsync(1L, Body(), Scope); });
- Assert.Equal(S8WriteRetiredException.WatchRuleUpdateMessage, ex.Message);
- // 不能沿用 CFG_ALERT 的文案:那会让用户误以为「监控规则整体只读了」。
- Assert.NotEqual(S8WriteRetiredException.UserMessage, ex.Message);
- Assert.Contains("专用配置接口", ex.Message);
- }
- /// <summary>越权 / 不存在的 id 同样 410,不得因 id 不同回落 404 分支(避免存在性探测)。</summary>
- [Theory]
- [InlineData(0L)]
- [InlineData(187L)] // UAT 真实存在的 stale 行
- [InlineData(1329909460001L)] // Golden watch rule
- [InlineData(999999999L)] // 不存在
- public void UpdateAsync_IsRetired_RegardlessOfId(long id)
- {
- Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().UpdateAsync(id, Body(), Scope); });
- }
- /// <summary>退役异常必须继承 S8BizException,使只捕获基类的旧调用方安全降级为 400 而非 500。</summary>
- [Fact]
- public void RetiredException_DerivesFromBizException_AndIsNotNotFound()
- {
- Assert.True(typeof(S8BizException).IsAssignableFrom(typeof(S8WriteRetiredException)));
- Assert.False(typeof(S8NotFoundException).IsAssignableFrom(typeof(S8WriteRetiredException)));
- }
- /// <summary>窄写入口与读路径必须保留——退役的只是整实体 PUT。</summary>
- [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
- // ================================================================================
- }
|