Files
supabase/e2e/studio/features/database-webhooks.spec.ts
Vaibhav 1cffe632e3 fix: webhook apikey (#47317)
## TL;DR
Database webhooks/Cron jobs now add `apikey: <secret-key>` for edge
function auth..

## ref:
- related to: https://github.com/supabase/supabase/pull/46890
- towards COM-269

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## New Features
* Improved edge function webhook authentication by automatically
selecting the appropriate API key or authorization header format.
* Authorization headers are now added or normalized when required, while
preserving existing custom headers and supported credentials.

## Improvements
* Simplified “Add header” and “Add parameter” controls with clearer
labels.
* Updated authentication actions to clearly describe the selected header
type.

## Tests
* Expanded coverage for key formats, authorization behavior, header
preservation, and revised control labels.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Tomás Pozo <tomaspozo@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-24 10:06:36 -06:00

226 lines
8.8 KiB
TypeScript

import { expect, Page } from '@playwright/test'
import { createTable, dropTable } from '../utils/db/index.js'
import { test } from '../utils/test.js'
import { toUrl } from '../utils/to-url.js'
const ensureWebhooksEnabled = async (page: Page, ref: string) => {
await page.goto(toUrl(`/project/${ref}/integrations/webhooks/overview`))
// The original "allow you to send real-time data" copy was removed when the
// webhooks page was migrated to the new integrations UI (PR #44277). Wait
// on the page-header heading instead — it's rendered whether or not the
// supabase_functions extension is enabled.
await expect(page.getByRole('heading', { name: 'Database Webhooks' })).toBeVisible({
timeout: 30000,
})
const enableButton = page.getByRole('button', { name: 'Enable webhooks' })
if ((await enableButton.count()) > 0) {
await enableButton.click()
await expect(page.getByText('Successfully enabled webhooks')).toBeVisible({ timeout: 15000 })
}
}
const navigateToWebhooksList = async (page: Page, ref: string) => {
await page.goto(toUrl(`/project/${ref}/integrations/webhooks/webhooks`))
await expect(page.getByPlaceholder('Search for a webhook')).toBeVisible({ timeout: 30000 })
}
const createWebhookViaUI = async (page: Page, hookName: string, tableName: string) => {
await page.getByRole('button', { name: 'Create a new hook' }).click()
await expect(page.getByText('Create a new database webhook')).toBeVisible({ timeout: 10000 })
await page.locator('input[name="name"]').fill(hookName)
await page.getByRole('combobox').filter({ hasText: 'Select a table' }).click()
await page.getByRole('option', { name: new RegExp(tableName) }).click()
await page.locator('#event-INSERT').click()
await page
.getByPlaceholder('http://api.com/path/resource')
.fill('http://localhost:3000/test-webhook')
await page.getByRole('button', { name: 'Create webhook' }).click()
await expect(page.getByText(`Successfully created new webhook "${hookName}"`)).toBeVisible({
timeout: 10000,
})
}
const openWebhookEditor = async (page: Page, hookName: string) => {
const webhookText = page.getByText(hookName, { exact: true })
const webhookRow = page.locator('tr').filter({ has: webhookText })
await webhookRow.getByRole('button').click()
await page.getByRole('menuitem', { name: 'Edit hook' }).click()
await expect(page.getByText(`Update webhook ${hookName}`)).toBeVisible({ timeout: 10000 })
}
const addCustomHeader = async (page: Page, name: string, value: string) => {
await page.getByRole('button', { name: 'Add header' }).click()
await page.getByPlaceholder('Header name').last().fill(name)
await page.getByPlaceholder('Header value').last().fill(value)
}
const deleteWebhookViaUI = async (page: Page, name: string) => {
const webhookText = page.getByText(name, { exact: true })
const webhookRow = page.locator('tr').filter({ has: webhookText })
await webhookRow.getByRole('button').click()
await page.getByRole('menuitem', { name: 'Delete hook' }).click()
await expect(page.getByRole('heading', { name: 'Delete database webhook' })).toBeVisible()
await page.getByPlaceholder('Type in name of webhook').fill(name)
await page.getByRole('button', { name: `Delete ${name}` }).click()
await expect(page.getByText(`Successfully deleted ${name}`)).toBeVisible({ timeout: 10000 })
await expect(webhookRow).not.toBeVisible({ timeout: 10000 })
}
const setupTable = async (tableName: string) => {
await dropTable(tableName)
await createTable(tableName, 'data')
}
const uniqueNames = (prefix: string) => {
const i = test.info().parallelIndex
return { table: `pw_wh_${prefix}_tbl_${i}`, hook: `pw_wh_${prefix}_${i}` }
}
test.describe('Database Webhooks', () => {
test('can view webhooks list page', async ({ page, ref }) => {
await ensureWebhooksEnabled(page, ref)
await navigateToWebhooksList(page, ref)
await expect(page.getByPlaceholder('Search for a webhook')).toBeVisible()
await expect(page.getByRole('button', { name: 'Create a new hook' })).toBeVisible()
})
test('can create a new webhook with correct toast message', async ({ page, ref }) => {
const { table, hook } = uniqueNames('create')
await setupTable(table)
try {
await ensureWebhooksEnabled(page, ref)
await navigateToWebhooksList(page, ref)
await page.getByRole('button', { name: 'Create a new hook' }).click()
await expect(page.getByText('Create a new database webhook')).toBeVisible({ timeout: 10000 })
await page.locator('input[name="name"]').fill(hook)
await page.getByRole('combobox').filter({ hasText: 'Select a table' }).click()
await page.getByRole('option', { name: new RegExp(table) }).click()
await page.locator('#event-INSERT').click()
await page
.getByPlaceholder('http://api.com/path/resource')
.fill('http://localhost:3000/test-webhook')
await page.getByRole('button', { name: 'Create webhook' }).click()
await expect(page.getByText(`Successfully created new webhook "${hook}"`)).toBeVisible({
timeout: 10000,
})
await expect(page.getByText(hook, { exact: true })).toBeVisible({ timeout: 10000 })
} finally {
await dropTable(table)
}
})
test('can edit a webhook with correct toast message', async ({ page, ref }) => {
const { table, hook } = uniqueNames('edit')
await setupTable(table)
try {
await ensureWebhooksEnabled(page, ref)
await navigateToWebhooksList(page, ref)
await createWebhookViaUI(page, hook, table)
const webhookText = page.getByText(hook, { exact: true })
const webhookRow = page.locator('tr').filter({ has: webhookText })
await webhookRow.getByRole('button').click()
await page.getByRole('menuitem', { name: 'Edit hook' }).click()
await expect(page.getByText(`Update webhook ${hook}`)).toBeVisible({ timeout: 10000 })
const urlInput = page.getByPlaceholder('http://api.com/path/resource')
await urlInput.clear()
await urlInput.fill('http://localhost:3000/test-webhook-updated')
await page.getByRole('button', { name: 'Update webhook' }).click()
await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({
timeout: 10000,
})
await expect(page.getByText('Webhook not found')).not.toBeVisible()
} finally {
await dropTable(table)
}
})
test('preserves webhook URL path and custom headers after editing', async ({ page, ref }) => {
const { table, hook } = uniqueNames('edit_persist')
const originalUrl = 'http://localhost:3000/test-webhook'
const updatedUrl = 'http://localhost:3000/test-webhook-updated/path'
const headerName = 'X-API-Key'
const headerValue = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12'
await setupTable(table)
try {
await ensureWebhooksEnabled(page, ref)
await navigateToWebhooksList(page, ref)
await createWebhookViaUI(page, hook, table)
await openWebhookEditor(page, hook)
await addCustomHeader(page, headerName, headerValue)
await page.getByRole('button', { name: 'Update webhook' }).click()
await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({
timeout: 10000,
})
await openWebhookEditor(page, hook)
await expect(page.getByPlaceholder('http://api.com/path/resource')).toHaveValue(originalUrl)
await expect(page.getByPlaceholder('Header name').last()).toHaveValue(headerName)
await expect(page.getByPlaceholder('Header value').last()).toHaveValue(headerValue)
const urlInput = page.getByPlaceholder('http://api.com/path/resource')
await urlInput.clear()
await urlInput.fill(updatedUrl)
await page.getByRole('button', { name: 'Update webhook' }).click()
await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({
timeout: 10000,
})
await openWebhookEditor(page, hook)
await expect(page.getByPlaceholder('http://api.com/path/resource')).toHaveValue(updatedUrl)
await expect(page.getByPlaceholder('Header name').last()).toHaveValue(headerName)
await expect(page.getByPlaceholder('Header value').last()).toHaveValue(headerValue)
} finally {
await dropTable(table)
}
})
test('can delete a webhook with correct toast message', async ({ page, ref }) => {
const { table, hook } = uniqueNames('delete')
await setupTable(table)
try {
await ensureWebhooksEnabled(page, ref)
await navigateToWebhooksList(page, ref)
await createWebhookViaUI(page, hook, table)
await deleteWebhookViaUI(page, hook)
await expect(page.getByText(`Successfully deleted ${hook}`)).toBeVisible({ timeout: 10000 })
await expect(page.getByText(hook, { exact: true })).not.toBeVisible()
} finally {
await dropTable(table)
}
})
})