/** * The freight API, booted IN THIS PROCESS — one instance per vitest worker. * * Each worker is a self-contained shard of the whole topology, so spec files can * run in parallel without sharing anything mutable: * * shard i = this in-process freight app on :3111+i * + database edr_it_s{i} (cloned from the seeded template) * + payment-api-it-{i} container on :3113+i, DB_NAME=edr_it_s{i} * + gateway-mock-it-{i} container on :4600+i * + RabbitMQ vhost payment_s{i} * * The payment schema lives INSIDE the shard's own database under the usual * `edr_payment` name, so every raw `edr_payment.*` query in the specs works * unchanged and `paymentDb()` stays an alias for `db()`. * * Why the app is loaded from `dist/` and not from TypeScript: the freight source * is CJS-flavoured — `src/config/database.config.ts` uses `__dirname`, * `require.resolve` and an entity glob, none of which survive vitest's ESM * transform. `createRequire` anchored at the freight app's own package.json * keeps all Nest / typeorm / @tria-plc resolution inside its node_modules, and * `dist` is rebuilt by `it.mjs test` on every run so it can never go stale. */ import { createRequire } from "node:module"; import { existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { Server } from "node:http"; const here = dirname(fileURLToPath(import.meta.url)); const freightDir = join(here, "..", "..", "apps", "edr-freight-api"); const distMain = join(freightDir, "dist", "main.js"); /** * `VITEST_POOL_ID` is 1-based and stable per worker for the life of the run. * Absent outside a worker (global-setup, scripts) — that context gets shard 0. */ export const SHARD = Math.max(0, Number(process.env.VITEST_POOL_ID ?? "1") - 1); /** * Each service takes a contiguous block of `IT_SHARDS` ports from its base, so * the bases must stay far apart — see the PORTS comment in scripts/it.mjs. */ const port = (base: string, fallback: number) => Number(process.env[base] ?? fallback) + SHARD; export const API_PORT = port("IT_API_PORT_BASE", 3111); export const PAYMENT_API = `http://localhost:${port("IT_PAYMENT_PORT_BASE", 3131)}`; export const GATEWAY = `http://localhost:${port("IT_GATEWAY_PORT_BASE", 4600)}`; export const SHARD_DB = `${process.env.IT_DB_PREFIX ?? "edr_it_s"}${SHARD}`; export const DB_URL = `postgres://${process.env.DB_USER ?? "edr_e2e"}:${process.env.DB_PASSWORD ?? "edr_e2e"}` + `@${process.env.DB_HOST ?? "localhost"}:${process.env.DB_PORT ?? "5543"}/${SHARD_DB}`; const RABBIT_URL = `amqp://edr:edr_secret@localhost:${process.env.IT_RABBIT_PORT ?? "5772"}` + `/payment_s${SHARD}`; /** Prefix every diagnostic — parallel worker output interleaves. */ export const tag = (msg: string) => `[shard ${SHARD}] ${msg}`; interface FreightMain { createFreightApp: () => Promise<{ listen: (port: number) => Promise; getHttpServer: () => Server; close: () => Promise; }>; } let booted: Promise | undefined; /** * The shard's HTTP server, booted once per worker. supertest takes this * directly in place of a base URL, so every call site in client.ts is a * one-token change. */ export function freightServer(): Promise { return (booted ??= boot()); } async function boot(): Promise { if (!existsSync(distMain)) { throw new Error( tag( `${distMain} is missing — build the API first ` + `(\`pnpm --filter @edr/freight-api run build\`, which \`it.mjs test\` does for you).`, ), ); } // Must be set BEFORE the require: payment.module.ts reads // PAYMENT_RABBITMQ_URL at module-definition time to decide whether // RabbitMQModule is in the graph at all, and database.config.ts reads DB_NAME // when ConfigModule loads it. process.env.DB_NAME = SHARD_DB; process.env.PORT = String(API_PORT); process.env.PAYMENT_API_URL = PAYMENT_API; process.env.PAYMENT_RABBITMQ_URL = RABBIT_URL; const { createFreightApp } = createRequire(join(freightDir, "package.json"))( "./dist/main.js", ) as FreightMain; // Postgres and the broker are up before vitest starts, but a shard's payment // API may still be finishing its own migrations when the first worker boots. // Retry the whole app rather than a connection: a half-initialised Nest app // cannot be resumed, only closed and rebuilt. let lastErr: unknown; for (let attempt = 1; attempt <= 3; attempt++) { const app = await createFreightApp().catch((err) => { lastErr = err; return undefined; }); if (app) { try { // A real port, not an ephemeral one: payment-api-it-{SHARD} calls back // in here for the inbound CBE Unified Bill query (cbe-bill.it.ts). await app.listen(API_PORT); console.log(tag(`freight-api in-process on :${API_PORT} → ${SHARD_DB}`)); return app.getHttpServer(); } catch (err) { lastErr = err; await app.close().catch(() => {}); } } if (attempt < 3) await new Promise((r) => setTimeout(r, 2000 * attempt)); } console.error(tag(`freight-api failed to boot on :${API_PORT} (${SHARD_DB})`), lastErr); throw lastErr; }