import { INestApplication } from "@nestjs/common"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { agent, bearer, closeTestApp, createTestApp, login, loginSuperAdmin, qaEmail, qaKey, qaName, type AuthedTokens, } from "./setup.js"; /** * Protects the edit-lock invariant ADR-0006 explicitly promises but that * ContractsService didn't actually enforce until now: "fluxo de * fechamento de oferta → geração de contrato preserva a trava equivalente * [...] exceto por papel com permissão de correção auditada equivalente * ao super_admin do legado" (docs/adr/0006-contract-first-class.md). * Mirrors the shape of quotes.service.ts's assertNotLocked, ported to * Contract's own status field (draft = editable, anything past it = * locked except for super_admin). */ describe("contracts — edit lock past 'draft'", () => { let app: INestApplication; let admin: AuthedTokens; let resellerId: string; let productId: string; let contractId: string; let lowRoleKey: string; let lowActorUserId: string; let lowActorEmail: string; let lowActorTokens: AuthedTokens; beforeAll(async () => { app = await createTestApp(); admin = await loginSuperAdmin(app); const reseller = await agent(app) .post("/api/v1/resellers") .set(...bearer(admin.accessToken)) .send({ name: "qa-test- Revenda Contract Lock" }) .expect(201); resellerId = reseller.body.id; const productType = await agent(app) .post("/api/v1/products/types") .set(...bearer(admin.accessToken)) .send({ name: qaName("Tipo Contract Lock") }) .expect(201); const unit = await agent(app) .post("/api/v1/products/units") .set(...bearer(admin.accessToken)) .send({ name: qaName("Unidade Contract Lock") }) .expect(201); const product = await agent(app) .post("/api/v1/products") .set(...bearer(admin.accessToken)) .send({ name: "qa-test- Produto Contract Lock", productTypeId: productType.body.id, unitId: unit.body.id, price0: 100, }) .expect(201); productId = product.body.id; const contract = await agent(app) .post("/api/v1/contracts") .set(...bearer(admin.accessToken)) .send({ resellerId, contractPeriod: 0, items: [{ productId, quantity: 1 }], parties: [{ partyRole: "contratante", fullName: "qa-test Cliente Contract Lock" }], }) .expect(201); contractId = contract.body.id; expect(contract.body.status).toBe("draft"); // Non-super_admin actor holding contracts:edit — isolates the lock // check itself from the generic PermissionGuard (same pattern as // role-weight.spec.ts). lowRoleKey = qaKey("contractlock"); await agent(app) .post("/api/v1/roles") .set(...bearer(admin.accessToken)) .send({ key: lowRoleKey, label: "QA test contract-lock role", weight: 10 }) .expect(201); await agent(app) .patch(`/api/v1/roles/${lowRoleKey}/permissions`) .set(...bearer(admin.accessToken)) .send({ permissions: [ { resource: "contracts", action: "edit", scope: "all" }, { resource: "contracts", action: "view", scope: "all" }, ], }) .expect(200); lowActorEmail = qaEmail("contractlock-actor"); const invite = await agent(app) .post("/api/v1/users/invite") .set(...bearer(admin.accessToken)) .send({ email: lowActorEmail, roleKey: lowRoleKey, applications: ["core"] }) .expect(201); lowActorUserId = invite.body.user.id; lowActorTokens = await login(app, lowActorEmail, invite.body.tempPassword); }); afterAll(async () => { await agent(app) .patch(`/api/v1/users/${lowActorUserId}`) .set(...bearer(admin.accessToken)) .send({ roleKey: "user", isActive: false }) .expect(200); await agent(app) .delete(`/api/v1/roles/${lowRoleKey}`) .set(...bearer(admin.accessToken)) .expect(204); await agent(app) .patch(`/api/v1/products/${productId}`) .set(...bearer(admin.accessToken)) .send({ active: false }) .expect(200); await agent(app) .patch(`/api/v1/resellers/${resellerId}`) .set(...bearer(admin.accessToken)) .send({ active: false }) .expect(200); await closeTestApp(app); }); it("holding contracts:edit is enough to add an item while the contract is still draft", async () => { await agent(app) .post(`/api/v1/contracts/${contractId}/items`) .set(...bearer(lowActorTokens.accessToken)) .send({ productId, quantity: 1 }) .expect(201); }); it("once the contract leaves draft, a non-super_admin holding contracts:edit is blocked (409)", async () => { await agent(app) .patch(`/api/v1/contracts/${contractId}/status`) .set(...bearer(admin.accessToken)) .send({ toStatus: "pending_signature" }) .expect(200); const res = await agent(app) .post(`/api/v1/contracts/${contractId}/items`) .set(...bearer(lowActorTokens.accessToken)) .send({ productId, quantity: 1 }) .expect(409); expect(res.body.message).toMatch(/rascunho/i); await agent(app) .post(`/api/v1/contracts/${contractId}/parties`) .set(...bearer(lowActorTokens.accessToken)) .send({ partyRole: "contratante", fullName: "qa-test Outra Parte" }) .expect(409); }); it("super_admin can still add an item past draft — an audited override, not a silent bypass", async () => { const res = await agent(app) .post(`/api/v1/contracts/${contractId}/items`) .set(...bearer(admin.accessToken)) .send({ productId, quantity: 1 }) .expect(201); expect(res.body.product_id).toBe(productId); }); });