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).
This commit is contained in:
1 parent
5ee5dcd893
commit
30d7ef701f
7 files changed
+273
-30
No files matched your search
@@ -0,0 +1,80 @@
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user