shoc-backend/Api.SeaHavenIndustries.Tests/LocationControllerSitesTests.cs
Alexandre Brandizzi c3865e56ac Sites API: site code uniqueness, soft delete with role check, open work orders, site notes
- Reject duplicate site codes per client (case-insensitive); site code is immutable once set
- Delete tombstones the site and requires the DeleteSites permission (Admin, Scheduler)
- GET /api/locations/{id}/open-work-orders returns the open count and ids
- PATCH /api/locations/{id}/contact-info saves contacts and notes from the work-order Site dialog
- Add nullable Locations.Notes, used as the site-level POC notes fallback
2026-09-25 11:19:29 -03:00

153 lines
5.3 KiB
C#

using Api.SeaHavenIndustries.Controllers;
using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using SeaHaven.DataServices.Implementation;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Implementation;
using SeaHaven.Services.Validation;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class LocationControllerSitesTests
{
private static ApplicationDbContext NewContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ApplicationDbContext(options);
}
private static LocationController NewController(ApplicationDbContext ctx)
{
var dataService = new LocationDataService(ctx);
var service = new LocationService(
dataService,
new AccountDataService(ctx),
Mock.Of<ICreateLocationValidation>(),
Mock.Of<IUpdateLocationValidation>(),
Mock.Of<SeaHaven.DataServices.Interfaces.ITeamPermissionOverrideDataService>(),
new SeaHaven.Services.Implementation.TeamPermissionPolicy());
var controller = new LocationController(service, Mock.Of<ILogger<LocationController>>())
{
ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext()
}
};
return controller;
}
private static async Task SeedSitesAsync(ApplicationDbContext ctx)
{
ctx.Locations.AddRange(
new Locations { Id = 1, Name = "STL8", City = "St. Louis", State = "MO", AccountId = 100 },
new Locations { Id = 2, Name = "BK5", City = "Dallas", State = "TX", AccountId = 100 },
new Locations { Id = 3, Name = "IND9", City = "Indianapolis", State = "IN", AccountId = 100 },
new Locations { Id = 4, Name = "DFW8", City = "Dallas", State = "TX", AccountId = 100 });
await ctx.SaveChangesAsync();
}
[Fact]
public async Task GetSites_ReturnsIdCodeCityState_OrderedByCode()
{
await using var ctx = NewContext();
await SeedSitesAsync(ctx);
var controller = NewController(ctx);
var result = await controller.GetSites();
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
data.Should().HaveCount(4);
data.Select(s => s.Code).Should().ContainInOrder("BK5", "DFW8", "IND9", "STL8");
data[0].Should().BeEquivalentTo(new SiteOptionDTO
{
Id = 2,
Code = "BK5",
City = "Dallas",
State = "TX"
});
}
[Fact]
public async Task GetSites_FiltersByCode()
{
await using var ctx = NewContext();
await SeedSitesAsync(ctx);
var controller = NewController(ctx);
var result = await controller.GetSites("bk5");
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
data.Should().ContainSingle();
data[0].Code.Should().Be("BK5");
}
[Fact]
public async Task GetSites_FiltersByCity()
{
await using var ctx = NewContext();
await SeedSitesAsync(ctx);
var controller = NewController(ctx);
var result = await controller.GetSites("Dallas");
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
data.Should().HaveCount(2);
data.Select(s => s.Code).Should().BeEquivalentTo("BK5", "DFW8");
}
[Fact]
public async Task GetSites_OmitsLocationsWithoutAccountMapping()
{
await using var ctx = NewContext();
ctx.Locations.AddRange(
new Locations { Id = 1, Name = "STL8", City = "St. Louis", State = "MO", AccountId = 100 },
new Locations { Id = 2, Name = "UNMAPPED1", City = "Nowhere", State = "ZZ" });
await ctx.SaveChangesAsync();
var controller = NewController(ctx);
var result = await controller.GetSites();
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var data = ok.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
data.Should().ContainSingle();
data[0].Code.Should().Be("STL8");
var searchResult = await controller.GetSites("unmapped1");
var searchOk = searchResult.Should().BeOfType<OkObjectResult>().Subject;
var searchData = searchOk.Value.Should().BeAssignableTo<IReadOnlyList<SiteOptionDTO>>().Subject;
searchData.Should().BeEmpty();
}
[Fact]
public async Task GetSites_ForwardsCancellationToken()
{
await using var ctx = NewContext();
await SeedSitesAsync(ctx);
var controller = NewController(ctx);
using var cancellation = new CancellationTokenSource();
await cancellation.CancelAsync();
var act = () => controller.GetSites(cancellationToken: cancellation.Token);
await act.Should().ThrowAsync<OperationCanceledException>();
}
}