Skip to content

Commit bfecdbf

Browse files
committed
✅(y-provider) add tests for y-provider server
We add jest tests for the y-provider server. The CI will be able to run the tests.
1 parent ba1cfc3 commit bfecdbf

File tree

10 files changed

+440
-17
lines changed

10 files changed

+440
-17
lines changed

.github/workflows/impress-frontend.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Setup Node.js
2020
uses: actions/setup-node@v4
2121
with:
22-
node-version: "18.x"
22+
node-version: "20.x"
2323

2424
- name: Restore the frontend cache
2525
uses: actions/cache@v4
@@ -46,6 +46,11 @@ jobs:
4646
- name: Checkout repository
4747
uses: actions/checkout@v4
4848

49+
- name: Setup Node.js
50+
uses: actions/setup-node@v4
51+
with:
52+
node-version: "20.x"
53+
4954
- name: Restore the frontend cache
5055
uses: actions/cache@v4
5156
id: front-node_modules
@@ -54,7 +59,7 @@ jobs:
5459
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
5560

5661
- name: Test App
57-
run: cd src/frontend/ && yarn app:test
62+
run: cd src/frontend/ && yarn test
5863

5964
lint-front:
6065
runs-on: ubuntu-latest

src/frontend/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@
1313
"APP_IMPRESS": "yarn workspace app-impress",
1414
"APP_E2E": "yarn workspace app-e2e",
1515
"I18N": "yarn workspace packages-i18n",
16+
"COLLABORATION_SERVER": "yarn workspace server-y-provider",
1617
"app:dev": "yarn APP_IMPRESS run dev",
1718
"app:start": "yarn APP_IMPRESS run start",
1819
"app:build": "yarn APP_IMPRESS run build",
1920
"app:test": "yarn APP_IMPRESS run test",
2021
"ci:build": "yarn APP_IMPRESS run build:ci",
2122
"e2e:test": "yarn APP_E2E run test",
22-
"lint": "yarn APP_IMPRESS run lint && yarn APP_E2E run lint && yarn workspace eslint-config-impress run lint && yarn I18N run lint",
23+
"lint": "yarn APP_IMPRESS run lint && yarn APP_E2E run lint && yarn workspace eslint-config-impress run lint && yarn I18N run lint && yarn COLLABORATION_SERVER run lint",
2324
"i18n:extract": "yarn I18N run extract-translation",
2425
"i18n:deploy": "yarn I18N run format-deploy && yarn APP_IMPRESS prettier",
25-
"i18n:test": "yarn I18N run test"
26+
"i18n:test": "yarn I18N run test",
27+
"test": "yarn server:test && yarn app:test",
28+
"server:test": "yarn COLLABORATION_SERVER run test"
2629
},
2730
"resolutions": {
2831
"@blocknote/core": "0.20.0",

src/frontend/servers/y-provider/.eslintrc.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
root: true,
3-
extends: ['impress/next'],
3+
extends: ['impress/jest', 'impress/next'],
44
parserOptions: {
55
tsconfigRootDir: __dirname,
66
project: ['./tsconfig.json'],
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import {
2+
HocuspocusProvider,
3+
HocuspocusProviderWebsocket,
4+
} from '@hocuspocus/provider';
5+
import request from 'supertest';
6+
import WebSocket from 'ws';
7+
8+
const port = 5555;
9+
const portWS = 6666;
10+
const origin = 'http://localhost:3000';
11+
12+
jest.mock('../src/env', () => {
13+
return {
14+
PORT: port,
15+
COLLABORATION_SERVER_ORIGIN: origin,
16+
COLLABORATION_SERVER_SECRET: 'test-secret-api-key',
17+
};
18+
});
19+
20+
console.error = jest.fn();
21+
22+
import { promiseDone } from '../src/helpers';
23+
import { hocuspocusServer, initServer } from '../src/server'; // Adjust the path to your server file
24+
25+
const { app, server } = initServer();
26+
27+
describe('Server Tests', () => {
28+
beforeAll(async () => {
29+
await hocuspocusServer.configure({ port: portWS }).listen();
30+
});
31+
32+
afterAll(() => {
33+
server.close();
34+
void hocuspocusServer.destroy();
35+
});
36+
37+
test('Ping Pong', async () => {
38+
const response = await request(app as any).get('/ping');
39+
40+
expect(response.status).toBe(200);
41+
expect(response.body.message).toBe('pong');
42+
});
43+
44+
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] invalid origin', async () => {
45+
const response = await request(app as any)
46+
.post('/collaboration/api/reset-connections/?room=test-room')
47+
.set('Origin', 'http://invalid-origin.com')
48+
.send({ document_id: 'test-document' });
49+
50+
expect(response.status).toBe(403);
51+
expect(response.body.error).toBe('CORS policy violation: Invalid Origin');
52+
});
53+
54+
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] with incorrect API key should return 403', async () => {
55+
const response = await request(app as any)
56+
.post('/collaboration/api/reset-connections/?room=test-room')
57+
.set('Origin', origin)
58+
.set('Authorization', 'wrong-api-key');
59+
60+
expect(response.status).toBe(403);
61+
expect(response.body.error).toBe('Forbidden: Invalid API Key');
62+
});
63+
64+
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] failed if room not indicated', async () => {
65+
const response = await request(app as any)
66+
.post('/collaboration/api/reset-connections/')
67+
.set('Origin', origin)
68+
.set('Authorization', 'test-secret-api-key')
69+
.send({ document_id: 'test-document' });
70+
71+
expect(response.status).toBe(400);
72+
expect(response.body.error).toBe('Room name not provided');
73+
});
74+
75+
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] with correct API key should reset connections', async () => {
76+
// eslint-disable-next-line jest/unbound-method
77+
const { closeConnections } = hocuspocusServer;
78+
const mockHandleConnection = jest.fn();
79+
(hocuspocusServer.closeConnections as jest.Mock) = mockHandleConnection;
80+
81+
const response = await request(app as any)
82+
.post('/collaboration/api/reset-connections?room=test-room')
83+
.set('Origin', origin)
84+
.set('Authorization', 'test-secret-api-key');
85+
86+
expect(response.status).toBe(200);
87+
expect(response.body.message).toBe('Connections reset');
88+
89+
expect(mockHandleConnection).toHaveBeenCalled();
90+
mockHandleConnection.mockClear();
91+
hocuspocusServer.closeConnections = closeConnections;
92+
});
93+
94+
['/collaboration/api/anything/', '/', '/anything'].forEach((path) => {
95+
test(`"${path}" endpoint should be forbidden`, async () => {
96+
const response = await request(app as any).post(path);
97+
98+
expect(response.status).toBe(403);
99+
expect(response.body.error).toBe('Forbidden');
100+
});
101+
});
102+
103+
test('WebSocket connection with correct API key can connect', () => {
104+
const { promise, done } = promiseDone();
105+
106+
// eslint-disable-next-line jest/unbound-method
107+
const { handleConnection } = hocuspocusServer;
108+
const mockHandleConnection = jest.fn();
109+
(hocuspocusServer.handleConnection as jest.Mock) = mockHandleConnection;
110+
111+
const clientWS = new WebSocket(
112+
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
113+
{
114+
headers: {
115+
authorization: 'test-secret-api-key',
116+
Origin: origin,
117+
},
118+
},
119+
);
120+
121+
clientWS.on('open', () => {
122+
expect(mockHandleConnection).toHaveBeenCalled();
123+
clientWS.close();
124+
mockHandleConnection.mockClear();
125+
hocuspocusServer.handleConnection = handleConnection;
126+
done();
127+
});
128+
129+
return promise;
130+
});
131+
132+
test('WebSocket connection with bad origin should be closed', () => {
133+
const { promise, done } = promiseDone();
134+
135+
const ws = new WebSocket(
136+
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
137+
{
138+
headers: {
139+
Origin: 'http://bad-origin.com',
140+
},
141+
},
142+
);
143+
144+
ws.onclose = () => {
145+
expect(ws.readyState).toBe(ws.CLOSED);
146+
done();
147+
};
148+
149+
return promise;
150+
});
151+
152+
test('WebSocket connection with incorrect API key should be closed', () => {
153+
const { promise, done } = promiseDone();
154+
const ws = new WebSocket(
155+
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
156+
{
157+
headers: {
158+
Authorization: 'wrong-api-key',
159+
Origin: origin,
160+
},
161+
},
162+
);
163+
164+
ws.onclose = () => {
165+
expect(ws.readyState).toBe(ws.CLOSED);
166+
done();
167+
};
168+
169+
return promise;
170+
});
171+
172+
test('WebSocket connection not allowed if room not matching provider name', () => {
173+
const { promise, done } = promiseDone();
174+
175+
const wsHocus = new HocuspocusProviderWebsocket({
176+
url: `ws://localhost:${portWS}/?room=my-test`,
177+
WebSocketPolyfill: WebSocket,
178+
maxAttempts: 1,
179+
quiet: true,
180+
});
181+
182+
const provider = new HocuspocusProvider({
183+
websocketProvider: wsHocus,
184+
name: 'hocuspocus-test',
185+
broadcast: false,
186+
quiet: true,
187+
preserveConnection: false,
188+
onClose: (data) => {
189+
wsHocus.stopConnectionAttempt();
190+
expect(data.event.reason).toBe('Forbidden');
191+
wsHocus.webSocket?.close();
192+
wsHocus.disconnect();
193+
provider.destroy();
194+
wsHocus.destroy();
195+
done();
196+
},
197+
});
198+
199+
return promise;
200+
});
201+
202+
test('WebSocket connection read-only', () => {
203+
const { promise, done } = promiseDone();
204+
205+
const wsHocus = new HocuspocusProviderWebsocket({
206+
url: `ws://localhost:${portWS}/?room=hocuspocus-test`,
207+
WebSocketPolyfill: WebSocket,
208+
});
209+
210+
const provider = new HocuspocusProvider({
211+
websocketProvider: wsHocus,
212+
name: 'hocuspocus-test',
213+
broadcast: false,
214+
quiet: true,
215+
onConnect: () => {
216+
void hocuspocusServer
217+
.openDirectConnection('hocuspocus-test')
218+
.then((connection) => {
219+
connection.document?.getConnections().forEach((connection) => {
220+
expect(connection.readOnly).toBe(true);
221+
});
222+
223+
void connection.disconnect();
224+
});
225+
226+
provider.destroy();
227+
wsHocus.destroy();
228+
done();
229+
},
230+
});
231+
232+
return promise;
233+
});
234+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
var config = {
2+
rootDir: './__tests__',
3+
testEnvironment: 'node',
4+
transform: {
5+
'^.+\\.(ts)$': 'ts-jest',
6+
},
7+
moduleNameMapper: {
8+
'^@/(.*)$': '<rootDir>/../src/$1',
9+
},
10+
};
11+
export default config;

src/frontend/servers/y-provider/package.json

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"license": "MIT",
77
"type": "module",
88
"scripts": {
9-
"build": "tsc -p ./src",
9+
"build": "tsc -p tsconfig.build.json && tsc-alias",
1010
"dev": "nodemon --config nodemon.json",
11-
"start": "node ./dist/server.js",
12-
"lint": "eslint . --ext .ts"
11+
"start": "node ./dist/start-server.js",
12+
"lint": "eslint . --ext .ts",
13+
"test": "jest"
1314
},
1415
"engines": {
1516
"node": ">=18"
@@ -21,13 +22,21 @@
2122
"y-protocols": "1.0.6"
2223
},
2324
"devDependencies": {
25+
"@hocuspocus/provider": "2.14.0",
2426
"@types/express": "5.0.0",
2527
"@types/express-ws": "3.0.5",
28+
"@types/jest": "29.5.14",
2629
"@types/node": "*",
30+
"@types/supertest": "6.0.2",
31+
"@types/ws": "8.5.13",
2732
"eslint-config-impress": "*",
33+
"jest": "29.7.0",
2834
"nodemon": "3.1.7",
35+
"supertest": "7.0.0",
2936
"ts-jest": "29.2.5",
3037
"ts-node": "10.9.2",
31-
"typescript": "*"
38+
"tsc-alias": "1.8.10",
39+
"typescript": "*",
40+
"ws": "8.18.0"
3241
}
3342
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const promiseDone = () => {
2+
let done: (value: void | PromiseLike<void>) => void = () => {};
3+
const promise = new Promise<void>((resolve) => {
4+
done = resolve;
5+
});
6+
7+
return { done, promise };
8+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "./src",
5+
},
6+
"exclude": ["node_modules", "dist", "__tests__"],
7+
}

src/frontend/servers/y-provider/tsconfig.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
"jsx": "preserve",
1414
"incremental": false,
1515
"outDir": "./dist",
16+
"paths": {
17+
"@/*": ["./src/*"]
18+
}
19+
},
20+
"tsc-alias": {
21+
"resolveFullPaths": true,
22+
"verbose": false
1623
},
1724
"include": ["**/*.ts"],
1825
"exclude": ["node_modules"]

0 commit comments

Comments
 (0)