Files
nutriscan/tests/cache-sync.test.ts
T
evandro 30d7ef701f fix: cache JSON concorrente, validação do sync e timeout nas chamadas ao Open Food Facts
- JsonCacheManager: a leitura inicial concorrente carregava o arquivo duas
  vezes e a segunda descartava as alterações da primeira; gravações
  simultâneas escreviam no mesmo arquivo. Agora a leitura é compartilhada
  e a gravação é enfileirada, via arquivo temporário + rename.
- POST /api/cache/sync: aceitava qualquer objeto e usava code como chave
  do cache (inclusive ausente ou não numérico); agora só grava produtos
  com code numérico e conta os demais em failed. JSON malformado responde
  400 em vez de 500.
- GET /api/products/[barcode]: resposta da API sem product virava 200
  com corpo vazio; agora responde 404.
- Chamadas ao Open Food Facts (busca e produto) passam a ter timeout de
  15 s e o User-Agent vem de um único lugar (lib/offSearch).
2026-10-10 04:18:59 -03:00

81 lines
3.1 KiB
TypeScript

import assert from "node:assert/strict";
import { existsSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import test, { before } from "node:test";
/** Rota POST /api/cache/sync e gravação concorrente no cache JSON, em pasta temporária. */
let POST: (req: import("next/server").NextRequest) => Promise<Response>;
let NextRequestCtor: typeof import("next/server").NextRequest;
let JsonCacheManager: typeof import("../lib/jsonCacheManager.ts").JsonCacheManager;
let cacheFile: string;
before(async () => {
const dir = mkdtempSync(path.join(tmpdir(), "nutriscan-sync-"));
mkdirSync(path.join(dir, "data"));
cacheFile = path.join(dir, "data", "products-cache.json");
process.chdir(dir);
console.log = () => {};
console.error = () => {};
({ POST } = await import("../app/api/cache/sync/route.ts"));
({ NextRequest: NextRequestCtor } = await import("next/server"));
({ JsonCacheManager } = await import("../lib/jsonCacheManager.ts"));
});
async function post(body: string) {
const req = new NextRequestCtor("http://localhost/api/cache/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});
const res = await POST(req);
return { status: res.status, body: (await res.json()) as Record<string, unknown> };
}
const readCache = () => JSON.parse(readFileSync(cacheFile, "utf-8")) as { products: Record<string, { product_name: string }> };
test("corpo que não é JSON, sem products ou com products fora de lista responde 400", async () => {
for (const body of ["não é json", "{}", "null", '{"products":"x"}', '{"products":{"a":1}}']) {
const { status, body: json } = await post(body);
assert.equal(status, 400, body);
assert.equal(json.error, "Produtos inválidos");
}
});
test("lista vazia é aceita", async () => {
const { status, body } = await post('{"products":[]}');
assert.equal(status, 200);
assert.deepEqual(body, { success: true, saved: 0, failed: 0 });
});
test("só produtos com code numérico são gravados; os demais contam como falha", async () => {
const products = [
{ code: "7622210575975", product_name: "Válido" },
{ code: "__proto__", product_name: "Chave perigosa" },
{ code: "abc", product_name: "Não numérico" },
{ code: 123, product_name: "Número" },
{ product_name: "Sem code" },
null,
"texto",
];
const { status, body } = await post(JSON.stringify({ products }));
assert.equal(status, 200);
assert.deepEqual(body, { success: true, saved: 1, failed: 6 });
assert.deepEqual(Object.keys(readCache().products), ["7622210575975"]);
});
test("gravações simultâneas não corrompem o arquivo nem deixam temporário", async () => {
const manager = new JsonCacheManager();
const batches = Array.from({ length: 25 }, (_, b) =>
Array.from({ length: 40 }, (_, i) => ({ code: String(1000 + b * 40 + i), product_name: `Produto ${b}-${i}` }))
);
await Promise.all(batches.map((batch) => manager.saveProducts(batch)));
const saved = readCache();
assert.equal(Object.keys(saved.products).length, 1000 + 1);
assert.equal(existsSync(`${cacheFile}.tmp`), false);
});