/** * 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); }); });