| import mongoose from 'mongoose'; |
| import { PrincipalType } from 'librechat-data-provider'; |
| import { MongoMemoryServer } from 'mongodb-memory-server'; |
| import type * as t from '~/types'; |
| import { createUserGroupMethods } from './userGroup'; |
| import groupSchema from '~/schema/group'; |
| import userSchema from '~/schema/user'; |
| import roleSchema from '~/schema/role'; |
|
|
| |
| jest.mock('~/config/winston', () => ({ |
| error: jest.fn(), |
| info: jest.fn(), |
| debug: jest.fn(), |
| })); |
|
|
| let mongoServer: MongoMemoryServer; |
| let Group: mongoose.Model<t.IGroup>; |
| let User: mongoose.Model<t.IUser>; |
| let Role: mongoose.Model<t.IRole>; |
| let methods: ReturnType<typeof createUserGroupMethods>; |
|
|
| beforeAll(async () => { |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| await mongoose.connect(mongoUri); |
|
|
| |
| Group = mongoose.models.Group || mongoose.model<t.IGroup>('Group', groupSchema); |
| User = mongoose.models.User || mongoose.model<t.IUser>('User', userSchema); |
| Role = mongoose.models.Role || mongoose.model<t.IRole>('Role', roleSchema); |
|
|
| |
| methods = createUserGroupMethods(mongoose); |
| }); |
|
|
| afterAll(async () => { |
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| }); |
|
|
| beforeEach(async () => { |
| await mongoose.connection.dropDatabase(); |
| }); |
|
|
| describe('Role-based Permissions Integration', () => { |
| describe('getUserPrincipals with roles', () => { |
| test('should include role principal for user with role', async () => { |
| const adminUser = await User.create({ |
| name: 'Admin User', |
| email: 'admin@test.com', |
| provider: 'local', |
| role: 'admin', |
| }); |
|
|
| const principals = await methods.getUserPrincipals({ |
| userId: adminUser._id as mongoose.Types.ObjectId, |
| }); |
|
|
| |
| expect(principals).toHaveLength(3); |
|
|
| const userPrincipal = principals.find((p) => p.principalType === PrincipalType.USER); |
| const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
| const publicPrincipal = principals.find((p) => p.principalType === PrincipalType.PUBLIC); |
|
|
| expect(userPrincipal).toBeDefined(); |
| expect(userPrincipal?.principalId?.toString()).toBe( |
| (adminUser._id as mongoose.Types.ObjectId).toString(), |
| ); |
|
|
| expect(rolePrincipal).toBeDefined(); |
| expect(rolePrincipal?.principalType).toBe(PrincipalType.ROLE); |
| expect(rolePrincipal?.principalId).toBe('admin'); |
|
|
| expect(publicPrincipal).toBeDefined(); |
| expect(publicPrincipal?.principalType).toBe(PrincipalType.PUBLIC); |
| expect(publicPrincipal?.principalId).toBeUndefined(); |
| }); |
|
|
| test('should not include role principal for user without role', async () => { |
| const regularUser = await User.create({ |
| name: 'Regular User', |
| email: 'user@test.com', |
| provider: 'local', |
| role: null, |
| }); |
|
|
| const principals = await methods.getUserPrincipals({ |
| userId: regularUser._id as mongoose.Types.ObjectId, |
| }); |
|
|
| |
| expect(principals).toHaveLength(2); |
|
|
| const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
| expect(rolePrincipal).toBeUndefined(); |
| }); |
|
|
| test('should include all principal types for user with role and groups', async () => { |
| const user = await User.create({ |
| name: 'Complete User', |
| email: 'complete@test.com', |
| provider: 'local', |
| role: 'moderator', |
| }); |
|
|
| |
| const group1 = await Group.create({ |
| name: 'Group 1', |
| source: 'local', |
| memberIds: [(user._id as mongoose.Types.ObjectId).toString()], |
| }); |
|
|
| const group2 = await Group.create({ |
| name: 'Group 2', |
| source: 'local', |
| memberIds: [(user._id as mongoose.Types.ObjectId).toString()], |
| }); |
|
|
| const principals = await methods.getUserPrincipals({ |
| userId: user._id as mongoose.Types.ObjectId, |
| }); |
|
|
| |
| expect(principals).toHaveLength(5); |
|
|
| const principalTypes = principals.map((p) => p.principalType); |
| expect(principalTypes).toContain(PrincipalType.USER); |
| expect(principalTypes).toContain(PrincipalType.ROLE); |
| expect(principalTypes).toContain(PrincipalType.GROUP); |
| expect(principalTypes).toContain(PrincipalType.PUBLIC); |
|
|
| |
| const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
| expect(rolePrincipal?.principalId).toBe('moderator'); |
|
|
| |
| const groupPrincipals = principals.filter((p) => p.principalType === PrincipalType.GROUP); |
| expect(groupPrincipals).toHaveLength(2); |
| const groupIds = groupPrincipals.map((p) => p.principalId?.toString()); |
| expect(groupIds).toContain(group1._id.toString()); |
| expect(groupIds).toContain(group2._id.toString()); |
| }); |
|
|
| test('should handle different role values', async () => { |
| const testCases = [ |
| { role: 'admin', expected: 'admin' }, |
| { role: 'moderator', expected: 'moderator' }, |
| { role: 'editor', expected: 'editor' }, |
| { role: 'viewer', expected: 'viewer' }, |
| { role: 'custom_role', expected: 'custom_role' }, |
| ]; |
|
|
| for (const testCase of testCases) { |
| const user = await User.create({ |
| name: `User with ${testCase.role}`, |
| email: `${testCase.role}@test.com`, |
| provider: 'local', |
| role: testCase.role, |
| }); |
|
|
| const principals = await methods.getUserPrincipals({ |
| userId: user._id as mongoose.Types.ObjectId, |
| }); |
| const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
|
|
| expect(rolePrincipal).toBeDefined(); |
| expect(rolePrincipal?.principalId).toBe(testCase.expected); |
| } |
| }); |
| }); |
|
|
| describe('searchPrincipals with role support', () => { |
| beforeEach(async () => { |
| |
| await Role.create([ |
| { name: 'admin', description: 'Administrator role' }, |
| { name: 'moderator', description: 'Moderator role' }, |
| { name: 'editor', description: 'Editor role' }, |
| { name: 'viewer', description: 'Viewer role' }, |
| { name: 'guest', description: 'Guest role' }, |
| ]); |
|
|
| |
| await User.create([ |
| { |
| name: 'Admin User', |
| email: 'admin@test.com', |
| username: 'adminuser', |
| provider: 'local', |
| role: 'admin', |
| }, |
| { |
| name: 'Moderator User', |
| email: 'moderator@test.com', |
| username: 'moduser', |
| provider: 'local', |
| role: 'moderator', |
| }, |
| ]); |
|
|
| |
| await Group.create([ |
| { |
| name: 'Admin Group', |
| source: 'local', |
| memberIds: [], |
| }, |
| { |
| name: 'Moderator Group', |
| source: 'local', |
| memberIds: [], |
| }, |
| ]); |
| }); |
|
|
| test('should search for roles when Role model exists', async () => { |
| const results = await methods.searchPrincipals('admin'); |
|
|
| const roleResults = results.filter((r) => r.type === PrincipalType.ROLE); |
| const userResults = results.filter((r) => r.type === PrincipalType.USER); |
| const groupResults = results.filter((r) => r.type === PrincipalType.GROUP); |
|
|
| |
| expect(roleResults).toHaveLength(1); |
| expect(roleResults[0].id).toBe('admin'); |
| expect(roleResults[0].name).toBe('admin'); |
| expect(roleResults[0].type).toBe(PrincipalType.ROLE); |
|
|
| |
| expect(userResults.some((u) => u.name === 'Admin User')).toBe(true); |
| expect(groupResults.some((g) => g.name === 'Admin Group')).toBe(true); |
| }); |
|
|
| test('should filter search results by role type', async () => { |
| const results = await methods.searchPrincipals('mod', 10, [PrincipalType.ROLE]); |
|
|
| expect(results.every((r) => r.type === PrincipalType.ROLE)).toBe(true); |
| expect(results).toHaveLength(1); |
| expect(results[0].name).toBe('moderator'); |
| }); |
|
|
| test('should respect limit for role search', async () => { |
| |
| for (let i = 0; i < 10; i++) { |
| await Role.create({ name: `testrole${i}` }); |
| } |
|
|
| const results = await methods.searchPrincipals('testrole', 5, [PrincipalType.ROLE]); |
|
|
| expect(results).toHaveLength(5); |
| expect(results.every((r) => r.type === PrincipalType.ROLE)).toBe(true); |
| }); |
|
|
| test('should search across all principal types', async () => { |
| const results = await methods.searchPrincipals('mod'); |
|
|
| |
| const types = new Set(results.map((r) => r.type)); |
| expect(types.has(PrincipalType.ROLE)).toBe(true); |
| expect(types.has(PrincipalType.USER)).toBe(true); |
| expect(types.has(PrincipalType.GROUP)).toBe(true); |
|
|
| |
| expect(results.some((r) => r.type === PrincipalType.ROLE && r.name === 'moderator')).toBe( |
| true, |
| ); |
| expect( |
| results.some((r) => r.type === PrincipalType.USER && r.name === 'Moderator User'), |
| ).toBe(true); |
| expect( |
| results.some((r) => r.type === PrincipalType.GROUP && r.name === 'Moderator Group'), |
| ).toBe(true); |
| }); |
|
|
| test('should handle case-insensitive role search', async () => { |
| const results = await methods.searchPrincipals('ADMIN', 10, [PrincipalType.ROLE]); |
|
|
| expect(results).toHaveLength(1); |
| expect(results[0].name).toBe('admin'); |
| }); |
|
|
| test('should return empty array for no role matches', async () => { |
| const results = await methods.searchPrincipals('nonexistentrole', 10, [PrincipalType.ROLE]); |
|
|
| expect(results).toEqual([]); |
| }); |
| }); |
|
|
| describe('Role principals in complex scenarios', () => { |
| test('should handle user role changes', async () => { |
| const user = await User.create({ |
| name: 'Changing User', |
| email: 'change@test.com', |
| provider: 'local', |
| role: 'viewer', |
| }); |
|
|
| |
| let principals = await methods.getUserPrincipals({ |
| userId: user._id as mongoose.Types.ObjectId, |
| }); |
| let rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
| expect(rolePrincipal?.principalId).toBe('viewer'); |
|
|
| |
| user.role = 'editor'; |
| await user.save(); |
|
|
| |
| principals = await methods.getUserPrincipals({ |
| userId: user._id as mongoose.Types.ObjectId, |
| }); |
| rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
| expect(rolePrincipal?.principalId).toBe('editor'); |
| }); |
|
|
| test('should handle user role removal', async () => { |
| const user = await User.create({ |
| name: 'Demoted User', |
| email: 'demoted@test.com', |
| provider: 'local', |
| role: 'admin', |
| }); |
|
|
| |
| let principals = await methods.getUserPrincipals({ |
| userId: user._id as mongoose.Types.ObjectId, |
| }); |
| expect(principals).toHaveLength(3); |
|
|
| |
| user.role = undefined; |
| await user.save(); |
|
|
| |
| principals = await methods.getUserPrincipals({ |
| userId: user._id as mongoose.Types.ObjectId, |
| }); |
| expect(principals).toHaveLength(2); |
| const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
| expect(rolePrincipal).toBeUndefined(); |
| }); |
|
|
| test('should handle empty or null role values', async () => { |
| const testCases = [ |
| { role: '', expected: false }, |
| { role: null, expected: false }, |
| { role: undefined, expected: true, expectedRole: 'USER' }, |
| { role: ' ', expected: false }, |
| { role: 'valid_role', expected: true, expectedRole: 'valid_role' }, |
| ]; |
|
|
| for (const testCase of testCases) { |
| const userData: Partial<t.IUser> = { |
| name: `User ${Math.random()}`, |
| email: `test${Math.random()}@test.com`, |
| provider: 'local', |
| }; |
|
|
| |
| if (testCase.role !== undefined) { |
| userData.role = testCase.role as string; |
| } |
|
|
| const user = await User.create(userData); |
|
|
| const principals = await methods.getUserPrincipals({ |
| userId: user._id as mongoose.Types.ObjectId, |
| }); |
| const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
|
|
| if (testCase.expected) { |
| expect(rolePrincipal).toBeDefined(); |
| expect(rolePrincipal?.principalId).toBe(testCase.expectedRole || testCase.role); |
| } else { |
| expect(rolePrincipal).toBeUndefined(); |
| } |
| } |
| }); |
| }); |
| }); |
|
|