mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +03:00
## What
Wires the existing (mocked) Platform Webhooks UI to the real Management
API backend, per FE-3749.
- New `data/platform-webhooks/` query/mutation hooks, calling the real
`/v2/organizations/{slug}/webhooks/...` and
`/v2/projects/{ref}/webhooks/...` endpoints
- Updated `PlatformWebhooks.types.ts` to match the real API contract
(field names, shapes)
- Added a signing secret field to the create-endpoint form (API requires
a user-provided secret, not server-generated like the mock assumed)
- Removed the event-type column from the deliveries table (the real
list-deliveries endpoint doesn't return event type, only the
single-delivery endpoint does)
- Removed `PlatformWebhooks.mock.ts` / `.store.ts` and their tests
## Why
UI was built ~80% against mock data months ago; this closes the gap to a
real, working feature.
## Known open items (not blocking, tracked separately)
- `name` field has no backend storage yet — client-only for now, open
question with Ali/Paweł
- Deliveries table only fetches the backend's default single page — real
server-side pagination not yet wired (see FE-3750)
## Follow-up PR (stacked)
Tests for the new `data/platform-webhooks/` hooks (MSW-based), per the
ticket's own scope split.
## Testing
Manually tested end-to-end against local backend: created/edited/deleted
an endpoint, verified the real signature using webhook.site,
independently recomputed the HMAC and confirmed it matches the
`webhook-signature` header.
Linear: FE-3749
---------
Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { WebhookDelivery, WebhookEndpoint } from './PlatformWebhooks.types'
|
|
import {
|
|
filterWebhookDeliveries,
|
|
filterWebhookEndpoints,
|
|
generateWebhookEndpointName,
|
|
getWebhookEndpointDisplayName,
|
|
} from './PlatformWebhooks.utils'
|
|
|
|
const createEndpoint = (overrides: Partial<WebhookEndpoint> = {}): WebhookEndpoint => ({
|
|
id: 'endpoint-1',
|
|
name: '',
|
|
url: 'https://hooks.example.com/billing',
|
|
description: '',
|
|
enabled: true,
|
|
eventTypes: ['v1.project.updated'],
|
|
customHeaders: [],
|
|
createdBy: 'user@supabase.io',
|
|
createdAt: '2026-03-16T00:00:00.000Z',
|
|
...overrides,
|
|
})
|
|
|
|
const createDelivery = (overrides: Partial<WebhookDelivery> = {}): WebhookDelivery => ({
|
|
id: 'delivery-1',
|
|
endpointId: 'endpoint-1',
|
|
eventId: 'event-1',
|
|
status: 'success',
|
|
responseCode: 200,
|
|
responseBody: null,
|
|
responseHeaders: null,
|
|
attemptAt: '2026-03-16T00:00:00.000Z',
|
|
...overrides,
|
|
})
|
|
|
|
describe('PlatformWebhooks.utils', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('returns the trimmed endpoint name when present', () => {
|
|
expect(
|
|
getWebhookEndpointDisplayName({
|
|
name: ' Billing events ',
|
|
url: 'https://hooks.example.com/billing',
|
|
})
|
|
).toBe('Billing events')
|
|
})
|
|
|
|
it('falls back to the endpoint url when the name is empty', () => {
|
|
expect(
|
|
getWebhookEndpointDisplayName({
|
|
name: ' ',
|
|
url: 'https://hooks.example.com/fallback',
|
|
})
|
|
).toBe('https://hooks.example.com/fallback')
|
|
})
|
|
|
|
it('generates an adjective-noun endpoint name', () => {
|
|
vi.spyOn(Math, 'random').mockReturnValueOnce(0).mockReturnValueOnce(0.1)
|
|
|
|
expect(generateWebhookEndpointName()).toBe('swift-courier')
|
|
})
|
|
|
|
it('filters endpoints by name, url, and description', () => {
|
|
const endpoints = [
|
|
createEndpoint({ id: '1', name: 'Billing events', url: 'https://hooks.example.com/billing' }),
|
|
createEndpoint({
|
|
id: '2',
|
|
name: 'Slack alerts',
|
|
url: 'https://hooks.slack.com/x',
|
|
description: 'operational alerts',
|
|
}),
|
|
]
|
|
|
|
expect(filterWebhookEndpoints(endpoints, 'billing')).toEqual([endpoints[0]])
|
|
expect(filterWebhookEndpoints(endpoints, 'slack')).toEqual([endpoints[1]])
|
|
expect(filterWebhookEndpoints(endpoints, 'alerts')).toEqual([endpoints[1]])
|
|
})
|
|
|
|
it('filters deliveries by event type, status, and response code, sorted newest first', () => {
|
|
const deliveries = [
|
|
createDelivery({
|
|
id: 'a',
|
|
eventType: 'v1.project.updated',
|
|
status: 'success',
|
|
responseCode: 200,
|
|
attemptAt: '2026-03-16T00:00:00.000Z',
|
|
}),
|
|
createDelivery({
|
|
id: 'b',
|
|
eventType: 'v1.project.paused',
|
|
status: 'failure',
|
|
responseCode: 500,
|
|
attemptAt: '2026-03-17T00:00:00.000Z',
|
|
}),
|
|
]
|
|
|
|
expect(filterWebhookDeliveries(deliveries, '')).toEqual([deliveries[1], deliveries[0]])
|
|
expect(filterWebhookDeliveries(deliveries, 'paused')).toEqual([deliveries[1]])
|
|
expect(filterWebhookDeliveries(deliveries, '500')).toEqual([deliveries[1]])
|
|
})
|
|
})
|