/** * Integration tests for GET /api/products/search. * Tests authorization, query validation, filtering, and Decimal serialisation. */ import { vi, describe, it, expect, beforeAll } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); import { auth } from "@/auth"; import { NextRequest } from "next/server"; import { GET } from "@/app/api/products/search/route"; import { makeSession, getSeedUser } from "./helpers"; let techId: string; let accountsId: string; beforeAll(async () => { const [tech, acct] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("accounts@pelagia.local"), ]); techId = tech.id; accountsId = acct.id; }); function makeRequest(query: string) { return new NextRequest(`http://localhost/api/products/search?q=${encodeURIComponent(query)}`); } // ── Authorization ───────────────────────────────────────────────────────────── describe("GET /api/products/search — authorization", () => { it("returns 401 for unauthenticated requests", async () => { vi.mocked(auth).mockResolvedValue(null); const res = await GET(makeRequest("oil")); expect(res.status).toBe(401); }); it("TECHNICAL can search products", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const res = await GET(makeRequest("oil")); expect(res.status).toBe(200); }); it("ACCOUNTS can search products", async () => { vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); const res = await GET(makeRequest("oil")); expect(res.status).toBe(200); }); }); // ── Query validation ────────────────────────────────────────────────────────── describe("GET /api/products/search — query validation", () => { beforeEach(() => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); }); it("returns empty array for query shorter than 2 chars", async () => { const res = await GET(makeRequest("a")); const data = await res.json(); expect(data).toEqual([]); }); it("returns empty array for empty query", async () => { const res = await GET(makeRequest("")); const data = await res.json(); expect(data).toEqual([]); }); it("returns results for query of exactly 2 chars", async () => { const res = await GET(makeRequest("oi")); const data = await res.json(); expect(Array.isArray(data)).toBe(true); }); }); // ── Search behaviour ────────────────────────────────────────────────────────── describe("GET /api/products/search — search behaviour", () => { beforeEach(() => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); }); it("finds products by name substring", async () => { const res = await GET(makeRequest("Gear Oil")); const data: { name: string }[] = await res.json(); expect(data.some((p) => p.name.toLowerCase().includes("gear oil"))).toBe(true); }); it("finds products by product code", async () => { const res = await GET(makeRequest("LUBE")); const data: { code: string }[] = await res.json(); expect(data.every((p) => p.code.toUpperCase().includes("LUBE"))).toBe(true); }); it("finds products by description text", async () => { const res = await GET(makeRequest("turbocharger")); const data: { description: string | null }[] = await res.json(); expect(data.length).toBeGreaterThan(0); expect(data.some((p) => p.description?.toLowerCase().includes("turbocharger"))).toBe(true); }); it("search is case-insensitive", async () => { const [upper, lower] = await Promise.all([ GET(makeRequest("GEAR OIL")).then((r) => r.json()), GET(makeRequest("gear oil")).then((r) => r.json()), ]); expect(upper.length).toBe(lower.length); }); it("returns at most 10 results", async () => { // Query a broad term likely to match many products const res = await GET(makeRequest("a")); const data: unknown[] = await res.json(); expect(data.length).toBeLessThanOrEqual(10); }); it("serialises lastPrice as a plain number, not a Decimal object", async () => { const res = await GET(makeRequest("Gear Oil")); const data: { lastPrice: unknown }[] = await res.json(); const withPrice = data.find((p) => p.lastPrice !== null); if (withPrice) { expect(typeof withPrice.lastPrice).toBe("number"); } }); it("excludes inactive products from results", async () => { const { db } = await import("@/lib/db"); // Deactivate a known product temporarily const product = await db.product.findFirst({ where: { code: "LUBE-EP80W90", isActive: true }, }); if (!product) return; await db.product.update({ where: { id: product.id }, data: { isActive: false } }); try { const res = await GET(makeRequest("EP 80W90")); const data: { code: string }[] = await res.json(); expect(data.find((p) => p.code === "LUBE-EP80W90")).toBeUndefined(); } finally { await db.product.update({ where: { id: product.id }, data: { isActive: true } }); } }); });