using Api.SeaHavenIndustries.Controllers; using Data.SeaHavenIndustries; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Moq; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Interfaces; using System.Security.Claims; using Xunit; namespace Api.SeaHavenIndustries.Tests; public sealed class WorkOrderUpliftControllerTests { private static WorkOrderDetailController NewController( Mock upliftService, params string[] roles) { var controller = new WorkOrderDetailController( Mock.Of(), Mock.Of(), upliftService.Object, Mock.Of>()); var claims = new List { new(ClaimTypes.NameIdentifier, "dispatcher-1") }; claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test")) } }; return controller; } [Fact] public async Task ListUplifts_ReturnsItemsEnvelope() { var service = new Mock(); service.Setup(x => x.ListAsync(7, It.IsAny(), It.IsAny())) .ReturnsAsync(new List { new() { Id = 11, Status = "pending", Amount = 500m, Notes = "Need parts" } }); var controller = NewController(service); var result = await controller.ListUplifts(7, CancellationToken.None); var ok = result.Should().BeOfType().Subject; var envelope = ok.Value.Should().BeOfType().Subject; var list = Assert.IsType(envelope.Data); Assert.Single(list.Items); Assert.Equal(11, list.Items[0].Id); } [Fact] public async Task CreateUplift_ReturnsCreatedItem() { var service = new Mock(); service.Setup(x => x.CreateAsync( 7, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new WorkOrderUpliftDto { Id = 12, Status = "pending", Amount = 750m }); var controller = NewController(service); var result = await controller.CreateUplift( 7, new CreateWorkOrderUpliftRequestDto { Amount = 750m, Notes = "Extra labor" }, CancellationToken.None); var ok = result.Should().BeOfType().Subject; var envelope = ok.Value.Should().BeOfType().Subject; var created = Assert.IsType(envelope.Data); Assert.Equal(12, created.Id); } [Fact] public async Task CreateUplift_RequestPermissionDenied_ReturnsExact403Message() { var service = new Mock(); service.Setup(x => x.CreateAsync( 7, It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new UpliftForbiddenException(UpliftForbiddenException.RequestUpliftsDeniedMessage)); var controller = NewController(service, "Dispatcher"); var result = await controller.CreateUplift( 7, new CreateWorkOrderUpliftRequestDto { Amount = 750m }, CancellationToken.None); var forbidden = result.Should().BeOfType().Subject; forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden); var response = forbidden.Value.Should().BeOfType().Subject; response.Message.Should().Be(UpliftForbiddenException.RequestUpliftsDeniedMessage); } [Fact] public async Task CreateUplift_UnrelatedForbiddenException_ReturnsSanitized403() { var service = new Mock(); service.Setup(x => x.CreateAsync( 7, It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new UpliftForbiddenException("SECRET-internal-tier-detail")); var controller = NewController(service, "Dispatcher"); var result = await controller.CreateUplift( 7, new CreateWorkOrderUpliftRequestDto { Amount = 750m }, CancellationToken.None); var forbidden = result.Should().BeOfType().Subject; forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden); var response = forbidden.Value.Should().BeOfType().Subject; response.Message.Should().NotContain("SECRET-internal-tier-detail"); response.Message.Should().Contain("reference"); } [Fact] public async Task CreateUplift_ConcurrentActiveRequest_ReturnsConflict() { var service = new Mock(); service.Setup(x => x.CreateAsync( 7, It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new UpliftConflictException()); var controller = NewController(service); var result = await controller.CreateUplift( 7, new CreateWorkOrderUpliftRequestDto { Amount = 750m, Notes = "Extra labor" }, CancellationToken.None); var conflict = result.Should().BeOfType().Subject; conflict.StatusCode.Should().Be(StatusCodes.Status409Conflict); conflict.Value.Should().NotBeNull(); } [Fact] public async Task CancelUplift_NotFound_Returns404() { var service = new Mock(); service.Setup(x => x.CancelAsync(7, 99, It.IsAny(), It.IsAny())) .ThrowsAsync(new KeyNotFoundException("Uplift request not found")); var controller = NewController(service); var result = await controller.CancelUplift(7, 99, CancellationToken.None); result.Should().BeOfType(); } [Fact] public async Task RevokeUplift_Forbidden_Returns403() { var service = new Mock(); service.Setup(x => x.RevokeAsync( 7, 99, It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new UpliftForbiddenException("Admin role is required")); var controller = NewController(service, "Dispatcher"); var result = await controller.RevokeUplift( 7, 99, new RevokeWorkOrderUpliftRequestDto { Reason = "Too high" }, CancellationToken.None); result.Should().BeOfType().Which.StatusCode.Should().Be(StatusCodes.Status403Forbidden); } [Fact] public async Task CancelUplift_TerminalWorkOrder_Returns400() { var service = new Mock(); service.Setup(x => x.CancelAsync(7, 99, It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("Cannot change uplifts on a 'Completed' work order")); var controller = NewController(service); var result = await controller.CancelUplift(7, 99, CancellationToken.None); result.Should().BeOfType(); } [Fact] public async Task RevokeUplift_TerminalWorkOrder_Returns400() { var service = new Mock(); service.Setup(x => x.RevokeAsync( 7, 99, It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("Cannot change uplifts on a 'Canceled' work order")); var controller = NewController(service, "Admin"); var result = await controller.RevokeUplift( 7, 99, new RevokeWorkOrderUpliftRequestDto { Reason = "Completed" }, CancellationToken.None); result.Should().BeOfType(); } }