diff --git a/src/app/api/chat/conversations/[id]/mark-read/route.js b/src/app/api/chat/conversations/[id]/mark-read/route.js index aadaccc4..5019386a 100644 --- a/src/app/api/chat/conversations/[id]/mark-read/route.js +++ b/src/app/api/chat/conversations/[id]/mark-read/route.js @@ -1,18 +1,24 @@ import { NextResponse } from 'next/server'; import { createSupabaseServerClient } from '@/lib/supabase.js'; +function normalizeConversationId(conversationId) { + return typeof conversationId === 'string' ? conversationId.trim() : ''; +} + /** * Mark all messages in a conversation as read for the current user * @type {import('./$types').RequestHandler} */ export async function POST(request, { params } = {}) { try { - const supabase = await createSupabaseServerClient(); const { id: conversationId } = (await params) || {}; + const normalizedConversationId = normalizeConversationId(conversationId); - if (!conversationId) { + if (!normalizedConversationId) { return NextResponse.json({ error: 'Conversation ID is required' }, { status: 400 }); } + + const supabase = await createSupabaseServerClient(); // Get user from session const { data: { user }, error: userError } = await supabase.auth.getUser(); @@ -42,7 +48,7 @@ export async function POST(request, { params } = {}) { supabase .from('messages') .select('id') - .eq('conversation_id', conversationId) + .eq('conversation_id', normalizedConversationId) ); if (updateError) { diff --git a/src/app/api/chat/conversations/[id]/mark-read/route.test.js b/src/app/api/chat/conversations/[id]/mark-read/route.test.js index 9398f70c..1bbae526 100644 --- a/src/app/api/chat/conversations/[id]/mark-read/route.test.js +++ b/src/app/api/chat/conversations/[id]/mark-read/route.test.js @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ + createSupabaseServerClient: vi.fn(), authGetUser: vi.fn(), from: vi.fn(), usersEq: vi.fn(), @@ -9,13 +10,17 @@ const mocks = vi.hoisted(() => ({ })); vi.mock('@/lib/supabase.js', () => ({ - createSupabaseServerClient: vi.fn(async () => ({ + createSupabaseServerClient: mocks.createSupabaseServerClient +})); + +function createSupabaseClient() { + return { auth: { getUser: mocks.authGetUser }, from: mocks.from - })) -})); + }; +} function createUsersQuery() { const query = { @@ -55,6 +60,7 @@ describe('POST /api/chat/conversations/[id]/mark-read', () => { vi.resetModules(); vi.clearAllMocks(); + mocks.createSupabaseServerClient.mockResolvedValue(createSupabaseClient()); mocks.authGetUser.mockResolvedValue({ data: { user: { id: 'auth-user-id' } }, error: null @@ -90,4 +96,17 @@ describe('POST /api/chat/conversations/[id]/mark-read', () => { expect(body).toEqual({ error: 'Conversation ID is required' }); expect(mocks.authGetUser).not.toHaveBeenCalled(); }); + + it('rejects blank conversation ids before creating a Supabase client', async () => { + const { POST } = await import('./route.js'); + const response = await POST(new Request('https://qrypt.chat/api/chat/conversations/%20/mark-read'), { + params: Promise.resolve({ id: ' ' }) + }); + const body = await response.json(); + + expect(response.status).toBe(400); + expect(body).toEqual({ error: 'Conversation ID is required' }); + expect(mocks.createSupabaseServerClient).not.toHaveBeenCalled(); + expect(mocks.authGetUser).not.toHaveBeenCalled(); + }); });