S8ExceptionReportDispatcherTests.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. using System.Reflection;
  2. using Furion.DependencyInjection;
  3. using System.Runtime.CompilerServices;
  4. using Admin.NET.Plugin.AiDOP.Entity.S8;
  5. using Admin.NET.Plugin.AiDOP.Service.S8;
  6. using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  7. using Xunit;
  8. namespace Admin.NET.Plugin.AiDOP.Tests.S8;
  9. /// <summary>
  10. /// S8-STEP6E-CFG-WATCH-ORCHESTRATION-DEDUPE-UI-CERT-1:钉住建单 seam 的
  11. /// 「生产语义等价 + 纯委派」契约。
  12. ///
  13. /// <para>seam 的全部价值在于「认证时可替换、生产时透明」。一旦有人往正式实现里加逻辑,
  14. /// Watch 认证得到的就不再是生产行为——本文件就是防这个的。</para>
  15. /// </summary>
  16. public class S8ExceptionReportDispatcherTests
  17. {
  18. /// <summary>接口方法签名必须与被委派的真实方法逐字一致,否则 seam 就偷偷改了契约。</summary>
  19. [Fact]
  20. public void InterfaceSignature_MatchesRealService()
  21. {
  22. var iface = typeof(IS8ExceptionReportDispatcher).GetMethod("CreateFromHitAsync")!;
  23. var real = typeof(S8ManualReportService).GetMethod("CreateFromHitAsync")!;
  24. Assert.Equal(real.ReturnType, iface.ReturnType);
  25. Assert.Equal(
  26. real.GetParameters().Select(p => p.ParameterType).ToArray(),
  27. iface.GetParameters().Select(p => p.ParameterType).ToArray());
  28. Assert.Equal(
  29. new[] { typeof(long), typeof(long), typeof(S8RuleHit) },
  30. iface.GetParameters().Select(p => p.ParameterType).ToArray());
  31. }
  32. /// <summary>正式实现只能持有被委派的服务这一个依赖——多一个字段就说明它开始自己干活了。</summary>
  33. [Fact]
  34. public void ProductionImplementation_HasOnlyTheDelegateDependency()
  35. {
  36. var fields = typeof(S8ExceptionReportDispatcher)
  37. .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
  38. Assert.Single(fields);
  39. Assert.Equal(typeof(S8ManualReportService), fields[0].FieldType);
  40. }
  41. /// <summary>
  42. /// 真正的委派证据:用未初始化的 S8ManualReportService 构造 dispatcher 并调用,
  43. /// 抛出的异常栈里必须出现 <c>S8ManualReportService.CreateFromHitAsync</c>——
  44. /// 即调用确实**穿透到了真实服务**,而不是被 dispatcher 自己拦下或改写。
  45. /// (不连 DB:真实服务在触仓储时抛 NRE,这正是我们要的穿透信号。)
  46. /// </summary>
  47. [Fact]
  48. public void ProductionImplementation_DelegatesIntoRealService()
  49. {
  50. var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService));
  51. var dispatcher = new S8ExceptionReportDispatcher(svc);
  52. var ex = Record.ExceptionAsync(
  53. () => dispatcher.CreateFromHitAsync(1L, 2L, new S8RuleHit())).GetAwaiter().GetResult();
  54. Assert.NotNull(ex);
  55. var trace = ex!.ToString();
  56. Assert.Contains(nameof(S8ManualReportService), trace);
  57. Assert.Contains("CreateFromHitAsync", trace);
  58. }
  59. /// <summary>
  60. /// 参数必须原样透传——tenantId / factoryId 不得被 dispatcher 改写或吞掉。
  61. /// 用真实服务最外层的守卫(tenantId&lt;=0 或 factoryId&lt;=0 抛 S8BizException)作为探针:
  62. /// 若 dispatcher 篡改了入参,这个守卫就不会按预期触发。
  63. /// </summary>
  64. [Theory]
  65. [InlineData(0L, 2L)]
  66. [InlineData(1L, 0L)]
  67. [InlineData(-1L, -1L)]
  68. public void ProductionImplementation_PassesArgumentsThroughUnchanged(long tenantId, long factoryId)
  69. {
  70. var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService));
  71. var dispatcher = new S8ExceptionReportDispatcher(svc);
  72. var ex = Record.ExceptionAsync(
  73. () => dispatcher.CreateFromHitAsync(tenantId, factoryId, new S8RuleHit())).GetAwaiter().GetResult();
  74. Assert.IsType<S8BizException>(ex);
  75. }
  76. /// <summary>seam 必须被 Furion 自动注册(ITransient),不需要改 Startup。</summary>
  77. [Fact]
  78. public void ProductionImplementation_IsAutoRegistrable()
  79. {
  80. Assert.True(typeof(IS8ExceptionReportDispatcher).IsAssignableFrom(typeof(S8ExceptionReportDispatcher)));
  81. Assert.True(typeof(ITransient).IsAssignableFrom(typeof(S8ExceptionReportDispatcher)));
  82. Assert.True(typeof(S8ExceptionReportDispatcher).IsSealed);
  83. }
  84. /// <summary>调度器必须消费 seam 接口而不是直接持有具体服务去自动建单。</summary>
  85. [Fact]
  86. public void Scheduler_DependsOnTheSeamInterface()
  87. {
  88. var ctor = typeof(S8WatchSchedulerService).GetConstructors().Single();
  89. Assert.Contains(ctor.GetParameters(), p => p.ParameterType == typeof(IS8ExceptionReportDispatcher));
  90. }
  91. }