feat: enhance JSON serialization safety & add dxt upload limit (#230)

This commit is contained in:
samanhappy
2025-07-20 19:18:10 +08:00
committed by GitHub
parent 4388084704
commit 20fd355b87
49 changed files with 3068 additions and 378 deletions

107
tests/utils/mockSettings.ts Normal file
View 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();
}
};

View 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));
};