mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 22:23:12 +00:00
* refactor(api): enforce service and data-service boundaries * refactor(api): complete feature service boundaries * refactor(identity): enforce service and data boundaries * refactor(vendors): enforce service and data boundaries * refactor(workorders): enforce service and data boundaries * refactor(backend): enforce architecture and optimize dispatch * style(backend): format changed architecture files * fix(architecture): address backend review follow-ups * fix(backend): sanitize exception disclosure in changed API endpoints Replace raw exception-message disclosure (ex.Message) returned to API callers with a stable sanitized public message plus correlated structured internal logging, across the endpoints changed in this PR. - Add SanitizedErrors helper: logs the original exception at Error with a generated correlation id and returns a stable public message referencing it so support can trace without exposing internals. - Inject ILogger<T> into the 14 changed controllers and route every ex.Message/dbex.Message disclosure through the helper, preserving status codes, response shapes, and business data (e.g. OpenWorkOrders). - Leave FluentValidation (vex.Errors) and existing fixed-message catches untouched; out-of-scope controllers (Account/Contact/Employee/Asset/ PMSchedule) are unchanged. - Add focused tests proving internal exception text is not returned and that Error logging carrying the original exception is invoked. * fix(architecture): abstract job run state access * style: format board update service * test: use collection assertion idiom
117 lines
5 KiB
C#
117 lines
5 KiB
C#
using FluentAssertions;
|
|
using Moq;
|
|
using SeaHaven.DataServices.Dto;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Interfaces;
|
|
using SeaHaven.Services.Validation;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
public class DispatchServiceTests
|
|
{
|
|
private static DispatchService NewService(Mock<IDispatchDataService> data) =>
|
|
new(data.Object, new CreateDispatchValidation(), new UpdateDispatchValidation());
|
|
|
|
[Fact]
|
|
public async Task GetDispatchListAsync_MapsDataReadModelToServiceDtoPreservingFields()
|
|
{
|
|
var data = new Mock<IDispatchDataService>();
|
|
var source = new DispatchListResult
|
|
{
|
|
Total = 1,
|
|
Page = 2,
|
|
PageSize = 25,
|
|
Items = new List<DispatchListItemDTO>
|
|
{
|
|
new()
|
|
{
|
|
Id = 7,
|
|
DispatchNumber = "DISP-7",
|
|
PONumber = "PO-1",
|
|
NTEAmount = 200m,
|
|
Status = "Pending",
|
|
VendorId = 9,
|
|
VendorName = "Acme",
|
|
WorkOrderId = 3,
|
|
PrimaryWOInternalNumber = "WO-3",
|
|
PrimaryWOTitle = "Fix AC",
|
|
LocationName = "Site A",
|
|
WorkOrderCount = 2,
|
|
PendingUpliftCount = 1
|
|
}
|
|
}
|
|
};
|
|
data.Setup(d => d.GetListAsync(It.IsAny<DispatchListQuery>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(source);
|
|
|
|
var result = await NewService(data).GetDispatchListAsync("a", "Pending", 9, null, null, "ponumber", "asc", 2, 25, CancellationToken.None);
|
|
|
|
result.Should().BeOfType<DispatchListResultDTO>();
|
|
result.Total.Should().Be(1);
|
|
result.Page.Should().Be(2);
|
|
result.PageSize.Should().Be(25);
|
|
var row = result.Items.Single();
|
|
row.Id.Should().Be(7);
|
|
row.DispatchNumber.Should().Be("DISP-7");
|
|
row.PONumber.Should().Be("PO-1");
|
|
row.NTEAmount.Should().Be(200m);
|
|
row.Status.Should().Be("Pending");
|
|
row.VendorId.Should().Be(9);
|
|
row.VendorName.Should().Be("Acme");
|
|
row.WorkOrderId.Should().Be(3);
|
|
row.PrimaryWOInternalNumber.Should().Be("WO-3");
|
|
row.PrimaryWOTitle.Should().Be("Fix AC");
|
|
row.LocationName.Should().Be("Site A");
|
|
row.WorkOrderCount.Should().Be(2);
|
|
row.PendingUpliftCount.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDispatchDetailAsync_MapsNestedCollectionsPreservingShape()
|
|
{
|
|
var data = new Mock<IDispatchDataService>();
|
|
var detail = new DispatchDetailDTO
|
|
{
|
|
Id = 42,
|
|
DispatchNumber = "DISP-42",
|
|
PONumber = "PO-9",
|
|
NTEAmount = 500m,
|
|
Status = "Dispatched",
|
|
Description = "desc",
|
|
WorkOrderId = 11,
|
|
EmailSent = true,
|
|
Vendor = new DispatchDetailVendorDTO { Id = 1, CompanyName = "VendorCo", Email = "v@x.com" },
|
|
PrimaryLocation = new DispatchPrimaryLocationDTO { Id = 5, Name = "Loc", City = "Austin" },
|
|
WorkOrders = new List<DispatchLinkedWorkOrderDTO> { new() { WorkOrderId = 11, Title = "T", Priority = "High" } },
|
|
Checklist = new List<DispatchChecklistDTO> { new() { Id = 1, ItemText = "Check", IsCompleted = true, SortOrder = 1 } },
|
|
Signoffs = new List<DispatchSignoffDTO> { new() { Id = 1, SignoffType = "Vendor", Name = "J" } },
|
|
Comments = new List<DispatchCommentDTO> { new() { Id = 1, Commenttext = "Hi", UserName = "Sara" } },
|
|
UpliftRequests = new List<DispatchUpliftRequestDTO> { new() { Id = 1, RequestedNTE = 600m, CurrentNTE = 500m, Delta = 100m, Status = "Pending" } },
|
|
AuditLog = new List<DispatchAuditLogDTO> { new() { Id = 1, Action = "Updated", FieldName = "DISP-42:Status" } }
|
|
};
|
|
data.Setup(d => d.GetDetailAsync(42, It.IsAny<CancellationToken>())).ReturnsAsync(detail);
|
|
data.Setup(d => d.GetDetailAsync(999, It.IsAny<CancellationToken>())).ReturnsAsync((DispatchDetailDTO?)null);
|
|
|
|
var service = NewService(data);
|
|
|
|
var found = await service.GetDispatchDetailAsync(42, CancellationToken.None);
|
|
var missing = await service.GetDispatchDetailAsync(999, CancellationToken.None);
|
|
|
|
missing.Should().BeNull();
|
|
found.Should().BeOfType<DispatchDetailResultDTO>();
|
|
found!.Id.Should().Be(42);
|
|
found.DispatchNumber.Should().Be("DISP-42");
|
|
found.EmailSent.Should().BeTrue();
|
|
found.Vendor!.CompanyName.Should().Be("VendorCo");
|
|
found.PrimaryLocation!.Name.Should().Be("Loc");
|
|
found.WorkOrders.Single().Title.Should().Be("T");
|
|
found.Checklist.Single().IsCompleted.Should().BeTrue();
|
|
found.Signoffs.Single().Name.Should().Be("J");
|
|
found.Comments.Single().UserName.Should().Be("Sara");
|
|
found.UpliftRequests.Single().Delta.Should().Be(100m);
|
|
found.AuditLog.Single().Action.Should().Be("Updated");
|
|
}
|
|
}
|