#!/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...');