S8WatchRuleDataAccessValidatorTests.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. using Admin.NET.Plugin.AiDOP.Entity.S8;
  2. using Admin.NET.Plugin.AiDOP.Service.S8;
  3. using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  4. using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
  5. using Xunit;
  6. namespace Admin.NET.Test.S8;
  7. /// <summary>
  8. /// 规则取数配置校验。
  9. ///
  10. /// S8-DATASET-FOUNDATION-HARDENING-2 起分两级:
  11. /// · ValidateForSave —— 结构 + 目录归属,不要求 Provider 已上线(允许存草稿);
  12. /// · ValidateForEnable —— 完整运行条件(委托 Enable Gate)。
  13. /// </summary>
  14. public class S8WatchRuleDataAccessValidatorTests
  15. {
  16. private const string ReadyDataset = "FAKE_VALIDATOR_DATASET";
  17. private const string DefinedNoProvider = "FAKE_VALIDATOR_DATASET_NO_PROVIDER";
  18. private static S8DatasetCapabilities TimeoutCapable() => new()
  19. {
  20. SupportsTimeout = true,
  21. HasSourceObjectId = true,
  22. HasRelatedObjectCode = true,
  23. HasDueAt = true,
  24. HasStatus = true
  25. };
  26. private sealed class FakeSource : IS8DatasetDefinitionSource
  27. {
  28. public IEnumerable<S8DatasetDefinition> GetDefinitions() => new[]
  29. {
  30. new S8DatasetDefinition
  31. {
  32. DatasetCode = ReadyDataset,
  33. DisplayName = "校验测试数据集",
  34. Kind = S8DatasetKind.Object,
  35. Capabilities = TimeoutCapable()
  36. },
  37. new S8DatasetDefinition
  38. {
  39. DatasetCode = DefinedNoProvider,
  40. DisplayName = "已定义未实现数据集",
  41. Kind = S8DatasetKind.Object,
  42. Capabilities = TimeoutCapable()
  43. }
  44. };
  45. }
  46. private sealed class TimeoutOnlyProvider : IS8MonitoringDataProvider
  47. {
  48. public string DatasetCode => ReadyDataset;
  49. public S8DatasetCapabilities Capabilities => TimeoutCapable();
  50. public Task<S8MonitoringRowSet> LoadAsync(S8MonitoringDataRequest request, CancellationToken cancellationToken = default)
  51. => Task.FromResult(S8MonitoringRowSet.Empty);
  52. }
  53. private static S8DatasetCatalog Catalog() => new(new[] { new FakeSource() });
  54. private static S8DatasetEnableGate Gate() =>
  55. new(Catalog(), new S8MonitoringDataProviderRegistry(new[] { new TimeoutOnlyProvider() }));
  56. private static AdoS8WatchRule Rule(string ruleType, string mode = null, string datasetCode = null, string expression = null) => new()
  57. {
  58. RuleCode = "UT_RULE",
  59. RuleType = ruleType,
  60. DataAccessMode = mode,
  61. DatasetCode = datasetCode,
  62. Expression = expression
  63. };
  64. // ---------- Legacy ----------
  65. [Fact]
  66. public void LegacyRule_WithNullMode_AndExpression_IsAccepted()
  67. {
  68. // 132 条历史规则的形态:mode=NULL、dataset_code=NULL、expression 有值。
  69. S8WatchRuleDataAccessValidator.ValidateForSave(
  70. Rule("TIMEOUT", mode: null, datasetCode: null, expression: "SELECT 1"), Catalog());
  71. }
  72. [Fact]
  73. public void LegacyRule_WithoutExpression_IsRejected()
  74. {
  75. var ex = Assert.Throws<S8BizException>(() =>
  76. S8WatchRuleDataAccessValidator.ValidateForSave(Rule("TIMEOUT", expression: null), Catalog()));
  77. Assert.Contains("expression", ex.Message);
  78. }
  79. [Fact]
  80. public void LegacyRule_MustNotCarryDatasetCode()
  81. {
  82. // 两种模式的字段混填会让实际生效路径不可预测,必须显式拒绝。
  83. var rule = Rule("TIMEOUT", mode: S8DataAccessMode.LegacySql, datasetCode: ReadyDataset, expression: "SELECT 1");
  84. Assert.Throws<S8BizException>(() => S8WatchRuleDataAccessValidator.ValidateForSave(rule, Catalog()));
  85. }
  86. [Fact]
  87. public void LegacyRule_PassesEnableGate_WithoutDatasetChecks()
  88. {
  89. var rule = Rule("TIMEOUT", mode: null, expression: "SELECT 1");
  90. S8WatchRuleDataAccessValidator.ValidateForEnable(rule, Gate(), 1, 1);
  91. }
  92. // ---------- Standard dataset · Save ----------
  93. [Fact]
  94. public void StandardDatasetRule_WithoutDatasetCode_IsRejectedOnSave()
  95. {
  96. var ex = Assert.Throws<S8BizException>(() =>
  97. S8WatchRuleDataAccessValidator.ValidateForSave(
  98. Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset), Catalog()));
  99. Assert.Contains(S8DatasetReasonCode.CodeMissing, ex.Message);
  100. }
  101. [Fact]
  102. public void StandardDatasetRule_WithUndefinedDataset_IsRejectedOnSave()
  103. {
  104. var ex = Assert.Throws<S8BizException>(() =>
  105. S8WatchRuleDataAccessValidator.ValidateForSave(
  106. Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: "NO_SUCH_DATASET"), Catalog()));
  107. Assert.Contains(S8DatasetReasonCode.NotDefined, ex.Message);
  108. }
  109. [Fact]
  110. public void StandardDatasetRule_DefinedButProviderMissing_CanBeSavedAsDraft()
  111. {
  112. // 关键行为:Provider 未上线时仍可保存配置。
  113. S8WatchRuleDataAccessValidator.ValidateForSave(
  114. Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: DefinedNoProvider), Catalog());
  115. }
  116. // ---------- Standard dataset · Enable ----------
  117. [Fact]
  118. public void StandardDatasetRule_DefinedButProviderMissing_CannotBeEnabled()
  119. {
  120. var ex = Assert.Throws<S8BizException>(() =>
  121. S8WatchRuleDataAccessValidator.ValidateForEnable(
  122. Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: DefinedNoProvider),
  123. Gate(), 1, 1));
  124. Assert.Contains(S8DatasetReasonCode.ProviderNotRegistered, ex.Message);
  125. }
  126. [Fact]
  127. public void StandardDatasetRule_WithCapabilityMismatch_CannotBeEnabled()
  128. {
  129. // TIMEOUT-only 数据集挂 OUT_OF_RANGE 规则。
  130. var ex = Assert.Throws<S8BizException>(() =>
  131. S8WatchRuleDataAccessValidator.ValidateForEnable(
  132. Rule("OUT_OF_RANGE", mode: S8DataAccessMode.StandardDataset, datasetCode: ReadyDataset),
  133. Gate(), 1, 1));
  134. Assert.Contains(S8DatasetReasonCode.CapabilityMismatch, ex.Message);
  135. }
  136. [Fact]
  137. public void StandardDatasetRule_Matching_IsEnabled_WithoutAnySql()
  138. {
  139. // 标准数据集规则不需要用户填写任何 SQL。
  140. var rule = Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: ReadyDataset, expression: null);
  141. S8WatchRuleDataAccessValidator.ValidateForSave(rule, Catalog());
  142. S8WatchRuleDataAccessValidator.ValidateForEnable(rule, Gate(), 1, 1);
  143. }
  144. // ---------- 其他 ----------
  145. [Fact]
  146. public void UnknownDataAccessMode_IsRejected()
  147. {
  148. Assert.Throws<S8BizException>(() =>
  149. S8WatchRuleDataAccessValidator.ValidateForSave(
  150. Rule("TIMEOUT", mode: "SOMETHING_ELSE", expression: "SELECT 1"), Catalog()));
  151. }
  152. [Fact]
  153. public void RequiresDataSource_TrueForLegacy_FalseForStandardDataset()
  154. {
  155. // LEGACY_SQL 仍必须绑定可信作用域内的数据源;STANDARD_DATASET 无物理数据源概念。
  156. Assert.True(S8WatchRuleDataAccessValidator.RequiresDataSource(Rule("TIMEOUT", mode: null)));
  157. Assert.True(S8WatchRuleDataAccessValidator.RequiresDataSource(Rule("TIMEOUT", mode: S8DataAccessMode.LegacySql)));
  158. Assert.False(S8WatchRuleDataAccessValidator.RequiresDataSource(
  159. Rule("TIMEOUT", mode: S8DataAccessMode.StandardDataset, datasetCode: ReadyDataset)));
  160. }
  161. }