/** * Backoffice config validation suite (matrix Suite H). The global ValidationPipe in src/main.ts:56 * enforces exactly these class-validator DTOs, so validating the DTOs directly reproduces what a * raw API call (bypassing the HTML-only frontend checks) would be allowed to submit. * H1 🔴 CreateFareRuleDto.baseFareMinor accepts NEGATIVE (no @Min) — while the sibling * CreateSegmentFareDto.baseFareMinor has @Min(0) (inconsistent). Still unfixed. * H2 ✅ FIXED (was 🔴 "CreateSeatClassDto.basePrice accepts negative/zero, no @Min"): * seat-classes.dto.ts now has @Min(0) on basePrice. Test updated to assert the fix. * H4 ✅ FIXED (was 🔴 "CreatePromotionDto.percentOff accepts 200, no @Max(100)"): * promos.dto.ts now has @Min(0) @Max(100) on percentOff. Test updated to assert the fix. * H5 🔴 CreatePromotionDto.validUntil is @IsString (not @IsDateString) → accepts non-dates. * Still unfixed. */ import "reflect-metadata"; import { plainToInstance } from "class-transformer"; import { validate } from "class-validator"; import { CreateFareRuleDto } from "../src/modules/schedules/schedules.dto"; import { CreateSegmentFareDto } from "../src/modules/segments/segment-fare.dto"; import { CreateSeatClassDto } from "../src/modules/seat-classes/seat-classes.dto"; import { CreatePromotionDto } from "../src/modules/promos/promos.dto"; /** Property names that produced a validation error. */ async function erroredProps(dto: object): Promise { const errors = await validate(dto); return errors.map((e) => e.property); } describe("Backoffice config validation (Suite H)", () => { it("H1 🔴 CreateFareRuleDto accepts a NEGATIVE baseFareMinor (no @Min)", async () => { const dto = plainToInstance(CreateFareRuleDto, { seatClassId: "sc-1", baseFareMinor: -100, validFrom: "2026-01-01T00:00:00Z", }); expect(await erroredProps(dto)).not.toContain("baseFareMinor"); }); it("H1 contrast: sibling CreateSegmentFareDto REJECTS negative baseFareMinor (@Min(0))", async () => { const dto = plainToInstance(CreateSegmentFareDto, { routeId: "rt-1", originStopSequence: 1, destinationStopSequence: 5, seatClassId: "sc-1", baseFareMinor: -100, }); expect(await erroredProps(dto)).toContain("baseFareMinor"); }); it("H2 ✅ CreateSeatClassDto rejects a negative basePrice (@Min(0))", async () => { const dto = plainToInstance(CreateSeatClassDto, { coachTypeId: "ct-1", name: "Economy", basePrice: -5000, }); expect(await erroredProps(dto)).toContain("basePrice"); }); it("H4 ✅ CreatePromotionDto rejects percentOff = 200 (@Max(100))", async () => { const dto = plainToInstance(CreatePromotionDto, { code: "OVER", title: "Overshoot", percentOff: 200, validUntil: "2026-12-31T23:59:59Z", }); expect(await erroredProps(dto)).toContain("percentOff"); }); it("H5 🔴 CreatePromotionDto.validUntil accepts a non-date string (@IsString, not @IsDateString)", async () => { const dto = plainToInstance(CreatePromotionDto, { code: "BADDATE", title: "Bad date", validUntil: "not-a-real-date", }); expect(await erroredProps(dto)).not.toContain("validUntil"); }); });