Files
eden/apps/api/test/setup.ts
Matheus (Handix) f38f2407ff feat(products): catálogos de Tipo/Grupo/Subgrupo/Marca/Unidade com submenu e dropdowns
Menu Produtos ganha submenu (AppShell agora suporta itens aninhados) para
cadastrar Tipos/Grupos/Marcas/Unidades; Subgrupo é gerenciado a partir da
tela do Grupo (sempre pertence a um). O cadastro de item passa a usar
dropdowns desses catálogos em vez de texto livre — exatamente o que o
comentário original da migration de products já previa como trabalho
futuro.

Backend: 5 tabelas de catálogo simples (mesmo padrão dos catálogos
fiscais), products ganha as FKs correspondentes com backfill, e
endpoints CRUD (sem exclusão física, active=false é o "apagar").

Corrige também um buraco de integridade real encontrado na integração:
PATCH /v1/products/:id usava COALESCE para os campos de catálogo
opcionais, então não dava para distinguir "não mexer" de "limpar" —
trocar o Grupo de um produto sem tocar no Subgrupo deixava o vínculo
antigo órfão e inconsistente, sem nenhuma validação pegando isso. Agora
a atualização valida o estado efetivo resultante (bloqueia com 400 em
vez de corromper o vínculo) e productSubgroupId: null limpa de verdade.

Suíte: 95/95 verdes (92→95). pnpm turbo run typecheck build: 28/28.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 23:46:14 -03:00

132 lines
4.5 KiB
TypeScript

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);
}
/**
* 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}`;
}