✅(frontend) switch to vitest and enhance testability
Migrated from jest to vitest for server/y-provider, gaining faster runs, esm-native support and cleaner mocking. Signed-off-by: Stephan Meijer <me@stephanmeijer.com>
This commit is contained in:
@@ -1 +0,0 @@
|
||||
module.exports = {};
|
||||
@@ -1,66 +1,64 @@
|
||||
import request from 'supertest';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const port = 5555;
|
||||
const origin = 'http://localhost:3000';
|
||||
|
||||
jest.mock('../src/env', () => {
|
||||
vi.mock('../src/env', async (importOriginal) => {
|
||||
return {
|
||||
PORT: port,
|
||||
COLLABORATION_SERVER_ORIGIN: origin,
|
||||
...(await importOriginal()),
|
||||
PORT: 5555,
|
||||
COLLABORATION_SERVER_ORIGIN: 'http://localhost:3000',
|
||||
COLLABORATION_SERVER_SECRET: 'test-secret-api-key',
|
||||
};
|
||||
});
|
||||
|
||||
console.error = jest.fn();
|
||||
console.error = vi.fn();
|
||||
|
||||
import { hocusPocusServer } from '@/servers/hocusPocusServer';
|
||||
|
||||
import { initServer } from '../src/servers/appServer';
|
||||
|
||||
const { app, server } = initServer();
|
||||
import { COLLABORATION_SERVER_ORIGIN as origin } from '@/env';
|
||||
import { hocuspocusServer, initApp } from '@/servers';
|
||||
|
||||
describe('Server Tests', () => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] with incorrect API key should return 403', async () => {
|
||||
const response = await request(app as any)
|
||||
const app = initApp();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/collaboration/api/reset-connections/?room=test-room')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'wrong-api-key');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden: Invalid API Key');
|
||||
expect(response.body).toStrictEqual({
|
||||
error: 'Forbidden: Invalid API Key',
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] failed if room not indicated', async () => {
|
||||
const response = await request(app as any)
|
||||
const app = initApp();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/collaboration/api/reset-connections/')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'test-secret-api-key')
|
||||
.send({ document_id: 'test-document' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Room name not provided');
|
||||
expect(response.body).toStrictEqual({ error: 'Room name not provided' });
|
||||
});
|
||||
|
||||
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] with correct API key should reset connections', async () => {
|
||||
// eslint-disable-next-line jest/unbound-method
|
||||
const { closeConnections } = hocusPocusServer;
|
||||
const mockHandleConnection = jest.fn();
|
||||
(hocusPocusServer.closeConnections as jest.Mock) = mockHandleConnection;
|
||||
const closeConnectionsMock = vi
|
||||
.spyOn(hocuspocusServer, 'closeConnections')
|
||||
.mockResolvedValue();
|
||||
|
||||
const response = await request(app as any)
|
||||
const app = initApp();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/collaboration/api/reset-connections?room=test-room')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'test-secret-api-key');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Connections reset');
|
||||
expect(response.body).toStrictEqual({ message: 'Connections reset' });
|
||||
|
||||
expect(mockHandleConnection).toHaveBeenCalled();
|
||||
mockHandleConnection.mockClear();
|
||||
hocusPocusServer.closeConnections = closeConnections;
|
||||
// eslint-disable-next-line jest/unbound-method
|
||||
expect(closeConnectionsMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,66 +1,93 @@
|
||||
import { Hocuspocus } from '@hocuspocus/server';
|
||||
import request from 'supertest';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
const port = 5556;
|
||||
const origin = 'http://localhost:3000';
|
||||
|
||||
jest.mock('../src/env', () => {
|
||||
vi.mock('../src/env', async (importOriginal) => {
|
||||
return {
|
||||
PORT: port,
|
||||
COLLABORATION_SERVER_ORIGIN: origin,
|
||||
...(await importOriginal()),
|
||||
COLLABORATION_SERVER_ORIGIN: 'http://localhost:3000',
|
||||
Y_PROVIDER_API_KEY: 'yprovider-api-key',
|
||||
};
|
||||
});
|
||||
|
||||
import { initServer } from '../src/servers/appServer';
|
||||
import { initApp } from '@/servers';
|
||||
|
||||
console.error = jest.fn();
|
||||
const { app, server } = initServer();
|
||||
import {
|
||||
Y_PROVIDER_API_KEY as apiKey,
|
||||
COLLABORATION_SERVER_ORIGIN as origin,
|
||||
} from '../src/env';
|
||||
|
||||
console.error = vi.fn();
|
||||
|
||||
const mockOpts = {
|
||||
fallbackMockImplementation: () => {
|
||||
throw new Error('Unexpected call.');
|
||||
},
|
||||
};
|
||||
|
||||
describe('Server Tests', () => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
test('POST /api/convert with incorrect API key should responds with 403', async () => {
|
||||
const hocuspocus = mock<Hocuspocus>({}, mockOpts);
|
||||
const app = initApp(hocuspocus);
|
||||
|
||||
test('POST /api/convert with incorrect API key should return 403', async () => {
|
||||
const response = await request(app as any)
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'wrong-api-key');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden: Invalid API Key');
|
||||
expect(response.body).toStrictEqual({
|
||||
error: 'Forbidden: Invalid API Key',
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /api/convert with a Bearer token', async () => {
|
||||
const response = await request(app as any)
|
||||
const hocuspocus = mock<Hocuspocus>({}, mockOpts);
|
||||
const app = initApp(hocuspocus);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'Bearer test-secret-api-key');
|
||||
|
||||
// Warning: Changing the authorization header to Bearer token format will break backend compatibility with this microservice.
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toStrictEqual({
|
||||
error: 'Forbidden: Invalid API Key',
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /api/convert with missing body param content', async () => {
|
||||
const response = await request(app as any)
|
||||
const hocuspocus = mock<Hocuspocus>({}, mockOpts);
|
||||
const app = initApp(hocuspocus);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'yprovider-api-key');
|
||||
.set('Authorization', apiKey);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid request: missing content');
|
||||
expect(response.body).toStrictEqual({
|
||||
error: 'Invalid request: missing content',
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /api/convert with body param content being an empty string', async () => {
|
||||
const response = await request(app as any)
|
||||
const hocuspocus = mock<Hocuspocus>({}, mockOpts);
|
||||
const app = initApp(hocuspocus);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/convert')
|
||||
.set('Origin', origin)
|
||||
.set('Authorization', 'yprovider-api-key')
|
||||
.set('Authorization', apiKey)
|
||||
.send({
|
||||
content: '',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid request: missing content');
|
||||
expect(response.body).toStrictEqual({
|
||||
error: 'Invalid request: missing content',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,56 +1,62 @@
|
||||
import { Server } from 'node:net';
|
||||
|
||||
import {
|
||||
HocuspocusProvider,
|
||||
HocuspocusProviderWebsocket,
|
||||
} from '@hocuspocus/provider';
|
||||
import { v1 as uuidv1, v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
test,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const port = 5559;
|
||||
const portWS = 6666;
|
||||
const origin = 'http://localhost:3000';
|
||||
|
||||
jest.mock('../src/env', () => {
|
||||
vi.mock('../src/env', async (importOriginal) => {
|
||||
return {
|
||||
PORT: port,
|
||||
COLLABORATION_SERVER_ORIGIN: origin,
|
||||
...(await importOriginal()),
|
||||
PORT: 5559,
|
||||
COLLABORATION_SERVER_ORIGIN: 'http://localhost:3000',
|
||||
COLLABORATION_SERVER_SECRET: 'test-secret-api-key',
|
||||
COLLABORATION_BACKEND_BASE_URL: 'http://app-dev:8000',
|
||||
COLLABORATION_LOGGING: 'true',
|
||||
};
|
||||
});
|
||||
|
||||
console.error = jest.fn();
|
||||
console.log = jest.fn();
|
||||
|
||||
const mockDocFetch = jest.fn();
|
||||
jest.mock('@/api/getDoc', () => ({
|
||||
fetchDocument: mockDocFetch,
|
||||
vi.mock('../src/api/collaborationBackend', () => ({
|
||||
fetchCurrentUser: vi.fn(),
|
||||
fetchDocument: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockGetMe = jest.fn();
|
||||
jest.mock('@/api/getMe', () => ({
|
||||
getMe: mockGetMe,
|
||||
}));
|
||||
console.error = vi.fn();
|
||||
console.log = vi.fn();
|
||||
|
||||
import { hocusPocusServer } from '@/servers/hocusPocusServer';
|
||||
|
||||
import { promiseDone } from '../src/helpers';
|
||||
import { initServer } from '../src/servers/appServer';
|
||||
|
||||
const { server } = initServer();
|
||||
import * as CollaborationBackend from '@/api/collaborationBackend';
|
||||
import { COLLABORATION_SERVER_ORIGIN as origin, PORT as port } from '@/env';
|
||||
import { promiseDone } from '@/helpers';
|
||||
import { hocuspocusServer, initApp } from '@/servers';
|
||||
|
||||
describe('Server Tests', () => {
|
||||
beforeAll(async () => {
|
||||
await hocusPocusServer.configure({ port: portWS }).listen();
|
||||
let server: Server;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
beforeAll(async () => {
|
||||
server = initApp().listen(port);
|
||||
await hocuspocusServer.configure({ port: portWS }).listen();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
void hocuspocusServer.destroy();
|
||||
server.close();
|
||||
void hocusPocusServer.destroy();
|
||||
});
|
||||
|
||||
test('WebSocket connection with bad origin should be closed', () => {
|
||||
@@ -209,7 +215,9 @@ describe('Server Tests', () => {
|
||||
test('WebSocket connection fails if user can not access document', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockRejectedValue('');
|
||||
const fetchDocumentMock = vi
|
||||
.spyOn(CollaborationBackend, 'fetchDocument')
|
||||
.mockRejectedValue(new Error('some error'));
|
||||
|
||||
const room = uuidv4();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
@@ -233,7 +241,10 @@ describe('Server Tests', () => {
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
expect(mockDocFetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith(
|
||||
room,
|
||||
expect.any(Object),
|
||||
);
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
@@ -249,11 +260,10 @@ describe('Server Tests', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
const room = uuidv4();
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: false,
|
||||
},
|
||||
});
|
||||
|
||||
const fetchDocumentMock = vi
|
||||
.spyOn(CollaborationBackend, 'fetchDocument')
|
||||
.mockResolvedValue({ abilities: { retrieve: false } } as any);
|
||||
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||
@@ -278,7 +288,10 @@ describe('Server Tests', () => {
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
expect(mockDocFetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith(
|
||||
room,
|
||||
expect.any(Object),
|
||||
);
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
@@ -294,12 +307,11 @@ describe('Server Tests', () => {
|
||||
test(`WebSocket connection ${canEdit ? 'can' : 'can not'} edit document`, () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: true,
|
||||
update: canEdit,
|
||||
},
|
||||
});
|
||||
const fetchDocumentMock = vi
|
||||
.spyOn(CollaborationBackend, 'fetchDocument')
|
||||
.mockResolvedValue({
|
||||
abilities: { retrieve: true, update: canEdit },
|
||||
} as any);
|
||||
|
||||
const room = uuidv4();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
@@ -313,7 +325,7 @@ describe('Server Tests', () => {
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
onConnect: () => {
|
||||
void hocusPocusServer
|
||||
void hocuspocusServer
|
||||
.openDirectConnection(room)
|
||||
.then((connection) => {
|
||||
connection.document?.getConnections().forEach((connection) => {
|
||||
@@ -324,6 +336,12 @@ describe('Server Tests', () => {
|
||||
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
|
||||
expect(fetchDocumentMock).toHaveBeenCalledWith(
|
||||
room,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
},
|
||||
@@ -336,16 +354,15 @@ describe('Server Tests', () => {
|
||||
test('Add request header x-user-id if found', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: true,
|
||||
update: true,
|
||||
},
|
||||
});
|
||||
const fetchDocumentMock = vi
|
||||
.spyOn(CollaborationBackend, 'fetchDocument')
|
||||
.mockResolvedValue({
|
||||
abilities: { retrieve: true, update: true },
|
||||
} as any);
|
||||
|
||||
mockGetMe.mockResolvedValue({
|
||||
id: 'test-user-id',
|
||||
});
|
||||
const fetchCurrentUserMock = vi
|
||||
.spyOn(CollaborationBackend, 'fetchCurrentUser')
|
||||
.mockResolvedValue({ id: 'test-user-id' } as any);
|
||||
|
||||
const room = uuidv4();
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
@@ -359,7 +376,7 @@ describe('Server Tests', () => {
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
onConnect: () => {
|
||||
void hocusPocusServer.openDirectConnection(room).then((connection) => {
|
||||
void hocuspocusServer.openDirectConnection(room).then((connection) => {
|
||||
connection.document?.getConnections().forEach((connection) => {
|
||||
expect(connection.context.userId).toBe('test-user-id');
|
||||
});
|
||||
@@ -367,6 +384,14 @@ describe('Server Tests', () => {
|
||||
void connection.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
|
||||
expect(fetchDocumentMock).toHaveBeenCalledWith(
|
||||
room,
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
expect(fetchCurrentUserMock).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
},
|
||||
@@ -1,45 +1,42 @@
|
||||
import request from 'supertest';
|
||||
import { describe, expect, it, test, vi } from 'vitest';
|
||||
|
||||
import { routes } from '@/routes';
|
||||
import { initApp } from '@/servers';
|
||||
|
||||
const port = 5557;
|
||||
const origin = 'http://localhost:3000';
|
||||
|
||||
jest.mock('../src/env', () => {
|
||||
vi.mock('../src/env', async (importOriginal) => {
|
||||
return {
|
||||
PORT: port,
|
||||
COLLABORATION_SERVER_ORIGIN: origin,
|
||||
...(await importOriginal()),
|
||||
COLLABORATION_SERVER_ORIGIN: 'http://localhost:3000',
|
||||
};
|
||||
});
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
import { initServer } from '../src/servers/appServer';
|
||||
|
||||
const { app, server } = initServer();
|
||||
console.error = vi.fn();
|
||||
|
||||
describe('Server Tests', () => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
test('Ping Pong', async () => {
|
||||
const response = await request(app as any).get('/ping');
|
||||
const app = initApp();
|
||||
|
||||
const response = await request(app).get('/ping');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('pong');
|
||||
expect(response.body).toStrictEqual({ message: 'pong' });
|
||||
});
|
||||
|
||||
['/collaboration/api/anything/', '/', '/anything'].forEach((path) => {
|
||||
test(`"${path}" endpoint should be forbidden`, async () => {
|
||||
const response = await request(app as any).post(path);
|
||||
const app = initApp();
|
||||
|
||||
const response = await request(app).post(path);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow JSON payloads up to 500kb for the CONVERT route', async () => {
|
||||
it('allows JSON payloads up to 500kb for the CONVERT route', async () => {
|
||||
const app = initApp();
|
||||
|
||||
const largePayload = 'a'.repeat(400 * 1024); // 400kb payload
|
||||
const response = await request(app)
|
||||
.post(routes.CONVERT)
|
||||
@@ -49,7 +46,9 @@ describe('Server Tests', () => {
|
||||
expect(response.status).not.toBe(413);
|
||||
});
|
||||
|
||||
it('should reject JSON payloads larger than 500kb for the CONVERT route', async () => {
|
||||
it('rejects JSON payloads larger than 500kb for the CONVERT route', async () => {
|
||||
const app = initApp();
|
||||
|
||||
const oversizedPayload = 'a'.repeat(501 * 1024); // 501kb payload
|
||||
const response = await request(app)
|
||||
.post(routes.CONVERT)
|
||||
@@ -59,7 +58,9 @@ describe('Server Tests', () => {
|
||||
expect(response.status).toBe(413);
|
||||
});
|
||||
|
||||
it('should use the default JSON limit for other routes', async () => {
|
||||
it('uses the default JSON limit for other routes', async () => {
|
||||
const app = initApp();
|
||||
|
||||
const largePayload = 'a'.repeat(150 * 1024);
|
||||
const response = await request(app)
|
||||
.post('/some-other-route')
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
const config = {
|
||||
rootDir: './__tests__',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.(ts)$': 'ts-jest',
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/../src/$1',
|
||||
'^@blocknote/server-util$': '<rootDir>/../__mocks__/mock.js',
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -10,7 +10,7 @@
|
||||
"dev": "cross-env COLLABORATION_LOGGING=true && nodemon --config nodemon.json",
|
||||
"start": "node ./dist/start-server.js",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest"
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
@@ -33,19 +33,19 @@
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/express-ws": "3.0.5",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "6.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-config-impress": "*",
|
||||
"jest": "30.0.3",
|
||||
"nodemon": "3.1.10",
|
||||
"supertest": "7.1.1",
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"tsc-alias": "1.8.16",
|
||||
"typescript": "*",
|
||||
"vitest": "3.2.4",
|
||||
"vitest-mock-extended": "3.1.0",
|
||||
"ws": "8.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,12 @@ import axios from 'axios';
|
||||
|
||||
import { COLLABORATION_BACKEND_BASE_URL } from '@/env';
|
||||
|
||||
enum LinkReach {
|
||||
RESTRICTED = 'restricted',
|
||||
PUBLIC = 'public',
|
||||
AUTHENTICATED = 'authenticated',
|
||||
}
|
||||
|
||||
enum LinkRole {
|
||||
READER = 'reader',
|
||||
EDITOR = 'editor',
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
type Base64 = string;
|
||||
@@ -23,8 +20,8 @@ interface Doc {
|
||||
content: Base64;
|
||||
creator: string;
|
||||
is_favorite: boolean;
|
||||
link_reach: LinkReach;
|
||||
link_role: LinkRole;
|
||||
link_reach: 'restricted' | 'public' | 'authenticated';
|
||||
link_role: 'reader' | 'editor';
|
||||
nb_accesses_ancestors: number;
|
||||
nb_accesses_direct: number;
|
||||
created_at: string;
|
||||
@@ -54,23 +51,36 @@ interface Doc {
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchDocument = async (
|
||||
documentName: string,
|
||||
async function fetch<T>(
|
||||
path: string,
|
||||
requestHeaders: IncomingHttpHeaders,
|
||||
) => {
|
||||
const response = await axios.get<Doc>(
|
||||
`${COLLABORATION_BACKEND_BASE_URL}/api/v1.0/documents/${documentName}/`,
|
||||
): Promise<T> {
|
||||
const response = await axios.get<T>(
|
||||
`${COLLABORATION_BACKEND_BASE_URL}${path}`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: requestHeaders['cookie'],
|
||||
Origin: requestHeaders['origin'],
|
||||
cookie: requestHeaders['cookie'],
|
||||
origin: requestHeaders['origin'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch document: ${response.statusText}`);
|
||||
throw new Error(`Failed to fetch ${path}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchDocument(
|
||||
name: string,
|
||||
requestHeaders: IncomingHttpHeaders,
|
||||
): Promise<Doc> {
|
||||
return fetch<Doc>(`/api/v1.0/documents/${name}/`, requestHeaders);
|
||||
}
|
||||
|
||||
export function fetchCurrentUser(
|
||||
requestHeaders: IncomingHttpHeaders,
|
||||
): Promise<User> {
|
||||
return fetch<User>('/api/v1.0/users/me/', requestHeaders);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { COLLABORATION_BACKEND_BASE_URL } from '@/env';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export const getMe = async (requestHeaders: IncomingHttpHeaders) => {
|
||||
const response = await axios.get<User>(
|
||||
`${COLLABORATION_BACKEND_BASE_URL}/api/v1.0/users/me/`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: requestHeaders['cookie'],
|
||||
Origin: requestHeaders['origin'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch user: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
import { hocusPocusServer } from '@/servers/hocusPocusServer';
|
||||
import { hocuspocusServer } from '@/servers';
|
||||
import { logger } from '@/utils';
|
||||
|
||||
type ResetConnectionsRequestQuery = {
|
||||
@@ -25,12 +25,12 @@ export const collaborationResetConnectionsHandler = (
|
||||
* If no user ID is provided, close all connections in the room
|
||||
*/
|
||||
if (!userId) {
|
||||
hocusPocusServer.closeConnections(room);
|
||||
hocuspocusServer.closeConnections(room);
|
||||
} else {
|
||||
/**
|
||||
* Close connections for the user in the room
|
||||
*/
|
||||
hocusPocusServer.documents.forEach((doc) => {
|
||||
hocuspocusServer.documents.forEach((doc) => {
|
||||
if (doc.name !== room) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Request } from 'express';
|
||||
import * as ws from 'ws';
|
||||
|
||||
import { hocusPocusServer } from '@/servers/hocusPocusServer';
|
||||
import { hocuspocusServer } from '@/servers/hocuspocusServer';
|
||||
|
||||
export const collaborationWSHandler = (ws: ws.WebSocket, req: Request) => {
|
||||
try {
|
||||
hocusPocusServer.handleConnection(ws, req);
|
||||
hocuspocusServer.handleConnection(ws, req);
|
||||
} catch (error) {
|
||||
console.error('Failed to handle WebSocket connection:', error);
|
||||
ws.close();
|
||||
|
||||
@@ -16,6 +16,8 @@ interface ErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
const editor = ServerBlockNoteEditor.create();
|
||||
|
||||
export const convertHandler = async (
|
||||
req: Request<
|
||||
object,
|
||||
@@ -33,8 +35,6 @@ export const convertHandler = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const editor = ServerBlockNoteEditor.create();
|
||||
|
||||
// Perform the conversion from markdown to Blocknote.js blocks
|
||||
const blocks = await editor.tryParseMarkdownToBlocks(content);
|
||||
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
// eslint-disable-next-line import/order
|
||||
import '../services/sentry';
|
||||
|
||||
import * as Sentry from '@sentry/node';
|
||||
import express from 'express';
|
||||
import expressWebsockets from 'express-ws';
|
||||
|
||||
import { PORT } from '../env';
|
||||
import {
|
||||
collaborationResetConnectionsHandler,
|
||||
collaborationWSHandler,
|
||||
convertHandler,
|
||||
} from '../handlers';
|
||||
import { corsMiddleware, httpSecurity, wsSecurity } from '../middlewares';
|
||||
import { routes } from '../routes';
|
||||
import { logger } from '../utils';
|
||||
} from '@/handlers';
|
||||
import { corsMiddleware, httpSecurity, wsSecurity } from '@/middlewares';
|
||||
import { routes } from '@/routes';
|
||||
import { logger } from '@/utils';
|
||||
|
||||
/**
|
||||
* init the collaboration server.
|
||||
*
|
||||
* @returns An object containing the Express app, Hocuspocus server, and HTTP server instance.
|
||||
*/
|
||||
export const initServer = () => {
|
||||
export const initApp = () => {
|
||||
const { app } = expressWebsockets(express());
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path === routes.CONVERT) {
|
||||
// Large transcript files are bigger than the default '100kb' limit
|
||||
@@ -62,9 +63,5 @@ export const initServer = () => {
|
||||
res.status(403).json({ error: 'Forbidden' });
|
||||
});
|
||||
|
||||
const server = app.listen(PORT, () =>
|
||||
console.log('App listening on port :', PORT),
|
||||
);
|
||||
|
||||
return { app, server };
|
||||
return app;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Server } from '@hocuspocus/server';
|
||||
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
|
||||
|
||||
import { fetchDocument } from '@/api/getDoc';
|
||||
import { getMe } from '@/api/getMe';
|
||||
import { fetchCurrentUser, fetchDocument } from '@/api/collaborationBackend';
|
||||
import { logger } from '@/utils';
|
||||
|
||||
export const hocusPocusServer = Server.configure({
|
||||
export const hocuspocusServer = Server.configure({
|
||||
name: 'docs-collaboration',
|
||||
timeout: 30000,
|
||||
quiet: true,
|
||||
@@ -37,7 +36,7 @@ export const hocusPocusServer = Server.configure({
|
||||
return Promise.reject(new Error('Wrong room name: Unauthorized'));
|
||||
}
|
||||
|
||||
let can_edit = false;
|
||||
let canEdit = false;
|
||||
|
||||
try {
|
||||
const document = await fetchDocument(documentName, requestHeaders);
|
||||
@@ -50,7 +49,7 @@ export const hocusPocusServer = Server.configure({
|
||||
return Promise.reject(new Error('Wrong abilities:Unauthorized'));
|
||||
}
|
||||
|
||||
can_edit = document.abilities.update;
|
||||
canEdit = document.abilities.update;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
logger('onConnect: backend error', error.message);
|
||||
@@ -59,14 +58,14 @@ export const hocusPocusServer = Server.configure({
|
||||
return Promise.reject(new Error('Backend error: Unauthorized'));
|
||||
}
|
||||
|
||||
connection.readOnly = !can_edit;
|
||||
connection.readOnly = !canEdit;
|
||||
|
||||
/*
|
||||
* Unauthenticated users can be allowed to connect
|
||||
* so we flag only authenticated users
|
||||
*/
|
||||
try {
|
||||
const user = await getMe(requestHeaders);
|
||||
const user = await fetchCurrentUser(requestHeaders);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
context.userId = user.id;
|
||||
} catch {}
|
||||
@@ -75,7 +74,7 @@ export const hocusPocusServer = Server.configure({
|
||||
'Connection established on room:',
|
||||
documentName,
|
||||
'canEdit:',
|
||||
can_edit,
|
||||
canEdit,
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './appServer';
|
||||
export * from './hocusPocusServer';
|
||||
export * from './hocuspocusServer';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { initServer } from './servers/appServer';
|
||||
import { PORT } from '@/env';
|
||||
import { initApp } from '@/servers';
|
||||
|
||||
initServer();
|
||||
initApp().listen(PORT, () => console.log('App listening on port :', PORT));
|
||||
|
||||
11
src/frontend/servers/y-provider/vitest.config.mts
Normal file
11
src/frontend/servers/y-provider/vitest.config.mts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { URL, fileURLToPath } from 'url';
|
||||
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user