import { describe, it, expect, beforeAll, afterAll } from "vitest"; import Fastify from "fastify"; import cookie from "@fastify/cookie"; import rateLimit from "@fastify/rate-limit"; import jwt from "jsonwebtoken"; import prisma from "../config/database"; import { config } from "../config/env"; import { securityHeaders } from "../middleware/security"; import warehouseRoutes from "../routes/admin/warehouse"; /** * Pinning tests for audit finding M8: warehouse receipts/issues create+update * must pre-validate FK existence (supplier, item, location, project, explicit * batch) and return Czech 400s — a dangling id otherwise raises P2003 at * Prisma and surfaces as a generic 500. The document modules and trips.ts * pre-validate this way; warehouse omitted the guard. */ const N = "wh_fk400_"; const MISSING_ID = 99999999; let app: ReturnType; let adminToken: string; let projectId: number; let itemId: number; let draftReceiptId: number; async function cleanup() { await prisma.sklad_issue_lines.deleteMany({ where: { issue: { notes: { contains: N } } }, }); await prisma.sklad_issues.deleteMany({ where: { notes: { contains: N } } }); await prisma.sklad_batches.deleteMany({ where: { item: { name: { contains: N } } }, }); await prisma.sklad_receipt_lines.deleteMany({ where: { item: { name: { contains: N } } }, }); await prisma.sklad_receipts.deleteMany({ where: { notes: { contains: N } } }); await prisma.sklad_items.deleteMany({ where: { name: { contains: N } } }); await prisma.projects.deleteMany({ where: { name: { startsWith: N } } }); } beforeAll(async () => { await cleanup(); app = Fastify({ logger: false }); await app.register(cookie); await app.register(rateLimit, { max: 1000, timeWindow: "1 minute" }); app.addHook("onRequest", securityHeaders); await app.register(warehouseRoutes, { prefix: "/api/admin/warehouse" }); const admin = await prisma.users.findFirst({ where: { roles: { name: "admin" } }, }); if (!admin) throw new Error("Test setup: admin user not found"); adminToken = jwt.sign( { sub: admin.id, username: admin.username, role: "admin" }, config.jwt.secret, { expiresIn: "15m" }, ); const project = await prisma.projects.create({ data: { name: `${N}project`, status: "active" }, }); projectId = project.id; const item = await prisma.sklad_items.create({ data: { name: `${N}item`, unit: "ks" }, }); itemId = item.id; // In-stock batch so issue tests get past availability checks. const receipt = await prisma.sklad_receipts.create({ data: { notes: `${N}stock`, status: "CONFIRMED" }, }); const line = await prisma.sklad_receipt_lines.create({ data: { receipt_id: receipt.id, item_id: itemId, quantity: 10, unit_price: 5, }, }); await prisma.sklad_batches.create({ data: { item_id: itemId, receipt_line_id: line.id, quantity: 10, original_qty: 10, unit_price: 5, received_at: new Date(2026, 0, 1, 12, 0, 0), is_consumed: false, }, }); // DRAFT receipt for the PUT test. const draft = await prisma.sklad_receipts.create({ data: { notes: `${N}draft`, status: "DRAFT", items: { create: [{ item_id: itemId, quantity: 1, unit_price: 5 }] }, }, }); draftReceiptId = draft.id; }); afterAll(async () => { if (app) await app.close(); await cleanup(); await prisma.$disconnect(); }); function post(url: string, payload: unknown) { return app.inject({ method: "POST", url, headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json", }, payload: payload as Record, }); } describe("warehouse FK pre-validation (audit M8)", () => { it("POST /receipts with a nonexistent item_id returns 400, not 500", async () => { const res = await post("/api/admin/warehouse/receipts", { notes: `${N}r1`, items: [{ item_id: MISSING_ID, quantity: 1, unit_price: 10 }], }); expect(res.statusCode).toBe(400); expect(res.json().error).toContain("nenalezen"); }); it("POST /receipts with a nonexistent supplier_id returns 400, not 500", async () => { const res = await post("/api/admin/warehouse/receipts", { notes: `${N}r2`, supplier_id: MISSING_ID, items: [{ item_id: itemId, quantity: 1, unit_price: 10 }], }); expect(res.statusCode).toBe(400); expect(res.json().error).toContain("nenalezen"); }); it("POST /receipts with a nonexistent location_id returns 400, not 500", async () => { const res = await post("/api/admin/warehouse/receipts", { notes: `${N}r3`, items: [ { item_id: itemId, quantity: 1, unit_price: 10, location_id: MISSING_ID, }, ], }); expect(res.statusCode).toBe(400); expect(res.json().error).toContain("nenalezen"); }); it("PUT /receipts/:id with a nonexistent item_id returns 400, not 500", async () => { const res = await app.inject({ method: "PUT", url: `/api/admin/warehouse/receipts/${draftReceiptId}`, headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json", }, payload: { notes: `${N}draft`, items: [{ item_id: MISSING_ID, quantity: 1, unit_price: 10 }], }, }); expect(res.statusCode).toBe(400); expect(res.json().error).toContain("nenalezen"); }); it("POST /issues with a nonexistent project_id returns 400, not 500", async () => { const res = await post("/api/admin/warehouse/issues", { notes: `${N}i1`, project_id: MISSING_ID, items: [{ item_id: itemId, quantity: 1 }], }); expect(res.statusCode).toBe(400); expect(res.json().error).toContain("nenalezen"); }); it("POST /issues with a nonexistent explicit batch_id returns 400, not 500", async () => { const res = await post("/api/admin/warehouse/issues", { notes: `${N}i2`, project_id: projectId, items: [{ item_id: itemId, quantity: 1, batch_id: MISSING_ID }], }); expect(res.statusCode).toBe(400); expect(res.json().error).toContain("nenalezen"); }); it("valid receipt create still succeeds", async () => { const res = await post("/api/admin/warehouse/receipts", { notes: `${N}ok`, items: [{ item_id: itemId, quantity: 2, unit_price: 10 }], }); expect(res.statusCode).toBe(201); }); });