/** * Docker Manager for Integration Tests * Manages Docker Compose services for integration testing */ import { execSync, spawn } from 'child_process'; import { setTimeout } from 'timers/promises'; export interface DockerServiceConfig { name: string; port: number; healthCheck: string; timeout?: number; } export class DockerManager { private static instance: DockerManager; private services: Map = new Map(); private composeProject = 'gridpilot-test'; private composeFile = 'docker-compose.test.yml'; private constructor() {} static getInstance(): DockerManager { if (!DockerManager.instance) { DockerManager.instance = new DockerManager(); } return DockerManager.instance; } /** * Check if Docker services are already running */ isRunning(): boolean { try { const output = execSync( `docker-compose -p ${this.composeProject} -f ${this.composeFile} ps -q 2>/dev/null || true`, { encoding: 'utf8' } ).trim(); return output.length > 0; } catch { return false; } } /** * Start Docker services with dependency checking */ async start(): Promise { console.log('[DockerManager] Starting test environment...'); if (this.isRunning()) { console.log('[DockerManager] Services already running, checking health...'); await this.waitForServices(); return; } // Start services execSync( `COMPOSE_PARALLEL_LIMIT=1 docker-compose -p ${this.composeProject} -f ${this.composeFile} up -d ready api`, { stdio: 'inherit' } ); console.log('[DockerManager] Services starting, waiting for health...'); await this.waitForServices(); } /** * Wait for all services to be healthy using polling */ async waitForServices(): Promise { const services: DockerServiceConfig[] = [ { name: 'db', port: 5433, healthCheck: 'pg_isready -U gridpilot_test_user -d gridpilot_test', timeout: 60000 }, { name: 'api', port: 3101, healthCheck: 'curl -f http://localhost:3101/health', timeout: 90000 } ]; for (const service of services) { await this.waitForService(service); } } /** * Wait for a single service to be healthy */ async waitForService(config: DockerServiceConfig): Promise { const timeout = config.timeout || 30000; const startTime = Date.now(); console.log(`[DockerManager] Waiting for ${config.name}...`); while (Date.now() - startTime < timeout) { try { // Try health check command if (config.name === 'db') { // For DB, check if it's ready to accept connections try { execSync( `docker exec ${this.composeProject}-${config.name}-1 ${config.healthCheck} 2>/dev/null`, { stdio: 'pipe' } ); console.log(`[DockerManager] ✓ ${config.name} is healthy`); return; } catch {} } else { // For API, check HTTP endpoint const response = await fetch(`http://localhost:${config.port}/health`); if (response.ok) { console.log(`[DockerManager] ✓ ${config.name} is healthy`); return; } } } catch (error) { // Service not ready yet, continue waiting } await setTimeout(1000); } throw new Error(`[DockerManager] ${config.name} failed to become healthy within ${timeout}ms`); } /** * Stop Docker services */ stop(): void { console.log('[DockerManager] Stopping test environment...'); try { execSync( `docker-compose -p ${this.composeProject} -f ${this.composeFile} down --remove-orphans`, { stdio: 'inherit' } ); } catch (error) { console.warn('[DockerManager] Warning: Failed to stop services cleanly:', error); } } /** * Clean up volumes and containers */ clean(): void { console.log('[DockerManager] Cleaning up test environment...'); try { execSync( `docker-compose -p ${this.composeProject} -f ${this.composeFile} down -v --remove-orphans --volumes`, { stdio: 'inherit' } ); } catch (error) { console.warn('[DockerManager] Warning: Failed to clean up cleanly:', error); } } /** * Execute a command in a service container */ execInService(service: string, command: string): string { try { return execSync( `docker exec ${this.composeProject}-${service}-1 ${command}`, { encoding: 'utf8' } ); } catch (error) { throw new Error(`Failed to execute command in ${service}: ${error}`); } } /** * Get service logs */ getLogs(service: string): string { try { return execSync( `docker logs ${this.composeProject}-${service}-1 --tail 100`, { encoding: 'utf8' } ); } catch (error) { return `Failed to get logs: ${error}`; } } }