Files
edr-platform/integration/scripts/it.mjs
2026-08-03 11:58:33 +00:00

174 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Freight integration-suite launcher.
*
* node integration/scripts/it.mjs <up|test|down|logs> [vitest args...]
*
* Overlays docker-compose.it.yaml on docker-compose.e2e.yaml: same freight
* stack, but the payment microservice is real and only the bank gateways are
* stubbed. Web/Cypress containers are never started — this suite is HTTP only.
*
* Ports are fixed (and distinct from the Cypress e2e defaults) so both stacks
* can be up at once; they are separate compose projects.
*
* No dependencies — plain Node spawning `docker compose` and `pnpm`.
*/
import { execFileSync, spawnSync } from "node:child_process";
import { generateKeyPairSync } from "node:crypto";
import { existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const itDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolve(itDir, "..");
const composeBase = [
"compose",
"-f",
join(repoRoot, "docker-compose.e2e.yaml"),
"-f",
join(repoRoot, "docker-compose.it.yaml"),
];
/** Deliberately offset from the Cypress stack's defaults (3101/5533/9310…). */
const PORTS = {
E2E_API_PORT: 3111,
E2E_DB_PORT: 5543,
E2E_MINIO_PORT: 9320,
E2E_MINIO_CONSOLE_PORT: 9321,
// Unused here (no web containers) but referenced by the base file's build args.
E2E_PORTAL_PORT: 5393,
E2E_BACKOFFICE_PORT: 5394,
IT_PAYMENT_PORT: 3113,
IT_GATEWAY_PORT: 4600,
IT_RABBIT_PORT: 5772,
IT_RABBIT_UI_PORT: 15772,
};
/** Everything the suite needs up — web + cypress are deliberately absent. */
const SERVICES = [
"postgres-freight-e2e",
"minio-e2e",
"minio-init-e2e",
"freight-migration-e2e",
"fayda-mock-e2e",
"etrade-mock-e2e",
// Still a base-stack dependency of freight-api-e2e (reconcile-before-expire
// has its own client); cheap to run alongside the real payment API.
"payment-mock-e2e",
"gateway-mock-it",
"rabbitmq-it",
"payment-api-it",
"freight-api-e2e",
];
const RUNNING = SERVICES.filter(
(s) => !["minio-init-e2e", "freight-migration-e2e"].includes(s),
);
function fail(msg) {
console.error(`\nit: ${msg}`);
process.exit(1);
}
function preflight() {
try {
execFileSync("docker", ["info"], { stdio: "ignore" });
} catch {
fail("docker is not running (or not installed) — start Docker and retry.");
}
if (!existsSync(join(repoRoot, ".npmrc"))) {
fail(".npmrc missing at repo root — image builds need GitHub Packages auth for @tria-plc.");
}
}
/** Throwaway RSA PEM — Telebirr PSS-signs every request object; the mock never
* verifies it, but the provider refuses to build a request without a real key. */
function fakeTelebirrPrivateKey() {
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
}
/** Throwaway RSA JWK for FAYDA_PRIVATE_KEY_BASE64 (see e2e.mjs — same reason). */
function fakeFaydaPrivateKeyBase64() {
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const jwk = privateKey.export({ format: "jwk" });
Object.assign(jwk, { kty: "RSA", use: "sig", alg: "RS256", kid: "it-fayda-mock" });
return Buffer.from(JSON.stringify(jwk)).toString("base64");
}
const env = {
...process.env,
...Object.fromEntries(Object.entries(PORTS).map(([k, v]) => [k, String(v)])),
IT_API_URL: `http://localhost:${PORTS.E2E_API_PORT}`,
IT_PAYMENT_URL: `http://localhost:${PORTS.IT_PAYMENT_PORT}`,
IT_GATEWAY_URL: `http://localhost:${PORTS.IT_GATEWAY_PORT}`,
IT_DB_URL: `postgres://edr_e2e:edr_e2e@localhost:${PORTS.E2E_DB_PORT}/edr_freight_e2e`,
FAYDA_PRIVATE_KEY_BASE64:
process.env.FAYDA_PRIVATE_KEY_BASE64 ?? fakeFaydaPrivateKeyBase64(),
IT_TELEBIRR_PRIVATE_KEY:
process.env.IT_TELEBIRR_PRIVATE_KEY ?? fakeTelebirrPrivateKey(),
};
function compose(args) {
const { status } = spawnSync("docker", [...composeBase, ...args], { stdio: "inherit", env });
return status ?? 1;
}
function stackRunning() {
try {
const out = execFileSync("docker", [...composeBase, "ps", "--services", "--status", "running"], {
encoding: "utf8",
env,
stdio: ["ignore", "pipe", "ignore"],
});
const running = new Set(out.split("\n").filter(Boolean));
return RUNNING.every((s) => running.has(s));
} catch {
return false;
}
}
function up() {
preflight();
console.log(
`it: starting stack — freight :${PORTS.E2E_API_PORT} payment :${PORTS.IT_PAYMENT_PORT} ` +
`gateway :${PORTS.IT_GATEWAY_PORT} db :${PORTS.E2E_DB_PORT}`,
);
if (compose(["up", "-d", "--build", "--wait", ...SERVICES]) !== 0) {
fail(
"stack failed to become healthy. Inspect with:\n" +
" node integration/scripts/it.mjs logs payment-api-it",
);
}
}
const [cmd, ...rawExtra] = process.argv.slice(2);
// `pnpm it:test -- src/foo.it.ts` hands us a literal "--" first. Forwarding it
// makes vitest treat everything after it as CLI options and ignore the file
// filter — the "one file" run silently becomes the whole suite.
const extra = rawExtra[0] === "--" ? rawExtra.slice(1) : rawExtra;
switch (cmd) {
case "up":
up();
break;
case "test": {
if (!stackRunning()) up();
const { status } = spawnSync(
"pnpm",
["--filter", "@edr/freight-integration", "run", "test", ...extra],
{ cwd: repoRoot, stdio: "inherit", env },
);
process.exit(status ?? 1);
}
case "logs":
process.exit(compose(["logs", "--tail", "200", ...extra]));
break;
case "down":
process.exit(compose(["down", "-v", "--remove-orphans"]));
break;
default:
fail(`unknown command "${cmd ?? ""}" — use up | test | logs | down`);
}