Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée. Co-authored-by: Cursor <cursoragent@cursor.com>
230 lines
6.5 KiB
JavaScript
230 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Memento MCP Server - Test Suite
|
|
*
|
|
* Run with: npm test
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } 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: '<script>alert("xss")</script>' });
|
|
expect(xss).toBe(true);
|
|
});
|
|
|
|
it('should allow safe HTML', () => {
|
|
const xss = checkXSS({ content: 'Hello <em>world</em>' });
|
|
// Safe formatting tags are not flagged as XSS
|
|
expect(xss).toBe(false);
|
|
});
|
|
|
|
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',
|
|
'get_upcoming_reminders',
|
|
'update_reminder',
|
|
'find_similar_notes',
|
|
'get_memory_echo_insights',
|
|
'dismiss_memory_echo_insight',
|
|
'get_notes_by_label',
|
|
'get_note_statistics',
|
|
'export_notes',
|
|
'import_notes',
|
|
];
|
|
|
|
it('should have all expected tools with schemas', async () => {
|
|
const { toolSchemas } = await import('../validation.js');
|
|
|
|
for (const toolName of toolNames) {
|
|
expect(toolSchemas[toolName]).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Run tests
|
|
console.log('Running MCP Server tests...');
|