• Blog
  • Documentation
  • Courses
  • Changelog
  • AI Starters
  • UI Kit
  • FAQ
  • Supamode
    New
  • Pricing

Launch your next SaaS in record time with Makerkit, a React SaaS Boilerplate for Next.js and Supabase.

Makerkit is a product of Makerkit Pte Ltd (registered in the Republic of Singapore)Company Registration No: 202407149CFor support or inquiries, please contact us

About
  • FAQ
  • Contact
  • Verify your Discord
  • Consultation
  • Open Source
  • Become an Affiliate
Product
  • Documentation
  • Blog
  • Changelog
  • UI Blocks
  • Figma UI Kit
  • AI SaaS Starters
License
  • Activate License
  • Upgrade License
  • Invite Member
Legal
  • Terms of License
    • Getting Started with Development
    • Database Architecture
    • Migrations
    • Extending the DB Schema
    • Database Functions
    • Loading data from the DB
    • Writing data to Database
    • Database Webhooks
    • RBAC: Roles and Permissions
    • Marketing Pages
    • Legal Pages
    • External Marketing Website
    • Application tests (E2E)
    • SEO
    • Database tests
    • Adding a Turborepo app
    • Adding a Turborepo package

Database Testing with pgTAP

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

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.

On this page
  1. Why Database Testing Matters
    1. Test Infrastructure
      1. Required Extensions
      2. Makerkit Test Helpers
    2. Test Structure
      1. Bypassing RLS in Tests
        1. Role Types for Testing
        2. Common Patterns for Role Switching
        3. When to Use Each Role
        4. Complete Test Example
        5. Key Principles
      2. Basic Security Testing Patterns
        1. 1. Testing Data Isolation
        2. 2. Testing Write Protection
        3. 3. Testing Permission Systems
      3. Team Account Security Testing
        1. Testing Team Membership Access
        2. Testing Role Hierarchy
      4. Storage Security Testing
        1. Common Testing Patterns
          1. 1. Cross-Account Data Isolation
          2. 2. Function Security Testing
          3. 3. Invitation Security Testing
        2. Advanced Testing Techniques
          1. 1. Testing Edge Cases
          2. 2. Performance Testing
          3. 3. Testing Trigger Security
        3. Best Practices
          1. 1. Always Test Both Positive and Negative Cases
          2. 2. Test All CRUD Operations
          3. 3. Use Descriptive Test Names
          4. 4. Test Permission Boundaries
          5. 5. Clean Up After Tests
        4. Common Anti-Patterns to Avoid
          1. Testing Silent RLS Failures
            1. Why RLS Failures Are Silent
            2. Testing Silent SELECT Filtering
            3. Testing Silent UPDATE/DELETE Prevention
            4. Testing Silent INSERT Prevention
            5. Testing Row-Level Filtering with Counts
            6. Testing Partial Data Exposure
            7. Testing Cross-Account Data Isolation
            8. Testing Permission Boundary Edge Cases
            9. Testing Multi-Condition RLS Policies
            10. Common Silent Failure Patterns to Test
            11. Best Practices for Silent Failure Testing
          2. Running Tests