mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-22 23:56:34 +01:00
269 lines
8.3 KiB
JavaScript
269 lines
8.3 KiB
JavaScript
|
|
const mongoose = require('mongoose');
|
||
|
|
|
||
|
|
const mockLogger = { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() };
|
||
|
|
|
||
|
|
jest.mock('@librechat/data-schemas', () => ({
|
||
|
|
logger: mockLogger,
|
||
|
|
}));
|
||
|
|
|
||
|
|
const { ResourceType, PrincipalType } = jest.requireActual('librechat-data-provider');
|
||
|
|
|
||
|
|
jest.mock('librechat-data-provider', () => ({
|
||
|
|
...jest.requireActual('librechat-data-provider'),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('@librechat/api', () => ({
|
||
|
|
enrichRemoteAgentPrincipals: jest.fn(),
|
||
|
|
backfillRemoteAgentPermissions: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const mockBulkUpdateResourcePermissions = jest.fn();
|
||
|
|
|
||
|
|
jest.mock('~/server/services/PermissionService', () => ({
|
||
|
|
bulkUpdateResourcePermissions: (...args) => mockBulkUpdateResourcePermissions(...args),
|
||
|
|
ensureGroupPrincipalExists: jest.fn(),
|
||
|
|
getEffectivePermissions: jest.fn(),
|
||
|
|
ensurePrincipalExists: jest.fn(),
|
||
|
|
getAvailableRoles: jest.fn(),
|
||
|
|
findAccessibleResources: jest.fn(),
|
||
|
|
getResourcePermissionsMap: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/models', () => ({
|
||
|
|
searchPrincipals: jest.fn(),
|
||
|
|
sortPrincipalsByRelevance: jest.fn(),
|
||
|
|
calculateRelevanceScore: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/server/services/GraphApiService', () => ({
|
||
|
|
entraIdPrincipalFeatureEnabled: jest.fn(() => false),
|
||
|
|
searchEntraIdPrincipals: jest.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const mockAgentFindOne = jest.fn();
|
||
|
|
const mockUserUpdateMany = jest.fn();
|
||
|
|
|
||
|
|
jest.mock('~/db/models', () => ({
|
||
|
|
Agent: {
|
||
|
|
findOne: (...args) => mockAgentFindOne(...args),
|
||
|
|
},
|
||
|
|
AclEntry: {},
|
||
|
|
AccessRole: {},
|
||
|
|
User: {
|
||
|
|
updateMany: (...args) => mockUserUpdateMany(...args),
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
const { updateResourcePermissions } = require('../PermissionsController');
|
||
|
|
|
||
|
|
const createMockReq = (overrides = {}) => ({
|
||
|
|
params: { resourceType: ResourceType.AGENT, resourceId: '507f1f77bcf86cd799439011' },
|
||
|
|
body: { updated: [], removed: [], public: false },
|
||
|
|
user: { id: 'user-1', role: 'USER' },
|
||
|
|
headers: { authorization: '' },
|
||
|
|
...overrides,
|
||
|
|
});
|
||
|
|
|
||
|
|
const createMockRes = () => {
|
||
|
|
const res = {};
|
||
|
|
res.status = jest.fn().mockReturnValue(res);
|
||
|
|
res.json = jest.fn().mockReturnValue(res);
|
||
|
|
return res;
|
||
|
|
};
|
||
|
|
|
||
|
|
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
|
||
|
|
|
||
|
|
describe('PermissionsController', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
jest.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('updateResourcePermissions — favorites cleanup', () => {
|
||
|
|
const agentObjectId = new mongoose.Types.ObjectId().toString();
|
||
|
|
const revokedUserId = new mongoose.Types.ObjectId().toString();
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||
|
|
granted: [],
|
||
|
|
updated: [],
|
||
|
|
revoked: [{ type: PrincipalType.USER, id: revokedUserId, name: 'Revoked User' }],
|
||
|
|
errors: [],
|
||
|
|
});
|
||
|
|
|
||
|
|
mockAgentFindOne.mockReturnValue({
|
||
|
|
lean: () => Promise.resolve({ _id: agentObjectId, id: 'agent_abc123' }),
|
||
|
|
});
|
||
|
|
mockUserUpdateMany.mockResolvedValue({ modifiedCount: 1 });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('removes agent from revoked users favorites on AGENT resource type', async () => {
|
||
|
|
const req = createMockReq({
|
||
|
|
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||
|
|
body: {
|
||
|
|
updated: [],
|
||
|
|
removed: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||
|
|
public: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const res = createMockRes();
|
||
|
|
|
||
|
|
await updateResourcePermissions(req, res);
|
||
|
|
await flushPromises();
|
||
|
|
|
||
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
||
|
|
expect(mockAgentFindOne).toHaveBeenCalledWith({ _id: agentObjectId }, { id: 1 });
|
||
|
|
expect(mockUserUpdateMany).toHaveBeenCalledWith(
|
||
|
|
{ _id: { $in: [revokedUserId] }, 'favorites.agentId': 'agent_abc123' },
|
||
|
|
{ $pull: { favorites: { agentId: 'agent_abc123' } } },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('removes agent from revoked users favorites on REMOTE_AGENT resource type', async () => {
|
||
|
|
const req = createMockReq({
|
||
|
|
params: { resourceType: ResourceType.REMOTE_AGENT, resourceId: agentObjectId },
|
||
|
|
body: {
|
||
|
|
updated: [],
|
||
|
|
removed: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||
|
|
public: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const res = createMockRes();
|
||
|
|
|
||
|
|
await updateResourcePermissions(req, res);
|
||
|
|
await flushPromises();
|
||
|
|
|
||
|
|
expect(mockAgentFindOne).toHaveBeenCalledWith({ _id: agentObjectId }, { id: 1 });
|
||
|
|
expect(mockUserUpdateMany).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses results.revoked (validated) not raw request payload', async () => {
|
||
|
|
const validId = new mongoose.Types.ObjectId().toString();
|
||
|
|
const invalidId = 'not-a-valid-id';
|
||
|
|
|
||
|
|
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||
|
|
granted: [],
|
||
|
|
updated: [],
|
||
|
|
revoked: [{ type: PrincipalType.USER, id: validId }],
|
||
|
|
errors: [{ principal: { type: PrincipalType.USER, id: invalidId }, error: 'Invalid ID' }],
|
||
|
|
});
|
||
|
|
|
||
|
|
const req = createMockReq({
|
||
|
|
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||
|
|
body: {
|
||
|
|
updated: [],
|
||
|
|
removed: [
|
||
|
|
{ type: PrincipalType.USER, id: validId },
|
||
|
|
{ type: PrincipalType.USER, id: invalidId },
|
||
|
|
],
|
||
|
|
public: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const res = createMockRes();
|
||
|
|
|
||
|
|
await updateResourcePermissions(req, res);
|
||
|
|
await flushPromises();
|
||
|
|
|
||
|
|
expect(mockUserUpdateMany).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({ _id: { $in: [validId] } }),
|
||
|
|
expect.any(Object),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('skips cleanup when no USER principals are revoked', async () => {
|
||
|
|
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||
|
|
granted: [],
|
||
|
|
updated: [],
|
||
|
|
revoked: [{ type: PrincipalType.GROUP, id: 'group-1' }],
|
||
|
|
errors: [],
|
||
|
|
});
|
||
|
|
|
||
|
|
const req = createMockReq({
|
||
|
|
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||
|
|
body: {
|
||
|
|
updated: [],
|
||
|
|
removed: [{ type: PrincipalType.GROUP, id: 'group-1' }],
|
||
|
|
public: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const res = createMockRes();
|
||
|
|
|
||
|
|
await updateResourcePermissions(req, res);
|
||
|
|
await flushPromises();
|
||
|
|
|
||
|
|
expect(mockAgentFindOne).not.toHaveBeenCalled();
|
||
|
|
expect(mockUserUpdateMany).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('skips cleanup for non-agent resource types', async () => {
|
||
|
|
mockBulkUpdateResourcePermissions.mockResolvedValue({
|
||
|
|
granted: [],
|
||
|
|
updated: [],
|
||
|
|
revoked: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||
|
|
errors: [],
|
||
|
|
});
|
||
|
|
|
||
|
|
const req = createMockReq({
|
||
|
|
params: { resourceType: ResourceType.PROMPTGROUP, resourceId: agentObjectId },
|
||
|
|
body: {
|
||
|
|
updated: [],
|
||
|
|
removed: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||
|
|
public: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const res = createMockRes();
|
||
|
|
|
||
|
|
await updateResourcePermissions(req, res);
|
||
|
|
await flushPromises();
|
||
|
|
|
||
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
||
|
|
expect(mockAgentFindOne).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles agent not found gracefully', async () => {
|
||
|
|
mockAgentFindOne.mockReturnValue({
|
||
|
|
lean: () => Promise.resolve(null),
|
||
|
|
});
|
||
|
|
|
||
|
|
const req = createMockReq({
|
||
|
|
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||
|
|
body: {
|
||
|
|
updated: [],
|
||
|
|
removed: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||
|
|
public: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const res = createMockRes();
|
||
|
|
|
||
|
|
await updateResourcePermissions(req, res);
|
||
|
|
await flushPromises();
|
||
|
|
|
||
|
|
expect(mockAgentFindOne).toHaveBeenCalled();
|
||
|
|
expect(mockUserUpdateMany).not.toHaveBeenCalled();
|
||
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('logs error when User.updateMany fails without blocking response', async () => {
|
||
|
|
mockUserUpdateMany.mockRejectedValue(new Error('DB connection lost'));
|
||
|
|
|
||
|
|
const req = createMockReq({
|
||
|
|
params: { resourceType: ResourceType.AGENT, resourceId: agentObjectId },
|
||
|
|
body: {
|
||
|
|
updated: [],
|
||
|
|
removed: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||
|
|
public: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const res = createMockRes();
|
||
|
|
|
||
|
|
await updateResourcePermissions(req, res);
|
||
|
|
await flushPromises();
|
||
|
|
|
||
|
|
expect(res.status).toHaveBeenCalledWith(200);
|
||
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
||
|
|
'[removeRevokedAgentFromFavorites] Error cleaning up favorites',
|
||
|
|
expect.any(Error),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|