diff --git a/apps/web/__tests__/unit/developer-videos-pagination.test.ts b/apps/web/__tests__/unit/developer-videos-pagination.test.ts new file mode 100644 index 00000000000..033b8c3c917 --- /dev/null +++ b/apps/web/__tests__/unit/developer-videos-pagination.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import { + DEFAULT_DEVELOPER_API_LIMIT, + MAX_DEVELOPER_API_LIMIT, + MAX_DEVELOPER_API_OFFSET, + parseDeveloperVideosPagination, +} from "../../lib/developer-pagination"; + +describe("parseDeveloperVideosPagination (#3568)", () => { + it("returns default values on missing or empty parameters", () => { + expect(parseDeveloperVideosPagination(undefined, undefined)).toEqual({ + limit: DEFAULT_DEVELOPER_API_LIMIT, + offset: 0, + }); + expect(parseDeveloperVideosPagination("", "")).toEqual({ + limit: DEFAULT_DEVELOPER_API_LIMIT, + offset: 0, + }); + }); + + it("parses valid positive integer limit and offset", () => { + expect(parseDeveloperVideosPagination("5", "10")).toEqual({ + limit: 5, + offset: 10, + }); + expect(parseDeveloperVideosPagination("100", "0")).toEqual({ + limit: 100, + offset: 0, + }); + }); + + it("floors fractional numbers to prevent SQL syntax errors", () => { + expect(parseDeveloperVideosPagination("5.5", "2.5")).toEqual({ + limit: 5, + offset: 2, + }); + expect(parseDeveloperVideosPagination("10.9", "0.8")).toEqual({ + limit: 10, + offset: 0, + }); + }); + + it("falls back to defaults for non-positive or non-numeric inputs", () => { + expect(parseDeveloperVideosPagination("0", "-5")).toEqual({ + limit: DEFAULT_DEVELOPER_API_LIMIT, + offset: 0, + }); + expect(parseDeveloperVideosPagination("-1", "invalid")).toEqual({ + limit: DEFAULT_DEVELOPER_API_LIMIT, + offset: 0, + }); + expect(parseDeveloperVideosPagination("xyz", "NaN")).toEqual({ + limit: DEFAULT_DEVELOPER_API_LIMIT, + offset: 0, + }); + }); + + it("caps limit to MAX_DEVELOPER_API_LIMIT and bounds oversized offsets", () => { + expect(parseDeveloperVideosPagination("500", "99999999999999999999")).toEqual({ + limit: MAX_DEVELOPER_API_LIMIT, + offset: MAX_DEVELOPER_API_OFFSET, + }); + }); +}); diff --git a/apps/web/app/api/developer/v1/[...route]/videos.ts b/apps/web/app/api/developer/v1/[...route]/videos.ts index e8cbecdb1e4..02d1f198699 100644 --- a/apps/web/app/api/developer/v1/[...route]/videos.ts +++ b/apps/web/app/api/developer/v1/[...route]/videos.ts @@ -2,6 +2,7 @@ import { db } from "@cap/database"; import { developerVideos } from "@cap/database/schema"; import { and, desc, eq, isNull } from "drizzle-orm"; import { Hono } from "hono"; +import { parseDeveloperVideosPagination } from "@/lib/developer-pagination"; import { withDeveloperSecretAuth } from "../../../utils"; export const app = new Hono<{ @@ -14,8 +15,10 @@ export const app = new Hono<{ app.get("/", async (c) => { const appId = c.get("developerAppId"); const userId = c.req.query("userId"); - const limit = Math.min(Number(c.req.query("limit") ?? 50) || 50, 100); - const offset = Math.max(0, Number(c.req.query("offset") ?? 0) || 0); + const { limit, offset } = parseDeveloperVideosPagination( + c.req.query("limit"), + c.req.query("offset"), + ); const conditions = [ eq(developerVideos.appId, appId), diff --git a/apps/web/lib/developer-pagination.ts b/apps/web/lib/developer-pagination.ts new file mode 100644 index 00000000000..51f132f5eab --- /dev/null +++ b/apps/web/lib/developer-pagination.ts @@ -0,0 +1,22 @@ +export const MAX_DEVELOPER_API_OFFSET = 1_000_000_000; +export const DEFAULT_DEVELOPER_API_LIMIT = 50; +export const MAX_DEVELOPER_API_LIMIT = 100; + +export function parseDeveloperVideosPagination( + rawLimit: string | undefined, + rawOffset: string | undefined, +): { limit: number; offset: number } { + const parsedLimit = Number(rawLimit); + const limit = + Number.isFinite(parsedLimit) && parsedLimit >= 1 + ? Math.min(Math.floor(parsedLimit), MAX_DEVELOPER_API_LIMIT) + : DEFAULT_DEVELOPER_API_LIMIT; + + const parsedOffset = Number(rawOffset); + const offset = + Number.isFinite(parsedOffset) && parsedOffset >= 0 + ? Math.min(Math.floor(parsedOffset), MAX_DEVELOPER_API_OFFSET) + : 0; + + return { limit, offset }; +}