mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
Test utilities ## What is the current behavior? E2E tests that need to verify database state go through the SQL Editor, which is unnecessarily slow and prone to future breakage. ## What is the new behavior? Adds shared db utils for checking database state in Studio E2E tests by querying the database directly. Refactors API access tests to use the new utilities. ## Summary by CodeRabbit * **Tests** * Refactored API access privilege verification to use direct database queries instead of SQL editor interaction. * Added database utility functions for e2e tests to query table existence and access privileges. * Updated test signatures to simplify privilege verification calls.
444 lines
16 KiB
TypeScript
444 lines
16 KiB
TypeScript
import { expect, Page } from '@playwright/test'
|
|
|
|
import { query } from '../utils/db/index.js'
|
|
import { test } from '../utils/test.js'
|
|
import { toUrl } from '../utils/to-url.js'
|
|
import { createApiResponseWaiter, waitForTableToLoad } from '../utils/wait-for-response.js'
|
|
import { dismissToastsIfAny } from '../utils/dismiss-toast.js'
|
|
import { openTableContextMenu, deleteTable } from '../utils/table-helpers.js'
|
|
|
|
const TABLE_NAME_PREFIX = 'pw_api_access'
|
|
|
|
/**
|
|
* Verifies that the table has the expected API privileges for anon and authenticated roles.
|
|
* Uses the database utility to query privileges directly.
|
|
*/
|
|
async function verifyTablePrivileges(
|
|
schemaName: string,
|
|
tableName: string,
|
|
expectedPrivileges: {
|
|
anon: Array<string>
|
|
authenticated: Array<string>
|
|
}
|
|
) {
|
|
const results = await query<{ grantee: string; privilege_type: string }>(
|
|
`SELECT grantee, privilege_type
|
|
FROM information_schema.role_table_grants
|
|
WHERE table_schema = $1
|
|
AND table_name = $2
|
|
AND grantee IN ('anon', 'authenticated')
|
|
AND privilege_type IN ('SELECT', 'INSERT', 'UPDATE', 'DELETE')
|
|
ORDER BY grantee, privilege_type`,
|
|
[schemaName, tableName]
|
|
)
|
|
|
|
// Group privileges by grantee
|
|
const actualPrivileges: Record<string, Array<string>> = {
|
|
anon: [],
|
|
authenticated: [],
|
|
}
|
|
|
|
for (const row of results) {
|
|
if (row.grantee === 'anon' || row.grantee === 'authenticated') {
|
|
actualPrivileges[row.grantee].push(row.privilege_type)
|
|
}
|
|
}
|
|
|
|
// Sort for comparison
|
|
actualPrivileges.anon.sort()
|
|
actualPrivileges.authenticated.sort()
|
|
const sortedExpected = {
|
|
anon: [...expectedPrivileges.anon].sort(),
|
|
authenticated: [...expectedPrivileges.authenticated].sort(),
|
|
}
|
|
|
|
expect(actualPrivileges.anon).toEqual(sortedExpected.anon)
|
|
expect(actualPrivileges.authenticated).toEqual(sortedExpected.authenticated)
|
|
}
|
|
|
|
/**
|
|
* Locates the API access toggle switch for Data API Access.
|
|
* The switch is labeled by the nearby "Data API Access" text.
|
|
*/
|
|
function getApiAccessToggle(page: Page) {
|
|
const sidePanel = page.getByTestId('table-editor-side-panel')
|
|
// The switch is near the "Data API Access" label - get the section first, then find the switch
|
|
const dataApiSection = sidePanel
|
|
.locator('div')
|
|
.filter({ hasText: 'Data API Access' })
|
|
.filter({ has: page.getByRole('switch') })
|
|
return dataApiSection.getByRole('switch')
|
|
}
|
|
|
|
/**
|
|
* Locates the settings button for granular privilege settings.
|
|
*/
|
|
function getPrivilegeSettingsButton(page: Page) {
|
|
const sidePanel = page.getByTestId('table-editor-side-panel')
|
|
return sidePanel.getByRole('button', { name: 'Configure API privileges' })
|
|
}
|
|
|
|
/**
|
|
* Gets the privilege selector combobox for a specific role in the privileges popover.
|
|
* The popover must already be open.
|
|
*/
|
|
function getRolePrivilegeSelector(page: Page, roleLabel: 'Anonymous (anon)' | 'Authenticated') {
|
|
// The popover is a dialog with structure: paragraph (role label) followed by combobox
|
|
// We find the paragraph with the role text, then get the adjacent combobox
|
|
const popoverContent = page.locator('[data-radix-popper-content-wrapper]')
|
|
// Get the paragraph containing the role label, then navigate to the sibling combobox
|
|
return popoverContent.getByText(roleLabel, { exact: true }).locator('..').getByRole('combobox')
|
|
}
|
|
|
|
test.describe('API Access Toggle', () => {
|
|
test.beforeEach(async ({ page, ref }) => {
|
|
const loadPromise = waitForTableToLoad(page, ref)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await loadPromise
|
|
})
|
|
|
|
test('API access is default on for a new table', async ({ page, ref }) => {
|
|
const tableName = `${TABLE_NAME_PREFIX}_default_on`
|
|
|
|
// Open new table dialog
|
|
await page.getByRole('button', { name: 'New table', exact: true }).click()
|
|
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
|
|
|
|
// Fill in table name
|
|
await page.getByTestId('table-name-input').fill(tableName)
|
|
|
|
// Find and click the API access toggle to turn it off
|
|
const toggle = getApiAccessToggle(page)
|
|
await expect(toggle).toBeChecked()
|
|
|
|
// Create the table
|
|
const createTablePromise = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=table-create'
|
|
)
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await createTablePromise
|
|
|
|
// Wait for success toast which indicates all operations are complete
|
|
await expect(
|
|
page.getByText(`Table ${tableName} is good to go!`),
|
|
'Success toast should appear after table creation'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
// Dismiss toast to prevent it from blocking subsequent interactions
|
|
await dismissToastsIfAny(page)
|
|
|
|
await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' })
|
|
|
|
// Verify table was created
|
|
await expect(
|
|
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
|
|
'Table should be visible after creation'
|
|
).toBeVisible()
|
|
|
|
// Verify all API access privileges were granted
|
|
await verifyTablePrivileges('public', tableName, {
|
|
anon: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
|
authenticated: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
|
})
|
|
})
|
|
|
|
test('can toggle API access off for a new table', async ({ page, ref }) => {
|
|
const tableName = `${TABLE_NAME_PREFIX}_toggle_off`
|
|
|
|
// Open new table dialog
|
|
await page.getByRole('button', { name: 'New table', exact: true }).click()
|
|
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
|
|
|
|
// Fill in table name
|
|
await page.getByTestId('table-name-input').fill(tableName)
|
|
|
|
// Find and click the API access toggle to turn it off
|
|
const toggle = getApiAccessToggle(page)
|
|
await expect(toggle).toBeChecked()
|
|
await toggle.click()
|
|
await expect(toggle, 'Toggle should be unchecked after clicking').not.toBeChecked()
|
|
|
|
// Create the table
|
|
const createTablePromise = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=table-create'
|
|
)
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await createTablePromise
|
|
|
|
// Wait for success toast which indicates all operations are complete
|
|
await expect(
|
|
page.getByText(`Table ${tableName} is good to go!`),
|
|
'Success toast should appear after table creation'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
// Dismiss toast to prevent it from blocking subsequent interactions
|
|
await dismissToastsIfAny(page)
|
|
|
|
await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' })
|
|
|
|
// Verify table was created
|
|
await expect(
|
|
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
|
|
'Table should be visible after creation'
|
|
).toBeVisible()
|
|
|
|
// Verify no API access privileges were granted
|
|
await verifyTablePrivileges('public', tableName, {
|
|
anon: [],
|
|
authenticated: [],
|
|
})
|
|
})
|
|
|
|
test('shows API access toggle when editing an existing table', async ({ page, ref }) => {
|
|
const tableName = `${TABLE_NAME_PREFIX}_edit`
|
|
|
|
// Create a table first
|
|
await page.getByRole('button', { name: 'New table', exact: true }).click()
|
|
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
|
|
await page.getByTestId('table-name-input').fill(tableName)
|
|
|
|
const createPromise = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-create')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await createPromise
|
|
|
|
// Wait for success toast which indicates all operations are complete
|
|
await expect(
|
|
page.getByText(`Table ${tableName} is good to go!`),
|
|
'Success toast should appear after table creation'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
// Dismiss toast to prevent it from blocking subsequent interactions
|
|
await dismissToastsIfAny(page)
|
|
|
|
await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' })
|
|
|
|
// Verify table was created
|
|
await expect(
|
|
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
|
|
'Table should be visible after creation'
|
|
).toBeVisible()
|
|
|
|
// Verify default full privileges were granted
|
|
await verifyTablePrivileges('public', tableName, {
|
|
anon: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
|
authenticated: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
|
})
|
|
|
|
// Navigate back to table editor
|
|
let loadPromise = waitForTableToLoad(page, ref)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await loadPromise
|
|
|
|
// Click on the table to view it
|
|
const navigationPromise = page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await navigationPromise
|
|
|
|
// Open edit table dialog via context menu
|
|
await openTableContextMenu(page, tableName)
|
|
await page.getByRole('menuitem', { name: 'Edit table' }).click()
|
|
|
|
// Verify the side panel is open
|
|
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
|
|
|
|
// Verify Data API Access section is visible
|
|
await expect(
|
|
page.getByText('Data API Access'),
|
|
'Data API Access label should be visible in edit mode'
|
|
).toBeVisible()
|
|
|
|
// Verify the toggle is present
|
|
const toggle = getApiAccessToggle(page)
|
|
await expect(toggle, 'API Access toggle should be visible in edit mode').toBeVisible()
|
|
})
|
|
|
|
test('creates table with partial privileges and verifies correct grants', async ({
|
|
page,
|
|
ref,
|
|
}) => {
|
|
const tableName = `${TABLE_NAME_PREFIX}_partial_grants`
|
|
|
|
// Open new table dialog
|
|
await page.getByRole('button', { name: 'New table', exact: true }).click()
|
|
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
|
|
|
|
// Fill in table name
|
|
await page.getByTestId('table-name-input').fill(tableName)
|
|
|
|
// Open the privilege settings popover
|
|
const settingsButton = getPrivilegeSettingsButton(page)
|
|
await settingsButton.click()
|
|
|
|
await expect(page.getByText('Adjust API privileges per role')).toBeVisible()
|
|
|
|
// Modify anon privileges - leave only SELECT
|
|
const anonSelector = getRolePrivilegeSelector(page, 'Anonymous (anon)')
|
|
await anonSelector.click()
|
|
|
|
// Click DELETE to toggle it off
|
|
await page.getByRole('option', { name: 'DELETE' }).click()
|
|
// Click UPDATE to toggle it off
|
|
await page.getByRole('option', { name: 'UPDATE' }).click()
|
|
await page.getByRole('option', { name: 'INSERT' }).click()
|
|
|
|
// Close the dropdown by clicking the combobox again
|
|
await anonSelector.click()
|
|
|
|
// Wait for dropdown to close
|
|
await expect(page.getByRole('option', { name: 'DELETE' })).not.toBeVisible({ timeout: 2000 })
|
|
|
|
// Modify authenticated privileges - remove DELETE and UPDATE (leave SELECT + INSERT)
|
|
const authSelector = getRolePrivilegeSelector(page, 'Authenticated')
|
|
await authSelector.click()
|
|
|
|
// Remove all except SELECT
|
|
await page.getByRole('option', { name: 'DELETE' }).click()
|
|
await page.getByRole('option', { name: 'UPDATE' }).click()
|
|
|
|
// Close the dropdown by clicking the combobox again
|
|
await authSelector.click()
|
|
|
|
// Wait for dropdown to close
|
|
await expect(page.getByRole('option', { name: 'DELETE' })).not.toBeVisible({ timeout: 2000 })
|
|
|
|
// Close the popover by pressing Escape
|
|
await page.keyboard.press('Escape')
|
|
|
|
// Create the table
|
|
const createTablePromise = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=table-create'
|
|
)
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await createTablePromise
|
|
|
|
// Wait for success toast which indicates all operations (including privilege updates) are complete
|
|
await expect(
|
|
page.getByText(`Table ${tableName} is good to go!`),
|
|
'Success toast should appear after table creation'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' })
|
|
|
|
// Verify table was created
|
|
await expect(
|
|
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
|
|
'Table should be visible after creation'
|
|
).toBeVisible()
|
|
|
|
// Verify partial grants - anon: SELECT; authenticated: SELECT, INSERT
|
|
await verifyTablePrivileges('public', tableName, {
|
|
anon: ['SELECT'],
|
|
authenticated: ['SELECT', 'INSERT'],
|
|
})
|
|
})
|
|
|
|
test('preserves API grants when editing non-privilege table properties', async ({
|
|
page,
|
|
ref,
|
|
}) => {
|
|
const tableName = `${TABLE_NAME_PREFIX}_preserve_grants`
|
|
|
|
// Step 1: Create a table with partial privileges (only SELECT and INSERT for anon)
|
|
await page.getByRole('button', { name: 'New table', exact: true }).click()
|
|
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
|
|
|
|
await page.getByTestId('table-name-input').fill(tableName)
|
|
|
|
// Open privilege settings and set partial privileges
|
|
const settingsButton = getPrivilegeSettingsButton(page)
|
|
await settingsButton.click()
|
|
await expect(page.getByText('Adjust API privileges per role')).toBeVisible()
|
|
|
|
// Modify anon privileges - keep only SELECT and INSERT
|
|
const anonSelector = getRolePrivilegeSelector(page, 'Anonymous (anon)')
|
|
await anonSelector.click()
|
|
await page.getByRole('option', { name: 'DELETE' }).click()
|
|
await page.getByRole('option', { name: 'UPDATE' }).click()
|
|
await anonSelector.click()
|
|
await expect(page.getByRole('option', { name: 'DELETE' })).not.toBeVisible({ timeout: 2000 })
|
|
|
|
// Keep authenticated with full privileges
|
|
await page.keyboard.press('Escape') // Close popover
|
|
|
|
// Create the table
|
|
let createPromise = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-create')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await createPromise
|
|
|
|
// Wait for success toast which indicates all operations (including privilege updates) are complete
|
|
await expect(
|
|
page.getByText(`Table ${tableName} is good to go!`),
|
|
'Success toast should appear after table creation'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' })
|
|
|
|
await expect(
|
|
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
|
|
'Table should be created'
|
|
).toBeVisible()
|
|
|
|
// Verify initial privileges before edit
|
|
await verifyTablePrivileges('public', tableName, {
|
|
anon: ['SELECT', 'INSERT'],
|
|
authenticated: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
|
})
|
|
|
|
// Navigate back to table editor
|
|
let loadPromise = waitForTableToLoad(page, ref)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await loadPromise
|
|
|
|
// Step 2: Edit the table's description (without touching privileges)
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await openTableContextMenu(page, tableName)
|
|
await page.getByRole('menuitem', { name: 'Edit table' }).click()
|
|
|
|
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
|
|
|
|
// Add a description without modifying privileges
|
|
const descriptionInput = page
|
|
.getByTestId('table-editor-side-panel')
|
|
.getByPlaceholder('Optional')
|
|
await descriptionInput.fill('Test description for grant preservation')
|
|
|
|
// Save the changes
|
|
const updatePromise = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-update')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await updatePromise
|
|
|
|
// Wait for success toast which indicates all operations are complete
|
|
await expect(
|
|
page.getByText(`Successfully updated ${tableName}!`),
|
|
'Success toast should appear after table update'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' })
|
|
|
|
// Step 3: Verify the privileges remain unchanged after edit
|
|
await verifyTablePrivileges('public', tableName, {
|
|
anon: ['SELECT', 'INSERT'],
|
|
authenticated: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
|
})
|
|
|
|
// Navigate back to table editor for cleanup
|
|
loadPromise = waitForTableToLoad(page, ref)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await loadPromise
|
|
|
|
// Clean up
|
|
await deleteTable(page, ref, tableName)
|
|
})
|
|
})
|