import { INestApplication } from "@nestjs/common"; import { Readable } from "node:stream"; import { createHash } from "node:crypto"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, type AuthedTokens } from "./setup.js"; /** * `renderPdf`/`uploadObject`/`getObjectStream` are mocked (the real call * path through documents.service.ts still runs) — a real Chromium render + * a real MinIO round trip isn't worth the flakiness/runtime cost in this * suite; renderer.ts itself is exercised by the manual smoke test noted in * docs/progress.md. What's under test here is documents.service.ts's own * logic: it must actually call the renderer with the merged HTML, hash and * persist the result, skip re-rendering on dedup, and stream the right S3 * key back out on download. */ const renderPdfMock = vi.fn(async (_input: { html: string; css?: string }) => Buffer.from("%PDF-1.4 fake pdf bytes")); const uploadObjectMock = vi.fn(async (_key: string, _buffer: Buffer, _mimeType: string) => undefined); const getObjectStreamMock = vi.fn(async (key: string) => Readable.from([Buffer.from(`content-of:${key}`)])); vi.mock("@eden/integrations", () => ({ renderPdf: (input: { html: string; css?: string }) => renderPdfMock(input), uploadObject: (key: string, buffer: Buffer, mimeType: string) => uploadObjectMock(key, buffer, mimeType), getObjectStream: (key: string) => getObjectStreamMock(key), })); describe("documents — PDF generation + download", () => { let app: INestApplication; let admin: AuthedTokens; let templateId: string; let resellerId: string; beforeAll(async () => { app = await createTestApp(); admin = await loginSuperAdmin(app); const template = await agent(app) .post("/api/v1/documents/templates") .set(...bearer(admin.accessToken)) .send({ key: `qa_test_pdf_${Date.now()}`, name: "qa-test- Template PDF", docGroup: "revenda" }) .expect(201); templateId = template.body.id; await agent(app) .post(`/api/v1/documents/templates/${templateId}/draft`) .set(...bearer(admin.accessToken)) .expect(201); await agent(app) .patch(`/api/v1/documents/templates/${templateId}/draft`) .set(...bearer(admin.accessToken)) .send({ editorContent: { type: "doc", plainText: "Revenda: {{revenda.nome}}" }, cssContent: "p { color: red; }", }) .expect(200); await agent(app) .post(`/api/v1/documents/templates/${templateId}/publish`) .set(...bearer(admin.accessToken)) .expect(201); const reseller = await agent(app) .post("/api/v1/resellers") .set(...bearer(admin.accessToken)) .send({ name: "qa-test- Revenda PDF" }) .expect(201); resellerId = reseller.body.id; }); afterAll(async () => { await agent(app) .patch(`/api/v1/resellers/${resellerId}`) .set(...bearer(admin.accessToken)) .send({ active: false }) .expect(200); await closeTestApp(app); }); it("renders a real PDF, uploads it, and persists the hash/size/key", async () => { const res = await agent(app) .post("/api/v1/documents/generations") .set(...bearer(admin.accessToken)) .send({ templateId, resellerId, context: { "revenda.nome": "ACME" } }) .expect(201); expect(res.body.deduped).toBe(false); expect(res.body.missingVariables).toEqual([]); expect(res.body.pdf_s3_key).toMatch(/^documents\/generations\/.+\.pdf$/); expect(res.body.pdf_size_bytes).toBeGreaterThan(0); // The HTML actually handed to the renderer had the variable resolved. expect(renderPdfMock).toHaveBeenCalledTimes(1); const renderedHtml = renderPdfMock.mock.calls[0]![0].html; expect(renderedHtml).toContain("Revenda: ACME"); expect(renderedHtml).not.toContain("{{revenda.nome}}"); // The template version's css_content must actually reach the renderer — // real gap found and fixed alongside the draft-preview feature: this // used to be silently discarded (renderPdf({ html: resolved }) only). expect(renderPdfMock.mock.calls[0]![0].css).toBe("p { color: red; }"); // Uploaded buffer's hash matches the mock's output and what got persisted. const uploadCall = uploadObjectMock.mock.calls[0]!; expect(uploadCall[0]).toBe(res.body.pdf_s3_key); expect(uploadCall[2]).toBe("application/pdf"); expect(createHash("sha256").update(uploadCall[1]).digest("hex")).toBe(res.body.pdf_sha256); }); it("dedupes an identical (template version, origin) request without re-rendering", async () => { renderPdfMock.mockClear(); uploadObjectMock.mockClear(); const res = await agent(app) .post("/api/v1/documents/generations") .set(...bearer(admin.accessToken)) .send({ templateId, resellerId, context: { "revenda.nome": "ACME" } }) .expect(201); expect(res.body.deduped).toBe(true); expect(renderPdfMock).not.toHaveBeenCalled(); expect(uploadObjectMock).not.toHaveBeenCalled(); }); it("streams the stored PDF back from the download endpoint", async () => { const listRes = await agent(app) .get("/api/v1/documents/generations") .set(...bearer(admin.accessToken)) .query({ resellerId }) .expect(200); const generation = listRes.body.find((g: { reseller_id: string | null }) => g.reseller_id === resellerId); expect(generation).toBeTruthy(); const download = await agent(app) .get(`/api/v1/documents/generations/${generation.id}/pdf`) .set(...bearer(admin.accessToken)) .expect(200); expect(download.headers["content-type"]).toBe("application/pdf"); expect(getObjectStreamMock).toHaveBeenCalledWith(generation.pdf_s3_key); expect(Buffer.isBuffer(download.body) && download.body.toString()).toBe(`content-of:${generation.pdf_s3_key}`); }); describe("draft preview", () => { it("renders the CURRENT (unsaved) editor state with placeholder variables, never persisting anything", async () => { renderPdfMock.mockClear(); uploadObjectMock.mockClear(); const res = await agent(app) .post(`/api/v1/documents/templates/${templateId}/draft/preview`) .set(...bearer(admin.accessToken)) .send({ editorContent: { plainText: "Olá {{cliente.nome}}, plano {{plano}}." }, cssContent: "p { color: blue; }", }) .expect(201); expect(res.headers["content-type"]).toBe("application/pdf"); expect(Buffer.isBuffer(res.body) && res.body.toString()).toBe("%PDF-1.4 fake pdf bytes"); expect(renderPdfMock).toHaveBeenCalledTimes(1); const call = renderPdfMock.mock.calls[0]![0]; expect(call.html).toContain("[cliente.nome]"); expect(call.html).toContain("[plano]"); expect(call.html).not.toContain("{{"); expect(call.css).toBe("p { color: blue; }"); // Never persisted: no S3 upload, and the real (published) generation // count for this template is unaffected. expect(uploadObjectMock).not.toHaveBeenCalled(); const listRes = await agent(app) .get("/api/v1/documents/generations") .set(...bearer(admin.accessToken)) .query({ resellerId }) .expect(200); expect(listRes.body).toHaveLength(1); // still just the one from the earlier test }); it("404s for an unknown template id", async () => { await agent(app) .post("/api/v1/documents/templates/00000000-0000-0000-0000-000000000000/draft/preview") .set(...bearer(admin.accessToken)) .send({ editorContent: { plainText: "x" } }) .expect(404); }); }); });