From 8ce45e5d5de757e2050343c090ea0f678342b32b Mon Sep 17 00:00:00 2001 From: phucnguyen1707 Date: Thu, 11 Jun 2026 16:05:39 +0700 Subject: [PATCH] Validate message load limits --- src/app/api/messages/load/route.js | 17 +++++++--- src/app/api/messages/load/route.test.js | 45 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 src/app/api/messages/load/route.test.js diff --git a/src/app/api/messages/load/route.js b/src/app/api/messages/load/route.js index f506b73..3828fec 100644 --- a/src/app/api/messages/load/route.js +++ b/src/app/api/messages/load/route.js @@ -15,6 +15,11 @@ export const POST = withAuth(async ({ request, locals }) => { return NextResponse.json({ error: 'Missing conversationId' }, { status: 400 }); } + const normalizedLimit = normalizeMessageLimit(limit); + if (normalizedLimit === null) { + return NextResponse.json({ error: 'limit must be an integer between 1 and 100' }, { status: 400 }); + } + const { supabase, user: authUser } = locals; // Get internal user ID from auth user ID @@ -35,7 +40,7 @@ export const POST = withAuth(async ({ request, locals }) => { conversationId, authUserId: authUser.id, internalUserId: userId, - limit, + limit: normalizedLimit, before }); @@ -64,7 +69,7 @@ export const POST = withAuth(async ({ request, locals }) => { .eq('message_recipients.recipient_user_id', userId) .is('deleted_at', null) .order('created_at', { ascending: true }) - .limit(limit); + .limit(normalizedLimit); if (before) { query = query.lt('created_at', before); @@ -132,10 +137,14 @@ export const POST = withAuth(async ({ request, locals }) => { return NextResponse.json({ success: true, messages: processedMessages, - hasMore: processedMessages.length === limit + hasMore: processedMessages.length === normalizedLimit }); } catch (error) { console.error('📨 [SSE-LOAD] Exception:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -}); \ No newline at end of file +}); + +function normalizeMessageLimit(limit) { + return Number.isInteger(limit) && limit >= 1 && limit <= 100 ? limit : null; +} diff --git a/src/app/api/messages/load/route.test.js b/src/app/api/messages/load/route.test.js new file mode 100644 index 0000000..6418cbb --- /dev/null +++ b/src/app/api/messages/load/route.test.js @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + mockSupabase: { + from: vi.fn() + }, + mockJoinRoom: vi.fn() +})); + +vi.mock('@/lib/api/middleware/auth.js', () => ({ + withAuth: (handler) => (request, context) => + handler({ + request, + locals: { + supabase: mocks.mockSupabase, + user: { id: 'auth-user-id' } + }, + context + }) +})); + +vi.mock('@/lib/api/sse-manager.js', () => ({ + sseManager: { + joinRoom: mocks.mockJoinRoom + } +})); + +describe('POST /api/messages/load validation', () => { + it('rejects non-integer limits before database work', async () => { + const { POST } = await import('./route.js'); + const request = { + json: vi.fn().mockResolvedValue({ + conversationId: 'conversation-1', + limit: 'many' + }) + }; + + const response = await POST(request); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('limit must be an integer between 1 and 100'); + expect(mocks.mockSupabase.from).not.toHaveBeenCalled(); + }); +});