| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- using System.Reflection;
- using Furion.DependencyInjection;
- using System.Runtime.CompilerServices;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Service.S8;
- using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S8;
- /// <summary>
- /// S8-STEP6E-CFG-WATCH-ORCHESTRATION-DEDUPE-UI-CERT-1:钉住建单 seam 的
- /// 「生产语义等价 + 纯委派」契约。
- ///
- /// <para>seam 的全部价值在于「认证时可替换、生产时透明」。一旦有人往正式实现里加逻辑,
- /// Watch 认证得到的就不再是生产行为——本文件就是防这个的。</para>
- /// </summary>
- public class S8ExceptionReportDispatcherTests
- {
- /// <summary>接口方法签名必须与被委派的真实方法逐字一致,否则 seam 就偷偷改了契约。</summary>
- [Fact]
- public void InterfaceSignature_MatchesRealService()
- {
- var iface = typeof(IS8ExceptionReportDispatcher).GetMethod("CreateFromHitAsync")!;
- var real = typeof(S8ManualReportService).GetMethod("CreateFromHitAsync")!;
- Assert.Equal(real.ReturnType, iface.ReturnType);
- Assert.Equal(
- real.GetParameters().Select(p => p.ParameterType).ToArray(),
- iface.GetParameters().Select(p => p.ParameterType).ToArray());
- Assert.Equal(
- new[] { typeof(long), typeof(long), typeof(S8RuleHit) },
- iface.GetParameters().Select(p => p.ParameterType).ToArray());
- }
- /// <summary>正式实现只能持有被委派的服务这一个依赖——多一个字段就说明它开始自己干活了。</summary>
- [Fact]
- public void ProductionImplementation_HasOnlyTheDelegateDependency()
- {
- var fields = typeof(S8ExceptionReportDispatcher)
- .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
- Assert.Single(fields);
- Assert.Equal(typeof(S8ManualReportService), fields[0].FieldType);
- }
- /// <summary>
- /// 真正的委派证据:用未初始化的 S8ManualReportService 构造 dispatcher 并调用,
- /// 抛出的异常栈里必须出现 <c>S8ManualReportService.CreateFromHitAsync</c>——
- /// 即调用确实**穿透到了真实服务**,而不是被 dispatcher 自己拦下或改写。
- /// (不连 DB:真实服务在触仓储时抛 NRE,这正是我们要的穿透信号。)
- /// </summary>
- [Fact]
- public void ProductionImplementation_DelegatesIntoRealService()
- {
- var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService));
- var dispatcher = new S8ExceptionReportDispatcher(svc);
- var ex = Record.ExceptionAsync(
- () => dispatcher.CreateFromHitAsync(1L, 2L, new S8RuleHit())).GetAwaiter().GetResult();
- Assert.NotNull(ex);
- var trace = ex!.ToString();
- Assert.Contains(nameof(S8ManualReportService), trace);
- Assert.Contains("CreateFromHitAsync", trace);
- }
- /// <summary>
- /// 参数必须原样透传——tenantId / factoryId 不得被 dispatcher 改写或吞掉。
- /// 用真实服务最外层的守卫(tenantId<=0 或 factoryId<=0 抛 S8BizException)作为探针:
- /// 若 dispatcher 篡改了入参,这个守卫就不会按预期触发。
- /// </summary>
- [Theory]
- [InlineData(0L, 2L)]
- [InlineData(1L, 0L)]
- [InlineData(-1L, -1L)]
- public void ProductionImplementation_PassesArgumentsThroughUnchanged(long tenantId, long factoryId)
- {
- var svc = (S8ManualReportService)RuntimeHelpers.GetUninitializedObject(typeof(S8ManualReportService));
- var dispatcher = new S8ExceptionReportDispatcher(svc);
- var ex = Record.ExceptionAsync(
- () => dispatcher.CreateFromHitAsync(tenantId, factoryId, new S8RuleHit())).GetAwaiter().GetResult();
- Assert.IsType<S8BizException>(ex);
- }
- /// <summary>seam 必须被 Furion 自动注册(ITransient),不需要改 Startup。</summary>
- [Fact]
- public void ProductionImplementation_IsAutoRegistrable()
- {
- Assert.True(typeof(IS8ExceptionReportDispatcher).IsAssignableFrom(typeof(S8ExceptionReportDispatcher)));
- Assert.True(typeof(ITransient).IsAssignableFrom(typeof(S8ExceptionReportDispatcher)));
- Assert.True(typeof(S8ExceptionReportDispatcher).IsSealed);
- }
- /// <summary>调度器必须消费 seam 接口而不是直接持有具体服务去自动建单。</summary>
- [Fact]
- public void Scheduler_DependsOnTheSeamInterface()
- {
- var ctor = typeof(S8WatchSchedulerService).GetConstructors().Single();
- Assert.Contains(ctor.GetParameters(), p => p.ParameterType == typeof(IS8ExceptionReportDispatcher));
- }
- }
|