| const mongoose = require('mongoose'); |
| const { v4: uuidv4 } = require('uuid'); |
| const { nanoid } = require('nanoid'); |
| const { MongoMemoryServer } = require('mongodb-memory-server'); |
| const { agentSchema } = require('@librechat/data-schemas'); |
|
|
| |
| jest.mock('~/server/services/Config', () => ({ |
| getCachedTools: jest.fn().mockResolvedValue({ |
| web_search: true, |
| execute_code: true, |
| file_search: true, |
| }), |
| })); |
|
|
| jest.mock('~/models/Project', () => ({ |
| getProjectByName: jest.fn().mockResolvedValue(null), |
| })); |
|
|
| jest.mock('~/server/services/Files/strategies', () => ({ |
| getStrategyFunctions: jest.fn(), |
| })); |
|
|
| jest.mock('~/server/services/Files/images/avatar', () => ({ |
| resizeAvatar: jest.fn(), |
| })); |
|
|
| jest.mock('~/server/services/Files/S3/crud', () => ({ |
| refreshS3Url: jest.fn(), |
| })); |
|
|
| jest.mock('~/server/services/Files/process', () => ({ |
| filterFile: jest.fn(), |
| })); |
|
|
| jest.mock('~/models/Action', () => ({ |
| updateAction: jest.fn(), |
| getActions: jest.fn().mockResolvedValue([]), |
| })); |
|
|
| jest.mock('~/models/File', () => ({ |
| deleteFileByFilter: jest.fn(), |
| })); |
|
|
| jest.mock('~/server/services/PermissionService', () => ({ |
| findAccessibleResources: jest.fn().mockResolvedValue([]), |
| findPubliclyAccessibleResources: jest.fn().mockResolvedValue([]), |
| grantPermission: jest.fn(), |
| hasPublicPermission: jest.fn().mockResolvedValue(false), |
| checkPermission: jest.fn().mockResolvedValue(true), |
| })); |
|
|
| jest.mock('~/models', () => ({ |
| getCategoriesWithCounts: jest.fn(), |
| })); |
|
|
| const { |
| createAgent: createAgentHandler, |
| updateAgent: updateAgentHandler, |
| getListAgents: getListAgentsHandler, |
| } = require('./v1'); |
|
|
| const { |
| findAccessibleResources, |
| findPubliclyAccessibleResources, |
| } = require('~/server/services/PermissionService'); |
|
|
| |
| |
| |
| let Agent; |
|
|
| describe('Agent Controllers - Mass Assignment Protection', () => { |
| let mongoServer; |
| let mockReq; |
| let mockRes; |
|
|
| beforeAll(async () => { |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| await mongoose.connect(mongoUri); |
| Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| }, 20000); |
|
|
| afterAll(async () => { |
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| }); |
|
|
| beforeEach(async () => { |
| await Agent.deleteMany({}); |
|
|
| |
| jest.clearAllMocks(); |
|
|
| |
| mockReq = { |
| user: { |
| id: new mongoose.Types.ObjectId().toString(), |
| role: 'USER', |
| }, |
| body: {}, |
| params: {}, |
| query: {}, |
| app: { |
| locals: { |
| fileStrategy: 'local', |
| }, |
| }, |
| }; |
|
|
| mockRes = { |
| status: jest.fn().mockReturnThis(), |
| json: jest.fn().mockReturnThis(), |
| }; |
| }); |
|
|
| describe('createAgentHandler', () => { |
| test('should create agent with allowed fields only', async () => { |
| const validData = { |
| name: 'Test Agent', |
| description: 'A test agent', |
| instructions: 'Be helpful', |
| provider: 'openai', |
| model: 'gpt-4', |
| tools: ['web_search'], |
| model_parameters: { temperature: 0.7 }, |
| tool_resources: { |
| file_search: { file_ids: ['file1', 'file2'] }, |
| }, |
| }; |
|
|
| mockReq.body = validData; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
| expect(mockRes.json).toHaveBeenCalled(); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
| expect(createdAgent.name).toBe('Test Agent'); |
| expect(createdAgent.description).toBe('A test agent'); |
| expect(createdAgent.provider).toBe('openai'); |
| expect(createdAgent.model).toBe('gpt-4'); |
| expect(createdAgent.author.toString()).toBe(mockReq.user.id); |
| expect(createdAgent.tools).toContain('web_search'); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: createdAgent.id }); |
| expect(agentInDb).toBeDefined(); |
| expect(agentInDb.name).toBe('Test Agent'); |
| expect(agentInDb.author.toString()).toBe(mockReq.user.id); |
| }); |
|
|
| test('should reject creation with unauthorized fields (mass assignment protection)', async () => { |
| const maliciousData = { |
| |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Malicious Agent', |
|
|
| |
| author: new mongoose.Types.ObjectId().toString(), |
| authorName: 'Hacker', |
| isCollaborative: true, |
| versions: [], |
| _id: new mongoose.Types.ObjectId(), |
| id: 'custom_agent_id', |
| createdAt: new Date('2020-01-01'), |
| updatedAt: new Date('2020-01-01'), |
| }; |
|
|
| mockReq.body = maliciousData; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
|
|
| |
| expect(createdAgent.author.toString()).toBe(mockReq.user.id); |
| expect(createdAgent.authorName).toBeUndefined(); |
| expect(createdAgent.isCollaborative).toBeFalsy(); |
| expect(createdAgent.versions).toHaveLength(1); |
| expect(createdAgent.id).not.toBe('custom_agent_id'); |
| expect(createdAgent.id).toMatch(/^agent_/); |
|
|
| |
| const createdTime = new Date(createdAgent.createdAt).getTime(); |
| const now = Date.now(); |
| expect(now - createdTime).toBeLessThan(5000); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: createdAgent.id }); |
| expect(agentInDb.author.toString()).toBe(mockReq.user.id); |
| expect(agentInDb.authorName).toBeUndefined(); |
| }); |
|
|
| test('should validate required fields', async () => { |
| const invalidData = { |
| name: 'Missing Required Fields', |
| |
| }; |
|
|
| mockReq.body = invalidData; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(400); |
| expect(mockRes.json).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| error: 'Invalid request data', |
| details: expect.any(Array), |
| }), |
| ); |
|
|
| |
| const count = await Agent.countDocuments(); |
| expect(count).toBe(0); |
| }); |
|
|
| test('should handle tool_resources validation', async () => { |
| const dataWithInvalidToolResources = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Agent with Tool Resources', |
| tool_resources: { |
| |
| file_search: { |
| file_ids: ['file1', 'file2'], |
| vector_store_ids: ['vs1'], |
| }, |
| execute_code: { |
| file_ids: ['file3'], |
| }, |
| |
| invalid_resource: { |
| file_ids: ['file4'], |
| }, |
| }, |
| }; |
|
|
| mockReq.body = dataWithInvalidToolResources; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
| expect(createdAgent.tool_resources).toBeDefined(); |
| expect(createdAgent.tool_resources.file_search).toBeDefined(); |
| expect(createdAgent.tool_resources.execute_code).toBeDefined(); |
| expect(createdAgent.tool_resources.invalid_resource).toBeUndefined(); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: createdAgent.id }); |
| expect(agentInDb.tool_resources.invalid_resource).toBeUndefined(); |
| }); |
|
|
| test('should handle support_contact with empty strings', async () => { |
| const dataWithEmptyContact = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Agent with Empty Contact', |
| support_contact: { |
| name: '', |
| email: '', |
| }, |
| }; |
|
|
| mockReq.body = dataWithEmptyContact; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
| expect(createdAgent.name).toBe('Agent with Empty Contact'); |
| expect(createdAgent.support_contact).toBeDefined(); |
| expect(createdAgent.support_contact.name).toBe(''); |
| expect(createdAgent.support_contact.email).toBe(''); |
| }); |
|
|
| test('should handle support_contact with valid email', async () => { |
| const dataWithValidContact = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Agent with Valid Contact', |
| support_contact: { |
| name: 'Support Team', |
| email: 'support@example.com', |
| }, |
| }; |
|
|
| mockReq.body = dataWithValidContact; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
| expect(createdAgent.support_contact).toBeDefined(); |
| expect(createdAgent.support_contact.name).toBe('Support Team'); |
| expect(createdAgent.support_contact.email).toBe('support@example.com'); |
| }); |
|
|
| test('should reject support_contact with invalid email', async () => { |
| const dataWithInvalidEmail = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Agent with Invalid Email', |
| support_contact: { |
| name: 'Support', |
| email: 'not-an-email', |
| }, |
| }; |
|
|
| mockReq.body = dataWithInvalidEmail; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(400); |
| expect(mockRes.json).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| error: 'Invalid request data', |
| details: expect.arrayContaining([ |
| expect.objectContaining({ |
| path: ['support_contact', 'email'], |
| }), |
| ]), |
| }), |
| ); |
| }); |
|
|
| test('should handle avatar validation', async () => { |
| const dataWithAvatar = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Agent with Avatar', |
| avatar: { |
| filepath: 'https://example.com/avatar.png', |
| source: 's3', |
| }, |
| }; |
|
|
| mockReq.body = dataWithAvatar; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
| expect(createdAgent.avatar).toEqual({ |
| filepath: 'https://example.com/avatar.png', |
| source: 's3', |
| }); |
| }); |
|
|
| test('should handle invalid avatar format', async () => { |
| const dataWithInvalidAvatar = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Agent with Invalid Avatar', |
| avatar: 'just-a-string', |
| }; |
|
|
| mockReq.body = dataWithInvalidAvatar; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(400); |
| expect(mockRes.json).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| error: 'Invalid request data', |
| }), |
| ); |
| }); |
| }); |
|
|
| describe('updateAgentHandler', () => { |
| let existingAgentId; |
| let existingAgentAuthorId; |
|
|
| beforeEach(async () => { |
| |
| existingAgentAuthorId = new mongoose.Types.ObjectId(); |
| const agent = await Agent.create({ |
| id: `agent_${uuidv4()}`, |
| name: 'Original Agent', |
| provider: 'openai', |
| model: 'gpt-3.5-turbo', |
| author: existingAgentAuthorId, |
| description: 'Original description', |
| isCollaborative: false, |
| versions: [ |
| { |
| name: 'Original Agent', |
| provider: 'openai', |
| model: 'gpt-3.5-turbo', |
| description: 'Original description', |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
| existingAgentId = agent.id; |
| }); |
|
|
| test('should update agent with allowed fields only', async () => { |
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| name: 'Updated Agent', |
| description: 'Updated description', |
| model: 'gpt-4', |
| isCollaborative: true, |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).not.toHaveBeenCalledWith(400); |
| expect(mockRes.status).not.toHaveBeenCalledWith(403); |
| expect(mockRes.json).toHaveBeenCalled(); |
|
|
| const updatedAgent = mockRes.json.mock.calls[0][0]; |
| expect(updatedAgent.name).toBe('Updated Agent'); |
| expect(updatedAgent.description).toBe('Updated description'); |
| expect(updatedAgent.model).toBe('gpt-4'); |
| expect(updatedAgent.isCollaborative).toBe(true); |
| expect(updatedAgent.author).toBe(existingAgentAuthorId.toString()); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: existingAgentId }); |
| expect(agentInDb.name).toBe('Updated Agent'); |
| expect(agentInDb.isCollaborative).toBe(true); |
| }); |
|
|
| test('should reject update with unauthorized fields (mass assignment protection)', async () => { |
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| name: 'Updated Name', |
|
|
| |
| author: new mongoose.Types.ObjectId().toString(), |
| authorName: 'Hacker', |
| id: 'different_agent_id', |
| _id: new mongoose.Types.ObjectId(), |
| versions: [], |
| createdAt: new Date('2020-01-01'), |
| updatedAt: new Date('2020-01-01'), |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.json).toHaveBeenCalled(); |
|
|
| const updatedAgent = mockRes.json.mock.calls[0][0]; |
|
|
| |
| expect(updatedAgent.author).toBe(existingAgentAuthorId.toString()); |
| expect(updatedAgent.authorName).toBeUndefined(); |
| expect(updatedAgent.id).toBe(existingAgentId); |
| expect(updatedAgent.name).toBe('Updated Name'); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: existingAgentId }); |
| expect(agentInDb.author.toString()).toBe(existingAgentAuthorId.toString()); |
| expect(agentInDb.id).toBe(existingAgentId); |
| }); |
|
|
| test('should allow admin to update any agent', async () => { |
| const adminUserId = new mongoose.Types.ObjectId().toString(); |
| mockReq.user.id = adminUserId; |
| mockReq.user.role = 'ADMIN'; |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| name: 'Admin Update', |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).not.toHaveBeenCalledWith(403); |
| expect(mockRes.json).toHaveBeenCalled(); |
|
|
| const updatedAgent = mockRes.json.mock.calls[0][0]; |
| expect(updatedAgent.name).toBe('Admin Update'); |
| }); |
|
|
| test('should handle projectIds updates', async () => { |
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
|
|
| const projectId1 = new mongoose.Types.ObjectId().toString(); |
| const projectId2 = new mongoose.Types.ObjectId().toString(); |
|
|
| mockReq.body = { |
| projectIds: [projectId1, projectId2], |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.json).toHaveBeenCalled(); |
|
|
| const updatedAgent = mockRes.json.mock.calls[0][0]; |
| expect(updatedAgent).toBeDefined(); |
| |
| }); |
|
|
| test('should validate tool_resources in updates', async () => { |
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| tool_resources: { |
| |
| ocr: { |
| file_ids: ['ocr1', 'ocr2'], |
| }, |
| execute_code: { |
| file_ids: ['img1'], |
| }, |
| |
| invalid_tool: { |
| file_ids: ['invalid'], |
| }, |
| }, |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.json).toHaveBeenCalled(); |
|
|
| const updatedAgent = mockRes.json.mock.calls[0][0]; |
| expect(updatedAgent.tool_resources).toBeDefined(); |
| expect(updatedAgent.tool_resources.ocr).toBeUndefined(); |
| expect(updatedAgent.tool_resources.context).toBeDefined(); |
| expect(updatedAgent.tool_resources.execute_code).toBeDefined(); |
| expect(updatedAgent.tool_resources.invalid_tool).toBeUndefined(); |
| }); |
|
|
| test('should return 404 for non-existent agent', async () => { |
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = `agent_${uuidv4()}`; |
| mockReq.body = { |
| name: 'Update Non-existent', |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(404); |
| expect(mockRes.json).toHaveBeenCalledWith({ error: 'Agent not found' }); |
| }); |
|
|
| test('should include version field in update response', async () => { |
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| name: 'Updated with Version Check', |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.json).toHaveBeenCalled(); |
| const updatedAgent = mockRes.json.mock.calls[0][0]; |
|
|
| |
| expect(updatedAgent).toHaveProperty('version'); |
| expect(typeof updatedAgent.version).toBe('number'); |
| expect(updatedAgent.version).toBeGreaterThanOrEqual(1); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: existingAgentId }); |
| expect(updatedAgent.version).toBe(agentInDb.versions.length); |
| }); |
|
|
| test('should allow resetting avatar when value is explicitly null', async () => { |
| await Agent.updateOne( |
| { id: existingAgentId }, |
| { |
| avatar: { |
| filepath: 'https://example.com/avatar.png', |
| source: 's3', |
| }, |
| }, |
| ); |
|
|
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| avatar: null, |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| const updatedAgent = mockRes.json.mock.calls[0][0]; |
| expect(updatedAgent.avatar).toBeNull(); |
|
|
| const agentInDb = await Agent.findOne({ id: existingAgentId }); |
| expect(agentInDb.avatar).toBeNull(); |
| }); |
|
|
| test('should ignore avatar field when value is undefined', async () => { |
| const originalAvatar = { |
| filepath: 'https://example.com/original.png', |
| source: 's3', |
| }; |
| await Agent.updateOne({ id: existingAgentId }, { avatar: originalAvatar }); |
|
|
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| avatar: undefined, |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| const agentInDb = await Agent.findOne({ id: existingAgentId }); |
| expect(agentInDb.avatar.filepath).toBe(originalAvatar.filepath); |
| expect(agentInDb.avatar.source).toBe(originalAvatar.source); |
| }); |
|
|
| test('should not bump version when no mutable fields change', async () => { |
| const existingAgent = await Agent.findOne({ id: existingAgentId }); |
| const originalVersionCount = existingAgent.versions.length; |
|
|
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| avatar: undefined, |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| const agentInDb = await Agent.findOne({ id: existingAgentId }); |
| expect(agentInDb.versions.length).toBe(originalVersionCount); |
| }); |
|
|
| test('should handle validation errors properly', async () => { |
| mockReq.user.id = existingAgentAuthorId.toString(); |
| mockReq.params.id = existingAgentId; |
| mockReq.body = { |
| model_parameters: 'invalid-not-an-object', |
| }; |
|
|
| await updateAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(400); |
| expect(mockRes.json).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| error: 'Invalid request data', |
| details: expect.any(Array), |
| }), |
| ); |
| }); |
| }); |
|
|
| describe('Mass Assignment Attack Scenarios', () => { |
| test('should prevent setting system fields during creation', async () => { |
| const systemFields = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'System Fields Test', |
|
|
| |
| __v: 99, |
| _id: new mongoose.Types.ObjectId(), |
| versions: [ |
| { |
| name: 'Fake Version', |
| provider: 'fake', |
| model: 'fake-model', |
| }, |
| ], |
| }; |
|
|
| mockReq.body = systemFields; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
|
|
| |
| expect(createdAgent.__v).not.toBe(99); |
| expect(createdAgent.versions).toHaveLength(1); |
| expect(createdAgent.versions[0].name).toBe('System Fields Test'); |
| expect(createdAgent.versions[0].provider).toBe('openai'); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: createdAgent.id }); |
| expect(agentInDb.__v).not.toBe(99); |
| }); |
|
|
| test('should prevent author hijacking', async () => { |
| const originalAuthorId = new mongoose.Types.ObjectId(); |
| const attackerId = new mongoose.Types.ObjectId(); |
|
|
| |
| mockReq.user.id = originalAuthorId.toString(); |
| mockReq.user.role = 'ADMIN'; |
| mockReq.body = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Admin Agent', |
| author: attackerId.toString(), |
| }; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
|
|
| |
| expect(createdAgent.author.toString()).toBe(originalAuthorId.toString()); |
| expect(createdAgent.author.toString()).not.toBe(attackerId.toString()); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: createdAgent.id }); |
| expect(agentInDb.author.toString()).toBe(originalAuthorId.toString()); |
| }); |
|
|
| test('should strip unknown fields to prevent future vulnerabilities', async () => { |
| mockReq.body = { |
| provider: 'openai', |
| model: 'gpt-4', |
| name: 'Future Proof Test', |
|
|
| |
| superAdminAccess: true, |
| bypassAllChecks: true, |
| internalFlag: 'secret', |
| futureFeature: 'exploit', |
| }; |
|
|
| await createAgentHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(201); |
|
|
| const createdAgent = mockRes.json.mock.calls[0][0]; |
|
|
| |
| expect(createdAgent.superAdminAccess).toBeUndefined(); |
| expect(createdAgent.bypassAllChecks).toBeUndefined(); |
| expect(createdAgent.internalFlag).toBeUndefined(); |
| expect(createdAgent.futureFeature).toBeUndefined(); |
|
|
| |
| const agentInDb = await Agent.findOne({ id: createdAgent.id }).lean(); |
| expect(agentInDb.superAdminAccess).toBeUndefined(); |
| expect(agentInDb.bypassAllChecks).toBeUndefined(); |
| expect(agentInDb.internalFlag).toBeUndefined(); |
| expect(agentInDb.futureFeature).toBeUndefined(); |
| }); |
| }); |
|
|
| describe('getListAgentsHandler - Security Tests', () => { |
| let userA, userB; |
| let agentA1, agentA2, agentA3, agentB1; |
|
|
| beforeEach(async () => { |
| await Agent.deleteMany({}); |
| jest.clearAllMocks(); |
|
|
| |
| userA = new mongoose.Types.ObjectId(); |
| userB = new mongoose.Types.ObjectId(); |
|
|
| |
| agentA1 = await Agent.create({ |
| id: `agent_${nanoid(12)}`, |
| name: 'Agent A1', |
| description: 'User A agent 1', |
| provider: 'openai', |
| model: 'gpt-4', |
| author: userA, |
| versions: [ |
| { |
| name: 'Agent A1', |
| description: 'User A agent 1', |
| provider: 'openai', |
| model: 'gpt-4', |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
|
|
| agentA2 = await Agent.create({ |
| id: `agent_${nanoid(12)}`, |
| name: 'Agent A2', |
| description: 'User A agent 2', |
| provider: 'openai', |
| model: 'gpt-4', |
| author: userA, |
| versions: [ |
| { |
| name: 'Agent A2', |
| description: 'User A agent 2', |
| provider: 'openai', |
| model: 'gpt-4', |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
|
|
| agentA3 = await Agent.create({ |
| id: `agent_${nanoid(12)}`, |
| name: 'Agent A3', |
| description: 'User A agent 3', |
| provider: 'openai', |
| model: 'gpt-4', |
| author: userA, |
| category: 'productivity', |
| versions: [ |
| { |
| name: 'Agent A3', |
| description: 'User A agent 3', |
| provider: 'openai', |
| model: 'gpt-4', |
| category: 'productivity', |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
|
|
| |
| agentB1 = await Agent.create({ |
| id: `agent_${nanoid(12)}`, |
| name: 'Agent B1', |
| description: 'User B agent 1', |
| provider: 'openai', |
| model: 'gpt-4', |
| author: userB, |
| versions: [ |
| { |
| name: 'Agent B1', |
| description: 'User B agent 1', |
| provider: 'openai', |
| model: 'gpt-4', |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
| }); |
|
|
| test('should return empty list when user has no accessible agents', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| findAccessibleResources.mockResolvedValue([]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| expect(findAccessibleResources).toHaveBeenCalledWith({ |
| userId: userB.toString(), |
| role: 'USER', |
| resourceType: 'agent', |
| requiredPermissions: 1, |
| }); |
|
|
| expect(mockRes.json).toHaveBeenCalledWith({ |
| object: 'list', |
| data: [], |
| first_id: null, |
| last_id: null, |
| has_more: false, |
| after: null, |
| }); |
| }); |
|
|
| test('should not return other users agents when accessibleIds is empty', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| findAccessibleResources.mockResolvedValue([]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(0); |
|
|
| |
| const agentIds = response.data.map((a) => a.id); |
| expect(agentIds).not.toContain(agentA1.id); |
| expect(agentIds).not.toContain(agentA2.id); |
| expect(agentIds).not.toContain(agentA3.id); |
| }); |
|
|
| test('should only return agents user has access to', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| findAccessibleResources.mockResolvedValue([agentA1._id]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(1); |
| expect(response.data[0].id).toBe(agentA1.id); |
| expect(response.data[0].name).toBe('Agent A1'); |
| }); |
|
|
| test('should return multiple accessible agents', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| findAccessibleResources.mockResolvedValue([agentA1._id, agentA3._id, agentB1._id]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(3); |
|
|
| const agentIds = response.data.map((a) => a.id); |
| expect(agentIds).toContain(agentA1.id); |
| expect(agentIds).toContain(agentA3.id); |
| expect(agentIds).toContain(agentB1.id); |
| expect(agentIds).not.toContain(agentA2.id); |
| }); |
|
|
| test('should apply category filter correctly with ACL', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| mockReq.query.category = 'productivity'; |
| findAccessibleResources.mockResolvedValue([agentA1._id, agentA2._id, agentA3._id]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(1); |
| expect(response.data[0].id).toBe(agentA3.id); |
| expect(response.data[0].category).toBe('productivity'); |
| }); |
|
|
| test('should apply search filter correctly with ACL', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| mockReq.query.search = 'A2'; |
| findAccessibleResources.mockResolvedValue([agentA1._id, agentA2._id, agentA3._id]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(1); |
| expect(response.data[0].id).toBe(agentA2.id); |
| }); |
|
|
| test('should handle pagination with ACL filtering', async () => { |
| |
| const moreAgents = []; |
| for (let i = 4; i <= 10; i++) { |
| const agent = await Agent.create({ |
| id: `agent_${nanoid(12)}`, |
| name: `Agent A${i}`, |
| description: `User A agent ${i}`, |
| provider: 'openai', |
| model: 'gpt-4', |
| author: userA, |
| versions: [ |
| { |
| name: `Agent A${i}`, |
| description: `User A agent ${i}`, |
| provider: 'openai', |
| model: 'gpt-4', |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
| moreAgents.push(agent); |
| } |
|
|
| |
| const allAgentIds = [agentA1, agentA2, agentA3, ...moreAgents].map((a) => a._id); |
| mockReq.user.id = userB.toString(); |
| mockReq.query.limit = '5'; |
| findAccessibleResources.mockResolvedValue(allAgentIds); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(5); |
| expect(response.has_more).toBe(true); |
| expect(response.after).toBeTruthy(); |
| }); |
|
|
| test('should mark publicly accessible agents', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| findAccessibleResources.mockResolvedValue([agentA1._id, agentA2._id]); |
| findPubliclyAccessibleResources.mockResolvedValue([agentA2._id]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(2); |
|
|
| const publicAgent = response.data.find((a) => a.id === agentA2.id); |
| const privateAgent = response.data.find((a) => a.id === agentA1.id); |
|
|
| expect(publicAgent.isPublic).toBe(true); |
| expect(privateAgent.isPublic).toBeUndefined(); |
| }); |
|
|
| test('should handle requiredPermission parameter', async () => { |
| |
| mockReq.user.id = userB.toString(); |
| mockReq.query.requiredPermission = '15'; |
| findAccessibleResources.mockResolvedValue([agentA1._id]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| expect(findAccessibleResources).toHaveBeenCalledWith({ |
| userId: userB.toString(), |
| role: 'USER', |
| resourceType: 'agent', |
| requiredPermissions: 15, |
| }); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(1); |
| }); |
|
|
| test('should handle promoted filter with ACL', async () => { |
| |
| const promotedAgent = await Agent.create({ |
| id: `agent_${nanoid(12)}`, |
| name: 'Promoted Agent', |
| description: 'A promoted agent', |
| provider: 'openai', |
| model: 'gpt-4', |
| author: userA, |
| is_promoted: true, |
| versions: [ |
| { |
| name: 'Promoted Agent', |
| description: 'A promoted agent', |
| provider: 'openai', |
| model: 'gpt-4', |
| is_promoted: true, |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
|
|
| mockReq.user.id = userB.toString(); |
| mockReq.query.promoted = '1'; |
| findAccessibleResources.mockResolvedValue([agentA1._id, agentA2._id, promotedAgent._id]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(1); |
| expect(response.data[0].id).toBe(promotedAgent.id); |
| expect(response.data[0].is_promoted).toBe(true); |
| }); |
|
|
| test('should handle errors gracefully', async () => { |
| mockReq.user.id = userB.toString(); |
| findAccessibleResources.mockRejectedValue(new Error('Permission service error')); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| expect(mockRes.status).toHaveBeenCalledWith(500); |
| expect(mockRes.json).toHaveBeenCalledWith({ |
| error: 'Permission service error', |
| }); |
| }); |
|
|
| test('should respect combined filters with ACL', async () => { |
| |
| const productivityPromoted = await Agent.create({ |
| id: `agent_${nanoid(12)}`, |
| name: 'Productivity Pro', |
| description: 'A promoted productivity agent', |
| provider: 'openai', |
| model: 'gpt-4', |
| author: userA, |
| category: 'productivity', |
| is_promoted: true, |
| versions: [ |
| { |
| name: 'Productivity Pro', |
| description: 'A promoted productivity agent', |
| provider: 'openai', |
| model: 'gpt-4', |
| category: 'productivity', |
| is_promoted: true, |
| createdAt: new Date(), |
| updatedAt: new Date(), |
| }, |
| ], |
| }); |
|
|
| mockReq.user.id = userB.toString(); |
| mockReq.query.category = 'productivity'; |
| mockReq.query.promoted = '1'; |
| findAccessibleResources.mockResolvedValue([ |
| agentA1._id, |
| agentA2._id, |
| agentA3._id, |
| productivityPromoted._id, |
| ]); |
| findPubliclyAccessibleResources.mockResolvedValue([]); |
|
|
| await getListAgentsHandler(mockReq, mockRes); |
|
|
| const response = mockRes.json.mock.calls[0][0]; |
| expect(response.data).toHaveLength(1); |
| expect(response.data[0].id).toBe(productivityPromoted.id); |
| expect(response.data[0].category).toBe('productivity'); |
| expect(response.data[0].is_promoted).toBe(true); |
| }); |
| }); |
| }); |
|
|