mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00

- Replaced string literals for principal types ('user', 'group', 'public') with the new PrincipalType enum across various models, services, and tests for improved type safety and consistency. - Updated permission handling in multiple files to utilize the PrincipalType enum, enhancing maintainability and reducing potential errors. - Ensured all relevant tests reflect these changes to maintain coverage and functionality.
616 lines
19 KiB
JavaScript
616 lines
19 KiB
JavaScript
const express = require('express');
|
|
const request = require('supertest');
|
|
const mongoose = require('mongoose');
|
|
const { ObjectId } = require('mongodb');
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
|
const {
|
|
SystemRoles,
|
|
ResourceType,
|
|
AccessRoleIds,
|
|
PrincipalType,
|
|
PermissionBits,
|
|
} = require('librechat-data-provider');
|
|
|
|
// Mock modules before importing
|
|
jest.mock('~/server/services/Config', () => ({
|
|
getCachedTools: jest.fn().mockResolvedValue({}),
|
|
getCustomConfig: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/models/Role', () => ({
|
|
getRoleByName: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/server/middleware', () => ({
|
|
requireJwtAuth: (req, res, next) => next(),
|
|
canAccessPromptViaGroup: jest.requireActual('~/server/middleware').canAccessPromptViaGroup,
|
|
canAccessPromptGroupResource:
|
|
jest.requireActual('~/server/middleware').canAccessPromptGroupResource,
|
|
}));
|
|
|
|
let app;
|
|
let mongoServer;
|
|
let promptRoutes;
|
|
let Prompt, PromptGroup, AclEntry, AccessRole, User;
|
|
let testUsers, testRoles;
|
|
let grantPermission;
|
|
|
|
// Helper function to set user in middleware
|
|
function setTestUser(app, user) {
|
|
app.use((req, res, next) => {
|
|
req.user = {
|
|
...(user.toObject ? user.toObject() : user),
|
|
id: user.id || user._id.toString(),
|
|
_id: user._id,
|
|
name: user.name,
|
|
role: user.role,
|
|
};
|
|
if (user.role === SystemRoles.ADMIN) {
|
|
console.log('Setting admin user with role:', req.user.role);
|
|
}
|
|
next();
|
|
});
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
mongoServer = await MongoMemoryServer.create();
|
|
const mongoUri = mongoServer.getUri();
|
|
await mongoose.connect(mongoUri);
|
|
|
|
// Initialize models
|
|
const dbModels = require('~/db/models');
|
|
Prompt = dbModels.Prompt;
|
|
PromptGroup = dbModels.PromptGroup;
|
|
AclEntry = dbModels.AclEntry;
|
|
AccessRole = dbModels.AccessRole;
|
|
User = dbModels.User;
|
|
|
|
// Import permission service
|
|
const permissionService = require('~/server/services/PermissionService');
|
|
grantPermission = permissionService.grantPermission;
|
|
|
|
// Create test data
|
|
await setupTestData();
|
|
|
|
// Setup Express app
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
// Mock authentication middleware - default to owner
|
|
setTestUser(app, testUsers.owner);
|
|
|
|
// Import routes after mocks are set up
|
|
promptRoutes = require('./prompts');
|
|
app.use('/api/prompts', promptRoutes);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await mongoose.disconnect();
|
|
await mongoServer.stop();
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
async function setupTestData() {
|
|
// Create access roles for promptGroups
|
|
testRoles = {
|
|
viewer: await AccessRole.create({
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
|
name: 'Viewer',
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
permBits: PermissionBits.VIEW,
|
|
}),
|
|
editor: await AccessRole.create({
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
|
|
name: 'Editor',
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
permBits: PermissionBits.VIEW | PermissionBits.EDIT,
|
|
}),
|
|
owner: await AccessRole.create({
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
|
|
name: 'Owner',
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
permBits:
|
|
PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE,
|
|
}),
|
|
};
|
|
|
|
// Create test users
|
|
testUsers = {
|
|
owner: await User.create({
|
|
id: new ObjectId().toString(),
|
|
_id: new ObjectId(),
|
|
name: 'Prompt Owner',
|
|
email: 'owner@example.com',
|
|
role: SystemRoles.USER,
|
|
}),
|
|
viewer: await User.create({
|
|
id: new ObjectId().toString(),
|
|
_id: new ObjectId(),
|
|
name: 'Prompt Viewer',
|
|
email: 'viewer@example.com',
|
|
role: SystemRoles.USER,
|
|
}),
|
|
editor: await User.create({
|
|
id: new ObjectId().toString(),
|
|
_id: new ObjectId(),
|
|
name: 'Prompt Editor',
|
|
email: 'editor@example.com',
|
|
role: SystemRoles.USER,
|
|
}),
|
|
noAccess: await User.create({
|
|
id: new ObjectId().toString(),
|
|
_id: new ObjectId(),
|
|
name: 'No Access',
|
|
email: 'noaccess@example.com',
|
|
role: SystemRoles.USER,
|
|
}),
|
|
admin: await User.create({
|
|
id: new ObjectId().toString(),
|
|
_id: new ObjectId(),
|
|
name: 'Admin',
|
|
email: 'admin@example.com',
|
|
role: SystemRoles.ADMIN,
|
|
}),
|
|
};
|
|
|
|
// Mock getRoleByName
|
|
const { getRoleByName } = require('~/models/Role');
|
|
getRoleByName.mockImplementation((roleName) => {
|
|
switch (roleName) {
|
|
case SystemRoles.USER:
|
|
return { permissions: { PROMPTS: { USE: true, CREATE: true } } };
|
|
case SystemRoles.ADMIN:
|
|
return { permissions: { PROMPTS: { USE: true, CREATE: true, SHARED_GLOBAL: true } } };
|
|
default:
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
describe('Prompt Routes - ACL Permissions', () => {
|
|
let consoleErrorSpy;
|
|
|
|
beforeEach(() => {
|
|
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
|
|
// Simple test to verify route is loaded
|
|
it('should have routes loaded', async () => {
|
|
// This should at least not crash
|
|
const response = await request(app).get('/api/prompts/test-404');
|
|
console.log('Test 404 response status:', response.status);
|
|
console.log('Test 404 response body:', response.body);
|
|
// We expect a 401 or 404, not 500
|
|
expect(response.status).not.toBe(500);
|
|
});
|
|
|
|
describe('POST /api/prompts - Create Prompt', () => {
|
|
afterEach(async () => {
|
|
await Prompt.deleteMany({});
|
|
await PromptGroup.deleteMany({});
|
|
await AclEntry.deleteMany({});
|
|
});
|
|
|
|
it('should create a prompt and grant owner permissions', async () => {
|
|
const promptData = {
|
|
prompt: {
|
|
prompt: 'Test prompt content',
|
|
type: 'text',
|
|
},
|
|
group: {
|
|
name: 'Test Prompt Group',
|
|
},
|
|
};
|
|
|
|
const response = await request(app).post('/api/prompts').send(promptData);
|
|
|
|
if (response.status !== 200) {
|
|
console.log('POST /api/prompts error status:', response.status);
|
|
console.log('POST /api/prompts error body:', response.body);
|
|
console.log('Console errors:', consoleErrorSpy.mock.calls);
|
|
}
|
|
|
|
console.log('POST response:', response.body);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.prompt).toBeDefined();
|
|
expect(response.body.prompt.prompt).toBe(promptData.prompt.prompt);
|
|
|
|
// Check ACL entry was created
|
|
const aclEntry = await AclEntry.findOne({
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: response.body.prompt.groupId,
|
|
principalType: PrincipalType.USER,
|
|
principalId: testUsers.owner._id,
|
|
});
|
|
|
|
expect(aclEntry).toBeTruthy();
|
|
expect(aclEntry.roleId.toString()).toBe(testRoles.owner._id.toString());
|
|
});
|
|
|
|
it('should create a prompt group with prompt and grant owner permissions', async () => {
|
|
const promptData = {
|
|
prompt: {
|
|
prompt: 'Group prompt content',
|
|
// Remove 'name' from prompt - it's not in the schema
|
|
},
|
|
group: {
|
|
name: 'Test Group',
|
|
category: 'testing',
|
|
},
|
|
};
|
|
|
|
const response = await request(app).post('/api/prompts').send(promptData).expect(200);
|
|
|
|
expect(response.body.prompt).toBeDefined();
|
|
expect(response.body.group).toBeDefined();
|
|
expect(response.body.group.name).toBe(promptData.group.name);
|
|
|
|
// Check ACL entry was created for the promptGroup
|
|
const aclEntry = await AclEntry.findOne({
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: response.body.group._id,
|
|
principalType: PrincipalType.USER,
|
|
principalId: testUsers.owner._id,
|
|
});
|
|
|
|
expect(aclEntry).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('GET /api/prompts/:promptId - Get Prompt', () => {
|
|
let testPrompt;
|
|
let testGroup;
|
|
|
|
beforeEach(async () => {
|
|
// Create a prompt group first
|
|
testGroup = await PromptGroup.create({
|
|
name: 'Test Group',
|
|
category: 'testing',
|
|
author: testUsers.owner._id,
|
|
authorName: testUsers.owner.name,
|
|
productionId: new ObjectId(),
|
|
});
|
|
|
|
// Create a prompt
|
|
testPrompt = await Prompt.create({
|
|
prompt: 'Test prompt for retrieval',
|
|
name: 'Get Test',
|
|
author: testUsers.owner._id,
|
|
type: 'text',
|
|
groupId: testGroup._id,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await Prompt.deleteMany({});
|
|
await PromptGroup.deleteMany({});
|
|
await AclEntry.deleteMany({});
|
|
});
|
|
|
|
it('should retrieve prompt when user has view permissions', async () => {
|
|
// Grant view permissions on the promptGroup
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: testUsers.owner._id,
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: testGroup._id,
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
|
grantedBy: testUsers.owner._id,
|
|
});
|
|
|
|
const response = await request(app).get(`/api/prompts/${testPrompt._id}`).expect(200);
|
|
|
|
expect(response.body._id).toBe(testPrompt._id.toString());
|
|
expect(response.body.prompt).toBe(testPrompt.prompt);
|
|
});
|
|
|
|
it('should deny access when user has no permissions', async () => {
|
|
// Change the user to one without access
|
|
setTestUser(app, testUsers.noAccess);
|
|
|
|
const response = await request(app).get(`/api/prompts/${testPrompt._id}`).expect(403);
|
|
|
|
// Verify error response
|
|
expect(response.body.error).toBe('Forbidden');
|
|
expect(response.body.message).toBe('Insufficient permissions to access this promptGroup');
|
|
});
|
|
|
|
it('should allow admin access without explicit permissions', async () => {
|
|
// First, reset the app to remove previous middleware
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
// Set admin user BEFORE adding routes
|
|
app.use((req, res, next) => {
|
|
req.user = {
|
|
...testUsers.admin.toObject(),
|
|
id: testUsers.admin._id.toString(),
|
|
_id: testUsers.admin._id,
|
|
name: testUsers.admin.name,
|
|
role: testUsers.admin.role,
|
|
};
|
|
next();
|
|
});
|
|
|
|
// Now add the routes
|
|
const promptRoutes = require('./prompts');
|
|
app.use('/api/prompts', promptRoutes);
|
|
|
|
console.log('Admin user:', testUsers.admin);
|
|
console.log('Admin role:', testUsers.admin.role);
|
|
console.log('SystemRoles.ADMIN:', SystemRoles.ADMIN);
|
|
|
|
const response = await request(app).get(`/api/prompts/${testPrompt._id}`).expect(200);
|
|
|
|
expect(response.body._id).toBe(testPrompt._id.toString());
|
|
});
|
|
});
|
|
|
|
describe('DELETE /api/prompts/:promptId - Delete Prompt', () => {
|
|
let testPrompt;
|
|
let testGroup;
|
|
|
|
beforeEach(async () => {
|
|
// Create group with prompt
|
|
testGroup = await PromptGroup.create({
|
|
name: 'Delete Test Group',
|
|
category: 'testing',
|
|
author: testUsers.owner._id,
|
|
authorName: testUsers.owner.name,
|
|
productionId: new ObjectId(),
|
|
});
|
|
|
|
testPrompt = await Prompt.create({
|
|
prompt: 'Test prompt for deletion',
|
|
name: 'Delete Test',
|
|
author: testUsers.owner._id,
|
|
type: 'text',
|
|
groupId: testGroup._id,
|
|
});
|
|
|
|
// Add prompt to group
|
|
testGroup.productionId = testPrompt._id;
|
|
testGroup.promptIds = [testPrompt._id];
|
|
await testGroup.save();
|
|
|
|
// Grant owner permissions on the promptGroup
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: testUsers.owner._id,
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: testGroup._id,
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
|
|
grantedBy: testUsers.owner._id,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await Prompt.deleteMany({});
|
|
await PromptGroup.deleteMany({});
|
|
await AclEntry.deleteMany({});
|
|
});
|
|
|
|
it('should delete prompt when user has delete permissions', async () => {
|
|
const response = await request(app)
|
|
.delete(`/api/prompts/${testPrompt._id}`)
|
|
.query({ groupId: testGroup._id.toString() })
|
|
.expect(200);
|
|
|
|
expect(response.body.prompt).toBe('Prompt deleted successfully');
|
|
|
|
// Verify prompt was deleted
|
|
const deletedPrompt = await Prompt.findById(testPrompt._id);
|
|
expect(deletedPrompt).toBeNull();
|
|
|
|
// Verify ACL entries were removed
|
|
const aclEntries = await AclEntry.find({
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: testGroup._id,
|
|
});
|
|
expect(aclEntries).toHaveLength(0);
|
|
});
|
|
|
|
it('should deny deletion when user lacks delete permissions', async () => {
|
|
// Create a prompt as a different user (not the one trying to delete)
|
|
const authorPrompt = await Prompt.create({
|
|
prompt: 'Test prompt by another user',
|
|
name: 'Another User Prompt',
|
|
author: testUsers.editor._id, // Different author
|
|
type: 'text',
|
|
groupId: testGroup._id,
|
|
});
|
|
|
|
// Grant only viewer permissions to viewer user on the promptGroup
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: testUsers.viewer._id,
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: testGroup._id,
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
|
grantedBy: testUsers.editor._id,
|
|
});
|
|
|
|
// Recreate app with viewer user
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use((req, res, next) => {
|
|
req.user = {
|
|
...testUsers.viewer.toObject(),
|
|
id: testUsers.viewer._id.toString(),
|
|
_id: testUsers.viewer._id,
|
|
name: testUsers.viewer.name,
|
|
role: testUsers.viewer.role,
|
|
};
|
|
next();
|
|
});
|
|
const promptRoutes = require('./prompts');
|
|
app.use('/api/prompts', promptRoutes);
|
|
|
|
await request(app)
|
|
.delete(`/api/prompts/${authorPrompt._id}`)
|
|
.query({ groupId: testGroup._id.toString() })
|
|
.expect(403);
|
|
|
|
// Verify prompt still exists
|
|
const prompt = await Prompt.findById(authorPrompt._id);
|
|
expect(prompt).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('PATCH /api/prompts/:promptId/tags/production - Make Production', () => {
|
|
let testPrompt;
|
|
let testGroup;
|
|
|
|
beforeEach(async () => {
|
|
// Create group
|
|
testGroup = await PromptGroup.create({
|
|
name: 'Production Test Group',
|
|
category: 'testing',
|
|
author: testUsers.owner._id,
|
|
authorName: testUsers.owner.name,
|
|
productionId: new ObjectId(),
|
|
});
|
|
|
|
testPrompt = await Prompt.create({
|
|
prompt: 'Test prompt for production',
|
|
name: 'Production Test',
|
|
author: testUsers.owner._id,
|
|
type: 'text',
|
|
groupId: testGroup._id,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await Prompt.deleteMany({});
|
|
await PromptGroup.deleteMany({});
|
|
await AclEntry.deleteMany({});
|
|
});
|
|
|
|
it('should make prompt production when user has edit permissions', async () => {
|
|
// Grant edit permissions on the promptGroup
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: testUsers.owner._id,
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: testGroup._id,
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
|
|
grantedBy: testUsers.owner._id,
|
|
});
|
|
|
|
// Recreate app to ensure fresh middleware
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use((req, res, next) => {
|
|
req.user = {
|
|
...testUsers.owner.toObject(),
|
|
id: testUsers.owner._id.toString(),
|
|
_id: testUsers.owner._id,
|
|
name: testUsers.owner.name,
|
|
role: testUsers.owner.role,
|
|
};
|
|
next();
|
|
});
|
|
const promptRoutes = require('./prompts');
|
|
app.use('/api/prompts', promptRoutes);
|
|
|
|
const response = await request(app)
|
|
.patch(`/api/prompts/${testPrompt._id}/tags/production`)
|
|
.expect(200);
|
|
|
|
expect(response.body.message).toBe('Prompt production made successfully');
|
|
|
|
// Verify the group was updated
|
|
const updatedGroup = await PromptGroup.findById(testGroup._id);
|
|
expect(updatedGroup.productionId.toString()).toBe(testPrompt._id.toString());
|
|
});
|
|
|
|
it('should deny making production when user lacks edit permissions', async () => {
|
|
// Grant only view permissions to viewer on the promptGroup
|
|
await grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: testUsers.viewer._id,
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: testGroup._id,
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
|
grantedBy: testUsers.owner._id,
|
|
});
|
|
|
|
// Recreate app with viewer user
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use((req, res, next) => {
|
|
req.user = {
|
|
...testUsers.viewer.toObject(),
|
|
id: testUsers.viewer._id.toString(),
|
|
_id: testUsers.viewer._id,
|
|
name: testUsers.viewer.name,
|
|
role: testUsers.viewer.role,
|
|
};
|
|
next();
|
|
});
|
|
const promptRoutes = require('./prompts');
|
|
app.use('/api/prompts', promptRoutes);
|
|
|
|
await request(app).patch(`/api/prompts/${testPrompt._id}/tags/production`).expect(403);
|
|
|
|
// Verify prompt hasn't changed
|
|
const unchangedGroup = await PromptGroup.findById(testGroup._id);
|
|
expect(unchangedGroup.productionId.toString()).not.toBe(testPrompt._id.toString());
|
|
});
|
|
});
|
|
|
|
describe('Public Access', () => {
|
|
let publicPrompt;
|
|
let publicGroup;
|
|
|
|
beforeEach(async () => {
|
|
// Create a prompt group
|
|
publicGroup = await PromptGroup.create({
|
|
name: 'Public Test Group',
|
|
category: 'testing',
|
|
author: testUsers.owner._id,
|
|
authorName: testUsers.owner.name,
|
|
productionId: new ObjectId(),
|
|
});
|
|
|
|
// Create a public prompt
|
|
publicPrompt = await Prompt.create({
|
|
prompt: 'Public prompt content',
|
|
name: 'Public Test',
|
|
author: testUsers.owner._id,
|
|
type: 'text',
|
|
groupId: publicGroup._id,
|
|
});
|
|
|
|
// Grant public viewer access on the promptGroup
|
|
await grantPermission({
|
|
principalType: PrincipalType.PUBLIC,
|
|
principalId: null,
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
resourceId: publicGroup._id,
|
|
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
|
grantedBy: testUsers.owner._id,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await Prompt.deleteMany({});
|
|
await PromptGroup.deleteMany({});
|
|
await AclEntry.deleteMany({});
|
|
});
|
|
|
|
it('should allow any user to view public prompts', async () => {
|
|
// Change user to someone without explicit permissions
|
|
setTestUser(app, testUsers.noAccess);
|
|
|
|
const response = await request(app).get(`/api/prompts/${publicPrompt._id}`).expect(200);
|
|
|
|
expect(response.body._id).toBe(publicPrompt._id.toString());
|
|
});
|
|
});
|
|
});
|