fix(api): parse price filters as dollars
This commit is contained in:
@@ -3,6 +3,7 @@ import { logger } from "../logger";
|
|||||||
import {
|
import {
|
||||||
emptySearchResponse,
|
emptySearchResponse,
|
||||||
getRequiredSearchQuery,
|
getRequiredSearchQuery,
|
||||||
|
parseDollarPriceParam,
|
||||||
parseNonNegativeIntegerParam,
|
parseNonNegativeIntegerParam,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
@@ -18,17 +19,11 @@ export async function ebayRoute(req: Request): Promise<Response> {
|
|||||||
return SEARCH_QUERY;
|
return SEARCH_QUERY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minPrice = parseNonNegativeIntegerParam(
|
const minPrice = parseDollarPriceParam(reqUrl.searchParams, "minPrice");
|
||||||
reqUrl.searchParams,
|
|
||||||
"minPrice",
|
|
||||||
);
|
|
||||||
if (minPrice instanceof Response) {
|
if (minPrice instanceof Response) {
|
||||||
return minPrice;
|
return minPrice;
|
||||||
}
|
}
|
||||||
const maxPrice = parseNonNegativeIntegerParam(
|
const maxPrice = parseDollarPriceParam(reqUrl.searchParams, "maxPrice");
|
||||||
reqUrl.searchParams,
|
|
||||||
"maxPrice",
|
|
||||||
);
|
|
||||||
if (maxPrice instanceof Response) {
|
if (maxPrice instanceof Response) {
|
||||||
return maxPrice;
|
return maxPrice;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,23 @@ export function parseNonNegativeIntegerParam(
|
|||||||
return Number(rawValue);
|
return Number(rawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseDollarPriceParam(
|
||||||
|
searchParams: URLSearchParams,
|
||||||
|
name: string,
|
||||||
|
): number | undefined | Response {
|
||||||
|
const rawValue = searchParams.get(name);
|
||||||
|
if (rawValue === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!/^\d+(?:\.\d{1,2})?$/.test(rawValue)) {
|
||||||
|
return Response.json(
|
||||||
|
{ message: `Invalid ${name} parameter` },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Math.round(Number(rawValue) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
export function emptySearchResponse(hint?: string): Response {
|
export function emptySearchResponse(hint?: string): Response {
|
||||||
const message = hint
|
const message = hint
|
||||||
? `Search didn't return any results! ${hint}`
|
? `Search didn't return any results! ${hint}`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { logger } from "../logger";
|
|||||||
import {
|
import {
|
||||||
emptySearchResponse,
|
emptySearchResponse,
|
||||||
getRequiredSearchQuery,
|
getRequiredSearchQuery,
|
||||||
|
parseDollarPriceParam,
|
||||||
parseNonNegativeIntegerParam,
|
parseNonNegativeIntegerParam,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
@@ -26,17 +27,11 @@ export async function kijijiRoute(req: Request): Promise<Response> {
|
|||||||
if (maxPages instanceof Response) {
|
if (maxPages instanceof Response) {
|
||||||
return maxPages;
|
return maxPages;
|
||||||
}
|
}
|
||||||
const priceMin = parseNonNegativeIntegerParam(
|
const priceMin = parseDollarPriceParam(reqUrl.searchParams, "priceMin");
|
||||||
reqUrl.searchParams,
|
|
||||||
"priceMin",
|
|
||||||
);
|
|
||||||
if (priceMin instanceof Response) {
|
if (priceMin instanceof Response) {
|
||||||
return priceMin;
|
return priceMin;
|
||||||
}
|
}
|
||||||
const priceMax = parseNonNegativeIntegerParam(
|
const priceMax = parseDollarPriceParam(reqUrl.searchParams, "priceMax");
|
||||||
reqUrl.searchParams,
|
|
||||||
"priceMax",
|
|
||||||
);
|
|
||||||
if (priceMax instanceof Response) {
|
if (priceMax instanceof Response) {
|
||||||
return priceMax;
|
return priceMax;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,6 +282,24 @@ describe("API routes", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("kijijiRoute forwards dollar price filters to core as cents", async () => {
|
||||||
|
const { kijijiRoute } = await import("../src/routes/kijiji");
|
||||||
|
|
||||||
|
await kijijiRoute(
|
||||||
|
new Request(
|
||||||
|
"http://localhost/api/kijiji?q=laptop&priceMin=999.99&priceMax=1000",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fetchKijijiItems).toHaveBeenCalledWith(
|
||||||
|
"laptop",
|
||||||
|
4,
|
||||||
|
"https://www.kijiji.ca",
|
||||||
|
expect.objectContaining({ priceMin: 99_999, priceMax: 100_000 }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("kijijiRoute does not forward unstableFilter when false", async () => {
|
test("kijijiRoute does not forward unstableFilter when false", async () => {
|
||||||
const { kijijiRoute } = await import("../src/routes/kijiji");
|
const { kijijiRoute } = await import("../src/routes/kijiji");
|
||||||
|
|
||||||
@@ -414,6 +432,24 @@ describe("API routes", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ebayRoute forwards dollar price filters to core as cents", async () => {
|
||||||
|
const { ebayRoute } = await import("../src/routes/ebay");
|
||||||
|
|
||||||
|
fetchEbayItems.mockImplementation(() => Promise.resolve([{ title: "a" }]));
|
||||||
|
|
||||||
|
await ebayRoute(
|
||||||
|
new Request(
|
||||||
|
"http://localhost/api/ebay?q=macbook&minPrice=999.99&maxPrice=1000",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fetchEbayItems).toHaveBeenCalledWith(
|
||||||
|
"macbook",
|
||||||
|
1,
|
||||||
|
expect.objectContaining({ minPrice: 99_999, maxPrice: 100_000 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("ebayRoute passes through scraper payload unchanged in unstable mode", async () => {
|
test("ebayRoute passes through scraper payload unchanged in unstable mode", async () => {
|
||||||
const { ebayRoute } = await import("../src/routes/ebay");
|
const { ebayRoute } = await import("../src/routes/ebay");
|
||||||
|
|
||||||
@@ -730,16 +766,18 @@ describe("API routes", () => {
|
|||||||
expect(body.message).toBe("Invalid minPrice parameter");
|
expect(body.message).toBe("Invalid minPrice parameter");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ebayRoute returns 400 for decimal minPrice", async () => {
|
test("ebayRoute accepts decimal minPrice", async () => {
|
||||||
const { ebayRoute } = await import("../src/routes/ebay");
|
const { ebayRoute } = await import("../src/routes/ebay");
|
||||||
|
|
||||||
const response = await ebayRoute(
|
await ebayRoute(
|
||||||
new Request("http://localhost/api/ebay?q=laptop&minPrice=1.5"),
|
new Request("http://localhost/api/ebay?q=laptop&minPrice=1.5"),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(fetchEbayItems).toHaveBeenCalledWith(
|
||||||
const body = await response.json();
|
"laptop",
|
||||||
expect(body.message).toBe("Invalid minPrice parameter");
|
1,
|
||||||
|
expect.objectContaining({ minPrice: 150 }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ebayRoute returns 400 for non-integer maxPrice", async () => {
|
test("ebayRoute returns 400 for non-integer maxPrice", async () => {
|
||||||
@@ -766,16 +804,18 @@ describe("API routes", () => {
|
|||||||
expect(body.message).toBe("Invalid maxPrice parameter");
|
expect(body.message).toBe("Invalid maxPrice parameter");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ebayRoute returns 400 for decimal maxPrice", async () => {
|
test("ebayRoute accepts decimal maxPrice", async () => {
|
||||||
const { ebayRoute } = await import("../src/routes/ebay");
|
const { ebayRoute } = await import("../src/routes/ebay");
|
||||||
|
|
||||||
const response = await ebayRoute(
|
await ebayRoute(
|
||||||
new Request("http://localhost/api/ebay?q=laptop&maxPrice=1.5"),
|
new Request("http://localhost/api/ebay?q=laptop&maxPrice=1.5"),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(fetchEbayItems).toHaveBeenCalledWith(
|
||||||
const body = await response.json();
|
"laptop",
|
||||||
expect(body.message).toBe("Invalid maxPrice parameter");
|
1,
|
||||||
|
expect.objectContaining({ maxPrice: 150 }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("kijijiRoute returns 400 for decimal maxPages", async () => {
|
test("kijijiRoute returns 400 for decimal maxPages", async () => {
|
||||||
@@ -862,16 +902,20 @@ describe("API routes", () => {
|
|||||||
expect(body.message).toBe("Invalid priceMin parameter");
|
expect(body.message).toBe("Invalid priceMin parameter");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("kijijiRoute returns 400 for decimal priceMin", async () => {
|
test("kijijiRoute accepts decimal priceMin", async () => {
|
||||||
const { kijijiRoute } = await import("../src/routes/kijiji");
|
const { kijijiRoute } = await import("../src/routes/kijiji");
|
||||||
|
|
||||||
const response = await kijijiRoute(
|
await kijijiRoute(
|
||||||
new Request("http://localhost/api/kijiji?q=laptop&priceMin=1.5"),
|
new Request("http://localhost/api/kijiji?q=laptop&priceMin=1.5"),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(fetchKijijiItems).toHaveBeenCalledWith(
|
||||||
const body = await response.json();
|
"laptop",
|
||||||
expect(body.message).toBe("Invalid priceMin parameter");
|
4,
|
||||||
|
"https://www.kijiji.ca",
|
||||||
|
expect.objectContaining({ priceMin: 150 }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("kijijiRoute returns 400 for non-integer priceMin", async () => {
|
test("kijijiRoute returns 400 for non-integer priceMin", async () => {
|
||||||
@@ -934,16 +978,20 @@ describe("API routes", () => {
|
|||||||
expect(body.message).toBe("Invalid priceMax parameter");
|
expect(body.message).toBe("Invalid priceMax parameter");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("kijijiRoute returns 400 for decimal priceMax", async () => {
|
test("kijijiRoute accepts decimal priceMax", async () => {
|
||||||
const { kijijiRoute } = await import("../src/routes/kijiji");
|
const { kijijiRoute } = await import("../src/routes/kijiji");
|
||||||
|
|
||||||
const response = await kijijiRoute(
|
await kijijiRoute(
|
||||||
new Request("http://localhost/api/kijiji?q=laptop&priceMax=1.5"),
|
new Request("http://localhost/api/kijiji?q=laptop&priceMax=1.5"),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(fetchKijijiItems).toHaveBeenCalledWith(
|
||||||
const body = await response.json();
|
"laptop",
|
||||||
expect(body.message).toBe("Invalid priceMax parameter");
|
4,
|
||||||
|
"https://www.kijiji.ca",
|
||||||
|
expect.objectContaining({ priceMax: 150 }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("kijijiRoute returns 400 for non-integer priceMax", async () => {
|
test("kijijiRoute returns 400 for non-integer priceMax", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user