mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
Merge pull request #77 from Sea-Haven-Industries/fix/ab/sh-257-vendor-notes-limit
fix(vendors): enforce 500-character notes limit (SH-257)
This commit is contained in:
commit
7a33928779
2 changed files with 163 additions and 2 deletions
|
|
@ -105,6 +105,48 @@ public class VendorCompanyRosterServiceTests
|
|||
captured.Name.Should().Be("Acme");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_AcceptsNotesAt500Characters()
|
||||
{
|
||||
var data = new Mock<IVendorCompanyRosterDataService>();
|
||||
VendorCompanyRosterWriteModel? captured = null;
|
||||
data.Setup(x => x.CreateRosterAsync(It.IsAny<VendorCompanyRosterWriteModel>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<VendorCompanyRosterWriteModel, CancellationToken>((model, _) => captured = model)
|
||||
.ReturnsAsync(SampleReadModel());
|
||||
var notes = new string('n', 500);
|
||||
|
||||
await NewService(data).CreateRosterAsync(new CreateVendorRosterDTO
|
||||
{
|
||||
Name = "Acme",
|
||||
Email = "acme@example.com",
|
||||
Notes = notes
|
||||
}, "42", CancellationToken.None);
|
||||
|
||||
captured!.Notes.Should().Be(notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_RejectsNotesAt501Characters()
|
||||
{
|
||||
var data = new Mock<IVendorCompanyRosterDataService>();
|
||||
var dto = new CreateVendorRosterDTO
|
||||
{
|
||||
Name = "Acme",
|
||||
Email = "acme@example.com",
|
||||
Notes = new string('n', 501)
|
||||
};
|
||||
|
||||
var act = () => NewService(data).CreateRosterAsync(dto, "42", CancellationToken.None);
|
||||
|
||||
(await act.Should().ThrowAsync<ValidationException>())
|
||||
.Which.Errors.Should().ContainSingle(error =>
|
||||
error.PropertyName == nameof(CreateVendorRosterDTO.Notes)
|
||||
&& error.ErrorMessage == "Notes cannot exceed 500 characters.");
|
||||
data.Verify(
|
||||
x => x.CreateRosterAsync(It.IsAny<VendorCompanyRosterWriteModel>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reconcile_RejectsDuplicateTechnicianIds()
|
||||
{
|
||||
|
|
@ -236,6 +278,50 @@ public class VendorCompanyRosterServiceTests
|
|||
captured.Name.Should().Be("Acme Co");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reconcile_AcceptsNotesAt500Characters()
|
||||
{
|
||||
var data = new Mock<IVendorCompanyRosterDataService>();
|
||||
VendorCompanyRosterWriteModel? captured = null;
|
||||
data.Setup(x => x.SaveRosterAsync(It.IsAny<VendorCompanyRosterWriteModel>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<VendorCompanyRosterWriteModel, CancellationToken>((model, _) => captured = model)
|
||||
.ReturnsAsync(SampleReadModel());
|
||||
var notes = new string('n', 500);
|
||||
|
||||
await NewService(data).ReconcileRosterAsync(7, new ReconcileVendorRosterDTO
|
||||
{
|
||||
RowVersion = "AAAAAAAAD8I=",
|
||||
Name = "Acme",
|
||||
Email = "acme@example.com",
|
||||
Notes = notes
|
||||
}, "42", CancellationToken.None);
|
||||
|
||||
captured!.Notes.Should().Be(notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reconcile_RejectsNotesAt501Characters()
|
||||
{
|
||||
var data = new Mock<IVendorCompanyRosterDataService>();
|
||||
var dto = new ReconcileVendorRosterDTO
|
||||
{
|
||||
RowVersion = "AAAAAAAAD8I=",
|
||||
Name = "Acme",
|
||||
Email = "acme@example.com",
|
||||
Notes = new string('n', 501)
|
||||
};
|
||||
|
||||
var act = () => NewService(data).ReconcileRosterAsync(7, dto, "42", CancellationToken.None);
|
||||
|
||||
(await act.Should().ThrowAsync<ValidationException>())
|
||||
.Which.Errors.Should().ContainSingle(error =>
|
||||
error.PropertyName == nameof(ReconcileVendorRosterDTO.Notes)
|
||||
&& error.ErrorMessage == "Notes cannot exceed 500 characters.");
|
||||
data.Verify(
|
||||
x => x.SaveRosterAsync(It.IsAny<VendorCompanyRosterWriteModel>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reconcile_RejectsMalformedRowVersion()
|
||||
{
|
||||
|
|
@ -490,6 +576,46 @@ public class VendorCompanyRosterServiceTests
|
|||
captured.CompanyFields.Email.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddTechnicians_AcceptsCompanyNotesAt500Characters()
|
||||
{
|
||||
var data = new Mock<IVendorCompanyRosterDataService>();
|
||||
VendorCompanyRosterAddWriteModel? captured = null;
|
||||
data.Setup(x => x.AddTechniciansAsync(It.IsAny<VendorCompanyRosterAddWriteModel>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<VendorCompanyRosterAddWriteModel, CancellationToken>((model, _) => captured = model)
|
||||
.ReturnsAsync(SampleReadModel());
|
||||
var notes = new string('n', 500);
|
||||
|
||||
await NewService(data).AddTechniciansAsync(7, new AddTechniciansVendorRosterDTO
|
||||
{
|
||||
RowVersion = "AAAAAAAAD8I=",
|
||||
CompanyFields = new VendorRosterCompanyFieldsDTO { Notes = notes }
|
||||
}, "42", CancellationToken.None);
|
||||
|
||||
captured!.CompanyFields!.Notes.Should().Be(notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddTechnicians_RejectsCompanyNotesAt501Characters()
|
||||
{
|
||||
var data = new Mock<IVendorCompanyRosterDataService>();
|
||||
var dto = new AddTechniciansVendorRosterDTO
|
||||
{
|
||||
RowVersion = "AAAAAAAAD8I=",
|
||||
CompanyFields = new VendorRosterCompanyFieldsDTO { Notes = new string('n', 501) }
|
||||
};
|
||||
|
||||
var act = () => NewService(data).AddTechniciansAsync(7, dto, "42", CancellationToken.None);
|
||||
|
||||
(await act.Should().ThrowAsync<ValidationException>())
|
||||
.Which.Errors.Should().ContainSingle(error =>
|
||||
error.PropertyName == "CompanyFields.Notes"
|
||||
&& error.ErrorMessage == "Notes cannot exceed 500 characters.");
|
||||
data.Verify(
|
||||
x => x.AddTechniciansAsync(It.IsAny<VendorCompanyRosterAddWriteModel>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddTechnicians_BlankCompanyFieldsMeanUnchangedAndKeepContactGroup()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
public class VendorCompanyRosterService : IVendorCompanyRosterService
|
||||
{
|
||||
private const int MaxNotesLength = 500;
|
||||
private const string NotesMaxLengthMessage = "Notes cannot exceed 500 characters.";
|
||||
|
||||
private readonly IVendorCompanyRosterDataService _rosterDataService;
|
||||
|
||||
public VendorCompanyRosterService(IVendorCompanyRosterDataService rosterDataService)
|
||||
|
|
@ -40,7 +43,14 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
EnsureAuthenticated(userId);
|
||||
dto.Technicians ??= new List<RosterTechnicianInputDTO>();
|
||||
NormalizeAndValidateCompanyFields(dto.Name, dto.CompanyPhone, dto.Email, dto.GoogleMapsUrl, dto.Technicians);
|
||||
NormalizeAndValidateCompanyFields(
|
||||
dto.Name,
|
||||
dto.CompanyPhone,
|
||||
dto.Email,
|
||||
dto.GoogleMapsUrl,
|
||||
dto.Notes,
|
||||
nameof(CreateVendorRosterDTO.Notes),
|
||||
dto.Technicians);
|
||||
|
||||
if (dto.Technicians.Any(technician => technician.Id.HasValue))
|
||||
throw new ValidationException(new[]
|
||||
|
|
@ -77,7 +87,14 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
EnsureAuthenticated(userId);
|
||||
dto.Technicians ??= new List<RosterTechnicianInputDTO>();
|
||||
NormalizeAndValidateCompanyFields(dto.Name, dto.CompanyPhone, dto.Email, dto.GoogleMapsUrl, dto.Technicians);
|
||||
NormalizeAndValidateCompanyFields(
|
||||
dto.Name,
|
||||
dto.CompanyPhone,
|
||||
dto.Email,
|
||||
dto.GoogleMapsUrl,
|
||||
dto.Notes,
|
||||
nameof(ReconcileVendorRosterDTO.Notes),
|
||||
dto.Technicians);
|
||||
|
||||
byte[] rowVersion = ParseRequiredRowVersion(dto.RowVersion);
|
||||
|
||||
|
|
@ -178,6 +195,8 @@ namespace SeaHaven.Services.Implementation
|
|||
string? companyPhone,
|
||||
string? email,
|
||||
string? googleMapsUrl,
|
||||
string? notes,
|
||||
string notesPropertyName,
|
||||
List<RosterTechnicianInputDTO> technicians)
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
|
@ -206,6 +225,8 @@ namespace SeaHaven.Services.Implementation
|
|||
nameof(CreateVendorRosterDTO.GoogleMapsUrl),
|
||||
"Google Maps URL must be an absolute HTTPS URL."));
|
||||
|
||||
ValidateNotes(notes, notesPropertyName, failures);
|
||||
|
||||
// Technician phones must be valid North American format when provided.
|
||||
NormalizeAndValidateTechnicians(nameof(CreateVendorRosterDTO.Technicians), technicians, failures);
|
||||
|
||||
|
|
@ -308,6 +329,11 @@ namespace SeaHaven.Services.Implementation
|
|||
nameof(VendorRosterCompanyFieldsDTO.GoogleMapsUrl),
|
||||
"Google Maps URL must be an absolute HTTPS URL."));
|
||||
|
||||
ValidateNotes(
|
||||
fields.Notes,
|
||||
$"{nameof(AddTechniciansVendorRosterDTO.CompanyFields)}.{nameof(VendorRosterCompanyFieldsDTO.Notes)}",
|
||||
failures);
|
||||
|
||||
return new VendorRosterCompanyFieldsWriteModel
|
||||
{
|
||||
Name = name,
|
||||
|
|
@ -322,6 +348,15 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
}
|
||||
|
||||
private static void ValidateNotes(
|
||||
string? notes,
|
||||
string propertyName,
|
||||
List<ValidationFailure> failures)
|
||||
{
|
||||
if (notes?.Length > MaxNotesLength)
|
||||
failures.Add(new ValidationFailure(propertyName, NotesMaxLengthMessage));
|
||||
}
|
||||
|
||||
private async Task EnsureTechnicianIdsBelongToCompanyAsync(
|
||||
int companyId,
|
||||
List<RosterTechnicianInputDTO> technicians,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue