Skip to content
Open
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
112 changes: 56 additions & 56 deletions src/app/api/webhooks/route.js
Original file line number Diff line number Diff line change
@@ -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 });
});
60 changes: 60 additions & 0 deletions tests/api/webhooks-route.test.js
Original file line number Diff line number Diff line change
@@ -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'],
});
});
});
Loading