import { spawn } from "node:child_process"; import { randomBytes, randomUUID } from "node:crypto"; import { E2E_DATABASE_PREFIX } from "./e2e-contract"; const composeFile = "tests/e2e/docker-compose.yml"; export interface E2EDatabaseEnvironment { readonly executionId: string; readonly databaseName: string; readonly projectName: string; readonly containerId: string; readonly host: string; readonly port: number; readonly username: string; readonly password: string; readonly environment: Record; destroy(): Promise; } function run(command: string, args: string[], environment: NodeJS.ProcessEnv): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { env: environment, stdio: ["ignore", "pipe", "pipe"] }); let output = ""; let errorOutput = ""; child.stdout.on("data", (chunk: Buffer) => (output += chunk)); child.stderr.on("data", (chunk: Buffer) => (errorOutput += chunk)); child.on("error", reject); child.on("close", (code) => code === 0 ? resolve(output.trim()) : reject(new Error(`${command} ${args.join(" ")}: ${errorOutput}`)) ); }); } /** Provisiona un Ășnico proyecto Compose y elimina exclusivamente ese proyecto. */ export async function createE2EDatabaseEnvironment(): Promise { const runId = randomUUID().replaceAll("-", ""); const databaseName = `${E2E_DATABASE_PREFIX}${runId}`; const projectName = `factuges_e2e_${runId}`; const user = "e2e"; const password = randomBytes(24).toString("hex"); const rootPassword = randomBytes(24).toString("hex"); const composeEnvironment = { ...process.env, E2E_DB_NAME: databaseName, E2E_DB_USER: user, E2E_DB_PASS: password, E2E_DB_ROOT_PASS: rootPassword, }; const composeArgs = ["compose", "--project-name", projectName, "--file", composeFile]; try { await run("docker", [...composeArgs, "up", "--detach", "--wait"], composeEnvironment); const containerId = await run( "docker", [...composeArgs, "ps", "--quiet", "db"], composeEnvironment ); const portOutput = await run( "docker", [...composeArgs, "port", "db", "3306"], composeEnvironment ); const port = portOutput.match(/:(\d+)$/)?.[1]; if (!(containerId && port)) { throw new Error("Compose did not return the E2E MariaDB container and port."); } const environment = { DB_HOST: "127.0.0.1", DB_PORT: port, DB_NAME: databaseName, DB_USER: user, DB_PASS: password, }; await run( "docker", [ "exec", containerId, "mariadb", `--user=${user}`, `--password=${password}`, databaseName, "--execute=SELECT 1", ], composeEnvironment ); return { executionId: runId, databaseName, projectName, containerId, host: "127.0.0.1", port: Number(port), username: user, password, environment, destroy: async () => run( "docker", [...composeArgs, "down", "--volumes", "--remove-orphans"], composeEnvironment ).then(() => undefined), }; } catch (error) { await run( "docker", [...composeArgs, "down", "--volumes", "--remove-orphans"], composeEnvironment ).catch(() => undefined); throw error; } }