mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 05:58:18 +00:00
Adding all the tests and fixes to the passengers app
This commit is contained in:
89
apps/edr-passenger-api/test/fixtures/seed-passenger-session.ts
vendored
Normal file
89
apps/edr-passenger-api/test/fixtures/seed-passenger-session.ts
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Seeds a passenger IAM user + session directly (no OTP flow) and mints an access token the portal
|
||||
* accepts. The API JwtGuard verifies the JWT signature (JWT_ACCESS_TOKEN_SECRET) and looks up the
|
||||
* session by its `id` claim; the portal then calls /auth/profile which needs a Passenger row linked
|
||||
* by iamUserId. Returns { token, profile } for the Playwright passenger storageState.
|
||||
*
|
||||
* Run standalone to validate: `npx ts-node test/fixtures/seed-passenger-session.ts` (prints the
|
||||
* token and the /auth/profile status via the running API on :4000).
|
||||
*/
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { SignJWT } from "jose";
|
||||
import { UI_IDS } from "./seed-ui";
|
||||
|
||||
export const PASSENGER_USER_ID = "11111111-0000-4000-8000-000000000001";
|
||||
export const PASSENGER_SESSION_ID = "11111111-0000-4000-8000-000000000002";
|
||||
const EMAIL = "test.passenger@edr.local";
|
||||
const USERNAME = "test_passenger";
|
||||
|
||||
export async function seedPassengerSession(prisma: PrismaClient): Promise<{ token: string }> {
|
||||
const secret = process.env.JWT_ACCESS_TOKEN_SECRET;
|
||||
if (!secret) throw new Error("JWT_ACCESS_TOKEN_SECRET is required to mint the passenger token");
|
||||
|
||||
const userInfo = {
|
||||
id: PASSENGER_USER_ID,
|
||||
name: { en: "Test Passenger" },
|
||||
email: EMAIL,
|
||||
roles: [] as unknown[],
|
||||
status: "accepted",
|
||||
employee: [] as unknown[],
|
||||
userType: "individual",
|
||||
username: USERNAME,
|
||||
permissions: [] as unknown[],
|
||||
};
|
||||
const expiry = new Date(Date.now() + 7 * 86400_000);
|
||||
|
||||
// iam.users (delete-then-insert so re-seeding is idempotent).
|
||||
await prisma.$executeRawUnsafe(`DELETE FROM iam.sessions WHERE id = $1::uuid`, PASSENGER_SESSION_ID);
|
||||
await prisma.$executeRawUnsafe(`DELETE FROM iam.users WHERE id = $1::uuid`, PASSENGER_USER_ID);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`INSERT INTO iam.users (created_at, id, name, username, email, user_type, status, is_active, has_set_password, is_phone_number_verified, verified_by)
|
||||
VALUES (now(), $1::uuid, $2::jsonb, $3, $4, 'individual', 'accepted', true, true, true, 'SYSTEM')`,
|
||||
PASSENGER_USER_ID,
|
||||
JSON.stringify(userInfo.name),
|
||||
USERNAME,
|
||||
EMAIL,
|
||||
);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`INSERT INTO iam.sessions (created_at, id, email, device, "userInfo", expiry_time, refresh_count, status, user_id)
|
||||
VALUES (now(), $1::uuid, $2, 'e2e', $3::jsonb, $4, 0, 'ACTIVE', $5::uuid)`,
|
||||
PASSENGER_SESSION_ID,
|
||||
EMAIL,
|
||||
JSON.stringify(userInfo),
|
||||
expiry,
|
||||
PASSENGER_USER_ID,
|
||||
);
|
||||
|
||||
// Link the seeded Passenger row to this IAM user so /auth/profile resolves.
|
||||
await prisma.passenger.update({
|
||||
where: { id: UI_IDS.passenger },
|
||||
data: { iamUserId: PASSENGER_USER_ID },
|
||||
});
|
||||
|
||||
const token = await new SignJWT({ id: PASSENGER_SESSION_ID })
|
||||
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime("7d")
|
||||
.sign(new TextEncoder().encode(secret));
|
||||
|
||||
return { token };
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
const { token } = await seedPassengerSession(prisma);
|
||||
const api = process.env.API_URL ?? "http://localhost:4000";
|
||||
const res = await fetch(`${api}/auth/profile`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[passenger-session] /auth/profile -> HTTP ${res.status}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log((await res.text()).slice(0, 400));
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
})();
|
||||
}
|
||||
197
apps/edr-passenger-api/test/fixtures/seed-ui.ts
vendored
Normal file
197
apps/edr-passenger-api/test/fixtures/seed-ui.ts
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* UI E2E seed — extends seed-core with a BOOKABLE trip + payment methods + promos so the portal
|
||||
* search/booking flow and the backoffice config flow have real data to drive. Runnable standalone
|
||||
* (`ts-node test/fixtures/seed-ui.ts`) or importable (`seedUi(prisma)`) from the Playwright
|
||||
* global-setup. Reads DATABASE_URL from the environment (the UI stack points at the 5544 test DB).
|
||||
*
|
||||
* Searchability: a schedule shows in POST /search when it is SCHEDULED, not package-only, has a
|
||||
* future departure on the searched date, operational coaches with AVAILABLE seats, and a resolvable
|
||||
* fare (seat-class distance formula using the seeded route-stop distances + USD→ETB rate).
|
||||
*/
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { IDS, resetAndSeedCore } from "./seed-core";
|
||||
|
||||
export const UI_IDS = {
|
||||
train: "00000000-0000-4000-8000-000000000100",
|
||||
schedule: "00000000-0000-4000-8000-000000000101",
|
||||
coach: "00000000-0000-4000-8000-000000000102",
|
||||
// Passenger.id is set EQUAL to the IAM user id. The bookings controller overrides passengerId
|
||||
// with the JWT user id (req.user.id), and the service only resolves iamUserId→passenger when it
|
||||
// is NON-UUID — since IAM ids are UUIDs, it uses the id directly, so Passenger.id must equal it.
|
||||
passenger: "11111111-0000-4000-8000-000000000001",
|
||||
promoValid: "PROMO10",
|
||||
promoExpired: "EXPIRED50",
|
||||
// Return leg C→A on the same calendar date, for ROUND_TRIP scenarios (UA-6). The route stops are
|
||||
// symmetric in distance (A=0, C=250) so the reverse leg prices identically to the outbound.
|
||||
returnSchedule: "00000000-0000-4000-8000-000000000201",
|
||||
returnCoach: "00000000-0000-4000-8000-000000000202",
|
||||
} as const;
|
||||
|
||||
/** Days-from-now the sample trip departs (tests search on this calendar date, Addis TZ). */
|
||||
export const DEPART_IN_DAYS = 2;
|
||||
|
||||
export function sampleDepartAt(): Date {
|
||||
const d = new Date();
|
||||
d.setUTCDate(d.getUTCDate() + DEPART_IN_DAYS);
|
||||
d.setUTCHours(6, 0, 0, 0); // 06:00Z ~ 09:00 Addis — safely same calendar day either TZ
|
||||
return d;
|
||||
}
|
||||
|
||||
/** The date string a test passes to POST /search for the sample trip (YYYY-MM-DD). */
|
||||
export function sampleDepartDate(): string {
|
||||
return sampleDepartAt().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export async function seedUi(prisma: PrismaClient): Promise<void> {
|
||||
await resetAndSeedCore(prisma);
|
||||
|
||||
// Give the two seed-core seat classes the exact names the portal review flow maps by.
|
||||
await prisma.seatClass.update({
|
||||
where: { id: IDS.seatClassLocal },
|
||||
data: { name: "Economy Regular" },
|
||||
});
|
||||
await prisma.seatClass.update({
|
||||
where: { id: IDS.seatClassIntl },
|
||||
data: { name: "Economy Regular Intl" },
|
||||
});
|
||||
|
||||
// Enabled payment methods — the portal pay page renders ONLY enabled PaymentMethod rows.
|
||||
await prisma.paymentMethod.createMany({
|
||||
data: [
|
||||
{ type: "WALLET", displayName: "Wallet", currency: "ETB", enabled: true, isDefault: true, sortOrder: 0 },
|
||||
{ type: "TELEBIRR", displayName: "telebirr", currency: "ETB", enabled: true, sortOrder: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
const departAt = sampleDepartAt();
|
||||
const arriveAt = new Date(departAt.getTime() + 4 * 3600_000);
|
||||
|
||||
await prisma.train.create({
|
||||
data: { id: UI_IDS.train, number: "UI-100", name: "UI Test Express" },
|
||||
});
|
||||
|
||||
await prisma.trainSchedule.create({
|
||||
data: {
|
||||
id: UI_IDS.schedule,
|
||||
trainId: UI_IDS.train,
|
||||
routeId: IDS.route,
|
||||
originStationId: IDS.stationA,
|
||||
destinationStationId: IDS.stationC,
|
||||
departureAt: departAt,
|
||||
arrivalAt: arriveAt,
|
||||
durationMinutes: 240,
|
||||
status: "SCHEDULED",
|
||||
stopsCount: 3,
|
||||
isPackageOnly: false,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.tripStopTime.createMany({
|
||||
data: [
|
||||
{ scheduleId: UI_IDS.schedule, stationId: IDS.stationA, sequence: 1, plannedDepartureAt: departAt, status: "OPEN" },
|
||||
{ scheduleId: UI_IDS.schedule, stationId: IDS.stationB, sequence: 2, plannedDepartureAt: new Date(departAt.getTime() + 2 * 3600_000), status: "OPEN" },
|
||||
{ scheduleId: UI_IDS.schedule, stationId: IDS.stationC, sequence: 3, plannedArrivalAt: arriveAt, status: "OPEN" },
|
||||
],
|
||||
});
|
||||
|
||||
// Enough seats that a full suite run (many bookings share one seeded DB, seats are not released
|
||||
// between specs) never exhausts availability: 12 rows × 4 cols = 48 seats.
|
||||
const SEAT_ROWS = 12;
|
||||
await prisma.coach.create({
|
||||
data: { id: UI_IDS.coach, coachTypeId: IDS.coachType, number: "UI-C1", capacity: SEAT_ROWS * 4, sequence: 1, status: "ACTIVE" },
|
||||
});
|
||||
await prisma.coachAssignment.create({
|
||||
data: { scheduleId: UI_IDS.schedule, coachId: UI_IDS.coach, positionNumber: 1, isOperational: true },
|
||||
});
|
||||
await prisma.seat.createMany({ data: buildSeats(UI_IDS.coach, SEAT_ROWS) });
|
||||
|
||||
// Logged-in passenger with a funded wallet + loyalty (used by the portal storageState + WALLET pay).
|
||||
await prisma.passenger.create({ data: { id: UI_IDS.passenger } });
|
||||
await prisma.walletAccount.create({
|
||||
data: { passengerId: UI_IDS.passenger, balanceMinor: 100_000_000 },
|
||||
});
|
||||
await prisma.loyaltyAccount.create({
|
||||
data: { passengerId: UI_IDS.passenger, pointsBalance: 500 },
|
||||
});
|
||||
|
||||
// Promotions (schema field names, NOT the backoffice UI names). Unique exact codes.
|
||||
await prisma.promotion.createMany({
|
||||
data: [
|
||||
{ title: "10% off", code: UI_IDS.promoValid, percentOff: 10, validUntil: new Date(Date.now() + 30 * 86400_000), active: true },
|
||||
{ title: "Expired", code: UI_IDS.promoExpired, percentOff: 50, validUntil: new Date(Date.now() - 86400_000), active: true },
|
||||
],
|
||||
});
|
||||
|
||||
// One baggage allowance (for excess-baggage flows later).
|
||||
await prisma.baggageAllowance.create({
|
||||
data: { seatClassId: IDS.seatClassLocal, maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 50 },
|
||||
});
|
||||
|
||||
// ── Return leg (C→A) for ROUND_TRIP (UA-6) ──────────────────────────────────
|
||||
// Same train, same day, departs after the outbound arrives. Distances are symmetric
|
||||
// (A=0km … C=250km) so the reverse leg prices the same as the outbound.
|
||||
const returnDepart = new Date(departAt.getTime() + 8 * 3600_000); // 8h after outbound departs
|
||||
const returnArrive = new Date(returnDepart.getTime() + 4 * 3600_000);
|
||||
await prisma.trainSchedule.create({
|
||||
data: {
|
||||
id: UI_IDS.returnSchedule,
|
||||
trainId: UI_IDS.train,
|
||||
routeId: IDS.route,
|
||||
originStationId: IDS.stationC,
|
||||
destinationStationId: IDS.stationA,
|
||||
departureAt: returnDepart,
|
||||
arrivalAt: returnArrive,
|
||||
durationMinutes: 240,
|
||||
status: "SCHEDULED",
|
||||
stopsCount: 3,
|
||||
isPackageOnly: false,
|
||||
},
|
||||
});
|
||||
await prisma.tripStopTime.createMany({
|
||||
data: [
|
||||
{ scheduleId: UI_IDS.returnSchedule, stationId: IDS.stationC, sequence: 1, plannedDepartureAt: returnDepart, status: "OPEN" },
|
||||
{ scheduleId: UI_IDS.returnSchedule, stationId: IDS.stationB, sequence: 2, plannedDepartureAt: new Date(returnDepart.getTime() + 2 * 3600_000), status: "OPEN" },
|
||||
{ scheduleId: UI_IDS.returnSchedule, stationId: IDS.stationA, sequence: 3, plannedArrivalAt: returnArrive, status: "OPEN" },
|
||||
],
|
||||
});
|
||||
await prisma.coach.create({
|
||||
data: { id: UI_IDS.returnCoach, coachTypeId: IDS.coachType, number: "UI-C2", capacity: 48, sequence: 1, status: "ACTIVE" },
|
||||
});
|
||||
await prisma.coachAssignment.create({
|
||||
data: { scheduleId: UI_IDS.returnSchedule, coachId: UI_IDS.returnCoach, positionNumber: 1, isOperational: true },
|
||||
});
|
||||
await prisma.seat.createMany({ data: buildSeats(UI_IDS.returnCoach, 12) });
|
||||
}
|
||||
|
||||
/** Build `rows × 4` seats (cols A–D) for a coach. */
|
||||
function buildSeats(coachId: string, rows: number) {
|
||||
const cols = ["A", "B", "C", "D"];
|
||||
const seats: Array<{ coachId: string; seatNumber: string; row: number; col: string; isWindow: boolean; isAisle: boolean }> = [];
|
||||
for (let row = 1; row <= rows; row++) {
|
||||
for (const col of cols) {
|
||||
seats.push({
|
||||
coachId,
|
||||
seatNumber: `${row}${col}`,
|
||||
row,
|
||||
col,
|
||||
isWindow: col === "A" || col === "D",
|
||||
isAisle: col === "B" || col === "C",
|
||||
});
|
||||
}
|
||||
}
|
||||
return seats;
|
||||
}
|
||||
|
||||
// Standalone runner
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
await seedUi(prisma);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[seed-ui] done. Sample trip ${IDS.stationA}→${IDS.stationC} on ${sampleDepartDate()} (schedule ${UI_IDS.schedule}).`);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
})();
|
||||
}
|
||||
Reference in New Issue
Block a user