using Api.SeaHavenIndustries.Controllers; using Data.SeaHavenIndustries; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using SeaHaven.DataServices.Models; using SeaHaven.Services.Interfaces; using Xunit; namespace Api.SeaHavenIndustries.Tests; /// /// SH-374: GET api/WorkOrder/GetWorkorderById?id= must read the id from the query string, while /// GET api/WorkOrder/{id} keeps reading it from the route. Both must return the same work order, /// and an unknown id keeps the existing "ID not found!" response. /// public class WorkOrderGetByIdBindingTests { private const int ExistingId = 374; private const int UnknownId = 999_999; private static ControllerActionDescriptor ActionForTemplate(string relativeTemplate) { var services = new ServiceCollection(); services.AddLogging(); services.AddMvcCore().AddApplicationPart(typeof(WorkOrderController).Assembly); using var provider = services.BuildServiceProvider(); return provider.GetRequiredService().ActionDescriptors.Items .OfType() .Where(cad => cad.ControllerTypeInfo == typeof(WorkOrderController)) .Where(cad => cad.ActionConstraints! .OfType() .Any(c => c.HttpMethods.Contains("GET"))) .Single(cad => string.Equals( cad.AttributeRouteInfo?.Template?.Trim('/'), $"api/WorkOrder/{relativeTemplate}", StringComparison.Ordinal)); } [Theory] [InlineData("GetWorkorderById", "Query")] [InlineData("{id:int}", "Path")] public void Id_binds_from_the_source_its_route_provides(string relativeTemplate, string expectedSource) { var action = ActionForTemplate(relativeTemplate); var id = action.Parameters.Single(p => p.Name == "id"); id.BindingInfo.Should().NotBeNull(); id.BindingInfo!.BindingSource!.Id.Should().Be(expectedSource); } private static (WorkOrderController Controller, WorkOrderDetailReadModel Existing) NewController() { var existing = new WorkOrderDetailReadModel { Id = ExistingId, Title = "Leaking roof" }; var service = new Mock(); service.Setup(s => s.GetWorkOrderDetailAsync(ExistingId, It.IsAny())).ReturnsAsync(existing); service.Setup(s => s.GetWorkOrderDetailAsync(UnknownId, It.IsAny())) .ReturnsAsync((WorkOrderDetailReadModel?)null); var controller = new WorkOrderController( service.Object, Mock.Of(), Mock.Of>()) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; return (controller, existing); } [Fact] public async Task Existing_id_returns_the_same_work_order_through_both_routes() { var (controller, existing) = NewController(); var byQuery = await controller.GetWorkorderById(ExistingId); var byRoute = await controller.GetWorkorderByRouteId(ExistingId); byQuery.Should().BeOfType().Which.Value.Should().BeSameAs(existing); byRoute.Should().BeOfType().Which.Value.Should().BeSameAs(existing); } [Fact] public async Task Unknown_id_keeps_the_not_found_response_through_both_routes() { var (controller, _) = NewController(); foreach (var result in new[] { await controller.GetWorkorderById(UnknownId), await controller.GetWorkorderByRouteId(UnknownId), }) { var body = result.Should().BeOfType().Which.Value .Should().BeOfType().Subject; body.Status.Should().Be("Error"); body.Message.Should().Be("ID not found!"); } } }