import { Server } from 'http'; import { AppServer } from '../../src/server.js'; import { McpSettings } from '../../src/types/index.js'; import * as fs from 'fs'; import * as path from 'path'; import { createMockSettings } from './mockSettings.js'; import { clearSettingsCache } from '../../src/config/index.js'; /** * Test server helper class for managing AppServer instances during testing */ export class TestServerHelper { private appServer: AppServer | null = null; private httpServer: Server | null = null; private originalConfigPath: string | null = null; private testConfigPath: string | null = null; /** * Creates and initializes a test server with mock settings * @param mockSettings Optional mock settings to use * @returns Object containing server instance and base URL */ async createTestServer(mockSettings?: McpSettings): Promise<{ appServer: AppServer; httpServer: Server; baseURL: string; port: number; }> { // Use provided mock settings or create default ones const settings = mockSettings || createMockSettings(); // Create temporary config file for testing await this.setupTemporaryConfig(settings); // Create and initialize AppServer this.appServer = new AppServer(); await this.appServer.initialize(); // Wait for server connection with timeout const maxAttempts = 30; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (this.appServer.connected()) { console.log('Test server is ready'); break; } else if (attempt === maxAttempts - 1) { throw new Error('Test server did not become ready in time'); } console.log(`Waiting for test server to be ready... Attempt ${attempt + 1}/${maxAttempts}`); await delay(3000); // Short delay between checks } // Start server on random available port const app = this.appServer.getApp(); this.httpServer = app.listen(0); const address = this.httpServer.address(); const port = typeof address === 'object' && address ? address.port : 3000; const baseURL = `http://localhost:${port}`; return { appServer: this.appServer, httpServer: this.httpServer, baseURL, port, }; } /** * Closes the test server and cleans up temporary files */ async closeTestServer(): Promise { if (this.httpServer) { await new Promise((resolve) => { this.httpServer!.close(() => resolve()); }); this.httpServer = null; } this.appServer = null; // Clean up temporary config file await this.cleanupTemporaryConfig(); } /** * Sets up a temporary config file for testing * @param settings Mock settings to write to the config file */ private async setupTemporaryConfig(settings: McpSettings): Promise { // Store original path if it exists this.originalConfigPath = process.env.MCPHUB_SETTING_PATH || null; const configDir = path.join(process.cwd(), 'temp-test-config'); // Create temp config directory if it doesn't exist if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } this.testConfigPath = path.join(configDir, 'mcp_settings.json'); // Write mock settings to temporary file fs.writeFileSync(this.testConfigPath, JSON.stringify(settings, null, 2)); // Override the settings path for the test process.env.MCPHUB_SETTING_PATH = this.testConfigPath; // Clear settings cache to force re-reading from the new config file clearSettingsCache(); console.log(`Set test config path: ${this.testConfigPath}`); } /** * Cleans up the temporary config file */ private async cleanupTemporaryConfig(): Promise { if (this.testConfigPath && fs.existsSync(this.testConfigPath)) { fs.unlinkSync(this.testConfigPath); // Try to remove the temp directory if empty const configDir = path.dirname(this.testConfigPath); try { fs.rmdirSync(configDir); } catch (error) { // Ignore error if directory is not empty } } // Reset environment variable if (this.originalConfigPath !== null) { process.env.MCPHUB_SETTING_PATH = this.originalConfigPath; } else { delete process.env.MCPHUB_SETTING_PATH; } this.testConfigPath = null; } } /** * Waits for a server to be ready by attempting to connect * @param baseURL Base URL of the server * @param maxAttempts Maximum number of connection attempts * @param delay Delay between attempts in milliseconds */ export const waitForServerReady = async ( baseURL: string, maxAttempts = 10, delay = 500, ): Promise => { for (let i = 0; i < maxAttempts; i++) { try { const response = await fetch(`${baseURL}/health`); if (response.ok || response.status === 404) { return; // Server is responding } } catch (error) { // Server not ready yet } if (i < maxAttempts - 1) { await new Promise((resolve) => setTimeout(resolve, delay)); } } throw new Error(`Server at ${baseURL} not ready after ${maxAttempts} attempts`); }; /** * Creates a promise that resolves after the specified delay * @param ms Delay in milliseconds */ export const delay = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); };