/** * v1.4.43 W6 — multi-issue 422 envelope on POST /api/bugreport. */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { NextRequest } from "next/server"; vi.mock("@/lib/db", () => ({ prisma: { appSettings: { findUnique: vi.fn(), }, }, })); vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() })); vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn().mockResolvedValue(undefined), })); vi.mock("@/lib/rate-limit", () => ({ checkRateLimit: vi.fn(), })); vi.mock("@/lib/crypto", () => ({ decrypt: (s: string) => s.replace(/^enc-/, ""), })); vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() })); vi.mock("@/lib/db-compat", () => ({ ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), })); vi.mock("next/headers", () => ({ headers: vi.fn(async () => ({ get: () => null })), cookies: vi.fn(async () => ({ get: () => undefined, set: () => {}, delete: () => {}, })), })); import { POST } from "../route"; import { prisma } from "@/lib/db"; import { getSession } from "@/lib/auth/session"; import { checkRateLimit } from "@/lib/rate-limit"; const SESSION_OK = { session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, user: { id: "user-1", username: "tester", role: "USER" as const }, }; function postReq(body: unknown): NextRequest { return new NextRequest("http://localhost/api/bugreport", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } beforeEach(() => { vi.resetAllMocks(); vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never); vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({ githubIssueTokenEncrypted: "enc-token", githubIssueRepo: "owner/repo", bugReportEnabled: true, } as never); }); describe("POST /api/bugreport — 422 multi-issue (v1.4.43 W6)", () => { it("surfaces TWO simultaneous validation errors", async () => { // description too short + screenshot doesn't match data-URL regex. const res = await POST( postReq({ description: "tiny", screenshot: "not-a-data-url" }), ); expect(res.status).toBe(422); const body = (await res.json()) as { data: null; error: string; details: { issues: Array<{ path: string; code: string; message: string }>; }; }; expect(body.data).toBeNull(); expect(body.error).toBe("Validation failed"); expect(body.details.issues.length).toBeGreaterThanOrEqual(2); for (const issue of body.details.issues) { expect(Object.keys(issue).sort()).toEqual(["code", "message", "path"]); } }); it("surfaces multiple simultaneous validation errors (≥ 2)", async () => { // Schema has only two knobs (description, screenshot). Pin contract // on ≥ 2. const res = await POST(postReq({ description: 123, screenshot: 999 })); expect(res.status).toBe(422); const body = (await res.json()) as { details: { issues: Array }; }; expect(body.details.issues.length).toBeGreaterThanOrEqual(2); }); });