using FluentAssertions; using ProposalSystem.Application.DTOs; using ProposalSystem.Application.Validators; using ProposalSystem.Domain.Entities; using Xunit; namespace ProposalSystem.Tests.Validators; /// /// Tests for CreateLineItemValidator — validates line item payloads. /// public class CreateLineItemValidatorTests { private readonly CreateLineItemValidator _sut = new(); [Fact(DisplayName = "Valid line item request passes validation")] public void ValidRequest_Passes() { var request = new CreateLineItemRequest( Description: "HVAC duct replacement", Quantity: 10, Unit: "linear ft", UnitPrice: 25.50m, TotalPrice: 255.00m, PricingMode: PricingMode.UnitPrice, SortOrder: 1, Source: LineItemSource.Manual ); var result = _sut.Validate(request); result.IsValid.Should().BeTrue(); } [Fact(DisplayName = "Empty description fails")] public void EmptyDescription_Fails() { var request = new CreateLineItemRequest("", 1, "each", null, 100m, PricingMode.TotalPrice, 1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == "Description"); } [Fact(DisplayName = "Zero quantity fails")] public void ZeroQuantity_Fails() { var request = new CreateLineItemRequest("Work item", 0, "each", null, 100m, PricingMode.TotalPrice, 1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == "Quantity"); } [Fact(DisplayName = "Negative quantity fails")] public void NegativeQuantity_Fails() { var request = new CreateLineItemRequest("Work item", -5, "each", null, 100m, PricingMode.TotalPrice, 1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == "Quantity"); } [Fact(DisplayName = "Negative total price fails")] public void NegativeTotalPrice_Fails() { var request = new CreateLineItemRequest("Work item", 1, "each", null, -100m, PricingMode.TotalPrice, 1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == "TotalPrice"); } [Fact(DisplayName = "Negative unit price fails")] public void NegativeUnitPrice_Fails() { var request = new CreateLineItemRequest("Work item", 1, "each", -10m, 100m, PricingMode.UnitPrice, 1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == "UnitPrice"); } [Fact(DisplayName = "Null unit price is valid (lump sum pricing)")] public void NullUnitPrice_Passes() { var request = new CreateLineItemRequest("Work item", 1, "each", null, 100m, PricingMode.TotalPrice, 1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeTrue(); } [Fact(DisplayName = "Empty unit fails")] public void EmptyUnit_Fails() { var request = new CreateLineItemRequest("Work item", 1, "", null, 100m, PricingMode.TotalPrice, 1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == "Unit"); } [Fact(DisplayName = "Negative sort order fails")] public void NegativeSortOrder_Fails() { var request = new CreateLineItemRequest("Work item", 1, "each", null, 100m, PricingMode.TotalPrice, -1, LineItemSource.Manual); var result = _sut.Validate(request); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == "SortOrder"); } }