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;
///
/// S8-LEGACY-SQL-RESIDUAL-CLEANUP-3:停止新增 SQL 技术债。
///
/// 治理口径不是"禁止一切 LEGACY_SQL"——132 条历史规则仍需运行,向导也仍在用服务端生成的 SQL。
/// 真正要掐断的是**调用方自带 expression** 这条路径:它既是技术债来源,也是注入面。
/// 因此按创建来源区分:
/// · EXTERNAL_API(裸 REST,调用方自带 SQL)→ 拒绝
/// · WIZARD_GENERATED(服务端字典白名单生成)→ 暂时放行,随向导迁移后收口
///
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(() =>
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(() =>
S8RuleCreationPolicy.EnsureCreationAllowed(rule, S8RuleCreationOrigin.ExternalApi));
}
[Fact]
public void WizardGenerated_LegacySqlRule_StillAllowed()
{
// 向导 SQL 由 S8ConfigDraftService 按字典白名单生成,非调用方自带;
// 该流程仍在生产使用,本批不阻断(见报告 KNOWN REMAINING SQL DEBT)。
S8RuleCreationPolicy.EnsureCreationAllowed(LegacyRule(), S8RuleCreationOrigin.WizardGenerated);
}
[Fact]
public void StandardDatasetRule_AllowedFromAnyOrigin()
{
S8RuleCreationPolicy.EnsureCreationAllowed(StandardRule(), S8RuleCreationOrigin.ExternalApi);
S8RuleCreationPolicy.EnsureCreationAllowed(StandardRule(), S8RuleCreationOrigin.WizardGenerated);
}
[Fact]
public void UnknownOrigin_IsTreatedAsExternal_FailClosed()
{
// 未知来源按最严格处理:新增调用方不会因为忘记声明来源而意外获得放行。
Assert.Throws(() =>
S8RuleCreationPolicy.EnsureCreationAllowed(LegacyRule(), "SOMETHING_NEW"));
}
// ============================================================
// D. 历史 Legacy 规则的维护不受治理影响
// ============================================================
[Fact]
public void ExistingLegacyRule_CanStillBeDisabled()
{
// 治理只作用于"新建"。若关停也被拦,数据集/数据源一出问题规则就再也关不掉。
var existing = LegacyRule();
S8RuleCreationPolicy.EnsureMaintenanceAllowed(existing, enabling: false);
}
[Fact]
public void ExistingLegacyRule_CanStillBeReEnabled()
{
// 历史规则重新启用不属于"新增技术债",不拦。
S8RuleCreationPolicy.EnsureMaintenanceAllowed(LegacyRule(), enabling: true);
}
// ============================================================
// 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 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 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 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.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);
}
}