mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-03 12:54:01 +00:00
* fix skill multipart imports under strict isolation * fix file upload context after multipart parsing * fix skill upload tenant resolution * fix rejected upload cleanup
151 lines
4.1 KiB
JavaScript
151 lines
4.1 KiB
JavaScript
process.env.TENANT_ISOLATION_STRICT = 'true';
|
|
|
|
const express = require('express');
|
|
const request = require('supertest');
|
|
const fs = require('fs/promises');
|
|
const { getTenantId, tenantStorage: mockTenantStorage } = require('@librechat/data-schemas');
|
|
|
|
const TEST_TENANT = 'tenant-files-strict';
|
|
|
|
let mockCurrentUser;
|
|
|
|
jest.mock('fs/promises', () => ({
|
|
unlink: jest.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
const mockTenantResponse = (route) => (req, res) =>
|
|
res.status(200).json({ route, tenantId: getTenantId() });
|
|
|
|
jest.mock('~/server/middleware', () => ({
|
|
createFileLimiters: jest.fn(() => ({
|
|
fileUploadIpLimiter: (req, res, next) => next(),
|
|
fileUploadUserLimiter: (req, res, next) => next(),
|
|
})),
|
|
configMiddleware: (req, res, next) => {
|
|
req.config = {
|
|
fileStrategy: 'local',
|
|
paths: { uploads: '/tmp/uploads', images: '/tmp/images' },
|
|
};
|
|
next();
|
|
},
|
|
requireJwtAuth: (req, res, next) => {
|
|
req.user = mockCurrentUser;
|
|
const tenantId = req.user?.tenantId;
|
|
if (!tenantId) {
|
|
next();
|
|
return;
|
|
}
|
|
mockTenantStorage.run({ tenantId }, async () => next());
|
|
},
|
|
uaParser: (req, res, next) => next(),
|
|
checkBan: (req, res, next) => next(),
|
|
}));
|
|
|
|
jest.mock('./multer', () => ({
|
|
createMulterInstance: jest.fn(async () => ({
|
|
single: jest.fn(() => (req, res, next) => {
|
|
req.file = {
|
|
path: '/tmp/uploaded-file',
|
|
originalname: 'uploaded.txt',
|
|
filename: 'uploaded.txt',
|
|
mimetype: 'text/plain',
|
|
size: 8,
|
|
};
|
|
req.file_id = 'file-upload-id';
|
|
mockTenantStorage.enterWith({});
|
|
next();
|
|
}),
|
|
})),
|
|
}));
|
|
|
|
jest.mock('./files', () => {
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
router.post('/', mockTenantResponse('files'));
|
|
return router;
|
|
});
|
|
|
|
jest.mock('./images', () => {
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
router.post('/', mockTenantResponse('images'));
|
|
return router;
|
|
});
|
|
|
|
jest.mock('./avatar', () => {
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
router.post('/', mockTenantResponse('avatar'));
|
|
return router;
|
|
});
|
|
|
|
jest.mock('./speech', () => {
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
router.post('/stt', mockTenantResponse('speech-stt'));
|
|
return router;
|
|
});
|
|
|
|
jest.mock('~/server/routes/agents/v1', () => {
|
|
const express = require('express');
|
|
const avatar = express.Router();
|
|
avatar.post('/:agent_id/avatar/', mockTenantResponse('agent-avatar'));
|
|
return { avatar };
|
|
});
|
|
|
|
jest.mock('~/server/routes/assistants/v1', () => {
|
|
const express = require('express');
|
|
const avatar = express.Router();
|
|
avatar.post('/:assistant_id/avatar/', mockTenantResponse('assistant-avatar'));
|
|
return { avatar };
|
|
});
|
|
|
|
describe('file upload routes restore strict isolation context after multer', () => {
|
|
let app;
|
|
|
|
beforeAll(async () => {
|
|
const { initialize } = require('./index');
|
|
app = express();
|
|
app.use('/api/files', await initialize());
|
|
});
|
|
|
|
beforeEach(() => {
|
|
fs.unlink.mockClear();
|
|
mockCurrentUser = {
|
|
id: 'user-files-strict',
|
|
role: 'USER',
|
|
tenantId: TEST_TENANT,
|
|
};
|
|
});
|
|
|
|
afterAll(() => {
|
|
delete process.env.TENANT_ISOLATION_STRICT;
|
|
});
|
|
|
|
it.each([
|
|
['files', '/api/files'],
|
|
['images', '/api/files/images'],
|
|
['avatar', '/api/files/images/avatar'],
|
|
['agent-avatar', '/api/files/images/agents/agent-1/avatar'],
|
|
['assistant-avatar', '/api/files/images/assistants/asst-1/avatar'],
|
|
['speech-stt', '/api/files/speech/stt'],
|
|
])('restores context for %s upload', async (route, url) => {
|
|
const res = await request(app).post(url);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ route, tenantId: TEST_TENANT });
|
|
});
|
|
|
|
it('rejects strict upload continuations when no tenant can be resolved', async () => {
|
|
mockCurrentUser = {
|
|
id: 'user-without-tenant',
|
|
role: 'USER',
|
|
};
|
|
|
|
const res = await request(app).post('/api/files');
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.error).toMatch(/Tenant context required/);
|
|
expect(fs.unlink).toHaveBeenCalledWith('/tmp/uploaded-file');
|
|
});
|
|
});
|