From 4f9544b6539f095407736ed45c564fc3c9afe817 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Fri, 31 Jul 2026 23:16:01 -0600 Subject: [PATCH] fix(conversations): validate chat participant ids --- src/app/api/chat/conversations/route.js | 21 ++++++-- src/app/api/chat/conversations/route.test.js | 55 ++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 src/app/api/chat/conversations/route.test.js diff --git a/src/app/api/chat/conversations/route.js b/src/app/api/chat/conversations/route.js index 21f9ec53..65c12ee5 100644 --- a/src/app/api/chat/conversations/route.js +++ b/src/app/api/chat/conversations/route.js @@ -113,14 +113,25 @@ export async function POST(request, { params } = {}) { return NextResponse.json({ error: 'participant_ids is required and must be a non-empty array' }, { status: 400 }); } + const normalizedParticipantIds = participant_ids.map((participantId) => + typeof participantId === 'string' ? participantId.trim() : '' + ); + + if ( + normalizedParticipantIds.length !== participant_ids.length || + normalizedParticipantIds.some((participantId) => participantId.length === 0) + ) { + return NextResponse.json({ error: 'participant_ids must contain non-empty strings' }, { status: 400 }); + } + // Skip participant validation for now due to network issues // TODO: Re-enable participant validation once network connectivity is stable console.log('Skipping participant validation due to network issues'); console.log('Participant IDs to add:', participant_ids); // For direct messages, check if conversation already exists - if (type === 'direct' && participant_ids.length === 1) { - const other_user_id = participant_ids[0]; + if (type === 'direct' && normalizedParticipantIds.length === 1) { + const other_user_id = normalizedParticipantIds[0]; // Check if conversation already exists between these users const { data: existingConversations } = await supabase @@ -175,7 +186,7 @@ export async function POST(request, { params } = {}) { const participants = []; // Add other participants (validate they exist in users table) - for (const participantId of participant_ids) { + for (const participantId of normalizedParticipantIds) { participants.push({ conversation_id: conversationId, user_id: participantId, @@ -185,7 +196,7 @@ export async function POST(request, { params } = {}) { if (internalUser) { // Add creator as admin if not already in participants - if (!participant_ids.includes(internalUser.id)) { + if (!normalizedParticipantIds.includes(internalUser.id)) { participants.push({ conversation_id: conversationId, user_id: internalUser.id, @@ -290,4 +301,4 @@ export async function PATCH(request, { params } = {}) { console.error('API error:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/api/chat/conversations/route.test.js b/src/app/api/chat/conversations/route.test.js new file mode 100644 index 00000000..3c6f13ff --- /dev/null +++ b/src/app/api/chat/conversations/route.test.js @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + from: vi.fn() +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(async () => ({ + auth: { getUser: mocks.authGetUser }, + from: mocks.from + })) +})); + +function createUsersQuery() { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + maybeSingle: vi.fn().mockResolvedValue({ data: null, error: null }) + }; + return query; +} + +describe('POST /api/chat/conversations participant validation', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + mocks.from.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + throw new Error(`Unexpected database table: ${table}`); + }); + }); + + it.each([ + ['blank', [' ']], + ['non-string', [null]] + ])('rejects %s participant ids before conversation creation', async (_label, participant_ids) => { + const { POST } = await import('./route.js'); + const response = await POST(new Request('https://qrypt.chat/api/chat/conversations', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ type: 'direct', participant_ids }) + })); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('participant_ids must contain non-empty strings'); + expect(mocks.from).toHaveBeenCalledWith('users'); + expect(mocks.from).toHaveBeenCalledTimes(1); + }); +});