using System.Security.Claims; using Api.SeaHavenIndustries.Controllers; using Data.SeaHavenIndustries.Enums; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Moq; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Interfaces; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class ServicesRegistryControllerTests { private static ServicesRegistryController NewController(Mock service, params string[] roles) { var controller = new ServicesRegistryController(service.Object) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity( roles.Select(r => new Claim(ClaimTypes.Role, r)), "Test")) } } }; return controller; } private static ServicesRegistryValidationErrorDto? ErrorValue(IActionResult result) => (result as ObjectResult)?.Value as ServicesRegistryValidationErrorDto; [Fact] public async Task GetAll_ReturnsOkWithServices() { var service = new Mock(); service.Setup(s => s.GetAllAsync(true, WorkOrderType.PM, It.IsAny())) .ReturnsAsync(new List { new() { Id = 1, Name = "Leak", IsActive = true } }); var result = await NewController(service, "Admin").GetAll(true, WorkOrderType.PM, CancellationToken.None); var ok = result.Should().BeOfType().Subject; (ok.Value as IEnumerable).Should().ContainSingle(s => s.Name == "Leak"); } [Fact] public async Task GetAll_WhenValidationCode_ReturnsBadRequestWithStableCodeAndMessage() { var service = new Mock(); service.Setup(s => s.GetAllAsync(null, WorkOrderType.Project, It.IsAny())) .Throws(new ServicesRegistryValidationException( "WorkOrderTypeInvalid", "Supported work order types may only include PM, Reactive, or Emergency.")); var result = await NewController(service, "Admin").GetAll(null, WorkOrderType.Project, CancellationToken.None); var bad = result.Should().BeOfType().Subject; ErrorValue(bad)!.Code.Should().Be("WorkOrderTypeInvalid"); ErrorValue(bad)!.Message.Should().Be("Supported work order types may only include PM, Reactive, or Emergency."); } [Fact] public async Task GetById_WhenMissing_ReturnsNotFoundWithCode() { var service = new Mock(); service.Setup(s => s.GetByIdAsync(9, It.IsAny())) .ReturnsAsync((ServiceDto?)null); var result = await NewController(service, "Admin").GetById(9, CancellationToken.None); var notFound = result.Should().BeOfType().Subject; ErrorValue(notFound)!.Code.Should().Be("NotFound"); } [Fact] public async Task Create_PassesCallerAndReturnsCreated() { var service = new Mock(); service.Setup(s => s.CreateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ServiceDto { Id = 42, Name = "Leak", IsActive = true }); var result = await NewController(service, "Scheduler").Create(new ServiceInput { Name = "Leak" }, CancellationToken.None); var created = result.Should().BeOfType().Subject; created.StatusCode.Should().Be(StatusCodes.Status201Created); (created.Value as ServiceDto)!.Id.Should().Be(42); service.Verify(s => s.CreateAsync( It.Is(p => p.IsInRole("Scheduler")), It.Is(i => i.Name == "Leak"), It.IsAny()), Times.Once); } [Fact] public async Task Create_WhenForbidden_Returns403WithStableCode() { var service = new Mock(); service.Setup(s => s.CreateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new ServicesRegistryValidationException("Forbidden", "Scheduler or Admin role is required.")); var result = await NewController(service, "User").Create(new ServiceInput { Name = "Leak" }, CancellationToken.None); var forbidden = result.Should().BeOfType().Subject; forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden); ErrorValue(forbidden)!.Code.Should().Be("Forbidden"); } [Fact] public async Task Create_WhenDuplicateName_Returns400WithStableCode() { var service = new Mock(); service.Setup(s => s.CreateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new ServicesRegistryValidationException("DuplicateName", "A service with this name already exists.")); var result = await NewController(service, "Admin").Create(new ServiceInput { Name = "Leak" }, CancellationToken.None); var bad = result.Should().BeOfType().Subject; ErrorValue(bad)!.Code.Should().Be("DuplicateName"); } [Fact] public async Task Update_ReturnsOkWithUpdatedService() { var service = new Mock(); service.Setup(s => s.UpdateAsync(It.IsAny(), 7, It.IsAny(), It.IsAny())) .ReturnsAsync(new ServiceDto { Id = 7, Name = "Renamed" }); var result = await NewController(service, "Admin").Update(7, new ServiceInput { Name = "Renamed" }, CancellationToken.None); var ok = result.Should().BeOfType().Subject; (ok.Value as ServiceDto)!.Name.Should().Be("Renamed"); } [Fact] public async Task Update_WhenNotFound_Returns404WithCode() { var service = new Mock(); service.Setup(s => s.UpdateAsync(It.IsAny(), 404, It.IsAny(), It.IsAny())) .Throws(new ServicesRegistryValidationException("NotFound", "Service not found.")); var result = await NewController(service, "Admin").Update(404, new ServiceInput(), CancellationToken.None); var notFound = result.Should().BeOfType().Subject; ErrorValue(notFound)!.Code.Should().Be("NotFound"); } [Fact] public async Task Deactivate_ReturnsOk() { var service = new Mock(); var result = await NewController(service, "Admin").Deactivate(3, CancellationToken.None); result.Should().BeOfType(); service.Verify(s => s.DeactivateAsync( It.Is(p => p.IsInRole("Admin")), 3, It.IsAny()), Times.Once); } [Fact] public async Task Deactivate_WhenForbidden_Returns403WithStableCode() { var service = new Mock(); service.Setup(s => s.DeactivateAsync(It.IsAny(), 3, It.IsAny())) .Throws(new ServicesRegistryValidationException("Forbidden", "Only administrators can deactivate services.")); var result = await NewController(service, "Scheduler").Deactivate(3, CancellationToken.None); var forbidden = result.Should().BeOfType().Subject; forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden); ErrorValue(forbidden)!.Code.Should().Be("Forbidden"); } [Fact] public async Task Deactivate_WhenNotFound_Returns404WithCode() { var service = new Mock(); service.Setup(s => s.DeactivateAsync(It.IsAny(), 404, It.IsAny())) .Throws(new ServicesRegistryValidationException("NotFound", "Service not found.")); var result = await NewController(service, "Admin").Deactivate(404, CancellationToken.None); result.Should().BeOfType(); } }