// Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Tests.Logging; namespace GFramework.Cqrs.Tests.Cqrs; /// /// 验证 CQRS 程序集注册协调器在程序集键去重层面的可观察行为。 /// [TestFixture] internal sealed class CqrsRegistrationServiceTests { /// /// 验证同一次调用内出现重复程序集键时,底层注册器只会接收到一次注册请求。 /// [Test] public void RegisterHandlers_Should_Register_Duplicate_Assembly_Key_Only_Once_Per_Call() { var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); var registrar = new Mock(MockBehavior.Strict); var duplicateAssemblyA = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0"); var duplicateAssemblyB = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0"); var expectedAssembly = duplicateAssemblyA.Object; IEnumerable? registeredAssemblies = null; registrar .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); service.RegisterHandlers([duplicateAssemblyA.Object, duplicateAssemblyB.Object]); registrar.Verify( static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), Times.Once); Assert.Multiple(() => { Assert.That(registeredAssemblies, Is.Not.Null); Assert.That(registeredAssemblies, Is.EqualTo([expectedAssembly])); Assert.That(logger.Logs, Has.Count.EqualTo(0)); }); } /// /// 验证跨两次调用重复程序集键时,协调器会跳过重复注册并写入 debug 日志。 /// [Test] public void RegisterHandlers_Should_Skip_Already_Registered_Assembly_Key_Across_Calls_And_Log_Debug_Message() { var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); var registrar = new Mock(MockBehavior.Strict); var firstAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"); var secondAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"); IEnumerable? registeredAssemblies = null; registrar .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); service.RegisterHandlers([firstAssembly.Object]); service.RegisterHandlers([secondAssembly.Object]); registrar.Verify( static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), Times.Once); Assert.Multiple(() => { Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object])); var debugMessages = logger.Logs .Where(static log => log.Level == LogLevel.Debug) .Select(static log => log.Message) .ToArray(); Assert.That(debugMessages, Has.Length.EqualTo(1)); Assert.That( debugMessages[0], Does.Contain("Skipping CQRS handler registration for assembly")); Assert.That( debugMessages[0], Does.Contain("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0")); Assert.That(debugMessages[0], Does.Contain("already registered")); }); } /// /// 验证当 缺失时,协调器会退化到 作为稳定程序集键。 /// [Test] public void RegisterHandlers_Should_Fallback_To_Simple_Name_When_Full_Name_Is_Missing() { var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); var registrar = new Mock(MockBehavior.Strict); var firstAssembly = CreateAssembly( assemblyFullName: null, assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback", assemblyDisplayName: "DisplayName-A"); var secondAssembly = CreateAssembly( assemblyFullName: null, assemblySimpleName: "GFramework.Cqrs.Tests.SimpleNameFallback", assemblyDisplayName: "DisplayName-B"); IEnumerable? registeredAssemblies = null; registrar .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); service.RegisterHandlers([firstAssembly.Object]); service.RegisterHandlers([secondAssembly.Object]); registrar.Verify( static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), Times.Once); Assert.Multiple(() => { Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object])); var debugMessages = logger.Logs .Where(static log => log.Level == LogLevel.Debug) .Select(static log => log.Message) .ToArray(); Assert.That(debugMessages, Has.Length.EqualTo(1)); Assert.That(debugMessages[0], Does.Contain("GFramework.Cqrs.Tests.SimpleNameFallback")); Assert.That(debugMessages[0], Does.Not.Contain("DisplayName-B")); }); } /// /// 验证当 均缺失时, /// 协调器会退化到 结果作为稳定程序集键。 /// [Test] public void RegisterHandlers_Should_Fallback_To_ToString_When_Full_Name_And_Simple_Name_Are_Missing() { var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug); var registrar = new Mock(MockBehavior.Strict); const string assemblyDisplayName = "GFramework.Cqrs.Tests.ToStringFallback"; var firstAssembly = CreateAssembly( assemblyFullName: null, assemblySimpleName: null, assemblyDisplayName: assemblyDisplayName); var secondAssembly = CreateAssembly( assemblyFullName: null, assemblySimpleName: null, assemblyDisplayName: assemblyDisplayName); IEnumerable? registeredAssemblies = null; registrar .Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>())) .Callback>(assemblies => registeredAssemblies = assemblies.ToArray()); var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger); service.RegisterHandlers([firstAssembly.Object]); service.RegisterHandlers([secondAssembly.Object]); registrar.Verify( static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny>()), Times.Once); Assert.Multiple(() => { Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object])); var debugMessages = logger.Logs .Where(static log => log.Level == LogLevel.Debug) .Select(static log => log.Message) .ToArray(); Assert.That(debugMessages, Has.Length.EqualTo(1)); Assert.That(debugMessages[0], Does.Contain(assemblyDisplayName)); Assert.That(debugMessages[0], Does.Contain("already registered")); }); } /// /// 创建一个带稳定程序集键的程序集 mock,用于模拟不同 实例表示同一程序集的场景。 /// /// 要返回的程序集完整名称。 /// 配置好完整名称的程序集 mock。 private static Mock CreateAssembly(string assemblyFullName) { return CreateAssembly(assemblyFullName, assemblySimpleName: null, assemblyDisplayName: assemblyFullName); } /// /// 创建一个可配置程序集元数据退化路径的程序集 mock,用于验证稳定程序集键的回退顺序。 /// /// 要返回的程序集完整名称;为 时模拟缺失完整名称。 /// 要返回的程序集简单名称;为 时模拟缺失简单名称。 /// 当需要退化到 时返回的显示名称。 /// 配置好程序集元数据的程序集 mock。 private static Mock CreateAssembly(string? assemblyFullName, string? assemblySimpleName, string assemblyDisplayName) { var assembly = new Mock(); var assemblyName = new AssemblyName(); assembly .SetupGet(static currentAssembly => currentAssembly.FullName) .Returns(assemblyFullName); assemblyName.Name = assemblySimpleName; assembly .Setup(static currentAssembly => currentAssembly.GetName()) .Returns(assemblyName); assembly .Setup(static currentAssembly => currentAssembly.ToString()) .Returns(assemblyDisplayName); return assembly; } }