using System.Text; using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Router.Common.Frames; using StellaOps.Router.Common.Models; using Xunit; namespace StellaOps.Microservice.Tests; public sealed class RequestDispatcherTests { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; [Fact] public async Task DispatchAsync_WhenEndpointNotFound_Returns404() { var registry = new EndpointRegistry(); var services = new ServiceCollection(); using var provider = services.BuildServiceProvider(); var dispatcher = new RequestDispatcher( registry, provider, CreateLogger()); var response = await dispatcher.DispatchAsync( new RequestFrame { RequestId = "req-404", Method = "GET", Path = "/missing", Payload = ReadOnlyMemory.Empty }, CancellationToken.None); response.StatusCode.Should().Be(404); Encoding.UTF8.GetString(response.Payload.Span).Should().Be("Not Found"); } [Fact] public async Task DispatchAsync_WhenBodyEmpty_BindsFromPathAndQueryParameters() { var registry = new EndpointRegistry(); registry.Register(new EndpointDescriptor { ServiceName = "inventory", Version = "1.0.0", Method = "GET", Path = "/items/{id}", HandlerType = typeof(GetItemHandler) }); var services = new ServiceCollection(); services.AddTransient(); using var provider = services.BuildServiceProvider(); var dispatcher = new RequestDispatcher( registry, provider, CreateLogger(), jsonOptions: JsonOptions); var response = await dispatcher.DispatchAsync( new RequestFrame { RequestId = "req-params", Method = "GET", Path = "/items/123?filter=active", Payload = ReadOnlyMemory.Empty }, CancellationToken.None); response.StatusCode.Should().Be(200); response.Headers.Should().ContainKey("Content-Type"); var dto = JsonSerializer.Deserialize(response.Payload.Span, JsonOptions); dto.Should().NotBeNull(); dto!.Id.Should().Be(123); dto.Filter.Should().Be("active"); } [Fact] public async Task DispatchAsync_WhenBodyPresent_PathAndQueryOverrideJsonProperties() { var registry = new EndpointRegistry(); registry.Register(new EndpointDescriptor { ServiceName = "inventory", Version = "1.0.0", Method = "POST", Path = "/items/{id}", HandlerType = typeof(GetItemHandler) }); var services = new ServiceCollection(); services.AddTransient(); using var provider = services.BuildServiceProvider(); var dispatcher = new RequestDispatcher( registry, provider, CreateLogger(), jsonOptions: JsonOptions); var body = JsonSerializer.SerializeToUtf8Bytes( new GetItemRequest { Id = 999, Filter = "fromBody" }, JsonOptions); var response = await dispatcher.DispatchAsync( new RequestFrame { RequestId = "req-body", Method = "POST", Path = "/items/123?filter=active", Payload = body }, CancellationToken.None); response.StatusCode.Should().Be(200); var dto = JsonSerializer.Deserialize(response.Payload.Span, JsonOptions); dto.Should().NotBeNull(); dto!.Id.Should().Be(123); dto.Filter.Should().Be("active"); } private static ILogger CreateLogger() { var factory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None)); return factory.CreateLogger(); } private sealed class GetItemRequest { public int Id { get; set; } public string? Filter { get; set; } } private sealed class GetItemResponse { public int Id { get; set; } public string? Filter { get; set; } } private sealed class GetItemHandler : IStellaEndpoint { public Task HandleAsync(GetItemRequest request, CancellationToken cancellationToken) { return Task.FromResult(new GetItemResponse { Id = request.Id, Filter = request.Filter }); } } }