Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/booking-window.gateway.spec.ts
Marshal 05b13e84a8 enhance booking and contract notification systems
- 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.
2026-07-06 21:03:08 +00:00

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