import { describe, it, expect, afterEach, 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 ordersRoutes from "../routes/admin/orders"; import { createOrder, listOrders, getOrder, getOrderAttachment, } from "../services/orders.service"; const createdOrderIds: number[] = []; const createdCustomerIds: number[] = []; afterEach(async () => { for (const id of createdOrderIds) await prisma.orders.deleteMany({ where: { id } }); for (const id of createdCustomerIds) await prisma.customers.deleteMany({ where: { id } }); createdOrderIds.length = 0; createdCustomerIds.length = 0; await prisma.number_sequences.deleteMany({ where: { type: "shared" } }); }); const baseOrder = { status: "prijata" as const, currency: "CZK", language: "cs", exchange_rate: 1, }; function failResult(res: unknown): never { throw new Error(`expected a success result, got ${JSON.stringify(res)}`); } async function makeCustomer() { const c = await prisma.customers.create({ data: { name: "Odběratel a.s." }, }); createdCustomerIds.push(c.id); return c; } const baseListParams = { page: 1, limit: 50, skip: 0, sort: "id", order: "desc" as const, }; describe("listOrders search filter", () => { it("returns the order when search matches its order_number", async () => { const customer = await makeCustomer(); const res = await createOrder({ ...baseOrder, customer_id: customer.id, create_project: false, }); if (!("id" in res)) failResult(res); createdOrderIds.push(res.id!); const list = await listOrders({ ...baseListParams, search: res.order_number!, }); expect(list.data.map((o) => o.id)).toContain(res.id); }); it("excludes the order when search matches nothing", async () => { const customer = await makeCustomer(); const res = await createOrder({ ...baseOrder, customer_id: customer.id, create_project: false, }); if (!("id" in res)) failResult(res); createdOrderIds.push(res.id!); const list = await listOrders({ ...baseListParams, search: "ZZZ-NO-SUCH-ORDER-ZZZ", }); expect(list.data.map((o) => o.id)).not.toContain(res.id); }); }); describe("order attachment payload hygiene", () => { // The PO attachment blob must be served ONLY by GET /:id/attachment — // list/detail payloads carry attachment_name as the existence signal. const PDF_BYTES = Buffer.from("%PDF-1.4 orders-list-test-attachment-bytes"); const TEST_ADMIN = "orders_list_test_admin"; let app: Awaited>; let adminToken: string; let adminUserId: number; async function buildOrdersApp() { const fastify = Fastify({ logger: false }); await fastify.register(cookie); await fastify.register(rateLimit, { max: 1000, timeWindow: "1 minute" }); // multipart is registered inside ordersRoutes itself await fastify.register(ordersRoutes, { prefix: "/api/admin/orders" }); return fastify; } beforeAll(async () => { app = await buildOrdersApp(); // Clean up any leftover test user from a previous failed run await prisma.users.deleteMany({ where: { username: TEST_ADMIN } }); const adminRole = await prisma.roles.findFirst({ where: { name: "admin" }, }); if (!adminRole) throw new Error("Admin role not found — seed the database first"); const adminUser = await prisma.users.create({ data: { username: TEST_ADMIN, email: `${TEST_ADMIN}@test.local`, password_hash: "$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO", first_name: "Orders", last_name: "ListTest", is_active: true, role_id: adminRole.id, }, }); adminUserId = adminUser.id; adminToken = jwt.sign( { sub: adminUser.id, username: adminUser.username, role: "admin" }, config.jwt.secret, { expiresIn: "15m" }, ); }); afterAll(async () => { await prisma.users.deleteMany({ where: { id: adminUserId } }); await app.close(); }); /** One order WITH a stored PO attachment, one WITHOUT. */ async function makeOrderPair() { const customer = await makeCustomer(); const withRes = await createOrder( { ...baseOrder, customer_id: customer.id, create_project: false }, PDF_BYTES, "po-test.pdf", ); if (!("id" in withRes)) failResult(withRes); createdOrderIds.push(withRes.id!); const withoutRes = await createOrder({ ...baseOrder, customer_id: customer.id, create_project: false, }); if (!("id" in withoutRes)) failResult(withoutRes); createdOrderIds.push(withoutRes.id!); return { withId: withRes.id!, withoutId: withoutRes.id! }; } it("listOrders rows never contain attachment_data; attachment_name signals existence", async () => { const { withId, withoutId } = await makeOrderPair(); const list = await listOrders(baseListParams); const withRow = list.data.find((o) => o.id === withId); const withoutRow = list.data.find((o) => o.id === withoutId); expect(withRow).toBeTruthy(); expect(withoutRow).toBeTruthy(); expect("attachment_data" in withRow!).toBe(false); expect("attachment_data" in withoutRow!).toBe(false); expect(withRow!.attachment_name).toBe("po-test.pdf"); expect(withoutRow!.attachment_name).toBeNull(); }); it("getOrder detail never contains attachment_data but keeps attachment_name", async () => { const { withId, withoutId } = await makeOrderPair(); const withDetail = await getOrder(withId); const withoutDetail = await getOrder(withoutId); expect(withDetail).toBeTruthy(); expect(withoutDetail).toBeTruthy(); expect("attachment_data" in withDetail!).toBe(false); expect("attachment_data" in withoutDetail!).toBe(false); expect(withDetail!.attachment_name).toBe("po-test.pdf"); expect(withoutDetail!.attachment_name).toBeNull(); }); it("PUT /:id accepts the canonical storno token through the Zod layer", async () => { // Regression: UpdateOrderSchema carried a phantom "zrusena" enum member // until 2026-07, so { status: "stornovana" } 400ed at parseBody with a // raw English Zod message. Service-level tests bypass the schema — this // must stay a route-level test. const customer = await makeCustomer(); const res = await createOrder({ ...baseOrder, customer_id: customer.id, create_project: false, }); if (!("id" in res)) failResult(res); createdOrderIds.push(res.id!); const put = await app.inject({ method: "PUT", url: `/api/admin/orders/${res.id}`, headers: { authorization: `Bearer ${adminToken}` }, payload: { status: "stornovana" }, }); expect(put.statusCode).toBe(200); const row = await prisma.orders.findUnique({ where: { id: res.id! } }); expect(row!.status).toBe("stornovana"); }); it("HTTP list/detail JSON carries no attachment_data; /attachment still serves the binary", async () => { const { withId, withoutId } = await makeOrderPair(); const headers = { authorization: `Bearer ${adminToken}` }; const listRes = await app.inject({ method: "GET", url: "/api/admin/orders?sort=id&order=desc&per_page=100", headers, }); expect(listRes.statusCode).toBe(200); const listJson = JSON.parse(listRes.body); expect(listJson.data.map((o: { id: number }) => o.id)).toContain(withId); expect(listRes.body).not.toContain("attachment_data"); const detailRes = await app.inject({ method: "GET", url: `/api/admin/orders/${withId}`, headers, }); expect(detailRes.statusCode).toBe(200); expect(detailRes.body).not.toContain("attachment_data"); expect(JSON.parse(detailRes.body).data.attachment_name).toBe("po-test.pdf"); // The dedicated endpoint is the ONE place the blob is served — byte-exact. const attRes = await app.inject({ method: "GET", url: `/api/admin/orders/${withId}/attachment`, headers, }); expect(attRes.statusCode).toBe(200); expect(attRes.headers["content-type"]).toContain("application/pdf"); expect(Buffer.from(attRes.rawPayload).equals(PDF_BYTES)).toBe(true); const noAttRes = await app.inject({ method: "GET", url: `/api/admin/orders/${withoutId}/attachment`, headers, }); expect(noAttRes.statusCode).toBe(404); }); it("getOrderAttachment round-trips the stored bytes (and null without one)", async () => { const { withId, withoutId } = await makeOrderPair(); const att = await getOrderAttachment(withId); expect(att).toBeTruthy(); expect(att!.filename).toBe("po-test.pdf"); expect(att!.data.equals(PDF_BYTES)).toBe(true); expect(await getOrderAttachment(withoutId)).toBeNull(); }); }); describe("listOrders month filter", () => { it("returns only orders whose created_at is in the given month", async () => { const customer = await makeCustomer(); const res = await createOrder({ ...baseOrder, customer_id: customer.id, create_project: false, }); if (!("id" in res)) failResult(res); createdOrderIds.push(res.id!); // Pin created_at to a known month so the range filter is deterministic. await prisma.orders.update({ where: { id: res.id }, data: { created_at: new Date(2026, 2, 15, 12, 0, 0) }, }); const inMonth = await listOrders({ ...baseListParams, month: 3, year: 2026, }); expect(inMonth.data.map((o) => o.id)).toContain(res.id); const otherMonth = await listOrders({ ...baseListParams, month: 4, year: 2026, }); expect(otherMonth.data.map((o) => o.id)).not.toContain(res.id); }); });