mcp-server.test.ts•8.42 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
mockBasecampProject,
mockBasecampTodo,
mockBasecampCard,
mockBasecampCardTable,
mockOAuthToken,
mockEnvironment,
mockMCPRequest,
mockMCPToolCall
} from './utils.js';
import { createServer } from '../index.js';
import { BasecampClient } from '../lib/basecamp-client.js';
import { tokenStorage } from '../lib/token-storage.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
// Mock the dependencies but use real server logic
vi.mock('../lib/basecamp-client.js');
vi.mock('../lib/token-storage.js');
vi.mock('axios');
const MockedBasecampClient = vi.mocked(BasecampClient);
const mockTokenStorage = vi.mocked(tokenStorage);
// Mock Basecamp client instance
const mockBasecampClientInstance = {
getProjects: vi.fn(),
getProject: vi.fn(),
getTodos: vi.fn(),
getTodoLists: vi.fn(),
createTodo: vi.fn(),
completeTodo: vi.fn(),
uncompleteTodo: vi.fn(),
search: vi.fn(),
getCardTable: vi.fn(),
getCardTableDetails: vi.fn(),
getCards: vi.fn(),
createCard: vi.fn(),
updateCard: vi.fn(),
moveCard: vi.fn(),
completeCard: vi.fn(),
getComments: vi.fn(),
getCampfireLines: vi.fn(),
getDailyCheckIns: vi.fn(),
getQuestionAnswers: vi.fn(),
createAttachment: vi.fn(),
getEvents: vi.fn(),
getWebhooks: vi.fn(),
createWebhook: vi.fn(),
deleteWebhook: vi.fn(),
getDocuments: vi.fn(),
getDocument: vi.fn(),
createDocument: vi.fn(),
updateDocument: vi.fn(),
trashDocument: vi.fn(),
testConnection: vi.fn()
};
MockedBasecampClient.mockImplementation(() => mockBasecampClientInstance as any);
describe('MCP Server Core Functionality', () => {
let server: any;
beforeEach(() => {
vi.clearAllMocks();
mockEnvironment();
// Setup default token storage behavior
mockTokenStorage.getToken.mockResolvedValue(mockOAuthToken());
mockTokenStorage.isTokenExpired.mockResolvedValue(false);
// Setup mock client methods
mockBasecampClientInstance.getProjects.mockResolvedValue([mockBasecampProject()]);
mockBasecampClientInstance.getProject.mockResolvedValue(mockBasecampProject());
mockBasecampClientInstance.getCardTable.mockResolvedValue({ id: '1', name: 'card_table' });
mockBasecampClientInstance.getCardTableDetails.mockResolvedValue(mockBasecampCardTable());
mockBasecampClientInstance.createCard.mockResolvedValue(mockBasecampCard());
server = createServer();
});
afterEach(() => {
vi.resetAllMocks();
});
describe('Server Initialization', () => {
it('should create server instance', () => {
expect(server).toBeDefined();
});
it('should be an instance with server properties', () => {
expect(server).toHaveProperty('run');
});
});
describe('Tools Registration', () => {
it('should have tools available through server', async () => {
// This test validates the server exists and can be created
expect(server).toBeDefined();
});
});
describe('Authentication Integration', () => {
it('should handle token storage setup', async () => {
const token = mockOAuthToken();
mockTokenStorage.getToken.mockResolvedValue(token);
mockTokenStorage.isTokenExpired.mockResolvedValue(false);
// Test that mocks are set up correctly
const retrievedToken = await mockTokenStorage.getToken();
const isExpired = await mockTokenStorage.isTokenExpired();
expect(retrievedToken).toEqual(token);
expect(isExpired).toBe(false);
});
it('should handle missing token', async () => {
mockTokenStorage.getToken.mockResolvedValue(null);
const retrievedToken = await mockTokenStorage.getToken();
expect(retrievedToken).toBeNull();
});
it('should handle expired token', async () => {
const expiredToken = mockOAuthToken();
mockTokenStorage.getToken.mockResolvedValue(expiredToken);
mockTokenStorage.isTokenExpired.mockResolvedValue(true);
const isExpired = await mockTokenStorage.isTokenExpired();
expect(isExpired).toBe(true);
});
});
describe('Client Integration', () => {
it('should mock BasecampClient correctly', () => {
// The client is only instantiated when actually needed (lazy loading)
// So we just verify the mock setup exists
expect(MockedBasecampClient).toBeDefined();
});
it('should setup client method mocks', async () => {
const projects = await mockBasecampClientInstance.getProjects();
expect(projects).toEqual([mockBasecampProject()]);
expect(mockBasecampClientInstance.getProjects).toHaveBeenCalled();
});
it('should handle client method calls', async () => {
const project = await mockBasecampClientInstance.getProject('123');
expect(project).toEqual(mockBasecampProject());
expect(mockBasecampClientInstance.getProject).toHaveBeenCalledWith('123');
});
it('should handle card creation', async () => {
const card = await mockBasecampClientInstance.createCard('123', '456', 'Test Card');
expect(card).toEqual(mockBasecampCard());
expect(mockBasecampClientInstance.createCard).toHaveBeenCalledWith('123', '456', 'Test Card');
});
});
describe('Error Handling', () => {
it('should handle client errors', async () => {
mockBasecampClientInstance.getProjects.mockRejectedValue(new Error('API Error'));
await expect(mockBasecampClientInstance.getProjects()).rejects.toThrow('API Error');
});
it('should handle network errors', async () => {
const networkError = new Error('Network error: Unable to reach Basecamp API');
mockBasecampClientInstance.getProjects.mockRejectedValue(networkError);
await expect(mockBasecampClientInstance.getProjects()).rejects.toThrow('Network error');
});
it('should handle rate limiting', async () => {
const rateLimitError = new Error('Rate limit exceeded');
mockBasecampClientInstance.getProjects.mockRejectedValue(rateLimitError);
await expect(mockBasecampClientInstance.getProjects()).rejects.toThrow('Rate limit exceeded');
});
});
describe('Mock Data Validation', () => {
it('should generate valid mock project data', () => {
const project = mockBasecampProject();
expect(project).toHaveProperty('id');
expect(project).toHaveProperty('name');
expect(project).toHaveProperty('dock');
});
it('should generate valid mock card data', () => {
const card = mockBasecampCard();
expect(card).toHaveProperty('id');
expect(card).toHaveProperty('title');
});
it('should generate valid mock token data', () => {
const token = mockOAuthToken();
expect(token).toHaveProperty('access_token');
expect(token).toHaveProperty('account_id');
});
});
});
// Additional simplified tests for tool schemas and validation
describe('Tool Schema Validation', () => {
const expectedCoreTools = [
'get_projects',
'get_project',
'get_todolists',
'get_todos',
'get_card_table',
'get_cards',
'create_card',
'search_basecamp',
'global_search'
];
it('should define expected tool names', () => {
expectedCoreTools.forEach(toolName => {
expect(typeof toolName).toBe('string');
expect(toolName.length).toBeGreaterThan(0);
});
});
it('should have required tools for card operations', () => {
const cardTools = expectedCoreTools.filter(name => name.includes('card'));
expect(cardTools.length).toBeGreaterThan(0);
expect(cardTools).toContain('get_card_table');
expect(cardTools).toContain('get_cards');
expect(cardTools).toContain('create_card');
});
it('should have required tools for project operations', () => {
const projectTools = expectedCoreTools.filter(name => name.includes('project'));
expect(projectTools.length).toBeGreaterThan(0);
expect(projectTools).toContain('get_projects');
expect(projectTools).toContain('get_project');
});
it('should have search tools', () => {
const searchTools = expectedCoreTools.filter(name => name.includes('search'));
expect(searchTools.length).toBeGreaterThan(0);
expect(searchTools).toContain('search_basecamp');
expect(searchTools).toContain('global_search');
});
});