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>
132 lines
4.5 KiB
TypeScript
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}`;
|
|
}
|