@devix-tecnologia/directus-extension-inframe
Version:
DEPRECATED: Use 'directus-extension-inframe' instead. This package is no longer maintained.
268 lines (220 loc) • 8.83 kB
text/typescript
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<string> {
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<string, string>,
testSuiteId?: string,
): Promise<any> {
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));
}
}
}