shoc-backend/Api.SeaHavenIndustries.Tests/ServicesRegistryControllerTests.cs
2026-09-16 17:07:59 -03:00

185 lines
8.2 KiB
C#

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<IServicesRegistryService> 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<IServicesRegistryService>();
service.Setup(s => s.GetAllAsync(true, WorkOrderType.PM, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<ServiceDto> { new() { Id = 1, Name = "Leak", IsActive = true } });
var result = await NewController(service, "Admin").GetAll(true, WorkOrderType.PM, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
(ok.Value as IEnumerable<ServiceDto>).Should().ContainSingle(s => s.Name == "Leak");
}
[Fact]
public async Task GetAll_WhenValidationCode_ReturnsBadRequestWithStableCodeAndMessage()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.GetAllAsync(null, WorkOrderType.Project, It.IsAny<CancellationToken>()))
.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<BadRequestObjectResult>().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<IServicesRegistryService>();
service.Setup(s => s.GetByIdAsync(9, It.IsAny<CancellationToken>()))
.ReturnsAsync((ServiceDto?)null);
var result = await NewController(service, "Admin").GetById(9, CancellationToken.None);
var notFound = result.Should().BeOfType<NotFoundObjectResult>().Subject;
ErrorValue(notFound)!.Code.Should().Be("NotFound");
}
[Fact]
public async Task Create_PassesCallerAndReturnsCreated()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.CreateAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<ServiceInput>(), It.IsAny<CancellationToken>()))
.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<ObjectResult>().Subject;
created.StatusCode.Should().Be(StatusCodes.Status201Created);
(created.Value as ServiceDto)!.Id.Should().Be(42);
service.Verify(s => s.CreateAsync(
It.Is<ClaimsPrincipal>(p => p.IsInRole("Scheduler")),
It.Is<ServiceInput>(i => i.Name == "Leak"),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Create_WhenForbidden_Returns403WithStableCode()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.CreateAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<ServiceInput>(), It.IsAny<CancellationToken>()))
.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<ObjectResult>().Subject;
forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
ErrorValue(forbidden)!.Code.Should().Be("Forbidden");
}
[Fact]
public async Task Create_WhenDuplicateName_Returns400WithStableCode()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.CreateAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<ServiceInput>(), It.IsAny<CancellationToken>()))
.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<BadRequestObjectResult>().Subject;
ErrorValue(bad)!.Code.Should().Be("DuplicateName");
}
[Fact]
public async Task Update_ReturnsOkWithUpdatedService()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.UpdateAsync(It.IsAny<ClaimsPrincipal>(), 7, It.IsAny<ServiceInput>(), It.IsAny<CancellationToken>()))
.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<OkObjectResult>().Subject;
(ok.Value as ServiceDto)!.Name.Should().Be("Renamed");
}
[Fact]
public async Task Update_WhenNotFound_Returns404WithCode()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.UpdateAsync(It.IsAny<ClaimsPrincipal>(), 404, It.IsAny<ServiceInput>(), It.IsAny<CancellationToken>()))
.Throws(new ServicesRegistryValidationException("NotFound", "Service not found."));
var result = await NewController(service, "Admin").Update(404, new ServiceInput(), CancellationToken.None);
var notFound = result.Should().BeOfType<NotFoundObjectResult>().Subject;
ErrorValue(notFound)!.Code.Should().Be("NotFound");
}
[Fact]
public async Task Deactivate_ReturnsOk()
{
var service = new Mock<IServicesRegistryService>();
var result = await NewController(service, "Admin").Deactivate(3, CancellationToken.None);
result.Should().BeOfType<OkObjectResult>();
service.Verify(s => s.DeactivateAsync(
It.Is<ClaimsPrincipal>(p => p.IsInRole("Admin")), 3, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Deactivate_WhenForbidden_Returns403WithStableCode()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.DeactivateAsync(It.IsAny<ClaimsPrincipal>(), 3, It.IsAny<CancellationToken>()))
.Throws(new ServicesRegistryValidationException("Forbidden", "Only administrators can deactivate services."));
var result = await NewController(service, "Scheduler").Deactivate(3, CancellationToken.None);
var forbidden = result.Should().BeOfType<ObjectResult>().Subject;
forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
ErrorValue(forbidden)!.Code.Should().Be("Forbidden");
}
[Fact]
public async Task Deactivate_WhenNotFound_Returns404WithCode()
{
var service = new Mock<IServicesRegistryService>();
service.Setup(s => s.DeactivateAsync(It.IsAny<ClaimsPrincipal>(), 404, It.IsAny<CancellationToken>()))
.Throws(new ServicesRegistryValidationException("NotFound", "Service not found."));
var result = await NewController(service, "Admin").Deactivate(404, CancellationToken.None);
result.Should().BeOfType<NotFoundObjectResult>();
}
}