Skip to content

Commit cba49e7

Browse files
Validate message load limits (#82)
1 parent fc64ff4 commit cba49e7

2 files changed

Lines changed: 58 additions & 4 deletions

File tree

src/app/api/messages/load/route.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export const POST = withAuth(async ({ request, locals }) => {
1515
return NextResponse.json({ error: 'Missing conversationId' }, { status: 400 });
1616
}
1717

18+
const normalizedLimit = normalizeMessageLimit(limit);
19+
if (normalizedLimit === null) {
20+
return NextResponse.json({ error: 'limit must be an integer between 1 and 100' }, { status: 400 });
21+
}
22+
1823
const { supabase, user: authUser } = locals;
1924

2025
// Get internal user ID from auth user ID
@@ -35,7 +40,7 @@ export const POST = withAuth(async ({ request, locals }) => {
3540
conversationId,
3641
authUserId: authUser.id,
3742
internalUserId: userId,
38-
limit,
43+
limit: normalizedLimit,
3944
before
4045
});
4146

@@ -64,7 +69,7 @@ export const POST = withAuth(async ({ request, locals }) => {
6469
.eq('message_recipients.recipient_user_id', userId)
6570
.is('deleted_at', null)
6671
.order('created_at', { ascending: true })
67-
.limit(limit);
72+
.limit(normalizedLimit);
6873

6974
if (before) {
7075
query = query.lt('created_at', before);
@@ -132,10 +137,14 @@ export const POST = withAuth(async ({ request, locals }) => {
132137
return NextResponse.json({
133138
success: true,
134139
messages: processedMessages,
135-
hasMore: processedMessages.length === limit
140+
hasMore: processedMessages.length === normalizedLimit
136141
});
137142
} catch (error) {
138143
console.error('📨 [SSE-LOAD] Exception:', error);
139144
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
140145
}
141-
});
146+
});
147+
148+
function normalizeMessageLimit(limit) {
149+
return Number.isInteger(limit) && limit >= 1 && limit <= 100 ? limit : null;
150+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
const mocks = vi.hoisted(() => ({
4+
mockSupabase: {
5+
from: vi.fn()
6+
},
7+
mockJoinRoom: vi.fn()
8+
}));
9+
10+
vi.mock('@/lib/api/middleware/auth.js', () => ({
11+
withAuth: (handler) => (request, context) =>
12+
handler({
13+
request,
14+
locals: {
15+
supabase: mocks.mockSupabase,
16+
user: { id: 'auth-user-id' }
17+
},
18+
context
19+
})
20+
}));
21+
22+
vi.mock('@/lib/api/sse-manager.js', () => ({
23+
sseManager: {
24+
joinRoom: mocks.mockJoinRoom
25+
}
26+
}));
27+
28+
describe('POST /api/messages/load validation', () => {
29+
it('rejects non-integer limits before database work', async () => {
30+
const { POST } = await import('./route.js');
31+
const request = {
32+
json: vi.fn().mockResolvedValue({
33+
conversationId: 'conversation-1',
34+
limit: 'many'
35+
})
36+
};
37+
38+
const response = await POST(request);
39+
const body = await response.json();
40+
41+
expect(response.status).toBe(400);
42+
expect(body.error).toBe('limit must be an integer between 1 and 100');
43+
expect(mocks.mockSupabase.from).not.toHaveBeenCalled();
44+
});
45+
});

0 commit comments

Comments
 (0)