| | 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'; |
| |
|
| | |
| | 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(); |
| | await mongoose.connect(mongoUri); |
| |
|
| | |
| | Group = mongoose.models.Group || mongoose.model<t.IGroup>('Group', groupSchema); |
| | User = mongoose.models.User || mongoose.model<t.IUser>('User', userSchema); |
| |
|
| | |
| | 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(); |
| | }); |
| | }); |
| |
|
| | 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<t.IUser>; |
| |
|
| | 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<t.IGroup> = { |
| | 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'); |
| | expect(group.memberIds).toEqual([]); |
| | }); |
| | }); |
| |
|
| | 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 () => { |
| | |
| | await Group.create({ |
| | name: 'Original Name', |
| | source: 'entra', |
| | idOnTheSource: 'existing-id', |
| | description: 'Original description', |
| | memberIds: ['user1'], |
| | }); |
| |
|
| | |
| | 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'); |
| | }); |
| |
|
| | 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', |
| | }); |
| |
|
| | |
| | expect(result?.name).toBe('Azure Group'); |
| | expect(result?.source).toBe('local'); |
| |
|
| | |
| | const groups = await Group.find({ idOnTheSource: 'shared-id' }); |
| | expect(groups).toHaveLength(2); |
| | }); |
| | }); |
| |
|
| | describe('addUserToGroup and removeUserFromGroup', () => { |
| | let user: mongoose.HydratedDocument<t.IUser>; |
| | let userWithExternal: mongoose.HydratedDocument<t.IUser>; |
| | let group: mongoose.HydratedDocument<t.IGroup>; |
| |
|
| | 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 () => { |
| | |
| | await methods.addUserToGroup( |
| | user._id as mongoose.Types.ObjectId, |
| | group._id as mongoose.Types.ObjectId, |
| | ); |
| |
|
| | |
| | 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 () => { |
| | |
| | await methods.addUserToGroup( |
| | user._id as mongoose.Types.ObjectId, |
| | group._id as mongoose.Types.ObjectId, |
| | ); |
| |
|
| | |
| | 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<t.IUser>; |
| |
|
| | beforeEach(async () => { |
| | user = await User.create({ |
| | name: 'Test User', |
| | email: 'user@test.com', |
| | provider: 'local', |
| | }); |
| | }); |
| |
|
| | test('should get all groups for a user', async () => { |
| | |
| | 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'], |
| | }); |
| |
|
| | 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<t.IUser>; |
| |
|
| | 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); |
| |
|
| | |
| | const groups = await Group.find({ source: 'entra' }); |
| | expect(groups).toHaveLength(2); |
| |
|
| | |
| | for (const group of groups) { |
| | expect(group.memberIds).toContain('entra-user-123'); |
| | } |
| | }); |
| |
|
| | test('should remove user from groups not in sync list', async () => { |
| | |
| | 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'], |
| | }); |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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 () => { |
| | |
| | const localGroup = await Group.create({ |
| | name: 'Local Group', |
| | source: 'local', |
| | memberIds: ['entra-user-123'], |
| | }); |
| |
|
| | |
| | await methods.syncUserEntraGroups(user._id as mongoose.Types.ObjectId, [ |
| | { id: 'entra-group', name: 'Entra Group' }, |
| | ]); |
| |
|
| | |
| | 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 }, |
| | ]; |
| |
|
| | |
| | const sorted = methods.sortPrincipalsByRelevance(principals); |
| |
|
| | |
| | |
| | expect(sorted).toBeDefined(); |
| | expect(sorted).toHaveLength(3); |
| | }); |
| | }); |
| | }); |
| |
|