Skip to main content
Glama
team-manager.test.ts21.8 kB
import { TeamManager } from '../team-manager'; import { WorkspaceManagementTool } from '../workspace-tool'; import { TeamService } from '../../services/team-service'; import { TallyApiClientConfig } from '../../services/TallyApiClient'; import { CreateTeamRequest, UpdateTeamRequest, BulkMembershipUpdate, TeamMembershipUpdate, TeamPermission, BulkTeamOperation, TeamResponse, TeamListResponse, TeamMember, BulkOperationResponse, OrganizationStructure, TeamAccessSummary, TeamAnalytics, TallySuccessResponse, TeamRole } from '../../models'; // Mock the dependencies jest.mock('../workspace-tool'); jest.mock('../../services/team-service'); describe('TeamManager', () => { let teamManager: TeamManager; let mockWorkspaceTool: jest.Mocked<WorkspaceManagementTool>; let mockTeamService: jest.Mocked<TeamService>; let mockApiClientConfig: TallyApiClientConfig; const mockTeamResponse: TeamResponse = { team: { id: 'team-123', name: 'Test Team', description: 'A test team', workspaceId: 'workspace-123', parentTeamId: undefined, childTeams: [], members: [ { id: 'member-1', userId: 'user-1', email: 'user1@example.com', role: 'team_lead' as TeamRole, permissions: ['read', 'write'], joinedAt: '2023-01-01T00:00:00Z' } ], settings: { isPrivate: false, allowSelfJoin: false, inheritPermissions: true }, metadata: { tags: [] }, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:00:00Z', createdBy: 'creator-1' }, memberCount: 1, childTeamCount: 0 }; const mockTeamMember: TeamMember = { id: 'member-2', userId: 'user-2', email: 'user2@example.com', role: 'member' as TeamRole, permissions: ['read'], joinedAt: '2023-01-01T00:00:00Z' }; const mockSuccessResponse: TallySuccessResponse = { success: true, message: 'Operation completed successfully' }; beforeEach(() => { mockApiClientConfig = { accessToken: 'test-token', baseURL: 'https://api.tally.so' }; // Clear all mocks jest.clearAllMocks(); // Create TeamManager instance teamManager = new TeamManager(mockApiClientConfig); // Get mocked instances mockWorkspaceTool = (teamManager as any).workspaceTool as jest.Mocked<WorkspaceManagementTool>; mockTeamService = (teamManager as any).teamService as jest.Mocked<TeamService>; }); describe('constructor', () => { it('should create an instance with required dependencies', () => { expect(teamManager).toBeInstanceOf(TeamManager); expect(WorkspaceManagementTool).toHaveBeenCalledWith(mockApiClientConfig); expect(TeamService).toHaveBeenCalledWith(mockApiClientConfig); }); it('should create an instance with default config when none provided', () => { const defaultTeamManager = new TeamManager(); expect(defaultTeamManager).toBeInstanceOf(TeamManager); expect(WorkspaceManagementTool).toHaveBeenCalledWith({}); expect(TeamService).toHaveBeenCalledWith({}); }); }); describe('createTeam', () => { const createRequest: CreateTeamRequest = { workspaceId: 'workspace-123', name: 'New Team', description: 'A new team' }; it('should create a team successfully after validating workspace', async () => { // Arrange mockWorkspaceTool.getWorkspaceDetails.mockResolvedValue({} as any); mockTeamService.createTeam.mockResolvedValue(mockTeamResponse); // Act const result = await teamManager.createTeam(createRequest); // Assert expect(mockWorkspaceTool.getWorkspaceDetails).toHaveBeenCalledWith('workspace-123'); expect(mockTeamService.createTeam).toHaveBeenCalledWith(createRequest); expect(result).toEqual(mockTeamResponse); }); it('should throw error if workspace validation fails', async () => { // Arrange const workspaceError = new Error('Workspace not found'); mockWorkspaceTool.getWorkspaceDetails.mockRejectedValue(workspaceError); // Act & Assert await expect(teamManager.createTeam(createRequest)).rejects.toThrow('Workspace not found'); expect(mockTeamService.createTeam).not.toHaveBeenCalled(); }); }); describe('getTeam', () => { it('should get team details successfully', async () => { // Arrange mockTeamService.getTeam.mockResolvedValue(mockTeamResponse); // Act const result = await teamManager.getTeam('team-123'); // Assert expect(mockTeamService.getTeam).toHaveBeenCalledWith('team-123'); expect(result).toEqual(mockTeamResponse); }); it('should propagate service errors', async () => { // Arrange const serviceError = new Error('Team not found'); mockTeamService.getTeam.mockRejectedValue(serviceError); // Act & Assert await expect(teamManager.getTeam('team-123')).rejects.toThrow('Team not found'); }); }); describe('updateTeam', () => { const updateRequest: UpdateTeamRequest = { name: 'Updated Team', description: 'Updated description' }; it('should update team without parent change', async () => { // Arrange mockTeamService.updateTeam.mockResolvedValue(mockTeamResponse); // Act const result = await teamManager.updateTeam('team-123', updateRequest); // Assert expect(mockTeamService.updateTeam).toHaveBeenCalledWith('team-123', updateRequest); expect(result).toEqual(mockTeamResponse); }); it('should validate hierarchy when changing parent team', async () => { // Arrange const updateWithParent = { ...updateRequest, parentTeamId: 'parent-team' }; const validateSpy = jest.spyOn(teamManager as any, 'validateTeamHierarchy').mockResolvedValue(undefined); mockTeamService.updateTeam.mockResolvedValue(mockTeamResponse); // Act const result = await teamManager.updateTeam('team-123', updateWithParent); // Assert expect(validateSpy).toHaveBeenCalledWith('team-123', 'parent-team'); expect(mockTeamService.updateTeam).toHaveBeenCalledWith('team-123', updateWithParent); expect(result).toEqual(mockTeamResponse); }); it('should handle setting parentTeamId to undefined', async () => { // Arrange const updateWithUndefinedParent = { ...updateRequest, parentTeamId: undefined }; const validateSpy = jest.spyOn(teamManager as any, 'validateTeamHierarchy').mockResolvedValue(undefined); mockTeamService.updateTeam.mockResolvedValue(mockTeamResponse); // Act await teamManager.updateTeam('team-123', updateWithUndefinedParent); // Assert expect(validateSpy).toHaveBeenCalledWith('team-123', undefined); }); }); describe('deleteTeam', () => { const teamWithMembers: TeamResponse = { team: { ...mockTeamResponse.team, members: [mockTeamMember], childTeams: ['child-team-1', 'child-team-2'] }, memberCount: 1, childTeamCount: 2 }; it('should delete team without options', async () => { // Arrange mockTeamService.getTeam.mockResolvedValue(mockTeamResponse); mockTeamService.deleteTeam.mockResolvedValue(mockSuccessResponse); // Act const result = await teamManager.deleteTeam('team-123'); // Assert expect(mockTeamService.getTeam).toHaveBeenCalledWith('team-123'); expect(mockTeamService.deleteTeam).toHaveBeenCalledWith('team-123'); expect(result).toEqual(mockSuccessResponse); }); it('should reassign members when reassignMembersTo option provided', async () => { // Arrange mockTeamService.getTeam.mockResolvedValue(teamWithMembers); mockTeamService.addTeamMember.mockResolvedValue(mockTeamMember); mockTeamService.deleteTeam.mockResolvedValue(mockSuccessResponse); // Act await teamManager.deleteTeam('team-123', { reassignMembersTo: 'new-team' }); // Assert expect(mockTeamService.addTeamMember).toHaveBeenCalledWith( 'new-team', 'user-2', 'member', ['read'] ); }); it('should delete child teams when deleteChildTeams option is true', async () => { // Arrange const childTeam1: TeamResponse = { team: { ...mockTeamResponse.team, id: 'child-team-1', childTeams: [] }, memberCount: 0, childTeamCount: 0 }; const childTeam2: TeamResponse = { team: { ...mockTeamResponse.team, id: 'child-team-2', childTeams: [] }, memberCount: 0, childTeamCount: 0 }; // Set up different responses for different team IDs mockTeamService.getTeam .mockImplementation((teamId: string) => { if (teamId === 'team-123') return Promise.resolve(teamWithMembers); if (teamId === 'child-team-1') return Promise.resolve(childTeam1); if (teamId === 'child-team-2') return Promise.resolve(childTeam2); return Promise.reject(new Error('Team not found')); }); const deleteSpy = jest.spyOn(teamManager, 'deleteTeam'); mockTeamService.deleteTeam.mockResolvedValue(mockSuccessResponse); // Act await teamManager.deleteTeam('team-123', { deleteChildTeams: true }); // Assert expect(deleteSpy).toHaveBeenCalledWith('child-team-1', { deleteChildTeams: true }); expect(deleteSpy).toHaveBeenCalledWith('child-team-2', { deleteChildTeams: true }); expect(deleteSpy).toHaveBeenCalledTimes(3); // original call + 2 child calls }); it('should move child teams to parent when deleteChildTeams is false', async () => { // Arrange const teamWithParent = { team: { ...teamWithMembers.team, parentTeamId: 'parent-team' } }; mockTeamService.getTeam.mockResolvedValue(teamWithParent); const moveSpy = jest.spyOn(teamManager, 'moveTeam').mockResolvedValue(mockTeamResponse); mockTeamService.deleteTeam.mockResolvedValue(mockSuccessResponse); // Act await teamManager.deleteTeam('team-123', { deleteChildTeams: false }); // Assert expect(moveSpy).toHaveBeenCalledWith('child-team-1', 'parent-team'); expect(moveSpy).toHaveBeenCalledWith('child-team-2', 'parent-team'); }); }); describe('getTeams', () => { const mockTeamListResponse: TeamListResponse = { teams: [mockTeamResponse.team], totalCount: 1, page: 1, limit: 10, hasMore: false }; it('should get teams list successfully', async () => { // Arrange mockWorkspaceTool.getWorkspaceDetails.mockResolvedValue({} as any); mockTeamService.getTeams.mockResolvedValue(mockTeamListResponse); // Act const result = await teamManager.getTeams('workspace-123'); // Assert expect(mockWorkspaceTool.getWorkspaceDetails).toHaveBeenCalledWith('workspace-123'); expect(mockTeamService.getTeams).toHaveBeenCalledWith('workspace-123', {}); expect(result).toEqual(mockTeamListResponse); }); it('should pass all options to service', async () => { // Arrange const options = { page: 2, limit: 20, parentTeamId: 'parent-123', includeSubteams: true, sortBy: 'name' as const, sortOrder: 'desc' as const }; mockWorkspaceTool.getWorkspaceDetails.mockResolvedValue({} as any); mockTeamService.getTeams.mockResolvedValue(mockTeamListResponse); // Act await teamManager.getTeams('workspace-123', options); // Assert expect(mockTeamService.getTeams).toHaveBeenCalledWith('workspace-123', options); }); }); describe('addTeamMember', () => { it('should add team member with default role and permissions', async () => { // Arrange mockTeamService.getTeam.mockResolvedValue(mockTeamResponse); mockTeamService.addTeamMember.mockResolvedValue(mockTeamMember); // Act const result = await teamManager.addTeamMember('team-123', 'user-123'); // Assert expect(mockTeamService.getTeam).toHaveBeenCalledWith('team-123'); expect(mockTeamService.addTeamMember).toHaveBeenCalledWith('team-123', 'user-123', 'member', []); expect(result).toEqual(mockTeamMember); }); it('should add team member with custom role and permissions', async () => { // Arrange mockTeamService.getTeam.mockResolvedValue(mockTeamResponse); mockTeamService.addTeamMember.mockResolvedValue(mockTeamMember); // Act const result = await teamManager.addTeamMember('team-123', 'user-123', 'manager', ['read', 'write', 'admin']); // Assert expect(mockTeamService.addTeamMember).toHaveBeenCalledWith('team-123', 'user-123', 'manager', ['read', 'write', 'admin']); expect(result).toEqual(mockTeamMember); }); it('should throw error if team validation fails', async () => { // Arrange const teamError = new Error('Team not found'); mockTeamService.getTeam.mockRejectedValue(teamError); // Act & Assert await expect(teamManager.addTeamMember('team-123', 'user-123')).rejects.toThrow('Team not found'); expect(mockTeamService.addTeamMember).not.toHaveBeenCalled(); }); }); describe('removeTeamMember', () => { it('should remove team member successfully', async () => { // Arrange mockTeamService.removeTeamMember.mockResolvedValue(mockSuccessResponse); // Act const result = await teamManager.removeTeamMember('team-123', 'user-123'); // Assert expect(mockTeamService.removeTeamMember).toHaveBeenCalledWith('team-123', 'user-123'); expect(result).toEqual(mockSuccessResponse); }); }); describe('updateTeamMember', () => { it('should update team member role and permissions', async () => { // Arrange const updates = { role: 'manager' as TeamRole, permissions: ['read', 'write'] }; mockTeamService.updateTeamMember.mockResolvedValue(mockTeamMember); // Act const result = await teamManager.updateTeamMember('team-123', 'user-123', updates); // Assert expect(mockTeamService.updateTeamMember).toHaveBeenCalledWith('team-123', 'user-123', updates); expect(result).toEqual(mockTeamMember); }); it('should update only role when permissions not provided', async () => { // Arrange const updates = { role: 'admin' as TeamRole }; mockTeamService.updateTeamMember.mockResolvedValue(mockTeamMember); // Act await teamManager.updateTeamMember('team-123', 'user-123', updates); // Assert expect(mockTeamService.updateTeamMember).toHaveBeenCalledWith('team-123', 'user-123', updates); }); }); describe('bulkUpdateMemberships', () => { it('should perform bulk membership updates', async () => { // Arrange const bulkUpdate: BulkMembershipUpdate = { teamId: 'team-123', updates: [ { userId: 'user-1', action: 'add', role: 'member' }, { userId: 'user-2', action: 'remove' } ] }; const mockBulkResponse: BulkOperationResponse = { success: true, processedCount: 2, errorCount: 0, errors: [] }; mockTeamService.bulkUpdateMemberships.mockResolvedValue(mockBulkResponse); // Act const result = await teamManager.bulkUpdateMemberships(bulkUpdate); // Assert expect(mockTeamService.bulkUpdateMemberships).toHaveBeenCalledWith(bulkUpdate); expect(result).toEqual(mockBulkResponse); }); }); describe('moveUsersBetweenTeams', () => { it('should move users between teams successfully', async () => { // Arrange const userIds = ['user-1', 'user-2']; const mockBulkResponse: BulkOperationResponse = { success: true, processedCount: 2, errorCount: 0, errors: [] }; mockTeamService.bulkUpdateMemberships.mockResolvedValue(mockBulkResponse); // Act const result = await teamManager.moveUsersBetweenTeams(userIds, 'from-team', 'to-team', 'manager'); // Assert expect(mockTeamService.bulkUpdateMemberships).toHaveBeenCalledTimes(2); // First call - remove from source team expect(mockTeamService.bulkUpdateMemberships).toHaveBeenNthCalledWith(1, { teamId: 'from-team', updates: [ { userId: 'user-1', action: 'remove' }, { userId: 'user-2', action: 'remove' } ] }); // Second call - add to destination team expect(mockTeamService.bulkUpdateMemberships).toHaveBeenNthCalledWith(2, { teamId: 'to-team', updates: [ { userId: 'user-1', role: 'manager', action: 'add' }, { userId: 'user-2', role: 'manager', action: 'add' } ] }); expect(result).toEqual(mockBulkResponse); }); it('should use default member role when newRole not provided', async () => { // Arrange const userIds = ['user-1']; const mockBulkResponse: BulkOperationResponse = { success: true, processedCount: 1, errorCount: 0, errors: [] }; mockTeamService.bulkUpdateMemberships.mockResolvedValue(mockBulkResponse); // Act await teamManager.moveUsersBetweenTeams(userIds, 'from-team', 'to-team'); // Assert expect(mockTeamService.bulkUpdateMemberships).toHaveBeenNthCalledWith(2, { teamId: 'to-team', updates: [ { userId: 'user-1', role: 'member', action: 'add' } ] }); }); }); describe('moveTeam', () => { it('should move team to new parent', async () => { // Arrange const validateSpy = jest.spyOn(teamManager as any, 'validateTeamHierarchy').mockResolvedValue(undefined); mockTeamService.updateTeam.mockResolvedValue(mockTeamResponse); // Act const result = await teamManager.moveTeam('team-123', 'new-parent'); // Assert expect(validateSpy).toHaveBeenCalledWith('team-123', 'new-parent'); expect(mockTeamService.updateTeam).toHaveBeenCalledWith('team-123', { parentTeamId: 'new-parent' }); expect(result).toEqual(mockTeamResponse); }); it('should make team a root team when newParentTeamId is undefined', async () => { // Arrange const validateSpy = jest.spyOn(teamManager as any, 'validateTeamHierarchy').mockResolvedValue(undefined); mockTeamService.updateTeam.mockResolvedValue(mockTeamResponse); // Act await teamManager.moveTeam('team-123'); // Assert expect(validateSpy).toHaveBeenCalledWith('team-123', undefined); expect(mockTeamService.updateTeam).toHaveBeenCalledWith('team-123', { parentTeamId: undefined }); }); }); describe('edge cases and error handling', () => { it('should handle empty user array in moveUsersBetweenTeams', async () => { // Arrange const mockBulkResponse: BulkOperationResponse = { success: true, processedCount: 0, errorCount: 0, errors: [] }; mockTeamService.bulkUpdateMemberships.mockResolvedValue(mockBulkResponse); // Act const result = await teamManager.moveUsersBetweenTeams([], 'from-team', 'to-team'); // Assert expect(mockTeamService.bulkUpdateMemberships).toHaveBeenCalledTimes(2); expect(result).toEqual(mockBulkResponse); }); it('should handle team with no members in deleteTeam with reassignment', async () => { // Arrange const teamWithoutMembers = { team: { ...mockTeamResponse.team, members: [], childTeams: [] } }; mockTeamService.getTeam.mockResolvedValue(teamWithoutMembers); mockTeamService.deleteTeam.mockResolvedValue(mockSuccessResponse); // Act const result = await teamManager.deleteTeam('team-123', { reassignMembersTo: 'new-team' }); // Assert expect(mockTeamService.addTeamMember).not.toHaveBeenCalled(); expect(result).toEqual(mockSuccessResponse); }); it('should handle team with no child teams in deleteTeam', async () => { // Arrange const teamWithoutChildren = { team: { ...mockTeamResponse.team, childTeams: [] }, memberCount: 1, childTeamCount: 0 }; mockTeamService.getTeam.mockResolvedValue(teamWithoutChildren); mockTeamService.deleteTeam.mockResolvedValue(mockSuccessResponse); // Create spy BEFORE the method call const deleteSpy = jest.spyOn(teamManager, 'deleteTeam'); // Act await teamManager.deleteTeam('team-123', { deleteChildTeams: true }); // Assert // Should only be called once (for the main team, not recursively) expect(deleteSpy).toHaveBeenCalledTimes(1); expect(deleteSpy).toHaveBeenCalledWith('team-123', { deleteChildTeams: true }); }); }); describe('method chaining', () => { it('should chain operations correctly', async () => { // Test that methods can be chained and work together mockWorkspaceTool.getWorkspaceDetails.mockResolvedValue({} as any); mockTeamService.createTeam.mockResolvedValue(mockTeamResponse); mockTeamService.getTeam.mockResolvedValue(mockTeamResponse); mockTeamService.addTeamMember.mockResolvedValue(mockTeamMember); // Create team, then add member const team = await teamManager.createTeam({ workspaceId: 'workspace-123', name: 'Test Team' }); const member = await teamManager.addTeamMember(team.team.id, 'user-123', 'member'); expect(team).toEqual(mockTeamResponse); expect(member).toEqual(mockTeamMember); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server