import "reflect-metadata"; import { INestApplication, ValidationPipe } from "@nestjs/common"; import { Test } from "@nestjs/testing"; import request from "supertest"; import { closePool } from "@eden/database"; import { AppModule } from "../src/app.module.js"; /** * Shared integration-test bootstrap (eden-qa). Boots the REAL Nest app — * every module, guard, pipe and repository exactly as main.ts wires them — * against the REAL Postgres pointed to by DATABASE_URL. The only thing * skipped versus a full deployment is the separate OS process/container: * HTTP goes through supertest -> app.getHttpServer() (real Express, real * routing, real guards), and the DB goes through the real `pg` Pool (no * mock/in-memory substitute anywhere in this stack). * * Mirrors main.ts's ValidationPipe + global prefix exactly (see * apps/api/src/main.ts) without importing/touching main.ts itself, so * this file can't conflict with other work landing there. */ export async function createTestApp(): Promise { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); const app = moduleRef.createNestApplication(); app.setGlobalPrefix("api", { exclude: ["health/live", "health/ready"] }); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.init(); return app; } export async function closeTestApp(app: INestApplication): Promise { await app.close(); // packages/database's pg Pool is a module-level singleton, not a Nest // provider tied to app.close() — close it explicitly so vitest's process // can exit instead of hanging on an idle DB connection. await closePool(); } /** Base path every controller in this API is mounted under (see main.ts's setGlobalPrefix). */ export const API = "/api"; export function agent(app: INestApplication) { return request(app.getHttpServer()); } export interface AuthedTokens { accessToken: string; refreshToken: string; userId: string; } /** * Logs in for real via POST /api/v1/auth/login (no guard/token mocking * anywhere in this suite). Requires EDEN_SUPERADMIN_EMAIL/PASSWORD to be * set in the environment (apps/api/src/bootstrap/seed.service.ts seeds * this account once at boot, only if `users` was empty when the API first * started) — the same account docker-compose's eden-api container seeds * against the shared dev Postgres. */ export async function loginSuperAdmin(app: INestApplication): Promise { const email = process.env.EDEN_SUPERADMIN_EMAIL; const password = process.env.EDEN_SUPERADMIN_PASSWORD; if (!email || !password) { throw new Error( "EDEN_SUPERADMIN_EMAIL/EDEN_SUPERADMIN_PASSWORD must be set (see .env) to run these integration tests — they authenticate for real, no mocked guard.", ); } return login(app, email, password); } export async function login( app: INestApplication, email: string, password: string, applicationKey: "core" | "reseller" | "subscriber" = "core", ): Promise { const res = await agent(app) .post(`${API}/v1/auth/login`) .send({ email, password, applicationKey }) .expect(200); return { accessToken: res.body.accessToken as string, refreshToken: res.body.refreshToken as string, userId: res.body.user.id as string, }; } export function bearer(token: string): [string, string] { return ["Authorization", `Bearer ${token}`]; } /** * Every fixture this suite creates is tagged with this prefix so it's * trivially identifiable (and greppable) as QA-test-owned data in the * shared dev database, even if a cleanup step is ever missed. */ export const QA_PREFIX = "qa-test-"; export function qaEmail(label: string): string { const rand = Math.random().toString(36).slice(2, 10); return `${QA_PREFIX}${label}-${rand}@example.com`; } export function qaKey(label: string): string { const rand = Math.random().toString(36).slice(2, 8); // Roles: lowercase, starts with a letter, a-z0-9_, 2-30 chars (see // apps/api/src/roles/dto/create-role.dto.ts). return `qa_${label}_${rand}`.slice(0, 30); } /** * Random-suffixed display name, for fixtures backed by a case-insensitive * UNIQUE index with no delete endpoint (e.g. product_types/product_groups/ * brands/units) — a hardcoded name would collide with a row left behind by * a previous run of this suite against the same shared dev database. */ export function qaName(label: string): string { const rand = Math.random().toString(36).slice(2, 8); return `${QA_PREFIX}${label} ${rand}`; }