LibreChat/packages/data-schemas/src/methods/accessRole.spec.ts
Danny Avila 81b32e400a
🔧 refactor: Organize Sharing/Agent Components and Improve Type Safety
refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids, rename enums to PascalCase

refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids

chore: move sharing related components to dedicated "Sharing" directory

chore: remove PublicSharingToggle component and update index exports

chore: move non-sidepanel agent components to `~/components/Agents`

chore: move AgentCategoryDisplay component with tests

chore: remove commented out code

refactor: change PERMISSION_BITS from const to enum for better type safety

refactor: reorganize imports in GenericGrantAccessDialog and update index exports for hooks

refactor: update type definitions to use ACCESS_ROLE_IDS for improved type safety

refactor: remove unused canAccessPromptResource middleware and related code

refactor: remove unused prompt access roles from createAccessRoleMethods

refactor: update resourceType in AclEntry type definition to remove unused 'prompt' value

refactor: introduce ResourceType enum and update resourceType usage across data provider files for improved type safety

refactor: update resourceType usage to ResourceType enum across sharing and permissions components for improved type safety

refactor: standardize resourceType usage to ResourceType enum across agent and prompt models, permissions controller, and middleware for enhanced type safety

refactor: update resourceType references from PROMPT_GROUP to PROMPTGROUP for consistency across models, middleware, and components

refactor: standardize access role IDs and resource type usage across agent, file, and prompt models for improved type safety and consistency

chore: add typedefs for TUpdateResourcePermissionsRequest and TUpdateResourcePermissionsResponse to enhance type definitions

chore: move SearchPicker to PeoplePicker dir

refactor: implement debouncing for query changes in SearchPicker for improved performance

chore: fix typing, import order for agent admin settings

fix: agent admin settings, prevent agent form submission

refactor: rename `ACCESS_ROLE_IDS` to `AccessRoleIds`

refactor: replace PermissionBits with PERMISSION_BITS

refactor: replace PERMISSION_BITS with PermissionBits
2025-08-13 16:24:20 -04:00

320 lines
11 KiB
TypeScript

import mongoose from 'mongoose';
import { AccessRoleIds, ResourceType, PermissionBits } from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server';
import type * as t from '~/types';
import { createAccessRoleMethods } from './accessRole';
import accessRoleSchema from '~/schema/accessRole';
import { RoleBits } from '~/common';
let mongoServer: MongoMemoryServer;
let AccessRole: mongoose.Model<t.IAccessRole>;
let methods: ReturnType<typeof createAccessRoleMethods>;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
AccessRole = mongoose.models.AccessRole || mongoose.model('AccessRole', accessRoleSchema);
methods = createAccessRoleMethods(mongoose);
await mongoose.connect(mongoUri);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
await mongoose.connection.dropDatabase();
});
describe('AccessRole Model Tests', () => {
describe('Basic CRUD Operations', () => {
const sampleRole: t.AccessRole = {
accessRoleId: 'test_viewer',
name: 'Test Viewer',
description: 'Test role for viewer permissions',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER,
};
test('should create a new role', async () => {
const role = await methods.createRole(sampleRole);
expect(role).toBeDefined();
expect(role.accessRoleId).toBe(sampleRole.accessRoleId);
expect(role.name).toBe(sampleRole.name);
expect(role.permBits).toBe(sampleRole.permBits);
});
test('should find a role by its ID', async () => {
const createdRole = await methods.createRole(sampleRole);
const foundRole = await methods.findRoleById(createdRole._id);
expect(foundRole).toBeDefined();
expect(foundRole?._id.toString()).toBe(createdRole._id.toString());
expect(foundRole?.accessRoleId).toBe(sampleRole.accessRoleId);
});
test('should find a role by its identifier', async () => {
await methods.createRole(sampleRole);
const foundRole = await methods.findRoleByIdentifier(sampleRole.accessRoleId);
expect(foundRole).toBeDefined();
expect(foundRole?.accessRoleId).toBe(sampleRole.accessRoleId);
expect(foundRole?.name).toBe(sampleRole.name);
});
test('should update an existing role', async () => {
await methods.createRole(sampleRole);
const updatedData = {
name: 'Updated Test Role',
description: 'Updated description',
};
const updatedRole = await methods.updateRole(sampleRole.accessRoleId, updatedData);
expect(updatedRole).toBeDefined();
expect(updatedRole?.name).toBe(updatedData.name);
expect(updatedRole?.description).toBe(updatedData.description);
// Check that other fields remain unchanged
expect(updatedRole?.accessRoleId).toBe(sampleRole.accessRoleId);
expect(updatedRole?.permBits).toBe(sampleRole.permBits);
});
test('should delete a role', async () => {
await methods.createRole(sampleRole);
const deleteResult = await methods.deleteRole(sampleRole.accessRoleId);
expect(deleteResult.deletedCount).toBe(1);
const foundRole = await methods.findRoleByIdentifier(sampleRole.accessRoleId);
expect(foundRole).toBeNull();
});
test('should get all roles', async () => {
const roles = [
sampleRole,
{
accessRoleId: 'test_editor',
name: 'Test Editor',
description: 'Test role for editor permissions',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR,
},
];
await Promise.all(roles.map((role) => methods.createRole(role)));
const allRoles = await methods.getAllRoles();
expect(allRoles).toHaveLength(2);
expect(allRoles.map((r) => r.accessRoleId).sort()).toEqual(
['test_editor', 'test_viewer'].sort(),
);
});
});
describe('Resource and Permission Queries', () => {
beforeEach(async () => {
await AccessRole.deleteMany({});
// Create sample roles for testing
await Promise.all([
methods.createRole({
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Agent Viewer',
description: 'Can view agents',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER,
}),
methods.createRole({
accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'Agent Editor',
description: 'Can edit agents',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR,
}),
methods.createRole({
accessRoleId: 'project_viewer',
name: 'Project Viewer',
description: 'Can view projects',
resourceType: 'project',
permBits: RoleBits.VIEWER,
}),
methods.createRole({
accessRoleId: 'project_editor',
name: 'Project Editor',
description: 'Can edit projects',
resourceType: 'project',
permBits: RoleBits.EDITOR,
}),
]);
});
test('should find roles by resource type', async () => {
const agentRoles = await methods.findRolesByResourceType('agent');
expect(agentRoles).toHaveLength(2);
expect(agentRoles.map((r) => r.accessRoleId).sort()).toEqual(
[AccessRoleIds.AGENT_EDITOR, AccessRoleIds.AGENT_VIEWER].sort(),
);
const projectRoles = await methods.findRolesByResourceType('project');
expect(projectRoles).toHaveLength(2);
expect(projectRoles.map((r) => r.accessRoleId).sort()).toEqual(
['project_editor', 'project_viewer'].sort(),
);
});
test('should find role by permissions', async () => {
const viewerRole = await methods.findRoleByPermissions('agent', RoleBits.VIEWER);
expect(viewerRole).toBeDefined();
expect(viewerRole?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
const editorRole = await methods.findRoleByPermissions('agent', RoleBits.EDITOR);
expect(editorRole).toBeDefined();
expect(editorRole?.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
});
test('should return null when no role matches the permissions', async () => {
// Create a custom permission that doesn't match any existing role
const customPerm = PermissionBits.VIEW | PermissionBits.SHARE;
const role = await methods.findRoleByPermissions('agent', customPerm);
expect(role).toBeNull();
});
});
describe('seedDefaultRoles', () => {
beforeEach(async () => {
await AccessRole.deleteMany({});
});
test('should seed default roles', async () => {
const result = await methods.seedDefaultRoles();
// Verify the result contains the default roles
expect(Object.keys(result).sort()).toEqual(
[
AccessRoleIds.AGENT_EDITOR,
AccessRoleIds.AGENT_OWNER,
AccessRoleIds.AGENT_VIEWER,
AccessRoleIds.PROMPTGROUP_EDITOR,
AccessRoleIds.PROMPTGROUP_OWNER,
AccessRoleIds.PROMPTGROUP_VIEWER,
].sort(),
);
// Verify each role exists in the database
const agentViewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(agentViewerRole).toBeDefined();
expect(agentViewerRole?.permBits).toBe(RoleBits.VIEWER);
const agentEditorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
expect(agentEditorRole).toBeDefined();
expect(agentEditorRole?.permBits).toBe(RoleBits.EDITOR);
const agentOwnerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
expect(agentOwnerRole).toBeDefined();
expect(agentOwnerRole?.permBits).toBe(RoleBits.OWNER);
});
test('should not modify existing roles when seeding', async () => {
// Create a modified version of a default role
const customRole = {
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Custom Viewer',
description: 'Custom viewer description',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER,
};
await methods.createRole(customRole);
// Seed default roles
await methods.seedDefaultRoles();
// Verify the custom role was not modified
const role = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(role?.name).toBe(customRole.name);
expect(role?.description).toBe(customRole.description);
});
});
describe('getRoleForPermissions', () => {
beforeEach(async () => {
await AccessRole.deleteMany({});
// Create sample roles with ascending permission levels
await Promise.all([
methods.createRole({
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Agent Viewer',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, // 1
}),
methods.createRole({
accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'Agent Editor',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR, // 3
}),
methods.createRole({
accessRoleId: 'agent_manager',
name: 'Agent Manager',
resourceType: ResourceType.AGENT,
permBits: RoleBits.MANAGER, // 7
}),
methods.createRole({
accessRoleId: AccessRoleIds.AGENT_OWNER,
name: 'Agent Owner',
resourceType: ResourceType.AGENT,
permBits: RoleBits.OWNER, // 15
}),
]);
});
test('should find exact matching role', async () => {
const role = await methods.getRoleForPermissions('agent', RoleBits.EDITOR);
expect(role).toBeDefined();
expect(role?.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
expect(role?.permBits).toBe(RoleBits.EDITOR);
});
test('should find closest compatible role without exceeding permissions', async () => {
// Create a custom permission between VIEWER and EDITOR
const customPerm = PermissionBits.VIEW | PermissionBits.SHARE; // 9
// Should return VIEWER (1) as closest matching role without exceeding the permission bits
const role = await methods.getRoleForPermissions('agent', customPerm);
expect(role).toBeDefined();
expect(role?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
});
test('should return null when no compatible role is found', async () => {
// Create a permission that doesn't match any existing permission pattern
const invalidPerm = 100;
const role = await methods.getRoleForPermissions('agent', invalidPerm as PermissionBits);
expect(role).toBeNull();
});
test('should find role for resource-specific permissions', async () => {
// Create a role for a different resource type
await methods.createRole({
accessRoleId: 'project_viewer',
name: 'Project Viewer',
resourceType: 'project',
permBits: RoleBits.VIEWER,
});
// Query for agent roles
const agentRole = await methods.getRoleForPermissions('agent', RoleBits.VIEWER);
expect(agentRole).toBeDefined();
expect(agentRole?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
// Query for project roles
const projectRole = await methods.getRoleForPermissions('project', RoleBits.VIEWER);
expect(projectRole).toBeDefined();
expect(projectRole?.accessRoleId).toBe('project_viewer');
});
});
});