mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-19 08:58:09 +01:00
* chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
425 lines
12 KiB
JavaScript
425 lines
12 KiB
JavaScript
const express = require('express');
|
|
const request = require('supertest');
|
|
const mongoose = require('mongoose');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const { createMethods } = require('@librechat/data-schemas');
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
|
const {
|
|
SystemRoles,
|
|
ResourceType,
|
|
AccessRoleIds,
|
|
PrincipalType,
|
|
} = require('librechat-data-provider');
|
|
const { createAgent, createFile } = require('~/models');
|
|
|
|
// Only mock the external dependencies that we don't want to test
|
|
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 { 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 fileId;
|
|
let File;
|
|
let Agent;
|
|
let AclEntry;
|
|
let User;
|
|
let methods;
|
|
let modelsToCleanup = [];
|
|
|
|
beforeAll(async () => {
|
|
mongoServer = await MongoMemoryServer.create();
|
|
const mongoUri = mongoServer.getUri();
|
|
await mongoose.connect(mongoUri);
|
|
|
|
// Initialize all models using createModels
|
|
const { createModels } = require('@librechat/data-schemas');
|
|
const models = createModels(mongoose);
|
|
|
|
// Track which models we're adding
|
|
modelsToCleanup = Object.keys(models);
|
|
|
|
// Register models on mongoose.models so methods can access them
|
|
Object.assign(mongoose.models, models);
|
|
|
|
// Create methods with our test mongoose instance
|
|
methods = createMethods(mongoose);
|
|
|
|
// Now we can access models from the db/models
|
|
File = models.File;
|
|
Agent = models.Agent;
|
|
AclEntry = models.AclEntry;
|
|
User = models.User;
|
|
|
|
// Seed default roles using our methods
|
|
await methods.seedDefaultRoles();
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
app.use((req, res, next) => {
|
|
req.user = {
|
|
id: otherUserId || 'default-user',
|
|
role: SystemRoles.USER,
|
|
};
|
|
req.app = { locals: {} };
|
|
next();
|
|
});
|
|
|
|
app.use('/files', router);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Clean up all collections before disconnecting
|
|
const collections = mongoose.connection.collections;
|
|
for (const key in collections) {
|
|
await collections[key].deleteMany({});
|
|
}
|
|
|
|
// Clear only the models we added
|
|
for (const modelName of modelsToCleanup) {
|
|
if (mongoose.models[modelName]) {
|
|
delete mongoose.models[modelName];
|
|
}
|
|
}
|
|
|
|
await mongoose.disconnect();
|
|
await mongoServer.stop();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
jest.clearAllMocks();
|
|
|
|
// Clear database - clean up all test data
|
|
await File.deleteMany({});
|
|
await Agent.deleteMany({});
|
|
await User.deleteMany({});
|
|
await AclEntry.deleteMany({});
|
|
// Don't delete AccessRole as they are seeded defaults needed for tests
|
|
|
|
// Create test data
|
|
authorId = new mongoose.Types.ObjectId();
|
|
otherUserId = new mongoose.Types.ObjectId();
|
|
fileId = uuidv4();
|
|
|
|
// Create users in database
|
|
await User.create({
|
|
_id: authorId,
|
|
username: 'author',
|
|
email: 'author@test.com',
|
|
});
|
|
|
|
await User.create({
|
|
_id: otherUserId,
|
|
username: 'other',
|
|
email: 'other@test.com',
|
|
});
|
|
|
|
// Create a file owned by the author
|
|
await createFile({
|
|
user: authorId,
|
|
file_id: fileId,
|
|
filename: 'test.txt',
|
|
filepath: '/uploads/test.txt',
|
|
bytes: 100,
|
|
type: 'text/plain',
|
|
});
|
|
});
|
|
|
|
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/user-file.txt',
|
|
bytes: 200,
|
|
type: 'text/plain',
|
|
});
|
|
|
|
const response = await request(app)
|
|
.delete('/files')
|
|
.send({
|
|
files: [
|
|
{
|
|
file_id: userFileId,
|
|
filepath: '/uploads/user-file.txt',
|
|
},
|
|
],
|
|
});
|
|
|
|
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/test.txt',
|
|
},
|
|
],
|
|
});
|
|
|
|
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 () => {
|
|
// Create an agent with the file attached
|
|
const agent = await createAgent({
|
|
id: uuidv4(),
|
|
name: 'Test Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: authorId,
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: [fileId],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Grant EDIT permission to user on the agent
|
|
const { grantPermission } = require('~/server/services/PermissionService');
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: otherUserId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
const response = await request(app)
|
|
.delete('/files')
|
|
.send({
|
|
agent_id: agent.id,
|
|
files: [
|
|
{
|
|
file_id: fileId,
|
|
filepath: '/uploads/test.txt',
|
|
},
|
|
],
|
|
});
|
|
|
|
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/unattached.txt',
|
|
bytes: 300,
|
|
type: 'text/plain',
|
|
});
|
|
|
|
// Create an agent without the unattached file
|
|
const agent = await createAgent({
|
|
id: uuidv4(),
|
|
name: 'Test Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: authorId,
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: [fileId], // Only fileId, not unattachedFileId
|
|
},
|
|
},
|
|
});
|
|
|
|
// Grant EDIT permission to user on the agent
|
|
const { grantPermission } = require('~/server/services/PermissionService');
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: otherUserId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
const response = await request(app)
|
|
.delete('/files')
|
|
.send({
|
|
agent_id: agent.id,
|
|
files: [
|
|
{
|
|
file_id: unattachedFileId,
|
|
filepath: '/uploads/unattached.txt',
|
|
},
|
|
],
|
|
});
|
|
|
|
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);
|
|
expect(processDeleteRequest).not.toHaveBeenCalled();
|
|
});
|
|
|
|
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/user-file.txt',
|
|
bytes: 200,
|
|
type: 'text/plain',
|
|
});
|
|
|
|
// Create an unauthorized file
|
|
const unauthorizedFileId = uuidv4();
|
|
await createFile({
|
|
user: authorId,
|
|
file_id: unauthorizedFileId,
|
|
filename: 'unauthorized.txt',
|
|
filepath: '/uploads/unauthorized.txt',
|
|
bytes: 400,
|
|
type: 'text/plain',
|
|
});
|
|
|
|
// Create an agent with only fileId attached
|
|
const agent = await createAgent({
|
|
id: uuidv4(),
|
|
name: 'Test Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: authorId,
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: [fileId],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Grant EDIT permission to user on the agent
|
|
const { grantPermission } = require('~/server/services/PermissionService');
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: otherUserId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
const response = await request(app)
|
|
.delete('/files')
|
|
.send({
|
|
agent_id: agent.id,
|
|
files: [
|
|
{ file_id: userFileId, filepath: '/uploads/user-file.txt' },
|
|
{ file_id: fileId, filepath: '/uploads/test.txt' },
|
|
{ file_id: unauthorizedFileId, filepath: '/uploads/unauthorized.txt' },
|
|
],
|
|
});
|
|
|
|
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(processDeleteRequest).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should prevent deleting files when user lacks EDIT permission on agent', async () => {
|
|
// Create an agent with the file attached
|
|
const agent = await createAgent({
|
|
id: uuidv4(),
|
|
name: 'Test Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: authorId,
|
|
tool_resources: {
|
|
file_search: {
|
|
file_ids: [fileId],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Grant only VIEW permission to user on the agent
|
|
const { grantPermission } = require('~/server/services/PermissionService');
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: otherUserId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
const response = await request(app)
|
|
.delete('/files')
|
|
.send({
|
|
agent_id: agent.id,
|
|
files: [
|
|
{
|
|
file_id: fileId,
|
|
filepath: '/uploads/test.txt',
|
|
},
|
|
],
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|