mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-03 06:53:32 +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);
|
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]
|
[Fact]
|
||||||
public async Task EditLocation_WhenMissing_ReturnsNotFound()
|
public async Task EditLocation_WhenMissing_ReturnsNotFound()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ using System.Security.Claims;
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moq;
|
||||||
using SeaHaven.DataServices.Implementation;
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Helpers;
|
using SeaHaven.Services.Helpers;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
|
|
@ -297,6 +299,34 @@ public class LocationServiceTests
|
||||||
ctx.Locations.Should().BeEmpty();
|
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]
|
[Fact]
|
||||||
public async Task CreateLocationAsync_IgnoresClientAccountId()
|
public async Task CreateLocationAsync_IgnoresClientAccountId()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ namespace Api.SeaHavenIndustries.DTOs
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
public string? AccountId { get; set; }
|
public string? AccountId { get; set; }
|
||||||
|
|
||||||
public int? GetAccountId() =>
|
public int? GetAccountId() => LocationAccountIdMapping.ParseOptional(AccountId);
|
||||||
int.TryParse(AccountId, out var id) ? id : null;
|
|
||||||
|
|
||||||
// ✅ Map API DTO to Service DTO
|
// ✅ Map API DTO to Service DTO
|
||||||
public UpdateLocationDTO ToServiceUpdateDTO()
|
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? Status { get; set; }
|
||||||
public string? AccountId { get; set; }
|
public string? AccountId { get; set; }
|
||||||
|
|
||||||
public int? GetAccountId() =>
|
public int? GetAccountId() => LocationAccountIdMapping.ParseOptional(AccountId);
|
||||||
int.TryParse(AccountId, out var id) ? id : null;
|
|
||||||
|
|
||||||
// ✅ Map API DTO to Service DTO
|
// ✅ Map API DTO to Service DTO
|
||||||
public CreateLocationDTO ToServiceCreateDTO()
|
public CreateLocationDTO ToServiceCreateDTO()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue