✅(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,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')
|
||||
|
||||
Reference in New Issue
Block a user