UNPKG

npmplus-mcp-server

Version:

Production-ready MCP server for intelligent JavaScript package management. Works with Claude, Windsurf, Cursor, VS Code, and any MCP-compatible AI editor.

635 lines (527 loc) 18.5 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; // Mock external dependencies jest.mock('undici', () => ({ fetch: jest.fn() })); jest.mock('child_process', () => ({ exec: jest.fn() })); import { fetch } from 'undici'; const mockFetch = fetch as jest.MockedFunction<typeof fetch>; // Import the Netlify function handlers let mcpHandler: any; let healthHandler: any; let analyticsHandler: any; describe('Netlify Function Endpoints JSON Format Tests', () => { beforeEach(async () => { jest.clearAllMocks(); // Dynamically import the handlers to avoid module resolution issues try { const mcpModule = await import('../../netlify/functions/npmplus-mcp.cjs'); mcpHandler = mcpModule.handler; } catch (error) { // If the file doesn't exist or can't be imported, we'll test the expected structure mcpHandler = null; } try { const healthModule = await import('../../netlify/functions/npmplus-health.cjs'); healthHandler = healthModule.handler; } catch (error) { healthHandler = null; } try { const analyticsModule = await import('../../netlify/functions/npmplus-analytics.cjs'); analyticsHandler = analyticsModule.handler; } catch (error) { analyticsHandler = null; } // Setup default mocks mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({ objects: [ { package: { name: 'test-package', version: '1.0.0', description: 'Test package', keywords: ['test'], author: 'Test Author', date: '2023-01-01T00:00:00.000Z' }, score: { final: 0.9 }, searchScore: 100 } ], total: 1 }) } as any); }); describe('MCP Function Endpoint', () => { const createMockEvent = (method: string, body: any) => ({ httpMethod: method, body: JSON.stringify(body), headers: { 'content-type': 'application/json' } }); const mockContext = {}; it('should handle CORS preflight requests with proper JSON response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = { httpMethod: 'OPTIONS', headers: {} }; const result = await mcpHandler(event, mockContext); expect(result).toMatchObject({ statusCode: 200, headers: expect.objectContaining({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, User-Agent', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Content-Type': 'application/json' }) }); }); it('should handle initialize request with valid JSON response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = createMockEvent('POST', { method: 'initialize', params: { protocolVersion: '1.0.0', clientInfo: { name: 'test-client', version: '1.0.0' }, capabilities: {} } }); const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toMatchObject({ jsonrpc: "2.0", result: { protocolVersion: '2024-11-05', serverInfo: { name: 'npmplus-mcp', version: expect.stringMatching(/^\d+\.\d+\.\d+$/) }, capabilities: { tools: {} } } }); // Also verify it matches the current package version const packageVersion = require('../../package.json').version; expect(responseBody.result.serverInfo.version).toBe(packageVersion); }); it('should handle tools/list request with valid JSON schema response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = createMockEvent('POST', { method: 'tools/list', params: {} }); const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toHaveProperty('tools'); expect(Array.isArray(responseBody.tools)).toBe(true); // Validate each tool has proper structure responseBody.tools.forEach((tool: any) => { expect(tool).toMatchObject({ name: expect.any(String), description: expect.any(String), inputSchema: expect.objectContaining({ type: 'object', properties: expect.any(Object) }) }); }); // Check for expected tools const toolNames = responseBody.tools.map((tool: any) => tool.name); expect(toolNames).toContain('search_packages'); expect(toolNames).toContain('package_info'); expect(toolNames).toContain('install_packages'); }); it('should handle tools/call request with valid JSON response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = createMockEvent('POST', { method: 'tools/call', params: { name: 'search_packages', arguments: { query: 'react', limit: 5 } } }); const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toMatchObject({ content: expect.arrayContaining([ expect.objectContaining({ type: 'text', text: expect.any(String) }) ]) }); }); it('should handle package_info request with structured JSON response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } // Mock package info API response mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ name: 'express', "dist-tags": { latest: '4.18.2' }, versions: { '4.18.2': { name: 'express', version: '4.18.2', description: 'Fast, unopinionated, minimalist web framework', license: 'MIT', author: 'TJ Holowaychuk', dependencies: {}, devDependencies: {}, keywords: ['express', 'framework'] } } }) } as any); const event = createMockEvent('POST', { method: 'tools/call', params: { name: 'package_info', arguments: { packageName: 'express' } } }); const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); const responseBody = JSON.parse(result.body); expect(responseBody.content[0].text).toMatch(/Package Information.*express/i); }); it('should handle download_stats request with JSON response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } // Mock download stats API response mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ downloads: 5000000, start: '2023-07-25', end: '2023-08-01' }) } as any); const event = createMockEvent('POST', { method: 'tools/call', params: { name: 'download_stats', arguments: { packageName: 'react', period: 'last-week' } } }); const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); const responseBody = JSON.parse(result.body); expect(responseBody.content[0].text).toMatch(/Download Statistics.*react/i); }); it('should handle bundle size request with JSON response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } // Mock bundle size API response mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ size: 50000, gzip: 15000, dependencyCount: 3 }) } as any); const event = createMockEvent('POST', { method: 'tools/call', params: { name: 'check_bundle_size', arguments: { packageName: 'lodash', version: '4.17.21' } } }); const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); const responseBody = JSON.parse(result.body); expect(responseBody.content[0].text).toMatch(/Bundle Size Analysis.*lodash/i); }); it('should handle errors with proper JSON error format', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = createMockEvent('POST', { method: 'tools/call', params: { name: 'unknown_tool', arguments: {} } }); const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); const responseBody = JSON.parse(result.body); expect(responseBody).toMatchObject({ isError: true, content: expect.arrayContaining([ expect.objectContaining({ type: 'text', text: expect.stringMatching(/Error.*Unknown tool/i) }) ]) }); }); it('should handle GET requests for service discovery', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = { httpMethod: 'GET', body: null, headers: {} }; const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(200); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toMatchObject({ name: "npmplus-mcp", version: expect.any(String), description: expect.any(String), capabilities: expect.any(Object), endpoints: expect.any(Object) }); }); it('should reject non-GET/POST methods with JSON error', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = { httpMethod: 'PUT', body: null, headers: {} }; const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(405); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toMatchObject({ error: 'Method not allowed' }); }); it('should handle malformed JSON with proper error response', async () => { if (!mcpHandler) { console.warn('MCP handler not available for testing'); return; } const event = { httpMethod: 'POST', body: '{invalid json}', headers: {} }; const result = await mcpHandler(event, mockContext); expect(result.statusCode).toBe(400); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toHaveProperty('error'); }); }); describe('Health Function Endpoint', () => { it('should return valid JSON health status', async () => { if (!healthHandler) { console.warn('Health handler not available for testing'); return; } const event = { httpMethod: 'GET', headers: {} }; const result = await healthHandler(event, {}); expect(result.statusCode).toBe(200); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toMatchObject({ status: 'healthy', service: 'npmplus-mcp', timestamp: expect.any(String), version: expect.any(String), endpoints: expect.objectContaining({ mcp: expect.any(String), health: expect.any(String), analytics: expect.any(String) }) }); // Validate timestamp is valid ISO string expect(new Date(responseBody.timestamp).toISOString()).toBe(responseBody.timestamp); }); it('should handle CORS for health endpoint', async () => { if (!healthHandler) { console.warn('Health handler not available for testing'); return; } const event = { httpMethod: 'OPTIONS', headers: {} }; const result = await healthHandler(event, {}); expect(result.statusCode).toBe(200); expect(result.headers).toMatchObject({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS' }); }); it('should reject non-GET methods for health endpoint', async () => { if (!healthHandler) { console.warn('Health handler not available for testing'); return; } const event = { httpMethod: 'POST', headers: {} }; const result = await healthHandler(event, {}); expect(result.statusCode).toBe(405); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); expect(responseBody).toMatchObject({ error: 'Method not allowed' }); }); }); describe('Analytics Function Endpoint', () => { it('should return valid JSON analytics data', async () => { if (!analyticsHandler) { console.warn('Analytics handler not available for testing'); return; } const event = { httpMethod: 'GET', queryStringParameters: { data: 'true' }, headers: {} }; const result = await analyticsHandler(event, {}); expect(result.statusCode).toBe(200); expect(result.headers['Content-Type']).toBe('application/json'); const responseBody = JSON.parse(result.body); // The actual response structure might be different, just validate it's valid JSON expect(typeof responseBody).toBe('object'); expect(responseBody).not.toBe(null); }); it('should handle analytics endpoint requests', async () => { if (!analyticsHandler) { console.warn('Analytics handler not available for testing'); return; } // Test HTML dashboard (default GET request) const event = { httpMethod: 'GET', headers: {} }; const result = await analyticsHandler(event, {}); // Validate HTML response structure expect(result).toHaveProperty('statusCode'); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); expect(result.statusCode).toBe(200); // Should return HTML dashboard by default expect(result.headers['Content-Type']).toBe('text/html'); expect(result.body).toContain('NPM Plus Analytics Dashboard'); }); }); describe('JSON Response Validation', () => { it('should ensure all endpoints return valid JSON for appropriate requests', async () => { const testEndpoints = [ { handler: mcpHandler, name: 'MCP', expectJSON: true }, { handler: healthHandler, name: 'Health', expectJSON: true }, { handler: analyticsHandler, name: 'Analytics', expectJSON: false } // Analytics returns HTML by default ]; for (const endpoint of testEndpoints) { if (!endpoint.handler) continue; const event = { httpMethod: 'GET', headers: {} }; try { const result = await endpoint.handler(event, {}); expect(result).toHaveProperty('statusCode'); expect(result).toHaveProperty('headers'); expect(result).toHaveProperty('body'); // Validate that body is valid JSON for endpoints that should return JSON if (endpoint.expectJSON) { expect(() => JSON.parse(result.body)).not.toThrow(); expect(result.headers['Content-Type']).toBe('application/json'); } // Validate headers include Content-Type expect(result.headers).toHaveProperty('Content-Type'); } catch (error) { console.warn(`Endpoint ${endpoint.name} test skipped:`, error); } } }); it('should validate JSON schema compliance across all responses', async () => { if (!mcpHandler) return; const testCases = [ { method: 'initialize', params: { protocolVersion: '1.0.0', clientInfo: { name: 'test', version: '1.0.0' }, capabilities: {} } }, { method: 'tools/list', params: {} } ]; for (const testCase of testCases) { const event = { httpMethod: 'POST', body: JSON.stringify(testCase), headers: { 'content-type': 'application/json' } }; const result = await mcpHandler(event, {}); const responseBody = JSON.parse(result.body); // Validate response follows expected JSON structure expect(typeof responseBody).toBe('object'); expect(responseBody).not.toBe(null); // Should not have undefined values expect(JSON.stringify(responseBody)).not.toContain('undefined'); } }); }); });