import { exec } from 'child_process'; import { promisify } from 'util'; import { setupTestEnv, testEnv } from './test-env.js'; import { logger } from './test-logger.js'; const execAsync = promisify(exec); // Detecta qual comando docker compose usar (docker-compose ou docker compose) let dockerComposeCommand: string | null = null; async function getDockerComposeCommand(): Promise { if (dockerComposeCommand) { return dockerComposeCommand; } // Tenta docker compose primeiro (novo padrão) try { await execAsync('docker compose version'); dockerComposeCommand = 'docker compose'; logger.debug('Using: docker compose'); return dockerComposeCommand; } catch { // Fallback para docker-compose (versão antiga) try { await execAsync('docker-compose version'); dockerComposeCommand = 'docker-compose'; logger.debug('Using: docker-compose'); return dockerComposeCommand; } catch { throw new Error('Neither "docker compose" nor "docker-compose" found'); } } } // Função para fazer requisições HTTP via Node.js dentro do container export async function dockerHttpRequest( method: string, path: string, data?: any, headers?: Record, testSuiteId?: string, ): Promise { const suiteId = testSuiteId || process.env.TEST_SUITE_ID || 'main'; const containerName = `directus-inframe-${suiteId}-${process.env.DIRECTUS_VERSION}`; // Cria um script Node.js para fazer a requisição HTTP const headersJson = JSON.stringify(headers || {}).replace(/"/g, '\\"'); const dataJson = data ? JSON.stringify(data).replace(/"/g, '\\"') : ''; const nodeScript = ` const http = require('http'); const options = { hostname: '127.0.0.1', port: 8055, path: '${path}', method: '${method}', headers: JSON.parse("${headersJson}") }; ${data ? `const postData = "${dataJson}"; options.headers['Content-Type'] = 'application/json'; options.headers['Content-Length'] = Buffer.byteLength(postData);` : ''} const req = http.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log(data); }); }); req.on('error', (error) => { console.error(JSON.stringify({error: error.message})); process.exit(1); }); ${data ? `req.write(postData);` : ''} req.end(); `; const escapedScript = nodeScript.replace(/\n/g, ' ').replace(/'/g, "'\\''"); const fullCommand = `docker exec ${containerName} node -e '${escapedScript}'`; try { const { stdout } = await execAsync(fullCommand); // Se stdout estiver vazio, retornar objeto vazio ao invés de tentar fazer parse if (!stdout || stdout.trim() === '') { return {}; } return JSON.parse(stdout); } catch (error: any) { logger.error('Docker HTTP request failed:', error); throw error; } } async function cleanupDocker(_testSuiteId: string) { try { logger.debug('Cleaning up test containers...'); // Para containers que possam estar rodando try { const composeCmd = await getDockerComposeCommand(); await execAsync( `TEST_SUITE_ID=${_testSuiteId} DIRECTUS_VERSION=${process.env.DIRECTUS_VERSION} ${composeCmd} -f docker-compose.test.yml down --remove-orphans --volumes 2>/dev/null || true`, ); } catch (error) { logger.debug('Error stopping containers (may not exist):', error); } // Remove containers forçadamente se necessário const containerName = `directus-inframe-${_testSuiteId}-${process.env.DIRECTUS_VERSION}`; try { await execAsync(`docker rm -f ${containerName} 2>/dev/null || true`); } catch (error) { logger.debug('Container does not exist or already removed', error); } // Remove network se existir const networkName = `directus-inframe-test-${_testSuiteId}-${process.env.DIRECTUS_VERSION}`; try { await execAsync(`docker network rm ${networkName} 2>/dev/null || true`); } catch (error) { logger.debug('Network does not exist or already removed', error); } // Aguarda um pouco para garantir que as portas foram liberadas await new Promise((resolve) => setTimeout(resolve, 1000)); logger.debug('Test containers removed'); } catch (error) { logger.warn('Warning while cleaning test containers:', error); } } async function waitForContainerHealth(containerName: string, retries = 100, delay = 2000) { for (let i = 0; i < retries; i++) { try { const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' ${containerName}`); const healthStatus = stdout.trim(); if (healthStatus === 'healthy') { logger.info(`Container ${containerName} is healthy`); return; } logger.debug(`Container health: ${healthStatus} (attempt ${i + 1}/${retries})`); } catch { logger.debug(`Waiting for container to be created (attempt ${i + 1}/${retries})`); } await new Promise((resolve) => setTimeout(resolve, delay)); } throw new Error(`Container ${containerName} did not become healthy`); } export async function setupTestEnvironment(testSuiteId: string = 'main') { try { // Limpa o ambiente Docker await cleanupDocker(testSuiteId); // Configura o ambiente de teste setupTestEnv(); // Start Docker containers logger.info('Starting test environment...'); const composeCmd = await getDockerComposeCommand(); const { stdout, stderr } = await execAsync( `TEST_SUITE_ID=${testSuiteId} DIRECTUS_VERSION=${process.env.DIRECTUS_VERSION} ${composeCmd} -f docker-compose.test.yml up -d`, ); // Docker Compose uses stderr for progress messages const realError = stderr && !stderr.includes('Creating') && !stderr.includes('Starting'); if (realError) { logger.error('Docker Compose error:', stderr); } else if (stdout || stderr) { logger.dockerProgress(stdout || stderr); } // Wait for container to be healthy (using docker healthcheck) logger.info('Waiting for container to be healthy...'); const containerName = `directus-inframe-${testSuiteId}-${process.env.DIRECTUS_VERSION}`; await waitForContainerHealth(containerName); // Wait for Directus to be ready logger.info('Waiting for Directus to be ready...'); await waitForBootstrap(testSuiteId); // Login to get admin access token via docker exec const loginResponse = await dockerHttpRequest( 'POST', '/auth/login', { email: testEnv.DIRECTUS_ADMIN_EMAIL, password: testEnv.DIRECTUS_ADMIN_PASSWORD, }, undefined, testSuiteId, ); const accessToken = loginResponse.data?.access_token || loginResponse.access_token; // Set access token for tests process.env.DIRECTUS_ACCESS_TOKEN = accessToken; return accessToken; } catch (error) { logger.error('Failed to setup test environment:', error); throw error; } } export async function teardownTestEnvironment(_testSuiteId: string = 'main') { try { logger.info('Shutting down test environment...'); // NOTE: The automatic teardown is commented out so the test containers // are left running for manual inspection after tests complete. To re-enable // automatic teardown, uncomment the lines below. // await execAsync( // `TEST_SUITE_ID=${_testSuiteId} DIRECTUS_VERSION=${process.env.DIRECTUS_VERSION} docker-compose -f docker-compose.test.yml down --remove-orphans`, // ); } catch (error) { logger.error('Erro ao finalizar ambiente de teste:', error); throw error; } } async function waitForBootstrap(testSuiteId: string, retries = 90, delay = 2000) { for (let i = 0; i < retries; i++) { try { logger.debug(`Connection attempt ${i + 1}/${retries}`); // Check if server is responding via docker exec const healthCheck = await dockerHttpRequest('GET', '/server/health', undefined, undefined, testSuiteId); if (healthCheck.status !== 'ok') { throw new Error('Health check failed'); } // Try to login to verify if the system is fully ready try { await dockerHttpRequest( 'POST', '/auth/login', { email: testEnv.DIRECTUS_ADMIN_EMAIL, password: testEnv.DIRECTUS_ADMIN_PASSWORD, }, undefined, testSuiteId, ); logger.info('Directus is ready and accepting authentication'); return; } catch { throw new Error('System not ready for authentication'); } } catch (error: any) { if (i === retries - 1) { logger.error('Failed to connect to Directus', error); throw new Error('Directus failed to start'); } await new Promise((resolve) => setTimeout(resolve, delay)); } } }