From 77b9fc993430a5d2337092faedf221184207cfcf Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 27 Apr 2026 09:45:47 -0400 Subject: [PATCH] fix: validate route params and reduce duplication --- packages/api-server/src/routes/ebay.ts | 45 +++++++------- packages/api-server/src/routes/facebook.ts | 18 ++++-- packages/api-server/src/routes/kijiji.ts | 32 ++++------ packages/api-server/test/routes.test.ts | 71 +++++++++++++++++----- 4 files changed, 103 insertions(+), 63 deletions(-) diff --git a/packages/api-server/src/routes/ebay.ts b/packages/api-server/src/routes/ebay.ts index 9ab874c..21f0730 100644 --- a/packages/api-server/src/routes/ebay.ts +++ b/packages/api-server/src/routes/ebay.ts @@ -37,32 +37,29 @@ export async function ebayRoute(req: Request): Promise { const maxItemsParam = reqUrl.searchParams.get("maxItems"); const maxItems = maxItemsParam ? parseInt(maxItemsParam, 10) : undefined; + if (maxItemsParam && Number.isNaN(maxItems)) { + return Response.json( + { message: "Invalid maxItems parameter" }, + { status: 400 }, + ); + } const hideUnstableResults = reqUrl.searchParams.get("unstableFilter") === "true"; - const items = hideUnstableResults - ? await fetchEbayItems( - SEARCH_QUERY, - 1, - { - minPrice, - maxPrice, - strictMode, - exclusions, - keywords, - buyItNowOnly, - canadaOnly, - }, - { hideUnstableResults: true }, - ) - : await fetchEbayItems(SEARCH_QUERY, 1, { - minPrice, - maxPrice, - strictMode, - exclusions, - keywords, - buyItNowOnly, - canadaOnly, - }); + const opts = { + minPrice, + maxPrice, + strictMode, + exclusions, + keywords, + buyItNowOnly, + canadaOnly, + }; + const items = await fetchEbayItems( + SEARCH_QUERY, + 1, + opts, + ...(hideUnstableResults ? [{ hideUnstableResults: true }] : []), + ); const isEmpty = hideUnstableResults ? items.results.length === 0 && items.unstableResults.length === 0 diff --git a/packages/api-server/src/routes/facebook.ts b/packages/api-server/src/routes/facebook.ts index 1236c40..a401cd0 100644 --- a/packages/api-server/src/routes/facebook.ts +++ b/packages/api-server/src/routes/facebook.ts @@ -20,15 +20,23 @@ export async function facebookRoute(req: Request): Promise { const LOCATION = reqUrl.searchParams.get("location") || "toronto"; const maxItemsParam = reqUrl.searchParams.get("maxItems"); const maxItems = maxItemsParam ? parseInt(maxItemsParam, 10) : 25; + if (maxItemsParam && Number.isNaN(maxItems)) { + return Response.json( + { message: "Invalid maxItems parameter" }, + { status: 400 }, + ); + } const hideUnstableResults = reqUrl.searchParams.get("unstableFilter") === "true"; try { - const items = hideUnstableResults - ? await fetchFacebookItems(SEARCH_QUERY, 1, LOCATION, maxItems, { - hideUnstableResults: true, - }) - : await fetchFacebookItems(SEARCH_QUERY, 1, LOCATION, maxItems); + const items = await fetchFacebookItems( + SEARCH_QUERY, + 1, + LOCATION, + maxItems, + ...(hideUnstableResults ? [{ hideUnstableResults: true }] : []), + ); const isEmpty = hideUnstableResults ? items.results.length === 0 && items.unstableResults.length === 0 diff --git a/packages/api-server/src/routes/kijiji.ts b/packages/api-server/src/routes/kijiji.ts index 014e04a..153824c 100644 --- a/packages/api-server/src/routes/kijiji.ts +++ b/packages/api-server/src/routes/kijiji.ts @@ -30,16 +30,16 @@ export async function kijijiRoute(req: Request): Promise { location: reqUrl.searchParams.get("location") || undefined, category: reqUrl.searchParams.get("category") || undefined, keywords: reqUrl.searchParams.get("keywords") || undefined, - sortBy: reqUrl.searchParams.get("sortBy") as + sortBy: (reqUrl.searchParams.get("sortBy") as | "relevancy" | "date" | "price" | "distance" - | undefined, - sortOrder: reqUrl.searchParams.get("sortOrder") as + | undefined) || undefined, + sortOrder: (reqUrl.searchParams.get("sortOrder") as | "desc" | "asc" - | undefined, + | undefined) || undefined, maxPages, priceMin, priceMax, @@ -47,22 +47,14 @@ export async function kijijiRoute(req: Request): Promise { }; try { - const items = hideUnstableResults - ? await fetchKijijiItems( - SEARCH_QUERY, - 4, // 4 requests per second for faster scraping - "https://www.kijiji.ca", - searchOptions, - {}, - { hideUnstableResults: true }, - ) - : await fetchKijijiItems( - SEARCH_QUERY, - 4, // 4 requests per second for faster scraping - "https://www.kijiji.ca", - searchOptions, - {}, - ); + const items = await fetchKijijiItems( + SEARCH_QUERY, + 4, // 4 requests per second for faster scraping + "https://www.kijiji.ca", + searchOptions, + {}, + ...(hideUnstableResults ? [{ hideUnstableResults: true }] : []), + ); const isEmpty = hideUnstableResults ? items.results.length === 0 && items.unstableResults.length === 0 diff --git a/packages/api-server/test/routes.test.ts b/packages/api-server/test/routes.test.ts index 358e45f..2c221f6 100644 --- a/packages/api-server/test/routes.test.ts +++ b/packages/api-server/test/routes.test.ts @@ -26,12 +26,6 @@ describe("API routes", () => { ); }); - afterEach(() => { - fetchFacebookItems.mockClear(); - fetchEbayItems.mockClear(); - fetchKijijiItems.mockClear(); - }); - test("facebookRoute ignores cookies query parameter", async () => { const { facebookRoute } = await import("../src/routes/facebook"); @@ -81,8 +75,8 @@ describe("API routes", () => { location: undefined, category: undefined, keywords: undefined, - sortBy: null, - sortOrder: null, + sortBy: undefined, + sortOrder: undefined, maxPages: 3, priceMin: undefined, priceMax: undefined, @@ -166,8 +160,8 @@ describe("API routes", () => { location: undefined, category: undefined, keywords: undefined, - sortBy: null, - sortOrder: null, + sortBy: undefined, + sortOrder: undefined, maxPages: 5, priceMin: undefined, priceMax: undefined, @@ -261,8 +255,8 @@ describe("API routes", () => { location: undefined, category: undefined, keywords: undefined, - sortBy: null, - sortOrder: null, + sortBy: undefined, + sortOrder: undefined, maxPages: 5, priceMin: undefined, priceMax: undefined, @@ -289,8 +283,8 @@ describe("API routes", () => { location: undefined, category: undefined, keywords: undefined, - sortBy: null, - sortOrder: null, + sortBy: undefined, + sortOrder: undefined, maxPages: 5, priceMin: undefined, priceMax: undefined, @@ -429,4 +423,53 @@ describe("API routes", () => { expect(body.results[0].title).toBe("a"); expect(body.unstableResults[0].title).toBe("d"); }); + + test("ebayRoute returns 404 when unstable results are empty", async () => { + const { ebayRoute } = await import("../src/routes/ebay"); + + fetchEbayItems.mockImplementation(() => + Promise.resolve({ + results: [], + unstableResults: [], + }), + ); + + const response = await ebayRoute( + new Request( + "http://localhost/api/ebay?q=laptop&unstableFilter=true", + ), + ); + + expect(response.status).toBe(404); + const body = await response.json(); + expect(body.message).toBe("Search didn't return any results!"); + }); + + test("ebayRoute returns 400 for invalid maxItems", async () => { + const { ebayRoute } = await import("../src/routes/ebay"); + + const response = await ebayRoute( + new Request( + "http://localhost/api/ebay?q=laptop&maxItems=abc", + ), + ); + + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.message).toBe("Invalid maxItems parameter"); + }); + + test("facebookRoute returns 400 for invalid maxItems", async () => { + const { facebookRoute } = await import("../src/routes/facebook"); + + const response = await facebookRoute( + new Request( + "http://localhost/api/facebook?q=laptop&maxItems=abc", + ), + ); + + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.message).toBe("Invalid maxItems parameter"); + }); });