From c19eb803c3707c797be4165460dc94d2228de62d Mon Sep 17 00:00:00 2001 From: rissrice2105-agent <289161642+rissrice2105-agent@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:23:19 -0600 Subject: [PATCH] fix(iptv): validate channel pagination --- src/app/api/iptv/channels/route.test.ts | 36 ++++++++++++++++++++++++- src/app/api/iptv/channels/route.ts | 9 +++---- vitest.config.ts | 1 - 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/app/api/iptv/channels/route.test.ts b/src/app/api/iptv/channels/route.test.ts index edd2e811..06fde89f 100644 --- a/src/app/api/iptv/channels/route.test.ts +++ b/src/app/api/iptv/channels/route.test.ts @@ -14,7 +14,9 @@ const mockCacheSet = vi.fn(); // Mock undici fetch const mockFetch = vi.fn(); vi.mock('undici', () => ({ - Agent: vi.fn().mockImplementation(() => ({})), + Agent: vi.fn(function MockAgent() { + return {}; + }), fetch: mockFetch, })); @@ -233,6 +235,38 @@ describe('IPTV Channels API', () => { expect(data.limit).toBe(10); }); + it.each([ + ['negative', '-5', '-20'], + ['partially numeric', '10items', '20items'], + ])('uses pagination defaults for %s values', async (_label, limit, offset) => { + const allChannels = Array.from({ length: 100 }, (_, i) => ({ + id: String(i), + name: `Channel ${i}`, + url: `https://example.com/ch${i}.m3u8`, + group: 'General', + })); + mockCacheGet.mockResolvedValue({ + channels: allChannels, + groups: ['General'], + fetchedAt: Date.now(), + m3uUrl: 'http://example.com/playlist.m3u', + }); + vi.mocked(searchChannels).mockReturnValue(allChannels); + + const request = new NextRequest( + `http://localhost/api/iptv/channels?m3uUrl=http://example.com/playlist.m3u&limit=${limit}&offset=${offset}` + ); + + const response = await GET(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.channels).toHaveLength(50); + expect(data.limit).toBe(50); + expect(data.offset).toBe(0); + expect(data.channels[0].id).toBe('0'); + }); + it('returns 502 when upstream fetch fails', async () => { mockCacheGet.mockResolvedValue(null); diff --git a/src/app/api/iptv/channels/route.ts b/src/app/api/iptv/channels/route.ts index bbef96ac..883933be 100644 --- a/src/app/api/iptv/channels/route.ts +++ b/src/app/api/iptv/channels/route.ts @@ -24,6 +24,7 @@ import { } from '@/lib/iptv'; import { getIptvCacheReader } from '@/lib/iptv/cache-reader'; import { createServerClient } from '@/lib/supabase'; +import { parseIntegerParam } from '@/lib/api/pagination'; /** * Undici agent that ignores SSL certificate errors. @@ -90,11 +91,9 @@ export async function GET(request: NextRequest): Promise { const m3uUrl = searchParams.get('m3uUrl'); const query = searchParams.get('q') ?? ''; const group = searchParams.get('group') ?? undefined; - const limit = Math.min( - parseInt(searchParams.get('limit') ?? String(DEFAULT_LIMIT), 10) || DEFAULT_LIMIT, - MAX_LIMIT - ); - const offset = parseInt(searchParams.get('offset') ?? '0', 10) || 0; + const parsedLimit = parseIntegerParam(searchParams.get('limit'), { min: 1 }); + const limit = Math.min(parsedLimit ?? DEFAULT_LIMIT, MAX_LIMIT); + const offset = parseIntegerParam(searchParams.get('offset'), { min: 0 }) ?? 0; // Validate that at least one identifier is provided if (!playlistId && !m3uUrl) { diff --git a/vitest.config.ts b/vitest.config.ts index 446a963c..ca9b765e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -49,7 +49,6 @@ export default defineConfig({ // Tests failing after vitest 3→4 migration (pre-existing failures, tracked separately) 'src/app/api/browse/route.test.ts', 'src/app/api/ice/turn/route.test.ts', - 'src/app/api/iptv/channels/route.test.ts', 'src/app/api/iptv/playlists/[id]/route.test.ts', 'src/app/api/iptv/playlists/route.test.ts', 'src/app/api/magnets/route.test.ts',