| 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'; |
|
|
| |
| 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 methods: ReturnType<typeof createUserGroupMethods>; |
|
|
| beforeAll(async () => { |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| Group = mongoose.models.Group || mongoose.model<t.IGroup>('Group', groupSchema); |
| User = mongoose.models.User || mongoose.model<t.IUser>('User', userSchema); |
| methods = createUserGroupMethods(mongoose); |
| await mongoose.connect(mongoUri); |
| }); |
|
|
| afterAll(async () => { |
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| }); |
|
|
| beforeEach(async () => { |
| await mongoose.connection.dropDatabase(); |
| }); |
|
|
| describe('User Group Methods Tests', () => { |
| describe('Group Query Methods', () => { |
| let testGroup: t.IGroup; |
| let testUser: t.IUser; |
|
|
| beforeEach(async () => { |
| |
| testUser = await User.create({ |
| name: 'Test User', |
| email: 'test@example.com', |
| password: 'password123', |
| provider: 'local', |
| }); |
|
|
| |
| testGroup = await Group.create({ |
| name: 'Test Group', |
| source: 'local', |
| memberIds: [(testUser._id as mongoose.Types.ObjectId).toString()], |
| }); |
|
|
| |
| }); |
|
|
| test('should find group by ID', async () => { |
| const group = await methods.findGroupById(testGroup._id as mongoose.Types.ObjectId); |
|
|
| expect(group).toBeDefined(); |
| expect(group?._id.toString()).toBe(testGroup._id.toString()); |
| expect(group?.name).toBe(testGroup.name); |
| }); |
|
|
| test('should find group by ID with specific projection', async () => { |
| const group = await methods.findGroupById(testGroup._id as mongoose.Types.ObjectId, { |
| name: 1, |
| }); |
|
|
| expect(group).toBeDefined(); |
| expect(group?._id).toBeDefined(); |
| expect(group?.name).toBe(testGroup.name); |
| expect(group?.memberIds).toBeUndefined(); |
| }); |
|
|
| test('should find group by external ID', async () => { |
| |
| const entraGroup = await Group.create({ |
| name: 'Entra Group', |
| source: 'entra', |
| idOnTheSource: 'entra-id-12345', |
| }); |
|
|
| const group = await methods.findGroupByExternalId('entra-id-12345', 'entra'); |
|
|
| expect(group).toBeDefined(); |
| expect(group?._id.toString()).toBe(entraGroup._id.toString()); |
| expect(group?.idOnTheSource).toBe('entra-id-12345'); |
| }); |
|
|
| test('should return null for non-existent external ID', async () => { |
| const group = await methods.findGroupByExternalId('non-existent-id', 'entra'); |
| expect(group).toBeNull(); |
| }); |
|
|
| test('should find groups by name pattern', async () => { |
| |
| await Group.create({ name: 'Test Group 2', source: 'local' }); |
| await Group.create({ name: 'Admin Group', source: 'local' }); |
| await Group.create({ |
| name: 'Test Entra Group', |
| source: 'entra', |
| idOnTheSource: 'entra-id-xyz', |
| }); |
|
|
| |
| const testGroups = await methods.findGroupsByNamePattern('Test'); |
| expect(testGroups).toHaveLength(3); |
|
|
| |
| const localTestGroups = await methods.findGroupsByNamePattern('Test', 'local'); |
| expect(localTestGroups).toHaveLength(2); |
|
|
| const entraTestGroups = await methods.findGroupsByNamePattern('Test', 'entra'); |
| expect(entraTestGroups).toHaveLength(1); |
| }); |
|
|
| test('should respect limit parameter in name search', async () => { |
| |
| for (let i = 0; i < 10; i++) { |
| await Group.create({ name: `Numbered Group ${i}`, source: 'local' }); |
| } |
|
|
| const limitedGroups = await methods.findGroupsByNamePattern('Numbered', null, 5); |
| expect(limitedGroups).toHaveLength(5); |
| }); |
|
|
| test('should find groups by member ID', async () => { |
| |
| const group2 = await Group.create({ |
| name: 'Second Group', |
| source: 'local', |
| memberIds: [(testUser._id as mongoose.Types.ObjectId).toString()], |
| }); |
|
|
| const group3 = await Group.create({ |
| name: 'Third Group', |
| source: 'local', |
| memberIds: [new mongoose.Types.ObjectId().toString()] , |
| }); |
|
|
| const userGroups = await methods.findGroupsByMemberId( |
| testUser._id as mongoose.Types.ObjectId, |
| ); |
| expect(userGroups).toHaveLength(2); |
|
|
| |
| const groupIds = userGroups.map((g) => g._id.toString()); |
| expect(groupIds).toContain(testGroup._id.toString()); |
| expect(groupIds).toContain(group2._id.toString()); |
| expect(groupIds).not.toContain(group3._id.toString()); |
| }); |
| }); |
|
|
| describe('Group Creation and Update Methods', () => { |
| test('should create a new group', async () => { |
| const groupData = { |
| name: 'New Test Group', |
| source: 'local' as const, |
| }; |
|
|
| const group = await methods.createGroup(groupData); |
|
|
| expect(group).toBeDefined(); |
| expect(group.name).toBe(groupData.name); |
| expect(group.source).toBe(groupData.source); |
|
|
| |
| const savedGroup = await Group.findById(group._id); |
| expect(savedGroup).toBeDefined(); |
| }); |
|
|
| test('should upsert a group by external ID (create new)', async () => { |
| const groupData = { |
| name: 'New Entra Group', |
| idOnTheSource: 'new-entra-id', |
| }; |
|
|
| const group = await methods.upsertGroupByExternalId(groupData.idOnTheSource, 'entra', { |
| name: groupData.name, |
| }); |
|
|
| expect(group).toBeDefined(); |
| expect(group?.name).toBe(groupData.name); |
| expect(group?.idOnTheSource).toBe(groupData.idOnTheSource); |
| expect(group?.source).toBe('entra'); |
|
|
| |
| const savedGroup = await Group.findOne({ idOnTheSource: 'new-entra-id' }); |
| expect(savedGroup).toBeDefined(); |
| }); |
|
|
| test('should upsert a group by external ID (update existing)', async () => { |
| |
| await Group.create({ |
| name: 'Original Name', |
| source: 'entra', |
| idOnTheSource: 'existing-entra-id', |
| }); |
|
|
| |
| const updatedGroup = await methods.upsertGroupByExternalId('existing-entra-id', 'entra', { |
| name: 'Updated Name', |
| }); |
|
|
| expect(updatedGroup).toBeDefined(); |
| expect(updatedGroup?.name).toBe('Updated Name'); |
| expect(updatedGroup?.idOnTheSource).toBe('existing-entra-id'); |
|
|
| |
| const savedGroup = await Group.findOne({ idOnTheSource: 'existing-entra-id' }); |
| expect(savedGroup?.name).toBe('Updated Name'); |
| }); |
| }); |
|
|
| describe('User-Group Relationship Methods', () => { |
| let testUser1: t.IUser; |
| let testGroup: t.IGroup; |
|
|
| beforeEach(async () => { |
| |
| testUser1 = await User.create({ |
| name: 'User One', |
| email: 'user1@example.com', |
| password: 'password123', |
| provider: 'local', |
| }); |
|
|
| |
| testGroup = await Group.create({ |
| name: 'Test Group', |
| source: 'local', |
| memberIds: [] , |
| }); |
| }); |
|
|
| test('should add user to group', async () => { |
| const result = await methods.addUserToGroup( |
| testUser1._id as mongoose.Types.ObjectId, |
| testGroup._id as mongoose.Types.ObjectId, |
| ); |
|
|
| |
| expect(result).toBeDefined(); |
| expect(result.user).toBeDefined(); |
| expect(result.group).toBeDefined(); |
|
|
| |
| const userIdOnTheSource = |
| result.user.idOnTheSource || (testUser1._id as mongoose.Types.ObjectId).toString(); |
| expect(result.group?.memberIds).toContain(userIdOnTheSource); |
|
|
| |
| const updatedGroup = await Group.findById(testGroup._id); |
| expect(updatedGroup?.memberIds).toContain(userIdOnTheSource); |
| }); |
|
|
| test('should remove user from group', async () => { |
| |
| await methods.addUserToGroup( |
| testUser1._id as mongoose.Types.ObjectId, |
| testGroup._id as mongoose.Types.ObjectId, |
| ); |
|
|
| |
| const result = await methods.removeUserFromGroup( |
| testUser1._id as mongoose.Types.ObjectId, |
| testGroup._id as mongoose.Types.ObjectId, |
| ); |
|
|
| |
| expect(result).toBeDefined(); |
| expect(result.user).toBeDefined(); |
| expect(result.group).toBeDefined(); |
|
|
| |
| const userIdOnTheSource = |
| result.user.idOnTheSource || (testUser1._id as mongoose.Types.ObjectId).toString(); |
| expect(result.group?.memberIds).not.toContain(userIdOnTheSource); |
|
|
| |
| const updatedGroup = await Group.findById(testGroup._id); |
| expect(updatedGroup?.memberIds).not.toContain(userIdOnTheSource); |
| }); |
|
|
| test('should get all groups for a user', async () => { |
| |
| const group1 = await Group.create({ name: 'Group 1', source: 'local', memberIds: [] }); |
| const group2 = await Group.create({ name: 'Group 2', source: 'local', memberIds: [] }); |
|
|
| await methods.addUserToGroup( |
| testUser1._id as mongoose.Types.ObjectId, |
| group1._id as mongoose.Types.ObjectId, |
| ); |
| await methods.addUserToGroup( |
| testUser1._id as mongoose.Types.ObjectId, |
| group2._id as mongoose.Types.ObjectId, |
| ); |
|
|
| |
| const userGroups = await methods.getUserGroups(testUser1._id as mongoose.Types.ObjectId); |
|
|
| expect(userGroups).toHaveLength(2); |
| const groupIds = userGroups.map((g) => g._id.toString()); |
| expect(groupIds).toContain(group1._id.toString()); |
| expect(groupIds).toContain(group2._id.toString()); |
| }); |
|
|
| test('should return empty array for getUserGroups when user has no groups', async () => { |
| const userGroups = await methods.getUserGroups(testUser1._id as mongoose.Types.ObjectId); |
| expect(userGroups).toEqual([]); |
| }); |
|
|
| test('should get user principals', async () => { |
| |
| await methods.addUserToGroup( |
| testUser1._id as mongoose.Types.ObjectId, |
| testGroup._id as mongoose.Types.ObjectId, |
| ); |
|
|
| |
| const principals = await methods.getUserPrincipals({ |
| userId: testUser1._id as mongoose.Types.ObjectId, |
| }); |
|
|
| |
| expect(principals).toHaveLength(4); |
|
|
| |
| const userPrincipal = principals.find((p) => p.principalType === PrincipalType.USER); |
| const groupPrincipal = principals.find((p) => p.principalType === PrincipalType.GROUP); |
| const publicPrincipal = principals.find((p) => p.principalType === PrincipalType.PUBLIC); |
|
|
| expect(userPrincipal).toBeDefined(); |
| expect(userPrincipal?.principalId?.toString()).toBe( |
| (testUser1._id as mongoose.Types.ObjectId).toString(), |
| ); |
|
|
| expect(groupPrincipal).toBeDefined(); |
| expect(groupPrincipal?.principalId?.toString()).toBe(testGroup._id.toString()); |
|
|
| expect(publicPrincipal).toBeDefined(); |
| expect(publicPrincipal?.principalId).toBeUndefined(); |
| }); |
|
|
| test('should return user and public principals for non-existent user in getUserPrincipals', async () => { |
| const nonExistentId = new mongoose.Types.ObjectId(); |
| const principals = await methods.getUserPrincipals({ |
| userId: nonExistentId, |
| }); |
|
|
| |
| expect(principals).toHaveLength(2); |
| expect(principals[0].principalType).toBe(PrincipalType.USER); |
| expect(principals[0].principalId?.toString()).toBe(nonExistentId.toString()); |
| expect(principals[1].principalType).toBe(PrincipalType.PUBLIC); |
| expect(principals[1].principalId).toBeUndefined(); |
| }); |
|
|
| test('should convert string userId to ObjectId in getUserPrincipals', async () => { |
| |
| await methods.addUserToGroup( |
| testUser1._id as mongoose.Types.ObjectId, |
| testGroup._id as mongoose.Types.ObjectId, |
| ); |
|
|
| |
| const principals = await methods.getUserPrincipals({ |
| userId: (testUser1._id as mongoose.Types.ObjectId).toString(), |
| }); |
|
|
| |
| expect(principals).toHaveLength(4); |
|
|
| |
| const userPrincipal = principals.find((p) => p.principalType === PrincipalType.USER); |
| expect(userPrincipal).toBeDefined(); |
| expect(userPrincipal?.principalId).toBeInstanceOf(mongoose.Types.ObjectId); |
| expect(userPrincipal?.principalId?.toString()).toBe( |
| (testUser1._id as mongoose.Types.ObjectId).toString(), |
| ); |
|
|
| |
| const groupPrincipal = principals.find((p) => p.principalType === PrincipalType.GROUP); |
| expect(groupPrincipal).toBeDefined(); |
| expect(groupPrincipal?.principalId).toBeInstanceOf(mongoose.Types.ObjectId); |
| expect(groupPrincipal?.principalId?.toString()).toBe(testGroup._id.toString()); |
| }); |
|
|
| test('should include role principal as string in getUserPrincipals', async () => { |
| |
| const userWithRole = await User.create({ |
| name: 'Admin User', |
| email: 'admin@example.com', |
| password: 'password123', |
| provider: 'local', |
| role: 'ADMIN', |
| }); |
|
|
| |
| const principals = await methods.getUserPrincipals({ |
| userId: userWithRole._id as mongoose.Types.ObjectId, |
| }); |
|
|
| |
| expect(principals).toHaveLength(3); |
|
|
| |
| const rolePrincipal = principals.find((p) => p.principalType === PrincipalType.ROLE); |
| expect(rolePrincipal).toBeDefined(); |
| expect(typeof rolePrincipal?.principalId).toBe('string'); |
| expect(rolePrincipal?.principalId).toBe('ADMIN'); |
| }); |
| }); |
|
|
| describe('Entra ID Synchronization', () => { |
| let testUser: t.IUser; |
|
|
| beforeEach(async () => { |
| testUser = await User.create({ |
| name: 'Entra User', |
| email: 'entra@example.com', |
| password: 'password123', |
| provider: 'entra', |
| idOnTheSource: 'entra-user-123', |
| }); |
| }); |
|
|
| |
| test.skip('should sync Entra groups for a user (add new groups)', async () => { |
| |
| const entraGroups = [ |
| { id: 'entra-group-1', name: 'Entra Group 1' }, |
| { id: 'entra-group-2', name: 'Entra Group 2' }, |
| ]; |
|
|
| const result = await methods.syncUserEntraGroups( |
| testUser._id as mongoose.Types.ObjectId, |
| entraGroups, |
| ); |
|
|
| |
| expect(result).toBeDefined(); |
| expect(result.user).toBeDefined(); |
| expect(result.addedGroups).toHaveLength(2); |
| expect(result.removedGroups).toHaveLength(0); |
|
|
| |
| const groups = await Group.find({ source: 'entra' }); |
| expect(groups).toHaveLength(2); |
|
|
| |
| const user = await User.findById(testUser._id); |
| expect(user).toBeDefined(); |
|
|
| |
| for (const group of groups) { |
| expect(group.memberIds).toContain( |
| testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), |
| ); |
| } |
| }); |
|
|
| test.skip('should sync Entra groups for a user (add and remove groups)', async () => { |
| |
| await Group.create({ |
| name: 'Existing Group 1', |
| source: 'entra', |
| idOnTheSource: 'existing-1', |
| memberIds: [testUser.idOnTheSource], |
| }); |
|
|
| const existingGroup2 = await Group.create({ |
| name: 'Existing Group 2', |
| source: 'entra', |
| idOnTheSource: 'existing-2', |
| memberIds: [testUser.idOnTheSource], |
| }); |
|
|
| |
|
|
| |
| const entraGroups = [ |
| { id: 'existing-1', name: 'Existing Group 1' } , |
| { id: 'new-group', name: 'New Group' } , |
| |
| ]; |
|
|
| const result = await methods.syncUserEntraGroups( |
| testUser._id as mongoose.Types.ObjectId, |
| entraGroups, |
| ); |
|
|
| |
| expect(result).toBeDefined(); |
| expect(result.addedGroups).toHaveLength(1); |
| expect(result.removedGroups).toHaveLength(1); |
|
|
| |
| const removedGroup = await Group.findById(existingGroup2._id); |
| expect(removedGroup?.memberIds).toHaveLength(0); |
|
|
| |
| const newGroup = await Group.findOne({ idOnTheSource: 'new-group' }); |
| expect(newGroup).toBeDefined(); |
| expect(newGroup?.memberIds).toContain( |
| testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), |
| ); |
| }); |
|
|
| test('should throw error for non-existent user in syncUserEntraGroups', async () => { |
| const nonExistentId = new mongoose.Types.ObjectId(); |
| const entraGroups = [{ id: 'some-id', name: 'Some Group' }]; |
|
|
| await expect(methods.syncUserEntraGroups(nonExistentId, entraGroups)).rejects.toThrow( |
| 'User not found', |
| ); |
| }); |
|
|
| test.skip('should preserve local groups when syncing Entra groups', async () => { |
| |
| const localGroup = await Group.create({ |
| name: 'Local Group', |
| source: 'local', |
| memberIds: [testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString()], |
| }); |
|
|
| |
|
|
| |
| const entraGroups = [{ id: 'entra-group', name: 'Entra Group' }]; |
|
|
| const result = await methods.syncUserEntraGroups( |
| testUser._id as mongoose.Types.ObjectId, |
| entraGroups, |
| ); |
|
|
| |
| expect(result).toBeDefined(); |
|
|
| |
| const savedLocalGroup = await Group.findById(localGroup._id); |
| expect(savedLocalGroup).toBeDefined(); |
| expect(savedLocalGroup?.memberIds).toContain( |
| testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), |
| ); |
|
|
| |
| const entraGroup = await Group.findOne({ idOnTheSource: 'entra-group' }); |
| expect(entraGroup).toBeDefined(); |
| expect(entraGroup?.memberIds).toContain( |
| testUser.idOnTheSource || (testUser._id as mongoose.Types.ObjectId).toString(), |
| ); |
| }); |
| }); |
| }); |
|
|