- 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).
81 lines
3.1 KiB
TypeScript
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);
|
|
});
|