refactor: streamline test suite — 33 fewer files, 11.9x faster (#670)

* refactor: streamline test suite - cut 33 files, enable parallel execution (11.9x speedup)

Remove duplicate, low-value, and fragmented test files while preserving
all meaningful coverage. Enable parallel test execution and remove
the entire benchmark infrastructure.

Key changes:
- Consolidate workflow-validator tests (13 files -> 3)
- Consolidate config-validator tests (9 files -> 3)
- Consolidate telemetry tests (11 files -> 6)
- Merge AI validator tests (2 files -> 1)
- Remove example/demo test files, mock-testing files, and already-skipped tests
- Remove benchmark infrastructure (10 files, CI workflow, 4 npm scripts)
- Enable parallel test execution (remove singleThread: true)
- Remove retry:2 that was masking flaky tests
- Slim CI publish-results job

Results: 224 -> 191 test files, 4690 -> 4303 tests, 121K -> 106K lines
Local runtime: 319s -> 27s (11.9x speedup)

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: absorb config-validator satellite tests into consolidated file

The previous commit deleted 4 config-validator satellite files. This
properly merges their unique tests into the consolidated config-validator.test.ts,
recovering 89 tests that were dropped during the bulk deletion.

Deduplicates 5 tests that existed in both the satellite files and the
security test file.

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: delete missed benchmark-pr.yml workflow, fix flaky session test

- Remove benchmark-pr.yml that referenced deleted benchmark:ci script
- Fix session-persistence round-trip test using timestamps closer to
  now to avoid edge cases exposed by removing retry:2

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rebuild FTS5 index after database rebuild to prevent stale rowid refs

The FTS5 content-synced index could retain phantom rowid references from
previous rebuild cycles, causing 'missing row N from content table'
errors on MATCH queries.

- Add explicit FTS5 rebuild command in rebuild script after all nodes saved
- Add FTS5 rebuild in test beforeAll as defense-in-depth
- Rebuild nodes.db with consistent FTS5 index

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use recent timestamps in all session persistence tests

Session round-trip tests used timestamps 5-10 minutes in the past which
could fail under CI load when combined with session timeout validation.
Use timestamps 30 seconds in the past for all valid-session test data.

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2026-03-27 14:22:22 +01:00
committed by GitHub
parent 07bd1d4cc2
commit de2abaf89d
75 changed files with 3718 additions and 21917 deletions

View File

@@ -1,147 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn, ChildProcess } from 'child_process';
import axios from 'axios';
/**
* Integration tests for rate limiting
*
* SECURITY: These tests verify rate limiting prevents brute force attacks
* See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-02)
*
* TODO: Re-enable when CI server startup issue is resolved
* Server process fails to start on port 3001 in CI with ECONNREFUSED errors
* Tests pass locally but consistently fail in GitHub Actions CI environment
* Rate limiting functionality is verified and working in production
*/
describe.skip('Integration: Rate Limiting', () => {
let serverProcess: ChildProcess;
const port = 3001;
const authToken = 'test-token-for-rate-limiting-test-32-chars';
beforeAll(async () => {
// Start HTTP server with rate limiting
serverProcess = spawn('node', ['dist/http-server-single-session.js'], {
env: {
...process.env,
MCP_MODE: 'http',
PORT: port.toString(),
AUTH_TOKEN: authToken,
NODE_ENV: 'test',
AUTH_RATE_LIMIT_WINDOW: '900000', // 15 minutes
AUTH_RATE_LIMIT_MAX: '20', // 20 attempts
},
stdio: 'pipe',
});
// Wait for server to start (longer wait for CI)
await new Promise(resolve => setTimeout(resolve, 8000));
}, 20000);
afterAll(() => {
if (serverProcess) {
serverProcess.kill();
}
});
it('should block after max authentication attempts (sequential requests)', async () => {
const baseUrl = `http://localhost:${port}/mcp`;
// IMPORTANT: Use sequential requests to ensure deterministic order
// Parallel requests can cause race conditions with in-memory rate limiter
for (let i = 1; i <= 25; i++) {
const response = await axios.post(
baseUrl,
{ jsonrpc: '2.0', method: 'initialize', id: i },
{
headers: { Authorization: 'Bearer wrong-token' },
validateStatus: () => true, // Don't throw on error status
}
);
if (i <= 20) {
// First 20 attempts should be 401 (invalid authentication)
expect(response.status).toBe(401);
expect(response.data.error.message).toContain('Unauthorized');
} else {
// Attempts 21+ should be 429 (rate limited)
expect(response.status).toBe(429);
expect(response.data.error.message).toContain('Too many');
}
}
}, 60000);
it('should include rate limit headers', async () => {
const baseUrl = `http://localhost:${port}/mcp`;
const response = await axios.post(
baseUrl,
{ jsonrpc: '2.0', method: 'initialize', id: 1 },
{
headers: { Authorization: 'Bearer wrong-token' },
validateStatus: () => true,
}
);
// Check for standard rate limit headers
expect(response.headers['ratelimit-limit']).toBeDefined();
expect(response.headers['ratelimit-remaining']).toBeDefined();
expect(response.headers['ratelimit-reset']).toBeDefined();
}, 15000);
it('should accept valid tokens within rate limit', async () => {
const baseUrl = `http://localhost:${port}/mcp`;
const response = await axios.post(
baseUrl,
{
jsonrpc: '2.0',
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'test', version: '1.0' },
},
id: 1,
},
{
headers: { Authorization: `Bearer ${authToken}` },
}
);
expect(response.status).toBe(200);
expect(response.data.result).toBeDefined();
}, 15000);
it('should return JSON-RPC formatted error on rate limit', async () => {
const baseUrl = `http://localhost:${port}/mcp`;
// Exhaust rate limit
for (let i = 0; i < 21; i++) {
await axios.post(
baseUrl,
{ jsonrpc: '2.0', method: 'initialize', id: i },
{
headers: { Authorization: 'Bearer wrong-token' },
validateStatus: () => true,
}
);
}
// Get rate limited response
const response = await axios.post(
baseUrl,
{ jsonrpc: '2.0', method: 'initialize', id: 999 },
{
headers: { Authorization: 'Bearer wrong-token' },
validateStatus: () => true,
}
);
// Verify JSON-RPC error format
expect(response.data).toHaveProperty('jsonrpc', '2.0');
expect(response.data).toHaveProperty('error');
expect(response.data.error).toHaveProperty('code', -32000);
expect(response.data.error).toHaveProperty('message');
expect(response.data).toHaveProperty('id', null);
}, 60000);
});