diff --git a/src/app/api/webhooks/route.js b/src/app/api/webhooks/route.js index cd8fd6c8..9531b5cb 100644 --- a/src/app/api/webhooks/route.js +++ b/src/app/api/webhooks/route.js @@ -1,61 +1,61 @@ -import { NextResponse } from 'next/server'; -import { withAuth } from '@/lib/api/middleware/auth.js'; - -/** List webhooks for the authenticated user */ -export const GET = withAuth(async (event) => { - const { supabase, user } = event.locals; - - const { data, error } = await supabase - .from('webhooks') - .select('id, url, events, created_at') - .eq('user_id', user.id) - .order('created_at', { ascending: false }); - - if (error) { - console.error('[WEBHOOKS] List error:', error.message); - return NextResponse.json({ error: 'Failed to list webhooks' }, { status: 500 }); - } - - return NextResponse.json({ webhooks: data }); -}); - -/** Create a new webhook */ -export const POST = withAuth(async (event) => { - const { supabase, user } = event.locals; - +import { NextResponse } from 'next/server'; +import { withAuth } from '@/lib/api/middleware/auth.js'; + +/** List webhooks for the authenticated user */ +export const GET = withAuth(async (event) => { + const { supabase, user } = event.locals; + + const { data, error } = await supabase + .from('webhooks') + .select('id, url, events, created_at') + .eq('user_id', user.id) + .order('created_at', { ascending: false }); + + if (error) { + console.error('[WEBHOOKS] List error:', error.message); + return NextResponse.json({ error: 'Failed to list webhooks' }, { status: 500 }); + } + + return NextResponse.json({ webhooks: data }); +}); + +/** Create a new webhook */ +export const POST = withAuth(async (event) => { + const { supabase, user } = event.locals; + let body; try { - body = await request.json(); + body = await event.request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } - - const { url, events } = body; - - if (!url || typeof url !== 'string') { - return NextResponse.json({ error: 'url is required' }, { status: 400 }); - } - - try { - new URL(url); - } catch { - return NextResponse.json({ error: 'Invalid URL' }, { status: 400 }); - } - - if (!Array.isArray(events) || events.length === 0) { - return NextResponse.json({ error: 'events must be a non-empty array' }, { status: 400 }); - } - - const { data, error } = await supabase - .from('webhooks') - .insert({ user_id: user.id, url, events }) - .select('id, url, events, created_at') - .single(); - - if (error) { - console.error('[WEBHOOKS] Create error:', error.message); - return NextResponse.json({ error: 'Failed to create webhook' }, { status: 500 }); - } - - return NextResponse.json({ webhook: data }, { status: 201 }); -}); + + const { url, events } = body; + + if (!url || typeof url !== 'string') { + return NextResponse.json({ error: 'url is required' }, { status: 400 }); + } + + try { + new URL(url); + } catch { + return NextResponse.json({ error: 'Invalid URL' }, { status: 400 }); + } + + if (!Array.isArray(events) || events.length === 0) { + return NextResponse.json({ error: 'events must be a non-empty array' }, { status: 400 }); + } + + const { data, error } = await supabase + .from('webhooks') + .insert({ user_id: user.id, url, events }) + .select('id, url, events, created_at') + .single(); + + if (error) { + console.error('[WEBHOOKS] Create error:', error.message); + return NextResponse.json({ error: 'Failed to create webhook' }, { status: 500 }); + } + + return NextResponse.json({ webhook: data }, { status: 201 }); +}); diff --git a/tests/api/webhooks-route.test.js b/tests/api/webhooks-route.test.js new file mode 100644 index 00000000..b729376e --- /dev/null +++ b/tests/api/webhooks-route.test.js @@ -0,0 +1,60 @@ +import { describe, expect, it, vi } from 'vitest'; + +const mockSupabase = { + auth: { + getUser: vi.fn(), + }, + from: vi.fn(), +}; + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(async () => mockSupabase), + createSupabaseServerClientWithToken: vi.fn(async () => mockSupabase), +})); + +import { POST } from '../../src/app/api/webhooks/route.js'; + +function jsonRequest(body) { + return { + headers: { + get: vi.fn(() => null), + }, + json: vi.fn(async () => body), + }; +} + +describe('POST /api/webhooks', () => { + it('parses the authenticated event request body before creating a webhook', async () => { + mockSupabase.auth.getUser.mockResolvedValue({ + data: { user: { id: 'user-123' } }, + error: null, + }); + + const single = vi.fn(async () => ({ + data: { + id: 'webhook-123', + url: 'https://example.com/hook', + events: ['message.created'], + created_at: '2026-06-13T00:00:00.000Z', + }, + error: null, + })); + const select = vi.fn(() => ({ single })); + const insert = vi.fn(() => ({ select })); + mockSupabase.from.mockReturnValue({ insert }); + + const response = await POST(jsonRequest({ + url: 'https://example.com/hook', + events: ['message.created'], + })); + const body = await response.json(); + + expect(response.status).toBe(201); + expect(body.webhook.id).toBe('webhook-123'); + expect(insert).toHaveBeenCalledWith({ + user_id: 'user-123', + url: 'https://example.com/hook', + events: ['message.created'], + }); + }); +});