fix: strictly parse route integers
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { fetchEbayItems } from "@marketplace-scrapers/core";
|
||||
import { logger } from "../logger";
|
||||
import {
|
||||
emptySearchResponse,
|
||||
getRequiredSearchQuery,
|
||||
parseNonNegativeIntegerParam,
|
||||
} from "./helpers";
|
||||
|
||||
/**
|
||||
* GET /api/ebay?q={query}&minPrice={minPrice}&maxPrice={maxPrice}&strictMode={strictMode}&exclusions={exclusions}&keywords={keywords}&buyItNowOnly={buyItNowOnly}&canadaOnly={canadaOnly}
|
||||
@@ -9,32 +14,24 @@ export async function ebayRoute(req: Request): Promise<Response> {
|
||||
try {
|
||||
const reqUrl = new URL(req.url);
|
||||
|
||||
const SEARCH_QUERY =
|
||||
req.headers.get("query") || reqUrl.searchParams.get("q") || null;
|
||||
if (!SEARCH_QUERY)
|
||||
return Response.json(
|
||||
{
|
||||
message:
|
||||
"Request didn't have 'query' header or 'q' search parameter!",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
const minPriceParam = reqUrl.searchParams.get("minPrice");
|
||||
const minPrice = minPriceParam ? parseInt(minPriceParam, 10) : undefined;
|
||||
if (minPriceParam && (Number.isNaN(minPrice) || (minPrice ?? 0) < 0)) {
|
||||
return Response.json(
|
||||
{ message: "Invalid minPrice parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const SEARCH_QUERY = getRequiredSearchQuery(req);
|
||||
if (SEARCH_QUERY instanceof Response) {
|
||||
return SEARCH_QUERY;
|
||||
}
|
||||
const maxPriceParam = reqUrl.searchParams.get("maxPrice");
|
||||
const maxPrice = maxPriceParam ? parseInt(maxPriceParam, 10) : undefined;
|
||||
if (maxPriceParam && (Number.isNaN(maxPrice) || (maxPrice ?? 0) < 0)) {
|
||||
return Response.json(
|
||||
{ message: "Invalid maxPrice parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
const minPrice = parseNonNegativeIntegerParam(
|
||||
reqUrl.searchParams,
|
||||
"minPrice",
|
||||
);
|
||||
if (minPrice instanceof Response) {
|
||||
return minPrice;
|
||||
}
|
||||
const maxPrice = parseNonNegativeIntegerParam(
|
||||
reqUrl.searchParams,
|
||||
"maxPrice",
|
||||
);
|
||||
if (maxPrice instanceof Response) {
|
||||
return maxPrice;
|
||||
}
|
||||
const strictMode = reqUrl.searchParams.get("strictMode") === "true";
|
||||
const buyItNowOnly = reqUrl.searchParams.get("buyItNowOnly") !== "false";
|
||||
@@ -48,13 +45,12 @@ export async function ebayRoute(req: Request): Promise<Response> {
|
||||
? keywordsParam.split(",").map((s) => s.trim())
|
||||
: [SEARCH_QUERY];
|
||||
|
||||
const maxItemsParam = reqUrl.searchParams.get("maxItems");
|
||||
const maxItems = maxItemsParam ? parseInt(maxItemsParam, 10) : undefined;
|
||||
if (maxItemsParam && (Number.isNaN(maxItems) || (maxItems ?? 0) < 0)) {
|
||||
return Response.json(
|
||||
{ message: "Invalid maxItems parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const maxItems = parseNonNegativeIntegerParam(
|
||||
reqUrl.searchParams,
|
||||
"maxItems",
|
||||
);
|
||||
if (maxItems instanceof Response) {
|
||||
return maxItems;
|
||||
}
|
||||
const hideUnstableResults =
|
||||
reqUrl.searchParams.get("unstableFilter") === "true";
|
||||
@@ -73,10 +69,7 @@ export async function ebayRoute(req: Request): Promise<Response> {
|
||||
hideUnstableResults: true,
|
||||
});
|
||||
if (items.results.length === 0 && items.unstableResults.length === 0) {
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
return emptySearchResponse();
|
||||
}
|
||||
return Response.json(items, { status: 200 });
|
||||
}
|
||||
@@ -84,11 +77,9 @@ export async function ebayRoute(req: Request): Promise<Response> {
|
||||
const items = await fetchEbayItems(SEARCH_QUERY, 1, opts);
|
||||
const isEmpty = !items || items.length === 0;
|
||||
|
||||
if (isEmpty)
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
if (isEmpty) {
|
||||
return emptySearchResponse();
|
||||
}
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
logger.error("eBay scraping error:", error);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { fetchFacebookItems } from "@marketplace-scrapers/core";
|
||||
import { logger } from "../logger";
|
||||
import {
|
||||
emptySearchResponse,
|
||||
getRequiredSearchQuery,
|
||||
parseNonNegativeIntegerParam,
|
||||
} from "./helpers";
|
||||
|
||||
/**
|
||||
* GET /api/facebook?q={query}&location={location}
|
||||
@@ -8,24 +13,19 @@ import { logger } from "../logger";
|
||||
export async function facebookRoute(req: Request): Promise<Response> {
|
||||
const reqUrl = new URL(req.url);
|
||||
|
||||
const SEARCH_QUERY =
|
||||
req.headers.get("query") || reqUrl.searchParams.get("q") || null;
|
||||
if (!SEARCH_QUERY)
|
||||
return Response.json(
|
||||
{
|
||||
message: "Request didn't have 'query' header or 'q' search parameter!",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
const SEARCH_QUERY = getRequiredSearchQuery(req);
|
||||
if (SEARCH_QUERY instanceof Response) {
|
||||
return SEARCH_QUERY;
|
||||
}
|
||||
|
||||
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) || maxItems < 0)) {
|
||||
return Response.json(
|
||||
{ message: "Invalid maxItems parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const maxItems = parseNonNegativeIntegerParam(
|
||||
reqUrl.searchParams,
|
||||
"maxItems",
|
||||
25,
|
||||
);
|
||||
if (maxItems instanceof Response) {
|
||||
return maxItems;
|
||||
}
|
||||
const hideUnstableResults =
|
||||
reqUrl.searchParams.get("unstableFilter") === "true";
|
||||
@@ -42,20 +42,15 @@ export async function facebookRoute(req: Request): Promise<Response> {
|
||||
},
|
||||
);
|
||||
if (items.results.length === 0 && items.unstableResults.length === 0) {
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
return emptySearchResponse();
|
||||
}
|
||||
return Response.json(items, { status: 200 });
|
||||
}
|
||||
|
||||
const items = await fetchFacebookItems(SEARCH_QUERY, 1, LOCATION, maxItems);
|
||||
if (!items || items.length === 0)
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
if (!items || items.length === 0) {
|
||||
return emptySearchResponse();
|
||||
}
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
logger.error("Facebook scraping error:", error);
|
||||
|
||||
39
packages/api-server/src/routes/helpers.ts
Normal file
39
packages/api-server/src/routes/helpers.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export function getRequiredSearchQuery(req: Request): string | Response {
|
||||
const reqUrl = new URL(req.url);
|
||||
const query = req.headers.get("query") || reqUrl.searchParams.get("q");
|
||||
if (!query) {
|
||||
return Response.json(
|
||||
{
|
||||
message: "Request didn't have 'query' header or 'q' search parameter!",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export function parseNonNegativeIntegerParam(
|
||||
searchParams: URLSearchParams,
|
||||
name: string,
|
||||
defaultValue?: number,
|
||||
): number | undefined | Response {
|
||||
const rawValue = searchParams.get(name);
|
||||
if (rawValue === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
const value = Number(rawValue);
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
return Response.json(
|
||||
{ message: `Invalid ${name} parameter` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function emptySearchResponse(): Response {
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { fetchKijijiItems } from "@marketplace-scrapers/core";
|
||||
import { logger } from "../logger";
|
||||
import {
|
||||
emptySearchResponse,
|
||||
getRequiredSearchQuery,
|
||||
parseNonNegativeIntegerParam,
|
||||
} from "./helpers";
|
||||
|
||||
/**
|
||||
* GET /api/kijiji?q={query}
|
||||
@@ -8,39 +13,32 @@ import { logger } from "../logger";
|
||||
export async function kijijiRoute(req: Request): Promise<Response> {
|
||||
const reqUrl = new URL(req.url);
|
||||
|
||||
const SEARCH_QUERY =
|
||||
req.headers.get("query") || reqUrl.searchParams.get("q") || null;
|
||||
if (!SEARCH_QUERY)
|
||||
return Response.json(
|
||||
{
|
||||
message: "Request didn't have 'query' header or 'q' search parameter!",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
const SEARCH_QUERY = getRequiredSearchQuery(req);
|
||||
if (SEARCH_QUERY instanceof Response) {
|
||||
return SEARCH_QUERY;
|
||||
}
|
||||
|
||||
const maxPagesParam = reqUrl.searchParams.get("maxPages");
|
||||
const maxPages = maxPagesParam ? parseInt(maxPagesParam, 10) : 5;
|
||||
if (maxPagesParam && (Number.isNaN(maxPages) || maxPages < 0)) {
|
||||
return Response.json(
|
||||
{ message: "Invalid maxPages parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const maxPages = parseNonNegativeIntegerParam(
|
||||
reqUrl.searchParams,
|
||||
"maxPages",
|
||||
5,
|
||||
);
|
||||
if (maxPages instanceof Response) {
|
||||
return maxPages;
|
||||
}
|
||||
const priceMinParam = reqUrl.searchParams.get("priceMin");
|
||||
const priceMin = priceMinParam ? parseInt(priceMinParam, 10) : undefined;
|
||||
if (priceMinParam && (Number.isNaN(priceMin) || (priceMin ?? 0) < 0)) {
|
||||
return Response.json(
|
||||
{ message: "Invalid priceMin parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const priceMin = parseNonNegativeIntegerParam(
|
||||
reqUrl.searchParams,
|
||||
"priceMin",
|
||||
);
|
||||
if (priceMin instanceof Response) {
|
||||
return priceMin;
|
||||
}
|
||||
const priceMaxParam = reqUrl.searchParams.get("priceMax");
|
||||
const priceMax = priceMaxParam ? parseInt(priceMaxParam, 10) : undefined;
|
||||
if (priceMaxParam && (Number.isNaN(priceMax) || (priceMax ?? 0) < 0)) {
|
||||
return Response.json(
|
||||
{ message: "Invalid priceMax parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const priceMax = parseNonNegativeIntegerParam(
|
||||
reqUrl.searchParams,
|
||||
"priceMax",
|
||||
);
|
||||
if (priceMax instanceof Response) {
|
||||
return priceMax;
|
||||
}
|
||||
const hideUnstableResults =
|
||||
reqUrl.searchParams.get("unstableFilter") === "true";
|
||||
@@ -75,10 +73,7 @@ export async function kijijiRoute(req: Request): Promise<Response> {
|
||||
{ hideUnstableResults: true },
|
||||
);
|
||||
if (items.results.length === 0 && items.unstableResults.length === 0) {
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
return emptySearchResponse();
|
||||
}
|
||||
return Response.json(items, { status: 200 });
|
||||
}
|
||||
@@ -90,11 +85,9 @@ export async function kijijiRoute(req: Request): Promise<Response> {
|
||||
searchOptions,
|
||||
{},
|
||||
);
|
||||
if (!items || items.length === 0)
|
||||
return Response.json(
|
||||
{ message: "Search didn't return any results!" },
|
||||
{ status: 404 },
|
||||
);
|
||||
if (!items || items.length === 0) {
|
||||
return emptySearchResponse();
|
||||
}
|
||||
return Response.json(items, { status: 200 });
|
||||
} catch (error) {
|
||||
logger.error("Kijiji scraping error:", error);
|
||||
|
||||
@@ -513,6 +513,18 @@ describe("API routes", () => {
|
||||
expect(body.message).toBe("Invalid maxItems parameter");
|
||||
});
|
||||
|
||||
test("ebayRoute rejects non-integer minPrice", async () => {
|
||||
const { ebayRoute } = await import("../src/routes/ebay");
|
||||
|
||||
const response = await ebayRoute(
|
||||
new Request("http://localhost/api/ebay?q=laptop&minPrice=10abc"),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const body = await response.json();
|
||||
expect(body.message).toBe("Invalid minPrice parameter");
|
||||
});
|
||||
|
||||
test("ebayRoute returns 400 for invalid minPrice", async () => {
|
||||
const { ebayRoute } = await import("../src/routes/ebay");
|
||||
|
||||
@@ -525,6 +537,18 @@ describe("API routes", () => {
|
||||
expect(body.message).toBe("Invalid minPrice parameter");
|
||||
});
|
||||
|
||||
test("ebayRoute rejects non-integer maxPrice", async () => {
|
||||
const { ebayRoute } = await import("../src/routes/ebay");
|
||||
|
||||
const response = await ebayRoute(
|
||||
new Request("http://localhost/api/ebay?q=laptop&maxPrice=10abc"),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const body = await response.json();
|
||||
expect(body.message).toBe("Invalid maxPrice parameter");
|
||||
});
|
||||
|
||||
test("ebayRoute returns 400 for invalid maxPrice", async () => {
|
||||
const { ebayRoute } = await import("../src/routes/ebay");
|
||||
|
||||
@@ -537,6 +561,18 @@ describe("API routes", () => {
|
||||
expect(body.message).toBe("Invalid maxPrice parameter");
|
||||
});
|
||||
|
||||
test("kijijiRoute rejects decimal maxPages", async () => {
|
||||
const { kijijiRoute } = await import("../src/routes/kijiji");
|
||||
|
||||
const response = await kijijiRoute(
|
||||
new Request("http://localhost/api/kijiji?q=laptop&maxPages=1.5"),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const body = await response.json();
|
||||
expect(body.message).toBe("Invalid maxPages parameter");
|
||||
});
|
||||
|
||||
test("kijijiRoute returns 400 for invalid maxPages", async () => {
|
||||
const { kijijiRoute } = await import("../src/routes/kijiji");
|
||||
|
||||
@@ -573,6 +609,18 @@ describe("API routes", () => {
|
||||
expect(body.message).toBe("Invalid priceMax parameter");
|
||||
});
|
||||
|
||||
test("facebookRoute rejects non-integer maxItems", async () => {
|
||||
const { facebookRoute } = await import("../src/routes/facebook");
|
||||
|
||||
const response = await facebookRoute(
|
||||
new Request("http://localhost/api/facebook?q=laptop&maxItems=10abc"),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const body = await response.json();
|
||||
expect(body.message).toBe("Invalid maxItems parameter");
|
||||
});
|
||||
|
||||
test("facebookRoute returns 400 for negative maxItems", async () => {
|
||||
const { facebookRoute } = await import("../src/routes/facebook");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user