From 804168b343228f0d694add4a70513ed7de82ba22 Mon Sep 17 00:00:00 2001 From: Jorel97 <83238249+Jorel97@users.noreply.github.com> Date: Fri, 29 May 2026 14:21:32 -0600 Subject: [PATCH 1/2] fix(affiliates): clamp offers pagination --- src/app/api/affiliates/offers/route.test.ts | 58 +++++++++++++++++++++ src/app/api/affiliates/offers/route.ts | 14 ++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/app/api/affiliates/offers/route.test.ts b/src/app/api/affiliates/offers/route.test.ts index 25b9adc2..b6d22797 100644 --- a/src/app/api/affiliates/offers/route.test.ts +++ b/src/app/api/affiliates/offers/route.test.ts @@ -98,6 +98,64 @@ describe("GET /api/affiliates/offers", () => { expect(res.status).toBe(200); expect(body.offers[0].product_url).toBeUndefined(); }); + + it("clamps invalid pagination values before querying", async () => { + const rangeSpy = vi.fn(); + const queryChain: Record = {}; + const chainHandler: ProxyHandler = { + get(_target, prop) { + if (prop === "then") return undefined; + if (prop === "data") return []; + if (prop === "error") return null; + if (prop === "count") return 0; + if (prop === "range") { + return (...args: any[]) => { + rangeSpy(...args); + return new Proxy(queryChain, chainHandler); + }; + } + return (..._args: any[]) => new Proxy(queryChain, chainHandler); + }, + }; + mockFrom.mockReturnValue(new Proxy(queryChain, chainHandler)); + + const res = await GET(makeRequest({ page: "abc", limit: "-5" })); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(rangeSpy).toHaveBeenCalledWith(0, 0); + expect(body.page).toBe(1); + expect(body.limit).toBe(1); + }); + + it("caps huge pagination values before querying", async () => { + const rangeSpy = vi.fn(); + const queryChain: Record = {}; + const chainHandler: ProxyHandler = { + get(_target, prop) { + if (prop === "then") return undefined; + if (prop === "data") return []; + if (prop === "error") return null; + if (prop === "count") return 0; + if (prop === "range") { + return (...args: any[]) => { + rangeSpy(...args); + return new Proxy(queryChain, chainHandler); + }; + } + return (..._args: any[]) => new Proxy(queryChain, chainHandler); + }, + }; + mockFrom.mockReturnValue(new Proxy(queryChain, chainHandler)); + + const res = await GET(makeRequest({ page: "1e308", limit: "999" })); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(rangeSpy).toHaveBeenCalledWith(4999950, 4999999); + expect(body.page).toBe(100000); + expect(body.limit).toBe(50); + }); }); describe("POST /api/affiliates/offers", () => { diff --git a/src/app/api/affiliates/offers/route.ts b/src/app/api/affiliates/offers/route.ts index afac2c94..e430e232 100644 --- a/src/app/api/affiliates/offers/route.ts +++ b/src/app/api/affiliates/offers/route.ts @@ -7,6 +7,16 @@ import { checkRateLimit, rateLimitExceeded, getRateLimitIdentifier } from "@/lib type AnySupabase = any; import { validateOfferInput } from "@/lib/affiliates/validation"; +function parsePaginationParam( + value: string | null, + defaultValue: number, + min: number, + max: number +) { + const parsed = Number(value && value.trim() !== "" ? value : defaultValue); + const finiteValue = Number.isFinite(parsed) ? parsed : defaultValue; + return Math.min(Math.max(Math.trunc(finiteValue), min), max); +} function slugify(text: string): string { return text @@ -22,8 +32,8 @@ function slugify(text: string): string { export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); - const page = Math.max(1, parseInt(searchParams.get("page") || "1")); - const limit = Math.min(50, Math.max(1, parseInt(searchParams.get("limit") || "20"))); + const page = parsePaginationParam(searchParams.get("page"), 1, 1, 100_000); + const limit = parsePaginationParam(searchParams.get("limit"), 20, 1, 50); const category = searchParams.get("category"); const tag = searchParams.get("tag"); const sort = searchParams.get("sort") || "newest"; From 220e0b9fa4d5f81cabbad413f150c28f9fb0a67c Mon Sep 17 00:00:00 2001 From: Jorel97 <83238249+Jorel97@users.noreply.github.com> Date: Fri, 29 May 2026 14:28:29 -0600 Subject: [PATCH 2/2] test(affiliates): share offers pagination mock --- src/app/api/affiliates/offers/route.test.ts | 56 ++++++++------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/src/app/api/affiliates/offers/route.test.ts b/src/app/api/affiliates/offers/route.test.ts index b6d22797..49675175 100644 --- a/src/app/api/affiliates/offers/route.test.ts +++ b/src/app/api/affiliates/offers/route.test.ts @@ -40,6 +40,26 @@ function chainable(data: unknown, error: unknown = null, count: number | null = return new Proxy(result, handler); } +function offerListChain(rangeSpy: ReturnType) { + const queryChain: Record = {}; + const chainHandler: ProxyHandler = { + get(_target, prop) { + if (prop === "then") return undefined; + if (prop === "data") return []; + if (prop === "error") return null; + if (prop === "count") return 0; + if (prop === "range") { + return (...args: any[]) => { + rangeSpy(...args); + return new Proxy(queryChain, chainHandler); + }; + } + return (..._args: any[]) => new Proxy(queryChain, chainHandler); + }, + }; + return new Proxy(queryChain, chainHandler); +} + describe("GET /api/affiliates/offers", () => { beforeEach(() => { vi.clearAllMocks(); @@ -101,23 +121,7 @@ describe("GET /api/affiliates/offers", () => { it("clamps invalid pagination values before querying", async () => { const rangeSpy = vi.fn(); - const queryChain: Record = {}; - const chainHandler: ProxyHandler = { - get(_target, prop) { - if (prop === "then") return undefined; - if (prop === "data") return []; - if (prop === "error") return null; - if (prop === "count") return 0; - if (prop === "range") { - return (...args: any[]) => { - rangeSpy(...args); - return new Proxy(queryChain, chainHandler); - }; - } - return (..._args: any[]) => new Proxy(queryChain, chainHandler); - }, - }; - mockFrom.mockReturnValue(new Proxy(queryChain, chainHandler)); + mockFrom.mockReturnValue(offerListChain(rangeSpy)); const res = await GET(makeRequest({ page: "abc", limit: "-5" })); const body = await res.json(); @@ -130,23 +134,7 @@ describe("GET /api/affiliates/offers", () => { it("caps huge pagination values before querying", async () => { const rangeSpy = vi.fn(); - const queryChain: Record = {}; - const chainHandler: ProxyHandler = { - get(_target, prop) { - if (prop === "then") return undefined; - if (prop === "data") return []; - if (prop === "error") return null; - if (prop === "count") return 0; - if (prop === "range") { - return (...args: any[]) => { - rangeSpy(...args); - return new Proxy(queryChain, chainHandler); - }; - } - return (..._args: any[]) => new Proxy(queryChain, chainHandler); - }, - }; - mockFrom.mockReturnValue(new Proxy(queryChain, chainHandler)); + mockFrom.mockReturnValue(offerListChain(rangeSpy)); const res = await GET(makeRequest({ page: "1e308", limit: "999" })); const body = await res.json();