MdpMonitorAdminGuardTests.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. using Xunit;
  2. namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform;
  3. /// <summary>
  4. /// 包 G 的越权守卫。测试项目不连库、没有认证夹具,所以用源文本扫描钉住三条不变式:
  5. /// 每个 admin 接口都拒绝非超管、都不走租户解析、都复用包 B 的指派写入侧。
  6. /// </summary>
  7. public class MdpMonitorAdminGuardTests
  8. {
  9. private static string MonitorSource()
  10. {
  11. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  12. while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  13. dir = dir.Parent;
  14. Assert.NotNull(dir);
  15. return File.ReadAllText(Path.Combine(
  16. dir!.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Order", "MdpMonitorService.cs"));
  17. }
  18. private static string RebuildSource()
  19. {
  20. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  21. while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  22. dir = dir.Parent;
  23. return File.ReadAllText(Path.Combine(
  24. dir!.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpRebuild", "ModuleRebuildService.cs"));
  25. }
  26. private static string WorkerSource()
  27. {
  28. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  29. while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  30. dir = dir.Parent;
  31. return File.ReadAllText(Path.Combine(
  32. dir!.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpRebuild", "ModuleRebuildWorker.cs"));
  33. }
  34. /// <summary>切出每个 admin 路由方法的方法体。</summary>
  35. private static IEnumerable<string> AdminMethodBodies(string source)
  36. {
  37. var marker = "mdp-monitor/admin/";
  38. var from = 0;
  39. while (true)
  40. {
  41. var at = source.IndexOf(marker, from, StringComparison.Ordinal);
  42. if (at < 0) yield break;
  43. var open = source.IndexOf('{', at);
  44. var depth = 0;
  45. var end = open;
  46. for (; end < source.Length; end++)
  47. {
  48. if (source[end] == '{') depth++;
  49. else if (source[end] == '}' && --depth == 0) break;
  50. }
  51. yield return source[open..(end + 1)];
  52. from = end + 1;
  53. }
  54. }
  55. [Fact]
  56. public void AdminEndpoints_AllCheckSuperAdmin()
  57. {
  58. var bodies = AdminMethodBodies(MonitorSource()).ToList();
  59. Assert.True(bodies.Count >= 6, $"admin 接口数量不符:{bodies.Count}");
  60. foreach (var body in bodies)
  61. Assert.Contains("RequireSuperAdmin()", body);
  62. }
  63. [Fact]
  64. public void AdminEndpoints_DoNotCallResolveOrThrow()
  65. {
  66. foreach (var body in AdminMethodBodies(MonitorSource()))
  67. Assert.DoesNotContain("ResolveOrThrow", body);
  68. }
  69. [Fact]
  70. public void LegacyEndpoints_StillCallResolveOrThrow()
  71. {
  72. var source = MonitorSource();
  73. foreach (var route in new[]
  74. {
  75. "mdp-monitor/outbound-gate", "mdp-monitor/ship-recon", "mdp-monitor/latest",
  76. "mdp-monitor/list", "mdp-monitor/detail", "mdp-monitor/lineage"
  77. })
  78. {
  79. var at = source.IndexOf($"\"{route}", StringComparison.Ordinal);
  80. Assert.True(at >= 0, $"原有路由丢失:{route}");
  81. var window = source[at..Math.Min(source.Length, at + 600)];
  82. Assert.Contains("ResolveOrThrow", window);
  83. }
  84. }
  85. [Fact]
  86. public void BuildMdpRunLogTenantWhere_Unchanged()
  87. {
  88. Assert.Equal("(tenant_id = @TenantId OR tenant_id = 0)",
  89. Admin.NET.Plugin.AiDOP.Order.MdpMonitorService.BuildMdpRunLogTenantWhere(9));
  90. Assert.Equal("tenant_id = 0",
  91. Admin.NET.Plugin.AiDOP.Order.MdpMonitorService.BuildMdpRunLogTenantWhere(0));
  92. }
  93. [Fact]
  94. public void CancelCheck_IsInProgressCallback_NotWorker()
  95. {
  96. Assert.Contains("IsCancelRequestedAsync", RebuildSource());
  97. Assert.DoesNotContain("IsCancelRequestedAsync", WorkerSource());
  98. Assert.DoesNotContain("cancel_requested", WorkerSource());
  99. }
  100. [Fact]
  101. public void CancelledCatch_PrecedesGenericCatch()
  102. {
  103. var source = RebuildSource();
  104. var specific = source.IndexOf("catch (ModuleRebuildCancelledException)", StringComparison.Ordinal);
  105. Assert.True(specific >= 0, "缺少取消异常的 catch");
  106. var generic = source.IndexOf("catch (Exception ex)", specific, StringComparison.Ordinal);
  107. Assert.True(generic > specific, "取消异常的 catch 必须早于通用 catch,否则会被吞成 FAILED");
  108. }
  109. [Fact]
  110. public void AssignRunner_ReusesPackageBStore()
  111. {
  112. var source = MonitorSource();
  113. Assert.Contains("_etlInstances.AssignRunnerAsync", source);
  114. Assert.DoesNotContain("SET is_runner", source);
  115. Assert.DoesNotContain("IsRunner = true", source);
  116. }
  117. [Fact]
  118. public void ModuleCatches_RethrowAdminCancelBeforeMarkingFailed()
  119. {
  120. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  121. while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  122. dir = dir.Parent;
  123. Assert.NotNull(dir);
  124. string[] files =
  125. [
  126. Path.Combine(dir!.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Order", "S1MdpSyncTransformService.cs"),
  127. Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Production", "S2MdpSyncTransformService.cs"),
  128. Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Supply", "S3MdpSyncTransformService.cs"),
  129. Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "ProcurementExecution", "S4MdpSyncTransformService.cs"),
  130. Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "S5MdpSyncTransformService.cs"),
  131. Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Manufacturing", "S6MdpSyncTransformService.cs"),
  132. Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "FinishedWarehouse", "S7MdpSyncTransformService.cs"),
  133. ];
  134. foreach (var file in files)
  135. {
  136. var source = File.ReadAllText(file);
  137. var failed = source.IndexOf("MarkTransformRunFailedAsync", StringComparison.Ordinal);
  138. var rethrow = source.IndexOf("ex is ModuleRebuildCancelledException", StringComparison.Ordinal);
  139. Assert.True(rethrow >= 0 && rethrow < failed, Path.GetFileName(file) + " 必须先把管理员取消抛回去,不能先写成 FAILED");
  140. }
  141. }
  142. }