S8OrderArchiveAuthorityTests.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. using Admin.NET.Plugin.AiDOP.Controllers.S8;
  2. using System.Reflection;
  3. using System.Text.RegularExpressions;
  4. using Xunit;
  5. namespace Admin.NET.Plugin.AiDOP.Tests.S8;
  6. /// <summary>
  7. /// S8-P1-FACTUAL-DISPLAY-1:订单执行档案的取数 Authority 守卫。
  8. ///
  9. /// <para>档案页的三张 KPI 卡(平均响应 / 处理 / 损失时间)与订单计数,
  10. /// 全部建立在 <c>GET /api/aidop/s8/order-flow/orders</c> 的返回上。
  11. /// 该路由必须走 <see cref="Service.S8.OrderFlow.S8RealOrderArchiveService"/>
  12. /// (读 <c>mdp_std_so</c> + <c>dwd_*</c>),<b>不得</b>回退到读
  13. /// <c>ado_s8_order_flow_order</c> 的 SEED 路径 —— 那张表唯一的写入方是 SeedData。</para>
  14. ///
  15. /// <para>SEED 路径(<c>S8OrderFlowService</c>)本身保留,供链路全景等既有消费方与回归 fixture 使用;
  16. /// 本测试钉住的是「两路不混用」,不是「删掉 SEED 路径」。</para>
  17. ///
  18. /// <para>手法沿用本仓既有范式(反射元数据 + 去注释源码契约),
  19. /// 与 <c>S8DebugTenantGuardTests</c> / <c>S8KpiProvenanceTests</c> 一致:
  20. /// 服务依赖 SqlSugar 实库,进程内无法构造。</para>
  21. /// </summary>
  22. public class S8OrderArchiveAuthorityTests
  23. {
  24. private static string PluginRoot => Path.GetFullPath(
  25. Path.Combine(AppContext.BaseDirectory, "../../../../Admin.NET.Plugin.AiDOP"));
  26. private static string ReadCode(string relativePath)
  27. {
  28. var raw = File.ReadAllText(Path.Combine(PluginRoot, relativePath));
  29. var noBlock = Regex.Replace(raw, @"/\*.*?\*/", string.Empty, RegexOptions.Singleline);
  30. return Regex.Replace(noBlock, @"^\s*//.*$", string.Empty, RegexOptions.Multiline);
  31. }
  32. /// <summary>SEED 订单链路实体:不得出现在真实档案取数链上。</summary>
  33. private static readonly string[] SeedOrderFlowEntities =
  34. {
  35. "AdoS8OrderFlowOrder",
  36. "AdoS8OrderFlowStage",
  37. "AdoS8OrderFlowProcurementPivot",
  38. "AdoS8OrderFlowManufacturingProcess",
  39. };
  40. // ───────── ⑦ OrderArchiveKpi_UsesRealOrderAuthority ─────────
  41. /// <summary><c>/orders</c> 必须由 REAL 服务提供。</summary>
  42. [Fact]
  43. public void OrdersEndpoint_IsServedByRealArchiveService()
  44. {
  45. var code = ReadCode("Controllers/S8/AdoS8OrderFlowController.cs");
  46. var idx = code.IndexOf("[HttpGet(\"orders\")]", StringComparison.Ordinal);
  47. Assert.True(idx > 0, "未找到 /orders 路由");
  48. var action = code[idx..Math.Min(idx + 420, code.Length)];
  49. Assert.Contains("_realSvc.GetRealOrdersAsync", action);
  50. Assert.DoesNotContain("_svc.GetOrdersAsync", action);
  51. }
  52. // ───────── ⑥ OrderArchiveKpi_DoesNotReadSeedRepository ─────────
  53. /// <summary>REAL 档案服务不得依赖任何 SEED 订单链路仓储。</summary>
  54. [Fact]
  55. public void RealArchiveService_DoesNotReadSeedOrderFlowTables()
  56. {
  57. var code = ReadCode("Service/S8/OrderFlow/S8RealOrderArchiveService.cs");
  58. // 判据落在「仓储依赖」与「物理表名」上,不能裸搜实体名 ——
  59. // 本服务的返回类型叫 AdoS8OrderFlowOrderListItemDto / ...DetailDto,
  60. // 那是 DTO,不是 SEED 实体;裸子串会把一条真守卫变成噪音。
  61. foreach (var entity in SeedOrderFlowEntities)
  62. Assert.DoesNotContain($"SqlSugarRepository<{entity}>", code);
  63. Assert.DoesNotContain("ado_s8_order_flow", code);
  64. // 更强的结构性保证:本服务根本不注入任何仓储,只走 Ado 原生 SQL。
  65. Assert.DoesNotContain("SqlSugarRepository", code);
  66. }
  67. /// <summary>反射层同一条守卫:构造函数不得注入 SEED 仓储。改文案绕不过去。</summary>
  68. [Fact]
  69. public void RealArchiveService_TakesNoSeedRepository()
  70. {
  71. var svc = typeof(AdoS8OrderFlowController).Assembly
  72. .GetType("Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow.S8RealOrderArchiveService");
  73. Assert.NotNull(svc);
  74. var ctor = Assert.Single(svc!.GetConstructors());
  75. var injected = ctor.GetParameters()
  76. .Select(p => p.ParameterType.IsGenericType
  77. ? p.ParameterType.GetGenericArguments()[0].Name
  78. : p.ParameterType.Name)
  79. .ToList();
  80. foreach (var entity in SeedOrderFlowEntities)
  81. Assert.DoesNotContain(entity, injected);
  82. }
  83. /// <summary>
  84. /// 真实档案取数必须读数据中台标准层,且不得为了「去 seed」改成直连业务源表。
  85. /// </summary>
  86. [Fact]
  87. public void RealArchiveService_ReadsMdpAuthorityOnly()
  88. {
  89. var code = ReadCode("Service/S8/OrderFlow/S8RealOrderArchiveService.cs");
  90. Assert.Contains("mdp_std_so", code);
  91. foreach (var src in new[]
  92. {
  93. "PurOrdMaster", "PurOrdDetail", "WorkOrdMaster",
  94. "NbrMaster", "NbrDetail", "qms_", "crm_seorder", "mdp_stg_",
  95. })
  96. Assert.DoesNotContain(src, code);
  97. }
  98. // ───────── ⑨ TenantA_KpiCannotSeeTenantB ─────────
  99. /// <summary>档案取数的租户作用域一律服务端解析,不接受调用方传入。</summary>
  100. [Fact]
  101. public void RealArchive_ResolvesTenantServerSide()
  102. {
  103. var code = ReadCode("Service/S8/OrderFlow/S8RealOrderArchiveService.cs");
  104. Assert.Contains("AidopTenantScope.ResolveOrThrow", code);
  105. // 原生 SQL 用反引号写列名
  106. Assert.Contains("`tenant_id` = @TenantId", code);
  107. Assert.DoesNotContain("ClearFilter", code);
  108. }
  109. }