import assert from "node:assert/strict"; import test from "node:test"; import { buildOffSearchParams, fetchOffProducts, OFF_RATE_LIMIT_MS, OFF_USER_AGENT, parsePagination, type OffFetchDeps, } from "../lib/offSearch.ts"; const qs = (query: string) => new URLSearchParams(query); test("parsePagination aplica padrões e rejeita valores inválidos", () => { assert.deepEqual(parsePagination(qs("")), { page: 1, pageSize: 20, pageParam: "1" }); assert.deepEqual(parsePagination(qs("page=3&page_size=50")), { page: 3, pageSize: 50, pageParam: "3" }); assert.deepEqual(parsePagination(qs("page=0")), { error: "Invalid page parameter" }); assert.deepEqual(parsePagination(qs("page=x")), { error: "Invalid page parameter" }); assert.deepEqual(parsePagination(qs("page_size=0")), { error: "Invalid page_size parameter" }); assert.deepEqual(parsePagination(qs("page_size=-1")), { error: "Invalid page_size parameter" }); }); test("buildOffSearchParams monta os parâmetros básicos e ignora filtros vazios", () => { const params = buildOffSearchParams(qs("brands=&sort_by="), "leite", null, "1"); assert.deepEqual([...params.keys()].sort(), ["fields", "json", "page", "page_size", "search_terms"]); assert.equal(params.get("search_terms"), "leite"); assert.equal(params.get("page_size"), "1000"); }); test("buildOffSearchParams ordena com sort_order padrão e campo popularity para valores desconhecidos", () => { const known = buildOffSearchParams(qs("sort_by=fat&sort_order=desc"), "x", null, "1"); assert.equal(known.get("sort_by"), "nutriments.fat_100g"); assert.equal(known.get("sort_order"), "desc"); const unknown = buildOffSearchParams(qs("sort_by=constructor"), "x", null, "1"); assert.equal(unknown.get("sort_by"), "popularity"); assert.equal(unknown.get("sort_order"), "asc"); }); test("buildOffSearchParams repassa país e filtros com os nomes da API", () => { const params = buildOffSearchParams(qs("allergens=milk&fat_max=5&no_additives=true"), "x", "brazil", "1"); assert.equal(params.get("countries_tags_en"), "brazil"); assert.equal(params.get("allergens_tags"), "milk"); assert.equal(params.get("fat_max"), "5"); assert.equal(params.get("additives_tags"), "without-additives"); }); function makeDeps(pages: Array | "erro">) { const urls: URL[] = []; const headers: Array = []; const sleeps: number[] = []; const deps: OffFetchDeps = { fetchFn: (async (input: string | URL | Request, init?: RequestInit) => { urls.push(new URL(String(input))); headers.push(init?.headers); const page = pages[urls.length - 1]; return page === "erro" || page === undefined ? ({ ok: false, status: 503, statusText: "Service Unavailable" } as Response) : ({ ok: true, json: async () => ({ products: page }) } as Response); }) as typeof fetch, sleep: async (ms) => { sleeps.push(ms); }, }; return { deps, urls, headers, sleeps }; } const batch = (n: number, prefix = "p") => Array.from({ length: n }, (_, i) => ({ code: `${prefix}${i}` })); test("fetchOffProducts envia User-Agent e pausa entre páginas cheias", async () => { const { deps, urls, headers, sleeps } = makeDeps([batch(1000, "a"), batch(1000, "b"), batch(5, "c")]); const products = await fetchOffProducts(new URLSearchParams({ search_terms: "x" }), 2500, deps); assert.equal(products.length, 2005); assert.deepEqual(urls.map((u) => u.searchParams.get("page")), ["1", "2", "3"]); assert.equal(urls[0].searchParams.get("search_terms"), "x"); assert.deepEqual(sleeps, [OFF_RATE_LIMIT_MS, OFF_RATE_LIMIT_MS]); assert.equal((headers[0] as Record)["User-Agent"], OFF_USER_AGENT); }); test("fetchOffProducts para quando já reuniu o necessário, sem pausa", async () => { const { deps, urls, sleeps } = makeDeps([batch(1000)]); const products = await fetchOffProducts(new URLSearchParams(), 500, deps); assert.equal(products.length, 1000); assert.equal(urls.length, 1); assert.deepEqual(sleeps, []); }); test("fetchOffProducts devolve o que já obteve quando uma página falha", async () => { const { deps, urls } = makeDeps([batch(1000), "erro"]); const products = await fetchOffProducts(new URLSearchParams(), 3000, deps); assert.equal(products.length, 1000); assert.equal(urls.length, 2); }); test("fetchOffProducts trata página vazia e resposta sem products", async () => { const empty = makeDeps([[]]); assert.deepEqual(await fetchOffProducts(new URLSearchParams(), 10, empty.deps), []); const noKey: OffFetchDeps = { fetchFn: (async () => ({ ok: true, json: async () => ({}) }) as Response) as typeof fetch, sleep: async () => {}, }; assert.deepEqual(await fetchOffProducts(new URLSearchParams(), 10, noKey), []); });