mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
99a8701bd4
- GET /api/trips/:tripId/reservations/google-wallet answers whether this server can make Google Wallet passes: passes on, and the TREK services' signer with its Google route mounted (it is once liketrek has a Wallet issuer). The probe is kept for ten minutes. - The Google route is derived from TREK_WALLET_SIGNER_URL by its path, the query kept; a signer URL that does not end in /pass has none, and the booking is refused with a 404 instead of going to the Apple signer. - End-to-end tests for the save link: owner, stranger, another trip, no issuer yet, a link that is not Google Wallet, and the availability. - Italian and Hungarian wording of the button.
215 lines
9.5 KiB
TypeScript
215 lines
9.5 KiB
TypeScript
/**
|
|
* GET /api/trips/:tripId/reservations/:id/wallet-pass end to end: the session,
|
|
* the trip guard, the booking read through the reservations module and the
|
|
* request the TREK services' pass signer receives. The signer itself is
|
|
* stubbed at the fetch seam.
|
|
*/
|
|
import { servicesConfig } from '../../src/nest/app-config/tokens';
|
|
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
|
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
|
import { WalletPassModule } from '../../src/nest/wallet-pass/wallet-pass.module';
|
|
import { createSnapshotTestDb } from '../helpers/db-mock';
|
|
import { makeReservation, makeReservationEndpoint } from '../helpers/factories/reservations';
|
|
import { makeTrip } from '../helpers/factories/trips';
|
|
import { makeUser } from '../helpers/factories/users';
|
|
import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm';
|
|
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
|
import { sessionCookie } from './harness';
|
|
import { Test } from '@nestjs/testing';
|
|
|
|
import cookieParser from 'cookie-parser';
|
|
import request from 'supertest';
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const { safeFetchAdminConfigured } = vi.hoisted(() => ({ safeFetchAdminConfigured: vi.fn() }));
|
|
vi.mock('../../src/utils/ssrfGuard', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/utils/ssrfGuard')>()),
|
|
safeFetchAdminConfigured,
|
|
}));
|
|
|
|
const db = createSnapshotTestDb();
|
|
const OWNER = 1;
|
|
const STRANGER = 2;
|
|
const PASS = Buffer.concat([Buffer.from([0x50, 0x4b, 0x03, 0x04]), Buffer.from('signed pass')]);
|
|
|
|
let orm: TestOrm;
|
|
let app: Awaited<ReturnType<typeof build>>;
|
|
let tripId: number;
|
|
let flightId: number;
|
|
|
|
async function build() {
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), WalletPassModule],
|
|
})
|
|
.overrideProvider(servicesConfig.KEY)
|
|
.useValue({ mode: 'auto', key: null, hosts: [], walletPassUrl: 'https://push.liketrek.com/v1/wallet/pass' })
|
|
.compile();
|
|
const nest = moduleRef.createNestApplication();
|
|
nest.use(cookieParser());
|
|
nest.useGlobalFilters(new TrekExceptionFilter());
|
|
nest.useGlobalPipes(new ZodValidationPipe());
|
|
await nest.init();
|
|
return nest;
|
|
}
|
|
|
|
const as = (userId: number) => ({ Cookie: sessionCookie(userId) });
|
|
const path = (trip: number, id: number, lang = 'de') =>
|
|
`/api/trips/${trip}/reservations/${id}/wallet-pass?lang=${lang}`;
|
|
|
|
beforeAll(async () => {
|
|
orm = await createTestOrm(db);
|
|
await makeUser(orm, { id: OWNER, username: 'owner', email: 'owner@example.test' });
|
|
await makeUser(orm, { id: STRANGER, username: 'stranger', email: 'stranger@example.test' });
|
|
const trip = await makeTrip(orm, OWNER, { title: 'Japan 2027' });
|
|
tripId = trip.id;
|
|
const flight = await makeReservation(orm, tripId, {
|
|
title: 'LH 716',
|
|
type: 'flight',
|
|
reservation_time: '2027-04-01T13:25',
|
|
confirmation_number: 'X7K2QP',
|
|
metadata: JSON.stringify({ airline: 'Lufthansa', flight_number: 'LH716', seat: '34K' }),
|
|
});
|
|
flightId = flight.id;
|
|
await makeReservationEndpoint(orm, flightId, {
|
|
role: 'from',
|
|
name: 'Frankfurt',
|
|
code: 'FRA',
|
|
lat: 50.03,
|
|
lng: 8.57,
|
|
timezone: 'Europe/Berlin',
|
|
local_date: '2027-04-01',
|
|
local_time: '13:25',
|
|
});
|
|
await makeReservationEndpoint(orm, flightId, {
|
|
role: 'to',
|
|
sequence: 1,
|
|
name: 'Tokyo Haneda',
|
|
code: 'HND',
|
|
lat: 35.55,
|
|
lng: 139.78,
|
|
timezone: 'Asia/Tokyo',
|
|
local_date: '2027-04-02',
|
|
local_time: '08:50',
|
|
});
|
|
app = await build();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
safeFetchAdminConfigured.mockReset();
|
|
safeFetchAdminConfigured.mockResolvedValue(
|
|
new Response(PASS, { status: 200, headers: { 'content-type': 'application/vnd.apple.pkpass' } }),
|
|
);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
await orm.close();
|
|
});
|
|
|
|
describe('GET /api/trips/:tripId/reservations/:id/wallet-pass', () => {
|
|
it('WALLET-E2E-001: answers 401 without a session and asks nobody', async () => {
|
|
const res = await request(app.getHttpServer()).get(path(tripId, flightId));
|
|
expect(res.status).toBe(401);
|
|
expect(safeFetchAdminConfigured).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('WALLET-E2E-002: someone outside the trip gets a 404, not a pass', async () => {
|
|
const res = await request(app.getHttpServer()).get(path(tripId, flightId)).set(as(STRANGER));
|
|
expect(res.status).toBe(404);
|
|
expect(safeFetchAdminConfigured).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('WALLET-E2E-003: the owner gets the signed pass, built from the booking in their language', async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.get(path(tripId, flightId))
|
|
.set(as(OWNER))
|
|
.buffer(true)
|
|
.parse((r, done) => {
|
|
const chunks: Buffer[] = [];
|
|
r.on('data', (c: Buffer) => chunks.push(c));
|
|
r.on('end', () => done(null, Buffer.concat(chunks)));
|
|
});
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers['content-type']).toContain('application/vnd.apple.pkpass');
|
|
expect(res.headers['content-disposition']).toBe('attachment; filename="LH-716.pkpass"');
|
|
expect(Buffer.compare(res.body as Buffer, PASS)).toBe(0);
|
|
|
|
const [url, init] = safeFetchAdminConfigured.mock.calls[0] as [string, RequestInit];
|
|
expect(url).toBe('https://push.liketrek.com/v1/wallet/pass');
|
|
const body = JSON.parse(String(init.body));
|
|
expect(body).toMatchObject({
|
|
kind: 'flight',
|
|
title: 'LH 716',
|
|
trip: 'Japan 2027',
|
|
start: '2027-04-01T13:25:00+02:00',
|
|
end: '2027-04-02T08:50:00+09:00',
|
|
from: { code: 'FRA', name: 'Frankfurt' },
|
|
to: { code: 'HND', name: 'Tokyo Haneda' },
|
|
confirmation: { value: 'X7K2QP' },
|
|
locale: 'de-DE',
|
|
});
|
|
expect(body.details.map((d: { label: string }) => d.label)).toContain('Sitzplatz');
|
|
});
|
|
|
|
it('WALLET-E2E-004: a booking of another trip, or none, is a 404', async () => {
|
|
const other = await makeTrip(orm, OWNER, { title: 'Elsewhere' });
|
|
expect((await request(app.getHttpServer()).get(path(other.id, flightId)).set(as(OWNER))).status).toBe(404);
|
|
expect((await request(app.getHttpServer()).get(path(tripId, 999_999)).set(as(OWNER))).status).toBe(404);
|
|
expect(safeFetchAdminConfigured).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('WALLET-E2E-005: a booking without a date gets a 422, and the signer failing a 502', async () => {
|
|
const undated = await makeReservation(orm, tripId, { title: 'Someday', type: 'event', reservation_time: null });
|
|
expect((await request(app.getHttpServer()).get(path(tripId, undated.id)).set(as(OWNER))).status).toBe(422);
|
|
|
|
safeFetchAdminConfigured.mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
|
expect((await request(app.getHttpServer()).get(path(tripId, flightId)).set(as(OWNER))).status).toBe(502);
|
|
});
|
|
});
|
|
|
|
describe('Google Wallet', () => {
|
|
const google = (trip: number, id: number) => `/api/trips/${trip}/reservations/${id}/wallet-pass/google?lang=de`;
|
|
const SAVE_URL = 'https://pay.google.com/gp/v/save/eyJhbGciOiJSUzI1NiJ9.payload.signature';
|
|
const answer = (status: number, body: string, type = 'application/json') =>
|
|
safeFetchAdminConfigured.mockResolvedValueOnce(new Response(body, { status, headers: { 'content-type': type } }));
|
|
|
|
it('WALLET-E2E-006: the owner gets the save link, signed at the Google route beside the Apple one', async () => {
|
|
answer(200, JSON.stringify({ saveUrl: SAVE_URL }));
|
|
const res = await request(app.getHttpServer()).get(google(tripId, flightId)).set(as(OWNER));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ saveUrl: SAVE_URL });
|
|
const [url, init] = safeFetchAdminConfigured.mock.calls[0] as [string, RequestInit];
|
|
expect(url).toBe('https://push.liketrek.com/v1/wallet/google');
|
|
expect(JSON.parse(String(init.body))).toMatchObject({ kind: 'flight', title: 'LH 716', locale: 'de-DE' });
|
|
});
|
|
|
|
it('WALLET-E2E-007: a stranger, another trip and a missing booking get a 404 and ask nobody', async () => {
|
|
expect((await request(app.getHttpServer()).get(google(tripId, flightId))).status).toBe(401);
|
|
expect((await request(app.getHttpServer()).get(google(tripId, flightId)).set(as(STRANGER))).status).toBe(404);
|
|
const other = await makeTrip(orm, OWNER, { title: 'Elsewhere too' });
|
|
expect((await request(app.getHttpServer()).get(google(other.id, flightId)).set(as(OWNER))).status).toBe(404);
|
|
expect((await request(app.getHttpServer()).get(google(tripId, 999_999)).set(as(OWNER))).status).toBe(404);
|
|
expect(safeFetchAdminConfigured).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('WALLET-E2E-008: no issuer yet is a 404, a link that is not Google Wallet a 502', async () => {
|
|
answer(404, 'not found', 'text/plain');
|
|
expect((await request(app.getHttpServer()).get(google(tripId, flightId)).set(as(OWNER))).status).toBe(404);
|
|
answer(200, JSON.stringify({ saveUrl: 'https://evil.example/save' }));
|
|
expect((await request(app.getHttpServer()).get(google(tripId, flightId)).set(as(OWNER))).status).toBe(502);
|
|
});
|
|
|
|
it('WALLET-E2E-009: the trip members can ask whether Google Wallet works here', async () => {
|
|
safeFetchAdminConfigured.mockResolvedValueOnce(new Response('', { status: 405 }));
|
|
const res = await request(app.getHttpServer())
|
|
.get(`/api/trips/${tripId}/reservations/google-wallet`)
|
|
.set(as(OWNER));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ available: true });
|
|
expect(
|
|
(await request(app.getHttpServer()).get(`/api/trips/${tripId}/reservations/google-wallet`).set(as(STRANGER)))
|
|
.status,
|
|
).toBe(404);
|
|
});
|
|
});
|