mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
185 lines
8.2 KiB
C#
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>();
|
|
}
|
|
}
|