mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
124 lines
5.2 KiB
TypeScript
124 lines
5.2 KiB
TypeScript
/**
|
|
* C-9-UI 🔴 Authenticated POST /bookings is broken: the controller overrides passengerId with the
|
|
* JWT user id (`bookings.controller.ts:528-532`, "never trust the request body"), but the service
|
|
* only resolves an iamUserId → Passenger when it is NON-UUID (`bookings.service.ts:773`). Real IAM
|
|
* ids are UUIDs, and registration creates `Passenger.id ≠ iamUserId` (`passenger-auth.service.ts:225`),
|
|
* so `booking.create` uses the iamUserId directly as passengerId → foreign-key violation.
|
|
*
|
|
* This reproduces the controller's behavior by calling BookingsService.create with passengerId set to
|
|
* a UUID iamUserId (not the Passenger.id), exactly as the authed controller does. It also shows the
|
|
* CONTROL: passing the real Passenger.id succeeds — proving the resolution gap, not a fixture problem.
|
|
*/
|
|
import { BookingsService } from "../src/modules/bookings/bookings.service";
|
|
import { getTestPrisma, disconnectTestPrisma } from "./setup/prisma";
|
|
import { truncateAllPassenger, seedCore, IDS } from "./fixtures/seed-core";
|
|
|
|
function asyncStub(): any {
|
|
return new Proxy({}, { get: () => async () => undefined });
|
|
}
|
|
|
|
let seq = 0;
|
|
async function buildBookableGraph(prisma: any, passengerId: string, iamUserId: string) {
|
|
const passenger = await prisma.passenger.create({ data: { id: passengerId, iamUserId } });
|
|
const train = await prisma.train.create({ data: { number: `AB-${++seq}`, name: "T" } });
|
|
const schedule = await prisma.trainSchedule.create({
|
|
data: {
|
|
trainId: train.id,
|
|
routeId: IDS.route,
|
|
originStationId: IDS.stationA,
|
|
destinationStationId: IDS.stationB,
|
|
departureAt: new Date(Date.now() + 86_400_000),
|
|
arrivalAt: new Date(Date.now() + 90_000_000),
|
|
durationMinutes: 60,
|
|
},
|
|
});
|
|
await prisma.tripStopTime.createMany({
|
|
data: [
|
|
{ scheduleId: schedule.id, stationId: IDS.stationA, sequence: 1 },
|
|
{ scheduleId: schedule.id, stationId: IDS.stationB, sequence: 2 },
|
|
],
|
|
});
|
|
const coach = await prisma.coach.create({ data: { coachTypeId: IDS.coachType, number: `AB-${seq}` } });
|
|
const seat = await prisma.seat.create({ data: { coachId: coach.id, seatNumber: "1A", row: 1, col: "1" } });
|
|
await prisma.fareRule.create({
|
|
data: { tripId: schedule.id, seatClassId: IDS.seatClassLocal, baseFareMinor: 30_000, currency: "ETB", validFrom: new Date("2020-01-01") },
|
|
});
|
|
const hold = await prisma.seatHold.create({
|
|
data: { scheduleId: schedule.id, seatIds: [seat.id], passengerId, expiresAt: new Date(Date.now() + 3_600_000) },
|
|
});
|
|
return { schedule, seat, hold };
|
|
}
|
|
|
|
function dtoFor(passengerId: string, schedule: any, hold: any, seat: any) {
|
|
return {
|
|
passengerId, // the controller passes req.user.id here (the iamUserId)
|
|
scheduleId: schedule.id,
|
|
holdId: hold.id,
|
|
originStationId: IDS.stationA,
|
|
destinationStationId: IDS.stationB,
|
|
seatClassId: IDS.seatClassLocal,
|
|
bookingType: "ONE_WAY",
|
|
passengers: [
|
|
{
|
|
seatId: seat.id,
|
|
passengerName: "Auth User",
|
|
dateOfBirth: new Date("1990-01-01"),
|
|
idDocumentType: "PASSPORT",
|
|
passportNumber: "P1",
|
|
passportCountry: "ET",
|
|
nationality: "Ethiopian",
|
|
seatFareMinor: 30_000,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
describe("Authenticated booking passengerId resolution (regression)", () => {
|
|
const prisma = getTestPrisma();
|
|
let bookings: BookingsService;
|
|
|
|
beforeAll(() => {
|
|
bookings = new BookingsService(
|
|
prisma as any,
|
|
{ query: async () => [] } as any, // dataSource (resolveIamContact raw SQL → [])
|
|
asyncStub(), // seatsService
|
|
asyncStub(), // ticketsService — constructor gained this param since this test was written
|
|
{ emit: () => true } as any,
|
|
asyncStub(), // verifaydaService (PASSPORT skips)
|
|
asyncStub(), // currencyService (ETB skips)
|
|
asyncStub(), // fareEngine (FareRule short-circuits)
|
|
asyncStub(), // auditService
|
|
);
|
|
});
|
|
beforeEach(async () => {
|
|
await truncateAllPassenger(prisma);
|
|
await seedCore(prisma);
|
|
});
|
|
afterAll(async () => {
|
|
await disconnectTestPrisma();
|
|
});
|
|
|
|
it("🔴 create() with a UUID iamUserId (as the authed controller passes) FAILS the passenger FK", async () => {
|
|
const passengerId = "aaaaaaaa-0000-4000-8000-000000000001"; // real Passenger.id
|
|
const iamUserId = "bbbbbbbb-0000-4000-8000-000000000002"; // UUID iamUserId ≠ Passenger.id
|
|
const { schedule, hold, seat } = await buildBookableGraph(prisma, passengerId, iamUserId);
|
|
|
|
// The controller calls service.create({ ...dto, passengerId: req.user.id }) — i.e. the iamUserId.
|
|
await expect(
|
|
bookings.create(dtoFor(iamUserId, schedule, hold, seat) as any),
|
|
).rejects.toThrow(); // Prisma P2003 on Booking_passengerId_fkey
|
|
|
|
expect(await prisma.booking.count()).toBe(0);
|
|
});
|
|
|
|
it("control: create() with the real Passenger.id succeeds — proving the gap is the id, not the fixture", async () => {
|
|
const passengerId = "aaaaaaaa-0000-4000-8000-000000000003";
|
|
const iamUserId = "bbbbbbbb-0000-4000-8000-000000000004";
|
|
const { schedule, hold, seat } = await buildBookableGraph(prisma, passengerId, iamUserId);
|
|
|
|
const booking: any = await bookings.create(dtoFor(passengerId, schedule, hold, seat) as any);
|
|
expect(booking.id).toBeTruthy();
|
|
expect(booking.passengerId).toBe(passengerId);
|
|
});
|
|
});
|