shoc-backend/Api.SeaHavenIndustries.Tests/WorkOrderUpliftControllerTests.cs
Alexandre Brandizzi beb091fcc8 Merge remote-tracking branch 'origin/main' into lane/sh-327
# Conflicts:
#	Api.SeaHavenIndustries.Tests/WorkOrderUpliftControllerTests.cs
2026-09-24 23:00:18 -03:00

220 lines
8.6 KiB
C#

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<IWorkOrderUpliftService> upliftService,
params string[] roles)
{
var controller = new WorkOrderDetailController(
Mock.Of<IWorkOrderDetailService>(),
Mock.Of<IWorkOrderCommentService>(),
upliftService.Object,
Mock.Of<ILogger<WorkOrderDetailController>>());
var claims = new List<Claim> { 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<IWorkOrderUpliftService>();
service.Setup(x => x.ListAsync(7, It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<WorkOrderUpliftDto>
{
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<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
var list = Assert.IsType<WorkOrderUpliftListDto>(envelope.Data);
Assert.Single(list.Items);
Assert.Equal(11, list.Items[0].Id);
}
[Fact]
public async Task CreateUplift_ReturnsCreatedItem()
{
var service = new Mock<IWorkOrderUpliftService>();
service.Setup(x => x.CreateAsync(
7,
It.IsAny<CreateWorkOrderUpliftRequestDto>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.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<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
var created = Assert.IsType<WorkOrderUpliftDto>(envelope.Data);
Assert.Equal(12, created.Id);
}
[Fact]
public async Task CreateUplift_RequestPermissionDenied_ReturnsExact403Message()
{
var service = new Mock<IWorkOrderUpliftService>();
service.Setup(x => x.CreateAsync(
7,
It.IsAny<CreateWorkOrderUpliftRequestDto>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.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<ObjectResult>().Subject;
forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
var response = forbidden.Value.Should().BeOfType<Response>().Subject;
response.Message.Should().Be(UpliftForbiddenException.RequestUpliftsDeniedMessage);
}
[Fact]
public async Task CreateUplift_UnrelatedForbiddenException_ReturnsSanitized403()
{
var service = new Mock<IWorkOrderUpliftService>();
service.Setup(x => x.CreateAsync(
7,
It.IsAny<CreateWorkOrderUpliftRequestDto>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.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<ObjectResult>().Subject;
forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
var response = forbidden.Value.Should().BeOfType<Response>().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<IWorkOrderUpliftService>();
service.Setup(x => x.CreateAsync(
7,
It.IsAny<CreateWorkOrderUpliftRequestDto>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.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<ConflictObjectResult>().Subject;
conflict.StatusCode.Should().Be(StatusCodes.Status409Conflict);
conflict.Value.Should().NotBeNull();
}
[Fact]
public async Task CancelUplift_NotFound_Returns404()
{
var service = new Mock<IWorkOrderUpliftService>();
service.Setup(x => x.CancelAsync(7, 99, It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new KeyNotFoundException("Uplift request not found"));
var controller = NewController(service);
var result = await controller.CancelUplift(7, 99, CancellationToken.None);
result.Should().BeOfType<NotFoundObjectResult>();
}
[Fact]
public async Task RevokeUplift_Forbidden_Returns403()
{
var service = new Mock<IWorkOrderUpliftService>();
service.Setup(x => x.RevokeAsync(
7,
99,
It.IsAny<RevokeWorkOrderUpliftRequestDto>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.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<ObjectResult>().Which.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
}
[Fact]
public async Task CancelUplift_TerminalWorkOrder_Returns400()
{
var service = new Mock<IWorkOrderUpliftService>();
service.Setup(x => x.CancelAsync(7, 99, It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
.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<BadRequestObjectResult>();
}
[Fact]
public async Task RevokeUplift_TerminalWorkOrder_Returns400()
{
var service = new Mock<IWorkOrderUpliftService>();
service.Setup(x => x.RevokeAsync(
7,
99,
It.IsAny<RevokeWorkOrderUpliftRequestDto>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.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<BadRequestObjectResult>();
}
}