mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
fix(locations): reject unparseable accountId and forward cancellation
Blank accountId stays optional; nonblank parse failures return 400. Account lookup uses ExistsActiveAsync with the request token.
This commit is contained in:
parent
2718fdd294
commit
4e4bb0ca90
5 changed files with 97 additions and 4 deletions
|
|
@ -110,6 +110,46 @@ public class LocationControllerTests
|
|||
captured!.AccountId.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddLocation_InvalidAccountIdString_ReturnsValidationError()
|
||||
{
|
||||
var service = new Mock<ILocationService>();
|
||||
|
||||
var result = await NewController(service).AddLocation(
|
||||
new Location_DTO { Name = "X", AccountId = "abc" },
|
||||
CancellationToken.None);
|
||||
|
||||
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||
bad.Value.Should().BeOfType<Response>().Subject.Message.Should().Contain("accountId must be a valid integer.");
|
||||
service.Verify(
|
||||
s => s.CreateLocationFromRequestAsync(
|
||||
It.IsAny<LocationCreateRequestDTO>(),
|
||||
It.IsAny<ClaimsPrincipal>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditLocation_InvalidAccountIdString_ReturnsValidationError()
|
||||
{
|
||||
var service = new Mock<ILocationService>();
|
||||
|
||||
var result = await NewController(service).EditLocation(
|
||||
1,
|
||||
new EditLocation_DTO { Name = "X", AccountId = "abc" },
|
||||
CancellationToken.None);
|
||||
|
||||
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||
bad.Value.Should().BeOfType<Response>().Subject.Message.Should().Contain("accountId must be a valid integer.");
|
||||
service.Verify(
|
||||
s => s.UpdateLocationFromRequestAsync(
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<LocationUpdateRequestDTO>(),
|
||||
It.IsAny<ClaimsPrincipal>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditLocation_WhenMissing_ReturnsNotFound()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ using System.Security.Claims;
|
|||
using Data.SeaHavenIndustries;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Implementation;
|
||||
|
|
@ -297,6 +299,34 @@ public class LocationServiceTests
|
|||
ctx.Locations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateLocationFromRequestAsync_ForwardsCancellationToAccountLookup()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var accounts = new Mock<IAccountDataService>();
|
||||
CancellationToken seen = default;
|
||||
accounts
|
||||
.Setup(a => a.ExistsActiveAsync(9, It.IsAny<CancellationToken>()))
|
||||
.Callback<int, CancellationToken>((_, token) => seen = token)
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var service = new LocationService(
|
||||
new LocationDataService(ctx),
|
||||
accounts.Object,
|
||||
new CreateLocationValidation(),
|
||||
new UpdateLocationValidation());
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
await service.CreateLocationFromRequestAsync(
|
||||
new LocationCreateRequestDTO { Name = "Site", AccountId = 9 },
|
||||
OrgWideAdmin(),
|
||||
cts.Token);
|
||||
|
||||
seen.Should().Be(cts.Token);
|
||||
accounts.Verify(a => a.ExistsActiveAsync(9, cts.Token), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateLocationAsync_IgnoresClientAccountId()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public string? Status { get; set; }
|
||||
public string? AccountId { get; set; }
|
||||
|
||||
public int? GetAccountId() =>
|
||||
int.TryParse(AccountId, out var id) ? id : null;
|
||||
public int? GetAccountId() => LocationAccountIdMapping.ParseOptional(AccountId);
|
||||
|
||||
// ✅ Map API DTO to Service DTO
|
||||
public UpdateLocationDTO ToServiceUpdateDTO()
|
||||
|
|
|
|||
25
Api.SeaHavenIndustries/DTOs/LocationAccountIdMapping.cs
Normal file
25
Api.SeaHavenIndustries/DTOs/LocationAccountIdMapping.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System.Globalization;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace Api.SeaHavenIndustries.DTOs
|
||||
{
|
||||
internal static class LocationAccountIdMapping
|
||||
{
|
||||
public static int? ParseOptional(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(raw.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var id))
|
||||
{
|
||||
throw new ValidationException(new[]
|
||||
{
|
||||
new ValidationFailure("AccountId", "accountId must be a valid integer.")
|
||||
});
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,7 @@ namespace Api.SeaHavenIndustries.DTOs
|
|||
public string? Status { get; set; }
|
||||
public string? AccountId { get; set; }
|
||||
|
||||
public int? GetAccountId() =>
|
||||
int.TryParse(AccountId, out var id) ? id : null;
|
||||
public int? GetAccountId() => LocationAccountIdMapping.ParseOptional(AccountId);
|
||||
|
||||
// ✅ Map API DTO to Service DTO
|
||||
public CreateLocationDTO ToServiceCreateDTO()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue