import mongoose from 'mongoose'; 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'; /** Mocking logger */ jest.mock('~/config/winston', () => ({ error: jest.fn(), info: jest.fn(), debug: jest.fn(), })); let mongoServer: MongoMemoryServer; let Group: mongoose.Model; let User: mongoose.Model; let methods: ReturnType; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); const mongoUri = mongoServer.getUri(); await mongoose.connect(mongoUri); /** Register models */ Group = mongoose.models.Group || mongoose.model('Group', groupSchema); User = mongoose.models.User || mongoose.model('User', userSchema); /** Initialize methods */ methods = createUserGroupMethods(mongoose); }); afterAll(async () => { await mongoose.disconnect(); await mongoServer.stop(); }); beforeEach(async () => { await mongoose.connection.dropDatabase(); }); describe('UserGroup Methods - Detailed Tests', () => { describe('findGroupById', () => { test('should find group by ObjectId', async () => { const group = await Group.create({ name: 'Test Group', source: 'local', memberIds: [], }); const found = await methods.findGroupById(group._id as mongoose.Types.ObjectId); expect(found).toBeDefined(); expect(found?._id.toString()).toBe(group._id.toString()); expect(found?.name).toBe('Test Group'); }); test('should find group by string ID', async () => { const group = await Group.create({ name: 'Test Group', source: 'local', memberIds: [], }); const found = await methods.findGroupById(group._id as mongoose.Types.ObjectId); expect(found).toBeDefined(); expect(found?._id.toString()).toBe(group._id.toString()); }); test('should apply projection correctly', async () => { const group = await Group.create({ name: 'Test Group', source: 'local', description: 'Test Description', memberIds: ['user1', 'user2'], }); const found = await methods.findGroupById(group._id as mongoose.Types.ObjectId, { name: 1, }); expect(found).toBeDefined(); expect(found?.name).toBe('Test Group'); expect(found?.description).toBeUndefined(); expect(found?.memberIds).toBeUndefined(); }); test('should return null for non-existent group', async () => { const fakeId = new mongoose.Types.ObjectId(); const found = await methods.findGroupById(fakeId as mongoose.Types.ObjectId); expect(found).toBeNull(); }); }); describe('findGroupByExternalId', () => { test('should find group by external ID and source', async () => { await Group.create({ name: 'Entra Group', source: 'entra', idOnTheSource: 'entra-123', memberIds: [], }); const found = await methods.findGroupByExternalId('entra-123', 'entra'); expect(found).toBeDefined(); expect(found?.idOnTheSource).toBe('entra-123'); expect(found?.source).toBe('entra'); }); test('should not find group with wrong source', async () => { await Group.create({ name: 'Entra Group', source: 'entra', idOnTheSource: 'entra-123', memberIds: [], }); const found = await methods.findGroupByExternalId('entra-123', 'local'); expect(found).toBeNull(); }); test('should handle multiple groups with same external ID but different sources', async () => { const id = 'shared-id'; await Group.create({ name: 'Entra Group', source: 'entra', idOnTheSource: id, memberIds: [], }); await Group.create({ name: 'Local Group', source: 'local', memberIds: [], }); const entraGroup = await methods.findGroupByExternalId(id, 'entra'); const localGroup = await methods.findGroupByExternalId(id, 'local'); expect(entraGroup?.name).toBe('Entra Group'); expect(localGroup).toBeNull(); // local groups don't use idOnTheSource by default }); }); describe('findGroupsByNamePattern', () => { beforeEach(async () => { await Group.create([ { name: 'Engineering Team', source: 'local', memberIds: [] }, { name: 'Engineering Managers', source: 'local', memberIds: [] }, { name: 'Marketing Team', source: 'local', memberIds: [] }, { name: 'Remote Engineering', source: 'entra', idOnTheSource: 'entra-remote-eng', memberIds: [], }, ]); }); test('should find groups by name pattern', async () => { const groups = await methods.findGroupsByNamePattern('Engineering'); expect(groups).toHaveLength(3); expect(groups.every((g) => g.name.includes('Engineering'))).toBe(true); }); test('should respect case insensitive search', async () => { const groups = await methods.findGroupsByNamePattern('engineering'); expect(groups).toHaveLength(3); }); test('should filter by source when provided', async () => { const groups = await methods.findGroupsByNamePattern('Engineering', 'local'); expect(groups).toHaveLength(2); expect(groups.every((g) => g.source === 'local')).toBe(true); }); test('should respect limit parameter', async () => { const groups = await methods.findGroupsByNamePattern('Engineering', null, 2); expect(groups).toHaveLength(2); }); test('should return empty array for no matches', async () => { const groups = await methods.findGroupsByNamePattern('NonExistent'); expect(groups).toEqual([]); }); }); describe('findGroupsByMemberId', () => { let user1: mongoose.HydratedDocument; beforeEach(async () => { user1 = await User.create({ name: 'User 1', email: 'user1@test.com', provider: 'local', }); }); test('should find groups by member ObjectId', async () => { await Group.create([ { name: 'Group 1', source: 'local', memberIds: [(user1._id as mongoose.Types.ObjectId).toString(), 'other-user'], }, { name: 'Group 2', source: 'local', memberIds: [(user1._id as mongoose.Types.ObjectId).toString()], }, { name: 'Group 3', source: 'local', memberIds: ['other-user'], }, ]); const groups = await methods.findGroupsByMemberId(user1._id as mongoose.Types.ObjectId); expect(groups).toHaveLength(2); expect(groups.map((g) => g.name).sort()).toEqual(['Group 1', 'Group 2']); }); test('should find groups by member string ID', async () => { await Group.create([ { name: 'Group 1', source: 'local', memberIds: [(user1._id as mongoose.Types.ObjectId).toString()], }, ]); const groups = await methods.findGroupsByMemberId(user1._id as mongoose.Types.ObjectId); expect(groups).toHaveLength(1); expect(groups[0].name).toBe('Group 1'); }); test('should return empty array for user with no groups', async () => { const groups = await methods.findGroupsByMemberId(user1._id as mongoose.Types.ObjectId); expect(groups).toEqual([]); }); }); describe('createGroup', () => { test('should create a group with all fields', async () => { const groupData: Partial = { name: 'New Group', source: 'local', description: 'A test group', email: 'group@test.com', avatar: 'avatar-url', memberIds: ['user1', 'user2'], }; const group = await methods.createGroup(groupData); expect(group).toBeDefined(); expect(group.name).toBe(groupData.name); expect(group.source).toBe(groupData.source); expect(group.description).toBe(groupData.description); expect(group.email).toBe(groupData.email); expect(group.avatar).toBe(groupData.avatar); expect(group.memberIds).toEqual(groupData.memberIds); }); test('should create group with minimal data', async () => { const group = await methods.createGroup({ name: 'Minimal Group', }); expect(group).toBeDefined(); expect(group.name).toBe('Minimal Group'); expect(group.source).toBe('local'); // default expect(group.memberIds).toEqual([]); // default }); }); describe('upsertGroupByExternalId', () => { test('should create new group when not exists', async () => { const group = await methods.upsertGroupByExternalId('new-external-id', 'entra', { name: 'New External Group', description: 'Created by upsert', }); expect(group).toBeDefined(); expect(group?.idOnTheSource).toBe('new-external-id'); expect(group?.source).toBe('entra'); expect(group?.name).toBe('New External Group'); expect(group?.description).toBe('Created by upsert'); }); test('should update existing group', async () => { // Create initial group await Group.create({ name: 'Original Name', source: 'entra', idOnTheSource: 'existing-id', description: 'Original description', memberIds: ['user1'], }); // Upsert with updates const updated = await methods.upsertGroupByExternalId('existing-id', 'entra', { name: 'Updated Name', description: 'Updated description', memberIds: ['user1', 'user2'], }); expect(updated).toBeDefined(); expect(updated?.name).toBe('Updated Name'); expect(updated?.description).toBe('Updated description'); expect(updated?.memberIds).toEqual(['user1', 'user2']); expect(updated?.idOnTheSource).toBe('existing-id'); // unchanged }); test('should not update group from different source', async () => { await Group.create({ name: 'Entra Group', source: 'entra', idOnTheSource: 'shared-id', }); const result = await methods.upsertGroupByExternalId('shared-id', 'local', { name: 'Azure Group', }); // Should create new group expect(result?.name).toBe('Azure Group'); expect(result?.source).toBe('local'); // Verify both exist const groups = await Group.find({ idOnTheSource: 'shared-id' }); expect(groups).toHaveLength(2); }); }); describe('addUserToGroup and removeUserFromGroup', () => { let user: mongoose.HydratedDocument; let userWithExternal: mongoose.HydratedDocument; let group: mongoose.HydratedDocument; beforeEach(async () => { user = await User.create({ name: 'Test User', email: 'user@test.com', provider: 'local', }); userWithExternal = await User.create({ name: 'External User', email: 'external@test.com', provider: 'entra', idOnTheSource: 'external-123', }); group = await Group.create({ name: 'Test Group', source: 'local', memberIds: [], }); }); test('should add user to group using user ID', async () => { const result = await methods.addUserToGroup( user._id as mongoose.Types.ObjectId, group._id as mongoose.Types.ObjectId, ); expect(result.user).toBeDefined(); expect(result.group).toBeDefined(); expect(result.group?.memberIds).toContain((user._id as mongoose.Types.ObjectId).toString()); }); test('should add user to group using idOnTheSource if available', async () => { const result = await methods.addUserToGroup( userWithExternal._id as mongoose.Types.ObjectId, group._id as mongoose.Types.ObjectId, ); expect(result.group?.memberIds).toContain('external-123'); expect(result.group?.memberIds).not.toContain( (userWithExternal._id as mongoose.Types.ObjectId).toString(), ); }); test('should not duplicate user in group', async () => { // Add user first time await methods.addUserToGroup( user._id as mongoose.Types.ObjectId, group._id as mongoose.Types.ObjectId, ); // Add same user again const result = await methods.addUserToGroup( user._id as mongoose.Types.ObjectId, group._id as mongoose.Types.ObjectId, ); expect(result.group?.memberIds).toHaveLength(1); expect(result.group?.memberIds).toContain((user._id as mongoose.Types.ObjectId).toString()); }); test('should remove user from group', async () => { // First add user await methods.addUserToGroup( user._id as mongoose.Types.ObjectId, group._id as mongoose.Types.ObjectId, ); // Then remove const result = await methods.removeUserFromGroup( user._id as mongoose.Types.ObjectId, group._id as mongoose.Types.ObjectId, ); expect(result.group?.memberIds).toHaveLength(0); expect(result.group?.memberIds).not.toContain( (user._id as mongoose.Types.ObjectId).toString(), ); }); test('should handle removing user not in group', async () => { const result = await methods.removeUserFromGroup( user._id as mongoose.Types.ObjectId, group._id as mongoose.Types.ObjectId, ); expect(result.group?.memberIds).toHaveLength(0); }); }); describe('getUserGroups', () => { let user: mongoose.HydratedDocument; beforeEach(async () => { user = await User.create({ name: 'Test User', email: 'user@test.com', provider: 'local', }); }); test('should get all groups for a user', async () => { // Create groups with user as member await Group.create([ { name: 'Group 1', source: 'local', memberIds: [(user._id as mongoose.Types.ObjectId).toString()], }, { name: 'Group 2', source: 'local', memberIds: [(user._id as mongoose.Types.ObjectId).toString(), 'other-user'], }, { name: 'Group 3', source: 'local', memberIds: ['other-user'], }, ]); const groups = await methods.getUserGroups(user._id as mongoose.Types.ObjectId); expect(groups).toHaveLength(2); expect(groups.map((g) => g.name).sort()).toEqual(['Group 1', 'Group 2']); }); test('should return empty array for user with no groups', async () => { const groups = await methods.getUserGroups(user._id as mongoose.Types.ObjectId); expect(groups).toEqual([]); }); test('should handle user with idOnTheSource', async () => { const externalUser = await User.create({ name: 'External User', email: 'external@test.com', provider: 'entra', idOnTheSource: 'external-456', }); await Group.create({ name: 'External Group', source: 'entra', idOnTheSource: 'entra-external-group', memberIds: ['external-456'], // Using idOnTheSource }); const groups = await methods.getUserGroups(externalUser._id as mongoose.Types.ObjectId); expect(groups).toHaveLength(1); expect(groups[0].name).toBe('External Group'); }); }); describe('syncUserEntraGroups', () => { let user: mongoose.HydratedDocument; beforeEach(async () => { user = await User.create({ name: 'Entra User', email: 'entra@test.com', provider: 'entra', idOnTheSource: 'entra-user-123', }); }); test('should create new groups and add user', async () => { const entraGroups = [ { id: 'group-1', name: 'Entra Group 1' }, { id: 'group-2', name: 'Entra Group 2' }, ]; const result = await methods.syncUserEntraGroups( user._id as mongoose.Types.ObjectId, entraGroups, ); expect(result.user).toBeDefined(); expect(result.addedGroups).toHaveLength(2); expect(result.removedGroups).toHaveLength(0); // Verify groups were created const groups = await Group.find({ source: 'entra' }); expect(groups).toHaveLength(2); // Verify user is member of both for (const group of groups) { expect(group.memberIds).toContain('entra-user-123'); } }); test('should remove user from groups not in sync list', async () => { // Create existing groups const group1 = await Group.create({ name: 'Keep Group', source: 'entra', idOnTheSource: 'keep-group', memberIds: ['entra-user-123'], }); const group2 = await Group.create({ name: 'Remove Group', source: 'entra', idOnTheSource: 'remove-group', memberIds: ['entra-user-123'], }); // Sync with only one group const result = await methods.syncUserEntraGroups(user._id as mongoose.Types.ObjectId, [ { id: 'keep-group', name: 'Keep Group' }, ]); expect(result.addedGroups).toHaveLength(0); expect(result.removedGroups).toHaveLength(1); // Verify membership const keepGroup = await Group.findById(group1._id); const removeGroup = await Group.findById(group2._id); expect(keepGroup?.memberIds).toContain('entra-user-123'); expect(removeGroup?.memberIds).not.toContain('entra-user-123'); }); test('should not affect local groups', async () => { // Create local group const localGroup = await Group.create({ name: 'Local Group', source: 'local', memberIds: ['entra-user-123'], }); // Sync entra groups await methods.syncUserEntraGroups(user._id as mongoose.Types.ObjectId, [ { id: 'entra-group', name: 'Entra Group' }, ]); // Verify local group unchanged const savedLocalGroup = await Group.findById(localGroup._id); expect(savedLocalGroup?.memberIds).toContain('entra-user-123'); }); test('should throw error for non-existent user', async () => { const fakeId = new mongoose.Types.ObjectId(); await expect(methods.syncUserEntraGroups(fakeId, [])).rejects.toThrow('User not found'); }); }); describe('sortPrincipalsByRelevance', () => { test('should sort principals by relevance score', async () => { const principals = [ { id: '1', name: 'Test User', type: 'user' as const, source: 'local' as const }, { id: '2', name: 'Admin Test', type: 'user' as const, source: 'local' as const }, { id: '3', name: 'Test Group', type: 'group' as const, source: 'local' as const }, ]; // Store original query in closure or pass it through const sorted = methods.sortPrincipalsByRelevance(principals); // Since we can't pass the query directly, the method should maintain // the original order or have been called in a context where it knows the query expect(sorted).toBeDefined(); expect(sorted).toHaveLength(3); }); }); });