Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/empty-spiders-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/ddp-streamer': patch
---

Fixes an issue where some DDP method calls could incorrectly return a 404 error.
7 changes: 7 additions & 0 deletions ee/apps/ddp-streamer/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import server from '@rocket.chat/jest-presets/server';
import type { Config } from 'jest';

export default {
preset: server.preset,
testMatch: ['<rootDir>/src/**/*.spec.(ts|js|mjs)'],
} satisfies Config;
6 changes: 5 additions & 1 deletion ee/apps/ddp-streamer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"build": "tsc -p tsconfig.json",
"lint": "eslint .",
"ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "jest",
"testunit": "jest",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"dependencies": {
Expand Down Expand Up @@ -50,14 +51,17 @@
"devDependencies": {
"@rocket.chat/apps-engine": "workspace:^",
"@rocket.chat/ddp-client": "workspace:~",
"@rocket.chat/jest-presets": "workspace:*",
"@types/ejson": "^2.2.2",
"@types/jest": "~30.0.0",
"@types/node": "~22.16.5",
"@types/polka": "^0.5.8",
"@types/prometheus-gc-stats": "^0.6.4",
"@types/underscore": "^1.13.0",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"eslint": "~9.39.3",
"jest": "~30.2.0",
"pino-pretty": "13.1.3",
"ts-node": "^10.9.2",
"typescript": "~5.9.3"
Expand Down
108 changes: 108 additions & 0 deletions ee/apps/ddp-streamer/src/Server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { MeteorService } from '@rocket.chat/core-services';
import WebSocket from 'ws';

import { Server } from './Server';
import type { IPacket } from './types/IPacket';

jest.mock('@rocket.chat/core-services', () => ({
...jest.requireActual('@rocket.chat/core-services'),
MeteorService: {
callMethodWithToken: jest.fn(),
},
}));

jest.mock('@rocket.chat/logger', () => ({
Logger: jest.fn().mockReturnValue({
error: jest.fn(),
}),
}));

const mockCallMethodWithToken = jest.mocked(MeteorService.callMethodWithToken);

function makeClient(readyState: number = WebSocket.OPEN) {
return {
ws: { readyState },
userId: 'user1',
userToken: 'token1',
send: jest.fn(),
} as unknown as Parameters<Server['call']>[0];
}

function makePacket(method: string, id = 'test-id'): IPacket {
return { msg: 'method', method, id, params: [] } as unknown as IPacket;
}

describe('Server.call', () => {
let server: Server;

beforeEach(() => {
server = new Server();
jest.clearAllMocks();
});

describe('when the method is delegated to MeteorService', () => {
it('returns the result value from MeteorService', async () => {
mockCallMethodWithToken.mockResolvedValue({ result: 'some-value' } as any);
const client = makeClient();
const resultSpy = jest.spyOn(server, 'result');

await server.call(client, makePacket('someMethod'));

expect(resultSpy).toHaveBeenCalledWith(client, expect.objectContaining({ id: 'test-id' }), 'some-value');
});

it('does not return an error when the method returns void', async () => {
mockCallMethodWithToken.mockResolvedValue({ result: undefined } as any);
const client = makeClient();
const resultSpy = jest.spyOn(server, 'result');

await server.call(client, makePacket('setAvatarFromService'));

expect(resultSpy).toHaveBeenCalledWith(client, expect.objectContaining({ id: 'test-id' }), undefined);
});

it('calls result with an error when MeteorService throws', async () => {
mockCallMethodWithToken.mockRejectedValue(new Error('boom'));
const client = makeClient();
const resultSpy = jest.spyOn(server, 'result');

await server.call(client, makePacket('someMethod'));

expect(resultSpy).toHaveBeenCalledWith(client, expect.objectContaining({ id: 'test-id' }), null, expect.any(Error));
});
});

describe('when the method is registered locally', () => {
it('returns the result value from the local method', async () => {
server.methods({ localMethod: async () => 'local-result' });
const client = makeClient();
const resultSpy = jest.spyOn(server, 'result');

await server.call(client, makePacket('localMethod'));

expect(resultSpy).toHaveBeenCalledWith(client, expect.objectContaining({ id: 'test-id' }), 'local-result');
});

it('does not return an error when the local method returns void', async () => {
server.methods({ voidMethod: async () => undefined });
const client = makeClient();
const resultSpy = jest.spyOn(server, 'result');

await server.call(client, makePacket('voidMethod'));

expect(resultSpy).toHaveBeenCalledWith(client, expect.objectContaining({ id: 'test-id' }), undefined);
});
});

describe('when the client WebSocket is not open', () => {
it('does nothing', async () => {
const client = makeClient(WebSocket.CLOSED);
const resultSpy = jest.spyOn(server, 'result');

await server.call(client, makePacket('anyMethod'));

expect(resultSpy).not.toHaveBeenCalled();
expect(mockCallMethodWithToken).not.toHaveBeenCalled();
});
});
});
6 changes: 1 addition & 5 deletions ee/apps/ddp-streamer/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,7 @@ export class Server extends EventEmitter {
// if method was not defined on DDP Streamer we fall back to Meteor
if (!this._methods.has(packet.method)) {
const result = await MeteorService.callMethodWithToken(client.userId, client.userToken, packet.method, packet.params);
if (result?.result) {
return this.result(client, packet, result.result);
}

throw new MeteorError(404, `Method '${packet.method}' not found`);
return this.result(client, packet, result.result);
}

const fn = this._methods.get(packet.method);
Expand Down
3 changes: 2 additions & 1 deletion ee/apps/ddp-streamer/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "@rocket.chat/tsconfig/server.json",
"compilerOptions": {
"strictPropertyInitialization": false, // TODO: Remove this line
"outDir": "./dist"
"outDir": "./dist",
"types": ["jest", "node"]
},
"include": ["./src/**/*", "./definition"],
"exclude": ["./dist", "./ecosystem.config.js"]
Expand Down
5 changes: 4 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9248,6 +9248,7 @@ __metadata:
"@rocket.chat/ddp-client": "workspace:~"
"@rocket.chat/emitter": "npm:^0.32.0"
"@rocket.chat/instance-status": "workspace:^"
"@rocket.chat/jest-presets": "workspace:*"
"@rocket.chat/logger": "workspace:^"
"@rocket.chat/model-typings": "workspace:^"
"@rocket.chat/models": "workspace:^"
Expand All @@ -9256,6 +9257,7 @@ __metadata:
"@rocket.chat/string-helpers": "npm:~0.32.0"
"@rocket.chat/tracing": "workspace:^"
"@types/ejson": "npm:^2.2.2"
"@types/jest": "npm:~30.0.0"
"@types/node": "npm:~22.16.5"
"@types/polka": "npm:^0.5.8"
"@types/prometheus-gc-stats": "npm:^0.6.4"
Expand All @@ -9267,6 +9269,7 @@ __metadata:
eslint: "npm:~9.39.3"
eventemitter3: "npm:^5.0.4"
jaeger-client: "npm:^3.19.0"
jest: "npm:~30.2.0"
mem: "npm:^8.1.1"
moleculer: "npm:^0.14.35"
mongodb: "npm:6.16.0"
Expand Down Expand Up @@ -9671,7 +9674,7 @@ __metadata:
languageName: unknown
linkType: soft

"@rocket.chat/jest-presets@workspace:^, @rocket.chat/jest-presets@workspace:packages/jest-presets, @rocket.chat/jest-presets@workspace:~":
"@rocket.chat/jest-presets@workspace:*, @rocket.chat/jest-presets@workspace:^, @rocket.chat/jest-presets@workspace:packages/jest-presets, @rocket.chat/jest-presets@workspace:~":
version: 0.0.0-use.local
resolution: "@rocket.chat/jest-presets@workspace:packages/jest-presets"
dependencies:
Expand Down
Loading