mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-03 14:27:20 +02:00
186 lines
5.7 KiB
JavaScript
186 lines
5.7 KiB
JavaScript
|
|
const express = require('express');
|
||
|
|
const request = require('supertest');
|
||
|
|
const mongoose = require('mongoose');
|
||
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||
|
|
const { createModels, createMethods } = require('@librechat/data-schemas');
|
||
|
|
const { PrincipalType, SystemRoles } = require('librechat-data-provider');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Integration test for the admin grants routes.
|
||
|
|
*
|
||
|
|
* Validates the full Express wiring: route registration → middleware →
|
||
|
|
* handler → real MongoDB. Auth middleware is injected (matching the repo
|
||
|
|
* pattern in keys.spec.js) so we can control the caller identity without
|
||
|
|
* a real JWT, while the handler DI deps use real DB methods.
|
||
|
|
*/
|
||
|
|
|
||
|
|
jest.mock('~/server/middleware', () => ({
|
||
|
|
requireJwtAuth: (_req, _res, next) => next(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/server/middleware/roles/capabilities', () => ({
|
||
|
|
requireCapability: () => (_req, _res, next) => next(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
let mongoServer;
|
||
|
|
let db;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
mongoServer = await MongoMemoryServer.create();
|
||
|
|
await mongoose.connect(mongoServer.getUri());
|
||
|
|
createModels(mongoose);
|
||
|
|
db = createMethods(mongoose);
|
||
|
|
await db.seedSystemGrants();
|
||
|
|
await db.initializeRoles();
|
||
|
|
await db.seedDefaultRoles();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await mongoose.disconnect();
|
||
|
|
await mongoServer.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
const SystemGrant = mongoose.models.SystemGrant;
|
||
|
|
// Clean non-seed grants (keep admin seed)
|
||
|
|
await SystemGrant.deleteMany({
|
||
|
|
$or: [
|
||
|
|
{ principalId: { $ne: SystemRoles.ADMIN } },
|
||
|
|
{ principalType: { $ne: PrincipalType.ROLE } },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
function createApp(user) {
|
||
|
|
const { createAdminGrantsHandlers, getCachedPrincipals } = require('@librechat/api');
|
||
|
|
|
||
|
|
const handlers = createAdminGrantsHandlers({
|
||
|
|
listGrants: db.listGrants,
|
||
|
|
countGrants: db.countGrants,
|
||
|
|
getCapabilitiesForPrincipal: db.getCapabilitiesForPrincipal,
|
||
|
|
getCapabilitiesForPrincipals: db.getCapabilitiesForPrincipals,
|
||
|
|
grantCapability: db.grantCapability,
|
||
|
|
revokeCapability: db.revokeCapability,
|
||
|
|
getUserPrincipals: db.getUserPrincipals,
|
||
|
|
hasCapabilityForPrincipals: db.hasCapabilityForPrincipals,
|
||
|
|
getHeldCapabilities: db.getHeldCapabilities,
|
||
|
|
getCachedPrincipals,
|
||
|
|
checkRoleExists: async (name) => (await db.getRoleByName(name)) != null,
|
||
|
|
});
|
||
|
|
|
||
|
|
const app = express();
|
||
|
|
app.use(express.json());
|
||
|
|
app.use((req, _res, next) => {
|
||
|
|
req.user = user;
|
||
|
|
next();
|
||
|
|
});
|
||
|
|
|
||
|
|
const router = express.Router();
|
||
|
|
router.get('/', handlers.listGrants);
|
||
|
|
router.get('/effective', handlers.getEffectiveCapabilities);
|
||
|
|
router.get('/:principalType/:principalId', handlers.getPrincipalGrants);
|
||
|
|
router.post('/', handlers.assignGrant);
|
||
|
|
router.delete('/:principalType/:principalId/:capability', handlers.revokeGrant);
|
||
|
|
app.use('/api/admin/grants', router);
|
||
|
|
|
||
|
|
return app;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('Admin Grants Routes — Integration', () => {
|
||
|
|
const adminUserId = new mongoose.Types.ObjectId();
|
||
|
|
const adminUser = {
|
||
|
|
_id: adminUserId,
|
||
|
|
id: adminUserId.toString(),
|
||
|
|
role: SystemRoles.ADMIN,
|
||
|
|
};
|
||
|
|
|
||
|
|
it('GET / returns seeded admin grants', async () => {
|
||
|
|
const app = createApp(adminUser);
|
||
|
|
const res = await request(app).get('/api/admin/grants').expect(200);
|
||
|
|
|
||
|
|
expect(res.body).toHaveProperty('grants');
|
||
|
|
expect(res.body).toHaveProperty('total');
|
||
|
|
expect(res.body.grants.length).toBeGreaterThan(0);
|
||
|
|
// Seeded grants are for the ADMIN role
|
||
|
|
expect(res.body.grants[0].principalType).toBe(PrincipalType.ROLE);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('GET /effective returns capabilities for admin', async () => {
|
||
|
|
const app = createApp(adminUser);
|
||
|
|
const res = await request(app).get('/api/admin/grants/effective').expect(200);
|
||
|
|
|
||
|
|
expect(res.body).toHaveProperty('capabilities');
|
||
|
|
expect(res.body.capabilities).toContain('access:admin');
|
||
|
|
expect(res.body.capabilities).toContain('manage:roles');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('POST / assigns a grant and DELETE / revokes it', async () => {
|
||
|
|
const app = createApp(adminUser);
|
||
|
|
|
||
|
|
// Assign
|
||
|
|
const assignRes = await request(app)
|
||
|
|
.post('/api/admin/grants')
|
||
|
|
.send({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.USER,
|
||
|
|
capability: 'read:users',
|
||
|
|
})
|
||
|
|
.expect(201);
|
||
|
|
|
||
|
|
expect(assignRes.body.grant).toMatchObject({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.USER,
|
||
|
|
capability: 'read:users',
|
||
|
|
});
|
||
|
|
|
||
|
|
// Verify via GET
|
||
|
|
const getRes = await request(app)
|
||
|
|
.get(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
|
||
|
|
.expect(200);
|
||
|
|
|
||
|
|
expect(getRes.body.grants.some((g) => g.capability === 'read:users')).toBe(true);
|
||
|
|
|
||
|
|
// Revoke
|
||
|
|
await request(app)
|
||
|
|
.delete(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}/read:users`)
|
||
|
|
.expect(200);
|
||
|
|
|
||
|
|
// Verify revoked
|
||
|
|
const afterRes = await request(app)
|
||
|
|
.get(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
|
||
|
|
.expect(200);
|
||
|
|
|
||
|
|
expect(afterRes.body.grants.some((g) => g.capability === 'read:users')).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('POST / returns 400 for non-existent role when checkRoleExists is wired', async () => {
|
||
|
|
const app = createApp(adminUser);
|
||
|
|
|
||
|
|
const res = await request(app)
|
||
|
|
.post('/api/admin/grants')
|
||
|
|
.send({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: 'nonexistent-role',
|
||
|
|
capability: 'read:users',
|
||
|
|
})
|
||
|
|
.expect(400);
|
||
|
|
|
||
|
|
expect(res.body.error).toBe('Role not found');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('POST / returns 401 without authenticated user', async () => {
|
||
|
|
const app = createApp(undefined);
|
||
|
|
|
||
|
|
const res = await request(app)
|
||
|
|
.post('/api/admin/grants')
|
||
|
|
.send({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.USER,
|
||
|
|
capability: 'read:users',
|
||
|
|
})
|
||
|
|
.expect(401);
|
||
|
|
|
||
|
|
expect(res.body).toHaveProperty('error', 'Authentication required');
|
||
|
|
});
|
||
|
|
});
|