Skip to content

Commit e2aee48

Browse files
Validate modules pagination params (#21)
1 parent 4872d3a commit e2aee48

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

‎apps/web/src/app/api/modules/__tests__/route.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,39 @@ describe("GET /api/modules", () => {
108108
expect(mockRange).toHaveBeenCalledWith(10, 19);
109109
});
110110

111+
it("falls back to defaults for invalid pagination values", async () => {
112+
const req = makeRequest("http://localhost/api/modules?page=nope&limit=Infinity");
113+
const res = await GET(req);
114+
const body = await res.json();
115+
116+
expect(res.status).toBe(200);
117+
expect(body.page).toBe(1);
118+
expect(body.limit).toBe(20);
119+
expect(mockRange).toHaveBeenCalledWith(0, 19);
120+
});
121+
122+
it("does not truncate fractional pagination values", async () => {
123+
const req = makeRequest("http://localhost/api/modules?page=2.5&limit=10.9");
124+
const res = await GET(req);
125+
const body = await res.json();
126+
127+
expect(res.status).toBe(200);
128+
expect(body.page).toBe(1);
129+
expect(body.limit).toBe(20);
130+
expect(mockRange).toHaveBeenCalledWith(0, 19);
131+
});
132+
133+
it("caps valid limit values at 50", async () => {
134+
const req = makeRequest("http://localhost/api/modules?page=2&limit=99");
135+
const res = await GET(req);
136+
const body = await res.json();
137+
138+
expect(res.status).toBe(200);
139+
expect(body.page).toBe(2);
140+
expect(body.limit).toBe(50);
141+
expect(mockRange).toHaveBeenCalledWith(50, 99);
142+
});
143+
111144
it("handles database errors", async () => {
112145
resetChain({
113146
rangeResult: { data: null, error: { message: "DB error" }, count: null },

‎apps/web/src/app/api/modules/route.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { getSupabaseAdmin, slugify } from "@/lib/supabase";
33

4+
function parsePositiveInteger(value: string | null, fallback: number, max?: number) {
5+
if (value === null) return fallback;
6+
7+
const parsed = Number(value);
8+
if (!Number.isInteger(parsed) || parsed < 1) return fallback;
9+
10+
return max === undefined ? parsed : Math.min(max, parsed);
11+
}
12+
413
async function getAuthenticatedUser(request: NextRequest) {
514
const sb = getSupabaseAdmin();
615
const authHeader = request.headers.get("authorization");
@@ -26,8 +35,8 @@ export async function GET(request: NextRequest) {
2635
const search = searchParams.get("search") || "";
2736
const category = searchParams.get("category") || "";
2837
const sort = searchParams.get("sort") || "newest"; // newest | popular | top-rated
29-
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10));
30-
const limit = Math.min(50, Math.max(1, parseInt(searchParams.get("limit") || "20", 10)));
38+
const page = parsePositiveInteger(searchParams.get("page"), 1);
39+
const limit = parsePositiveInteger(searchParams.get("limit"), 20, 50);
3140
const offset = (page - 1) * limit;
3241

3342
const sb = getSupabaseAdmin();

0 commit comments

Comments
 (0)