# Database Testing with pgTAP

> Learn how to write comprehensive database tests using pgTAP to secure your application against common vulnerabilities

*Canonical: https://makerkit.dev/docs/tanstack-supabase/development/database-tests*

---

Database testing is critical for ensuring your application's security and data integrity. This guide covers how to write comprehensive database tests using **pgTAP** and **Basejump utilities** to protect against common vulnerabilities.

## Why Database Testing Matters

Database tests verify that your **Row Level Security (RLS)** policies work correctly and protect against:

- **Unauthorized data access** - Users reading data they shouldn't see
- **Data modification attacks** - Users updating/deleting records they don't own
- **Privilege escalation** - Users gaining higher permissions than intended
- **Cross-account data leaks** - Team members accessing other teams' data
- **Storage security bypasses** - Unauthorized file access

## Test Infrastructure

### Required Extensions

Makerkit uses these extensions for database testing:

```sql
-- Install Basejump test helpers
create extension "basejump-supabase_test_helpers" version '0.0.6';

-- The extension provides authentication simulation
-- and user management utilities
```

### Makerkit Test Helpers

The `/apps/web/supabase/tests/database/00000-makerkit-helpers.sql` file provides essential utilities:

```sql
-- Authenticate as a test user
select makerkit.authenticate_as('user_identifier');

-- Get account by slug
select makerkit.get_account_by_slug('team-slug');

-- Get account ID by slug
select makerkit.get_account_id_by_slug('team-slug');

-- Set user as super admin
select makerkit.set_super_admin();

-- Set MFA level
select makerkit.set_session_aal('aal1'); -- or 'aal2'
```

## Test Structure

Every pgTAP test follows this structure:

```sql
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';

select no_plan(); -- or select plan(N) for exact test count

-- Test setup
select makerkit.set_identifier('user1', 'user1@example.com');
select makerkit.set_identifier('user2', 'user2@example.com');

-- Your tests here
select is(
  actual_result,
  expected_result,
  'Test description'
);

select * from finish();
rollback;
```

## Bypassing RLS in Tests

When you need to set up test data or verify data exists independently of RLS policies, use role switching:

### Role Types for Testing

```sql
-- postgres: Full superuser access, bypasses all RLS
set local role postgres;

-- service_role: Service-level access, bypasses RLS
set local role service_role;

-- authenticated: Normal user with RLS enforced (default for makerkit.authenticate_as)
-- No need to set this explicitly - makerkit.authenticate_as() handles it
```

### Common Patterns for Role Switching

#### Pattern 1: Setup Test Data
```sql
-- Use postgres role to insert test data that bypasses RLS
set local role postgres;
insert into accounts_memberships (account_id, user_id, account_role)
values (team_id, user_id, 'member');

-- Test as normal user (RLS enforced)
select makerkit.authenticate_as('member');
select isnt_empty($$ select * from team_data $$, 'Member can see team data');
```

#### Pattern 2: Verify Data Exists
```sql
-- Test that unauthorized user cannot see data
select makerkit.authenticate_as('unauthorized_user');
select is_empty($$ select * from private_data $$, 'Unauthorized user sees nothing');

-- Use postgres role to verify data actually exists
set local role postgres;
select isnt_empty($$ select * from private_data $$, 'Data exists (confirms RLS filtering)');
```

#### Pattern 3: Grant Permissions for Testing
```sql
-- Use postgres role to grant permissions
set local role postgres;
insert into role_permissions (role, permission)
values ('custom-role', 'invites.manage');

-- Test as user with the role
select makerkit.authenticate_as('custom_role_user');
select lives_ok($$ select create_invitation(...) $$, 'User with permission can invite');
```

### When to Use Each Role

#### Use `postgres` role when:
- Setting up complex test data with foreign key relationships
- Inserting data that would normally be restricted by RLS
- Verifying data exists independently of user permissions
- Modifying system tables (roles, permissions, etc.)

#### Use `service_role` when:
- You need RLS bypass but want to stay closer to application-level permissions
- Testing service-level operations
- Working with data that should be accessible to services but not users

#### Use `makerkit.authenticate_as()` when:
- Testing normal user operations (automatically sets `authenticated` role)
- Verifying RLS policies work correctly
- Testing user-specific access patterns

### Complete Test Example

```sql
begin;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select no_plan();

-- Setup test users
select makerkit.set_identifier('owner', 'owner@example.com');
select makerkit.set_identifier('member', 'member@example.com');
select makerkit.set_identifier('stranger', 'stranger@example.com');

-- Create team (as owner)
select makerkit.authenticate_as('owner');
select public.create_team_account('TestTeam');

-- Add member using postgres role (bypasses RLS)
set local role postgres;
insert into accounts_memberships (account_id, user_id, account_role)
values (
  (select id from accounts where slug = 'testteam'),
  tests.get_supabase_uid('member'),
  'member'
);

-- Test member access (RLS enforced)
select makerkit.authenticate_as('member');
select isnt_empty(
  $$ select * from accounts where slug = 'testteam' $$,
  'Member can see their team'
);

-- Test stranger cannot see team (RLS enforced)
select makerkit.authenticate_as('stranger');
select is_empty(
  $$ select * from accounts where slug = 'testteam' $$,
  'Stranger cannot see team due to RLS'
);

-- Verify team actually exists (bypass RLS)
set local role postgres;
select isnt_empty(
  $$ select * from accounts where slug = 'testteam' $$,
  'Team exists in database (confirms RLS is working, not missing data)'
);

select * from finish();
rollback;
```

### Key Principles

1. **Use `postgres` role for test setup**, then switch back to test actual user permissions
2. **Always verify data exists** using `postgres` role when testing that users cannot see data
3. **Never test application logic as `postgres`** - it bypasses all security
4. **Use role switching to confirm RLS is filtering**, not that data is missing

## Basic Security Testing Patterns

### 1. Testing Data Isolation

Verify users can only access their own data:

```sql
-- Create test users
select makerkit.set_identifier('owner', 'owner@example.com');
select tests.create_supabase_user('stranger', 'stranger@example.com');

-- Owner creates a record
select makerkit.authenticate_as('owner');
insert into notes (title, content, user_id)
values ('Secret Note', 'Private content', auth.uid());

-- Stranger cannot see the record
select makerkit.authenticate_as('stranger');
select is_empty(
  $$ select * from notes where title = 'Secret Note' $$,
  'Strangers cannot see other users notes'
);
```

### 2. Testing Write Protection

Ensure users cannot modify others' data:

```sql
-- Owner creates a record
select makerkit.authenticate_as('owner');
insert into posts (title, user_id)
values ('My Post', auth.uid()) returning id;

-- Store the post ID for testing
\set post_id (select id from posts where title = 'My Post')

-- Stranger cannot update the record
select makerkit.authenticate_as('stranger');
select throws_ok(
  $$ update posts set title = 'Hacked!' where id = :post_id $$,
  'update or delete on table "posts" violates row-level security policy',
  'Strangers cannot update other users posts'
);
```

### 3. Testing Permission Systems

Verify role-based access control:

```sql
-- Test that only users with 'posts.manage' permission can create posts
select makerkit.authenticate_as('member');

select throws_ok(
  $$ insert into admin_posts (title, content) values ('Test', 'Content') $$,
  'new row violates row-level security policy',
  'Members without permission cannot create admin posts'
);

-- Grant permission and test again
set local role postgres;
insert into user_permissions (user_id, permission)
values (tests.get_supabase_uid('member'), 'posts.manage');

select makerkit.authenticate_as('member');
select lives_ok(
  $$ insert into admin_posts (title, content) values ('Test', 'Content') $$,
  'Members with permission can create admin posts'
);
```

## Team Account Security Testing

### Testing Team Membership Access

```sql
-- Setup team and members
select makerkit.authenticate_as('owner');
select public.create_team_account('TestTeam');

-- Add member to team
set local role postgres;
insert into accounts_memberships (account_id, user_id, account_role)
values (
  makerkit.get_account_id_by_slug('testteam'),
  tests.get_supabase_uid('member'),
  'member'
);

-- Test member can see team data
select makerkit.authenticate_as('member');
select isnt_empty(
  $$ select * from team_posts where account_id = makerkit.get_account_id_by_slug('testteam') $$,
  'Team members can see team posts'
);

-- Test non-members cannot see team data
select makerkit.authenticate_as('stranger');
select is_empty(
  $$ select * from team_posts where account_id = makerkit.get_account_id_by_slug('testteam') $$,
  'Non-members cannot see team posts'
);
```

### Testing Role Hierarchy

```sql
-- Test that members cannot promote themselves
select makerkit.authenticate_as('member');

select throws_ok(
  $$ update accounts_memberships
     set account_role = 'owner'
     where user_id = auth.uid() $$,
  'Only the account_role can be updated',
  'Members cannot promote themselves to owner'
);

-- Test that members cannot remove the owner
select throws_ok(
  $$ delete from accounts_memberships
     where user_id = tests.get_supabase_uid('owner')
     and account_id = makerkit.get_account_id_by_slug('testteam') $$,
  'The primary account owner cannot be removed from the account membership list',
  'Members cannot remove the account owner'
);
```

## Storage Security Testing

```sql
-- Test file access control
select makerkit.authenticate_as('user1');

-- User can upload to their own folder
select lives_ok(
  $$ insert into storage.objects (bucket_id, name, owner, owner_id)
     values ('avatars', auth.uid()::text, auth.uid(), auth.uid()) $$,
  'Users can upload files with their own UUID as filename'
);

-- User cannot upload using another user's UUID as filename
select makerkit.authenticate_as('user2');
select throws_ok(
  $$ insert into storage.objects (bucket_id, name, owner, owner_id)
     values ('avatars', tests.get_supabase_uid('user1')::text, auth.uid(), auth.uid()) $$,
  'new row violates row-level security policy',
  'Users cannot upload files with other users UUIDs as filename'
);
```

## Common Testing Patterns

### 1. Cross-Account Data Isolation

```sql
-- Verify team A members cannot access team B data
select makerkit.authenticate_as('team_a_member');
insert into documents (title, team_id)
values ('Secret Doc', makerkit.get_account_id_by_slug('team-a'));

select makerkit.authenticate_as('team_b_member');
select is_empty(
  $$ select * from documents where title = 'Secret Doc' $$,
  'Team B members cannot see Team A documents'
);
```

### 2. Function Security Testing

```sql
-- Test that protected functions check permissions
select makerkit.authenticate_as('regular_user');

select throws_ok(
  $$ select admin_delete_all_posts() $$,
  'permission denied for function admin_delete_all_posts',
  'Regular users cannot call admin functions'
);

-- Test with proper permissions
select makerkit.set_super_admin();
select lives_ok(
  $$ select admin_delete_all_posts() $$,
  'Super admins can call admin functions'
);
```

### 3. Invitation Security Testing

```sql
-- Test invitation creation permissions
select makerkit.authenticate_as('member');

-- Members can invite to same or lower roles
select lives_ok(
  $$ insert into invitations (email, account_id, role, invite_token)
     values ('new@example.com', makerkit.get_account_id_by_slug('team'), 'member', gen_random_uuid()) $$,
  'Members can invite other members'
);

-- Members cannot invite to higher roles
select throws_ok(
  $$ insert into invitations (email, account_id, role, invite_token)
     values ('admin@example.com', makerkit.get_account_id_by_slug('team'), 'owner', gen_random_uuid()) $$,
  'new row violates row-level security policy',
  'Members cannot invite owners'
);
```

## Advanced Testing Techniques

### 1. Testing Edge Cases

```sql
-- Test NULL handling in RLS policies
select lives_ok(
  $$ select * from posts where user_id IS NULL $$,
  'Queries with NULL filters should not crash'
);

-- Test empty result sets
select is_empty(
  $$ select * from posts where user_id = '00000000-0000-0000-0000-000000000000'::uuid $$,
  'Invalid UUIDs should return empty results'
);
```

### 2. Performance Testing

```sql
-- Test that RLS policies don't create N+1 queries
select makerkit.authenticate_as('team_owner');

-- This should be efficient even with many team members
select isnt_empty(
  $$ select p.*, u.name from posts p join users u on p.user_id = u.id
     where p.team_id = makerkit.get_account_id_by_slug('large-team') $$,
  'Joined queries with RLS should perform well'
);
```

### 3. Testing Trigger Security

```sql
-- Test that triggers properly validate permissions
select makerkit.authenticate_as('regular_user');

select throws_ok(
  $$ update sensitive_settings set admin_only_field = 'hacked' $$,
  'You do not have permission to update this field',
  'Triggers should prevent unauthorized field updates'
);
```

## Best Practices

### 1. Always Test Both Positive and Negative Cases
- Verify authorized users CAN access data
- Verify unauthorized users CANNOT access data

### 2. Test All CRUD Operations
- CREATE: Can users insert the records they should?
- READ: Can users only see their authorized data?
- UPDATE: Can users only modify records they own?
- DELETE: Can users only remove their own records?

### 3. Use Descriptive Test Names
```sql
select is(
  actual_result,
  expected_result,
  'Team members should be able to read team posts but not modify other teams data'
);
```

### 4. Test Permission Boundaries
- Test the minimum permission level that grants access
- Test that one level below is denied
- Test that users with higher permissions can also access

### 5. Clean Up After Tests
Always use transactions that rollback:
```sql
begin;
-- Your tests here
rollback; -- This cleans up all test data
```

## Common Anti-Patterns to Avoid

❌ **Don't test only happy paths**
```sql
-- Bad: Only testing that authorized access works
select isnt_empty($$ select * from posts $$, 'User can see posts');
```

✅ **Test both authorized and unauthorized access**
```sql
-- Good: Test both positive and negative cases
select makerkit.authenticate_as('owner');
select isnt_empty($$ select * from posts where user_id = auth.uid() $$, 'Owner can see own posts');

select makerkit.authenticate_as('stranger');
select is_empty($$ select * from posts where user_id != auth.uid() $$, 'Stranger cannot see others posts');
```

❌ **Don't forget to test cross-account scenarios**
```sql
-- Bad: Only testing within same account
select lives_ok($$ insert into team_docs (title) values ('Doc') $$, 'Can create doc');
```

✅ **Test cross-account isolation**
```sql
-- Good: Test that team A cannot access team B data
select makerkit.authenticate_as('team_a_member');
insert into team_docs (title, team_id) values ('Secret', team_a_id);

select makerkit.authenticate_as('team_b_member');
select is_empty($$ select * from team_docs where title = 'Secret' $$, 'Team B cannot see Team A docs');
```

## Testing Silent RLS Failures

**Critical Understanding**: RLS policies often fail **silently**. They don't throw errors - they just filter out data or prevent operations. This makes testing RLS policies tricky because you need to verify what **didn't** happen, not just what did.

### Why RLS Failures Are Silent

```sql
-- RLS policies work by:
-- 1. INSERT/UPDATE: If the policy check fails, the operation is ignored (no error)
-- 2. SELECT: If the policy fails, rows are filtered out (no error)
-- 3. DELETE: If the policy fails, nothing is deleted (no error)
```

### Testing Silent SELECT Filtering

When RLS policies prevent users from seeing data, queries return empty results instead of errors:

```sql
-- Setup: Create posts for different users
select makerkit.authenticate_as('user_a');
insert into posts (title, content, user_id)
values ('User A Post', 'Content A', auth.uid());

select makerkit.authenticate_as('user_b');
insert into posts (title, content, user_id)
values ('User B Post', 'Content B', auth.uid());

-- Test: User A cannot see User B's posts (silent filtering)
select makerkit.authenticate_as('user_a');
select is_empty(
  $$ select * from posts where title = 'User B Post' $$,
  'User A cannot see User B posts due to RLS filtering'
);

-- Test: User A can still see their own posts
select isnt_empty(
  $$ select * from posts where title = 'User A Post' $$,
  'User A can see their own posts'
);

-- Critical: Verify the post actually exists by switching context
select makerkit.authenticate_as('user_b');
select isnt_empty(
  $$ select * from posts where title = 'User B Post' $$,
  'User B post actually exists (not a test data issue)'
);
```

### Testing Silent UPDATE/DELETE Prevention

RLS policies can silently prevent modifications without throwing errors:

```sql
-- Setup: User A creates a post
select makerkit.authenticate_as('user_a');
insert into posts (title, content, user_id)
values ('Original Title', 'Original Content', auth.uid())
returning id;

-- Store the post ID for testing
\set post_id (select id from posts where title = 'Original Title')

-- Test: User B attempts to modify User A's post (silently fails)
select makerkit.authenticate_as('user_b');
update posts set title = 'Hacked Title' where id = :post_id;

-- Verify the update was silently ignored
select makerkit.authenticate_as('user_a');
select is(
  (select title from posts where id = :post_id),
  'Original Title',
  'User B update attempt was silently ignored by RLS'
);

-- Test: User B attempts to delete User A's post (silently fails)
select makerkit.authenticate_as('user_b');
delete from posts where id = :post_id;

-- Verify the delete was silently ignored
select makerkit.authenticate_as('user_a');
select isnt_empty(
  $$ select * from posts where title = 'Original Title' $$,
  'User B delete attempt was silently ignored by RLS'
);
```

### Testing Silent INSERT Prevention

INSERT operations can also fail silently with restrictive RLS policies:

```sql
-- Test: Non-admin tries to insert into admin_settings table
select makerkit.authenticate_as('regular_user');

-- Attempt to insert (may succeed but be silently filtered on read)
insert into admin_settings (key, value) values ('test_key', 'test_value');

-- Critical: Don't just check for errors - verify the data isn't there
select is_empty(
  $$ select * from admin_settings where key = 'test_key' $$,
  'Regular user cannot insert admin settings (silent prevention)'
);

-- Verify an admin can actually insert this data
set local role postgres;
insert into admin_settings (key, value) values ('admin_key', 'admin_value');

select makerkit.set_super_admin();
select isnt_empty(
  $$ select * from admin_settings where key = 'admin_key' $$,
  'Admins can insert admin settings (confirms table works)'
);
```

### Testing Row-Level Filtering with Counts

Use count comparisons to detect silent filtering:

```sql
-- Setup: Create team data
select makerkit.authenticate_as('team_owner');
insert into team_documents (title, team_id) values
  ('Doc 1', (select id from accounts where slug = 'team-a')),
  ('Doc 2', (select id from accounts where slug = 'team-a')),
  ('Doc 3', (select id from accounts where slug = 'team-a'));

-- Test: Team member sees all team docs
select makerkit.authenticate_as('team_member_a');
select is(
  (select count(*) from team_documents where team_id = (select id from accounts where slug = 'team-a')),
  3::bigint,
  'Team member can see all team documents'
);

-- Test: Non-member sees no team docs (silent filtering)
select makerkit.authenticate_as('external_user');
select is(
  (select count(*) from team_documents where team_id = (select id from accounts where slug = 'team-a')),
  0::bigint,
  'External user cannot see any team documents due to RLS filtering'
);
```

### Testing Partial Data Exposure

Sometimes RLS policies expose some fields but not others:

```sql
-- Test: Public can see user profiles but not sensitive data
select tests.create_supabase_user('public_user', 'public@example.com');

-- Create user profile with sensitive data
select makerkit.authenticate_as('profile_owner');
insert into user_profiles (user_id, name, email, phone, ssn) values
  (auth.uid(), 'John Doe', 'john@example.com', '555-1234', '123-45-6789');

-- Test: Public can see basic info but not sensitive fields
select makerkit.authenticate_as('public_user');

select is(
  (select name from user_profiles where user_id = tests.get_supabase_uid('profile_owner')),
  'John Doe',
  'Public can see user name'
);

-- Critical: Test that sensitive fields are silently filtered
select is(
  (select ssn from user_profiles where user_id = tests.get_supabase_uid('profile_owner')),
  null,
  'Public cannot see SSN (silently filtered by RLS)'
);

select is(
  (select phone from user_profiles where user_id = tests.get_supabase_uid('profile_owner')),
  null,
  'Public cannot see phone number (silently filtered by RLS)'
);
```

### Testing Cross-Account Data Isolation

Verify users cannot access other accounts' data:

```sql
-- Setup: Create data for multiple teams
select makerkit.authenticate_as('team_a_owner');
insert into billing_info (team_id, subscription_id) values
  ((select id from accounts where slug = 'team-a'), 'sub_123');

select makerkit.authenticate_as('team_b_owner');
insert into billing_info (team_id, subscription_id) values
  ((select id from accounts where slug = 'team-b'), 'sub_456');

-- Test: Team A members cannot see Team B billing (silent filtering)
select makerkit.authenticate_as('team_a_member');
select is_empty(
  $$ select * from billing_info where subscription_id = 'sub_456' $$,
  'Team A members cannot see Team B billing info'
);

-- Test: Team A members can see their own billing
select isnt_empty(
  $$ select * from billing_info where subscription_id = 'sub_123' $$,
  'Team A members can see their own billing info'
);

-- Verify both billing records actually exist
set local role postgres;
select is(
  (select count(*) from billing_info),
  2::bigint,
  'Both billing records exist in database (not a test data issue)'
);
```

### Testing Permission Boundary Edge Cases

Test the exact boundaries where permissions change:

```sql
-- Setup users with different permission levels
select makerkit.authenticate_as('admin_user');
select makerkit.authenticate_as('editor_user');
select makerkit.authenticate_as('viewer_user');

-- Test: Admins can see all data
select makerkit.authenticate_as('admin_user');
select isnt_empty(
  $$ select * from sensitive_documents $$,
  'Admins can see sensitive documents'
);

-- Test: Editors cannot see sensitive docs (silent filtering)
select makerkit.authenticate_as('editor_user');
select is_empty(
  $$ select * from sensitive_documents $$,
  'Editors cannot see sensitive documents due to RLS'
);

-- Test: Viewers cannot see sensitive docs (silent filtering)
select makerkit.authenticate_as('viewer_user');
select is_empty(
  $$ select * from sensitive_documents $$,
  'Viewers cannot see sensitive documents due to RLS'
);
```

### Testing Multi-Condition RLS Policies

When RLS policies have multiple conditions, test each condition:

```sql
-- Policy example: Users can only see posts if they are:
-- 1. The author, OR
-- 2. A team member of the author's team, AND
-- 3. The post is published

-- Test condition 1: Author can see unpublished posts
select makerkit.authenticate_as('author');
insert into posts (title, published, user_id) values
  ('Draft Post', false, auth.uid());

select isnt_empty(
  $$ select * from posts where title = 'Draft Post' $$,
  'Authors can see their own unpublished posts'
);

-- Test condition 2: Team members cannot see unpublished posts (silent filtering)
select makerkit.authenticate_as('team_member');
select is_empty(
  $$ select * from posts where title = 'Draft Post' $$,
  'Team members cannot see unpublished posts from teammates'
);

-- Test condition 3: Team members can see published posts
select makerkit.authenticate_as('author');
update posts set published = true where title = 'Draft Post';

select makerkit.authenticate_as('team_member');
select isnt_empty(
  $$ select * from posts where title = 'Draft Post' $$,
  'Team members can see published posts from teammates'
);

-- Test condition boundary: Non-team members cannot see any posts
select makerkit.authenticate_as('external_user');
select is_empty(
  $$ select * from posts where title = 'Draft Post' $$,
  'External users cannot see any posts (even published ones)'
);
```

### Common Silent Failure Patterns to Test

#### 1. The "Empty Result" Pattern
```sql
-- Always test that restricted queries return empty results, not errors
select is_empty(
  $$ select * from restricted_table where condition = true $$,
  'Unauthorized users see empty results, not errors'
);
```

#### 2. The "No-Effect" Pattern
```sql
-- Test that unauthorized modifications have no effect
update restricted_table set field = 'hacked' where id = target_id;
select is(
  (select field from restricted_table where id = target_id),
  'original_value',
  'Unauthorized updates are silently ignored'
);
```

#### 3. The "Partial Visibility" Pattern
```sql
-- Test that only authorized fields are visible
select is(
  (select public_field from mixed_table where id = target_id),
  'visible_value',
  'Public fields are visible'
);

select is(
  (select private_field from mixed_table where id = target_id),
  null,
  'Private fields are silently filtered out'
);
```

#### 4. The "Context Switch" Verification Pattern
```sql
-- Always verify data exists by switching to authorized context
select makerkit.authenticate_as('unauthorized_user');
select is_empty(
  $$ select * from protected_data $$,
  'Unauthorized user sees no data'
);

-- Switch to authorized user to prove data exists
select makerkit.authenticate_as('authorized_user');
select isnt_empty(
  $$ select * from protected_data $$,
  'Data actually exists (confirms RLS filtering, not missing data)'
);
```

### Best Practices for Silent Failure Testing

#### ✅ Do: Test Both Positive and Negative Cases
```sql
-- Test that authorized users CAN access data
select makerkit.authenticate_as('authorized_user');
select isnt_empty($$ select * from protected_data $$, 'Authorized access works');

-- Test that unauthorized users CANNOT access data (silent filtering)
select makerkit.authenticate_as('unauthorized_user');
select is_empty($$ select * from protected_data $$, 'Unauthorized access silently filtered');
```

#### ✅ Do: Verify Data Exists in Different Context
```sql
-- Don't just test that unauthorized users see nothing
-- Verify the data actually exists by checking as an authorized user
select makerkit.authenticate_as('data_owner');
select isnt_empty($$ select * from my_data $$, 'Data exists');

select makerkit.authenticate_as('unauthorized_user');
select is_empty($$ select * from my_data $$, 'But unauthorized user cannot see it');
```

#### ✅ Do: Test Modification Boundaries
```sql
-- Test that unauthorized modifications are ignored
update sensitive_table set value = 'hacked';
select is(
  (select value from sensitive_table),
  'original_value',
  'Unauthorized updates silently ignored'
);
```

#### ❌ Don't: Expect Errors from RLS Violations
```sql
-- Bad: RLS violations usually don't throw errors
select throws_ok(
  $$ select * from protected_data $$,
  'permission denied'
);

-- Good: RLS violations return empty results
select is_empty(
  $$ select * from protected_data $$,
  'Unauthorized users see no data due to RLS filtering'
);
```

#### ❌ Don't: Test Only Happy Paths
```sql
-- Bad: Only testing authorized access
select isnt_empty($$ select * from my_data $$, 'I can see my data');

-- Good: Test both authorized and unauthorized access
select makerkit.authenticate_as('owner');
select isnt_empty($$ select * from my_data $$, 'Owner can see data');

select makerkit.authenticate_as('stranger');
select is_empty($$ select * from my_data $$, 'Stranger cannot see data');
```

Remember: **RLS is designed to be invisible to attackers**. Your tests must verify this invisibility by checking for empty results and unchanged data, not for error messages.

## Running Tests

To run your database tests:

```bash
# Start Supabase locally
pnpm supabase:web:start

# Run all database tests
pnpm supabase:web:test

# Run specific test file
pnpm supabase test ./tests/database/your-test.test.sql
```

Your tests will help ensure your application is secure against common database vulnerabilities and that your RLS policies work as expected.
