fix: DDP method calls returning 404 for void methods (#40057)

Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
pull/40103/head
Abhinav Kumar 2 months ago committed by GitHub
parent a6c863a35d
commit eb78ae4e4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/empty-spiders-vanish.md
  2. 7
      ee/apps/ddp-streamer/jest.config.ts
  3. 6
      ee/apps/ddp-streamer/package.json
  4. 108
      ee/apps/ddp-streamer/src/Server.spec.ts
  5. 6
      ee/apps/ddp-streamer/src/Server.ts
  6. 3
      ee/apps/ddp-streamer/tsconfig.json
  7. 5
      yarn.lock

@ -0,0 +1,5 @@
---
'@rocket.chat/ddp-streamer': patch
---
Fixes an issue where some DDP method calls could incorrectly return a 404 error.

@ -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;

@ -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": {
@ -50,7 +51,9 @@
"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",
@ -58,6 +61,7 @@
"@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"

@ -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();
});
});
});

@ -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);

@ -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"]

@ -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:^"
@ -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"
@ -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"
@ -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:

Loading…
Cancel
Save