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
text/typescript
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');
}
});
});
});