Files
edr-platform/apps/edr-passenger-api/test/authed-booking-passengerid.e2e-spec.ts

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