test(api): add Vitest+supertest integration test suite (eden-qa)

Boots the real Nest app (Test.createTestingModule + AppModule) against a
real Postgres — no mocked DB or guards anywhere in this suite. Uses SWC
(unplugin-swc) instead of Vite's default esbuild transform because
esbuild doesn't implement emitDecoratorMetadata, which silently breaks
NestJS's constructor DI (every provider resolves to undefined).

Covers:
- auth.spec.ts: anti-enumeration login message, refresh-token rotation,
  replay-detection revoking the whole session chain, idempotent logout
  (ADR-0003).
- role-weight.spec.ts: invalid role_key on invite returns 400 not 500
  (regression guard), lower-weight actor forbidden from assigning a
  heavier role even when holding users:edit (ADR-0004).
- companies.spec.ts: CNPJ digit-check validation (reject/accept).
- mass-assignment.spec.ts: forbidNonWhitelisted actually rejects
  unexpected fields on companies/products.
- reseller-assignment.spec.ts: assign-user always targets the URL's
  :id, never a body resellerId (regression guard for commit 8d108e0);
  unassign-user clears the assignment.

All fixtures are qa-test-/qa_ tagged and cleaned up (deactivated where
no DELETE endpoint exists, deleted where one does) — verified no
pre-existing seed data (super_admin, Handix Telecom LTDA, Handix revenda
própria) was touched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 11:14:43 -03:00
parent c9ba96f61f
commit cdafb7f853
12 changed files with 1767 additions and 3 deletions

120
apps/api/test/setup.ts Normal file
View File

@@ -0,0 +1,120 @@
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<INestApplication> {
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<void> {
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<AuthedTokens> {
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<AuthedTokens> {
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);
}