using Admin.NET.Plugin.AiDOP.Controllers.S8;
using System.Reflection;
using System.Text.RegularExpressions;
using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.S8;
///
/// S8-P1-FACTUAL-DISPLAY-1:订单执行档案的取数 Authority 守卫。
///
/// 档案页的三张 KPI 卡(平均响应 / 处理 / 损失时间)与订单计数,
/// 全部建立在 GET /api/aidop/s8/order-flow/orders 的返回上。
/// 该路由必须走
/// (读 mdp_std_so + dwd_*),不得回退到读
/// ado_s8_order_flow_order 的 SEED 路径 —— 那张表唯一的写入方是 SeedData。
///
/// SEED 路径(S8OrderFlowService)本身保留,供链路全景等既有消费方与回归 fixture 使用;
/// 本测试钉住的是「两路不混用」,不是「删掉 SEED 路径」。
///
/// 手法沿用本仓既有范式(反射元数据 + 去注释源码契约),
/// 与 S8DebugTenantGuardTests / S8KpiProvenanceTests 一致:
/// 服务依赖 SqlSugar 实库,进程内无法构造。
///
public class S8OrderArchiveAuthorityTests
{
private static string PluginRoot => Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, "../../../../Admin.NET.Plugin.AiDOP"));
private static string ReadCode(string relativePath)
{
var raw = File.ReadAllText(Path.Combine(PluginRoot, relativePath));
var noBlock = Regex.Replace(raw, @"/\*.*?\*/", string.Empty, RegexOptions.Singleline);
return Regex.Replace(noBlock, @"^\s*//.*$", string.Empty, RegexOptions.Multiline);
}
/// SEED 订单链路实体:不得出现在真实档案取数链上。
private static readonly string[] SeedOrderFlowEntities =
{
"AdoS8OrderFlowOrder",
"AdoS8OrderFlowStage",
"AdoS8OrderFlowProcurementPivot",
"AdoS8OrderFlowManufacturingProcess",
};
// ───────── ⑦ OrderArchiveKpi_UsesRealOrderAuthority ─────────
/// /orders 必须由 REAL 服务提供。
[Fact]
public void OrdersEndpoint_IsServedByRealArchiveService()
{
var code = ReadCode("Controllers/S8/AdoS8OrderFlowController.cs");
var idx = code.IndexOf("[HttpGet(\"orders\")]", StringComparison.Ordinal);
Assert.True(idx > 0, "未找到 /orders 路由");
var action = code[idx..Math.Min(idx + 420, code.Length)];
Assert.Contains("_realSvc.GetRealOrdersAsync", action);
Assert.DoesNotContain("_svc.GetOrdersAsync", action);
}
// ───────── ⑥ OrderArchiveKpi_DoesNotReadSeedRepository ─────────
/// REAL 档案服务不得依赖任何 SEED 订单链路仓储。
[Fact]
public void RealArchiveService_DoesNotReadSeedOrderFlowTables()
{
var code = ReadCode("Service/S8/OrderFlow/S8RealOrderArchiveService.cs");
// 判据落在「仓储依赖」与「物理表名」上,不能裸搜实体名 ——
// 本服务的返回类型叫 AdoS8OrderFlowOrderListItemDto / ...DetailDto,
// 那是 DTO,不是 SEED 实体;裸子串会把一条真守卫变成噪音。
foreach (var entity in SeedOrderFlowEntities)
Assert.DoesNotContain($"SqlSugarRepository<{entity}>", code);
Assert.DoesNotContain("ado_s8_order_flow", code);
// 更强的结构性保证:本服务根本不注入任何仓储,只走 Ado 原生 SQL。
Assert.DoesNotContain("SqlSugarRepository", code);
}
/// 反射层同一条守卫:构造函数不得注入 SEED 仓储。改文案绕不过去。
[Fact]
public void RealArchiveService_TakesNoSeedRepository()
{
var svc = typeof(AdoS8OrderFlowController).Assembly
.GetType("Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow.S8RealOrderArchiveService");
Assert.NotNull(svc);
var ctor = Assert.Single(svc!.GetConstructors());
var injected = ctor.GetParameters()
.Select(p => p.ParameterType.IsGenericType
? p.ParameterType.GetGenericArguments()[0].Name
: p.ParameterType.Name)
.ToList();
foreach (var entity in SeedOrderFlowEntities)
Assert.DoesNotContain(entity, injected);
}
///
/// 真实档案取数必须读数据中台标准层,且不得为了「去 seed」改成直连业务源表。
///
[Fact]
public void RealArchiveService_ReadsMdpAuthorityOnly()
{
var code = ReadCode("Service/S8/OrderFlow/S8RealOrderArchiveService.cs");
Assert.Contains("mdp_std_so", code);
foreach (var src in new[]
{
"PurOrdMaster", "PurOrdDetail", "WorkOrdMaster",
"NbrMaster", "NbrDetail", "qms_", "crm_seorder", "mdp_stg_",
})
Assert.DoesNotContain(src, code);
}
// ───────── ⑨ TenantA_KpiCannotSeeTenantB ─────────
/// 档案取数的租户作用域一律服务端解析,不接受调用方传入。
[Fact]
public void RealArchive_ResolvesTenantServerSide()
{
var code = ReadCode("Service/S8/OrderFlow/S8RealOrderArchiveService.cs");
Assert.Contains("AidopTenantScope.ResolveOrThrow", code);
// 原生 SQL 用反引号写列名
Assert.Contains("`tenant_id` = @TenantId", code);
Assert.DoesNotContain("ClearFilter", code);
}
}