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)));
}
}