| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- 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 Microsoft.Extensions.Logging.Abstractions;
- using Xunit;
- namespace Admin.NET.Test.S8;
- /// <summary>
- /// S8-LEGACY-SQL-RESIDUAL-CLEANUP-3:停止新增 SQL 技术债。
- ///
- /// 治理口径不是"禁止一切 LEGACY_SQL"——132 条历史规则仍需运行,向导也仍在用服务端生成的 SQL。
- /// 真正要掐断的是**调用方自带 expression** 这条路径:它既是技术债来源,也是注入面。
- /// 因此按创建来源区分:
- /// · EXTERNAL_API(裸 REST,调用方自带 SQL)→ 拒绝
- /// · WIZARD_GENERATED(服务端字典白名单生成)→ 暂时放行,随向导迁移后收口
- /// </summary>
- public class S8LegacySqlGovernanceTests
- {
- private static AdoS8WatchRule LegacyRule() => new()
- {
- RuleCode = "UT_NEW_LEGACY",
- RuleType = "TIMEOUT",
- DataAccessMode = null, // 等价 LEGACY_SQL
- Expression = "SELECT * FROM some_physical_table WHERE 1=1"
- };
- private static AdoS8WatchRule StandardRule() => new()
- {
- RuleCode = "UT_NEW_STANDARD",
- RuleType = "TIMEOUT",
- DataAccessMode = S8DataAccessMode.StandardDataset,
- DatasetCode = "ANY_DATASET"
- };
- // ============================================================
- // C. 新建 Legacy SQL 规则
- // ============================================================
- [Fact]
- public void ExternalApi_CreatingLegacySqlRule_IsRejected()
- {
- var ex = Assert.Throws<S8BizException>(() =>
- S8RuleCreationPolicy.EnsureCreationAllowed(LegacyRule(), S8RuleCreationOrigin.ExternalApi));
- Assert.Contains(S8RuleCreationPolicy.LegacySqlCreationDisabledReason, ex.Message);
- }
- [Fact]
- public void ExternalApi_CreatingLegacySqlRule_ExplicitMode_IsAlsoRejected()
- {
- // 显式写 LEGACY_SQL 与留空等价,不能靠显式声明绕过治理。
- var rule = LegacyRule();
- rule.DataAccessMode = S8DataAccessMode.LegacySql;
- Assert.Throws<S8BizException>(() =>
- S8RuleCreationPolicy.EnsureCreationAllowed(rule, S8RuleCreationOrigin.ExternalApi));
- }
- [Fact]
- public void WizardGenerated_LegacySqlRule_IsNowAlsoRejected()
- {
- // 语义变更(S8-LEGACY-RUNTIME-RETIREMENT-1):本用例原名
- // WizardGenerated_LegacySqlRule_StillAllowed —— 当时向导是唯一豁免通道,
- // 因为它是在产流程且 SQL 由服务端按字典白名单生成。
- //
- // 该豁免现已取消:没有治理 Dataset 就没有可运行 Rule,不再用 Legacy SQL 兜底。
- // 向导保留独立文案(提示"当前对象无治理数据集"),因为在向导里报
- // "不接受你自带的 SQL" 会让用户完全对不上号 —— 他并没有写过 SQL。
- var ex = Assert.Throws<S8BizException>(() =>
- S8RuleCreationPolicy.EnsureCreationAllowed(LegacyRule(), S8RuleCreationOrigin.WizardGenerated));
- Assert.Contains(S8RuleCreationPolicy.LegacySqlRetiredReason, ex.Message);
- }
- [Fact]
- public void StandardDatasetRule_AllowedFromAnyOrigin()
- {
- S8RuleCreationPolicy.EnsureCreationAllowed(StandardRule(), S8RuleCreationOrigin.ExternalApi);
- S8RuleCreationPolicy.EnsureCreationAllowed(StandardRule(), S8RuleCreationOrigin.WizardGenerated);
- }
- [Fact]
- public void UnknownOrigin_IsTreatedAsExternal_FailClosed()
- {
- // 未知来源按最严格处理:新增调用方不会因为忘记声明来源而意外获得放行。
- Assert.Throws<S8BizException>(() =>
- S8RuleCreationPolicy.EnsureCreationAllowed(LegacyRule(), "SOMETHING_NEW"));
- }
- // ============================================================
- // D. 历史 Legacy 规则的维护不受治理影响
- // ============================================================
- [Fact]
- public void ExistingLegacyRule_CanStillBeDisabled()
- {
- // 这条**必须**保持放行:若关停也被拦,数据集/数据源一出问题规则就再也关不掉,
- // 只能去改库。退役门禁的方向是单向的 —— 只拦「进入运行态」。
- var existing = LegacyRule();
- S8RuleCreationPolicy.EnsureMaintenanceAllowed(existing, enabling: false);
- }
- [Fact]
- public void ExistingLegacyRule_CannotBeReEnabled_AfterRuntimeRetirement()
- {
- // 语义变更(S8-LEGACY-RUNTIME-RETIREMENT-1):本用例原名
- // ExistingLegacyRule_CanStillBeReEnabled —— 上一阶段的治理目标是"不新增旧债",
- // 故存量规则重新启用是放行的。本阶段目标升级为"旧债不得运行",因此反转。
- var ex = Assert.Throws<S8BizException>(() =>
- S8RuleCreationPolicy.EnsureMaintenanceAllowed(LegacyRule(), enabling: true));
- Assert.Contains(S8RuleCreationPolicy.LegacySqlRetiredReason, ex.Message);
- }
- [Fact]
- public void ExplicitLegacyMode_CannotBeReEnabled_Either()
- {
- // NULL(历史现状)与显式 LEGACY_SQL 必须同样被拦,否则回填一次字面值就能绕过退役。
- var rule = LegacyRule();
- rule.DataAccessMode = S8DataAccessMode.LegacySql;
- Assert.Throws<S8BizException>(() =>
- S8RuleCreationPolicy.EnsureMaintenanceAllowed(rule, enabling: true));
- }
- [Fact]
- public void StandardDatasetRule_CanBeEnabled()
- {
- // 正向对照:退役门禁不能误伤 STANDARD_DATASET(Rule 01 走的正是这条路)。
- S8RuleCreationPolicy.EnsureMaintenanceAllowed(StandardRule(), enabling: true);
- }
- // ============================================================
- // E. 运行资格判定(调度器纵深防御所依赖的同一个谓词)
- // ============================================================
- [Theory]
- [InlineData(null)] // 132 条历史规则的现状
- [InlineData("")] // 空串
- [InlineData(" ")] // 空白
- [InlineData("LEGACY_SQL")] // 显式声明
- [InlineData("legacy_sql")] // 大小写变体
- [InlineData("SOMETHING_ELSE")] // 脏数据:既非 Legacy 也非 Standard
- public void IsRuntimeEligible_IsFalse_ForEverythingButStandardDataset(string? mode)
- {
- var rule = LegacyRule();
- rule.DataAccessMode = mode;
- Assert.False(S8RuleCreationPolicy.IsRuntimeEligible(rule));
- }
- [Theory]
- [InlineData("STANDARD_DATASET")]
- [InlineData("standard_dataset")] // Resolve 对模式值大小写不敏感
- public void IsRuntimeEligible_IsTrue_ForStandardDataset(string mode)
- {
- var rule = StandardRule();
- rule.DataAccessMode = mode;
- Assert.True(S8RuleCreationPolicy.IsRuntimeEligible(rule));
- }
- [Fact]
- public void IsRuntimeEligible_DoesNotThrow_OnDirtyModeValue()
- {
- // 调度器每个 tick 都会调它。脏数据必须返回 false 而不是抛异常中断整个 tick,
- // 否则一条坏行就能让全租户的监控停摆(S8DataAccessMode.Resolve 对未知值是抛错的)。
- var rule = LegacyRule();
- rule.DataAccessMode = "NOT_A_REAL_MODE";
- Assert.False(S8RuleCreationPolicy.IsRuntimeEligible(rule));
- }
- // ============================================================
- // B. STANDARD_DATASET 即使带恶意 SQL 也绝不执行
- // ============================================================
- private const string PoisonDataset = "FAKE_POISON_DATASET";
- private static S8DatasetCapabilities TimeoutCapable() => new()
- {
- SupportsTimeout = true,
- HasSourceObjectId = true,
- HasRelatedObjectCode = true,
- HasDueAt = true,
- HasStatus = true
- };
- private sealed class PoisonSource : IS8DatasetDefinitionSource
- {
- public IEnumerable<S8DatasetDefinition> GetDefinitions() => new[]
- {
- new S8DatasetDefinition
- {
- DatasetCode = PoisonDataset,
- DisplayName = "恶意SQL防护测试数据集",
- Kind = S8DatasetKind.Object,
- Capabilities = TimeoutCapable()
- }
- };
- }
- private sealed class EmptyProvider : IS8MonitoringDataProvider
- {
- public string DatasetCode => PoisonDataset;
- public S8DatasetCapabilities Capabilities => TimeoutCapable();
- public Task<S8MonitoringRowSet> LoadAsync(S8MonitoringDataRequest request, CancellationToken cancellationToken = default)
- {
- request.EnsureValid();
- return Task.FromResult(S8MonitoringRowSet.Empty);
- }
- }
- private sealed class SpyLegacy : IS8LegacySqlDataProvider
- {
- public int CallCount { get; private set; }
- public string LastExpression { get; private set; }
- public Task<S8MonitoringDataResult> LoadAsync(
- long tenantId, long factoryId, AdoS8WatchRule rule, string ruleType,
- int timeoutSeconds, CancellationToken cancellationToken = default)
- {
- CallCount++;
- LastExpression = rule.Expression;
- return Task.FromResult(new S8MonitoringDataResult { RowSet = S8MonitoringRowSet.Empty });
- }
- }
- [Fact]
- public async Task StandardDataset_MaliciousExpression_IsNeverExecuted()
- {
- var legacy = new SpyLegacy();
- var gateway = new S8MonitoringDataGateway(
- legacy,
- new S8MonitoringDataProviderRegistry(new[] { new EmptyProvider() }),
- new S8DatasetCatalog(new[] { new PoisonSource() }),
- NullLogger<S8MonitoringDataGateway>.Instance);
- var rule = new AdoS8WatchRule
- {
- RuleCode = "UT_POISON",
- RuleType = "TIMEOUT",
- DataAccessMode = S8DataAccessMode.StandardDataset,
- DatasetCode = PoisonDataset,
- // 即使历史遗留字段里躺着可执行 SQL,STANDARD_DATASET 路径也绝不能碰它。
- Expression = "DROP TABLE ado_s8_exception; SELECT SHOULD_NEVER_RUN"
- };
- var result = await gateway.LoadAsync(1, 1, rule, "TIMEOUT", 60, 1000);
- Assert.Equal(S8DataAccessMode.StandardDataset, result.DataAccessMode);
- Assert.Equal(0, legacy.CallCount);
- Assert.Null(legacy.LastExpression);
- }
- }
|