Files
n8n-mcp/tests/unit/session-management-api.test.ts
czlonkowski be0211d826 fix: update session-management-api tests for relaxed validation
Updates session-management-api.test.ts to align with the relaxed
session ID validation policy introduced for MCP proxy compatibility.

Changes:
- Remove short session IDs from invalid test cases (they're now valid)
- Add new test "should accept short session IDs (relaxed for MCP proxy compatibility)"
- Keep testing truly invalid IDs: empty strings, too long (101+), invalid chars
- Add more comprehensive invalid character tests (spaces, special chars)

Valid short session IDs now accepted:
- 'short' (5 chars)
- 'a' (1 char)
- 'only-nineteen-chars' (19 chars)
- '12345' (5 digits)

Invalid session IDs still rejected:
- Empty strings
- Over 100 characters
- Contains invalid characters (spaces, special chars, quotes, slashes)

This maintains security (character whitelist, max length) while
improving MCP proxy compatibility.

Resolves the last failing CI test in PR #312

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 19:05:54 +02:00

350 lines
12 KiB
TypeScript

/**
* Unit tests for Session Management API (Phase 2 - REQ-5)
* Tests the public API methods for session management in v2.19.0
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { N8NMCPEngine } from '../../src/mcp-engine';
import { InstanceContext } from '../../src/types/instance-context';
describe('Session Management API (Phase 2 - REQ-5)', () => {
let engine: N8NMCPEngine;
const testContext: InstanceContext = {
n8nApiUrl: 'https://test.n8n.cloud',
n8nApiKey: 'test-api-key',
instanceId: 'test-instance'
};
beforeEach(() => {
// Set required AUTH_TOKEN environment variable for testing
process.env.AUTH_TOKEN = 'test-token-for-session-management-testing-32chars';
// Create engine with session restoration disabled for these tests
engine = new N8NMCPEngine({
sessionTimeout: 30 * 60 * 1000 // 30 minutes
});
});
describe('getActiveSessions()', () => {
it('should return empty array when no sessions exist', () => {
const sessionIds = engine.getActiveSessions();
expect(sessionIds).toEqual([]);
});
it('should return session IDs after session creation via restoreSession', () => {
// Create session using direct API (not through HTTP request)
const sessionId = 'instance-test-abc123-uuid-session-test-1';
engine.restoreSession(sessionId, testContext);
const sessionIds = engine.getActiveSessions();
expect(sessionIds.length).toBe(1);
expect(sessionIds).toContain(sessionId);
});
it('should return multiple session IDs when multiple sessions exist', () => {
// Create multiple sessions using direct API
const sessions = [
{ id: 'instance-test1-abc123-uuid-session-1', context: { ...testContext, instanceId: 'instance-1' } },
{ id: 'instance-test2-abc123-uuid-session-2', context: { ...testContext, instanceId: 'instance-2' } }
];
sessions.forEach(({ id, context }) => {
engine.restoreSession(id, context);
});
const sessionIds = engine.getActiveSessions();
expect(sessionIds.length).toBe(2);
expect(sessionIds).toContain(sessions[0].id);
expect(sessionIds).toContain(sessions[1].id);
});
});
describe('getSessionState()', () => {
it('should return null for non-existent session', () => {
const state = engine.getSessionState('non-existent-session-id');
expect(state).toBeNull();
});
it('should return session state for existing session', () => {
// Create a session using direct API
const sessionId = 'instance-test-abc123-uuid-session-state-test';
engine.restoreSession(sessionId, testContext);
const state = engine.getSessionState(sessionId);
expect(state).not.toBeNull();
expect(state).toMatchObject({
sessionId: sessionId,
instanceContext: expect.objectContaining({
n8nApiUrl: testContext.n8nApiUrl,
n8nApiKey: testContext.n8nApiKey,
instanceId: testContext.instanceId
}),
createdAt: expect.any(Date),
lastAccess: expect.any(Date),
expiresAt: expect.any(Date)
});
});
it('should include metadata in session state if available', () => {
const contextWithMetadata: InstanceContext = {
...testContext,
metadata: { userId: 'user-123', tier: 'premium' }
};
const sessionId = 'instance-test-abc123-uuid-metadata-test';
engine.restoreSession(sessionId, contextWithMetadata);
const state = engine.getSessionState(sessionId);
expect(state?.metadata).toEqual({ userId: 'user-123', tier: 'premium' });
});
it('should calculate correct expiration time', () => {
const sessionId = 'instance-test-abc123-uuid-expiry-test';
engine.restoreSession(sessionId, testContext);
const state = engine.getSessionState(sessionId);
expect(state).not.toBeNull();
if (state) {
const expectedExpiry = new Date(state.lastAccess.getTime() + 30 * 60 * 1000);
const actualExpiry = state.expiresAt;
// Allow 1 second difference for test timing
expect(Math.abs(actualExpiry.getTime() - expectedExpiry.getTime())).toBeLessThan(1000);
}
});
});
describe('getAllSessionStates()', () => {
it('should return empty array when no sessions exist', () => {
const states = engine.getAllSessionStates();
expect(states).toEqual([]);
});
it('should return all session states', () => {
// Create two sessions using direct API
const session1Id = 'instance-test1-abc123-uuid-all-states-1';
const session2Id = 'instance-test2-abc123-uuid-all-states-2';
engine.restoreSession(session1Id, {
...testContext,
instanceId: 'instance-1'
});
engine.restoreSession(session2Id, {
...testContext,
instanceId: 'instance-2'
});
const states = engine.getAllSessionStates();
expect(states.length).toBe(2);
expect(states[0]).toMatchObject({
sessionId: expect.any(String),
instanceContext: expect.objectContaining({
n8nApiUrl: testContext.n8nApiUrl
}),
createdAt: expect.any(Date),
lastAccess: expect.any(Date),
expiresAt: expect.any(Date)
});
});
it('should filter out sessions without state', () => {
// Create session using direct API
const sessionId = 'instance-test-abc123-uuid-filter-test';
engine.restoreSession(sessionId, testContext);
// Get states
const states = engine.getAllSessionStates();
expect(states.length).toBe(1);
// All returned states should be non-null
states.forEach(state => {
expect(state).not.toBeNull();
});
});
});
describe('restoreSession()', () => {
it('should create a new session with provided ID and context', () => {
const sessionId = 'instance-test-abc123-uuid-test-session-id';
const result = engine.restoreSession(sessionId, testContext);
expect(result).toBe(true);
expect(engine.getActiveSessions()).toContain(sessionId);
});
it('should be idempotent - return true for existing session', () => {
const sessionId = 'instance-test-abc123-uuid-test-session-id2';
// First restoration
const result1 = engine.restoreSession(sessionId, testContext);
expect(result1).toBe(true);
// Second restoration with same ID
const result2 = engine.restoreSession(sessionId, testContext);
expect(result2).toBe(true);
// Should still only have one session
const sessionIds = engine.getActiveSessions();
expect(sessionIds.filter(id => id === sessionId).length).toBe(1);
});
it('should return false for invalid session ID format', () => {
const invalidSessionIds = [
'', // Empty string
'a'.repeat(101), // Too long (101 chars, exceeds max)
"'; DROP TABLE sessions--", // SQL injection attempt (invalid characters: ', ;, space)
'../../../etc/passwd', // Path traversal attempt (invalid characters: ., /)
'has spaces here', // Invalid character (space)
'special@chars#here' // Invalid characters (@, #)
];
invalidSessionIds.forEach(sessionId => {
const result = engine.restoreSession(sessionId, testContext);
expect(result).toBe(false);
});
});
it('should accept short session IDs (relaxed for MCP proxy compatibility)', () => {
const validShortIds = [
'short', // 5 chars - now valid
'a', // 1 char - now valid
'only-nineteen-chars', // 19 chars - now valid
'12345' // 5 digit ID - now valid
];
validShortIds.forEach(sessionId => {
const result = engine.restoreSession(sessionId, testContext);
expect(result).toBe(true);
expect(engine.getActiveSessions()).toContain(sessionId);
});
});
it('should return false for invalid instance context', () => {
const sessionId = 'instance-test-abc123-uuid-test-session-id3';
const invalidContext = {
n8nApiUrl: 'not-a-valid-url', // Invalid URL
n8nApiKey: 'test-key',
instanceId: 'test'
} as any;
const result = engine.restoreSession(sessionId, invalidContext);
expect(result).toBe(false);
});
it('should create session that can be retrieved with getSessionState', () => {
const sessionId = 'instance-test-abc123-uuid-test-session-id4';
engine.restoreSession(sessionId, testContext);
const state = engine.getSessionState(sessionId);
expect(state).not.toBeNull();
expect(state?.sessionId).toBe(sessionId);
expect(state?.instanceContext).toEqual(testContext);
});
});
describe('deleteSession()', () => {
it('should return false for non-existent session', () => {
const result = engine.deleteSession('non-existent-session-id');
expect(result).toBe(false);
});
it('should delete existing session and return true', () => {
// Create a session using direct API
const sessionId = 'instance-test-abc123-uuid-delete-test';
engine.restoreSession(sessionId, testContext);
// Delete the session
const result = engine.deleteSession(sessionId);
expect(result).toBe(true);
// Session should no longer exist
expect(engine.getActiveSessions()).not.toContain(sessionId);
expect(engine.getSessionState(sessionId)).toBeNull();
});
it('should return false when trying to delete already deleted session', () => {
// Create and delete session using direct API
const sessionId = 'instance-test-abc123-uuid-double-delete-test';
engine.restoreSession(sessionId, testContext);
engine.deleteSession(sessionId);
// Try to delete again
const result = engine.deleteSession(sessionId);
expect(result).toBe(false);
});
});
describe('Integration workflows', () => {
it('should support periodic backup workflow', () => {
// Create multiple sessions using direct API
for (let i = 0; i < 3; i++) {
const sessionId = `instance-test${i}-abc123-uuid-backup-${i}`;
engine.restoreSession(sessionId, {
...testContext,
instanceId: `instance-${i}`
});
}
// Simulate periodic backup
const states = engine.getAllSessionStates();
expect(states.length).toBe(3);
// Each state should be serializable
states.forEach(state => {
const serialized = JSON.stringify(state);
expect(serialized).toBeTruthy();
const deserialized = JSON.parse(serialized);
expect(deserialized.sessionId).toBe(state.sessionId);
});
});
it('should support bulk restore workflow', () => {
const sessionData = [
{ sessionId: 'instance-test1-abc123-uuid-bulk-session-1', context: { ...testContext, instanceId: 'user-1' } },
{ sessionId: 'instance-test2-abc123-uuid-bulk-session-2', context: { ...testContext, instanceId: 'user-2' } },
{ sessionId: 'instance-test3-abc123-uuid-bulk-session-3', context: { ...testContext, instanceId: 'user-3' } }
];
// Restore all sessions
for (const { sessionId, context } of sessionData) {
const restored = engine.restoreSession(sessionId, context);
expect(restored).toBe(true);
}
// Verify all sessions exist
const sessionIds = engine.getActiveSessions();
expect(sessionIds.length).toBe(3);
sessionData.forEach(({ sessionId }) => {
expect(sessionIds).toContain(sessionId);
});
});
it('should support session lifecycle workflow (create → get → delete)', () => {
// 1. Create session using direct API
const sessionId = 'instance-test-abc123-uuid-lifecycle-test';
engine.restoreSession(sessionId, testContext);
// 2. Get session state
const state = engine.getSessionState(sessionId);
expect(state).not.toBeNull();
// 3. Simulate saving to database (serialization test)
const serialized = JSON.stringify(state);
expect(serialized).toBeTruthy();
// 4. Delete session
const deleted = engine.deleteSession(sessionId);
expect(deleted).toBe(true);
// 5. Verify deletion
expect(engine.getSessionState(sessionId)).toBeNull();
expect(engine.getActiveSessions()).not.toContain(sessionId);
});
});
});