using Api.SeaHavenIndustries.Controllers; using Api.SeaHavenIndustries.DTOs; 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 VendorControllerTests { private static VendorController NewController(Mock service) { var controller = new VendorController(service.Object, Mock.Of>()); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "42") }, "Test")) } }; return controller; } [Fact] public async Task GetVendorList_ForwardsRequestCancellationToken() { var service = new Mock(); using var cancellation = new CancellationTokenSource(); var token = cancellation.Token; service.Setup(x => x.GetVendorDirectoryPagedAsync( 1, 10, "clayton", true, It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), token)) .ReturnsAsync(new PagedResult()); await NewController(service).GetVendorList(search: "clayton", cancellationToken: token); service.Verify(x => x.GetVendorDirectoryPagedAsync( 1, 10, "clayton", true, It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), token), Times.Once); } [Fact] public async Task Update_WithoutIsActive_PassesNullToServiceToPreserveStatus() { var service = new Mock(); service.Setup(x => x.UpdateVendorAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new VendorDTO()); var controller = NewController(service); var model = new EditVendor_DTO { Id = 8, CompanyName = "Updated Name" }; var result = await controller.Update(8, model); result.Should().BeOfType(); service.Verify(x => x.UpdateVendorAsync( 8, It.Is(dto => dto.IsActive == null), "42"), Times.Once); } [Fact] public async Task Update_WithExplicitIsActive_ForwardsValueUnchanged() { var service = new Mock(); service.Setup(x => x.UpdateVendorAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new VendorDTO()); var controller = NewController(service); var model = new EditVendor_DTO { Id = 8, CompanyName = "Updated Name", IsActive = false }; await controller.Update(8, model); service.Verify(x => x.UpdateVendorAsync( 8, It.Is(dto => dto.IsActive == false), "42"), Times.Once); } [Fact] public async Task Create_WithoutIsActive_DefaultsToActive() { var service = new Mock(); service.Setup(x => x.CreateVendorAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new VendorDTO()); var controller = NewController(service); var model = new Vendor_DTO { CompanyName = "New Co" }; await controller.Create(model); service.Verify(x => x.CreateVendorAsync( It.Is(dto => dto.IsActive == true), "42"), Times.Once); } [Fact] public async Task GetFacets_DelegatesToServiceAndReturnsOk() { var service = new Mock(); var facets = new VendorFacetsDTO { Companies = new[] { new VendorFacetCompanyDTO { Name = "Active Co" } }, Trades = new[] { "Plumbing" }, Locations = new[] { new VendorFacetLocationDTO { Label = "Chicago, IL" } }, JobBuckets = new[] { new VendorJobBucketDTO { Id = "under-50", Label = "Under 50" } } }; service.Setup(x => x.GetFacetsAsync(true, It.IsAny())) .ReturnsAsync(facets); var controller = NewController(service); var result = await controller.GetFacets(true); var ok = result.Should().BeOfType().Subject; ok.Value.Should().Be(facets); service.Verify(x => x.GetFacetsAsync(true, It.IsAny()), Times.Once); } [Fact] public async Task GetDropdown_DelegatesToServiceAndReturnsOk() { var service = new Mock(); var items = new[] { new VendorDropdownItemDTO { Id = 1, CompanyName = "Vendor A", DistanceMiles = 5.2 } }; service.Setup(x => x.GetDropdownAsync("Plumbing", "60601", It.IsAny())) .ReturnsAsync(items); var controller = NewController(service); var result = await controller.GetDropdown("Plumbing", "60601"); var ok = result.Should().BeOfType().Subject; ok.Value.Should().Be(items); } [Fact] public async Task GetPortalToken_ReturnsTokenDtoFromService() { var service = new Mock(); var token = new VendorPortalTokenDTO { Token = "abc", PortalUrl = "https://example.com/v/abc/dashboard" }; service.Setup(x => x.GetPortalTokenAsync(5, It.IsAny())) .ReturnsAsync(token); var controller = NewController(service); var result = await controller.GetPortalToken(5); var ok = result.Should().BeOfType().Subject; ok.Value.Should().Be(token); } [Fact] public async Task GetPortalToken_ReturnsNotFound_WhenVendorDoesNotExist() { var service = new Mock(); service.Setup(x => x.GetPortalTokenAsync(99, It.IsAny())) .ReturnsAsync((VendorPortalTokenDTO?)null); var controller = NewController(service); var result = await controller.GetPortalToken(99); result.Should().BeOfType(); } [Fact] public async Task RotatePortalToken_ReturnsRotatedToken() { var service = new Mock(); var token = new VendorPortalTokenDTO { Token = "new-token" }; service.Setup(x => x.RotatePortalTokenAsync(5, It.IsAny())) .ReturnsAsync(token); var controller = NewController(service); var result = await controller.RotatePortalToken(5); var ok = result.Should().BeOfType().Subject; ok.Value.Should().Be(token); } [Fact] public async Task RevokePortalToken_ReturnsRevokedTrue_OnSuccess() { var service = new Mock(); service.Setup(x => x.RevokePortalTokenAsync(5, It.IsAny())) .ReturnsAsync(true); var controller = NewController(service); var result = await controller.RevokePortalToken(5); var ok = result.Should().BeOfType().Subject; ok.Value.Should().NotBeNull(); } [Fact] public async Task RevokePortalToken_ReturnsNotFound_WhenVendorDoesNotExist() { var service = new Mock(); service.Setup(x => x.RevokePortalTokenAsync(99, It.IsAny())) .ReturnsAsync(false); var controller = NewController(service); var result = await controller.RevokePortalToken(99); result.Should().BeOfType(); } [Fact] public async Task Delete_WhenDeactivationBlocked_ReturnsSanitizedConflictWithOpenWorkOrdersAndLogs() { var openWorkOrders = new List { new() { WorkOrderId = 7 } }; var service = new Mock(); service.Setup(x => x.DeleteVendorAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new VendorDeactivationBlockedException("SECRET-internal-reason", openWorkOrders)); var logger = new Mock>(); logger.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); var controller = new VendorController(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.Delete(5); var conflict = result.Should().BeOfType().Subject; var value = conflict.Value!; var message = (string)value.GetType().GetProperty("Message")!.GetValue(value)!; message.Should().NotContain("SECRET"); message.Should().Contain("reference"); var returnedOpenWorkOrders = (List)value.GetType().GetProperty("OpenWorkOrders")!.GetValue(value)!; returnedOpenWorkOrders.Should().HaveCount(1); 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 Delete_MapsDeactivationBlocked_ToConflict() { var service = new Mock(); service.Setup(x => x.DeleteVendorAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new VendorDeactivationBlockedException("blocked", new List())); var controller = NewController(service); var result = await controller.Delete(5); result.Should().BeOfType(); } [Fact] public async Task Delete_MapsNotFoundInvalidOperation_ToNotFound() { var service = new Mock(); service.Setup(x => x.DeleteVendorAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException()); var controller = NewController(service); var result = await controller.Delete(5); var nf = result.Should().BeOfType().Subject; nf.Value.Should().BeOfType(); } [Fact] public async Task GetVendorById_ReturnsNotFound_WhenVendorIsNull() { var service = new Mock(); service.Setup(x => x.GetVendorByIdAsync(99)) .ReturnsAsync((VendorDTO?)null); var controller = NewController(service); var result = await controller.GetVendorById(99, null); result.Should().BeOfType(); } [Fact] public async Task Update_UnauthenticatedUser_ReturnsUnauthorized() { var service = new Mock(); var controller = new VendorController(service.Object, Mock.Of>()); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } }; var result = await controller.Update(5, new EditVendor_DTO { Id = 5, CompanyName = "X" }); result.Should().BeOfType(); } }