118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
|
|
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<string, string>;
|
||
|
|
destroy(): Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
function run(command: string, args: string[], environment: NodeJS.ProcessEnv): Promise<string> {
|
||
|
|
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<E2EDatabaseEnvironment> {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|