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); } }