diff --git a/src/app/api/conversations/delete/route.js b/src/app/api/conversations/delete/route.js index 02db7cf3..a0f8fae2 100644 --- a/src/app/api/conversations/delete/route.js +++ b/src/app/api/conversations/delete/route.js @@ -84,17 +84,7 @@ export const POST = withAuth(async ({ request, locals }) => { return NextResponse.json({ error: 'Failed to fetch conversation details' }, { status: 500 }); } - const { count: participantCount, error: countError } = await supabase - .from('conversation_participants') - .select('*', { count: 'exact', head: true }) - .eq('conversation_id', conversationId); - - if (countError) { - console.error('Error counting participants:', countError); - } - - // Determine if this is a direct message (2 participants) or group - const isDirectMessage = conversation.type === 'direct' || participantCount === 2; + const isDirectMessage = conversation.type === 'direct'; if (isDirectMessage) { // For direct messages, delete everything for all participants @@ -278,4 +268,4 @@ export const POST = withAuth(async ({ request, locals }) => { console.error('Delete conversation error:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -}); \ No newline at end of file +}); diff --git a/src/app/api/conversations/delete/route.test.js b/src/app/api/conversations/delete/route.test.js new file mode 100644 index 00000000..c7d6c1ba --- /dev/null +++ b/src/app/api/conversations/delete/route.test.js @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + from: vi.fn(), + getServiceRoleClient: vi.fn() +})); + +vi.mock('@/lib/api/middleware/auth.js', () => ({ + withAuth: (handler) => (request, context) => + handler({ + request, + locals: { + supabase: { from: mocks.from }, + user: { id: 'auth-user-id' } + }, + context + }) +})); + +vi.mock('@/lib/supabase/service-role.js', () => ({ + getServiceRoleClient: mocks.getServiceRoleClient +})); + +function selectSingle(data) { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + single: vi.fn().mockResolvedValue({ data, error: null }) + }; + return query; +} + +function participantLookup() { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + is: vi.fn(() => query), + maybeSingle: vi.fn().mockResolvedValue({ + data: { id: 'participant-id', left_at: null, archived_at: null }, + error: null + }) + }; + return query; +} + +function deleteQuery() { + const query = { + error: null, + delete: vi.fn(() => query), + eq: vi.fn(() => query) + }; + return query; +} + +describe('POST /api/conversations/delete', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it('keeps a two-member group and removes only the requesting participant data', async () => { + const messages = deleteQuery(); + const files = deleteQuery(); + const participantDelete = deleteQuery(); + participantDelete.count = 2; + participantDelete.select = vi.fn(() => participantDelete); + let participantQueries = 0; + + mocks.from.mockImplementation((table) => { + if (table === 'users') return selectSingle({ id: 'internal-user-id' }); + if (table === 'conversations') return selectSingle({ type: 'group' }); + if (table === 'messages') return messages; + if (table === 'file_attachments') return files; + if (table === 'conversation_participants') { + participantQueries += 1; + return participantQueries === 1 ? participantLookup() : participantDelete; + } + throw new Error(`Unexpected table: ${table}`); + }); + + const { POST } = await import('./route.js'); + const response = await POST({ + json: vi.fn().mockResolvedValue({ conversationId: 'group-1' }) + }); + + expect(response.status).toBe(200); + expect(mocks.getServiceRoleClient).not.toHaveBeenCalled(); + expect(messages.eq).toHaveBeenCalledWith('sender_id', 'internal-user-id'); + expect(files.eq).toHaveBeenCalledWith('uploaded_by', 'internal-user-id'); + expect(participantDelete.eq).toHaveBeenCalledWith('user_id', 'internal-user-id'); + }); +});