test: add EDR passenger pricing/config E2E bug-hunt harness

Hermetic E2E harness targeting pricing integrity and backoffice config:
- e2e/ docker Postgres (5544) + prepare.sh/run.sh one-command runner + HTML report
- 6 suites / 23 tests reproducing pricing, FX, wallet, refund, config and auth
  defects (see docs/ISSUES.md); docs/e2e-test-matrix.md documents the matrix
- two-tier harness (slim module boot + direct service instantiation) to work
  around the IAM/RabbitMQ/file-type boot wall
- .env.test.example tracked; loader falls back to it for fresh checkouts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Muluhabt
2026-07-20 16:22:40 +03:00
parent 5d94e8acf2
commit c4f54a666b
24 changed files with 1938 additions and 2 deletions

View File

@@ -0,0 +1,74 @@
/**
* 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).
* H2 🔴 CreateSeatClassDto.basePrice accepts negative/zero (no @Min) — drives every distance fare.
* H4 🔴 CreatePromotionDto.percentOff accepts 200 (no @Max(100)) → discount > subtotal.
* H5 🔴 CreatePromotionDto.validUntil is @IsString (not @IsDateString) → accepts non-dates.
*/
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<string[]> {
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 accepts a negative basePrice (no @Min)", async () => {
const dto = plainToInstance(CreateSeatClassDto, {
coachTypeId: "ct-1",
name: "Economy",
basePrice: -5000,
});
expect(await erroredProps(dto)).not.toContain("basePrice");
});
it("H4 🔴 CreatePromotionDto accepts percentOff = 200 (no @Max(100))", async () => {
const dto = plainToInstance(CreatePromotionDto, {
code: "OVER",
title: "Overshoot",
percentOff: 200,
validUntil: "2026-12-31T23:59:59Z",
});
expect(await erroredProps(dto)).not.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");
});
});