From 626b6b3944f2679b9768e48b8ade1fcb378d4377 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Wed, 29 Jul 2026 12:58:12 -0600 Subject: [PATCH] fix(messages): reject encrypted contents arrays --- src/app/api/chat/messages/route.js | 2 +- src/app/api/chat/messages/route.test.js | 75 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/app/api/chat/messages/route.test.js diff --git a/src/app/api/chat/messages/route.js b/src/app/api/chat/messages/route.js index d81cce12..3e42fe05 100644 --- a/src/app/api/chat/messages/route.js +++ b/src/app/api/chat/messages/route.js @@ -114,7 +114,7 @@ export async function POST(request) { } // Validate encrypted_contents is an object with user_id -> encrypted_content mappings - if (typeof encrypted_contents !== 'object' || Object.keys(encrypted_contents).length === 0) { + if (typeof encrypted_contents !== 'object' || Array.isArray(encrypted_contents) || Object.keys(encrypted_contents).length === 0) { return NextResponse.json({ error: 'encrypted_contents must be an object with user_id -> encrypted_content mappings' }, { status: 400 }); } diff --git a/src/app/api/chat/messages/route.test.js b/src/app/api/chat/messages/route.test.js new file mode 100644 index 00000000..19e39800 --- /dev/null +++ b/src/app/api/chat/messages/route.test.js @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + serviceFrom: vi.fn(), + userEq: vi.fn() +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(() => ({ + auth: { + getUser: mocks.authGetUser + } + })) +})); + +vi.mock('@/lib/supabase/service-role.js', () => ({ + createServiceRoleClient: vi.fn(() => ({ + from: mocks.serviceFrom + })) +})); + +function createUsersQuery() { + const query = { + select: vi.fn(() => query), + eq: mocks.userEq, + single: vi.fn().mockResolvedValue({ + data: { id: 'internal-user-id' }, + error: null + }) + }; + mocks.userEq.mockReturnValue(query); + return query; +} + +describe('POST /api/chat/messages validation', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + }); + + it('rejects encrypted_contents arrays before participant lookup', async () => { + mocks.serviceFrom.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + if (table === 'conversation_participants') throw new Error('Participant query should not run'); + throw new Error(`Unexpected table: ${table}`); + }); + + const { POST } = await import('./route.js'); + const response = await POST( + new Request('https://qrypt.chat/api/chat/messages', { + method: 'POST', + headers: { + cookie: 'sb-access-token=valid-token', + 'content-type': 'application/json' + }, + body: JSON.stringify({ + conversation_id: 'conversation-1', + encrypted_contents: ['not-a-user-map'] + }) + }) + ); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body.error).toBe('encrypted_contents must be an object with user_id -> encrypted_content mappings'); + expect(mocks.userEq).toHaveBeenCalledWith('auth_user_id', 'auth-user-id'); + expect(mocks.serviceFrom).toHaveBeenCalledTimes(1); + }); +});