LibreChat/api/server/controllers/__tests__/PermissionsController.spec.js

243 lines
7.7 KiB
JavaScript
Raw Normal View History

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(),
}));
const mockRemoveAgentFromUserFavorites = jest.fn();
jest.mock('~/models', () => ({
searchPrincipals: jest.fn(),
sortPrincipalsByRelevance: jest.fn(),
calculateRelevanceScore: jest.fn(),
removeAgentFromUserFavorites: (...args) => mockRemoveAgentFromUserFavorites(...args),
}));
jest.mock('~/server/services/GraphApiService', () => ({
entraIdPrincipalFeatureEnabled: jest.fn(() => false),
searchEntraIdPrincipals: jest.fn(),
}));
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: [],
});
mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);
});
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(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]);
});
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(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [revokedUserId]);
});
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(mockRemoveAgentFromUserFavorites).toHaveBeenCalledWith(agentObjectId, [validId]);
});
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(mockRemoveAgentFromUserFavorites).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(mockRemoveAgentFromUserFavorites).not.toHaveBeenCalled();
});
it('handles agent not found gracefully', async () => {
mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);
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(mockRemoveAgentFromUserFavorites).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
});
it('logs error when removeAgentFromUserFavorites fails without blocking response', async () => {
mockRemoveAgentFromUserFavorites.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),
);
});
});
});