Jake Shore 0d9d410564 fix: make API connection test non-fatal for host compatibility
Server now starts and responds to initialize/tools/list even without
valid API credentials. Tools will return auth errors when called.
This is required for Claude Desktop and other MCP hosts that need
to enumerate tools before the user provides credentials.

Host compat testing: stdio transport verified, 474 tools listed.
2026-02-09 14:03:49 -05:00

313 lines
9.8 KiB
TypeScript

/**
* Unit Tests for Contact Tools
* Tests all 31 contact management MCP tools
*/
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { ContactTools } from '../../src/tools/contact-tools.js';
import { MockGHLApiClient, mockContact } from '../mocks/ghl-api-client.mock.js';
describe('ContactTools', () => {
let contactTools: ContactTools;
let mockGhlClient: MockGHLApiClient;
beforeEach(() => {
mockGhlClient = new MockGHLApiClient();
contactTools = new ContactTools(mockGhlClient as any);
});
describe('getToolDefinitions', () => {
it('should return 31 contact tool definitions', () => {
const tools = contactTools.getToolDefinitions();
expect(tools).toHaveLength(31);
const toolNames = tools.map(tool => tool.name);
expect(toolNames).toEqual([
'create_contact',
'search_contacts',
'get_contact',
'update_contact',
'delete_contact',
'add_contact_tags',
'remove_contact_tags',
'get_contact_tasks',
'create_contact_task',
'get_contact_task',
'update_contact_task',
'delete_contact_task',
'update_task_completion',
'get_contact_notes',
'create_contact_note',
'get_contact_note',
'update_contact_note',
'delete_contact_note',
'upsert_contact',
'get_duplicate_contact',
'get_contacts_by_business',
'get_contact_appointments',
'bulk_update_contact_tags',
'bulk_update_contact_business',
'add_contact_followers',
'remove_contact_followers',
'add_contact_to_campaign',
'remove_contact_from_campaign',
'remove_contact_from_all_campaigns',
'add_contact_to_workflow',
'remove_contact_from_workflow'
]);
});
it('should have proper schema definitions for all tools', () => {
const tools = contactTools.getToolDefinitions();
tools.forEach(tool => {
expect(tool.name).toBeDefined();
expect(tool.description).toBeDefined();
expect(tool.inputSchema).toBeDefined();
expect(tool.inputSchema.type).toBe('object');
expect(tool.inputSchema.properties).toBeDefined();
});
});
});
describe('executeTool', () => {
it('should route tool calls correctly', async () => {
const createSpy = jest.spyOn(contactTools as any, 'createContact');
const getSpy = jest.spyOn(contactTools as any, 'getContact');
await contactTools.executeTool('create_contact', { email: 'test@example.com' });
await contactTools.executeTool('get_contact', { contactId: 'contact_123' });
expect(createSpy).toHaveBeenCalledWith({ email: 'test@example.com' });
expect(getSpy).toHaveBeenCalledWith('contact_123');
});
it('should throw error for unknown tool', async () => {
await expect(
contactTools.executeTool('unknown_tool', {})
).rejects.toThrow('Unknown tool: unknown_tool');
});
});
describe('create_contact', () => {
it('should create contact successfully', async () => {
const contactData = {
firstName: 'Jane',
lastName: 'Doe',
email: 'jane.doe@example.com',
phone: '+1-555-987-6543'
};
const result = await contactTools.executeTool('create_contact', contactData);
expect(result).toBeDefined();
expect(result.email).toBe(contactData.email);
expect(result.firstName).toBe(contactData.firstName);
});
it('should handle API errors', async () => {
const mockError = new Error('GHL API Error (400): Invalid email');
jest.spyOn(mockGhlClient, 'createContact').mockRejectedValueOnce(mockError);
await expect(
contactTools.executeTool('create_contact', { email: 'invalid-email' })
).rejects.toThrow('GHL API Error (400): Invalid email');
});
it('should set default source if not provided', async () => {
const spy = jest.spyOn(mockGhlClient, 'createContact');
await contactTools.executeTool('create_contact', {
firstName: 'John',
email: 'john@example.com'
});
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
email: 'john@example.com'
})
);
});
});
describe('search_contacts', () => {
it('should search contacts successfully', async () => {
const searchParams = {
query: 'John Doe',
limit: 10
};
const result = await contactTools.executeTool('search_contacts', searchParams);
expect(result).toBeDefined();
expect(result.contacts).toBeDefined();
expect(Array.isArray(result.contacts)).toBe(true);
expect(result.total).toBeDefined();
});
it('should use default limit if not provided', async () => {
const spy = jest.spyOn(mockGhlClient, 'searchContacts');
await contactTools.executeTool('search_contacts', { query: 'test' });
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
query: 'test'
})
);
});
it('should handle search with email filter', async () => {
const result = await contactTools.executeTool('search_contacts', {
email: 'john@example.com'
});
expect(result).toBeDefined();
expect(result.contacts).toBeDefined();
});
});
describe('get_contact', () => {
it('should get contact by ID successfully', async () => {
const result = await contactTools.executeTool('get_contact', {
contactId: 'contact_123'
});
expect(result).toBeDefined();
expect(result.id).toBe('contact_123');
});
it('should handle contact not found', async () => {
await expect(
contactTools.executeTool('get_contact', { contactId: 'not_found' })
).rejects.toThrow('GHL API Error (404): Contact not found');
});
});
describe('update_contact', () => {
it('should update contact successfully', async () => {
const updateData = {
contactId: 'contact_123',
firstName: 'Updated',
lastName: 'Name'
};
const result = await contactTools.executeTool('update_contact', updateData);
expect(result).toBeDefined();
expect(result.firstName).toBe('Updated');
});
it('should handle partial updates', async () => {
const spy = jest.spyOn(mockGhlClient, 'updateContact');
await contactTools.executeTool('update_contact', {
contactId: 'contact_123',
email: 'newemail@example.com'
});
expect(spy).toHaveBeenCalledWith('contact_123', {
firstName: undefined,
lastName: undefined,
email: 'newemail@example.com',
phone: undefined,
tags: undefined
});
});
});
describe('add_contact_tags', () => {
it('should add tags successfully', async () => {
const result = await contactTools.executeTool('add_contact_tags', {
contactId: 'contact_123',
tags: ['vip', 'premium']
});
expect(result).toBeDefined();
expect(result.tags).toBeDefined();
expect(Array.isArray(result.tags)).toBe(true);
});
it('should validate required parameters', async () => {
await expect(
contactTools.executeTool('add_contact_tags', { contactId: 'contact_123' })
).rejects.toThrow();
});
});
describe('remove_contact_tags', () => {
it('should remove tags successfully', async () => {
const result = await contactTools.executeTool('remove_contact_tags', {
contactId: 'contact_123',
tags: ['old-tag']
});
expect(result).toBeDefined();
expect(result.tags).toBeDefined();
});
it('should handle empty tags array', async () => {
const spy = jest.spyOn(mockGhlClient, 'removeContactTags');
await contactTools.executeTool('remove_contact_tags', {
contactId: 'contact_123',
tags: []
});
expect(spy).toHaveBeenCalledWith('contact_123', []);
});
});
describe('delete_contact', () => {
it('should delete contact successfully', async () => {
const result = await contactTools.executeTool('delete_contact', {
contactId: 'contact_123'
});
expect(result).toBeDefined();
expect(result.succeded).toBe(true);
});
it('should handle deletion errors', async () => {
const mockError = new Error('GHL API Error (404): Contact not found');
jest.spyOn(mockGhlClient, 'deleteContact').mockRejectedValueOnce(mockError);
await expect(
contactTools.executeTool('delete_contact', { contactId: 'not_found' })
).rejects.toThrow('GHL API Error (404): Contact not found');
});
});
describe('error handling', () => {
it('should propagate API client errors', async () => {
const mockError = new Error('Network error');
jest.spyOn(mockGhlClient, 'createContact').mockRejectedValueOnce(mockError);
await expect(
contactTools.executeTool('create_contact', { email: 'test@example.com' })
).rejects.toThrow('Network error');
});
it('should handle missing required fields', async () => {
// Test with missing email (required field)
const result = await contactTools.executeTool('create_contact', { firstName: 'John' });
expect(result).toBeDefined();
});
});
describe('input validation', () => {
it('should validate email format in schema', () => {
const tools = contactTools.getToolDefinitions();
const createContactTool = tools.find(tool => tool.name === 'create_contact');
expect(createContactTool?.inputSchema.properties.email.type).toBe('string');
});
it('should validate required fields in schema', () => {
const tools = contactTools.getToolDefinitions();
const createContactTool = tools.find(tool => tool.name === 'create_contact');
expect(createContactTool?.inputSchema.required).toEqual(['email']);
});
});
});