mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
303 lines
8.7 KiB
JavaScript
303 lines
8.7 KiB
JavaScript
|
|
const express = require('express');
|
||
|
|
const request = require('supertest');
|
||
|
|
const mongoose = require('mongoose');
|
||
|
|
const { v4: uuidv4 } = require('uuid');
|
||
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||
|
|
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
|
||
|
|
|
||
|
|
// Mock dependencies
|
||
|
|
jest.mock('~/server/services/Files/process', () => ({
|
||
|
|
processDeleteRequest: jest.fn().mockResolvedValue({}),
|
||
|
|
filterFile: jest.fn(),
|
||
|
|
processFileUpload: jest.fn(),
|
||
|
|
processAgentFileUpload: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/server/services/Files/strategies', () => ({
|
||
|
|
getStrategyFunctions: jest.fn(() => ({})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/server/controllers/assistants/helpers', () => ({
|
||
|
|
getOpenAIClient: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/server/services/Tools/credentials', () => ({
|
||
|
|
loadAuthValues: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/server/services/Files/S3/crud', () => ({
|
||
|
|
refreshS3FileUrls: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/cache', () => ({
|
||
|
|
getLogStores: jest.fn(() => ({
|
||
|
|
get: jest.fn(),
|
||
|
|
set: jest.fn(),
|
||
|
|
})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/config', () => ({
|
||
|
|
logger: {
|
||
|
|
error: jest.fn(),
|
||
|
|
warn: jest.fn(),
|
||
|
|
debug: jest.fn(),
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
const { createFile } = require('~/models/File');
|
||
|
|
const { createAgent } = require('~/models/Agent');
|
||
|
|
const { getProjectByName } = require('~/models/Project');
|
||
|
|
const { processDeleteRequest } = require('~/server/services/Files/process');
|
||
|
|
|
||
|
|
// Import the router after mocks
|
||
|
|
const router = require('./files');
|
||
|
|
|
||
|
|
describe('File Routes - Delete with Agent Access', () => {
|
||
|
|
let app;
|
||
|
|
let mongoServer;
|
||
|
|
let authorId;
|
||
|
|
let otherUserId;
|
||
|
|
let agentId;
|
||
|
|
let fileId;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
mongoServer = await MongoMemoryServer.create();
|
||
|
|
await mongoose.connect(mongoServer.getUri());
|
||
|
|
|
||
|
|
// Initialize models
|
||
|
|
require('~/db/models');
|
||
|
|
|
||
|
|
app = express();
|
||
|
|
app.use(express.json());
|
||
|
|
|
||
|
|
// Mock authentication middleware
|
||
|
|
app.use((req, res, next) => {
|
||
|
|
req.user = { id: otherUserId || 'default-user' };
|
||
|
|
req.app = { locals: {} };
|
||
|
|
next();
|
||
|
|
});
|
||
|
|
|
||
|
|
app.use('/files', router);
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await mongoose.disconnect();
|
||
|
|
await mongoServer.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
jest.clearAllMocks();
|
||
|
|
|
||
|
|
// Clear database
|
||
|
|
const collections = mongoose.connection.collections;
|
||
|
|
for (const key in collections) {
|
||
|
|
await collections[key].deleteMany({});
|
||
|
|
}
|
||
|
|
|
||
|
|
authorId = new mongoose.Types.ObjectId().toString();
|
||
|
|
otherUserId = new mongoose.Types.ObjectId().toString();
|
||
|
|
fileId = uuidv4();
|
||
|
|
|
||
|
|
// Create a file owned by the author
|
||
|
|
await createFile({
|
||
|
|
user: authorId,
|
||
|
|
file_id: fileId,
|
||
|
|
filename: 'test.txt',
|
||
|
|
filepath: `/uploads/${authorId}/${fileId}`,
|
||
|
|
bytes: 1024,
|
||
|
|
type: 'text/plain',
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create an agent with the file attached
|
||
|
|
const agent = await createAgent({
|
||
|
|
id: uuidv4(),
|
||
|
|
name: 'Test Agent',
|
||
|
|
author: authorId,
|
||
|
|
model: 'gpt-4',
|
||
|
|
provider: 'openai',
|
||
|
|
isCollaborative: true,
|
||
|
|
tool_resources: {
|
||
|
|
file_search: {
|
||
|
|
file_ids: [fileId],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
agentId = agent.id;
|
||
|
|
|
||
|
|
// Share the agent globally
|
||
|
|
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, '_id');
|
||
|
|
if (globalProject) {
|
||
|
|
const { updateAgent } = require('~/models/Agent');
|
||
|
|
await updateAgent({ id: agentId }, { projectIds: [globalProject._id] });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('DELETE /files', () => {
|
||
|
|
it('should allow deleting files owned by the user', async () => {
|
||
|
|
// Create a file owned by the current user
|
||
|
|
const userFileId = uuidv4();
|
||
|
|
await createFile({
|
||
|
|
user: otherUserId,
|
||
|
|
file_id: userFileId,
|
||
|
|
filename: 'user-file.txt',
|
||
|
|
filepath: `/uploads/${otherUserId}/${userFileId}`,
|
||
|
|
bytes: 1024,
|
||
|
|
type: 'text/plain',
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await request(app)
|
||
|
|
.delete('/files')
|
||
|
|
.send({
|
||
|
|
files: [
|
||
|
|
{
|
||
|
|
file_id: userFileId,
|
||
|
|
filepath: `/uploads/${otherUserId}/${userFileId}`,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(200);
|
||
|
|
expect(response.body.message).toBe('Files deleted successfully');
|
||
|
|
expect(processDeleteRequest).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should prevent deleting files not owned by user without agent context', async () => {
|
||
|
|
const response = await request(app)
|
||
|
|
.delete('/files')
|
||
|
|
.send({
|
||
|
|
files: [
|
||
|
|
{
|
||
|
|
file_id: fileId,
|
||
|
|
filepath: `/uploads/${authorId}/${fileId}`,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(403);
|
||
|
|
expect(response.body.message).toBe('You can only delete files you have access to');
|
||
|
|
expect(response.body.unauthorizedFiles).toContain(fileId);
|
||
|
|
expect(processDeleteRequest).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should allow deleting files accessible through shared agent', async () => {
|
||
|
|
const response = await request(app)
|
||
|
|
.delete('/files')
|
||
|
|
.send({
|
||
|
|
agent_id: agentId,
|
||
|
|
files: [
|
||
|
|
{
|
||
|
|
file_id: fileId,
|
||
|
|
filepath: `/uploads/${authorId}/${fileId}`,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(200);
|
||
|
|
expect(response.body.message).toBe('Files deleted successfully');
|
||
|
|
expect(processDeleteRequest).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should prevent deleting files not attached to the specified agent', async () => {
|
||
|
|
// Create another file not attached to the agent
|
||
|
|
const unattachedFileId = uuidv4();
|
||
|
|
await createFile({
|
||
|
|
user: authorId,
|
||
|
|
file_id: unattachedFileId,
|
||
|
|
filename: 'unattached.txt',
|
||
|
|
filepath: `/uploads/${authorId}/${unattachedFileId}`,
|
||
|
|
bytes: 1024,
|
||
|
|
type: 'text/plain',
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await request(app)
|
||
|
|
.delete('/files')
|
||
|
|
.send({
|
||
|
|
agent_id: agentId,
|
||
|
|
files: [
|
||
|
|
{
|
||
|
|
file_id: unattachedFileId,
|
||
|
|
filepath: `/uploads/${authorId}/${unattachedFileId}`,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(403);
|
||
|
|
expect(response.body.message).toBe('You can only delete files you have access to');
|
||
|
|
expect(response.body.unauthorizedFiles).toContain(unattachedFileId);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle mixed authorized and unauthorized files', async () => {
|
||
|
|
// Create a file owned by the current user
|
||
|
|
const userFileId = uuidv4();
|
||
|
|
await createFile({
|
||
|
|
user: otherUserId,
|
||
|
|
file_id: userFileId,
|
||
|
|
filename: 'user-file.txt',
|
||
|
|
filepath: `/uploads/${otherUserId}/${userFileId}`,
|
||
|
|
bytes: 1024,
|
||
|
|
type: 'text/plain',
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create an unauthorized file
|
||
|
|
const unauthorizedFileId = uuidv4();
|
||
|
|
await createFile({
|
||
|
|
user: authorId,
|
||
|
|
file_id: unauthorizedFileId,
|
||
|
|
filename: 'unauthorized.txt',
|
||
|
|
filepath: `/uploads/${authorId}/${unauthorizedFileId}`,
|
||
|
|
bytes: 1024,
|
||
|
|
type: 'text/plain',
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await request(app)
|
||
|
|
.delete('/files')
|
||
|
|
.send({
|
||
|
|
agent_id: agentId,
|
||
|
|
files: [
|
||
|
|
{
|
||
|
|
file_id: fileId, // Authorized through agent
|
||
|
|
filepath: `/uploads/${authorId}/${fileId}`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
file_id: userFileId, // Owned by user
|
||
|
|
filepath: `/uploads/${otherUserId}/${userFileId}`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
file_id: unauthorizedFileId, // Not authorized
|
||
|
|
filepath: `/uploads/${authorId}/${unauthorizedFileId}`,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(403);
|
||
|
|
expect(response.body.message).toBe('You can only delete files you have access to');
|
||
|
|
expect(response.body.unauthorizedFiles).toContain(unauthorizedFileId);
|
||
|
|
expect(response.body.unauthorizedFiles).not.toContain(fileId);
|
||
|
|
expect(response.body.unauthorizedFiles).not.toContain(userFileId);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should prevent deleting files when agent is not collaborative', async () => {
|
||
|
|
// Update the agent to be non-collaborative
|
||
|
|
const { updateAgent } = require('~/models/Agent');
|
||
|
|
await updateAgent({ id: agentId }, { isCollaborative: false });
|
||
|
|
|
||
|
|
const response = await request(app)
|
||
|
|
.delete('/files')
|
||
|
|
.send({
|
||
|
|
agent_id: agentId,
|
||
|
|
files: [
|
||
|
|
{
|
||
|
|
file_id: fileId,
|
||
|
|
filepath: `/uploads/${authorId}/${fileId}`,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(403);
|
||
|
|
expect(response.body.message).toBe('You can only delete files you have access to');
|
||
|
|
expect(response.body.unauthorizedFiles).toContain(fileId);
|
||
|
|
expect(processDeleteRequest).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|