using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Service.S8; using Admin.NET.Plugin.AiDOP.Service.S8.Rules; using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess; using Xunit; namespace Admin.NET.Test.S8; /// /// 规则取数配置校验。 /// /// S8-DATASET-FOUNDATION-HARDENING-2 起分两级: /// · ValidateForSave —— 结构 + 目录归属,不要求 Provider 已上线(允许存草稿); /// · ValidateForEnable —— 完整运行条件(委托 Enable Gate)。 /// public class S8WatchRuleDataAccessValidatorTests { private const string ReadyDataset = "FAKE_VALIDATOR_DATASET"; private const string DefinedNoProvider = "FAKE_VALIDATOR_DATASET_NO_PROVIDER"; private static S8DatasetCapabilities TimeoutCapable() => new() { SupportsTimeout = true, HasSourceObjectId = true, HasRelatedObjectCode = true, HasDueAt = true, HasStatus = true }; private sealed class FakeSource : IS8DatasetDefinitionSource { public IEnumerable GetDefinitions() => new[] { new S8DatasetDefinition { DatasetCode = ReadyDataset, DisplayName = "校验测试数据集", Kind = S8DatasetKind.Object, Capabilities = TimeoutCapable() }, new S8DatasetDefinition { DatasetCode = DefinedNoProvider, DisplayName = "已定义未实现数据集", Kind = S8DatasetKind.Object, Capabilities = TimeoutCapable() } }; } private sealed class TimeoutOnlyProvider : IS8MonitoringDataProvider { public string DatasetCode => ReadyDataset; public S8DatasetCapabilities Capabilities => TimeoutCapable(); public Task LoadAsync(S8MonitoringDataRequest request, CancellationToken cancellationToken = default) => Task.FromResult(S8MonitoringRowSet.Empty); } private static S8DatasetCatalog Catalog() => new(new[] { new FakeSource() }); private static S8DatasetEnableGate Gate() => new(Catalog(), new S8MonitoringDataProviderRegistry(new[] { new TimeoutOnlyProvider() })); private static AdoS8WatchRule Rule(string ruleType, string mode = null, string datasetCode = null, string expression = null) => new() { RuleCode = "UT_RULE", RuleType = ruleType, DataAccessMode = mode, DatasetCode = datasetCode, Expression = expression }; // ---------- Legacy ---------- [Fact] public void LegacyRule_WithNullMode_AndExpression_IsAccepted() { // 132 条历史规则的形态:mode=NULL、dataset_code=NULL、expression 有值。 S8WatchRuleDataAccessValidator.ValidateForSave( Rule("TIMEOUT", mode: null, datasetCode: null, expression: "SELECT 1"), Catalog()); } [Fact] public void LegacyRule_WithoutExpression_IsRejected() { var ex = Assert.Throws(() => S8WatchRuleDataAccessValidator.ValidateForSave(Rule("TIMEOUT", expression: null), Catalog())); Assert.Contains("expression", ex.Message); } [Fact] public void LegacyRule_MustNotCarryDatasetCode() { // 两种模式的字段混填会让实际生效路径不可预测,必须显式拒绝。 var rule = Rule("TIMEOUT", mode: S8DataAccessMode.LegacySql, datasetCode: ReadyDataset, expression: "SELECT 1"); Assert.Throws(() => S8WatchRuleDataAccessValidator.ValidateForSave(rule, Catalog())); } [Fact] public void LegacyRule_PassesEnableGate_WithoutDatasetChecks() { var rule = Rule("TIMEOUT", mode: null, expression: "SELECT 1"); S8WatchRuleDataAccessValidator.ValidateForEnable(rule, Gate(), 1, 1); } // ---------- Standard dataset · Save ---------- [Fact] public void StandardDatasetRule_WithoutDatasetCode_IsRejectedOnSave() { var ex = Assert.Throws(() => S8WatchRuleDataAccessValidator.ValidateForSave( Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset), Catalog())); Assert.Contains(S8DatasetReasonCode.CodeMissing, ex.Message); } [Fact] public void StandardDatasetRule_WithUndefinedDataset_IsRejectedOnSave() { var ex = Assert.Throws(() => S8WatchRuleDataAccessValidator.ValidateForSave( Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: "NO_SUCH_DATASET"), Catalog())); Assert.Contains(S8DatasetReasonCode.NotDefined, ex.Message); } [Fact] public void StandardDatasetRule_DefinedButProviderMissing_CanBeSavedAsDraft() { // 关键行为:Provider 未上线时仍可保存配置。 S8WatchRuleDataAccessValidator.ValidateForSave( Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: DefinedNoProvider), Catalog()); } // ---------- Standard dataset · Enable ---------- [Fact] public void StandardDatasetRule_DefinedButProviderMissing_CannotBeEnabled() { var ex = Assert.Throws(() => S8WatchRuleDataAccessValidator.ValidateForEnable( Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: DefinedNoProvider), Gate(), 1, 1)); Assert.Contains(S8DatasetReasonCode.ProviderNotRegistered, ex.Message); } [Fact] public void StandardDatasetRule_WithCapabilityMismatch_CannotBeEnabled() { // TIMEOUT-only 数据集挂 OUT_OF_RANGE 规则。 var ex = Assert.Throws(() => S8WatchRuleDataAccessValidator.ValidateForEnable( Rule("OUT_OF_RANGE", mode: S8DataAccessMode.StandardDataset, datasetCode: ReadyDataset), Gate(), 1, 1)); Assert.Contains(S8DatasetReasonCode.CapabilityMismatch, ex.Message); } [Fact] public void StandardDatasetRule_Matching_IsEnabled_WithoutAnySql() { // 标准数据集规则不需要用户填写任何 SQL。 var rule = Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: ReadyDataset, expression: null); S8WatchRuleDataAccessValidator.ValidateForSave(rule, Catalog()); S8WatchRuleDataAccessValidator.ValidateForEnable(rule, Gate(), 1, 1); } // ---------- 其他 ---------- [Fact] public void UnknownDataAccessMode_IsRejected() { Assert.Throws(() => S8WatchRuleDataAccessValidator.ValidateForSave( Rule("TIMEOUT", mode: "SOMETHING_ELSE", expression: "SELECT 1"), Catalog())); } [Fact] public void RequiresDataSource_TrueForLegacy_FalseForStandardDataset() { // LEGACY_SQL 仍必须绑定可信作用域内的数据源;STANDARD_DATASET 无物理数据源概念。 Assert.True(S8WatchRuleDataAccessValidator.RequiresDataSource(Rule("TIMEOUT", mode: null))); Assert.True(S8WatchRuleDataAccessValidator.RequiresDataSource(Rule("TIMEOUT", mode: S8DataAccessMode.LegacySql))); Assert.False(S8WatchRuleDataAccessValidator.RequiresDataSource( Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: ReadyDataset))); } }