S8ExceptionReportDispatcherTests.cs 4.6 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. // S8-TENANT-ONLY-BATCH6:factoryId 形参已从建单契约移除,seam 必须同步收敛。
  29. Assert.Equal(
  30. new[] { typeof(long), typeof(S8RuleHit) },
  31. iface.GetParameters().Select(p => p.ParameterType).ToArray());
  32. }
  33. /// <summary>正式实现只能持有被委派的服务这一个依赖——多一个字段就说明它开始自己干活了。</summary>
  34. [Fact]
  35. public void ProductionImplementation_HasOnlyTheDelegateDependency()
  36. {
  37. var fields = typeof(S8ExceptionReportDispatcher)
  38. .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
  39. Assert.Single(fields);
  40. Assert.Equal(typeof(S8ManualReportService), fields[0].FieldType);
  41. }
  42. /// <summary>
  43. /// 真正的委派证据:用未初始化的 S8ManualReportService 构造 dispatcher 并调用,
  44. /// 抛出的异常栈里必须出现 <c>S8ManualReportService.CreateFromHitAsync</c>——
  45. /// 即调用确实**穿透到了真实服务**,而不是被 dispatcher 自己拦下或改写。
  46. /// (不连 DB:真实服务在触仓储时抛 NRE,这正是我们要的穿透信号。)
  47. /// </summary>
  48. [Fact]
  49. public void ProductionImplementation_DelegatesIntoRealService()
  50. {
  51. var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService));
  52. var dispatcher = new S8ExceptionReportDispatcher(svc);
  53. var ex = Record.ExceptionAsync(
  54. () => dispatcher.CreateFromHitAsync(1L, new S8RuleHit())).GetAwaiter().GetResult();
  55. Assert.NotNull(ex);
  56. var trace = ex!.ToString();
  57. Assert.Contains(nameof(S8ManualReportService), trace);
  58. Assert.Contains("CreateFromHitAsync", trace);
  59. }
  60. /// <summary>
  61. /// 参数必须原样透传——tenantId 不得被 dispatcher 改写或吞掉。
  62. /// 用真实服务最外层的守卫(tenantId&lt;=0 抛 S8BizException)作为探针:
  63. /// 若 dispatcher 篡改了入参,这个守卫就不会按预期触发。
  64. /// </summary>
  65. [Theory]
  66. [InlineData(0L)]
  67. [InlineData(-1L)]
  68. public void ProductionImplementation_PassesArgumentsThroughUnchanged(long tenantId)
  69. {
  70. var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService));
  71. var dispatcher = new S8ExceptionReportDispatcher(svc);
  72. var ex = Record.ExceptionAsync(
  73. () => dispatcher.CreateFromHitAsync(tenantId, 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. }