snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
133
packages/ddp-client/__tests__/Account.spec.ts
Normal file
133
packages/ddp-client/__tests__/Account.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import WS from 'jest-websocket-mock';
|
||||
|
||||
import { handleConnection, handleMethod } from './helpers';
|
||||
import { DDPSDK } from '../src/DDPSDK';
|
||||
|
||||
let server: WS;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.close();
|
||||
WS.clean();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should save credentials to user object - loginWithToken', async () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const messageResult = {
|
||||
id: 123,
|
||||
token: 'token',
|
||||
tokenExpires: { $date: 99999999 },
|
||||
};
|
||||
|
||||
await handleMethod(server, 'login', [{ resume: 'token' }], JSON.stringify(messageResult), sdk.account.loginWithToken('token'));
|
||||
|
||||
const { user } = sdk.account;
|
||||
expect(user?.token).toBe(messageResult.token);
|
||||
expect((user?.tokenExpires as Date)?.toISOString()).toBe(new Date(messageResult.tokenExpires.$date).toISOString());
|
||||
expect(user?.id).toBe(messageResult.id);
|
||||
});
|
||||
|
||||
it('should save credentials to user object - loginWithPassword', async () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const messageResult = {
|
||||
id: 123,
|
||||
token: 'token',
|
||||
tokenExpires: { $date: 99999999 },
|
||||
};
|
||||
|
||||
await handleMethod(
|
||||
server,
|
||||
'login',
|
||||
[{ user: { username: 'username' }, password: { digest: 'password', algorithm: 'sha-256' } }],
|
||||
JSON.stringify(messageResult),
|
||||
sdk.account.loginWithPassword('username', 'password'),
|
||||
);
|
||||
|
||||
const { user } = sdk.account;
|
||||
expect(user?.token).toBe(messageResult.token);
|
||||
expect((user?.tokenExpires as Date)?.toISOString()).toBe(new Date(messageResult.tokenExpires.$date).toISOString());
|
||||
expect(user?.id).toBe(messageResult.id);
|
||||
});
|
||||
|
||||
it('should logout', async () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const loginResult = {
|
||||
id: '123',
|
||||
token: 'token',
|
||||
tokenExpires: { $date: 99999999 },
|
||||
};
|
||||
|
||||
await handleMethod(server, 'login', [{ resume: 'token' }], JSON.stringify(loginResult), sdk.account.loginWithToken('token'));
|
||||
|
||||
const cb = jest.fn();
|
||||
sdk.account.on('uid', cb);
|
||||
|
||||
await handleMethod(server, 'logout', [], '{}', sdk.account.logout());
|
||||
|
||||
// uid setter only fires on transitions, so logging out from a logged-in
|
||||
// state emits exactly once (string → undefined).
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenCalledWith(undefined);
|
||||
|
||||
const { user } = sdk.account;
|
||||
expect(user).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should fire onLogin / onLogout on uid transitions only', async () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const onLogin = jest.fn();
|
||||
const onLogout = jest.fn();
|
||||
sdk.account.onLogin(onLogin);
|
||||
sdk.account.onLogout(onLogout);
|
||||
|
||||
const loginResult = {
|
||||
id: '123',
|
||||
token: 'token',
|
||||
tokenExpires: { $date: 99999999 },
|
||||
};
|
||||
await handleMethod(server, 'login', [{ resume: 'token' }], JSON.stringify(loginResult), sdk.account.loginWithToken('token'));
|
||||
|
||||
expect(onLogin).toHaveBeenCalledTimes(1);
|
||||
expect(onLogout).not.toHaveBeenCalled();
|
||||
|
||||
await handleMethod(server, 'logout', [], '{}', sdk.account.logout());
|
||||
|
||||
expect(onLogin).toHaveBeenCalledTimes(1);
|
||||
expect(onLogout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fan out emailVerificationLink and pageLoadLogin events', () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
const verify = jest.fn();
|
||||
const pageLoad = jest.fn();
|
||||
const stopVerify = sdk.account.onEmailVerificationLink(verify);
|
||||
sdk.account.onPageLoadLogin(pageLoad);
|
||||
|
||||
sdk.account.emit('emailVerificationLink', 'tok-1');
|
||||
sdk.account.emit('pageLoadLogin', { error: 'totp-required' });
|
||||
|
||||
expect(verify).toHaveBeenCalledWith('tok-1');
|
||||
expect(pageLoad).toHaveBeenCalledWith({ error: 'totp-required' });
|
||||
|
||||
stopVerify();
|
||||
sdk.account.emit('emailVerificationLink', 'tok-2');
|
||||
expect(verify).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
275
packages/ddp-client/__tests__/ClientStream.spec.ts
Normal file
275
packages/ddp-client/__tests__/ClientStream.spec.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { ClientStreamImpl } from '../src/ClientStream';
|
||||
import { DDPDispatcher } from '../src/DDPDispatcher';
|
||||
import { MinimalDDPClient } from '../src/MinimalDDPClient';
|
||||
|
||||
describe('call procedures', () => {
|
||||
it('should be able to call a method and receive a result', async () => {
|
||||
const callback = jest.fn();
|
||||
const ws = new DDPDispatcher();
|
||||
const client = new ClientStreamImpl(ws);
|
||||
|
||||
const id = client.call('test', callback);
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
result: ['arg1', 'arg2'],
|
||||
id,
|
||||
}),
|
||||
);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback).toHaveBeenCalledWith(null, ['arg1', 'arg2']);
|
||||
});
|
||||
|
||||
it('should be able to handle errors thrown by the method call', async () => {
|
||||
const callback = jest.fn();
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const id = client.call('test', callback);
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
error: {
|
||||
error: 400,
|
||||
reason: 'Bad Request',
|
||||
message: 'Bad Request [400]',
|
||||
errorType: 'Meteor.Error',
|
||||
},
|
||||
id,
|
||||
}),
|
||||
);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
error: 400,
|
||||
reason: 'Bad Request',
|
||||
message: 'Bad Request [400]',
|
||||
errorType: 'Meteor.Error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to callAsync a method and receive a result', async () => {
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const promise = client.callAsync('test');
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
result: ['arg1', 'arg2'],
|
||||
id: promise.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toEqual(['arg1', 'arg2']);
|
||||
});
|
||||
|
||||
it('should be able to callAsync a method and receive an error', async () => {
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const promise = client.callAsync('test');
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
error: {
|
||||
error: 400,
|
||||
reason: 'Bad Request',
|
||||
message: 'Bad Request [400]',
|
||||
errorType: 'Meteor.Error',
|
||||
},
|
||||
id: promise.id,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(promise).rejects.toEqual({
|
||||
error: 400,
|
||||
reason: 'Bad Request',
|
||||
message: 'Bad Request [400]',
|
||||
errorType: 'Meteor.Error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should only call the further methods after the previous one has been resolved respecting the wait option', async () => {
|
||||
const fn = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
const ws = new DDPDispatcher();
|
||||
const client = new ClientStreamImpl(ws);
|
||||
|
||||
client.dispatcher.on('send', dispatch);
|
||||
|
||||
const call = client.callWithOptions('wait 1', { wait: true }, fn);
|
||||
|
||||
expect(client.dispatcher.queue.length).toBe(1);
|
||||
|
||||
const callNoWait = client.call('no wait', fn);
|
||||
|
||||
expect(client.dispatcher.queue.length).toBe(2);
|
||||
|
||||
const call2 = client.callWithOptions('wait 2', { wait: true }, fn);
|
||||
|
||||
expect(client.dispatcher.queue.length).toBe(3);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(0);
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
result: ['arg1', 'arg2'],
|
||||
id: call,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(3);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
result: ['arg1', 'arg2'],
|
||||
id: call2,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
result: ['arg1', 'arg2'],
|
||||
id: callNoWait,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe procedures', () => {
|
||||
it('should be able to subscribe to a collection and receive a result', async () => {
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const subscription = client.subscribe('test');
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [subscription.id],
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(subscription.ready()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should be able to subscribe to a collection and receive an error', async () => {
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const subscription = client.subscribe('test');
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'nosub',
|
||||
error: {
|
||||
error: 400,
|
||||
reason: 'Bad Request',
|
||||
message: 'Bad Request [400]',
|
||||
errorType: 'Meteor.Error',
|
||||
},
|
||||
id: subscription.id,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(subscription.ready()).rejects.toEqual({
|
||||
error: 400,
|
||||
reason: 'Bad Request',
|
||||
message: 'Bad Request [400]',
|
||||
errorType: 'Meteor.Error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to unsubscribe from a collection', async () => {
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const subscription = client.subscribe('test');
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [subscription.id],
|
||||
}),
|
||||
);
|
||||
await expect(subscription.ready()).resolves.toBeUndefined();
|
||||
const unsubPromise = client.unsubscribe(subscription.id);
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'nosub',
|
||||
id: subscription.id,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(unsubPromise).resolves.toEqual({
|
||||
msg: 'nosub',
|
||||
id: subscription.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should subscribe to a collection and receive values through the observer', async () => {
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const promise = client.subscribe('test');
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [promise.id],
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(promise.ready()).resolves.toBeUndefined();
|
||||
|
||||
const observer = jest.fn();
|
||||
|
||||
client.onCollection('test', observer);
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'added',
|
||||
collection: 'test',
|
||||
id: '1',
|
||||
fields: {
|
||||
name: 'test',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(observer).toHaveBeenCalledWith({
|
||||
msg: 'added',
|
||||
collection: 'test',
|
||||
id: '1',
|
||||
fields: {
|
||||
name: 'test',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('connect procedure', () => {
|
||||
it('should be able to connect to a server', async () => {
|
||||
const ws = new MinimalDDPClient(() => undefined);
|
||||
const client = new ClientStreamImpl(ws);
|
||||
const promise = client.connect();
|
||||
|
||||
ws.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'connected',
|
||||
session: 'test',
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(promise).resolves.toEqual({
|
||||
msg: 'connected',
|
||||
session: 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
370
packages/ddp-client/__tests__/Connection.spec.ts
Normal file
370
packages/ddp-client/__tests__/Connection.spec.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import WS from 'jest-websocket-mock';
|
||||
|
||||
import { handleConnection, handleConnectionAndRejects, handleMethod } from './helpers';
|
||||
import { ConnectionImpl } from '../src/Connection';
|
||||
import { MinimalDDPClient } from '../src/MinimalDDPClient';
|
||||
|
||||
let server: WS;
|
||||
let connection: ConnectionImpl;
|
||||
beforeEach(() => {
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
connection?.close();
|
||||
server.close();
|
||||
WS.clean();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should connect', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
connection = new ConnectionImpl('ws://localhost:1234', WebSocket as any, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
expect(connection.status).toBe('idle');
|
||||
expect(connection.session).toBeUndefined();
|
||||
await handleConnection(server, connection.connect());
|
||||
|
||||
expect(connection.session).toBe('session');
|
||||
expect(connection.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('should handle a failing connection', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
connection = new ConnectionImpl('ws://localhost:1234', WebSocket as any, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
expect(connection.status).toBe('idle');
|
||||
expect(connection.session).toBeUndefined();
|
||||
|
||||
await expect(handleConnectionAndRejects(server, connection.connect())).rejects.toBe('1');
|
||||
|
||||
expect(connection.session).toBeUndefined();
|
||||
expect(connection.status).toBe('failed');
|
||||
});
|
||||
|
||||
it('should trigger a disconnect callback', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
expect(connection.status).toBe('idle');
|
||||
expect(connection.session).toBeUndefined();
|
||||
const disconnectCallback = jest.fn();
|
||||
connection.on('connection', disconnectCallback);
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
|
||||
expect(disconnectCallback).toHaveBeenNthCalledWith(1, 'connecting');
|
||||
expect(disconnectCallback).toHaveBeenNthCalledWith(2, 'connected');
|
||||
expect(disconnectCallback).toHaveBeenCalledTimes(2);
|
||||
server.close();
|
||||
expect(disconnectCallback).toHaveBeenCalledTimes(3);
|
||||
expect(disconnectCallback).toHaveBeenNthCalledWith(3, 'disconnected');
|
||||
expect(connection.status).toBe('disconnected');
|
||||
});
|
||||
|
||||
it('should handle the close method', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, {
|
||||
retryCount: 0,
|
||||
retryTime: 0,
|
||||
});
|
||||
|
||||
server.nextMessage.then((message) => {
|
||||
expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}');
|
||||
server.send('{"msg":"connected","session":"123"}');
|
||||
});
|
||||
|
||||
expect(connection.status).toBe('idle');
|
||||
expect(connection.session).toBeUndefined();
|
||||
|
||||
await expect(connection.connect()).resolves.toBe(true);
|
||||
|
||||
expect(connection.session).toBe('123');
|
||||
expect(connection.status).toBe('connected');
|
||||
|
||||
connection.close();
|
||||
|
||||
expect(connection.status).toBe('closed');
|
||||
});
|
||||
|
||||
it('should handle reconnecting', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', WebSocket, client, { retryCount: 1, retryTime: 100 });
|
||||
|
||||
expect(connection.status).toBe('idle');
|
||||
expect(connection.session).toBeUndefined();
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
|
||||
expect(connection.session).toBe('session');
|
||||
expect(connection.status).toBe('connected');
|
||||
|
||||
// Fake timers are used to avoid waiting for the reconnect timeout
|
||||
jest.useFakeTimers();
|
||||
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
expect(connection.status).toBe('disconnected');
|
||||
|
||||
await handleConnection(
|
||||
server,
|
||||
jest.advanceTimersByTimeAsync(200),
|
||||
new Promise((resolve) => connection.once('reconnecting', () => resolve(undefined))),
|
||||
new Promise((resolve) => {
|
||||
const handler = (status: string) => {
|
||||
if (status === 'connected') {
|
||||
connection.off('connection', handler);
|
||||
resolve(status);
|
||||
}
|
||||
};
|
||||
connection.on('connection', handler);
|
||||
}),
|
||||
);
|
||||
|
||||
expect(connection.status).toBe('connected');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should queue messages if the connection is not ready', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
|
||||
connection.close();
|
||||
|
||||
expect(connection.status).toBe('closed');
|
||||
|
||||
client.emit('send', { msg: 'method', method: 'method', params: ['arg1', 'arg2'], id: '1' });
|
||||
|
||||
expect(connection.queue.size).toBe(1);
|
||||
|
||||
await handleConnection(server, connection.reconnect());
|
||||
|
||||
expect(connection.queue.size).toBe(0);
|
||||
|
||||
await handleMethod(server, 'method', ['arg1', 'arg2'], '1');
|
||||
});
|
||||
|
||||
it('should be idempotent if reconnect is called while already connected', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
|
||||
// Previous behavior was to throw "Connection in progress" — the consumer's
|
||||
// `void this.reconnect()` paths (notably the ws.onclose retry timer)
|
||||
// surfaced that as an unhandled rejection / pageError. Now a redundant
|
||||
// reconnect is just a no-op resolving with the current state.
|
||||
await expect(connection.reconnect()).resolves.toBe(true);
|
||||
expect(connection.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('should be idempotent if connect is called while already connected', async () => {
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
|
||||
await expect(connection.connect()).resolves.toBe(true);
|
||||
expect(connection.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('should share the in-flight connect promise with a concurrent connect() caller', async () => {
|
||||
// Regression: while status === 'connecting', a second connect() used to
|
||||
// receive Promise.resolve(true) immediately, hiding any subsequent
|
||||
// 'failed' payload from the in-flight handshake. Both callers must now
|
||||
// observe the real outcome.
|
||||
const client = new MinimalDDPClient();
|
||||
connection = new ConnectionImpl('ws://localhost:1234', WebSocket as any, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
const first = connection.connect();
|
||||
expect(connection.status).toBe('connecting');
|
||||
|
||||
const second = connection.connect();
|
||||
expect(second).toBe(first);
|
||||
|
||||
await expect(handleConnectionAndRejects(server, first, second)).rejects.toBe('1');
|
||||
expect(connection.status).toBe('failed');
|
||||
});
|
||||
|
||||
it('should share the in-flight connect promise with a concurrent reconnect() caller', async () => {
|
||||
// Same as above for the reconnect() entry point — the ws.onclose retry
|
||||
// timer fires `reconnect()` and must piggyback on any handshake the
|
||||
// consumer's bootstrap path already started.
|
||||
const client = new MinimalDDPClient();
|
||||
connection = new ConnectionImpl('ws://localhost:1234', WebSocket as any, client, { retryCount: 0, retryTime: 0 });
|
||||
|
||||
const first = connection.connect();
|
||||
expect(connection.status).toBe('connecting');
|
||||
|
||||
const second = connection.reconnect();
|
||||
expect(second).toBe(first);
|
||||
|
||||
await handleConnection(server, first, second);
|
||||
expect(connection.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('should not surface the retry timer rejection when an external connect won the race', async () => {
|
||||
// Regression: ws.onclose schedules a `void this.reconnect()` timer; if the
|
||||
// consumer (e.g. ddpSdk.ts startConnect) opens a fresh socket before that
|
||||
// timer fires, the timer used to reject with "Connection in progress" and,
|
||||
// because of the leading `void`, the rejection became an unhandled
|
||||
// rejection on the page. The timer must now no-op silently when the
|
||||
// connection has already been re-established.
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', WebSocket, client, { retryCount: 1, retryTime: 100 });
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
expect(connection.status).toBe('connected');
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
expect(connection.status).toBe('disconnected');
|
||||
|
||||
// Track unhandled rejections on the timer's promise.
|
||||
const unhandled = jest.fn();
|
||||
process.on('unhandledRejection', unhandled);
|
||||
|
||||
// External code opens a new connection BEFORE the retry timer fires.
|
||||
const externalConnect = handleConnection(server, connection.connect());
|
||||
|
||||
// Run the timer.
|
||||
await jest.advanceTimersByTimeAsync(200);
|
||||
await externalConnect;
|
||||
|
||||
// Drain any microtasks the timer might have queued.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(connection.status).toBe('connected');
|
||||
expect(unhandled).not.toHaveBeenCalled();
|
||||
process.off('unhandledRejection', unhandled);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should reset retryCount on a successful connection so subsequent drops can retry', async () => {
|
||||
// Regression: retryCount was only ever incremented on disconnect, never
|
||||
// zeroed on successful (re)connect. With default retryCount=1 budget, a
|
||||
// single force-logout cycle (server close → SDK reconnects → app calls
|
||||
// `Meteor.logout()` → server's logout handler closes WS again) drained the
|
||||
// budget, and the second close left the SDK permanently disconnected.
|
||||
// Method frames queued on the SDK during that window stayed queued
|
||||
// forever — observed in e2e-encryption/e2ee-passphrase-management as the
|
||||
// next loginByUserState login frame never being delivered.
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', WebSocket, client, { retryCount: 1, retryTime: 100 });
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
expect(connection.status).toBe('connected');
|
||||
expect((connection as unknown as { retryCount: number }).retryCount).toBe(0);
|
||||
|
||||
// First disconnect → schedules retry (retryCount: 0 → 1).
|
||||
jest.useFakeTimers();
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
expect(connection.status).toBe('disconnected');
|
||||
|
||||
await handleConnection(
|
||||
server,
|
||||
jest.advanceTimersByTimeAsync(200),
|
||||
new Promise((resolve) => connection.once('reconnecting', () => resolve(undefined))),
|
||||
new Promise((resolve) => {
|
||||
const handler = (status: string) => {
|
||||
if (status === 'connected') {
|
||||
connection.off('connection', handler);
|
||||
resolve(status);
|
||||
}
|
||||
};
|
||||
connection.on('connection', handler);
|
||||
}),
|
||||
);
|
||||
jest.useRealTimers();
|
||||
expect(connection.status).toBe('connected');
|
||||
|
||||
// Successful reconnect must zero the retry budget.
|
||||
expect((connection as unknown as { retryCount: number }).retryCount).toBe(0);
|
||||
|
||||
// Second disconnect should still schedule a retry now that the budget reset.
|
||||
jest.useFakeTimers();
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
await handleConnection(
|
||||
server,
|
||||
jest.advanceTimersByTimeAsync(200),
|
||||
new Promise((resolve) => connection.once('reconnecting', () => resolve(undefined))),
|
||||
new Promise((resolve) => {
|
||||
const handler = (status: string) => {
|
||||
if (status === 'connected') {
|
||||
connection.off('connection', handler);
|
||||
resolve(status);
|
||||
}
|
||||
};
|
||||
connection.on('connection', handler);
|
||||
}),
|
||||
);
|
||||
jest.useRealTimers();
|
||||
expect(connection.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('should ignore a stale ws.onclose that fires after the socket has been replaced', async () => {
|
||||
// Regression: ws.onclose handlers were closed over the original ws but
|
||||
// mutated `this.status`/`this.retryCount` unconditionally. If a late close
|
||||
// event from an old socket arrives after a new socket is connected, the
|
||||
// handler would flip status back to 'disconnected' and schedule another
|
||||
// retry timer.
|
||||
const client = new MinimalDDPClient();
|
||||
connection = ConnectionImpl.create('ws://localhost:1234', WebSocket, client, { retryCount: 1, retryTime: 100 });
|
||||
|
||||
await handleConnection(server, connection.connect());
|
||||
const firstWs = (connection as unknown as { ws: WebSocket }).ws;
|
||||
expect(connection.status).toBe('connected');
|
||||
|
||||
jest.useFakeTimers();
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
expect(connection.status).toBe('disconnected');
|
||||
|
||||
await handleConnection(
|
||||
server,
|
||||
jest.advanceTimersByTimeAsync(200),
|
||||
new Promise((resolve) => connection.once('reconnecting', () => resolve(undefined))),
|
||||
new Promise((resolve) => {
|
||||
const handler = (status: string) => {
|
||||
if (status === 'connected') {
|
||||
connection.off('connection', handler);
|
||||
resolve(status);
|
||||
}
|
||||
};
|
||||
connection.on('connection', handler);
|
||||
}),
|
||||
);
|
||||
|
||||
expect(connection.status).toBe('connected');
|
||||
jest.useRealTimers();
|
||||
const secondWs = (connection as unknown as { ws: WebSocket }).ws;
|
||||
expect(secondWs).not.toBe(firstWs);
|
||||
|
||||
const statusBefore = connection.status;
|
||||
const retryBefore = (connection as unknown as { retryCount: number }).retryCount;
|
||||
|
||||
// Synthesize a late `close` event on the original socket — the handler
|
||||
// must short-circuit because `this.ws !== ws` for the closed-over ws.
|
||||
(firstWs as unknown as { onclose?: () => void }).onclose?.();
|
||||
|
||||
expect(connection.status).toBe(statusBefore);
|
||||
expect((connection as unknown as { retryCount: number }).retryCount).toBe(retryBefore);
|
||||
});
|
||||
149
packages/ddp-client/__tests__/DDPDispatcher.spec.ts
Normal file
149
packages/ddp-client/__tests__/DDPDispatcher.spec.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { DDPDispatcher } from '../src/DDPDispatcher';
|
||||
import { MinimalDDPClient } from '../src/MinimalDDPClient';
|
||||
|
||||
const ddp = new MinimalDDPClient();
|
||||
|
||||
it('should create a block properly', () => {
|
||||
const ddpDispatcher = new DDPDispatcher();
|
||||
|
||||
const test = ddp.call('test');
|
||||
|
||||
ddpDispatcher.wait(test);
|
||||
expect(ddpDispatcher.queue).toEqual([
|
||||
{
|
||||
wait: true,
|
||||
items: [test],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should push an item to the block properly', () => {
|
||||
const ddpDispatcher = new DDPDispatcher();
|
||||
|
||||
const test1 = ddp.call('test1');
|
||||
const test2 = ddp.call('test2');
|
||||
ddpDispatcher.wait(test1);
|
||||
ddpDispatcher.dispatch(test2);
|
||||
expect(ddpDispatcher.queue).toEqual([
|
||||
{
|
||||
wait: true,
|
||||
items: [test1],
|
||||
},
|
||||
{
|
||||
wait: false,
|
||||
items: [test2],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not keep in the queue if the method doesnt wait', () => {
|
||||
const ddpDispatcher = new DDPDispatcher();
|
||||
|
||||
const blockToRemove = ddp.call('test1');
|
||||
const test2 = ddp.call('test2');
|
||||
|
||||
ddpDispatcher.dispatch(blockToRemove, { wait: true });
|
||||
ddpDispatcher.dispatch(test2, { wait: false });
|
||||
|
||||
expect(ddpDispatcher.queue).toEqual([
|
||||
{
|
||||
wait: true,
|
||||
items: [blockToRemove],
|
||||
},
|
||||
{
|
||||
wait: false,
|
||||
items: [test2],
|
||||
},
|
||||
]);
|
||||
|
||||
ddpDispatcher.removeItem(blockToRemove);
|
||||
|
||||
expect(ddpDispatcher.queue).toEqual([]);
|
||||
});
|
||||
|
||||
it('should send outstanding blocks if there is no block waiting and item is added', () => {
|
||||
const fn = jest.fn();
|
||||
|
||||
const ddpDispatcher = new DDPDispatcher();
|
||||
ddpDispatcher.on('send', fn);
|
||||
|
||||
ddpDispatcher.dispatch(ddp.call('test1'));
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('emits non-method payloads immediately, even when a wait block is at the head', () => {
|
||||
// Regression: a connect frame dispatched while a wait `login` method is
|
||||
// queued must still reach the server. Otherwise the DDP handshake never
|
||||
// completes and the socket wedges open but unconnected.
|
||||
const fn = jest.fn();
|
||||
const ddpDispatcher = new DDPDispatcher();
|
||||
ddpDispatcher.on('send', fn);
|
||||
|
||||
const login = ddp.call('login');
|
||||
ddpDispatcher.dispatch(login, { wait: true });
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(fn).toHaveBeenNthCalledWith(1, login);
|
||||
|
||||
const connectPayload = { msg: 'connect' as const, version: '1', support: ['1'] };
|
||||
ddpDispatcher.dispatch(connectPayload);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(fn).toHaveBeenNthCalledWith(2, connectPayload);
|
||||
|
||||
const subPayload = { msg: 'sub' as const, id: 'a', name: 'foo', params: [] };
|
||||
ddpDispatcher.dispatch(subPayload);
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
expect(fn).toHaveBeenNthCalledWith(3, subPayload);
|
||||
|
||||
// Wait block remains pending — only the wait method is queued, the
|
||||
// non-method frames bypassed it.
|
||||
expect(ddpDispatcher.queue).toEqual([{ wait: true, items: [login] }]);
|
||||
});
|
||||
|
||||
it('should send the next blocks if the outstanding block was completed', () => {
|
||||
const fn = jest.fn();
|
||||
|
||||
const ddpDispatcher = new DDPDispatcher();
|
||||
ddpDispatcher.on('send', fn);
|
||||
|
||||
const block1 = ddp.call('block1');
|
||||
|
||||
const block2 = ddp.call('block2');
|
||||
|
||||
const block3 = ddp.call('test2');
|
||||
|
||||
ddpDispatcher.dispatch(block1, { wait: true });
|
||||
ddpDispatcher.dispatch(block2, { wait: true });
|
||||
|
||||
ddpDispatcher.dispatch(block3);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
ddpDispatcher.removeItem(block1);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(ddpDispatcher.queue.length).toBe(2);
|
||||
|
||||
expect(ddpDispatcher.queue).toEqual([
|
||||
{
|
||||
wait: true,
|
||||
items: [block2],
|
||||
},
|
||||
{
|
||||
wait: false,
|
||||
items: [block3],
|
||||
},
|
||||
]);
|
||||
|
||||
ddpDispatcher.removeItem(block2);
|
||||
|
||||
expect(ddpDispatcher.queue.length).toBe(0);
|
||||
|
||||
expect(ddpDispatcher.queue).toEqual([]);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
expect(fn).toHaveBeenNthCalledWith(1, block1);
|
||||
expect(fn).toHaveBeenNthCalledWith(2, block2);
|
||||
expect(fn).toHaveBeenNthCalledWith(3, block3);
|
||||
});
|
||||
310
packages/ddp-client/__tests__/DDPSDK.spec.ts
Normal file
310
packages/ddp-client/__tests__/DDPSDK.spec.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import util from 'node:util';
|
||||
|
||||
import WS from 'jest-websocket-mock';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
import { fireStreamChange, fireStreamAdded, fireStreamRemove, handleConnection, handleSubscription, handleMethod } from './helpers';
|
||||
import { DDPSDK } from '../src/DDPSDK';
|
||||
|
||||
(global as any).WebSocket = (global as any).WebSocket || WebSocket;
|
||||
|
||||
export let server: WS;
|
||||
|
||||
const callXTimes = <F extends (...args: any) => any>(fn: F, times: number): F => {
|
||||
return (async (...args) => {
|
||||
const methods = [].concat(...Array(times));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const _ of methods) {
|
||||
await fn(...args);
|
||||
}
|
||||
}) as F;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.close();
|
||||
WS.clean();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should handle a stream of messages', async () => {
|
||||
const cb = jest.fn();
|
||||
|
||||
const streamName = 'stream';
|
||||
const streamParams = '123';
|
||||
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const stream = sdk.stream(streamName, streamParams, cb);
|
||||
|
||||
await handleSubscription(server, stream.id, streamName, streamParams);
|
||||
|
||||
await stream.ready();
|
||||
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(3);
|
||||
|
||||
fireStreamChange(server, streamName, `${streamParams}-another`);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(cb).toHaveBeenNthCalledWith(1, 1);
|
||||
sdk.connection.close();
|
||||
});
|
||||
|
||||
it('should ignore messages other from changed', async () => {
|
||||
const cb = jest.fn();
|
||||
|
||||
const streamName = 'stream';
|
||||
const streamParams = '123';
|
||||
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const stream = sdk.stream(streamName, streamParams, cb);
|
||||
|
||||
await handleSubscription(server, stream.id, streamName, streamParams);
|
||||
|
||||
await stream.ready();
|
||||
|
||||
fireStreamAdded(server, streamName, streamParams);
|
||||
|
||||
fireStreamRemove(server, streamName, streamParams);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(0);
|
||||
sdk.connection.close();
|
||||
});
|
||||
|
||||
it('should handle streams after reconnect', async () => {
|
||||
const cb = jest.fn();
|
||||
|
||||
const streamName = 'stream';
|
||||
const streamParams = '123';
|
||||
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const result = sdk.stream(streamName, streamParams, cb);
|
||||
|
||||
expect(result.isReady).toBe(false);
|
||||
|
||||
expect(sdk.client.subscriptions.size).toBe(1);
|
||||
|
||||
await handleSubscription(server, result.id, streamName, streamParams);
|
||||
|
||||
await result.ready();
|
||||
|
||||
const fire = callXTimes(fireStreamChange, 3);
|
||||
|
||||
await fire(server, streamName, streamParams);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Fake timers are used to avoid waiting for the reconnect timeout
|
||||
jest.useFakeTimers();
|
||||
server.close();
|
||||
WS.clean();
|
||||
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
|
||||
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
|
||||
const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));
|
||||
await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);
|
||||
|
||||
// the client should reconnect and resubscribe
|
||||
await Promise.all([handleSubscription(server, result.id, streamName, streamParams), jest.advanceTimersByTimeAsync(1000)]);
|
||||
|
||||
fire(server, streamName, streamParams);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(6);
|
||||
|
||||
jest.useRealTimers();
|
||||
sdk.connection.close();
|
||||
});
|
||||
|
||||
it('should handle an unsubscribe stream after reconnect', async () => {
|
||||
const cb = jest.fn();
|
||||
|
||||
const streamName = 'stream';
|
||||
const streamParams = '123';
|
||||
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const subscription = sdk.stream(streamName, streamParams, cb);
|
||||
|
||||
expect(subscription.isReady).toBe(false);
|
||||
|
||||
expect(sdk.client.subscriptions.size).toBe(1);
|
||||
|
||||
await handleSubscription(server, subscription.id, streamName, streamParams);
|
||||
|
||||
await expect(subscription.ready()).resolves.toBe(undefined);
|
||||
|
||||
expect(subscription.isReady).toBe(true);
|
||||
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Fake timers are used to avoid waiting for the reconnect timeout
|
||||
jest.useFakeTimers();
|
||||
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
|
||||
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
|
||||
const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));
|
||||
await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);
|
||||
|
||||
await handleSubscription(server, subscription.id, streamName, streamParams);
|
||||
|
||||
expect(subscription.isReady).toBe(true);
|
||||
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
|
||||
subscription.stop();
|
||||
|
||||
expect(sdk.client.subscriptions.size).toBe(0);
|
||||
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
fireStreamChange(server, streamName, streamParams);
|
||||
jest.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(4);
|
||||
|
||||
expect(sdk.client.subscriptions.size).toBe(0);
|
||||
jest.useRealTimers();
|
||||
sdk.connection.close();
|
||||
sdk.connection.close();
|
||||
});
|
||||
|
||||
it('should create and connect to a stream', async () => {
|
||||
const promise = DDPSDK.createAndConnect('ws://localhost:1234');
|
||||
await handleConnection(server, promise);
|
||||
const sdk = await promise;
|
||||
sdk.connection.close();
|
||||
});
|
||||
|
||||
it.skip('should try to loginWithToken after reconnection', async () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const messageResult = {
|
||||
id: 123,
|
||||
token: 'token1234',
|
||||
tokenExpires: { $date: 99999999 },
|
||||
};
|
||||
|
||||
const { loginWithToken } = sdk.account;
|
||||
const loginFn = jest.fn((token: string) => loginWithToken.apply(sdk.account, [token]));
|
||||
sdk.account.loginWithToken = loginFn;
|
||||
|
||||
await handleMethod(server, 'login', [{ resume: 'token' }], JSON.stringify(messageResult), sdk.account.loginWithToken('token'));
|
||||
|
||||
expect(sdk.account.user?.token).toBe(messageResult.token);
|
||||
expect(loginFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Fake timers are used to avoid waiting for the reconnect timeout
|
||||
jest.useFakeTimers();
|
||||
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
|
||||
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
|
||||
const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));
|
||||
await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);
|
||||
|
||||
jest.advanceTimersByTimeAsync(1000);
|
||||
expect(loginFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
jest.useRealTimers();
|
||||
sdk.connection.close();
|
||||
});
|
||||
|
||||
describe('Method call and Disconnection cases', () => {
|
||||
it('should handle properly if the message was sent after disconnection', async () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const [result] = await handleMethod(server, 'method', ['args1'], '1', sdk.client.callAsync('method', 'args1'));
|
||||
|
||||
expect(result).toBe(1);
|
||||
// Fake timers are used to avoid waiting for the reconnect timeout
|
||||
jest.useFakeTimers();
|
||||
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
|
||||
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
|
||||
const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));
|
||||
|
||||
const callResult = sdk.client.callAsync('method', 'args2');
|
||||
|
||||
expect(util.inspect(callResult).includes('pending')).toBe(true);
|
||||
|
||||
await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);
|
||||
|
||||
const [result2] = await handleMethod(server, 'method', ['args2'], '1', callResult);
|
||||
|
||||
expect(util.inspect(callResult).includes('pending')).toBe(false);
|
||||
expect(result2).toBe(1);
|
||||
sdk.connection.close();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it.skip('should handle properly if the message was sent before disconnection but got disconnected before receiving the response', async () => {
|
||||
const sdk = DDPSDK.create('ws://localhost:1234');
|
||||
|
||||
await handleConnection(server, sdk.connection.connect());
|
||||
|
||||
const callResult = sdk.client.callAsync('method', 'args');
|
||||
|
||||
expect(util.inspect(callResult).includes('pending')).toBe(true);
|
||||
|
||||
// Fake timers are used to avoid waiting for the reconnect timeout
|
||||
jest.useFakeTimers();
|
||||
|
||||
server.close();
|
||||
WS.clean();
|
||||
server = new WS('ws://localhost:1234/websocket');
|
||||
|
||||
const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
|
||||
const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
|
||||
const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));
|
||||
|
||||
await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);
|
||||
|
||||
expect(util.inspect(callResult).includes('pending')).toBe(true);
|
||||
|
||||
const [result] = await handleMethod(server, 'method', ['args2'], '1', callResult);
|
||||
|
||||
expect(result).toBe(1);
|
||||
|
||||
jest.useRealTimers();
|
||||
sdk.connection.close();
|
||||
});
|
||||
});
|
||||
374
packages/ddp-client/__tests__/MinimalDDPClient.spec.ts
Normal file
374
packages/ddp-client/__tests__/MinimalDDPClient.spec.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import { MinimalDDPClient } from '../src/MinimalDDPClient';
|
||||
|
||||
describe('ping/pong mechanism', () => {
|
||||
it('should reply with pong if no id is provided', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
const senderCallback = jest.fn();
|
||||
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
client.once('message', handlerCallback);
|
||||
client.once('send', senderCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ping',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'ping',
|
||||
});
|
||||
|
||||
expect(senderCallback).toHaveBeenCalledTimes(1);
|
||||
expect(senderCallback).toHaveBeenCalledWith({
|
||||
msg: 'pong',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reply with pong if id is provided', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
const senderCallback = jest.fn();
|
||||
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
client.on('message', handlerCallback);
|
||||
client.on('send', senderCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ping',
|
||||
id: '123',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'ping',
|
||||
id: '123',
|
||||
});
|
||||
|
||||
expect(senderCallback).toHaveBeenCalledTimes(1);
|
||||
expect(senderCallback).toHaveBeenCalledWith({
|
||||
msg: 'pong',
|
||||
id: '123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit "pong" event if pong is received', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
client.once('pong', handlerCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'pong',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'pong',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscription mechanism', () => {
|
||||
it('should emit "ready" event after subscribe', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const id = client.subscribe('test');
|
||||
|
||||
client.onPublish(id, handlerCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit "nosub" event if something goes wrong', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const id = client.subscribe('test');
|
||||
|
||||
client.onPublish(id, handlerCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'nosub',
|
||||
id,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'nosub',
|
||||
id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit "nosub" event after unsubscribe', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
|
||||
const nosubCallback = jest.fn();
|
||||
|
||||
const client = new MinimalDDPClient();
|
||||
|
||||
const id = client.subscribe('test');
|
||||
|
||||
client.onPublish(id, handlerCallback);
|
||||
client.onNoSub(id, nosubCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
}),
|
||||
);
|
||||
|
||||
client.unsubscribe(id);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'nosub',
|
||||
id,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
});
|
||||
|
||||
expect(nosubCallback).toHaveBeenCalledTimes(1);
|
||||
expect(nosubCallback).toHaveBeenCalledWith({
|
||||
msg: 'nosub',
|
||||
id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscription added/changed/removed mechanism', () => {
|
||||
it('should receive "added" events after subscribe', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
const readyCallback = jest.fn();
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const id = client.subscribe('test');
|
||||
|
||||
client.onCollection('test', handlerCallback);
|
||||
|
||||
client.onPublish(id, readyCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
}),
|
||||
);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'added',
|
||||
collection: 'test',
|
||||
id,
|
||||
fields: {
|
||||
test: 'test',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(readyCallback).toHaveBeenCalledTimes(1);
|
||||
expect(readyCallback).toHaveBeenCalledWith({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
});
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'added',
|
||||
collection: 'test',
|
||||
id,
|
||||
fields: {
|
||||
test: 'test',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive "changed" events after subscribe', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
const readyCallback = jest.fn();
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const id = client.subscribe('test');
|
||||
|
||||
client.onCollection('test', handlerCallback);
|
||||
|
||||
client.onPublish(id, readyCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
}),
|
||||
);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'changed',
|
||||
collection: 'test',
|
||||
id,
|
||||
fields: {
|
||||
test: 'test',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(readyCallback).toHaveBeenCalledTimes(1);
|
||||
expect(readyCallback).toHaveBeenCalledWith({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
});
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'changed',
|
||||
collection: 'test',
|
||||
id,
|
||||
fields: {
|
||||
test: 'test',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive "removed" events after subscribe', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
const readyCallback = jest.fn();
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const id = client.subscribe('test');
|
||||
|
||||
client.onCollection('test', handlerCallback);
|
||||
|
||||
client.onPublish(id, readyCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
}),
|
||||
);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'removed',
|
||||
collection: 'test',
|
||||
id,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(readyCallback).toHaveBeenCalledTimes(1);
|
||||
expect(readyCallback).toHaveBeenCalledWith({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
});
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'removed',
|
||||
collection: 'test',
|
||||
id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscription updated mechanism', () => {
|
||||
it('should receive "updated" events after subscribe and before ready', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
const readyCallback = jest.fn();
|
||||
const updatedCallback = jest.fn();
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const id = client.subscribe('test');
|
||||
|
||||
client.onCollection(id, handlerCallback);
|
||||
client.onPublish(id, readyCallback);
|
||||
client.onUpdate(id, updatedCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'ready',
|
||||
subs: [id],
|
||||
}),
|
||||
);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'updated',
|
||||
methods: [id],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(readyCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(0);
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updatedCallback).toHaveBeenCalledWith({
|
||||
msg: 'updated',
|
||||
methods: [id],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('method mechanism', () => {
|
||||
it('should emit "result" event after method call', () => {
|
||||
const handlerCallback = jest.fn();
|
||||
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
const { id } = client.call('test');
|
||||
|
||||
client.onResult(id, handlerCallback);
|
||||
|
||||
client.handleMessage(
|
||||
JSON.stringify({
|
||||
msg: 'result',
|
||||
params: [],
|
||||
id,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handlerCallback).toHaveBeenCalledTimes(1);
|
||||
expect(handlerCallback).toHaveBeenCalledWith({
|
||||
msg: 'result',
|
||||
params: [],
|
||||
id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error mechanism', () => {
|
||||
it('should throw error if the payload is not valid', () => {
|
||||
const client = new MinimalDDPClient(() => undefined);
|
||||
|
||||
expect(() => {
|
||||
client.handleMessage('test');
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
64
packages/ddp-client/__tests__/Timeout.spec.ts
Normal file
64
packages/ddp-client/__tests__/Timeout.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { TimeoutControl } from '../src/TimeoutControl';
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(global, 'setTimeout');
|
||||
|
||||
it('should call the heartbeat and timeout callbacks respecting the informed time', async function () {
|
||||
const heartbeatCallback = jest.fn();
|
||||
const timeoutCallback = jest.fn();
|
||||
|
||||
const timeout = new TimeoutControl(100);
|
||||
timeout.reset();
|
||||
|
||||
expect(setTimeout).toHaveBeenCalledTimes(2);
|
||||
|
||||
timeout.on('heartbeat', heartbeatCallback);
|
||||
timeout.on('timeout', timeoutCallback);
|
||||
|
||||
// At this point in time, the callback should not have been called yet
|
||||
expect(heartbeatCallback).not.toHaveBeenCalled();
|
||||
expect(timeoutCallback).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(60);
|
||||
|
||||
expect(heartbeatCallback).toHaveBeenCalledTimes(1);
|
||||
expect(timeoutCallback).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(60);
|
||||
|
||||
expect(heartbeatCallback).toHaveBeenCalledTimes(1);
|
||||
expect(timeoutCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should never call the timeout callback if the reset method is called', async function () {
|
||||
const heartbeatCallback = jest.fn();
|
||||
const timeoutCallback = jest.fn();
|
||||
|
||||
const timeout = new TimeoutControl(100);
|
||||
timeout.reset();
|
||||
|
||||
expect(setTimeout).toHaveBeenCalledTimes(2);
|
||||
|
||||
timeout.on('heartbeat', heartbeatCallback);
|
||||
timeout.on('timeout', timeoutCallback);
|
||||
|
||||
// At this point in time, the callback should not have been called yet
|
||||
expect(heartbeatCallback).not.toHaveBeenCalled();
|
||||
expect(timeoutCallback).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(60);
|
||||
|
||||
expect(heartbeatCallback).toHaveBeenCalledTimes(1);
|
||||
expect(timeoutCallback).not.toHaveBeenCalled();
|
||||
|
||||
timeout.reset();
|
||||
|
||||
jest.advanceTimersByTime(60);
|
||||
|
||||
expect(heartbeatCallback).toHaveBeenCalledTimes(2);
|
||||
expect(timeoutCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
64
packages/ddp-client/__tests__/helpers/index.ts
Normal file
64
packages/ddp-client/__tests__/helpers/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type WS from 'jest-websocket-mock';
|
||||
|
||||
const acceptConnection = async (server: WS) => {
|
||||
await server.nextMessage.then(async (message) => {
|
||||
await expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}');
|
||||
server.send(`{"msg":"connected","session":"session"}`);
|
||||
});
|
||||
};
|
||||
export const handleConnection = async (server: WS, ...client: Promise<unknown>[]) => {
|
||||
await Promise.all([acceptConnection(server), ...client]);
|
||||
};
|
||||
|
||||
export const handleConnectionAndRejects = async (server: WS, ...client: Promise<unknown>[]) => {
|
||||
const suggestedVersion = '1';
|
||||
|
||||
return Promise.all([
|
||||
server.nextMessage.then((message) => {
|
||||
expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}');
|
||||
return server.send(`{"msg":"failed","version":"${suggestedVersion}"}`);
|
||||
}),
|
||||
...client,
|
||||
]);
|
||||
};
|
||||
|
||||
const handleConnectionButNoResponse = async (server: WS, method: string, params: string[]) => {
|
||||
return server.nextMessage.then(async (msg) => {
|
||||
if (typeof msg !== 'string') throw new Error('Expected message to be a string');
|
||||
const message = JSON.parse(msg);
|
||||
await expect(message).toMatchObject({
|
||||
msg: 'method',
|
||||
method,
|
||||
params,
|
||||
});
|
||||
return message;
|
||||
});
|
||||
};
|
||||
|
||||
export const handleMethod = async (server: WS, method: string, params: any[], responseResult: string, ...client: Promise<unknown>[]) => {
|
||||
const result = await handleConnectionButNoResponse(server, method, params);
|
||||
return Promise.all([server.send(`{"msg":"result","id":"${result.id}","result":${responseResult}}`), ...client]).then((result) => {
|
||||
result.shift();
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
export const handleSubscription = async (server: WS, id: string, streamName: string, streamParams: string) => {
|
||||
await server.nextMessage.then(async (message) => {
|
||||
await expect(message).toBe(
|
||||
`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}",{"useCollection":false,"args":[null]}]}`,
|
||||
);
|
||||
server.send(`{"msg":"ready","subs":["${id}"]}`);
|
||||
});
|
||||
};
|
||||
export const fireStream = (action: 'changed' | 'removed' | 'added') => {
|
||||
return (server: WS, streamName: string, streamParams: string) => {
|
||||
return server.send(
|
||||
`{"msg":"${action}","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const fireStreamChange = fireStream('changed');
|
||||
export const fireStreamRemove = fireStream('removed');
|
||||
export const fireStreamAdded = fireStream('added');
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Emitter } from '@rocket.chat/emitter';
|
||||
|
||||
import { wrapOnceEventIntoPromise } from '../src/wrapOnceEventIntoPromise';
|
||||
|
||||
it('should resolve', async () => {
|
||||
const emitter = new Emitter();
|
||||
const promise = wrapOnceEventIntoPromise(emitter, 'test');
|
||||
|
||||
emitter.emit('test', 'test');
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toBe('test');
|
||||
});
|
||||
|
||||
it('should reject', async () => {
|
||||
const emitter = new Emitter();
|
||||
const promise = wrapOnceEventIntoPromise(emitter, 'test');
|
||||
|
||||
emitter.emit('test', { error: 'test' });
|
||||
|
||||
await expect(promise).rejects.toBe('test');
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
Reference in New Issue
Block a user