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.Interfaces; using System.Security.Claims; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class UpliftControllerTests { private static UpliftController NewController(Mock service, params string[] roles) { var controller = new UpliftController(service.Object, Mock.Of>()); var claims = new List { new(ClaimTypes.NameIdentifier, "42") }; 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 List_ReturnsPagedResult() { var service = new Mock(); var dto = new UpliftListResultDTO { Total = 1, Page = 1, PageSize = 25, Items = new[] { new UpliftListItemDTO { Id = 5, Status = "Pending" } } }; service.Setup(x => x.ListAsync(It.IsAny(), "Pending", null, 1, 25, It.IsAny())) .ReturnsAsync(dto); var controller = NewController(service); var result = await controller.List(); var ok = result.Should().BeOfType().Subject; var envelope = ok.Value.Should().BeOfType().Subject; envelope.Status.Should().Be("Success"); } [Fact] public async Task Approve_NotFound_Returns404() { var service = new Mock(); service.Setup(x => x.ApproveAsync(It.IsAny(), 99, null, It.IsAny())) .ThrowsAsync(new KeyNotFoundException("not found")); var controller = NewController(service); var result = await controller.Approve(99, null); var nf = result.Should().BeOfType().Subject; nf.Value.Should().BeOfType(); } [Fact] public async Task Approve_Forbidden_ReturnsSanitized403AndLogsInternally() { var service = new Mock(); service.Setup(x => x.ApproveAsync(It.IsAny(), 5, null, It.IsAny())) .ThrowsAsync(new UpliftForbiddenException("SECRET-tier-restriction-detail")); var logger = new Mock>(); logger.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); var controller = new UpliftController(service.Object, logger.Object); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "42") }, "Test")) } }; var result = await controller.Approve(5, null); var status = result.Should().BeOfType().Subject; status.StatusCode.Should().Be(403); var response = status.Value.Should().BeOfType().Subject; response.Message.Should().NotContain("SECRET"); response.Message.Should().Contain("reference"); logger.Verify( x => x.Log( LogLevel.Error, It.IsAny(), It.IsAny(), It.Is(ex => ex.Message.Contains("SECRET")), It.IsAny>()), Times.Once); } [Fact] public async Task Approve_ForbiddenTier_Returns403() { var service = new Mock(); service.Setup(x => x.ApproveAsync(It.IsAny(), 5, null, It.IsAny())) .ThrowsAsync(new UpliftForbiddenException("requires Tier 2")); var controller = NewController(service); var result = await controller.Approve(5, null); var status = result.Should().BeOfType().Subject; status.StatusCode.Should().Be(403); } [Fact] public async Task Approve_AlreadyDecided_Returns400() { var service = new Mock(); service.Setup(x => x.ApproveAsync(It.IsAny(), 5, null, It.IsAny())) .ThrowsAsync(new InvalidOperationException("already decided")); var controller = NewController(service); var result = await controller.Approve(5, null); result.Should().BeOfType(); } [Fact] public async Task Approve_Success_ReturnsApprovedResult() { var service = new Mock(); var approved = new UpliftApproveResultDTO { Id = 5, Status = "Approved", NTEAmount = 750m }; service.Setup(x => x.ApproveAsync(It.IsAny(), 5, "ok", It.IsAny())) .ReturnsAsync(approved); var controller = NewController(service); var result = await controller.Approve(5, new UpliftController.DecisionRequest { Note = "ok" }); var ok = result.Should().BeOfType().Subject; var envelope = ok.Value.Should().BeOfType().Subject; var approveData = envelope.Data as UpliftApproveResultDTO; approveData.Should().Be(approved); } [Fact] public async Task Deny_WithoutNote_Returns400() { var service = new Mock(); service.Setup(x => x.DenyAsync(It.IsAny(), 5, null, It.IsAny())) .ThrowsAsync(new InvalidOperationException("A note is required when denying")); var controller = NewController(service); var result = await controller.Deny(5, null); result.Should().BeOfType(); } [Fact] public async Task Deny_Success_ReturnsDeniedResult() { var service = new Mock(); var denied = new UpliftDenyResultDTO { Id = 5, Status = "Denied" }; service.Setup(x => x.DenyAsync(It.IsAny(), 5, "too high", It.IsAny())) .ReturnsAsync(denied); var controller = NewController(service); var result = await controller.Deny(5, new UpliftController.DecisionRequest { Note = "too high" }); var ok = result.Should().BeOfType().Subject; service.Verify(x => x.DenyAsync(It.IsAny(), 5, "too high", It.IsAny()), Times.Once); } [Fact] public void CanApprove_DelegatesToService() { var service = new Mock(); service.Setup(x => x.CanApprove(It.IsAny(), 2)) .Returns(true); var controller = NewController(service, "Manager"); var result = controller.CanApprove(2); var ok = result.Should().BeOfType().Subject; service.Verify(x => x.CanApprove(It.Is(p => p.IsInRole("Manager")), 2), Times.Once); } [Fact] public async Task ListForDispatch_ReturnsItems() { var service = new Mock(); var items = new[] { new UpliftForDispatchDTO { Id = 1, DispatchId = 10 } }; service.Setup(x => x.ListForDispatchAsync(It.IsAny(), 10, It.IsAny())) .ReturnsAsync(items); var controller = NewController(service); var result = await controller.ListForDispatch(10); var ok = result.Should().BeOfType().Subject; var envelope = ok.Value.Should().BeOfType().Subject; envelope.Status.Should().Be("Success"); } }