Skip to main content
Glama

Basecamp MCP Server

by jhliberty
basecamp-client.test.ts34.7 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import axios from 'axios'; import { BasecampClient } from '../lib/basecamp-client.js'; import type { BasecampProject, TodoList, Todo, Card, CardTable, Column, CardStep, Comment, CampfireLine, Document, Upload, Webhook, DailyCheckIn, QuestionAnswer } from '../types/basecamp.js'; // Mock axios vi.mock('axios'); const mockedAxios = vi.mocked(axios); describe('BasecampClient', () => { let client: BasecampClient; let mockAxiosInstance: any; const mockProject: BasecampProject = { id: '123', name: 'Test Project', description: 'A test project', dock: [ { id: '456', name: 'todoset', enabled: true, position: 1, url: 'test-url', app_url: 'test-app-url' }, { id: '789', name: 'card_table', enabled: true, position: 2, url: 'test-url', app_url: 'test-app-url' }, { id: '101', name: 'questionnaire', enabled: true, position: 3, url: 'test-url', app_url: 'test-app-url' } ], status: 'active', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; const mockTodoList: TodoList = { id: '456', title: 'Test Todo List', description: 'A test todo list', todos: [], status: 'active', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; const mockTodo: Todo = { id: '789', title: 'Test Todo', description: 'A test todo', completed: false, due_on: '2023-12-31', assignees: [], completion: null, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; const mockCard: Card = { id: '101', title: 'Test Card', content: 'A test card', due_on: '2023-12-31', completed: false, assignees: [], steps: [], created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; beforeEach(() => { vi.clearAllMocks(); mockAxiosInstance = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), defaults: { auth: {}, headers: { common: {} } } }; mockedAxios.create.mockReturnValue(mockAxiosInstance); }); afterEach(() => { vi.resetAllMocks(); }); describe('Constructor and Authentication', () => { it('should create client with basic auth', () => { client = new BasecampClient({ username: 'testuser', password: 'testpass', accountId: '12345', userAgent: 'Test Agent', authMode: 'basic' }); expect(mockedAxios.create).toHaveBeenCalledWith({ baseURL: 'https://3.basecampapi.com/12345', headers: { 'User-Agent': 'Test Agent', 'Content-Type': 'application/json', }, timeout: 30000, }); expect(mockAxiosInstance.defaults.auth).toEqual({ username: 'testuser', password: 'testpass' }); }); it('should create client with OAuth', () => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); expect(mockAxiosInstance.defaults.headers.common['Authorization']).toBe('Bearer test-token'); }); it('should default to basic auth mode', () => { client = new BasecampClient({ username: 'testuser', password: 'testpass', accountId: '12345', userAgent: 'Test Agent' }); expect(mockAxiosInstance.defaults.auth).toEqual({ username: 'testuser', password: 'testpass' }); }); it('should throw error for basic auth without credentials', () => { expect(() => new BasecampClient({ accountId: '12345', userAgent: 'Test Agent', authMode: 'basic' })).toThrow('Username and password required for basic auth'); }); it('should throw error for OAuth without access token', () => { expect(() => new BasecampClient({ accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' })).toThrow('Access token required for OAuth auth'); }); }); describe('Connection Testing', () => { beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should return success for valid connection', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockProject] }); const result = await client.testConnection(); expect(result).toEqual({ success: true, message: 'Connection successful' }); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/projects.json'); }); it('should return failure for invalid connection', async () => { const error = new Error('Network error'); mockAxiosInstance.get.mockRejectedValue(error); const result = await client.testConnection(); expect(result).toEqual({ success: false, message: 'Connection failed: Network error' }); }); it('should handle unknown errors in connection test', async () => { mockAxiosInstance.get.mockRejectedValue('Unknown error'); const result = await client.testConnection(); expect(result).toEqual({ success: false, message: 'Connection failed: Unknown error' }); }); }); describe('Project Methods', () => { beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get all projects', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockProject] }); const projects = await client.getProjects(); expect(projects).toEqual([mockProject]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/projects.json'); }); it('should get specific project', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockProject }); const project = await client.getProject('123'); expect(project).toEqual(mockProject); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/projects/123.json'); }); }); describe('Todo Methods', () => { beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get todo lists', async () => { mockAxiosInstance.get .mockResolvedValueOnce({ data: mockProject }) // getProject call .mockResolvedValueOnce({ data: [mockTodoList] }); // getTodoLists call const todoLists = await client.getTodoLists('123'); expect(todoLists).toEqual([mockTodoList]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/projects/123.json'); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/todosets/456/todolists.json'); }); it('should throw error when no todoset found', async () => { const projectWithoutTodoset = { ...mockProject, dock: [] }; mockAxiosInstance.get.mockResolvedValue({ data: projectWithoutTodoset }); await expect(client.getTodoLists('123')).rejects.toThrow('No todoset found for project 123'); }); it('should get todos from todolist', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockTodo] }); const todos = await client.getTodos('123', '456'); expect(todos).toEqual([mockTodo]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/todolists/456/todos.json'); }); it('should get specific todo', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockTodo }); const todo = await client.getTodo('789'); expect(todo).toEqual(mockTodo); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/todos/789.json'); }); }); describe('Card Table Methods', () => { beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get card tables', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockProject }); const cardTables = await client.getCardTables('123'); expect(cardTables).toEqual([mockProject.dock[1]]); // card_table entry expect(mockAxiosInstance.get).toHaveBeenCalledWith('/projects/123.json'); }); it('should get primary card table', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockProject }); const cardTable = await client.getCardTable('123'); expect(cardTable).toEqual(mockProject.dock[1]); }); it('should throw error when no card tables found', async () => { const projectWithoutCardTable = { ...mockProject, dock: [mockProject.dock[0]] // Only todoset, no card_table }; mockAxiosInstance.get.mockResolvedValue({ data: projectWithoutCardTable }); await expect(client.getCardTable('123')).rejects.toThrow('No card tables found for project: 123'); }); it('should get card table details', async () => { const mockCardTable: CardTable = { id: '789', title: 'Test Card Table', lists: [], status: 'active' }; mockAxiosInstance.get.mockResolvedValue({ data: mockCardTable }); const cardTableDetails = await client.getCardTableDetails('123', '789'); expect(cardTableDetails).toEqual(mockCardTable); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/card_tables/789.json'); }); it('should handle empty card table (204 response)', async () => { const error = { response: { status: 204 } }; mockAxiosInstance.get.mockRejectedValue(error); const cardTableDetails = await client.getCardTableDetails('123', '789'); expect(cardTableDetails).toEqual({ id: '789', title: 'Card Table', lists: [], status: 'empty' }); }); it('should rethrow non-204 errors', async () => { const error = { response: { status: 500 } }; mockAxiosInstance.get.mockRejectedValue(error); await expect(client.getCardTableDetails('123', '789')).rejects.toEqual(error); }); }); describe('Column Methods', () => { const mockColumn: Column = { id: '111', title: 'Test Column', position: 1, color: 'blue', cards: [] }; beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get columns', async () => { const mockCardTable: CardTable = { id: '789', title: 'Test Card Table', lists: [mockColumn], status: 'active' }; mockAxiosInstance.get.mockResolvedValue({ data: mockCardTable }); const columns = await client.getColumns('123', '789'); expect(columns).toEqual([mockColumn]); }); it('should get specific column', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockColumn }); const column = await client.getColumn('123', '111'); expect(column).toEqual(mockColumn); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/card_tables/columns/111.json'); }); it('should create column', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockColumn }); const column = await client.createColumn('123', '789', 'New Column'); expect(column).toEqual(mockColumn); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/789/columns.json', { title: 'New Column' }); }); it('should update column', async () => { mockAxiosInstance.put.mockResolvedValue({ data: mockColumn }); const column = await client.updateColumn('123', '111', 'Updated Column'); expect(column).toEqual(mockColumn); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/buckets/123/card_tables/columns/111.json', { title: 'Updated Column' }); }); it('should move column', async () => { mockAxiosInstance.post.mockResolvedValue({ data: {} }); await client.moveColumn('123', '111', 2, '789'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/789/moves.json', { source_id: '111', target_id: '789', position: 2 }); }); it('should update column color', async () => { mockAxiosInstance.patch.mockResolvedValue({ data: mockColumn }); const column = await client.updateColumnColor('123', '111', 'red'); expect(column).toEqual(mockColumn); expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/buckets/123/card_tables/columns/111/color.json', { color: 'red' }); }); it('should put column on hold', async () => { mockAxiosInstance.post.mockResolvedValue({ data: {} }); await client.putColumnOnHold('123', '111'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/columns/111/on_hold.json'); }); it('should remove column hold', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); await client.removeColumnHold('123', '111'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/buckets/123/card_tables/columns/111/on_hold.json'); }); it('should watch column', async () => { mockAxiosInstance.post.mockResolvedValue({ data: {} }); await client.watchColumn('123', '111'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/lists/111/subscription.json'); }); it('should unwatch column', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); await client.unwatchColumn('123', '111'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/buckets/123/card_tables/lists/111/subscription.json'); }); }); describe('Card Methods', () => { beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get cards', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockCard] }); const cards = await client.getCards('123', '111'); expect(cards).toEqual([mockCard]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/card_tables/lists/111/cards.json'); }); it('should get specific card', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockCard }); const card = await client.getCard('123', '101'); expect(card).toEqual(mockCard); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/card_tables/cards/101.json'); }); it('should create card with minimal data', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockCard }); const card = await client.createCard('123', '111', 'New Card'); expect(card).toEqual(mockCard); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/lists/111/cards.json', { title: 'New Card' }); }); it('should create card with full data', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockCard }); const card = await client.createCard('123', '111', 'New Card', 'Card content', '2023-12-31', true); expect(card).toEqual(mockCard); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/lists/111/cards.json', { title: 'New Card', content: 'Card content', due_on: '2023-12-31', notify: true }); }); it('should update card with partial data', async () => { mockAxiosInstance.put.mockResolvedValue({ data: mockCard }); const card = await client.updateCard('123', '101', 'Updated Card'); expect(card).toEqual(mockCard); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/buckets/123/card_tables/cards/101.json', { title: 'Updated Card' }); }); it('should update card with full data', async () => { mockAxiosInstance.put.mockResolvedValue({ data: mockCard }); const card = await client.updateCard('123', '101', 'Updated Card', 'Updated content', '2023-12-31', ['user1']); expect(card).toEqual(mockCard); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/buckets/123/card_tables/cards/101.json', { title: 'Updated Card', content: 'Updated content', due_on: '2023-12-31', assignee_ids: ['user1'] }); }); it('should move card', async () => { mockAxiosInstance.post.mockResolvedValue({ data: {} }); await client.moveCard('123', '101', '222'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/cards/101/moves.json', { column_id: '222' }); }); it('should complete card', async () => { mockAxiosInstance.post.mockResolvedValue({ data: { completed: true } }); const result = await client.completeCard('123', '101'); expect(result).toEqual({ completed: true }); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/todos/101/completion.json'); }); it('should uncomplete card', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); await client.uncompleteCard('123', '101'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/buckets/123/todos/101/completion.json'); }); }); describe('Card Step Methods', () => { const mockCardStep: CardStep = { id: '333', title: 'Test Step', completed: false, assignees: [], due_on: '2023-12-31', created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get card steps', async () => { const cardWithSteps = { ...mockCard, steps: [mockCardStep] }; mockAxiosInstance.get.mockResolvedValue({ data: cardWithSteps }); const steps = await client.getCardSteps('123', '101'); expect(steps).toEqual([mockCardStep]); }); it('should return empty array for card without steps', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockCard }); const steps = await client.getCardSteps('123', '101'); expect(steps).toEqual([]); }); it('should create card step with minimal data', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockCardStep }); const step = await client.createCardStep('123', '101', 'New Step'); expect(step).toEqual(mockCardStep); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/cards/101/steps.json', { title: 'New Step' }); }); it('should create card step with full data', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockCardStep }); const step = await client.createCardStep('123', '101', 'New Step', '2023-12-31', ['user1']); expect(step).toEqual(mockCardStep); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/card_tables/cards/101/steps.json', { title: 'New Step', due_on: '2023-12-31', assignee_ids: ['user1'] }); }); it('should get specific card step', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockCardStep }); const step = await client.getCardStep('123', '333'); expect(step).toEqual(mockCardStep); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/card_tables/steps/333.json'); }); it('should update card step', async () => { mockAxiosInstance.put.mockResolvedValue({ data: mockCardStep }); const step = await client.updateCardStep('123', '333', 'Updated Step', '2023-12-31', ['user1']); expect(step).toEqual(mockCardStep); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/buckets/123/card_tables/steps/333.json', { title: 'Updated Step', due_on: '2023-12-31', assignee_ids: ['user1'] }); }); it('should delete card step', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); await client.deleteCardStep('123', '333'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/buckets/123/card_tables/steps/333.json'); }); it('should complete card step', async () => { mockAxiosInstance.post.mockResolvedValue({ data: { completed: true } }); const result = await client.completeCardStep('123', '333'); expect(result).toEqual({ completed: true }); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/todos/333/completion.json'); }); it('should uncomplete card step', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); await client.uncompleteCardStep('123', '333'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/buckets/123/todos/333/completion.json'); }); }); describe('Communication Methods', () => { const mockCampfireLine: CampfireLine = { id: '444', content: 'Test message', creator: { id: 'user1', name: 'Test User' }, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z' }; const mockComment: Comment = { id: '555', content: 'Test comment', creator: { id: 'user1', name: 'Test User' }, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z' }; beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get campfire lines', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockCampfireLine] }); const lines = await client.getCampfireLines('123', '444'); expect(lines).toEqual([mockCampfireLine]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/chats/444/lines.json'); }); it('should get comments', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockComment] }); const comments = await client.getComments('123', '555'); expect(comments).toEqual([mockComment]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/recordings/555/comments.json'); }); }); describe('Document Methods', () => { const mockDocument: Document = { id: '666', title: 'Test Document', content: 'Test content', creator: { id: 'user1', name: 'Test User' }, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get documents', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockDocument] }); const documents = await client.getDocuments('123', '777'); expect(documents).toEqual([mockDocument]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/vaults/777/documents.json'); }); it('should get specific document', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockDocument }); const document = await client.getDocument('123', '666'); expect(document).toEqual(mockDocument); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/documents/666.json'); }); it('should create document with default status', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockDocument }); const document = await client.createDocument('123', '777', 'New Document', 'Document content'); expect(document).toEqual(mockDocument); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/vaults/777/documents.json', { title: 'New Document', content: 'Document content', status: 'active' }); }); it('should create document with custom status', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockDocument }); const document = await client.createDocument('123', '777', 'New Document', 'Document content', 'draft'); expect(document).toEqual(mockDocument); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/vaults/777/documents.json', { title: 'New Document', content: 'Document content', status: 'draft' }); }); it('should update document', async () => { mockAxiosInstance.put.mockResolvedValue({ data: mockDocument }); const document = await client.updateDocument('123', '666', 'Updated Title', 'Updated content'); expect(document).toEqual(mockDocument); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/buckets/123/documents/666.json', { title: 'Updated Title', content: 'Updated content' }); }); it('should update document with partial data', async () => { mockAxiosInstance.put.mockResolvedValue({ data: mockDocument }); const document = await client.updateDocument('123', '666', 'Updated Title'); expect(document).toEqual(mockDocument); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/buckets/123/documents/666.json', { title: 'Updated Title' }); }); it('should trash document', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); await client.trashDocument('123', '666'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/buckets/123/documents/666.json'); }); }); describe('File Methods', () => { const mockUpload: Upload = { id: '888', filename: 'test.txt', byte_size: 1024, content_type: 'text/plain', creator: { id: 'user1', name: 'Test User' }, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get uploads from project', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockUpload] }); const uploads = await client.getUploads('123'); expect(uploads).toEqual([mockUpload]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/uploads.json'); }); it('should get uploads from vault', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockUpload] }); const uploads = await client.getUploads('123', '777'); expect(uploads).toEqual([mockUpload]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/vaults/777/uploads.json'); }); it('should get specific upload', async () => { mockAxiosInstance.get.mockResolvedValue({ data: mockUpload }); const upload = await client.getUpload('123', '888'); expect(upload).toEqual(mockUpload); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/uploads/888.json'); }); it('should throw error for createAttachment (not implemented)', async () => { await expect(client.createAttachment('test.txt', 'Test File')).rejects.toThrow( 'File upload not implemented in TypeScript version yet' ); }); }); describe('Webhook Methods', () => { const mockWebhook: Webhook = { id: '999', payload_url: 'https://example.com/webhook', types: ['todo.created', 'card.created'], creator: { id: 'user1', name: 'Test User' }, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', url: 'test-url', app_url: 'test-app-url' }; beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get webhooks', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockWebhook] }); const webhooks = await client.getWebhooks('123'); expect(webhooks).toEqual([mockWebhook]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/webhooks.json'); }); it('should create webhook without types', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockWebhook }); const webhook = await client.createWebhook('123', 'https://example.com/webhook'); expect(webhook).toEqual(mockWebhook); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/webhooks.json', { payload_url: 'https://example.com/webhook' }); }); it('should create webhook with types', async () => { mockAxiosInstance.post.mockResolvedValue({ data: mockWebhook }); const webhook = await client.createWebhook('123', 'https://example.com/webhook', ['todo.created']); expect(webhook).toEqual(mockWebhook); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/buckets/123/webhooks.json', { payload_url: 'https://example.com/webhook', types: ['todo.created'] }); }); it('should delete webhook', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); await client.deleteWebhook('123', '999'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/buckets/123/webhooks/999.json'); }); }); describe('Check-in Methods', () => { const mockDailyCheckIn: DailyCheckIn = { id: '1010', title: 'Daily Check-in', paused: false, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z' }; const mockQuestionAnswer: QuestionAnswer = { id: '1111', content: 'Test answer', creator: { id: 'user1', name: 'Test User' }, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z' }; beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get daily check-ins', async () => { mockAxiosInstance.get .mockResolvedValueOnce({ data: mockProject }) // getProject call .mockResolvedValueOnce({ data: [mockDailyCheckIn] }); // getDailyCheckIns call const checkIns = await client.getDailyCheckIns('123'); expect(checkIns).toEqual([mockDailyCheckIn]); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/projects/123.json'); expect(mockAxiosInstance.get).toHaveBeenCalledWith( '/buckets/123/questionnaires/101/questions.json', { params: { page: 1 } } ); }); it('should get daily check-ins with custom page', async () => { mockAxiosInstance.get .mockResolvedValueOnce({ data: mockProject }) .mockResolvedValueOnce({ data: [mockDailyCheckIn] }); const checkIns = await client.getDailyCheckIns('123', 2); expect(mockAxiosInstance.get).toHaveBeenCalledWith( '/buckets/123/questionnaires/101/questions.json', { params: { page: 2 } } ); }); it('should throw error when no questionnaire found', async () => { const projectWithoutQuestionnaire = { ...mockProject, dock: [mockProject.dock[0]] }; mockAxiosInstance.get.mockResolvedValue({ data: projectWithoutQuestionnaire }); await expect(client.getDailyCheckIns('123')).rejects.toThrow('No questionnaire found for project 123'); }); it('should get question answers', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockQuestionAnswer] }); const answers = await client.getQuestionAnswers('123', '1010'); expect(answers).toEqual([mockQuestionAnswer]); expect(mockAxiosInstance.get).toHaveBeenCalledWith( '/buckets/123/questions/1010/answers.json', { params: { page: 1 } } ); }); it('should get question answers with custom page', async () => { mockAxiosInstance.get.mockResolvedValue({ data: [mockQuestionAnswer] }); const answers = await client.getQuestionAnswers('123', '1010', 3); expect(mockAxiosInstance.get).toHaveBeenCalledWith( '/buckets/123/questions/1010/answers.json', { params: { page: 3 } } ); }); }); describe('Utility Methods', () => { beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should get events', async () => { const mockEvents = [{ id: '1212', type: 'created' }]; mockAxiosInstance.get.mockResolvedValue({ data: mockEvents }); const events = await client.getEvents('123', '555'); expect(events).toEqual(mockEvents); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/buckets/123/recordings/555/events.json'); }); }); describe('Error Handling', () => { beforeEach(() => { client = new BasecampClient({ accessToken: 'test-token', accountId: '12345', userAgent: 'Test Agent', authMode: 'oauth' }); }); it('should propagate axios errors', async () => { const axiosError = new Error('Network error'); mockAxiosInstance.get.mockRejectedValue(axiosError); await expect(client.getProjects()).rejects.toThrow('Network error'); }); it('should handle HTTP error responses', async () => { const httpError = { response: { status: 404, data: { error: 'Not found' } } }; mockAxiosInstance.get.mockRejectedValue(httpError); await expect(client.getProject('999')).rejects.toEqual(httpError); }); }); });

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/jhliberty/basecamp-mcp-server'

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