mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: enhance JSON serialization safety & add dxt upload limit (#230)
This commit is contained in:
465
tests/integration/sse-service-real-client.test.ts
Normal file
465
tests/integration/sse-service-real-client.test.ts
Normal file
@@ -0,0 +1,465 @@
|
||||
import { Server } from 'http';
|
||||
import { AppServer } from '../../src/server.js';
|
||||
import { TestServerHelper } from '../utils/testServerHelper.js';
|
||||
import * as mockSettings from '../utils/mockSettings.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { cleanupAllServers } from '../../src/services/mcpService.js';
|
||||
|
||||
describe('Real Client Transport Integration Tests', () => {
|
||||
let _appServer: AppServer;
|
||||
let httpServer: Server;
|
||||
let baseURL: string;
|
||||
let testServerHelper: TestServerHelper;
|
||||
|
||||
beforeAll(async () => {
|
||||
const settings = mockSettings.createMockSettings();
|
||||
testServerHelper = new TestServerHelper();
|
||||
const result = await testServerHelper.createTestServer(settings);
|
||||
|
||||
_appServer = result.appServer;
|
||||
httpServer = result.httpServer;
|
||||
baseURL = result.baseURL;
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up all MCP server connections first
|
||||
cleanupAllServers();
|
||||
|
||||
// Close the test server properly using the helper
|
||||
if (testServerHelper) {
|
||||
await testServerHelper.closeTestServer();
|
||||
} else if (httpServer) {
|
||||
// Fallback to direct close if helper is not available
|
||||
await new Promise<void>((resolve) => {
|
||||
httpServer.close(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
// Wait a bit to ensure all async operations complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
describe('SSE Client Transport Tests', () => {
|
||||
it('should connect using real SSEClientTransport', async () => {
|
||||
const sseUrl = new URL(`${baseURL}/sse`);
|
||||
const options = {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: 'Bearer test-auth-token-123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const transport = new SSEClientTransport(sseUrl, options);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'real-sse-test-client',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let isConnected = false;
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await client.connect(transport, {});
|
||||
isConnected = true;
|
||||
console.log('SSE Client connected successfully');
|
||||
|
||||
// Test list tools
|
||||
const tools = await client.listTools({});
|
||||
console.log('Available tools (SSE):', JSON.stringify(tools, null, 2));
|
||||
|
||||
await client.close();
|
||||
console.log('SSE Client closed successfully');
|
||||
} catch (err) {
|
||||
error = err;
|
||||
console.error('SSE Client test failed:', err);
|
||||
|
||||
if (isConnected) {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (closeErr) {
|
||||
console.error('Error closing client:', closeErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(isConnected).toBe(true);
|
||||
}, 30000);
|
||||
|
||||
it('should connect using real SSEClientTransport with group', async () => {
|
||||
const testGroup = 'integration-test-group';
|
||||
const options = {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: 'Bearer test-auth-token-123',
|
||||
},
|
||||
},
|
||||
};
|
||||
const sseUrl = new URL(`${baseURL}/sse/${testGroup}`);
|
||||
|
||||
const transport = new SSEClientTransport(sseUrl, options);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'real-sse-group-test-client',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let isConnected = false;
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await client.connect(transport, {});
|
||||
isConnected = true;
|
||||
|
||||
console.log(`SSE Client with group ${testGroup} connected successfully`);
|
||||
|
||||
// Test basic operations
|
||||
const tools = await client.listTools({});
|
||||
console.log('Available tools (SSE with group):', JSON.stringify(tools, null, 2));
|
||||
|
||||
await client.close();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
console.error('SSE Client with group test failed:', err);
|
||||
|
||||
if (isConnected) {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (closeErr) {
|
||||
console.error('Error closing client:', closeErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(isConnected).toBe(true);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('StreamableHTTP Client Transport Tests', () => {
|
||||
it('should connect using real StreamableHTTPClientTransport', async () => {
|
||||
const mcpUrl = new URL(`${baseURL}/mcp`);
|
||||
const options: any = {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: `Bearer test-auth-token-123`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(mcpUrl, options);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'real-http-test-client',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let isConnected = false;
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await client.connect(transport, {});
|
||||
isConnected = true;
|
||||
console.log('HTTP Client connected successfully');
|
||||
|
||||
// Test list tools
|
||||
const tools = await client.listTools({});
|
||||
console.log('Available tools (HTTP):', JSON.stringify(tools, null, 2));
|
||||
|
||||
await client.close();
|
||||
console.log('HTTP Client closed successfully');
|
||||
} catch (err) {
|
||||
error = err;
|
||||
console.error('HTTP Client test failed:', err);
|
||||
|
||||
if (isConnected) {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (closeErr) {
|
||||
console.error('Error closing client:', closeErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(isConnected).toBe(true);
|
||||
}, 30000);
|
||||
|
||||
it('should connect using real StreamableHTTPClientTransport with group', async () => {
|
||||
const testGroup = 'integration-test-group';
|
||||
const mcpUrl = new URL(`${baseURL}/mcp/${testGroup}`);
|
||||
const options: any = {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: `Bearer test-auth-token-123`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(mcpUrl, options);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'real-http-group-test-client',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let isConnected = false;
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await client.connect(transport, {});
|
||||
isConnected = true;
|
||||
|
||||
console.log(`HTTP Client with group ${testGroup} connected successfully`);
|
||||
|
||||
// Test basic operations
|
||||
const tools = await client.listTools({});
|
||||
console.log('Available tools (HTTP with group):', JSON.stringify(tools, null, 2));
|
||||
|
||||
await client.close();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
console.error('HTTP Client with group test failed:', err);
|
||||
|
||||
if (isConnected) {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (closeErr) {
|
||||
console.error('Error closing client:', closeErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(isConnected).toBe(true);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('Real Client Authentication Tests', () => {
|
||||
let _authAppServer: AppServer;
|
||||
let _authHttpServer: Server;
|
||||
let authBaseURL: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const authSettings = mockSettings.createMockSettingsWithAuth();
|
||||
const authTestServerHelper = new TestServerHelper();
|
||||
const authResult = await authTestServerHelper.createTestServer(authSettings);
|
||||
|
||||
_authAppServer = authResult.appServer;
|
||||
_authHttpServer = authResult.httpServer;
|
||||
authBaseURL = authResult.baseURL;
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (_authHttpServer) {
|
||||
_authHttpServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail to connect with SSEClientTransport without auth', async () => {
|
||||
const sseUrl = new URL(`${authBaseURL}/sse`);
|
||||
const options = {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: 'Bearer test-auth-token-123',
|
||||
},
|
||||
},
|
||||
};
|
||||
const transport = new SSEClientTransport(sseUrl, options);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'real-sse-test-client-no-auth',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await client.connect(transport, {});
|
||||
|
||||
// Should not reach here due to auth failure
|
||||
await client.listTools({});
|
||||
|
||||
await client.close();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
console.log('Expected auth error:', err);
|
||||
|
||||
try {
|
||||
await client.close();
|
||||
} catch (closeErr) {
|
||||
// Ignore close errors after connection failure
|
||||
}
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
if (error) {
|
||||
expect(error.message).toContain('401');
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it('should connect with SSEClientTransport with valid auth', async () => {
|
||||
const sseUrl = new URL(`${authBaseURL}/sse`);
|
||||
|
||||
const options = {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: 'Bearer test-auth-token-123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const transport = new SSEClientTransport(sseUrl, options);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'real-sse-auth-test-client',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let isConnected = false;
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await client.connect(transport, {});
|
||||
isConnected = true;
|
||||
console.log('SSE Client with auth connected successfully');
|
||||
|
||||
// Test basic operations
|
||||
const tools = await client.listTools({});
|
||||
console.log('Available tools (SSE with auth):', JSON.stringify(tools, null, 2));
|
||||
|
||||
await client.close();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
console.error('SSE Client with auth test failed:', err);
|
||||
|
||||
if (isConnected) {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (closeErr) {
|
||||
console.error('Error closing client:', closeErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(isConnected).toBe(true);
|
||||
}, 30000);
|
||||
|
||||
it('should connect with StreamableHTTPClientTransport with auth', async () => {
|
||||
const mcpUrl = new URL(`${authBaseURL}/mcp`);
|
||||
|
||||
const options = {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: 'Bearer test-auth-token-123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(mcpUrl, options);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'real-http-auth-test-client',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let isConnected = false;
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await client.connect(transport, {});
|
||||
isConnected = true;
|
||||
|
||||
console.log('HTTP Client with auth connected successfully');
|
||||
|
||||
// Test basic operations
|
||||
const tools = await client.listTools({});
|
||||
console.log('Available tools (HTTP with auth):', JSON.stringify(tools, null, 2));
|
||||
|
||||
await client.close();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
console.error('HTTP Client with auth test failed:', err);
|
||||
|
||||
if (isConnected) {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (closeErr) {
|
||||
console.error('Error closing client:', closeErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(isConnected).toBe(true);
|
||||
}, 30000);
|
||||
});
|
||||
});
|
||||
107
tests/utils/mockSettings.ts
Normal file
107
tests/utils/mockSettings.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { McpSettings, ServerConfig, SystemConfig, IGroup, IUser } from '../../src/types/index.js';
|
||||
|
||||
/**
|
||||
* Creates mock MCP settings for testing
|
||||
* @param overrides Optional configuration overrides
|
||||
* @returns Mock McpSettings object
|
||||
*/
|
||||
export const createMockSettings = (overrides: Partial<McpSettings> = {}): McpSettings => {
|
||||
const defaultSettings: McpSettings = {
|
||||
mcpServers: {
|
||||
'test-server-1': {
|
||||
command: 'npx',
|
||||
args: ['-y', 'time-mcp'],
|
||||
env: {},
|
||||
enabled: true,
|
||||
keepAliveInterval: 30000,
|
||||
type: 'stdio',
|
||||
} as ServerConfig,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
name: 'integration-test-group',
|
||||
servers: ['test-server-1'],
|
||||
description: 'Test group for integration tests',
|
||||
owner: 'admin',
|
||||
} as IGroup,
|
||||
],
|
||||
systemConfig: {
|
||||
routing: {
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true,
|
||||
enableBearerAuth: true,
|
||||
bearerAuthKey: 'test-auth-token-123',
|
||||
},
|
||||
} as SystemConfig,
|
||||
users: [
|
||||
{
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
isAdmin: false,
|
||||
} as IUser,
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
...defaultSettings,
|
||||
...overrides,
|
||||
mcpServers: {
|
||||
...defaultSettings.mcpServers,
|
||||
...(overrides.mcpServers || {}),
|
||||
},
|
||||
groups: [...(defaultSettings.groups || []), ...(overrides.groups || [])],
|
||||
systemConfig: {
|
||||
...defaultSettings.systemConfig,
|
||||
...(overrides.systemConfig || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates mock settings with bearer authentication enabled
|
||||
*/
|
||||
export const createMockSettingsWithAuth = (bearerKey = 'test-auth-token-123'): McpSettings => {
|
||||
return createMockSettings({
|
||||
systemConfig: {
|
||||
routing: {
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true,
|
||||
enableBearerAuth: true,
|
||||
bearerAuthKey: bearerKey,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates mock settings with global routes disabled
|
||||
*/
|
||||
export const createMockSettingsNoGlobalRoutes = (): McpSettings => {
|
||||
return createMockSettings({
|
||||
systemConfig: {
|
||||
routing: {
|
||||
enableGlobalRoute: false,
|
||||
enableGroupNameRoute: true,
|
||||
enableBearerAuth: false,
|
||||
bearerAuthKey: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock settings helper for specific test scenarios
|
||||
*/
|
||||
export const getMockSettingsForScenario = (
|
||||
scenario: 'auth' | 'no-global' | 'basic',
|
||||
): McpSettings => {
|
||||
switch (scenario) {
|
||||
case 'auth':
|
||||
return createMockSettingsWithAuth();
|
||||
case 'no-global':
|
||||
return createMockSettingsNoGlobalRoutes();
|
||||
case 'basic':
|
||||
default:
|
||||
return createMockSettings();
|
||||
}
|
||||
};
|
||||
176
tests/utils/testServerHelper.ts
Normal file
176
tests/utils/testServerHelper.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
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));
|
||||
};
|
||||
Reference in New Issue
Block a user