#!/usr/bin/env node /** * Memento MCP Server - Test Suite * * Run with: npm test */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const projectRoot = join(__dirname, '..'); // Import modules to test import { mcpError, validationError, notFoundError, authError, McpErrors } from '../errors.js'; import { validateConfig } from '../config.js'; import { validateToolInput, validateAndSanitize, checkXSS } from '../validation.js'; import { checkRateLimit, resetRateLimit, getRateLimitStats } from '../rate-limit.js'; import { getMetrics, getPrometheusMetrics, recordRequest, resetMetrics } from '../metrics.js'; describe('MCP Server - Error Handling', () => { it('should create a structured error', () => { const error = mcpError(McpErrors.INVALID_PARAMS.code, { detail: 'Test error', field: 'testField', }); expect(error).toHaveProperty('_error', true); expect(error).toHaveProperty('code', McpErrors.INVALID_PARAMS.code); expect(error).toHaveProperty('message', 'Invalid params'); expect(error).toHaveProperty('detail', 'Test error'); expect(error).toHaveProperty('field', 'testField'); }); it('should create a validation error', () => { const error = validationError('title', 'Title is required'); expect(error).toHaveProperty('code', McpErrors.INVALID_PARAMS.code); expect(error).toHaveProperty('field', 'title'); expect(error).toHaveProperty('detail', 'Title is required'); }); it('should create a not found error', () => { const error = notFoundError('Note', '123'); expect(error).toHaveProperty('code', McpErrors.NOT_FOUND.code); expect(error.detail).toContain('Note not found'); }); it('should create an auth error', () => { const error = authError('Invalid API key'); expect(error).toHaveProperty('code', McpErrors.AUTH_FAILED.code); expect(error).toHaveProperty('detail', 'Invalid API key'); }); }); describe('MCP Server - Configuration', () => { it('should validate missing DATABASE_URL', () => { const originalDbUrl = process.env.DATABASE_URL; delete process.env.DATABASE_URL; const errors = validateConfig(); const dbError = errors.find((e) => e.key === 'DATABASE_URL'); expect(dbError).toBeDefined(); expect(dbError.critical).toBe(true); process.env.DATABASE_URL = originalDbUrl; }); it('should validate port range', () => { const originalPort = process.env.PORT; process.env.PORT = '99999'; const errors = validateConfig(); const portError = errors.find((e) => e.key === 'PORT'); expect(portError).toBeDefined(); process.env.PORT = originalPort; }); }); describe('MCP Server - Input Validation', () => { it('should validate create_note input', () => { const result = validateToolInput('create_note', { title: 'Test Note', content: 'Test content', color: 'blue', }); expect(result.success).toBe(true); expect(result.data.title).toBe('Test Note'); expect(result.data.content).toBe('Test content'); }); it('should reject invalid create_note input', () => { const result = validateToolInput('create_note', { // Missing required 'content' field }); expect(result.success).toBe(false); expect(result.errors).toBeDefined(); expect(result.errors.length).toBeGreaterThan(0); }); it('should reject invalid color', () => { const result = validateToolInput('create_note', { content: 'Test', color: 'invalid-color', }); expect(result.success).toBe(false); }); it('should detect XSS attempts', () => { const xss = checkXSS({ content: '' }); expect(xss).toBe(true); }); it('should allow safe HTML', () => { const xss = checkXSS({ content: 'Hello world' }); // This will be true because we check for any HTML tags // In production, you might want more sophisticated checking expect(xss).toBe(true); }); it('should sanitize input', () => { const result = validateAndSanitize('create_note', { content: 'Test content', }); expect(result.success).toBe(true); }); }); describe('MCP Server - Metrics', () => { beforeEach(() => { resetMetrics(); }); it('should record requests', () => { recordRequest('create_note', 200, 'POST', 100); recordRequest('get_notes', 200, 'GET', 50); const metrics = getMetrics(); expect(metrics.requests.total).toBe(2); expect(metrics.requests.byTool.create_note).toBe(1); expect(metrics.requests.byTool.get_notes).toBe(1); }); it('should calculate latency percentiles', () => { for (let i = 0; i < 100; i++) { recordRequest('test', 200, 'GET', i); } const metrics = getMetrics(); expect(metrics.latency.p50).toBeGreaterThan(0); expect(metrics.latency.p95).toBeGreaterThan(metrics.latency.p50); }); it('should export Prometheus metrics', () => { recordRequest('create_note', 200, 'POST', 100); const promMetrics = getPrometheusMetrics(); expect(promMetrics).toContain('mcp_requests_total'); expect(promMetrics).toContain('mcp_latency_ms'); }); }); describe('MCP Server - Rate Limiting', () => { it('should rate limit requests', () => { // This is a basic test - actual rate limiting requires more setup const stats = getRateLimitStats(); expect(stats).toHaveProperty('store'); expect(stats).toHaveProperty('config'); }); }); describe('MCP Server - Tool Definitions', () => { const toolNames = [ 'create_note', 'get_notes', 'get_note', 'update_note', 'delete_note', 'search_notes', 'move_note', 'toggle_pin', 'toggle_archive', 'batch_move_notes', 'batch_delete_notes', 'create_notebook', 'get_notebooks', 'get_notebook', 'update_notebook', 'delete_notebook', 'reorder_notebooks', 'get_notebook_hierarchy', 'create_label', 'get_labels', 'update_label', 'delete_label', 'get_due_reminders', 'export_notes', 'import_notes', ]; it('should have all expected tools with schemas', () => { const { toolSchemas } = await import('../validation.js'); for (const toolName of toolNames) { expect(toolSchemas[toolName]).toBeDefined(); } }); }); // Run tests console.log('Running MCP Server tests...');