// Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Geekeey.Request.Dispatcher.Tests; internal sealed class ScalarDispatcherTests { [Test] public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(OpenScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new OpenScalarRequest { Data = "Hello" }; var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("Hello-Handled"); } [Test] public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler_that_has_constraints() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(ConstrainedScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new ConstrainedScalarRequest { Value = 123 }; var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("123-Constrained"); } [Test] public async Task I_can_see_it_fail_if_no_handler_is_found_even_with_an_open_generic_available() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(OpenScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new UnhandledScalarRequest(); await Assert.ThrowsAsync(async () => await dispatcher.DispatchAsync(request)); } [Test] public async Task I_can_dispatch_a_request_async_with_an_inherited_request() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(OpenScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); // InheritedScalarRequest : OpenScalarRequest. // There is no Handler but there is OpenScalarHandler where TRequest : OpenScalarRequest. // It should be able to handle InheritedScalarRequest. var request = new InheritedScalarRequest { Data = "Sub" }; var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("Sub-Handled"); } [Test] public async Task I_can_dispatch_a_request_async_with_an_inherited_handler() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(DerivedScalarHandler))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new DerivedScalarRequest { Value = 42 }; var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("Derived: 42"); } [Test] public async Task I_can_dispatch_a_request_async_with_an_interface_inherited_request() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(InterfaceInheritedScalarHandler))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new InterfaceInheritedScalarRequest { Name = "InterfaceTest" }; var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("InterfaceTest-InterfaceHandled"); } [Test] public async Task I_can_dispatch_a_request_async_with_deep_inheritance_in_the_request() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(OpenScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new DeepDerivedScalarRequest { Data = "Deep", DeepValue = 99 }; var result = await dispatcher.DispatchAsync(request); // OpenScalarHandler where TRequest : OpenScalarRequest should handle this await Assert.That(result).IsEquivalentTo("Deep-Handled"); } [Test] public async Task I_can_dispatch_a_request_async_with_an_interface_constrained_handler() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(InterfaceInheritedScalarHandler)) .Add(typeof(InterfaceConstrainedScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new InterfaceInheritedScalarRequest { Name = "Constrained" }; var result = await dispatcher.DispatchAsync(request); // Both InterfaceInheritedScalarHandler and InterfaceConstrainedScalarHandler could match. // InterfaceInheritedScalarHandler is a concrete match for InterfaceInheritedScalarRequest. // InterfaceConstrainedScalarHandler is an open generic match. // Currently Dispatcher.SendAsync checks concrete handlers first. await Assert.That(result).IsEquivalentTo("Constrained-InterfaceHandled"); } [Test] public async Task I_can_dispatch_a_request_async_with_an_interface_only_match() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(InterfaceConstrainedScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new AnotherNamedScalarRequest { Name = "InterfaceOnly" }; var result = await dispatcher.DispatchAsync(request); // No concrete handler for AnotherNamedScalarRequest, but InterfaceConstrainedScalarHandler where T : INamedScalarRequest matches. await Assert.That(result).IsEquivalentTo("InterfaceOnly-ConstrainedByInterface"); } [Test] public async Task I_can_dispatch_a_request_async_with_a_nested_generic_request() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(WrapperScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new WrapperScalarRequest { Item = 42 }; var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("Handled-42"); } [Test] public async Task I_can_handle_multiple_interface_implementations() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(MultiInterfaceScalarHandler))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new MultiInterfaceScalarRequest(); var result1 = await dispatcher.DispatchAsync(request); var result2 = await dispatcher.DispatchAsync(request); await Assert.That(result1).IsEqualTo(1); await Assert.That(result2).IsEquivalentTo("One"); } [Test] public async Task I_can_see_it_fail_if_there_are_ambiguous_handle_methods() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(AmbiguousScalarHandler))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new AmbiguousScalarRequest(); var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("Interface-Handled"); } [Test] public async Task I_can_dispatch_a_request_async_with_a_generic_interface_explicit_implementation() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(ExplicitGenericScalarHandler<>))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new ExplicitGenericScalarRequest { Value = "Explicit" }; var result = await dispatcher.DispatchAsync(request); await Assert.That(result).IsEquivalentTo("Explicit-ExplicitHandled"); } [Test] public async Task I_can_see_it_throw_the_original_exception() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(FailingScalarHandler))); var provider = sc.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); var request = new FailingScalarRequest(); var ex = await Assert.That(async () => await dispatcher.DispatchAsync(request)).Throws(); using (Assert.Multiple()) { await Assert.That(ex?.Message).IsEquivalentTo("Handler failed"); // Assert that the stack trace contains the handler's method name, // which proves the exception was rethrown while preserving its origin. await Assert.That(ex?.StackTrace).Contains(nameof(FailingScalarHandler.HandleAsync)); } } [Test] public async Task I_can_see_it_throw_if_dispatcher_options_are_modified_after_build() { var sc = new ServiceCollection(); sc.AddRequestDispatcher(builder => builder .Add(typeof(FailingScalarHandler))); var provider = sc.BuildServiceProvider(); var options = provider.GetRequiredService>().Value; options.GetRequestBehaviors>(default!); await Assert.That(() => options.Inspect([])).Throws(); } } // Moved to _fixtures