Skip to content

Commit 96a57e2

Browse files
authored
fix(conversations): validate chat participant ids (#220)
1 parent 67c7291 commit 96a57e2

2 files changed

Lines changed: 71 additions & 5 deletions

File tree

‎src/app/api/chat/conversations/route.js‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,25 @@ export async function POST(request, { params } = {}) {
113113
return NextResponse.json({ error: 'participant_ids is required and must be a non-empty array' }, { status: 400 });
114114
}
115115

116+
const normalizedParticipantIds = participant_ids.map((participantId) =>
117+
typeof participantId === 'string' ? participantId.trim() : ''
118+
);
119+
120+
if (
121+
normalizedParticipantIds.length !== participant_ids.length ||
122+
normalizedParticipantIds.some((participantId) => participantId.length === 0)
123+
) {
124+
return NextResponse.json({ error: 'participant_ids must contain non-empty strings' }, { status: 400 });
125+
}
126+
116127
// Skip participant validation for now due to network issues
117128
// TODO: Re-enable participant validation once network connectivity is stable
118129
console.log('Skipping participant validation due to network issues');
119130
console.log('Participant IDs to add:', participant_ids);
120131

121132
// For direct messages, check if conversation already exists
122-
if (type === 'direct' && participant_ids.length === 1) {
123-
const other_user_id = participant_ids[0];
133+
if (type === 'direct' && normalizedParticipantIds.length === 1) {
134+
const other_user_id = normalizedParticipantIds[0];
124135

125136
// Check if conversation already exists between these users
126137
const { data: existingConversations } = await supabase
@@ -175,7 +186,7 @@ export async function POST(request, { params } = {}) {
175186
const participants = [];
176187

177188
// Add other participants (validate they exist in users table)
178-
for (const participantId of participant_ids) {
189+
for (const participantId of normalizedParticipantIds) {
179190
participants.push({
180191
conversation_id: conversationId,
181192
user_id: participantId,
@@ -185,7 +196,7 @@ export async function POST(request, { params } = {}) {
185196

186197
if (internalUser) {
187198
// Add creator as admin if not already in participants
188-
if (!participant_ids.includes(internalUser.id)) {
199+
if (!normalizedParticipantIds.includes(internalUser.id)) {
189200
participants.push({
190201
conversation_id: conversationId,
191202
user_id: internalUser.id,
@@ -290,4 +301,4 @@ export async function PATCH(request, { params } = {}) {
290301
console.error('API error:', error);
291302
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
292303
}
293-
}
304+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
const mocks = vi.hoisted(() => ({
4+
authGetUser: vi.fn(),
5+
from: vi.fn()
6+
}));
7+
8+
vi.mock('@/lib/supabase.js', () => ({
9+
createSupabaseServerClient: vi.fn(async () => ({
10+
auth: { getUser: mocks.authGetUser },
11+
from: mocks.from
12+
}))
13+
}));
14+
15+
function createUsersQuery() {
16+
const query = {
17+
select: vi.fn(() => query),
18+
eq: vi.fn(() => query),
19+
maybeSingle: vi.fn().mockResolvedValue({ data: null, error: null })
20+
};
21+
return query;
22+
}
23+
24+
describe('POST /api/chat/conversations participant validation', () => {
25+
beforeEach(() => {
26+
vi.resetModules();
27+
vi.clearAllMocks();
28+
mocks.authGetUser.mockResolvedValue({
29+
data: { user: { id: 'auth-user-id' } },
30+
error: null
31+
});
32+
mocks.from.mockImplementation((table) => {
33+
if (table === 'users') return createUsersQuery();
34+
throw new Error(`Unexpected database table: ${table}`);
35+
});
36+
});
37+
38+
it.each([
39+
['blank', [' ']],
40+
['non-string', [null]]
41+
])('rejects %s participant ids before conversation creation', async (_label, participant_ids) => {
42+
const { POST } = await import('./route.js');
43+
const response = await POST(new Request('https://qrypt.chat/api/chat/conversations', {
44+
method: 'POST',
45+
headers: { 'content-type': 'application/json' },
46+
body: JSON.stringify({ type: 'direct', participant_ids })
47+
}));
48+
const body = await response.json();
49+
50+
expect(response.status).toBe(400);
51+
expect(body.error).toBe('participant_ids must contain non-empty strings');
52+
expect(mocks.from).toHaveBeenCalledWith('users');
53+
expect(mocks.from).toHaveBeenCalledTimes(1);
54+
});
55+
});

0 commit comments

Comments
 (0)