| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- import { fireEvent, screen, waitFor } from '@testing-library/react'
- import userEvent from '@testing-library/user-event'
- import type { ComponentProps } from 'react'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import type { WebhookEndpoint } from './PlatformWebhooks.types'
- import { PlatformWebhooksEndpointSheet, toEndpointPayload } from './PlatformWebhooksEndpointSheet'
- import { customRender } from '@/tests/lib/custom-render'
- const { generateWebhookEndpointNameMock } = vi.hoisted(() => ({
- generateWebhookEndpointNameMock: vi.fn(() => 'winged-envelope'),
- }))
- vi.mock(import('./PlatformWebhooks.utils'), async (importOriginal) => {
- const actual = await importOriginal()
- return {
- ...actual,
- generateWebhookEndpointName: generateWebhookEndpointNameMock,
- }
- })
- const PROJECT_EVENT_TYPES = ['project.updated', 'project.paused']
- const createEndpoint = (overrides?: Partial<WebhookEndpoint>): WebhookEndpoint => ({
- id: '3c9b7e21-8d54-4f63-b2a1-6e7d8c9f0a12',
- name: 'Billing events',
- url: 'https://hooks.example.com/billing',
- description: 'Invoices and receipts',
- enabled: true,
- eventTypes: ['project.updated'],
- customHeaders: [],
- createdBy: 'user@supabase.io',
- createdAt: '2026-03-16T00:00:00.000Z',
- ...overrides,
- })
- const renderEndpointSheet = (
- props?: Partial<ComponentProps<typeof PlatformWebhooksEndpointSheet>>
- ) => {
- const onClose = vi.fn()
- const onSubmit = vi.fn()
- customRender(
- <PlatformWebhooksEndpointSheet
- visible
- mode="create"
- scope="project"
- eventTypes={PROJECT_EVENT_TYPES}
- onClose={onClose}
- onSubmit={onSubmit}
- {...props}
- />
- )
- return { onClose, onSubmit }
- }
- const submitForm = () =>
- fireEvent.submit(document.getElementById('platform-webhook-endpoint-form')!)
- const getUrlInput = () => screen.getByPlaceholderText('https://api.example.com/webhooks/briven')
- const findEventTypeCheckbox = (eventType: string) =>
- screen.findByRole('checkbox', {
- name: new RegExp(eventType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
- })
- describe('PlatformWebhooksEndpointSheet', () => {
- afterEach(() => {
- vi.clearAllMocks()
- })
- it('prefills the name field when creating an endpoint', async () => {
- renderEndpointSheet()
- expect(await screen.findByDisplayValue('winged-envelope')).toBeInTheDocument()
- })
- it('loads the existing name and description in edit mode', async () => {
- renderEndpointSheet({
- mode: 'edit',
- endpoint: createEndpoint(),
- })
- expect(await screen.findByDisplayValue('Billing events')).toBeInTheDocument()
- expect(screen.getByDisplayValue('Invoices and receipts')).toBeInTheDocument()
- })
- it('submits an empty name when an existing name is cleared', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet({
- mode: 'edit',
- endpoint: createEndpoint(),
- })
- const nameInput = await screen.findByDisplayValue('Billing events')
- await user.clear(nameInput)
- submitForm()
- await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
- expect(onSubmit).toHaveBeenCalledWith(
- expect.objectContaining({
- name: '',
- url: 'https://hooks.example.com/billing',
- description: 'Invoices and receipts',
- }),
- expect.anything()
- )
- })
- it('blocks submit when the endpoint URL is empty', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet()
- await user.click(await findEventTypeCheckbox('project.updated'))
- submitForm()
- expect(await screen.findByText('Please provide a URL')).toBeInTheDocument()
- expect(onSubmit).not.toHaveBeenCalled()
- })
- it('blocks submit when the endpoint URL is malformed', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet()
- await user.type(getUrlInput(), 'https://not a url')
- await user.click(await findEventTypeCheckbox('project.updated'))
- submitForm()
- expect(await screen.findByText('Please provide a valid URL')).toBeInTheDocument()
- expect(onSubmit).not.toHaveBeenCalled()
- })
- it('blocks submit when the endpoint URL uses an incomplete hostname', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet()
- await user.type(getUrlInput(), 'https://webhook')
- await user.click(await findEventTypeCheckbox('project.updated'))
- submitForm()
- expect(await screen.findByText('Please provide a valid URL')).toBeInTheDocument()
- expect(onSubmit).not.toHaveBeenCalled()
- })
- it('blocks submit when the endpoint URL does not include a protocol', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet()
- await user.type(getUrlInput(), 'hooks.example.com/billing')
- await user.click(await findEventTypeCheckbox('project.updated'))
- submitForm()
- expect(
- await screen.findByText('Please prefix your URL with http:// or https://')
- ).toBeInTheDocument()
- expect(onSubmit).not.toHaveBeenCalled()
- })
- it('shows an error when no event types are selected', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet()
- await user.type(getUrlInput(), 'https://hooks.example.com/billing')
- submitForm()
- expect(await screen.findByText('Select at least one event type')).toBeInTheDocument()
- expect(onSubmit).not.toHaveBeenCalled()
- })
- it('clears the event type error after selecting an event and allows submit', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet()
- await user.type(getUrlInput(), 'https://hooks.example.com/billing')
- submitForm()
- expect(await screen.findByText('Select at least one event type')).toBeInTheDocument()
- await user.click(await findEventTypeCheckbox('project.updated'))
- await waitFor(() => {
- expect(screen.queryByText('Select at least one event type')).not.toBeInTheDocument()
- })
- submitForm()
- await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
- expect(onSubmit).toHaveBeenCalledWith(
- expect.objectContaining({
- url: 'https://hooks.example.com/billing',
- eventTypes: ['project.updated'],
- }),
- expect.anything()
- )
- })
- it('allows submit when subscribe all is enabled', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet()
- await user.type(getUrlInput(), 'https://hooks.example.com/billing')
- await user.click(screen.getByRole('checkbox', { name: /subscribe to all events/i }))
- submitForm()
- await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
- expect(onSubmit).toHaveBeenCalledWith(
- expect.objectContaining({
- subscribeAll: true,
- eventTypes: PROJECT_EVENT_TYPES,
- url: 'https://hooks.example.com/billing',
- }),
- expect.anything()
- )
- })
- it('submits custom headers added through the shared header editor', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet({
- mode: 'edit',
- endpoint: createEndpoint(),
- })
- await user.click(screen.getByRole('button', { name: 'Add header' }))
- await user.type(screen.getByPlaceholderText('Header name'), 'X-Webhook-Secret')
- await user.type(screen.getByPlaceholderText('Header value'), 'super-secret')
- submitForm()
- await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
- expect(onSubmit).toHaveBeenCalledWith(
- expect.objectContaining({
- customHeaders: [{ key: 'X-Webhook-Secret', value: 'super-secret' }],
- }),
- expect.anything()
- )
- })
- it('blocks submit when a custom header is missing its value', async () => {
- const user = userEvent.setup()
- const { onSubmit } = renderEndpointSheet({
- mode: 'edit',
- endpoint: createEndpoint(),
- })
- await user.click(screen.getByRole('button', { name: 'Add header' }))
- await user.type(screen.getByPlaceholderText('Header name'), 'X-Webhook-Secret')
- submitForm()
- expect(await screen.findByText('Header value is required')).toBeInTheDocument()
- expect(onSubmit).not.toHaveBeenCalled()
- })
- it('strips fully empty custom header rows from the payload', () => {
- expect(
- toEndpointPayload({
- name: 'Billing events',
- url: 'https://hooks.example.com/billing',
- description: '',
- enabled: true,
- subscribeAll: false,
- eventTypes: ['project.updated'],
- customHeaders: [
- { key: 'X-Webhook-Secret', value: 'super-secret' },
- { key: '', value: '' },
- ],
- })
- ).toEqual(
- expect.objectContaining({
- customHeaders: [{ key: 'X-Webhook-Secret', value: 'super-secret' }],
- })
- )
- })
- })
|