using System.Reflection;
using System.Text.RegularExpressions;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess.Providers;
using Xunit;
namespace Admin.NET.Test.S8;
///
/// S8-RULE01-SQL-MAPPING-GUARD-1:SQL 结果别名与 POCO 属性的映射契约。
///
/// 为什么需要单独一个文件:本仓 DbSettings.EnableUnderLine = false,
/// SqlSugar 不做 snake_case → PascalCase 换算,Ado.SqlQueryAsync<T> 按名字直接匹配。
/// 别名一旦写成 snake_case,映射会**静默失败**:
/// · 不抛异常
/// · 行数照常返回
/// · 但每个属性都是 null / default
/// 于是 evaluator 在 due == null 处逐行跳过,命中恒为 0 —— 表面像"当前没有超期单",
/// 实际是取数根本没落到对象上。本地全链实验中真实发生过一次(36 候选 / 0 命中)。
///
/// 为什么既有测试抓不到:Provider 映射契约测试是直接 new PurchaseDeliveryRow{...}
/// 再喂 evaluator,整条 SqlSugar 映射被绕开;E2E 测试用的 stub Provider 同理。
/// 两者都在"映射之后"开始验证,因此本文件必须从 SQL 文本本身入手。
///
/// 不接 DB、不接 DI:解析 SQL 常量 + 反射比对属性名。
///
public class S8PurchaseDeliverySqlMappingTests
{
/// 取 SELECT 列表里所有 AS <alias> 的别名。
private static string[] SelectAliases() =>
Regex.Matches(S8PurchaseDeliveryDataProvider.Sql, @"\bAS\s+([A-Za-z_][A-Za-z0-9_]*)", RegexOptions.IgnoreCase)
.Select(m => m.Groups[1].Value)
.ToArray();
private static string[] RowProperties() =>
typeof(S8PurchaseDeliveryDataProvider.PurchaseDeliveryRow)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToArray();
///
/// 每个 SQL 别名都必须**逐字**等于某个属性名。
/// 用 Ordinal 而非忽略大小写:SqlSugar 的匹配行为不该靠"大概能对上"来赌。
///
[Fact]
public void EverySelectAlias_MapsToRowProperty()
{
var props = RowProperties();
var aliases = SelectAliases();
Assert.NotEmpty(aliases);
var unmapped = aliases.Where(a => !props.Contains(a, StringComparer.Ordinal)).ToArray();
Assert.True(unmapped.Length == 0,
"以下 SQL 别名在 PurchaseDeliveryRow 上找不到同名属性,SqlSugar 会静默映射为 null:\n "
+ string.Join("\n ", unmapped));
}
///
/// 反向:每个属性都必须被某个别名覆盖。
/// 漏掉一个属性同样是静默 null —— 例如漏掉 DueAt 就直接导致命中恒为 0。
///
[Fact]
public void EveryRowProperty_IsProducedBySql()
{
var aliases = SelectAliases();
var missing = RowProperties().Where(p => !aliases.Contains(p, StringComparer.Ordinal)).ToArray();
Assert.True(missing.Length == 0,
"以下属性没有对应的 SQL 别名,取数后将恒为 null:\n " + string.Join("\n ", missing));
}
///
/// 别名不得含下划线。EnableUnderLine=false 时 snake_case 别名等于"注定映射不上",
/// 而失败形态是静默的,所以这条要单独钉死,而不是依赖上面两条的间接覆盖。
///
[Fact]
public void NoAlias_UsesSnakeCase()
{
var snake = SelectAliases().Where(a => a.Contains('_', StringComparison.Ordinal)).ToArray();
Assert.True(snake.Length == 0,
"本仓 EnableUnderLine=false,snake_case 别名会静默映射失败:\n " + string.Join("\n ", snake));
}
///
/// 驱动判定的四个 canonical 字段必须齐备。
/// 其余诊断列缺失只是 payload 少信息,这四个缺一个就直接改变判定结果。
///
[Theory]
[InlineData("SourceObjectId")]
[InlineData("RelatedObjectCode")]
[InlineData("DueAt")]
[InlineData("Status")]
public void CanonicalDrivingField_IsSelected(string alias)
{
Assert.Contains(alias, SelectAliases(), StringComparer.Ordinal);
}
///
/// WHERE / FROM 仍必须用数据库真实列名(snake_case)——
/// 别名只改输出侧,改错这一侧会直接让 SQL 报「Unknown column」。
///
[Fact]
public void PhysicalColumnNames_StayUnchangedInPredicates()
{
var sql = S8PurchaseDeliveryDataProvider.Sql;
Assert.Contains("d.tenant_id = @TenantId", sql, StringComparison.Ordinal);
Assert.Contains("d.stat_date =", sql, StringComparison.Ordinal);
Assert.Contains("d.po_no <> ''", sql, StringComparison.Ordinal);
Assert.Contains("d.po_line <> ''", sql, StringComparison.Ordinal);
Assert.Contains("FROM dwd_supplier_delivery d", sql, StringComparison.Ordinal);
}
}