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>
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
import { INestApplication } from "@nestjs/common";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, type AuthedTokens } from "./setup.js";
|
|
|
|
/**
|
|
* Protects the global ValidationPipe wiring itself
|
|
* (whitelist: true, forbidNonWhitelisted: true — apps/api/src/main.ts,
|
|
* mirrored in test/setup.ts's createTestApp). Master Prompt §12: the
|
|
* server never trusts a client-supplied field it didn't ask for. This
|
|
* doesn't test any one DTO's specific fields so much as prove the pipe
|
|
* is actually reaching these routes and actually configured to reject
|
|
* (not silently strip) unknown fields.
|
|
*
|
|
* No fixtures are created — every request here is expected to be
|
|
* rejected by the pipe *before* the controller/service runs, so nothing
|
|
* ever reaches the database. No cleanup needed.
|
|
*/
|
|
describe("mass-assignment — unexpected fields are rejected, not silently dropped", () => {
|
|
let app: INestApplication;
|
|
let admin: AuthedTokens;
|
|
|
|
beforeAll(async () => {
|
|
app = await createTestApp();
|
|
admin = await loginSuperAdmin(app);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await closeTestApp(app);
|
|
});
|
|
|
|
it("POST /v1/companies rejects an unexpected 'id' field", async () => {
|
|
await agent(app)
|
|
.post("/api/v1/companies")
|
|
.set(...bearer(admin.accessToken))
|
|
.send({ companyName: "qa-test- mass assignment id", id: "11111111-1111-1111-1111-111111111111" })
|
|
.expect(400);
|
|
});
|
|
|
|
it("POST /v1/companies rejects an unexpected 'isActive' field (not part of CreateCompanyDto)", async () => {
|
|
await agent(app)
|
|
.post("/api/v1/companies")
|
|
.set(...bearer(admin.accessToken))
|
|
.send({ companyName: "qa-test- mass assignment isActive", isActive: false })
|
|
.expect(400);
|
|
});
|
|
|
|
it("POST /v1/products rejects an unexpected 'createdBy' field", async () => {
|
|
await agent(app)
|
|
.post("/api/v1/products")
|
|
.set(...bearer(admin.accessToken))
|
|
.send({
|
|
name: "qa-test- mass assignment product",
|
|
productTypeId: "11111111-1111-1111-1111-111111111111",
|
|
unitId: "11111111-1111-1111-1111-111111111111",
|
|
createdBy: "11111111-1111-1111-1111-111111111111",
|
|
})
|
|
.expect(400);
|
|
});
|
|
});
|