File size: 5,380 Bytes
07af8f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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<void> {
    if (this.httpServer) {
      await new Promise<void>((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<void> {
    // 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<void> {
    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<void> => {
  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<void> => {
  return new Promise((resolve) => setTimeout(resolve, ms));
};