// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Cqrs; using GFramework.Cqrs.Notification; using GFramework.Cqrs.Tests.Logging; namespace GFramework.Cqrs.Tests.Cqrs; /// /// 验证默认 CQRS runtime 的通知发布策略接缝。 /// [TestFixture] internal sealed class CqrsNotificationPublisherTests { /// /// 验证当调用方显式提供自定义通知发布器时,dispatcher 会按该发布器定义的顺序执行处理器。 /// [Test] public async Task PublishAsync_Should_Use_Custom_NotificationPublisher_When_Runtime_Is_Created_With_It() { var invocationOrder = new List(); var handlers = new object[] { new RecordingNotificationHandler("first", invocationOrder), new RecordingNotificationHandler("second", invocationOrder) }; var runtime = CreateRuntime( container => { container .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) .Returns(handlers); }, new ReverseOrderNotificationPublisher()); await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false); Assert.That(invocationOrder, Is.EqualTo(["second", "first"])); } /// /// 验证当容器在 runtime 创建前已显式注册自定义通知发布器时, /// `RegisterInfrastructure` 这条默认接线会复用该策略。 /// [Test] public async Task RegisterInfrastructure_Should_Use_PreRegistered_NotificationPublisher() { LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); var container = new MicrosoftDiContainer(); var publisher = new TrackingNotificationPublisher(); container.Register(publisher); container.Register>(new RecordingNotificationHandler("only", [])); CqrsTestRuntime.RegisterInfrastructure(container); container.Freeze(); var context = new ArchitectureContext(container); await context.PublishAsync(new PublisherNotification()).ConfigureAwait(false); Assert.That(publisher.WasCalled, Is.True); } /// /// 验证自定义通知发布器通过发布上下文回调执行处理器时,dispatcher 仍会在调用前注入当前架构上下文。 /// [Test] public async Task PublishAsync_Should_Prepare_Context_Before_Custom_Publisher_Invokes_Handler() { var handler = new ContextAwarePublisherTestHandler(); var architectureContext = new Mock(MockBehavior.Strict); var runtime = CreateRuntime( container => { container .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) .Returns([handler]); }, new PassthroughNotificationPublisher()); await runtime.PublishAsync(architectureContext.Object, new PublisherNotification()).ConfigureAwait(false); Assert.That(handler.ObservedContext, Is.SameAs(architectureContext.Object)); } /// /// 验证默认通知发布器在零处理器场景下会保持静默完成。 /// [Test] public void PublishAsync_Should_Complete_When_No_Handlers_Are_Registered() { var runtime = CreateRuntime( container => { container .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) .Returns(Array.Empty()); }); Assert.That( async () => await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false), Throws.Nothing); } /// /// 验证默认通知发布器会保持“首个异常立即中断后续处理器”的既有语义。 /// [Test] public void PublishAsync_Should_Stop_After_First_Handler_Exception_When_Using_Default_Publisher() { var trailingHandler = new RecordingNotificationHandler("second", []); var runtime = CreateRuntime( container => { container .Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler))) .Returns( [ new ThrowingNotificationHandler(), trailingHandler ]); }); Assert.That( async () => await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false), Throws.InvalidOperationException.With.Message.EqualTo("boom")); Assert.That(trailingHandler.Invoked, Is.False); } /// /// 创建一个只满足当前测试最小依赖面的 dispatcher runtime。 /// /// 对容器 mock 的额外配置。 /// 要注入的自定义通知发布器;若为 则使用默认发布器。 /// 默认 CQRS runtime。 private static GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime CreateRuntime( Action> configureContainer, INotificationPublisher? notificationPublisher = null) { var container = new Mock(MockBehavior.Strict); var logger = new TestLogger(nameof(CqrsNotificationPublisherTests), LogLevel.Debug); configureContainer(container); return CqrsRuntimeFactory.CreateRuntime(container.Object, logger, notificationPublisher); } /// /// 为当前测试提供最小的 CQRS 上下文标记。 /// private sealed class FakeCqrsContext : ICqrsContext { } /// /// 为通知发布器测试提供最小通知类型。 /// private sealed record PublisherNotification : INotification; /// /// 按传入顺序直接执行处理器的测试发布器。 /// private sealed class PassthroughNotificationPublisher : INotificationPublisher { /// /// 按当前处理器集合顺序执行所有处理器。 /// /// 通知类型。 /// 当前发布上下文。 /// 取消令牌。 /// 表示通知发布完成的值任务。 public async ValueTask PublishAsync( NotificationPublishContext context, CancellationToken cancellationToken = default) where TNotification : INotification { foreach (var handler in context.Handlers) { await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false); } } } /// /// 按逆序执行处理器的测试发布器,用于证明 dispatcher 已真正委托给自定义策略。 /// private sealed class ReverseOrderNotificationPublisher : INotificationPublisher { /// /// 按逆序执行当前发布上下文中的所有处理器。 /// /// 通知类型。 /// 当前发布上下文。 /// 取消令牌。 /// 表示通知发布完成的值任务。 public async ValueTask PublishAsync( NotificationPublishContext context, CancellationToken cancellationToken = default) where TNotification : INotification { for (var index = context.Handlers.Count - 1; index >= 0; index--) { await context.InvokeHandlerAsync(context.Handlers[index], cancellationToken).ConfigureAwait(false); } } } /// /// 仅记录自身是否被调用的测试发布器,用于验证默认接线是否已接管到自定义策略。 /// private sealed class TrackingNotificationPublisher : INotificationPublisher { /// /// 获取当前发布器是否至少执行过一次发布。 /// public bool WasCalled { get; private set; } /// /// 记录当前发布器已被调用,并继续按当前顺序执行所有处理器。 /// /// 通知类型。 /// 当前发布上下文。 /// 取消令牌。 /// 表示通知发布完成的值任务。 public async ValueTask PublishAsync( NotificationPublishContext context, CancellationToken cancellationToken = default) where TNotification : INotification { WasCalled = true; foreach (var handler in context.Handlers) { await context.InvokeHandlerAsync(handler, cancellationToken).ConfigureAwait(false); } } } /// /// 记录调用顺序的最小通知处理器。 /// private sealed class RecordingNotificationHandler : INotificationHandler { private readonly List _invocationOrder; private readonly string _name; /// /// 初始化一个记录调用顺序的测试处理器。 /// /// 当前处理器对应的名称。 /// 承载调用顺序的列表。 public RecordingNotificationHandler(string name, List invocationOrder) { ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(invocationOrder); _name = name; _invocationOrder = invocationOrder; } /// /// 获取当前处理器是否已被调用。 /// public bool Invoked { get; private set; } /// /// 把当前处理器名称追加到调用顺序列表。 /// /// 当前通知。 /// 取消令牌。 /// 已完成的值任务。 public ValueTask Handle(PublisherNotification notification, CancellationToken cancellationToken) { Invoked = true; _invocationOrder.Add(_name); return ValueTask.CompletedTask; } } /// /// 在被调用时主动抛出异常的测试处理器。 /// private sealed class ThrowingNotificationHandler : INotificationHandler { /// /// 抛出固定异常,验证默认发布器的失败即停语义。 /// /// 当前通知。 /// 取消令牌。 /// 不会成功返回。 /// 始终抛出,表示当前处理器失败。 public ValueTask Handle(PublisherNotification notification, CancellationToken cancellationToken) { throw new InvalidOperationException("boom"); } } /// /// 记录 dispatcher 是否在自定义发布器路径中完成上下文注入的测试处理器。 /// private sealed class ContextAwarePublisherTestHandler : CqrsContextAwareHandlerBase, INotificationHandler { /// /// 获取当前处理器在执行时观察到的架构上下文。 /// public IArchitectureContext? ObservedContext { get; private set; } /// /// 记录当前执行时观察到的架构上下文。 /// /// 当前通知。 /// 取消令牌。 /// 已完成的值任务。 public ValueTask Handle(PublisherNotification notification, CancellationToken cancellationToken) { ObservedContext = Context; return ValueTask.CompletedTask; } } }