mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
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>
350 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|