// 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));
}
///
/// 验证当容器里可见多个通知发布策略时,dispatcher 会拒绝在歧义状态下继续发布。
///
[Test]
public void PublishAsync_Should_Throw_When_Multiple_NotificationPublishers_Are_Registered()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler)))
.Returns([new RecordingNotificationHandler("only", [])]);
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
.Returns(
[
new TrackingNotificationPublisher(),
new TrackingNotificationPublisher()
]);
});
Assert.That(
async () => await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.EqualTo(
$"Multiple {typeof(INotificationPublisher).FullName} instances are registered. Remove duplicate notification publisher strategies before publishing notifications."));
}
///
/// 验证 dispatcher 在首次发布时解析通知发布器后,会复用同一实例并停止继续查询容器。
///
[Test]
public async Task PublishAsync_Should_Cache_Resolved_NotificationPublisher_After_First_Publish()
{
var firstPublisher = new TrackingNotificationPublisher();
var secondPublisher = new TrackingNotificationPublisher();
var notificationPublisherLookupCount = 0;
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler)))
.Returns([new RecordingNotificationHandler("only", [])]);
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationPublisher)))
.Returns(() =>
{
notificationPublisherLookupCount++;
return notificationPublisherLookupCount switch
{
1 => [firstPublisher],
2 => [secondPublisher],
_ => throw new AssertionException("Notification publisher should be resolved at most once.")
};
});
});
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
await runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).ConfigureAwait(false);
Assert.That(notificationPublisherLookupCount, Is.EqualTo(1));
Assert.That(firstPublisher.PublishCallCount, Is.EqualTo(2));
Assert.That(secondPublisher.PublishCallCount, Is.Zero);
}
///
/// 验证内置 `TaskWhenAll` 发布器会继续调度所有处理器,而不是沿用默认顺序发布器的失败即停语义。
///
[Test]
public async Task PublishAsync_Should_Invoke_All_Handlers_When_Using_TaskWhenAll_NotificationPublisher()
{
var trailingHandler = new RecordingNotificationHandler("second", []);
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler)))
.Returns(
[
new ThrowingNotificationHandler(),
trailingHandler
]);
},
new TaskWhenAllNotificationPublisher());
var publishTask = runtime.PublishAsync(new FakeCqrsContext(), new PublisherNotification()).AsTask();
try
{
await publishTask.ConfigureAwait(false);
}
catch (Exception)
{
// 并行发布会把处理器失败收敛到返回任务;这里仅消费异常并继续验证所有处理器都已被触发。
}
Assert.That(trailingHandler.Invoked, Is.True);
Assert.That(publishTask.Exception, Is.Not.Null);
}
///
/// 验证默认通知发布器在零处理器场景下会保持静默完成。
///
[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