Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/app/api/conversations/join/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ export const POST = withAuth(async ({ request, locals }) => {

const userId = userData.id;

const { data: participant, error: participantError } = await supabase
.from('conversation_participants')
.select('id')
.eq('conversation_id', conversationId)
.eq('user_id', userId)
.is('left_at', null)
.single();

if (participantError || !participant) {
return NextResponse.json({ error: 'Access denied to conversation' }, { status: 403 });
}

// Join SSE room for real-time updates
sseManager.joinRoom(userId, conversationId);

Expand Down
40 changes: 39 additions & 1 deletion src/app/api/conversations/join/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ const mocks = vi.hoisted(() => ({
from: vi.fn()
},
mockJoinRoom: vi.fn(),
userEq: vi.fn()
userEq: vi.fn(),
participantEq: vi.fn(),
participantIs: vi.fn(),
participantSingle: vi.fn()
}));

vi.mock('@/lib/api/middleware/auth.js', () => ({
Expand Down Expand Up @@ -39,6 +42,18 @@ function createUserQuery() {
return query;
}

function createParticipantQuery() {
const query = {
select: vi.fn(() => query),
eq: mocks.participantEq,
is: mocks.participantIs,
single: mocks.participantSingle
};
mocks.participantEq.mockReturnValue(query);
mocks.participantIs.mockReturnValue(query);
return query;
}

function createRequest(conversationId) {
return {
json: vi.fn().mockResolvedValue({ conversationId })
Expand All @@ -49,8 +64,13 @@ describe('POST /api/conversations/join', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mocks.participantSingle.mockResolvedValue({
data: { id: 'participant-id' },
error: null
});
mocks.mockSupabase.from.mockImplementation((table) => {
if (table === 'users') return createUserQuery();
if (table === 'conversation_participants') return createParticipantQuery();
throw new Error(`Unexpected table: ${table}`);
});
});
Expand All @@ -72,6 +92,24 @@ describe('POST /api/conversations/join', () => {

expect(response.status).toBe(200);
expect(mocks.userEq).toHaveBeenCalledWith('auth_user_id', 'auth-user-id');
expect(mocks.participantEq).toHaveBeenCalledWith('conversation_id', 'conversation-1');
expect(mocks.participantEq).toHaveBeenCalledWith('user_id', 'internal-user-id');
expect(mocks.participantIs).toHaveBeenCalledWith('left_at', null);
expect(mocks.mockJoinRoom).toHaveBeenCalledWith('internal-user-id', 'conversation-1');
});

it('rejects users who are not active conversation participants', async () => {
mocks.participantSingle.mockResolvedValue({
data: null,
error: { message: 'No rows found' }
});

const { POST } = await import('./route.js');
const response = await POST(createRequest('conversation-1'));
const body = await response.json();

expect(response.status).toBe(403);
expect(body).toEqual({ error: 'Access denied to conversation' });
expect(mocks.mockJoinRoom).not.toHaveBeenCalled();
});
});
Loading