Skip to content

feat(ws): Support for dynamic path params in websocket, add tests #15488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions integration/websockets/e2e/wildcard-param.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { INestApplication } from '@nestjs/common';
import { WsAdapter } from '@nestjs/platform-ws';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as WebSocket from 'ws';
import {
WildcardParamGateway,
MultipleParamsGateway,
} from '../src/wildcard-param.gateway';

async function createNestApp(...gateways: any[]): Promise<INestApplication> {
const testingModule = await Test.createTestingModule({
providers: gateways,
}).compile();
const app = testingModule.createNestApplication();
app.useWebSocketAdapter(new WsAdapter(app) as any);
return app;
}

describe('WebSocket Wildcard URL Parameters', () => {
let app: INestApplication;
let ws: WebSocket;

afterEach(async () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
if (app) {
await app.close();
}
});

describe('Single Parameter Gateway', () => {
beforeEach(async () => {
app = await createNestApp(WildcardParamGateway);
await app.listen(3000);
});

it('should extract roomId parameter from URL path', async () => {
const roomId = 'test-room-123';
ws = new WebSocket(`ws://localhost:3000/chat/${roomId}/socket`);

await new Promise(resolve => ws.on('open', resolve));

const testMessage = { message: 'Hello World' };
ws.send(
JSON.stringify({
event: 'join',
data: testMessage,
}),
);

await new Promise<void>(resolve => {
ws.on('message', data => {
const response = JSON.parse(data.toString());
expect(response.event).to.equal('joined');
expect(response.data.roomId).to.equal(roomId);
expect(response.data.message).to.equal(testMessage.message);
expect(response.data.timestamp).to.be.a('string');
resolve();
});
});
});

it('should handle different roomId values', async () => {
const roomId = 'room-with-dashes-and-numbers-456';
ws = new WebSocket(`ws://localhost:3000/chat/${roomId}/socket`);

await new Promise(resolve => ws.on('open', resolve));

ws.send(
JSON.stringify({
event: 'join',
data: { message: 'Different room test' },
}),
);

await new Promise<void>(resolve => {
ws.on('message', data => {
const response = JSON.parse(data.toString());
expect(response.data.roomId).to.equal(roomId);
resolve();
});
});
});

it('should return all parameters when using @WsParam() without argument', async () => {
const roomId = 'all-params-test';
ws = new WebSocket(`ws://localhost:3000/chat/${roomId}/socket`);

await new Promise(resolve => ws.on('open', resolve));

const testData = { info: 'test data' };
ws.send(
JSON.stringify({
event: 'getAllParams',
data: testData,
}),
);

await new Promise<void>(resolve => {
ws.on('message', data => {
const response = JSON.parse(data.toString());
expect(response.event).to.equal('allParams');
expect(response.data.params).to.be.an('object');
expect(response.data.params.roomId).to.equal(roomId);
expect(response.data.receivedData).to.deep.equal(testData);
resolve();
});
});
});

it('should handle URL encoded parameters', async () => {
const roomId = 'room%20with%20spaces';
ws = new WebSocket(`ws://localhost:3000/chat/${roomId}/socket`);

await new Promise(resolve => ws.on('open', resolve));

ws.send(
JSON.stringify({
event: 'join',
data: { message: 'Encoded test' },
}),
);

await new Promise<void>(resolve => {
ws.on('message', data => {
const response = JSON.parse(data.toString());
expect(response.data.roomId).to.equal('room with spaces');
resolve();
});
});
});
});

describe('Multiple Parameters Gateway', () => {
beforeEach(async () => {
app = await createNestApp(MultipleParamsGateway);
await app.listen(3000);
});

it('should extract multiple parameters from complex URL path', async () => {
const gameId = 'game-123';
const roomId = 'room-456';
const playerId = 'player-789';

ws = new WebSocket(
`ws://localhost:3000/game/${gameId}/room/${roomId}/player/${playerId}/socket`,
);

await new Promise(resolve => ws.on('open', resolve));

const moveData = { x: 10, y: 20, action: 'attack' };
ws.send(
JSON.stringify({
event: 'move',
data: moveData,
}),
);

await new Promise<void>(resolve => {
ws.on('message', data => {
const response = JSON.parse(data.toString());
expect(response.event).to.equal('moveProcessed');
expect(response.data.gameId).to.equal(gameId);
expect(response.data.roomId).to.equal(roomId);
expect(response.data.playerId).to.equal(playerId);
expect(response.data.move).to.deep.equal(moveData);
expect(response.data.timestamp).to.be.a('string');
resolve();
});
});
});

it('should get all parameters as object in multiple params scenario', async () => {
const gameId = 'test-game';
const roomId = 'test-room';
const playerId = 'test-player';

ws = new WebSocket(
`ws://localhost:3000/game/${gameId}/room/${roomId}/player/${playerId}/socket`,
);

await new Promise(resolve => ws.on('open', resolve));

ws.send(
JSON.stringify({
event: 'status',
data: {},
}),
);

await new Promise<void>(resolve => {
ws.on('message', data => {
const response = JSON.parse(data.toString());
expect(response.event).to.equal('statusUpdate');
expect(response.data.gameId).to.equal(gameId);
expect(response.data.roomId).to.equal(roomId);
expect(response.data.playerId).to.equal(playerId);
expect(response.data.status).to.equal('active');
resolve();
});
});
});

it('should handle numeric-like parameters as strings', async () => {
const gameId = '12345';
const roomId = '67890';
const playerId = '99999';

ws = new WebSocket(
`ws://localhost:3000/game/${gameId}/room/${roomId}/player/${playerId}/socket`,
);

await new Promise(resolve => ws.on('open', resolve));

ws.send(
JSON.stringify({
event: 'move',
data: { test: 'numeric params' },
}),
);

await new Promise<void>(resolve => {
ws.on('message', data => {
const response = JSON.parse(data.toString());
expect(response.data.gameId).to.equal('12345');
expect(response.data.roomId).to.equal('67890');
expect(response.data.playerId).to.equal('99999');
expect(typeof response.data.gameId).to.equal('string');
expect(typeof response.data.roomId).to.equal('string');
expect(typeof response.data.playerId).to.equal('string');
resolve();
});
});
});
});

describe('Error Handling', () => {
beforeEach(async () => {
app = await createNestApp(WildcardParamGateway);
await app.listen(3000);
});

it('should fail to connect to non-matching static path', async () => {
const promise = new Promise((resolve, reject) => {
ws = new WebSocket('ws://localhost:3000/invalid-path');
ws.on('open', () => reject(new Error('Should not connect')));
ws.on('error', () => resolve('Expected error'));
setTimeout(() => resolve('Timeout as expected'), 1000);
});

await promise;
});

it('should fail to connect to path missing required parameters', async () => {
const promise = new Promise((resolve, reject) => {
ws = new WebSocket('ws://localhost:3000/chat/socket'); // Missing roomId
ws.on('open', () => reject(new Error('Should not connect')));
ws.on('error', () => resolve('Expected error'));
setTimeout(() => resolve('Timeout as expected'), 1000);
});

await promise;
});
});
});
82 changes: 82 additions & 0 deletions integration/websockets/src/wildcard-param.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
ConnectedSocket,
MessageBody,
SubscribeMessage,
WebSocketGateway,
} from '@nestjs/websockets';
import { WsParam } from '@nestjs/websockets';

@WebSocketGateway({
path: '/chat/:roomId/socket',
})
export class WildcardParamGateway {
@SubscribeMessage('join')
handleJoin(
@ConnectedSocket() client: any,
@MessageBody() data: any,
@WsParam('roomId') roomId: string,
) {
return {
event: 'joined',
data: {
roomId,
message: data.message,
timestamp: new Date().toISOString(),
},
};
}

@SubscribeMessage('getAllParams')
handleGetAllParams(
@ConnectedSocket() client: any,
@MessageBody() data: any,
@WsParam() params: Record<string, string>,
) {
return {
event: 'allParams',
data: {
params,
receivedData: data,
},
};
}
}

@WebSocketGateway({
path: '/game/:gameId/room/:roomId/player/:playerId/socket',
})
export class MultipleParamsGateway {
@SubscribeMessage('move')
handleMove(
@ConnectedSocket() client: any,
@MessageBody() moveData: any,
@WsParam('gameId') gameId: string,
@WsParam('roomId') roomId: string,
@WsParam('playerId') playerId: string,
) {
return {
event: 'moveProcessed',
data: {
gameId,
roomId,
playerId,
move: moveData,
timestamp: new Date().toISOString(),
},
};
}

@SubscribeMessage('status')
handleStatus(
@ConnectedSocket() client: any,
@WsParam() allParams: Record<string, string>,
) {
return {
event: 'statusUpdate',
data: {
...allParams,
status: 'active',
},
};
}
}
Loading