mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 16:00:56 +00:00
- Added detailed logging for socket connection events in useBookingWindowSocket. - Introduced new notification types for contract status and schedule updates. - Updated notification visuals to include new icons for contract status. - Enhanced notification href resolution for contract status and schedule updates. - Implemented booking lifecycle notifier service for customer and staff notifications. - Created contract notifier service for managing contract lifecycle notifications. - Added end-to-end tests for booking window socket functionality.
110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
import { BOOKING_WINDOW_WS_EVENTS, BOOKING_WINDOW_WS_NAMESPACE } from '@edr/types';
|
|
import { INestApplication } from '@nestjs/common';
|
|
import { Test } from '@nestjs/testing';
|
|
import { io, type Socket } from 'socket.io-client';
|
|
|
|
import { WsAuthService } from '../notification-inbox/ws-auth.service';
|
|
import { BookingWindowGateway } from './booking-window.gateway';
|
|
import type { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
|
|
|
/**
|
|
* End-to-end proof the booking-window socket works: boots a real Nest app with
|
|
* the gateway, connects a real socket.io client to the namespace, emits a phase
|
|
* change, and asserts the client receives the exact payload. If this passes,
|
|
* any "no live update" report is environmental (stale server process, wrong
|
|
* checkout running, client not connecting) — not the gateway.
|
|
*/
|
|
describe('BookingWindowGateway (e2e)', () => {
|
|
let app: INestApplication;
|
|
let gateway: BookingWindowGateway;
|
|
let client: Socket;
|
|
let baseUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
const moduleRef = await Test.createTestingModule({
|
|
providers: [
|
|
BookingWindowGateway,
|
|
// Accept any token — auth plumbing is covered by the real WsAuthService.
|
|
{ provide: WsAuthService, useValue: { resolveUserId: async () => 'user-1' } },
|
|
],
|
|
}).compile();
|
|
|
|
app = moduleRef.createNestApplication();
|
|
await app.listen(0);
|
|
const address = app.getHttpServer().address() as { port: number };
|
|
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
gateway = app.get(BookingWindowGateway);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
client?.disconnect();
|
|
await app?.close();
|
|
});
|
|
|
|
it('authenticated client receives the phase event with the schedule state', async () => {
|
|
client = io(`${baseUrl}/${BOOKING_WINDOW_WS_NAMESPACE}`, {
|
|
auth: { token: 'any' },
|
|
transports: ['websocket'],
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
client.on('connect', () => resolve());
|
|
client.on('connect_error', (err) => reject(err));
|
|
});
|
|
|
|
const received = new Promise<Record<string, unknown>>((resolve) => {
|
|
client.on(BOOKING_WINDOW_WS_EVENTS.PHASE, (payload) => resolve(payload));
|
|
});
|
|
|
|
gateway.emitPhase({
|
|
id: 'sched-1',
|
|
originStationId: 'yard-a',
|
|
destinationStationId: 'yard-b',
|
|
direction: 'IMPORT',
|
|
windowPhase: 'OPEN',
|
|
bookingWindowStatus: 'OPEN',
|
|
bookingCycleNo: 2,
|
|
windowOpensAt: new Date('2026-07-06T16:15:00Z'),
|
|
windowClosesAt: new Date('2026-07-06T16:18:00Z'),
|
|
docReviewEndsAt: null,
|
|
paymentPhaseEndsAt: null,
|
|
scheduledDepartureDate: new Date('2026-07-09T05:53:00Z'),
|
|
} as unknown as TrainSchedule);
|
|
|
|
const payload = await received;
|
|
expect(payload).toMatchObject({
|
|
scheduleId: 'sched-1',
|
|
phase: 'OPEN',
|
|
bookingWindowStatus: 'OPEN',
|
|
bookingCycleNo: 2,
|
|
windowOpensAt: '2026-07-06T16:15:00.000Z',
|
|
});
|
|
});
|
|
|
|
it('rejects a client whose token does not resolve to a user', async () => {
|
|
const moduleRef = await Test.createTestingModule({
|
|
providers: [
|
|
BookingWindowGateway,
|
|
{ provide: WsAuthService, useValue: { resolveUserId: async () => null } },
|
|
],
|
|
}).compile();
|
|
const rejectingApp = moduleRef.createNestApplication();
|
|
await rejectingApp.listen(0);
|
|
const addr = rejectingApp.getHttpServer().address() as { port: number };
|
|
|
|
const rejected = io(`http://127.0.0.1:${addr.port}/${BOOKING_WINDOW_WS_NAMESPACE}`, {
|
|
auth: { token: 'bad' },
|
|
transports: ['websocket'],
|
|
reconnection: false,
|
|
});
|
|
const outcome = await new Promise<string>((resolve) => {
|
|
rejected.on('disconnect', () => resolve('disconnected'));
|
|
rejected.on('connect_error', () => resolve('rejected'));
|
|
// The server accepts the transport then drops it in handleConnection.
|
|
setTimeout(() => resolve(rejected.connected ? 'still-connected' : 'disconnected'), 500);
|
|
});
|
|
rejected.disconnect();
|
|
await rejectingApp.close();
|
|
expect(outcome).not.toBe('still-connected');
|
|
});
|
|
});
|