import assert from "node:assert/strict"; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test, { before, beforeEach } from "node:test"; /** Rota GET /api/products/[barcode], com fetch mockado e cache JSON em pasta temporária. */ type Handler = (url: URL, init?: RequestInit) => { ok: boolean; body?: unknown }; type Json = Record; // eslint-disable-line @typescript-eslint/no-explicit-any const calls: Array<{ url: URL; init?: RequestInit }> = []; let handler: Handler = () => ({ ok: true, body: {} }); let GET: (req: import("next/server").NextRequest, ctx: { params: Promise<{ barcode: string }> }) => Promise; let NextRequestCtor: typeof import("next/server").NextRequest; let cacheFile: string; before(async () => { const dir = mkdtempSync(path.join(tmpdir(), "nutriscan-products-")); mkdirSync(path.join(dir, "data")); cacheFile = path.join(dir, "data", "products-cache.json"); const meta = { firstSeen: 1, lastUpdated: 1, updateCount: 1 }; const cached = { code: "7622210575975", product_name: "Bis", _metadata: meta }; writeFileSync(cacheFile, JSON.stringify({ products: { [cached.code]: cached }, lastUpdated: 1, version: "1.0.0" })); process.chdir(dir); console.log = () => {}; console.error = () => {}; globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url); calls.push({ url, init }); const result = handler(url, init); return { ok: result.ok, status: result.ok ? 200 : 404, json: async () => result.body } as Response; }) as typeof fetch; ({ GET } = await import("../app/api/products/[barcode]/route.ts")); ({ NextRequest: NextRequestCtor } = await import("next/server")); }); beforeEach(() => { calls.length = 0; handler = () => ({ ok: true, body: {} }); delete process.env.NEXT_PUBLIC_CACHE_ONLY_MODE; }); async function call(barcode: string, query = "") { const req = new NextRequestCtor(`http://localhost/api/products/${barcode}${query}`); const res = await GET(req, { params: Promise.resolve({ barcode }) }); return { status: res.status, body: (await res.json()) as Json }; } test("código de barras com menos de 8 ou mais de 13 dígitos responde 400", async () => { for (const code of ["123", "12345678901234"]) { const { status, body } = await call(code); assert.equal(status, 400, code); assert.equal(body.error, "Código de barras inválido"); } assert.equal(calls.length, 0); }); test("produto no cache é devolvido sem chamar a API e sem metadados internos", async () => { const { status, body } = await call("7622210575975"); assert.equal(status, 200); assert.equal(body.product_name, "Bis"); assert.equal(body.fromCache, true); assert.equal(body.fromAPI, false); assert.equal(body._metadata, undefined); assert.equal(calls.length, 0); }); test("não-dígitos são removidos antes da validação", async () => { const { status, body } = await call("7622-2105-75975"); assert.equal(status, 200); assert.equal(body.fromCache, true); }); test("cache_only sem o produto no cache responde 404 sem chamar a API", async () => { const { status } = await call("3017620422003", "?cache_only=true"); assert.equal(status, 404); assert.equal(calls.length, 0); process.env.NEXT_PUBLIC_CACHE_ONLY_MODE = "true"; assert.equal((await call("3017620422003")).status, 404); assert.equal(calls.length, 0); }); test("produto fora do cache é buscado na API com User-Agent e gravado no cache", async () => { handler = () => ({ ok: true, body: { status: 1, product: { code: "3017620422003", product_name: "Nutella" } } }); const { status, body } = await call("3017620422003"); assert.equal(status, 200); assert.equal(body.product_name, "Nutella"); assert.equal(body.fromAPI, true); assert.equal(body.fromCache, false); assert.equal(calls.length, 1); assert.equal(calls[0].url.href, "https://world.openfoodfacts.org/api/v2/product/3017620422003.json"); assert.match((calls[0].init?.headers as Record)["User-Agent"], /^NutriScan\//); assert.ok(calls[0].init?.signal, "a requisição deve ter timeout"); const saved = JSON.parse(readFileSync(cacheFile, "utf-8")); assert.equal(saved.products["3017620422003"].product_name, "Nutella"); calls.length = 0; assert.equal((await call("3017620422003")).body.fromCache, true); assert.equal(calls.length, 0); }); test("API com erro HTTP, status 0 ou sem product responde 404", async () => { handler = () => ({ ok: false }); assert.equal((await call("1111111111111")).status, 404); handler = () => ({ ok: true, body: { status: 0 } }); assert.equal((await call("2222222222222")).status, 404); handler = () => ({ ok: true, body: { status: 1 } }); const { status, body } = await call("3333333333333"); assert.equal(status, 404); assert.equal(body.error, "Produto não encontrado"); }); test("falha de rede na API responde 500", async () => { handler = () => { throw new Error("rede caiu"); }; const { status, body } = await call("4444444444444"); assert.equal(status, 500); assert.equal(body.error, "Erro interno do servidor"); });