@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
697 lines (567 loc) • 23.5 kB
text/typescript
/**
* @jest-environment node
*/
import {
describe,
it,
expect,
jest,
beforeEach,
afterEach,
} from '@jest/globals';
// Mock console to avoid output during tests
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
// Import after mocking console
import { connectionManager } from '../utils/connection-manager.js';
describe('ConnectionManager', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
// Clear any existing connections before each test
// Force cleanup by calling private cleanup method
(connectionManager as any).connections.clear();
(connectionManager as any).activeRequests.clear();
(connectionManager as any).requestQueue.clear();
});
afterEach(() => {
jest.useRealTimers();
});
describe('Connection Registration', () => {
it('should register a new connection', () => {
connectionManager.registerConnection('test-connection-1');
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(1);
expect(stats.activeConnections).toBe(1);
// Connection registration logging removed for CI compliance
});
it('should initialize request tracking for new connections', () => {
connectionManager.registerConnection('test-connection-2');
// Test internal state
const activeRequests = (connectionManager as any).activeRequests;
const requestQueue = (connectionManager as any).requestQueue;
expect(activeRequests.has('test-connection-2')).toBe(true);
expect(activeRequests.get('test-connection-2')).toBe(0);
expect(requestQueue.has('test-connection-2')).toBe(true);
expect(requestQueue.get('test-connection-2')).toEqual([]);
});
it('should set connection properties correctly', () => {
const beforeRegistration = Date.now();
connectionManager.registerConnection('test-connection-3');
const afterRegistration = Date.now();
const connections = (connectionManager as any).connections;
const connection = connections.get('test-connection-3');
expect(connection).toBeDefined();
expect(connection.id).toBe('test-connection-3');
expect(connection.createdAt.getTime()).toBeGreaterThanOrEqual(
beforeRegistration
);
expect(connection.createdAt.getTime()).toBeLessThanOrEqual(
afterRegistration
);
expect(connection.lastUsed.getTime()).toBeGreaterThanOrEqual(
beforeRegistration
);
expect(connection.lastUsed.getTime()).toBeLessThanOrEqual(
afterRegistration
);
expect(connection.requestCount).toBe(0);
expect(connection.isActive).toBe(true);
});
});
describe('Request Handling', () => {
describe('Basic Request Processing', () => {
it('should handle request successfully', async () => {
connectionManager.registerConnection('test-connection');
const mockHandler = jest.fn(() => Promise.resolve('success'));
const result = await connectionManager.handleRequest(
'test-connection',
mockHandler as any
);
expect(result).toBe('success');
expect(mockHandler).toHaveBeenCalledTimes(1);
});
it('should auto-register connection if not exists', async () => {
const mockHandler = jest.fn(() => Promise.resolve('success'));
const result = await connectionManager.handleRequest(
'auto-connection',
mockHandler as any
);
expect(result).toBe('success');
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(1);
// Auto-registration logging removed for CI compliance
});
it('should update connection properties on request', async () => {
connectionManager.registerConnection('test-connection');
const connections = (connectionManager as any).connections;
const initialConnection = connections.get('test-connection');
const initialLastUsed = initialConnection.lastUsed.getTime();
const initialRequestCount = initialConnection.requestCount;
// Wait a bit to ensure timestamp difference
jest.advanceTimersByTime(100);
const mockHandler = jest.fn(() => Promise.resolve('success'));
await connectionManager.handleRequest(
'test-connection',
mockHandler as any
);
const updatedConnection = connections.get('test-connection');
expect(updatedConnection.lastUsed.getTime()).toBeGreaterThan(
initialLastUsed
);
expect(updatedConnection.requestCount).toBe(initialRequestCount + 1);
});
it('should handle request errors gracefully', async () => {
connectionManager.registerConnection('test-connection');
const mockHandler = jest
.fn()
.mockRejectedValue(new Error('Request failed') as never);
await expect(
connectionManager.handleRequest('test-connection', mockHandler as any)
).rejects.toThrow('Request failed');
// Should still decrement active requests counter
const stats = connectionManager.getStats();
expect(stats.queuedRequests).toBe(0);
});
});
describe('Concurrency Control', () => {
it('should allow requests up to max concurrent limit', async () => {
connectionManager.registerConnection('test-connection');
const mockHandlers = Array(10)
.fill(0)
.map(() =>
jest
.fn()
.mockImplementation(
() =>
new Promise(resolve =>
setTimeout(() => resolve('success'), 100)
)
)
);
// Start all requests simultaneously
const promises = mockHandlers.map(handler =>
connectionManager.handleRequest('test-connection', handler as any)
);
// All should be executing (not queued)
const stats = connectionManager.getStats();
expect(stats.queuedRequests).toBe(0);
// Fast-forward timers to resolve promises
jest.advanceTimersByTime(100);
const results = await Promise.all(promises);
expect(results).toEqual(Array(10).fill('success'));
mockHandlers.forEach(handler => {
expect(handler).toHaveBeenCalledTimes(1);
});
});
it('should queue requests beyond max concurrent limit', async () => {
connectionManager.registerConnection('test-connection');
const longRunningHandler = jest
.fn()
.mockImplementation(
() =>
new Promise(resolve => setTimeout(() => resolve('long'), 1000))
);
const quickHandler = jest.fn(() => Promise.resolve('quick'));
// Fill up all concurrent slots with long-running requests
const longRunningPromises = Array(10)
.fill(0)
.map(() =>
connectionManager.handleRequest(
'test-connection',
longRunningHandler as any
)
);
// This request should be queued
const queuedPromise = connectionManager.handleRequest(
'test-connection',
quickHandler as any
);
// Check that the request is queued
const stats = connectionManager.getStats();
expect(stats.queuedRequests).toBe(1);
// The quick handler shouldn't have been called yet
expect(quickHandler).not.toHaveBeenCalled();
// Fast-forward to complete one long-running request
jest.advanceTimersByTime(1000);
await Promise.all(longRunningPromises);
// Now the queued request should execute
const queuedResult = await queuedPromise;
expect(queuedResult).toBe('quick');
expect(quickHandler).toHaveBeenCalledTimes(1);
});
it('should process queue in FIFO order', async () => {
connectionManager.registerConnection('test-connection');
const executionOrder: string[] = [];
const createHandler = (id: string) =>
jest.fn().mockImplementation(async () => {
executionOrder.push(id);
return id;
});
// Fill concurrent slots
const concurrentHandlers = Array(10)
.fill(0)
.map((_, i) => createHandler(`concurrent-${i}`));
const concurrentPromises = concurrentHandlers.map(handler =>
connectionManager.handleRequest('test-connection', handler as any)
);
// Queue additional requests
const queuedHandlers = [
createHandler('queued-1'),
createHandler('queued-2'),
createHandler('queued-3'),
];
const queuedPromises = queuedHandlers.map(handler =>
connectionManager.handleRequest('test-connection', handler as any)
);
// Complete concurrent requests to process queue
await Promise.all(concurrentPromises);
await Promise.all(queuedPromises);
// Check that queued requests were processed in order
const queuedExecutions = executionOrder.filter(id =>
id.startsWith('queued-')
);
expect(queuedExecutions).toEqual(['queued-1', 'queued-2', 'queued-3']);
});
it('should track active requests correctly', async () => {
connectionManager.registerConnection('test-connection');
const slowHandler = jest
.fn()
.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve('done'), 500))
);
// Start multiple requests
const promises = Array(5)
.fill(0)
.map(() =>
connectionManager.handleRequest(
'test-connection',
slowHandler as any
)
);
// Check active request count
const activeRequests = (connectionManager as any).activeRequests;
expect(activeRequests.get('test-connection')).toBe(5);
// Complete requests
jest.advanceTimersByTime(500);
await Promise.all(promises);
// Active count should be back to 0
expect(activeRequests.get('test-connection')).toBe(0);
});
});
});
describe('Connection Closure', () => {
it('should close connection and cleanup resources', () => {
connectionManager.registerConnection('test-connection');
// Add some queued requests (not awaited intentionally)
Array(3)
.fill(0)
.map((_, i) =>
connectionManager.handleRequest(
'test-connection',
() =>
new Promise(resolve =>
setTimeout(() => resolve(`queued-${i}`), 1000)
)
)
);
connectionManager.closeConnection('test-connection');
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(0);
expect(stats.queuedRequests).toBe(0);
// Connection closure logging removed for CI compliance
});
it('should reject queued requests on connection close', async () => {
connectionManager.registerConnection('test-connection');
// Fill concurrent slots (not awaited intentionally)
Array(10)
.fill(0)
.map(() =>
connectionManager.handleRequest(
'test-connection',
() =>
new Promise(resolve =>
setTimeout(() => resolve('blocking'), 1000)
)
)
);
// Queue additional requests
const queuedPromises = Array(3)
.fill(0)
.map(() =>
connectionManager.handleRequest('test-connection', () =>
Promise.resolve('queued')
)
);
// Close connection - should reject queued requests
connectionManager.closeConnection('test-connection');
// Queued requests should be rejected
for (const promise of queuedPromises) {
await expect(promise).rejects.toThrow('Connection closed');
}
});
it('should handle closing non-existent connection gracefully', () => {
connectionManager.closeConnection('non-existent');
// Should not throw error or log anything
expect(mockConsoleLog).not.toHaveBeenCalledWith(
expect.stringContaining('Closed connection: non-existent')
);
});
it('should mark connection as inactive', () => {
connectionManager.registerConnection('test-connection');
const connections = (connectionManager as any).connections;
const connection = connections.get('test-connection');
expect(connection.isActive).toBe(true);
connectionManager.closeConnection('test-connection');
// Connection should be removed entirely, but let's test the flow by checking
// that closeConnection sets isActive to false before cleanup
expect(connections.has('test-connection')).toBe(false);
});
});
describe('Statistics', () => {
it('should return correct statistics for empty state', () => {
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(0);
expect(stats.activeConnections).toBe(0);
expect(stats.totalRequests).toBe(0);
expect(stats.queuedRequests).toBe(0);
});
it('should calculate statistics correctly', async () => {
// Register multiple connections
connectionManager.registerConnection('conn1');
connectionManager.registerConnection('conn2');
// Make some requests to update request counts
await connectionManager.handleRequest('conn1', () =>
Promise.resolve('test' as any)
);
await connectionManager.handleRequest('conn1', () =>
Promise.resolve('test' as any)
);
await connectionManager.handleRequest('conn2', () =>
Promise.resolve('test' as any)
);
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(2);
expect(stats.activeConnections).toBe(2);
expect(stats.totalRequests).toBe(3);
expect(stats.queuedRequests).toBe(0);
});
it('should count queued requests correctly', async () => {
connectionManager.registerConnection('test-connection');
// Fill concurrent slots with blocking requests (not awaited intentionally)
Array(10)
.fill(0)
.map(() =>
connectionManager.handleRequest(
'test-connection',
() =>
new Promise(resolve =>
setTimeout(() => resolve('blocking'), 1000)
)
)
);
// Add queued requests (not awaited intentionally)
Array(5)
.fill(0)
.map(() =>
connectionManager.handleRequest('test-connection', () =>
Promise.resolve('queued')
)
);
const stats = connectionManager.getStats();
expect(stats.queuedRequests).toBe(5);
});
it('should distinguish between active and inactive connections', () => {
connectionManager.registerConnection('active-conn');
connectionManager.registerConnection('inactive-conn');
// Close one connection
connectionManager.closeConnection('inactive-conn');
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(1);
expect(stats.activeConnections).toBe(1);
});
});
describe('Stale Connection Cleanup', () => {
it('should identify and cleanup stale connections', () => {
const now = Date.now();
jest.setSystemTime(now);
connectionManager.registerConnection('fresh-connection');
connectionManager.registerConnection('stale-connection');
// Manually set last used time to simulate stale connection
const connections = (connectionManager as any).connections;
const staleConnection = connections.get('stale-connection');
staleConnection.lastUsed = new Date(now - 400000); // 6 minutes ago (> 5 min timeout)
connectionManager.cleanupStaleConnections();
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(1);
expect(connections.has('fresh-connection')).toBe(true);
expect(connections.has('stale-connection')).toBe(false);
// Stale connection cleanup logging removed for CI compliance
});
it('should not cleanup fresh connections', () => {
connectionManager.registerConnection('fresh-connection-1');
connectionManager.registerConnection('fresh-connection-2');
connectionManager.cleanupStaleConnections();
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(2);
expect(stats.activeConnections).toBe(2);
// Should not log cleanup message for 0 stale connections
expect(mockConsoleLog).not.toHaveBeenCalledWith(
expect.stringContaining('Cleaned up')
);
});
it('should handle cleanup with no connections', () => {
connectionManager.cleanupStaleConnections();
// Should not throw error
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(0);
});
it('should use correct timeout value', () => {
const now = Date.now();
jest.setSystemTime(now);
connectionManager.registerConnection('borderline-connection');
const connections = (connectionManager as any).connections;
const connection = connections.get('borderline-connection');
// Set slightly over timeout boundary (5 minutes = 300000ms, using 300001ms)
connection.lastUsed = new Date(now - 300001);
connectionManager.cleanupStaleConnections();
// Should be cleaned up (> timeout)
expect(connections.has('borderline-connection')).toBe(false);
});
it('should cleanup multiple stale connections', () => {
const now = Date.now();
jest.setSystemTime(now);
connectionManager.registerConnection('fresh');
connectionManager.registerConnection('stale1');
connectionManager.registerConnection('stale2');
connectionManager.registerConnection('stale3');
const connections = (connectionManager as any).connections;
// Make connections stale
['stale1', 'stale2', 'stale3'].forEach(id => {
connections.get(id).lastUsed = new Date(now - 400000);
});
connectionManager.cleanupStaleConnections();
const stats = connectionManager.getStats();
expect(stats.totalConnections).toBe(1);
expect(connections.has('fresh')).toBe(true);
// Multiple stale connections cleanup logging removed for CI compliance
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle concurrent modifications safely', async () => {
connectionManager.registerConnection('test-connection');
const handler1 = jest.fn().mockImplementation(() => {
// Simulate concurrent close during request
setTimeout(
() => connectionManager.closeConnection('test-connection'),
10
);
return new Promise(resolve => setTimeout(() => resolve('result1'), 50));
});
const handler2 = jest.fn(() => Promise.resolve('result2'));
// Start request and close connection concurrently
const promise1 = connectionManager.handleRequest(
'test-connection',
handler1 as any
);
const promise2 = connectionManager.handleRequest(
'test-connection',
handler2 as any
);
jest.advanceTimersByTime(100);
const result1 = await promise1;
expect(result1).toBe('result1');
// Second request might fail due to connection closure
try {
await promise2;
} catch (error: any) {
expect(error).toBeInstanceOf(Error);
}
});
it('should handle queue processing with errors', async () => {
connectionManager.registerConnection('test-connection');
// Fill concurrent slots
const blockingHandlers = Array(10)
.fill(0)
.map(() =>
jest
.fn()
.mockImplementation(
() =>
new Promise(resolve =>
setTimeout(() => resolve('blocking'), 100)
)
)
);
const blockingPromises = blockingHandlers.map(handler =>
connectionManager.handleRequest('test-connection', handler as any)
);
// Queue requests - one that will fail, one that should succeed
const failingHandler = jest
.fn()
.mockRejectedValue(new Error('Queued request failed') as never);
const successHandler = jest.fn(() => Promise.resolve('success'));
const failingPromise = connectionManager.handleRequest(
'test-connection',
failingHandler as any
);
const successPromise = connectionManager.handleRequest(
'test-connection',
successHandler as any
);
// Complete blocking requests to process queue
jest.advanceTimersByTime(100);
await Promise.all(blockingPromises);
// First queued request should fail
await expect(failingPromise).rejects.toThrow('Queued request failed');
// Second queued request should succeed
const result = await successPromise;
expect(result).toBe('success');
});
it('should handle empty queue gracefully', () => {
connectionManager.registerConnection('test-connection');
// Call processQueue with empty queue - bind the context
const processQueue = (connectionManager as any).processQueue.bind(
connectionManager
);
expect(() => processQueue('test-connection')).not.toThrow();
// Should not affect active request count
const activeRequests = (connectionManager as any).activeRequests;
expect(activeRequests.get('test-connection')).toBe(0);
});
it('should handle missing connection in processQueue', () => {
// Call processQueue for non-existent connection - bind the context
const processQueue = (connectionManager as any).processQueue.bind(
connectionManager
);
expect(() => processQueue('non-existent')).not.toThrow();
});
it('should handle extreme request counts', async () => {
connectionManager.registerConnection('test-connection');
// Test with many small requests
const handlers = Array(100)
.fill(0)
.map((_, i) => jest.fn(() => Promise.resolve(`result-${i}`)));
const promises = handlers.map(handler =>
connectionManager.handleRequest('test-connection', handler as any)
);
const results = await Promise.all(promises);
expect(results).toHaveLength(100);
handlers.forEach((handler, i) => {
expect(handler).toHaveBeenCalledTimes(1);
expect(results[i]).toBe(`result-${i}`);
});
});
});
describe('Module-level Behavior', () => {
it('should export a singleton instance', async () => {
// Import again to test singleton behavior
const { connectionManager: anotherRef } = await import(
'../utils/connection-manager.js'
);
expect(anotherRef).toBe(connectionManager);
});
// Note: Testing the setInterval cleanup timer would require more complex mocking
// of setInterval and would be more of an integration test. The cleanup logic
// is already tested above.
});
});