mirror of
https://github.com/supabase/supabase.git
synced 2026-06-18 21:54:18 +08:00
Contributes to DOCS-1052 ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Resolves MDX linting errors related to "simple" where it applies. There was a couple cases that did not apply. For example, a product with "Simple" in the name. These changes are made in context, either by removing or using a more descriptive synonym like "minimal" or "basic". ## Tophatting 1. Read each of the diffs. 2. See that the text still makes sense in context. For extra due diligence, you can run `pnpm lint:mdx` locally and see the 'simple' errors that remain and whether they are worth addressing. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **Documentation** * Updated many guide, tutorial, and troubleshooting pages with clearer “basic”/“minimal” wording across setup steps, local testing instructions, security cautions, and RLS guidance. * Refined headings, example descriptions, and inline comments for consistency (including deployment, MCP, metrics API, and search/function phrasing). * Improved readability with small snippet formatting tweaks (whitespace plus import/comment ordering) and added a self-hosting debugging note for Envoy admin endpoints via a short-lived `curl` container. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Chris Chinchilla <chris.ward@supabase.io> Co-authored-by: Nik Richers <nrichers@gmail.com>
326 lines
10 KiB
Plaintext
326 lines
10 KiB
Plaintext
---
|
|
id: 'local-development-testing-overview'
|
|
title: 'Testing Overview'
|
|
description: 'Learn how to develop and test database schemas, tables, functions, and Row Level Security (RLS) policies.'
|
|
---
|
|
|
|
Testing is a critical part of database development, especially when working with features like Row Level Security (RLS) policies. This guide provides a comprehensive approach to testing your Supabase database.
|
|
|
|
## Testing approaches
|
|
|
|
### Database unit testing with pgTAP
|
|
|
|
[pgTAP](https://pgtap.org) is a unit testing framework for Postgres that allows testing:
|
|
|
|
- Database structure: tables, columns, constraints
|
|
- Row Level Security (RLS) policies
|
|
- Functions and procedures
|
|
- Data integrity
|
|
|
|
This example demonstrates setting up and testing RLS policies for a basic todo application:
|
|
|
|
1. Create a test table with RLS enabled:
|
|
|
|
```sql
|
|
-- Create a todos table
|
|
create table todos (
|
|
id uuid primary key default gen_random_uuid(),
|
|
task text not null,
|
|
user_id uuid references auth.users not null,
|
|
completed boolean default false
|
|
);
|
|
|
|
-- Enable RLS
|
|
alter table todos enable row level security;
|
|
|
|
-- Create a policy
|
|
create policy "Users can only access their own todos"
|
|
on todos for all -- this policy applies to all operations
|
|
to authenticated
|
|
using ((select auth.uid()) = user_id);
|
|
```
|
|
|
|
2. Set up your testing environment:
|
|
|
|
```bash
|
|
# Create a new test for our policies using supabase cli
|
|
supabase test new todos_rls.test
|
|
```
|
|
|
|
3. Write your RLS tests:
|
|
|
|
```sql
|
|
begin;
|
|
-- install tests utilities
|
|
-- install pgtap extension for testing
|
|
create extension if not exists pgtap with schema extensions;
|
|
-- Start declare we'll have 4 test cases in our test suite
|
|
select plan(4);
|
|
|
|
-- Setup our testing data
|
|
-- Set up auth.users entries
|
|
insert into auth.users (id, email) values
|
|
('123e4567-e89b-12d3-a456-426614174000', 'user1@test.com'),
|
|
('987fcdeb-51a2-43d7-9012-345678901234', 'user2@test.com');
|
|
|
|
-- Create test todos
|
|
insert into public.todos (task, user_id) values
|
|
('User 1 Task 1', '123e4567-e89b-12d3-a456-426614174000'),
|
|
('User 1 Task 2', '123e4567-e89b-12d3-a456-426614174000'),
|
|
('User 2 Task 1', '987fcdeb-51a2-43d7-9012-345678901234');
|
|
|
|
-- as User 1
|
|
set local role authenticated;
|
|
set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';
|
|
|
|
-- Test 1: User 1 should only see their own todos
|
|
select results_eq(
|
|
'select count(*) from todos',
|
|
ARRAY[2::bigint],
|
|
'User 1 should only see their 2 todos'
|
|
);
|
|
|
|
-- Test 2: User 1 can create their own todo
|
|
select lives_ok(
|
|
$$insert into todos (task, user_id) values ('New Task', '123e4567-e89b-12d3-a456-426614174000'::uuid)$$,
|
|
'User 1 can create their own todo'
|
|
);
|
|
|
|
-- as User 2
|
|
set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';
|
|
|
|
-- Test 3: User 2 should only see their own todos
|
|
select results_eq(
|
|
'select count(*) from todos',
|
|
ARRAY[1::bigint],
|
|
'User 2 should only see their 1 todo'
|
|
);
|
|
|
|
-- Test 4: User 2 cannot modify User 1's todo
|
|
SELECT results_ne(
|
|
$$ update todos set task = 'Hacked!' where user_id = '123e4567-e89b-12d3-a456-426614174000'::uuid returning 1 $$,
|
|
$$ values(1) $$,
|
|
'User 2 cannot modify User 1 todos'
|
|
);
|
|
|
|
select * from finish();
|
|
rollback;
|
|
```
|
|
|
|
4. Run the tests:
|
|
|
|
```bash
|
|
supabase test db
|
|
psql:todos_rls.test.sql:4: NOTICE: extension "pgtap" already exists, skipping
|
|
./todos_rls.test.sql .. ok
|
|
All tests successful.
|
|
Files=1, Tests=6, 0 wallclock secs ( 0.01 usr + 0.00 sys = 0.01 CPU)
|
|
Result: PASS
|
|
```
|
|
|
|
### Application-Level testing
|
|
|
|
Testing through application code provides end-to-end verification. Unlike database-level testing with pgTAP, application-level tests cannot use transactions for isolation.
|
|
|
|
<Admonition type="caution">
|
|
|
|
Application-level tests should not rely on a clean database state, as resetting the database before each test can be slow and makes tests difficult to parallelize.
|
|
Instead, design your tests to be independent by using unique user IDs for each test case.
|
|
|
|
</Admonition>
|
|
|
|
Here's an example using TypeScript that mirrors the pgTAP tests above:
|
|
|
|
```typescript
|
|
import crypto from 'crypto'
|
|
import { createClient } from '@supabase/supabase-js'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
describe('Todos RLS', () => {
|
|
// Generate unique IDs for this test suite to avoid conflicts with other tests
|
|
const USER_1_ID = crypto.randomUUID()
|
|
const USER_2_ID = crypto.randomUUID()
|
|
|
|
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLISHABLE_KEY!)
|
|
|
|
beforeAll(async () => {
|
|
// Setup test data specific to this test suite
|
|
const adminSupabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!)
|
|
|
|
// Create test users with unique IDs
|
|
await adminSupabase.auth.admin.createUser({
|
|
id: USER_1_ID,
|
|
email: `user1-${USER_1_ID}@test.com`,
|
|
password: 'password123',
|
|
// We want the user to be usable right away without email confirmation
|
|
email_confirm: true,
|
|
})
|
|
await adminSupabase.auth.admin.createUser({
|
|
id: USER_2_ID,
|
|
email: `user2-${USER_2_ID}@test.com`,
|
|
password: 'password123',
|
|
email_confirm: true,
|
|
})
|
|
|
|
// Create initial todos
|
|
await adminSupabase.from('todos').insert([
|
|
{ task: 'User 1 Task 1', user_id: USER_1_ID },
|
|
{ task: 'User 1 Task 2', user_id: USER_1_ID },
|
|
{ task: 'User 2 Task 1', user_id: USER_2_ID },
|
|
])
|
|
})
|
|
|
|
it('should allow User 1 to only see their own todos', async () => {
|
|
// Sign in as User 1
|
|
await supabase.auth.signInWithPassword({
|
|
email: `user1-${USER_1_ID}@test.com`,
|
|
password: 'password123',
|
|
})
|
|
|
|
const { data: todos } = await supabase.from('todos').select('*')
|
|
|
|
expect(todos).toHaveLength(2)
|
|
todos?.forEach((todo) => {
|
|
expect(todo.user_id).toBe(USER_1_ID)
|
|
})
|
|
})
|
|
|
|
it('should allow User 1 to create their own todo', async () => {
|
|
await supabase.auth.signInWithPassword({
|
|
email: `user1-${USER_1_ID}@test.com`,
|
|
password: 'password123',
|
|
})
|
|
|
|
const { error } = await supabase.from('todos').insert({ task: 'New Task', user_id: USER_1_ID })
|
|
|
|
expect(error).toBeNull()
|
|
})
|
|
|
|
it('should allow User 2 to only see their own todos', async () => {
|
|
// Sign in as User 2
|
|
await supabase.auth.signInWithPassword({
|
|
email: `user2-${USER_2_ID}@test.com`,
|
|
password: 'password123',
|
|
})
|
|
|
|
const { data: todos } = await supabase.from('todos').select('*')
|
|
expect(todos).toHaveLength(1)
|
|
todos?.forEach((todo) => {
|
|
expect(todo.user_id).toBe(USER_2_ID)
|
|
})
|
|
})
|
|
|
|
it('should prevent User 2 from modifying User 1 todos', async () => {
|
|
await supabase.auth.signInWithPassword({
|
|
email: `user2-${USER_2_ID}@test.com`,
|
|
password: 'password123',
|
|
})
|
|
|
|
// Attempt to update the todos we shouldn't have access to
|
|
// result will be a no-op
|
|
await supabase.from('todos').update({ task: 'Hacked!' }).eq('user_id', USER_1_ID)
|
|
|
|
// Log back in as User 1 to verify their todos weren't changed
|
|
await supabase.auth.signInWithPassword({
|
|
email: `user1-${USER_1_ID}@test.com`,
|
|
password: 'password123',
|
|
})
|
|
|
|
// Fetch User 1's todos
|
|
const { data: todos } = await supabase.from('todos').select('*')
|
|
|
|
// Verify that none of the todos were changed to "Hacked!"
|
|
expect(todos).toBeDefined()
|
|
todos?.forEach((todo) => {
|
|
expect(todo.task).not.toBe('Hacked!')
|
|
})
|
|
})
|
|
})
|
|
```
|
|
|
|
#### Test isolation strategies
|
|
|
|
For application-level testing, consider these approaches for test isolation:
|
|
|
|
1. **Unique Identifiers**: Generate unique IDs for each test suite to prevent data conflicts
|
|
2. **Cleanup After Tests**: If necessary, clean up created data in an `afterAll` or `afterEach` hook
|
|
3. **Isolated Data Sets**: Use prefixes or namespaces in data to separate test cases
|
|
|
|
### Continuous integration testing
|
|
|
|
Set up automated database testing in your CI pipeline:
|
|
|
|
1. Create a GitHub Actions workflow `.github/workflows/db-tests.yml`:
|
|
|
|
```yaml
|
|
name: Database Tests
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Supabase CLI
|
|
uses: supabase/setup-cli@v1
|
|
|
|
- name: Start Supabase
|
|
run: supabase start
|
|
|
|
- name: Run Tests
|
|
run: supabase test db
|
|
```
|
|
|
|
## Best practices
|
|
|
|
1. **Test Data Setup**
|
|
- Use begin and rollback to ensure test isolation
|
|
- Create realistic test data that covers edge cases
|
|
- Use different user roles and permissions in tests
|
|
|
|
2. **RLS Policy Testing**
|
|
- Test Create, Read, Update, Delete operations
|
|
- Test with different user roles: anonymous and authenticated
|
|
- Test edge cases and potential security bypasses
|
|
- Always test negative cases: what users should not be able to do
|
|
|
|
3. **CI/CD Integration**
|
|
- Run tests automatically on every pull request
|
|
- Include database tests in deployment pipeline
|
|
- Keep test runs fast using transactions
|
|
|
|
## Real-World examples
|
|
|
|
For more complex, real-world examples of database testing, check out:
|
|
|
|
- [Database Tests Example Repository](https://github.com/usebasejump/basejump/tree/main/supabase/tests/database) - A production-grade example of testing RLS policies
|
|
- [RLS Guide and Best Practices](https://github.com/orgs/supabase/discussions/14576)
|
|
|
|
## Troubleshooting
|
|
|
|
Common issues and solutions:
|
|
|
|
1. **Test Failures Due to RLS**
|
|
- Ensure you've set the correct role `set local role authenticated;`
|
|
- Verify JWT claims are set `set local "request.jwt.claims"`
|
|
- Check policy definitions match your test assumptions
|
|
|
|
2. **CI Pipeline Issues**
|
|
- Verify Supabase CLI is properly installed
|
|
- Ensure database migrations are run before tests
|
|
- Check for proper test isolation using transactions
|
|
|
|
## Additional resources
|
|
|
|
- [pgTAP Documentation](https://pgtap.org)
|
|
- [Supabase CLI Reference](/docs/reference/cli/supabase-test)
|
|
- [pgTAP Supabase reference](/docs/guides/database/extensions/pgtap?queryGroups=database-method&database-method=sql#testing-rls-policies)
|
|
- [Database testing reference](/docs/guides/database/testing)
|