mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-15 12:16:33 +01:00
* fix: add rate limiting to conversation duplicate endpoint * chore: linter * fix: address review findings for conversation duplicate rate limiting * refactor: streamline test mocks for conversation routes - Consolidated mock implementations into a dedicated `convos-route-mocks.js` file to enhance maintainability and readability of test files. - Updated tests in `convos-duplicate-ratelimit.spec.js` and `convos.spec.js` to utilize the new mock structure, improving clarity and reducing redundancy. - Enhanced the `duplicateConversation` function to accept an optional title parameter for better flexibility in conversation duplication. * chore: rename files
135 lines
4.6 KiB
JavaScript
135 lines
4.6 KiB
JavaScript
const express = require('express');
|
|
const request = require('supertest');
|
|
|
|
const MOCKS = '../__test-utils__/convos-route-mocks';
|
|
|
|
jest.mock('@librechat/agents', () => require(MOCKS).agents());
|
|
jest.mock('@librechat/api', () => require(MOCKS).api({ limiterCache: jest.fn(() => undefined) }));
|
|
jest.mock('@librechat/data-schemas', () => require(MOCKS).dataSchemas());
|
|
jest.mock('librechat-data-provider', () =>
|
|
require(MOCKS).dataProvider({ ViolationTypes: { FILE_UPLOAD_LIMIT: 'file_upload_limit' } }),
|
|
);
|
|
|
|
jest.mock('~/cache/logViolation', () => jest.fn().mockResolvedValue(undefined));
|
|
jest.mock('~/cache/getLogStores', () => require(MOCKS).logStores());
|
|
jest.mock('~/models/Conversation', () => require(MOCKS).conversationModel());
|
|
jest.mock('~/models/ToolCall', () => require(MOCKS).toolCallModel());
|
|
jest.mock('~/models', () => require(MOCKS).sharedModels());
|
|
jest.mock('~/server/middleware/requireJwtAuth', () => require(MOCKS).requireJwtAuth());
|
|
|
|
jest.mock('~/server/middleware', () => {
|
|
const { createForkLimiters } = jest.requireActual('~/server/middleware/limiters/forkLimiters');
|
|
return {
|
|
createImportLimiters: jest.fn(() => ({
|
|
importIpLimiter: (req, res, next) => next(),
|
|
importUserLimiter: (req, res, next) => next(),
|
|
})),
|
|
createForkLimiters,
|
|
configMiddleware: (req, res, next) => next(),
|
|
validateConvoAccess: (req, res, next) => next(),
|
|
};
|
|
});
|
|
|
|
jest.mock('~/server/utils/import/fork', () => require(MOCKS).forkUtils());
|
|
jest.mock('~/server/utils/import', () => require(MOCKS).importUtils());
|
|
jest.mock('~/server/routes/files/multer', () => require(MOCKS).multerSetup());
|
|
jest.mock('multer', () => require(MOCKS).multerLib());
|
|
jest.mock('~/server/services/Endpoints/azureAssistants', () => require(MOCKS).assistantEndpoint());
|
|
jest.mock('~/server/services/Endpoints/assistants', () => require(MOCKS).assistantEndpoint());
|
|
|
|
describe('POST /api/convos/duplicate - Rate Limiting', () => {
|
|
let app;
|
|
let duplicateConversation;
|
|
const savedEnv = {};
|
|
|
|
beforeAll(() => {
|
|
savedEnv.FORK_USER_MAX = process.env.FORK_USER_MAX;
|
|
savedEnv.FORK_USER_WINDOW = process.env.FORK_USER_WINDOW;
|
|
savedEnv.FORK_IP_MAX = process.env.FORK_IP_MAX;
|
|
savedEnv.FORK_IP_WINDOW = process.env.FORK_IP_WINDOW;
|
|
});
|
|
|
|
afterAll(() => {
|
|
for (const key of Object.keys(savedEnv)) {
|
|
if (savedEnv[key] === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = savedEnv[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
const setupApp = () => {
|
|
jest.clearAllMocks();
|
|
jest.isolateModules(() => {
|
|
const convosRouter = require('../convos');
|
|
({ duplicateConversation } = require('~/server/utils/import/fork'));
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use((req, res, next) => {
|
|
req.user = { id: 'rate-limit-test-user' };
|
|
next();
|
|
});
|
|
app.use('/api/convos', convosRouter);
|
|
});
|
|
|
|
duplicateConversation.mockResolvedValue({
|
|
conversation: { conversationId: 'duplicated-conv' },
|
|
});
|
|
};
|
|
|
|
describe('user limit', () => {
|
|
beforeEach(() => {
|
|
process.env.FORK_USER_MAX = '2';
|
|
process.env.FORK_USER_WINDOW = '1';
|
|
process.env.FORK_IP_MAX = '100';
|
|
process.env.FORK_IP_WINDOW = '1';
|
|
setupApp();
|
|
});
|
|
|
|
it('should return 429 after exceeding the user rate limit', async () => {
|
|
const userMax = parseInt(process.env.FORK_USER_MAX, 10);
|
|
|
|
for (let i = 0; i < userMax; i++) {
|
|
const res = await request(app)
|
|
.post('/api/convos/duplicate')
|
|
.send({ conversationId: 'conv-123' });
|
|
expect(res.status).toBe(201);
|
|
}
|
|
|
|
const res = await request(app)
|
|
.post('/api/convos/duplicate')
|
|
.send({ conversationId: 'conv-123' });
|
|
expect(res.status).toBe(429);
|
|
expect(res.body.message).toMatch(/too many/i);
|
|
});
|
|
});
|
|
|
|
describe('IP limit', () => {
|
|
beforeEach(() => {
|
|
process.env.FORK_USER_MAX = '100';
|
|
process.env.FORK_USER_WINDOW = '1';
|
|
process.env.FORK_IP_MAX = '2';
|
|
process.env.FORK_IP_WINDOW = '1';
|
|
setupApp();
|
|
});
|
|
|
|
it('should return 429 after exceeding the IP rate limit', async () => {
|
|
const ipMax = parseInt(process.env.FORK_IP_MAX, 10);
|
|
|
|
for (let i = 0; i < ipMax; i++) {
|
|
const res = await request(app)
|
|
.post('/api/convos/duplicate')
|
|
.send({ conversationId: 'conv-123' });
|
|
expect(res.status).toBe(201);
|
|
}
|
|
|
|
const res = await request(app)
|
|
.post('/api/convos/duplicate')
|
|
.send({ conversationId: 'conv-123' });
|
|
expect(res.status).toBe(429);
|
|
expect(res.body.message).toMatch(/too many/i);
|
|
});
|
|
});
|
|
});
|