mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🤖 fix: Edge Case for Agent List Access Control
- Refactored `getListAgentsByAccess` to streamline query construction for accessible agents. - Added comprehensive security tests for `getListAgentsByAccess` and `getListAgentsHandler` to ensure proper access control and filtering based on user permissions. - Enhanced test coverage for various scenarios, including pagination, category filtering, and handling of non-existent IDs.
This commit is contained in:
parent
c191af6c9b
commit
9585db14ba
3 changed files with 600 additions and 6 deletions
|
|
@ -533,11 +533,7 @@ const getListAgentsByAccess = async ({
|
||||||
const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null;
|
const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null;
|
||||||
|
|
||||||
// Build base query combining ACL accessible agents with other filters
|
// Build base query combining ACL accessible agents with other filters
|
||||||
const baseQuery = { ...otherParams };
|
const baseQuery = { ...otherParams, _id: { $in: accessibleIds } };
|
||||||
|
|
||||||
if (accessibleIds.length > 0) {
|
|
||||||
baseQuery._id = { $in: accessibleIds };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cursor condition
|
// Add cursor condition
|
||||||
if (after) {
|
if (after) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const {
|
||||||
updateAgent,
|
updateAgent,
|
||||||
deleteAgent,
|
deleteAgent,
|
||||||
getListAgents,
|
getListAgents,
|
||||||
|
getListAgentsByAccess,
|
||||||
revertAgentVersion,
|
revertAgentVersion,
|
||||||
updateAgentProjects,
|
updateAgentProjects,
|
||||||
addAgentResourceFile,
|
addAgentResourceFile,
|
||||||
|
|
@ -3034,6 +3035,212 @@ describe('Support Contact Field', () => {
|
||||||
// Verify support_contact is undefined when not provided
|
// Verify support_contact is undefined when not provided
|
||||||
expect(agent.support_contact).toBeUndefined();
|
expect(agent.support_contact).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getListAgentsByAccess - Security Tests', () => {
|
||||||
|
let userA, userB;
|
||||||
|
let agentA1, agentA2, agentA3;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema);
|
||||||
|
await Agent.deleteMany({});
|
||||||
|
await AclEntry.deleteMany({});
|
||||||
|
|
||||||
|
// Create two users
|
||||||
|
userA = new mongoose.Types.ObjectId();
|
||||||
|
userB = new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
|
// Create agents for user A
|
||||||
|
agentA1 = await createAgent({
|
||||||
|
id: `agent_${uuidv4().slice(0, 12)}`,
|
||||||
|
name: 'Agent A1',
|
||||||
|
description: 'User A agent 1',
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4',
|
||||||
|
author: userA,
|
||||||
|
});
|
||||||
|
|
||||||
|
agentA2 = await createAgent({
|
||||||
|
id: `agent_${uuidv4().slice(0, 12)}`,
|
||||||
|
name: 'Agent A2',
|
||||||
|
description: 'User A agent 2',
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4',
|
||||||
|
author: userA,
|
||||||
|
});
|
||||||
|
|
||||||
|
agentA3 = await createAgent({
|
||||||
|
id: `agent_${uuidv4().slice(0, 12)}`,
|
||||||
|
name: 'Agent A3',
|
||||||
|
description: 'User A agent 3',
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4',
|
||||||
|
author: userA,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty list when user has no accessible agents (empty accessibleIds)', async () => {
|
||||||
|
// User B has no agents and no shared agents
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds: [],
|
||||||
|
otherParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(0);
|
||||||
|
expect(result.has_more).toBe(false);
|
||||||
|
expect(result.first_id).toBeNull();
|
||||||
|
expect(result.last_id).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not return other users agents when accessibleIds is empty', async () => {
|
||||||
|
// User B trying to list agents with empty accessibleIds should not see User A's agents
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds: [],
|
||||||
|
otherParams: { author: userB },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(0);
|
||||||
|
expect(result.has_more).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should only return agents in accessibleIds list', async () => {
|
||||||
|
// Give User B access to only one of User A's agents
|
||||||
|
const accessibleIds = [agentA1._id];
|
||||||
|
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds,
|
||||||
|
otherParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(1);
|
||||||
|
expect(result.data[0].id).toBe(agentA1.id);
|
||||||
|
expect(result.data[0].name).toBe('Agent A1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return multiple accessible agents when provided', async () => {
|
||||||
|
// Give User B access to two of User A's agents
|
||||||
|
const accessibleIds = [agentA1._id, agentA3._id];
|
||||||
|
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds,
|
||||||
|
otherParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(2);
|
||||||
|
const returnedIds = result.data.map((agent) => agent.id);
|
||||||
|
expect(returnedIds).toContain(agentA1.id);
|
||||||
|
expect(returnedIds).toContain(agentA3.id);
|
||||||
|
expect(returnedIds).not.toContain(agentA2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect other query parameters while enforcing accessibleIds', async () => {
|
||||||
|
// Give access to all agents but filter by name
|
||||||
|
const accessibleIds = [agentA1._id, agentA2._id, agentA3._id];
|
||||||
|
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds,
|
||||||
|
otherParams: { name: 'Agent A2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(1);
|
||||||
|
expect(result.data[0].id).toBe(agentA2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle pagination correctly with accessibleIds filter', async () => {
|
||||||
|
// Create more agents
|
||||||
|
const moreAgents = [];
|
||||||
|
for (let i = 4; i <= 10; i++) {
|
||||||
|
const agent = await createAgent({
|
||||||
|
id: `agent_${uuidv4().slice(0, 12)}`,
|
||||||
|
name: `Agent A${i}`,
|
||||||
|
description: `User A agent ${i}`,
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4',
|
||||||
|
author: userA,
|
||||||
|
});
|
||||||
|
moreAgents.push(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give access to all agents
|
||||||
|
const allAgentIds = [agentA1, agentA2, agentA3, ...moreAgents].map((a) => a._id);
|
||||||
|
|
||||||
|
// First page
|
||||||
|
const page1 = await getListAgentsByAccess({
|
||||||
|
accessibleIds: allAgentIds,
|
||||||
|
otherParams: {},
|
||||||
|
limit: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(page1.data).toHaveLength(5);
|
||||||
|
expect(page1.has_more).toBe(true);
|
||||||
|
expect(page1.after).toBeTruthy();
|
||||||
|
|
||||||
|
// Second page
|
||||||
|
const page2 = await getListAgentsByAccess({
|
||||||
|
accessibleIds: allAgentIds,
|
||||||
|
otherParams: {},
|
||||||
|
limit: 5,
|
||||||
|
after: page1.after,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(page2.data).toHaveLength(5);
|
||||||
|
expect(page2.has_more).toBe(false);
|
||||||
|
|
||||||
|
// Verify no overlap between pages
|
||||||
|
const page1Ids = page1.data.map((a) => a.id);
|
||||||
|
const page2Ids = page2.data.map((a) => a.id);
|
||||||
|
const intersection = page1Ids.filter((id) => page2Ids.includes(id));
|
||||||
|
expect(intersection).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty list when accessibleIds contains non-existent IDs', async () => {
|
||||||
|
// Try with non-existent agent IDs
|
||||||
|
const fakeIds = [new mongoose.Types.ObjectId(), new mongoose.Types.ObjectId()];
|
||||||
|
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds: fakeIds,
|
||||||
|
otherParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(0);
|
||||||
|
expect(result.has_more).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle undefined accessibleIds as empty array', async () => {
|
||||||
|
// When accessibleIds is undefined, it should be treated as empty array
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds: undefined,
|
||||||
|
otherParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(0);
|
||||||
|
expect(result.has_more).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should combine accessibleIds with author filter correctly', async () => {
|
||||||
|
// Create an agent for User B
|
||||||
|
const agentB1 = await createAgent({
|
||||||
|
id: `agent_${uuidv4().slice(0, 12)}`,
|
||||||
|
name: 'Agent B1',
|
||||||
|
description: 'User B agent 1',
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4',
|
||||||
|
author: userB,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give User B access to one of User A's agents
|
||||||
|
const accessibleIds = [agentA1._id, agentB1._id];
|
||||||
|
|
||||||
|
// Filter by author should further restrict the results
|
||||||
|
const result = await getListAgentsByAccess({
|
||||||
|
accessibleIds,
|
||||||
|
otherParams: { author: userB },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(1);
|
||||||
|
expect(result.data[0].id).toBe(agentB1.id);
|
||||||
|
expect(result.data[0].author).toBe(userB.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createBasicAgent(overrides = {}) {
|
function createBasicAgent(overrides = {}) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const { nanoid } = require('nanoid');
|
||||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
const { agentSchema } = require('@librechat/data-schemas');
|
const { agentSchema } = require('@librechat/data-schemas');
|
||||||
|
|
||||||
|
|
@ -41,7 +42,27 @@ jest.mock('~/models/File', () => ({
|
||||||
deleteFileByFilter: jest.fn(),
|
deleteFileByFilter: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { createAgent: createAgentHandler, updateAgent: updateAgentHandler } = require('./v1');
|
jest.mock('~/server/services/PermissionService', () => ({
|
||||||
|
findAccessibleResources: jest.fn().mockResolvedValue([]),
|
||||||
|
findPubliclyAccessibleResources: jest.fn().mockResolvedValue([]),
|
||||||
|
grantPermission: jest.fn(),
|
||||||
|
hasPublicPermission: jest.fn().mockResolvedValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/models', () => ({
|
||||||
|
getCategoriesWithCounts: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
createAgent: createAgentHandler,
|
||||||
|
updateAgent: updateAgentHandler,
|
||||||
|
getListAgents: getListAgentsHandler,
|
||||||
|
} = require('./v1');
|
||||||
|
|
||||||
|
const {
|
||||||
|
findAccessibleResources,
|
||||||
|
findPubliclyAccessibleResources,
|
||||||
|
} = require('~/server/services/PermissionService');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('mongoose').Model<import('@librechat/data-schemas').IAgent>}
|
* @type {import('mongoose').Model<import('@librechat/data-schemas').IAgent>}
|
||||||
|
|
@ -79,6 +100,7 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
||||||
},
|
},
|
||||||
body: {},
|
body: {},
|
||||||
params: {},
|
params: {},
|
||||||
|
query: {},
|
||||||
app: {
|
app: {
|
||||||
locals: {
|
locals: {
|
||||||
fileStrategy: 'local',
|
fileStrategy: 'local',
|
||||||
|
|
@ -668,4 +690,373 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
||||||
expect(agentInDb.futureFeature).toBeUndefined();
|
expect(agentInDb.futureFeature).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getListAgentsHandler - Security Tests', () => {
|
||||||
|
let userA, userB;
|
||||||
|
let agentA1, agentA2, agentA3, agentB1;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Agent.deleteMany({});
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Create two test users
|
||||||
|
userA = new mongoose.Types.ObjectId();
|
||||||
|
userB = new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
|
// Create agents for User A
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an agent for User B
|
||||||
|
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 () => {
|
||||||
|
// User B has no permissions and no owned agents
|
||||||
|
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, // VIEW permission
|
||||||
|
});
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
// User B trying to see agents with no permissions
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Verify User A's agents are not included
|
||||||
|
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 () => {
|
||||||
|
// User B has access to one of User A's agents
|
||||||
|
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 () => {
|
||||||
|
// User B has access to multiple agents
|
||||||
|
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 () => {
|
||||||
|
// User has access to all agents but filters by category
|
||||||
|
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 () => {
|
||||||
|
// User has access to multiple agents but searches for specific one
|
||||||
|
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 () => {
|
||||||
|
// Create more agents for pagination testing
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User has access to all agents
|
||||||
|
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 () => {
|
||||||
|
// User has access to agents, some are public
|
||||||
|
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 () => {
|
||||||
|
// Test with different permission levels
|
||||||
|
mockReq.user.id = userB.toString();
|
||||||
|
mockReq.query.requiredPermission = '15'; // FULL_ACCESS
|
||||||
|
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 () => {
|
||||||
|
// Create a promoted agent
|
||||||
|
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 () => {
|
||||||
|
// Create agents with specific attributes
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue