proposal-system/api/tests/ProposalSystem.Tests/Validators/CreatePricingLibraryItemValidatorTests.cs
Adam Moussa 5d84399a0d
Some checks are pending
Deploy / Deploy to AWS (push) Waiting to run
feat: pricing library — curated priced items feed the RAG corpus (#127)
Adds a managed pricing library so admins can seed/curate reference priced items
directly, instead of the corpus being populated only by ingesting Sent proposals.
v1 PR5.

API:
- PricingLibraryItem entity + migration; /api/pricing-library CRUD (admin), with
  GET {id} reachable by internal Lambda callers (admins role via internal key).
- Create/update publish an ADDITIVE library-ingest SQS job {pricingLibraryItemId},
  wrapped so a publish failure never rolls back the save.

Lambda (library-ingest):
- Additive event-shape branch: pricingLibraryItemId -> fetch item, format markdown,
  upload to pricing-library/{category}/{id}.md, trigger KB sync. The existing
  proposalId path is byte-for-byte unchanged. Explicit error when neither id present;
  warns when both present.

Web:
- Pricing Library management page (/admin/pricing-library): list / create / edit / delete.

GPT-4.1 cross-review on the event-shape change: no BLOCK (neither/both-id handling
applied). Verified: api 159 tests; web tsc + 26 tests; lambdas ruff + 37 pytest.
2026-06-18 12:49:47 -04:00

239 lines
7.1 KiB
C#

using FluentAssertions;
using ProposalSystem.Application.DTOs;
using ProposalSystem.Application.Validators;
using ProposalSystem.Domain.Entities;
using Xunit;
namespace ProposalSystem.Tests.Validators;
/// <summary>
/// PR5: Tests for CreatePricingLibraryItemValidator.
/// </summary>
public class CreatePricingLibraryItemValidatorTests
{
private readonly CreatePricingLibraryItemValidator _sut = new();
[Fact(DisplayName = "PR5: Valid pricing library item passes validation")]
public void ValidRequest_Passes()
{
var request = new CreatePricingLibraryItemRequest(
Title: "HVAC Filter Replacement",
Description: "Standard 20x25x1 MERV-13 filter replacement",
ServiceCategory: ServiceCategory.HVAC,
Unit: "ea",
UnitPrice: 45.00m,
Keywords: "filter HVAC maintenance preventive",
Source: "Manual"
);
var result = _sut.Validate(request);
result.IsValid.Should().BeTrue();
}
[Fact(DisplayName = "PR5: Empty title fails validation")]
public void EmptyTitle_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: "",
Description: null,
ServiceCategory: ServiceCategory.General,
Unit: null,
UnitPrice: null,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "Title");
}
[Fact(DisplayName = "PR5: Title exceeding 200 chars fails")]
public void Title_TooLong_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: new string('X', 201),
Description: null,
ServiceCategory: ServiceCategory.General,
Unit: null,
UnitPrice: null,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "Title");
}
[Fact(DisplayName = "PR5: Description exceeding 5000 chars fails")]
public void Description_TooLong_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Valid Title",
Description: new string('X', 5001),
ServiceCategory: ServiceCategory.General,
Unit: null,
UnitPrice: null,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "Description");
}
[Fact(DisplayName = "PR5: Negative UnitPrice fails")]
public void NegativeUnitPrice_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Valid Title",
Description: null,
ServiceCategory: ServiceCategory.Plumbing,
Unit: "hr",
UnitPrice: -10.00m,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "UnitPrice");
}
[Fact(DisplayName = "PR5: Zero UnitPrice is valid")]
public void ZeroUnitPrice_Passes()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Complimentary Inspection",
Description: null,
ServiceCategory: ServiceCategory.General,
Unit: "ea",
UnitPrice: 0m,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeTrue();
}
[Fact(DisplayName = "PR5: Null UnitPrice is valid")]
public void NullUnitPrice_Passes()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Variable Price Item",
Description: null,
ServiceCategory: ServiceCategory.General,
Unit: null,
UnitPrice: null,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeTrue();
}
[Fact(DisplayName = "PR5: Keywords exceeding 2000 chars fails")]
public void Keywords_TooLong_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Valid Title",
Description: null,
ServiceCategory: ServiceCategory.General,
Unit: null,
UnitPrice: null,
Keywords: new string('X', 2001),
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "Keywords");
}
[Fact(DisplayName = "PR5: Invalid ServiceCategory enum value fails")]
public void InvalidServiceCategory_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Valid Title",
Description: null,
ServiceCategory: (ServiceCategory)999,
Unit: null,
UnitPrice: null,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "ServiceCategory");
}
[Fact(DisplayName = "PR5: Unit exceeding 50 chars fails")]
public void Unit_TooLong_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Valid Title",
Description: null,
ServiceCategory: ServiceCategory.General,
Unit: new string('X', 51),
UnitPrice: null,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "Unit");
}
[Fact(DisplayName = "PR5: Source exceeding 100 chars fails")]
public void Source_TooLong_Fails()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Valid Title",
Description: null,
ServiceCategory: ServiceCategory.General,
Unit: null,
UnitPrice: null,
Keywords: null,
Source: new string('X', 101)
);
var result = _sut.Validate(request);
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.PropertyName == "Source");
}
[Fact(DisplayName = "PR5: Minimal valid request (title + category only) passes")]
public void MinimalValidRequest_Passes()
{
var request = new CreatePricingLibraryItemRequest(
Title: "Basic Item",
Description: null,
ServiceCategory: ServiceCategory.Other,
Unit: null,
UnitPrice: null,
Keywords: null,
Source: null
);
var result = _sut.Validate(request);
result.IsValid.Should().BeTrue();
}
}