shoc-backend/Api.SeaHavenIndustries.Tests/VendorPortalControllerTests.cs
2026-08-11 08:58:19 -03:00

383 lines
16 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.Interfaces;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class VendorPortalControllerTests
{
private static VendorPortalController NewController(Mock<IVendorPortalService> service, string? token = "valid-token")
{
var controller = new VendorPortalController(service.Object, Mock.Of<ILogger<VendorPortalController>>());
var context = new DefaultHttpContext();
if (token != null)
context.Request.Headers["X-Vendor-Token"] = token;
controller.ControllerContext = new ControllerContext { HttpContext = context };
return controller;
}
private static readonly VendorPortalSession Session = new()
{
Id = 1,
CompanyName = "Test Co",
ContactName = "Sam",
Email = "sam@test.co",
Phone = "555-0100"
};
[Fact]
public async Task Session_MissingToken_ReturnsUnauthorized()
{
var service = new Mock<IVendorPortalService>();
var controller = NewController(service, token: null);
var result = await controller.Session();
result.Should().BeOfType<UnauthorizedObjectResult>();
}
[Fact]
public async Task Session_InvalidToken_ReturnsUnauthorized()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync("bad", It.IsAny<CancellationToken>()))
.ReturnsAsync((VendorPortalSession?)null);
var controller = NewController(service, token: "bad");
var result = await controller.Session();
result.Should().BeOfType<UnauthorizedObjectResult>();
}
[Fact]
public async Task Session_ValidToken_ReturnsSessionData()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync("valid-token", It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
var controller = NewController(service);
var result = await controller.Session();
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
envelope.Status.Should().Be("Success");
var sessionData = envelope.Data as VendorPortalSession;
sessionData.Should().Be(Session);
}
[Fact]
public async Task ListDispatches_DelegatesToServiceWithStatus()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
var dispatches = new[] { new VendorDispatchSummaryDTO { Id = 10, Status = "Sent" } };
service.Setup(x => x.ListDispatchesAsync(Session, "Sent", It.IsAny<CancellationToken>()))
.ReturnsAsync(dispatches);
var controller = NewController(service);
var result = await controller.ListDispatches("Sent");
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
service.Verify(x => x.ListDispatchesAsync(Session, "Sent", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task GetDispatch_NotFound_Returns404()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.GetDispatchDetailAsync(Session, 99, It.IsAny<CancellationToken>()))
.ReturnsAsync((VendorDispatchDetailDTO?)null);
var controller = NewController(service);
var result = await controller.GetDispatch(99);
result.Should().BeOfType<NotFoundObjectResult>();
}
[Fact]
public async Task Accept_KeyNotFound_Returns404()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.AcceptDispatchAsync(Session, 99, It.IsAny<CancellationToken>()))
.ThrowsAsync(new KeyNotFoundException());
var controller = NewController(service);
var result = await controller.Accept(99);
result.Should().BeOfType<NotFoundObjectResult>();
}
[Fact]
public async Task Accept_InvalidOperation_Returns400()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.AcceptDispatchAsync(Session, 10, It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Cannot accept"));
var controller = NewController(service);
var result = await controller.Accept(10);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var resp = bad.Value.Should().BeOfType<Response>().Subject;
resp.Message.Should().NotContain("Cannot accept");
resp.Message.Should().Contain("reference");
}
[Fact]
public async Task Accept_Success_ReturnsAcknowledgedResult()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
var acceptResult = new AcceptDispatchResultDTO { Id = 10, Status = "Acknowledged" };
service.Setup(x => x.AcceptDispatchAsync(Session, 10, It.IsAny<CancellationToken>()))
.ReturnsAsync(acceptResult);
var controller = NewController(service);
var result = await controller.Accept(10);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
var acceptData = envelope.Data as AcceptDispatchResultDTO;
acceptData.Should().Be(acceptResult);
}
[Fact]
public async Task Refuse_KeyNotFound_Returns404()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.RefuseDispatchAsync(Session, 99, It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new KeyNotFoundException());
var controller = NewController(service);
var result = await controller.Refuse(99, new VendorPortalController.RefuseRequest { Reason = null });
result.Should().BeOfType<NotFoundObjectResult>();
}
[Fact]
public async Task Refuse_InvalidOperation_Returns400WithoutInternalDetail()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.RefuseDispatchAsync(Session, 10, "no", It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Cannot refuse a dispatch with status 'Acknowledged'"));
var controller = NewController(service);
var result = await controller.Refuse(10, new VendorPortalController.RefuseRequest { Reason = "no" });
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var resp = bad.Value.Should().BeOfType<Response>().Subject;
resp.Message.Should().NotContain("Cannot refuse");
resp.Message.Should().Contain("reference");
}
[Fact]
public async Task Refuse_Success_ReturnsRefusedResult()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
var refusedAt = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
var refuseResult = new RefuseDispatchResultDTO { Id = 10, Status = "Refused", RefusedAt = refusedAt };
service.Setup(x => x.RefuseDispatchAsync(Session, 10, "Too far", It.IsAny<CancellationToken>()))
.ReturnsAsync(refuseResult);
var controller = NewController(service);
var result = await controller.Refuse(10, new VendorPortalController.RefuseRequest { Reason = "Too far" });
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
var data = envelope.Data as RefuseDispatchResultDTO;
data.Should().Be(refuseResult);
service.Verify(x => x.RefuseDispatchAsync(Session, 10, "Too far", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public void RefuseEndpoint_AdvertisesDualCompatibilityRoutes()
{
var routes = typeof(VendorPortalController)
.GetMethod(nameof(VendorPortalController.Refuse))!
.GetCustomAttributes(typeof(HttpPostAttribute), inherit: false)
.Cast<HttpPostAttribute>()
.Select(attribute => attribute.Template);
routes.Should().BeEquivalentTo("dispatches/{id:int}/refuse");
}
[Fact]
public async Task ChangeStatus_ForbiddenTransition_Returns400()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.ChangeStatusAsync(Session, 10, "Completed", It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Transition not allowed"));
var controller = NewController(service);
var result = await controller.ChangeStatus(10, new VendorPortalController.ChangeStatusRequest { Status = "Completed" });
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var resp = bad.Value.Should().BeOfType<Response>().Subject;
resp.Message.Should().NotContain("Transition");
resp.Message.Should().Contain("reference");
}
[Fact]
public async Task RequestCancel_Success_ReturnsSuccessWithMessage()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
var controller = NewController(service);
var result = await controller.RequestCancel(10, new VendorPortalController.RequestCancelRequest { Reason = "Parts delay" });
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
envelope.Status.Should().Be("Success");
envelope.Message.Should().Be("Cancel request sent to dispatcher");
service.Verify(x => x.RequestCancelAsync(Session, 10, "Parts delay", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task UpdateChecklistItem_LockedDispatch_Returns400()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.UpdateChecklistItemAsync(Session, 10, 5, true, It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Dispatch is locked"));
var controller = NewController(service);
var result = await controller.UpdateChecklistItem(10, 5, new VendorPortalController.ChecklistUpdateRequest { IsCompleted = true });
result.Should().BeOfType<BadRequestObjectResult>();
}
[Fact]
public async Task AddSignoff_DuplicateSignoff_Returns400()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.AddSignoffAsync(Session, 10, "vendor", "Name", "sig", "drawn", It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("A vendor signoff already exists"));
var controller = NewController(service);
var result = await controller.AddSignoff(10, new VendorPortalController.SignoffRequest
{
SignoffType = "vendor",
Name = "Name",
Signature = "sig",
SignatureMethod = "drawn"
});
result.Should().BeOfType<BadRequestObjectResult>();
}
[Fact]
public async Task RequestUplift_NTEBelowCurrent_Returns400()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.RequestUpliftAsync(Session, 10, 100m, null, It.IsAny<string?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Requested NTE must be greater than the current NTE"));
var controller = NewController(service);
var result = await controller.RequestUplift(10, new VendorPortalController.UpliftRequestBody { RequestedNTE = 100m });
result.Should().BeOfType<BadRequestObjectResult>();
}
[Fact]
public async Task RequestUplift_Success_ReturnsResult()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
var upliftResult = new UpliftRequestResultDTO { Id = 77, Status = "Pending", RequiredTier = 1 };
service.Setup(x => x.RequestUpliftAsync(Session, 10, 500m, "Need more parts", It.IsAny<string?>(), It.IsAny<int?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(upliftResult);
var controller = NewController(service);
var result = await controller.RequestUplift(10, new VendorPortalController.UpliftRequestBody
{
RequestedNTE = 500m,
Reason = "Need more parts"
});
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
var upliftData = envelope.Data as UpliftRequestResultDTO;
upliftData.Should().Be(upliftResult);
}
[Fact]
public async Task CancelUpliftRequest_NonPending_Returns400()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.CancelUpliftRequestAsync(Session, 10, 77, It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Cannot cancel a 'Approved' uplift request"));
var controller = NewController(service);
var result = await controller.CancelUpliftRequest(10, 77);
result.Should().BeOfType<BadRequestObjectResult>();
}
[Fact]
public async Task CancelUpliftRequest_Success_ReturnsCancelledStatus()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
var cancelResult = new CancelUpliftResultDTO { Id = 77, Status = "Cancelled" };
service.Setup(x => x.CancelUpliftRequestAsync(Session, 10, 77, It.IsAny<CancellationToken>()))
.ReturnsAsync(cancelResult);
var controller = NewController(service);
var result = await controller.CancelUpliftRequest(10, 77);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var envelope = ok.Value.Should().BeOfType<DataResponse>().Subject;
var cancelData = envelope.Data as CancelUpliftResultDTO;
cancelData.Should().Be(cancelResult);
}
[Fact]
public async Task AddComment_EmptyText_Returns400()
{
var service = new Mock<IVendorPortalService>();
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Session);
service.Setup(x => x.AddCommentAsync(Session, 10, "", It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Comment cannot be empty"));
var controller = NewController(service);
var result = await controller.AddComment(10, new VendorPortalController.CommentRequest { CommentText = "" });
result.Should().BeOfType<BadRequestObjectResult>();
}
}