proposal-system/api/tests/ProposalSystem.Tests/Validators/CreateLineItemValidatorTests.cs

130 lines
4.1 KiB
C#
Raw Normal View History

test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6) QA-C1: Create xUnit test project, add to solution, wire dependencies - api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute - InMemory EF Core provider for isolated DB tests QA-C2: Proposal state machine transition tests (16 tests) - Valid: InReview->Approved, Approved->Sent, Sent->Revised - Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc. - Edge cases: idempotency, missing line items, revision line item copying - Audit and job publisher verification QA-C3: Authorization attribute tests (16 tests) - Controller-level [Authorize] on all controllers except AuthController - Role requirements: admins/sysadmins on admin actions - Dispatcher exclusion from admin/sysadmin routes - SysAdmin-only user management enforcement QA-C4: InternalApiKeyMiddleware tests (8 tests) - Valid key sets claims and calls next - Invalid key passes through to JWT (no 401/403) - Missing key/empty config disables middleware - Documents API-C1 vulnerability (key works on any path) QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest) - ProtectedRoute: renders children when authenticated, redirects when not - RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement - authSlice: setUser, logout, expired token handling QA-C6: Lambda SQS handler tests (19 tests, pytest) - pdf-generate: batch processing, failure reporting, malformed body - suggestions: batch processing, proposal-not-found skip, AI item preservation - API key caching, retry helpers Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
using FluentAssertions;
using ProposalSystem.Application.DTOs;
using ProposalSystem.Application.Validators;
using ProposalSystem.Domain.Entities;
using Xunit;
namespace ProposalSystem.Tests.Validators;
/// <summary>
/// Tests for CreateLineItemValidator — validates line item payloads.
/// </summary>
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");
}
}