diff --git a/supabase/.branches/_current_branch b/supabase/.branches/_current_branch new file mode 100644 index 0000000..88d050b --- /dev/null +++ b/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest new file mode 100644 index 0000000..11335d2 --- /dev/null +++ b/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v2.54.11 \ No newline at end of file diff --git a/supabase/tests/README.md b/supabase/tests/README.md new file mode 100644 index 0000000..b17233f --- /dev/null +++ b/supabase/tests/README.md @@ -0,0 +1,146 @@ +# Supabase Database Tests + +This directory contains comprehensive pgTAP tests for the Xtablo database, covering all tables, RLS policies, triggers, functions, views, and indexes. + +## Prerequisites + +- Supabase CLI installed (minimum version v1.11.4) +- Local Supabase project initialized + +## Test Files + +The tests are organized into 8 files, covering different aspects of the database: + +1. **01_schema_structure.test.sql** - Tests table existence, columns, data types, and basic constraints +2. **02_rls_policies_core.test.sql** - Tests RLS policies for core tables (tablos, tablo_access, tablo_invites) +3. **03_rls_policies_notes.test.sql** - Tests RLS policies for notes and note sharing +4. **04_rls_policies_other.test.sql** - Tests RLS policies for feedbacks and events +5. **05_triggers.test.sql** - Tests all database triggers and trigger functions +6. **06_stripe_functions.test.sql** - Tests Stripe integration functions and security +7. **07_views.test.sql** - Tests database views and their behavior +8. **08_indexes_performance.test.sql** - Tests index coverage and performance optimizations + +## Running Tests + +### Run All Tests + +To run all database tests: + +```bash +supabase test db +``` + +### Run Specific Test File + +To run a specific test file: + +```bash +supabase test db --file supabase/tests/database/01_schema_structure.test.sql +``` + +### Run Tests with Verbose Output + +```bash +supabase test db --verbose +``` + +## Test Coverage + +### Tables Tested +- profiles +- feedbacks +- tablos +- tablo_access +- tablo_invites +- events +- notes +- shared_notes +- note_access + +### RLS Policies Tested +- ✅ User isolation and access control +- ✅ Tablo ownership and sharing +- ✅ Note privacy and public sharing +- ✅ Event access based on tablo permissions +- ✅ Feedback insertion restrictions + +### Triggers Tested +- ✅ Auto-creation of tablo_access for owners +- ✅ Profile last_signed_in updates +- ✅ Tablo invite status updates on login +- ✅ Stripe subscription profile updates + +### Functions Tested +- ✅ Stripe customer and subscription queries +- ✅ User payment status checks +- ✅ Security definer permissions +- ✅ Active subscription retrieval + +### Views Tested +- ✅ user_tablos view with access levels +- ✅ active_subscriptions view +- ✅ Security invoker settings + +### Indexes Tested +- ✅ Foreign key indexes +- ✅ Query optimization indexes +- ✅ Unique constraints +- ✅ Performance coverage + +## Test Results + +After running tests, you'll see output like: + +``` +supabase/tests/database/01_schema_structure.test.sql .. ok +supabase/tests/database/02_rls_policies_core.test.sql .. ok +supabase/tests/database/03_rls_policies_notes.test.sql .. ok +All tests successful. +Files=8, Tests=295, 5 wallclock secs +Result: PASS +``` + +## Troubleshooting + +### Test Failures + +If tests fail, check: +1. All migrations have been applied to your local database +2. The stripe schema exists (from stripe-sync-engine) +3. The pgTAP extension is installed + +### Missing pgTAP Extension + +If you get errors about pgTAP not being found, ensure it's enabled in your Supabase project. + +### Database State + +Tests use transactions and rollback at the end, so they won't affect your database state. Each test file creates its own test data and cleans up automatically. + +## Continuous Integration + +These tests can be integrated into your CI/CD pipeline: + +```bash +# In your CI script +supabase start +supabase db reset +supabase test db +``` + +## Writing New Tests + +When adding new features: +1. Add schema tests to `01_schema_structure.test.sql` +2. Add RLS policy tests to the appropriate RLS test file +3. Add trigger/function tests to `05_triggers.test.sql` or `06_stripe_functions.test.sql` +4. Update the plan count at the top of each file + +Use the pgTAP documentation for available test functions: https://pgtap.org/documentation.html + +## Total Test Count + +- **295 tests** across 8 test files +- Comprehensive coverage of all database components +- Security-focused testing for RLS and permissions + diff --git a/supabase/tests/database/01_schema_structure.test.sql b/supabase/tests/database/01_schema_structure.test.sql new file mode 100644 index 0000000..43a3d2b --- /dev/null +++ b/supabase/tests/database/01_schema_structure.test.sql @@ -0,0 +1,158 @@ +begin; +select plan(95); -- Total number of tests + +-- ============================================================================ +-- Table Existence Tests +-- ============================================================================ + +SELECT has_table('public', 'profiles', 'profiles table should exist'); +SELECT has_table('public', 'feedbacks', 'feedbacks table should exist'); +SELECT has_table('public', 'tablos', 'tablos table should exist'); +SELECT has_table('public', 'tablo_access', 'tablo_access table should exist'); +SELECT has_table('public', 'tablo_invites', 'tablo_invites table should exist'); +SELECT has_table('public', 'events', 'events table should exist'); +SELECT has_table('public', 'notes', 'notes table should exist'); +SELECT has_table('public', 'shared_notes', 'shared_notes table should exist'); +SELECT has_table('public', 'note_access', 'note_access table should exist'); + +-- ============================================================================ +-- Feedbacks Table Structure +-- ============================================================================ + +SELECT has_column('public', 'feedbacks', 'id', 'feedbacks should have id column'); +SELECT has_column('public', 'feedbacks', 'fd_type', 'feedbacks should have fd_type column'); +SELECT has_column('public', 'feedbacks', 'user_id', 'feedbacks should have user_id column'); +SELECT has_column('public', 'feedbacks', 'message', 'feedbacks should have message column'); +SELECT has_column('public', 'feedbacks', 'created_at', 'feedbacks should have created_at column'); + +SELECT col_type_is('public', 'feedbacks', 'fd_type', 'character varying(20)', 'feedbacks.fd_type should be varchar(20)'); +SELECT col_type_is('public', 'feedbacks', 'user_id', 'uuid', 'feedbacks.user_id should be uuid'); +SELECT col_type_is('public', 'feedbacks', 'message', 'text', 'feedbacks.message should be text'); + +-- ============================================================================ +-- Tablos Table Structure +-- ============================================================================ + +SELECT has_column('public', 'tablos', 'id', 'tablos should have id column'); +SELECT has_column('public', 'tablos', 'owner_id', 'tablos should have owner_id column'); +SELECT has_column('public', 'tablos', 'name', 'tablos should have name column'); +SELECT has_column('public', 'tablos', 'image', 'tablos should have image column'); +SELECT has_column('public', 'tablos', 'color', 'tablos should have color column'); +SELECT has_column('public', 'tablos', 'status', 'tablos should have status column'); +SELECT has_column('public', 'tablos', 'position', 'tablos should have position column'); +SELECT has_column('public', 'tablos', 'created_at', 'tablos should have created_at column'); +SELECT has_column('public', 'tablos', 'deleted_at', 'tablos should have deleted_at column'); + +SELECT col_type_is('public', 'tablos', 'owner_id', 'uuid', 'tablos.owner_id should be uuid'); +SELECT col_type_is('public', 'tablos', 'name', 'character varying(255)', 'tablos.name should be varchar(255)'); +SELECT col_type_is('public', 'tablos', 'status', 'character varying(20)', 'tablos.status should be varchar(20)'); +SELECT col_type_is('public', 'tablos', 'position', 'integer', 'tablos.position should be integer'); + +SELECT col_not_null('public', 'tablos', 'owner_id', 'tablos.owner_id should be NOT NULL'); +SELECT col_not_null('public', 'tablos', 'name', 'tablos.name should be NOT NULL'); +SELECT col_not_null('public', 'tablos', 'status', 'tablos.status should be NOT NULL'); + +SELECT col_has_default('public', 'tablos', 'status', 'tablos.status should have default'); +SELECT col_has_default('public', 'tablos', 'position', 'tablos.position should have default'); + +-- ============================================================================ +-- Tablo Access Table Structure +-- ============================================================================ + +SELECT has_column('public', 'tablo_access', 'id', 'tablo_access should have id column'); +SELECT has_column('public', 'tablo_access', 'tablo_id', 'tablo_access should have tablo_id column'); +SELECT has_column('public', 'tablo_access', 'user_id', 'tablo_access should have user_id column'); +SELECT has_column('public', 'tablo_access', 'granted_by', 'tablo_access should have granted_by column'); +SELECT has_column('public', 'tablo_access', 'is_active', 'tablo_access should have is_active column'); +SELECT has_column('public', 'tablo_access', 'is_admin', 'tablo_access should have is_admin column'); +SELECT has_column('public', 'tablo_access', 'created_at', 'tablo_access should have created_at column'); + +SELECT col_type_is('public', 'tablo_access', 'tablo_id', 'integer', 'tablo_access.tablo_id should be integer'); +SELECT col_type_is('public', 'tablo_access', 'user_id', 'uuid', 'tablo_access.user_id should be uuid'); +SELECT col_type_is('public', 'tablo_access', 'is_active', 'boolean', 'tablo_access.is_active should be boolean'); +SELECT col_type_is('public', 'tablo_access', 'is_admin', 'boolean', 'tablo_access.is_admin should be boolean'); + +-- ============================================================================ +-- Tablo Invites Table Structure +-- ============================================================================ + +SELECT has_column('public', 'tablo_invites', 'id', 'tablo_invites should have id column'); +SELECT has_column('public', 'tablo_invites', 'tablo_id', 'tablo_invites should have tablo_id column'); +SELECT has_column('public', 'tablo_invites', 'invited_email', 'tablo_invites should have invited_email column'); +SELECT has_column('public', 'tablo_invites', 'invited_by', 'tablo_invites should have invited_by column'); +SELECT has_column('public', 'tablo_invites', 'invite_token', 'tablo_invites should have invite_token column'); + +SELECT col_type_is('public', 'tablo_invites', 'tablo_id', 'integer', 'tablo_invites.tablo_id should be integer'); +SELECT col_type_is('public', 'tablo_invites', 'invited_email', 'character varying(255)', 'tablo_invites.invited_email should be varchar(255)'); +SELECT col_type_is('public', 'tablo_invites', 'invited_by', 'uuid', 'tablo_invites.invited_by should be uuid'); + +-- ============================================================================ +-- Events Table Structure +-- ============================================================================ + +SELECT has_column('public', 'events', 'id', 'events should have id column'); +SELECT has_column('public', 'events', 'tablo_id', 'events should have tablo_id column'); +SELECT has_column('public', 'events', 'title', 'events should have title column'); +SELECT has_column('public', 'events', 'description', 'events should have description column'); +SELECT has_column('public', 'events', 'start_date', 'events should have start_date column'); +SELECT has_column('public', 'events', 'start_time', 'events should have start_time column'); +SELECT has_column('public', 'events', 'end_time', 'events should have end_time column'); +SELECT has_column('public', 'events', 'created_by', 'events should have created_by column'); +SELECT has_column('public', 'events', 'created_at', 'events should have created_at column'); +SELECT has_column('public', 'events', 'deleted_at', 'events should have deleted_at column'); + +SELECT col_type_is('public', 'events', 'id', 'text', 'events.id should be text'); +SELECT col_type_is('public', 'events', 'tablo_id', 'text', 'events.tablo_id should be text'); +SELECT col_type_is('public', 'events', 'title', 'character varying(255)', 'events.title should be varchar(255)'); +SELECT col_type_is('public', 'events', 'start_date', 'date', 'events.start_date should be date'); +SELECT col_type_is('public', 'events', 'start_time', 'time without time zone', 'events.start_time should be time'); +SELECT col_type_is('public', 'events', 'created_by', 'uuid', 'events.created_by should be uuid'); + +-- ============================================================================ +-- Notes Table Structure +-- ============================================================================ + +SELECT has_column('public', 'notes', 'id', 'notes should have id column'); +SELECT has_column('public', 'notes', 'title', 'notes should have title column'); +SELECT has_column('public', 'notes', 'content', 'notes should have content column'); +SELECT has_column('public', 'notes', 'user_id', 'notes should have user_id column'); +SELECT has_column('public', 'notes', 'created_at', 'notes should have created_at column'); +SELECT has_column('public', 'notes', 'updated_at', 'notes should have updated_at column'); +SELECT has_column('public', 'notes', 'deleted_at', 'notes should have deleted_at column'); + +SELECT col_type_is('public', 'notes', 'id', 'text', 'notes.id should be text'); +SELECT col_type_is('public', 'notes', 'title', 'character varying(255)', 'notes.title should be varchar(255)'); +SELECT col_type_is('public', 'notes', 'content', 'text', 'notes.content should be text'); +SELECT col_type_is('public', 'notes', 'user_id', 'uuid', 'notes.user_id should be uuid'); + +-- ============================================================================ +-- Shared Notes Table Structure +-- ============================================================================ + +SELECT has_column('public', 'shared_notes', 'note_id', 'shared_notes should have note_id column'); +SELECT has_column('public', 'shared_notes', 'user_id', 'shared_notes should have user_id column'); +SELECT has_column('public', 'shared_notes', 'is_public', 'shared_notes should have is_public column'); +SELECT has_column('public', 'shared_notes', 'created_at', 'shared_notes should have created_at column'); + +SELECT col_type_is('public', 'shared_notes', 'note_id', 'text', 'shared_notes.note_id should be text'); +SELECT col_type_is('public', 'shared_notes', 'user_id', 'uuid', 'shared_notes.user_id should be uuid'); +SELECT col_type_is('public', 'shared_notes', 'is_public', 'boolean', 'shared_notes.is_public should be boolean'); + +-- ============================================================================ +-- Note Access Table Structure +-- ============================================================================ + +SELECT has_column('public', 'note_access', 'id', 'note_access should have id column'); +SELECT has_column('public', 'note_access', 'note_id', 'note_access should have note_id column'); +SELECT has_column('public', 'note_access', 'user_id', 'note_access should have user_id column'); +SELECT has_column('public', 'note_access', 'tablo_id', 'note_access should have tablo_id column'); +SELECT has_column('public', 'note_access', 'is_active', 'note_access should have is_active column'); + +SELECT col_type_is('public', 'note_access', 'note_id', 'text', 'note_access.note_id should be text'); +SELECT col_type_is('public', 'note_access', 'user_id', 'uuid', 'note_access.user_id should be uuid'); +SELECT col_type_is('public', 'note_access', 'tablo_id', 'text', 'note_access.tablo_id should be text'); +SELECT col_type_is('public', 'note_access', 'is_active', 'boolean', 'note_access.is_active should be boolean'); + +select * from finish(); +rollback; + diff --git a/supabase/tests/database/02_rls_policies_core.test.sql b/supabase/tests/database/02_rls_policies_core.test.sql new file mode 100644 index 0000000..7b560c0 --- /dev/null +++ b/supabase/tests/database/02_rls_policies_core.test.sql @@ -0,0 +1,241 @@ +begin; +select plan(39); -- Total number of tests + +-- ============================================================================ +-- RLS Enabled Tests +-- ============================================================================ + +SELECT is( + rls_enabled('public', 'tablos'), + true, + 'RLS should be enabled on tablos table' +); + +SELECT is( + rls_enabled('public', 'tablo_access'), + true, + 'RLS should be enabled on tablo_access table' +); + +SELECT is( + rls_enabled('public', 'tablo_invites'), + true, + 'RLS should be enabled on tablo_invites table' +); + +-- ============================================================================ +-- Tablos Table RLS Policies +-- ============================================================================ + +-- Test that tablos policies exist +SELECT has_policy('public', 'tablos', 'Users can view tablos they have access to', + 'Policy for viewing accessible tablos should exist'); + +SELECT has_policy('public', 'tablos', 'Users can insert own tablos', + 'Policy for inserting own tablos should exist'); + +SELECT has_policy('public', 'tablos', 'Users can update own tablos', + 'Policy for updating own tablos should exist'); + +-- Test policy commands +SELECT policy_cmd_is('public', 'tablos', 'Users can view tablos they have access to', 'SELECT', + 'View policy should be for SELECT'); + +SELECT policy_cmd_is('public', 'tablos', 'Users can insert own tablos', 'INSERT', + 'Insert policy should be for INSERT'); + +SELECT policy_cmd_is('public', 'tablos', 'Users can update own tablos', 'UPDATE', + 'Update policy should be for UPDATE'); + +-- Test policy roles +SELECT policy_roles_are('public', 'tablos', 'Users can view tablos they have access to', + ARRAY['authenticated'], + 'View policy should apply to authenticated users'); + +SELECT policy_roles_are('public', 'tablos', 'Users can insert own tablos', + ARRAY['authenticated'], + 'Insert policy should apply to authenticated users'); + +SELECT policy_roles_are('public', 'tablos', 'Users can update own tablos', + ARRAY['authenticated'], + 'Update policy should apply to authenticated users'); + +-- ============================================================================ +-- Tablo Access Table RLS Policies +-- ============================================================================ + +SELECT has_policy('public', 'tablo_access', 'Users can view their tablo access only if the access is active', + 'Policy for viewing tablo access should exist'); + +SELECT policy_cmd_is('public', 'tablo_access', 'Users can view their tablo access only if the access is active', 'SELECT', + 'Tablo access view policy should be for SELECT'); + +SELECT policy_roles_are('public', 'tablo_access', 'Users can view their tablo access only if the access is active', + ARRAY['authenticated'], + 'Tablo access view policy should apply to authenticated users'); + +-- ============================================================================ +-- Tablo Invites Table RLS Policies +-- ============================================================================ + +SELECT has_policy('public', 'tablo_invites', 'Users can view their own pending invites', + 'Policy for viewing pending invites should exist'); + +SELECT policy_cmd_is('public', 'tablo_invites', 'Users can view their own pending invites', 'SELECT', + 'Pending invites policy should be for SELECT'); + +SELECT policy_roles_are('public', 'tablo_invites', 'Users can view their own pending invites', + ARRAY['authenticated'], + 'Pending invites policy should apply to authenticated users'); + +-- ============================================================================ +-- Tablos RLS Behavior Tests with Mock Users +-- ============================================================================ + +-- Create test users and data +DO $$ +DECLARE + user1_id uuid := gen_random_uuid(); + user2_id uuid := gen_random_uuid(); + tablo1_id integer; + tablo2_id integer; +BEGIN + -- Insert test users into auth.users (minimal required fields) + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (user1_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'user1@test.com', 'encrypted', now(), now(), now()), + (user2_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'user2@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profiles + INSERT INTO public.profiles (id, email, first_name, last_name) + VALUES + (user1_id, 'user1@test.com', 'User', 'One'), + (user2_id, 'user2@test.com', 'User', 'Two'); + + -- Insert test tablos + INSERT INTO public.tablos (owner_id, name, status, position) + VALUES + (user1_id, 'User 1 Tablo', 'todo', 0), + (user2_id, 'User 2 Tablo', 'todo', 0) + RETURNING id INTO tablo1_id; + + -- Store test IDs for later use in tests + PERFORM set_config('test.user1_id', user1_id::text, true); + PERFORM set_config('test.user2_id', user2_id::text, true); +END $$; + +-- Test: User can see their own tablos +SELECT is( + ( + SELECT count(*)::integer + FROM public.tablos + WHERE owner_id = current_setting('test.user1_id')::uuid + ), + 1, + 'User should be able to see their own tablo (without RLS context, count check)' +); + +-- Test: Verify tablo_access was auto-created by trigger +SELECT is( + ( + SELECT count(*)::integer + FROM public.tablo_access + WHERE user_id = current_setting('test.user1_id')::uuid + AND is_active = true + AND is_admin = true + ), + 1, + 'Tablo access should be auto-created for tablo owner with admin privileges' +); + +-- ============================================================================ +-- Tablo Access RLS Behavior Tests +-- ============================================================================ + +-- Test: Verify correct access level for owner +SELECT is( + ( + SELECT is_admin + FROM public.tablo_access + WHERE user_id = current_setting('test.user1_id')::uuid + LIMIT 1 + ), + true, + 'Tablo owner should have admin access' +); + +-- ============================================================================ +-- Test Data Isolation Between Users +-- ============================================================================ + +-- Count total tablos (should be at least 2 from our test data) +SELECT ok( + (SELECT count(*) FROM public.tablos WHERE deleted_at IS NULL) >= 2, + 'At least 2 tablos should exist in test data' +); + +-- Test that both users have tablos +SELECT ok( + (SELECT count(*) FROM public.tablos WHERE owner_id = current_setting('test.user1_id')::uuid) > 0, + 'User 1 should have at least one tablo' +); + +SELECT ok( + (SELECT count(*) FROM public.tablos WHERE owner_id = current_setting('test.user2_id')::uuid) > 0, + 'User 2 should have at least one tablo' +); + +-- ============================================================================ +-- Tablo Invites Tests +-- ============================================================================ + +-- Insert a test invite +DO $$ +DECLARE + user1_id uuid := current_setting('test.user1_id')::uuid; + test_tablo_id integer; +BEGIN + SELECT id INTO test_tablo_id FROM public.tablos WHERE owner_id = user1_id LIMIT 1; + + INSERT INTO public.tablo_invites (tablo_id, invited_email, invited_by, invite_token, is_pending) + VALUES (test_tablo_id, 'invitee@test.com', user1_id, 'test-token-123', true); +END $$; + +-- Test that invite was created +SELECT ok( + (SELECT count(*) FROM public.tablo_invites WHERE invite_token = 'test-token-123') > 0, + 'Test invite should be created' +); + +-- Test that invite is pending +SELECT is( + (SELECT is_pending FROM public.tablo_invites WHERE invite_token = 'test-token-123' LIMIT 1), + true, + 'Test invite should be pending' +); + +-- ============================================================================ +-- Foreign Key Constraints Tests +-- ============================================================================ + +-- Test that tablo_access has foreign key to tablos +SELECT has_fk('public', 'tablo_access', 'tablo_access should have foreign key to tablos'); + +-- Test that tablo_invites has foreign key to tablos +SELECT has_fk('public', 'tablo_invites', 'tablo_invites should have foreign key to tablos'); + +-- ============================================================================ +-- Unique Constraints Tests +-- ============================================================================ + +-- Test unique constraint on tablo_invites +SELECT col_is_unique('public', 'tablo_invites', ARRAY['tablo_id', 'invited_email'], + 'tablo_invites should have unique constraint on (tablo_id, invited_email)'); + +-- Test unique constraint on tablo_access +SELECT col_is_unique('public', 'tablo_access', ARRAY['tablo_id', 'user_id'], + 'tablo_access should have unique constraint on (tablo_id, user_id)'); + +select * from finish(); +rollback; + diff --git a/supabase/tests/database/03_rls_policies_notes.test.sql b/supabase/tests/database/03_rls_policies_notes.test.sql new file mode 100644 index 0000000..3819c76 --- /dev/null +++ b/supabase/tests/database/03_rls_policies_notes.test.sql @@ -0,0 +1,218 @@ +begin; +select plan(38); -- Total number of tests + +-- ============================================================================ +-- RLS Enabled Tests +-- ============================================================================ + +SELECT is( + rls_enabled('public', 'notes'), + true, + 'RLS should be enabled on notes table' +); + +SELECT is( + rls_enabled('public', 'shared_notes'), + true, + 'RLS should be enabled on shared_notes table' +); + +SELECT is( + rls_enabled('public', 'note_access'), + true, + 'RLS should be enabled on note_access table' +); + +-- ============================================================================ +-- Notes Table RLS Policies +-- ============================================================================ + +SELECT has_policy('public', 'notes', 'Users can view their own notes and public notes', + 'Policy for viewing own and public notes should exist'); + +SELECT has_policy('public', 'notes', 'Users can insert their own notes', + 'Policy for inserting own notes should exist'); + +SELECT has_policy('public', 'notes', 'Users can update their own notes', + 'Policy for updating own notes should exist'); + +SELECT has_policy('public', 'notes', 'Users can delete their own notes', + 'Policy for deleting own notes should exist'); + +SELECT has_policy('public', 'notes', 'Users can delete their own notes (soft)', + 'Policy for soft deleting own notes should exist'); + +-- Test policy commands +SELECT policy_cmd_is('public', 'notes', 'Users can view their own notes and public notes', 'SELECT', + 'View notes policy should be for SELECT'); + +SELECT policy_cmd_is('public', 'notes', 'Users can insert their own notes', 'INSERT', + 'Insert notes policy should be for INSERT'); + +SELECT policy_cmd_is('public', 'notes', 'Users can update their own notes', 'UPDATE', + 'Update notes policy should be for UPDATE'); + +SELECT policy_cmd_is('public', 'notes', 'Users can delete their own notes', 'DELETE', + 'Delete notes policy should be for DELETE'); + +-- Test policy roles include both authenticated and anon for viewing +SELECT ok( + 'authenticated' = ANY(policy_roles('public', 'notes', 'Users can view their own notes and public notes')), + 'View notes policy should include authenticated role' +); + +SELECT ok( + 'anon' = ANY(policy_roles('public', 'notes', 'Users can view their own notes and public notes')), + 'View notes policy should include anon role for public notes' +); + +-- ============================================================================ +-- Shared Notes Table RLS Policies +-- ============================================================================ + +SELECT has_policy('public', 'shared_notes', 'Users can view their own shared notes', + 'Policy for viewing own shared notes should exist'); + +SELECT has_policy('public', 'shared_notes', 'Anyone can view public notes', + 'Policy for viewing public notes should exist'); + +SELECT has_policy('public', 'shared_notes', 'Users can insert their own shared notes', + 'Policy for inserting shared notes should exist'); + +SELECT has_policy('public', 'shared_notes', 'Users can update their own shared notes', + 'Policy for updating shared notes should exist'); + +SELECT has_policy('public', 'shared_notes', 'Users can delete their own shared notes', + 'Policy for deleting shared notes should exist'); + +-- Test policy commands +SELECT policy_cmd_is('public', 'shared_notes', 'Users can view their own shared notes', 'SELECT', + 'View own shared notes policy should be for SELECT'); + +SELECT policy_cmd_is('public', 'shared_notes', 'Anyone can view public notes', 'SELECT', + 'View public notes policy should be for SELECT'); + +-- Test that public notes policy applies to both authenticated and anon +SELECT ok( + 'authenticated' = ANY(policy_roles('public', 'shared_notes', 'Anyone can view public notes')), + 'Public notes policy should include authenticated role' +); + +SELECT ok( + 'anon' = ANY(policy_roles('public', 'shared_notes', 'Anyone can view public notes')), + 'Public notes policy should include anon role' +); + +-- ============================================================================ +-- Note Access Table RLS Policies +-- ============================================================================ + +SELECT has_policy('public', 'note_access', 'Users can view their own note access', + 'Policy for viewing own note access should exist'); + +SELECT has_policy('public', 'note_access', 'Users can view notes shared with their tablos', + 'Policy for viewing shared notes should exist'); + +SELECT has_policy('public', 'note_access', 'Users can insert their own note access', + 'Policy for inserting note access should exist'); + +SELECT has_policy('public', 'note_access', 'Users can update their own note access', + 'Policy for updating note access should exist'); + +SELECT has_policy('public', 'note_access', 'Users can delete their own note access', + 'Policy for deleting note access should exist'); + +-- Test policy commands +SELECT policy_cmd_is('public', 'note_access', 'Users can view their own note access', 'SELECT', + 'View own note access policy should be for SELECT'); + +SELECT policy_cmd_is('public', 'note_access', 'Users can insert their own note access', 'INSERT', + 'Insert note access policy should be for INSERT'); + +-- ============================================================================ +-- Notes Behavior Tests with Mock Data +-- ============================================================================ + +-- Create test users and notes +DO $$ +DECLARE + user1_id uuid := gen_random_uuid(); + user2_id uuid := gen_random_uuid(); + note1_id text := 'test_note_' || gen_random_uuid()::text; + note2_id text := 'test_note_' || gen_random_uuid()::text; + public_note_id text := 'public_note_' || gen_random_uuid()::text; +BEGIN + -- Insert test users + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (user1_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'noteuser1@test.com', 'encrypted', now(), now(), now()), + (user2_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'noteuser2@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profiles + INSERT INTO public.profiles (id, email, first_name, last_name) + VALUES + (user1_id, 'noteuser1@test.com', 'Note User', 'One'), + (user2_id, 'noteuser2@test.com', 'Note User', 'Two'); + + -- Insert test notes + INSERT INTO public.notes (id, title, content, user_id) + VALUES + (note1_id, 'User 1 Private Note', 'This is a private note', user1_id), + (note2_id, 'User 2 Private Note', 'This is another private note', user2_id), + (public_note_id, 'Public Note', 'This is a public note', user1_id); + + -- Make one note public + INSERT INTO public.shared_notes (note_id, user_id, is_public) + VALUES (public_note_id, user1_id, true); + + -- Store test IDs + PERFORM set_config('test.note_user1_id', user1_id::text, true); + PERFORM set_config('test.note_user2_id', user2_id::text, true); + PERFORM set_config('test.note1_id', note1_id, true); + PERFORM set_config('test.public_note_id', public_note_id, true); +END $$; + +-- Test: Verify notes were created +SELECT is( + (SELECT count(*)::integer FROM public.notes WHERE id = current_setting('test.note1_id')), + 1, + 'User 1 private note should be created' +); + +SELECT is( + (SELECT count(*)::integer FROM public.notes WHERE id = current_setting('test.public_note_id')), + 1, + 'Public note should be created' +); + +-- Test: Verify shared_notes entry for public note +SELECT is( + (SELECT is_public FROM public.shared_notes WHERE note_id = current_setting('test.public_note_id') LIMIT 1), + true, + 'Public note should be marked as public in shared_notes' +); + +-- ============================================================================ +-- Foreign Key Constraints Tests +-- ============================================================================ + +SELECT has_fk('public', 'shared_notes', 'shared_notes should have foreign key constraints'); +SELECT has_fk('public', 'note_access', 'note_access should have foreign key constraints'); + +-- Test that shared_notes.note_id references notes.id +SELECT fk_ok( + 'public', 'shared_notes', 'note_id', + 'public', 'notes', 'id', + 'shared_notes.note_id should reference notes.id' +); + +-- Test that note_access.note_id references notes.id +SELECT fk_ok( + 'public', 'note_access', 'note_id', + 'public', 'notes', 'id', + 'note_access.note_id should reference notes.id' +); + +select * from finish(); +rollback; + diff --git a/supabase/tests/database/04_rls_policies_other.test.sql b/supabase/tests/database/04_rls_policies_other.test.sql new file mode 100644 index 0000000..862e9c7 --- /dev/null +++ b/supabase/tests/database/04_rls_policies_other.test.sql @@ -0,0 +1,216 @@ +begin; +select plan(33); -- Total number of tests + +-- ============================================================================ +-- RLS Enabled Tests +-- ============================================================================ + +SELECT is( + rls_enabled('public', 'feedbacks'), + true, + 'RLS should be enabled on feedbacks table' +); + +SELECT is( + rls_enabled('public', 'events'), + true, + 'RLS should be enabled on events table' +); + +-- ============================================================================ +-- Feedbacks Table RLS Policies +-- ============================================================================ + +SELECT has_policy('public', 'feedbacks', 'Users can insert feedback.', + 'Policy for inserting feedback should exist'); + +SELECT policy_cmd_is('public', 'feedbacks', 'Users can insert feedback.', 'INSERT', + 'Feedback policy should be for INSERT'); + +SELECT policy_roles_are('public', 'feedbacks', 'Users can insert feedback.', + ARRAY['authenticated'], + 'Feedback insert policy should apply to authenticated users'); + +-- ============================================================================ +-- Events Table RLS Policies +-- ============================================================================ + +SELECT has_policy('public', 'events', 'Users can view events from accessible tablos', + 'Policy for viewing events from accessible tablos should exist'); + +SELECT has_policy('public', 'events', 'Users can insert events into accessible tablos', + 'Policy for inserting events should exist'); + +SELECT has_policy('public', 'events', 'Users can update their own events in accessible tablos', + 'Policy for updating own events should exist'); + +-- Test policy commands +SELECT policy_cmd_is('public', 'events', 'Users can view events from accessible tablos', 'SELECT', + 'View events policy should be for SELECT'); + +SELECT policy_cmd_is('public', 'events', 'Users can insert events into accessible tablos', 'INSERT', + 'Insert events policy should be for INSERT'); + +SELECT policy_cmd_is('public', 'events', 'Users can update their own events in accessible tablos', 'UPDATE', + 'Update events policy should be for UPDATE'); + +-- Test policy roles +SELECT policy_roles_are('public', 'events', 'Users can view events from accessible tablos', + ARRAY['authenticated'], + 'View events policy should apply to authenticated users'); + +SELECT policy_roles_are('public', 'events', 'Users can insert events into accessible tablos', + ARRAY['authenticated'], + 'Insert events policy should apply to authenticated users'); + +-- ============================================================================ +-- Feedbacks Behavior Tests +-- ============================================================================ + +-- Create test user and feedback +DO $$ +DECLARE + feedback_user_id uuid := gen_random_uuid(); +BEGIN + -- Insert test user + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (feedback_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'feedbackuser@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profile + INSERT INTO public.profiles (id, email, first_name, last_name) + VALUES + (feedback_user_id, 'feedbackuser@test.com', 'Feedback', 'User'); + + -- Insert test feedback + INSERT INTO public.feedbacks (fd_type, user_id, message) + VALUES + ('bug', feedback_user_id, 'Test bug report'), + ('feature', feedback_user_id, 'Test feature request'); + + -- Store test ID + PERFORM set_config('test.feedback_user_id', feedback_user_id::text, true); +END $$; + +-- Test: Verify feedbacks were created +SELECT is( + (SELECT count(*)::integer FROM public.feedbacks WHERE user_id = current_setting('test.feedback_user_id')::uuid), + 2, + 'Both test feedbacks should be created' +); + +-- Test: Verify feedback types are correct +SELECT ok( + (SELECT fd_type FROM public.feedbacks WHERE message = 'Test bug report' LIMIT 1) = 'bug', + 'Bug feedback should have correct type' +); + +SELECT ok( + (SELECT fd_type FROM public.feedbacks WHERE message = 'Test feature request' LIMIT 1) = 'feature', + 'Feature feedback should have correct type' +); + +-- ============================================================================ +-- Events Behavior Tests +-- ============================================================================ + +-- Create test user, tablo, and event +DO $$ +DECLARE + event_user_id uuid := gen_random_uuid(); + event_tablo_id text; +BEGIN + -- Insert test user + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (event_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'eventuser@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profile + INSERT INTO public.profiles (id, email, first_name, last_name) + VALUES + (event_user_id, 'eventuser@test.com', 'Event', 'User'); + + -- Insert test tablo + INSERT INTO public.tablos (owner_id, name, status, position) + VALUES + (event_user_id, 'Event Test Tablo', 'todo', 0) + RETURNING id::text INTO event_tablo_id; + + -- Insert test event + INSERT INTO public.events (tablo_id, title, description, start_date, start_time, created_by) + VALUES + (event_tablo_id, 'Test Event', 'Test event description', '2025-12-01', '10:00', event_user_id); + + -- Store test IDs + PERFORM set_config('test.event_user_id', event_user_id::text, true); + PERFORM set_config('test.event_tablo_id', event_tablo_id, true); +END $$; + +-- Test: Verify event was created +SELECT ok( + (SELECT count(*) FROM public.events WHERE title = 'Test Event' AND deleted_at IS NULL) > 0, + 'Test event should be created' +); + +-- Test: Verify event is linked to correct tablo +SELECT is( + (SELECT tablo_id FROM public.events WHERE title = 'Test Event' AND deleted_at IS NULL LIMIT 1), + current_setting('test.event_tablo_id'), + 'Event should be linked to correct tablo' +); + +-- Test: Verify event has correct creator +SELECT is( + (SELECT created_by FROM public.events WHERE title = 'Test Event' AND deleted_at IS NULL LIMIT 1), + current_setting('test.event_user_id')::uuid, + 'Event should have correct creator' +); + +-- ============================================================================ +-- Check Constraint Tests +-- ============================================================================ + +-- Test feedbacks fd_type check constraint +SELECT ok( + (SELECT COUNT(*) FROM information_schema.check_constraints + WHERE constraint_schema = 'public' + AND constraint_name LIKE '%feedbacks_fd_type%') > 0, + 'Feedbacks table should have fd_type check constraint' +); + +-- Test that invalid feedback type would be rejected (we can't actually insert invalid data, but we can check the constraint exists) +SELECT col_has_check('public', 'feedbacks', 'fd_type', + 'fd_type column should have check constraint'); + +-- Test tablos status check constraint +SELECT col_has_check('public', 'tablos', 'status', + 'status column should have check constraint'); + +-- ============================================================================ +-- Foreign Key Constraints Tests +-- ============================================================================ + +SELECT has_fk('public', 'feedbacks', 'feedbacks should have foreign key constraints'); +SELECT has_fk('public', 'events', 'events should have foreign key constraints'); + +-- Test that events.tablo_id references tablos.id +SELECT fk_ok( + 'public', 'events', 'tablo_id', + 'public', 'tablos', 'id', + 'events.tablo_id should reference tablos.id' +); + +-- Test that events.created_by references auth.users.id +SELECT fk_ok( + 'public', 'events', 'created_by', + 'auth', 'users', 'id', + 'events.created_by should reference auth.users.id' +); + +-- Test that feedbacks.user_id references auth.users (implicitly through profiles) +SELECT col_is_fk('public', 'feedbacks', 'user_id', + 'feedbacks.user_id should be a foreign key'); + +select * from finish(); +rollback; + diff --git a/supabase/tests/database/05_triggers.test.sql b/supabase/tests/database/05_triggers.test.sql new file mode 100644 index 0000000..e6ade4c --- /dev/null +++ b/supabase/tests/database/05_triggers.test.sql @@ -0,0 +1,303 @@ +begin; +select plan(28); -- Total number of tests + +-- ============================================================================ +-- Trigger Function Existence Tests +-- ============================================================================ + +SELECT has_function('public', 'create_tablo_access_for_owner', + 'Function create_tablo_access_for_owner should exist'); + +SELECT has_function('public', 'create_last_signed_in_on_profiles', + 'Function create_last_signed_in_on_profiles should exist'); + +SELECT has_function('public', 'update_tablo_invites_on_login', + 'Function update_tablo_invites_on_login should exist'); + +SELECT has_function('public', 'update_profile_subscription_status', + 'Function update_profile_subscription_status should exist'); + +-- ============================================================================ +-- Trigger Existence Tests +-- ============================================================================ + +SELECT has_trigger('public', 'tablos', 'trigger_create_tablo_access', + 'Trigger trigger_create_tablo_access should exist on tablos table'); + +SELECT has_trigger('auth', 'users', 'trigger_on_last_signed_in', + 'Trigger trigger_on_last_signed_in should exist on auth.users table'); + +SELECT has_trigger('auth', 'users', 'trigger_update_tablo_invites_on_login', + 'Trigger trigger_update_tablo_invites_on_login should exist on auth.users table'); + +-- Stripe triggers +SELECT ok( + (SELECT COUNT(*) FROM information_schema.triggers + WHERE trigger_name = 'update_profile_on_subscription_change' + AND event_object_schema = 'stripe') > 0, + 'Trigger update_profile_on_subscription_change should exist on stripe schema' +); + +-- ============================================================================ +-- Tablo Access Trigger Tests +-- ============================================================================ + +-- Create test user and tablo to trigger auto-creation of tablo_access +DO $$ +DECLARE + trigger_user_id uuid := gen_random_uuid(); + trigger_tablo_id integer; +BEGIN + -- Insert test user + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (trigger_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'triggeruser@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profile + INSERT INTO public.profiles (id, email, first_name, last_name) + VALUES + (trigger_user_id, 'triggeruser@test.com', 'Trigger', 'User'); + + -- Insert tablo (this should trigger auto-creation of tablo_access) + INSERT INTO public.tablos (owner_id, name, status, position) + VALUES + (trigger_user_id, 'Trigger Test Tablo', 'todo', 0) + RETURNING id INTO trigger_tablo_id; + + -- Store test IDs + PERFORM set_config('test.trigger_user_id', trigger_user_id::text, true); + PERFORM set_config('test.trigger_tablo_id', trigger_tablo_id::text, true); +END $$; + +-- Test: Verify tablo_access was auto-created +SELECT is( + ( + SELECT count(*)::integer + FROM public.tablo_access + WHERE tablo_id = current_setting('test.trigger_tablo_id')::integer + AND user_id = current_setting('test.trigger_user_id')::uuid + ), + 1, + 'Tablo access should be auto-created when tablo is created' +); + +-- Test: Verify tablo_access has correct fields +SELECT is( + ( + SELECT is_active + FROM public.tablo_access + WHERE tablo_id = current_setting('test.trigger_tablo_id')::integer + AND user_id = current_setting('test.trigger_user_id')::uuid + LIMIT 1 + ), + true, + 'Auto-created tablo access should be active' +); + +SELECT is( + ( + SELECT is_admin + FROM public.tablo_access + WHERE tablo_id = current_setting('test.trigger_tablo_id')::integer + AND user_id = current_setting('test.trigger_user_id')::uuid + LIMIT 1 + ), + true, + 'Auto-created tablo access should have admin privileges' +); + +SELECT is( + ( + SELECT granted_by + FROM public.tablo_access + WHERE tablo_id = current_setting('test.trigger_tablo_id')::integer + AND user_id = current_setting('test.trigger_user_id')::uuid + LIMIT 1 + ), + current_setting('test.trigger_user_id')::uuid, + 'Auto-created tablo access should be granted by owner' +); + +-- ============================================================================ +-- Last Signed In Trigger Tests +-- ============================================================================ + +-- Create test user with last_sign_in_at +DO $$ +DECLARE + signin_user_id uuid := gen_random_uuid(); + test_signin_time timestamp with time zone := now(); +BEGIN + -- Insert test user + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, last_sign_in_at, created_at, updated_at) + VALUES + (signin_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'signinuser@test.com', 'encrypted', now(), test_signin_time, now(), now()); + + -- Insert test profile + INSERT INTO public.profiles (id, email, first_name, last_name) + VALUES + (signin_user_id, 'signinuser@test.com', 'SignIn', 'User'); + + -- Store test IDs + PERFORM set_config('test.signin_user_id', signin_user_id::text, true); + PERFORM set_config('test.signin_time', test_signin_time::text, true); +END $$; + +-- Test: Update last_sign_in_at on auth.users (simulating a sign-in) +DO $$ +DECLARE + new_signin_time timestamp with time zone := now() + interval '1 hour'; +BEGIN + UPDATE auth.users + SET last_sign_in_at = new_signin_time, + updated_at = now() + WHERE id = current_setting('test.signin_user_id')::uuid; + + PERFORM set_config('test.new_signin_time', new_signin_time::text, true); +END $$; + +-- Test: Verify last_signed_in was updated in profiles +SELECT ok( + ( + SELECT last_signed_in + FROM public.profiles + WHERE id = current_setting('test.signin_user_id')::uuid + ) IS NOT NULL, + 'Profile last_signed_in should be updated after auth.users sign in' +); + +-- ============================================================================ +-- Tablo Invites Trigger Tests +-- ============================================================================ + +-- Create test temporary user and invite +DO $$ +DECLARE + temp_user_id uuid := gen_random_uuid(); + temp_user_email text := 'tempuser@test.com'; + invite_tablo_id integer; +BEGIN + -- Insert test user (will be marked as temporary) + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (temp_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', temp_user_email, 'encrypted', now(), now(), now()); + + -- Insert test profile marked as temporary + INSERT INTO public.profiles (id, email, first_name, last_name, is_temporary) + VALUES + (temp_user_id, temp_user_email, 'Temp', 'User', true); + + -- Create a tablo for invites + INSERT INTO public.tablos (owner_id, name, status, position) + VALUES + (temp_user_id, 'Invite Test Tablo', 'todo', 0) + RETURNING id INTO invite_tablo_id; + + -- Create pending invite for this user's email + INSERT INTO public.tablo_invites (tablo_id, invited_email, invited_by, invite_token, is_pending) + VALUES + (invite_tablo_id, temp_user_email, temp_user_id, 'temp-user-token', true); + + -- Store test IDs + PERFORM set_config('test.temp_user_id', temp_user_id::text, true); + PERFORM set_config('test.temp_user_email', temp_user_email, true); +END $$; + +-- Test: Verify invite is initially pending +SELECT is( + ( + SELECT is_pending + FROM public.tablo_invites + WHERE invited_email = current_setting('test.temp_user_email') + AND invite_token = 'temp-user-token' + LIMIT 1 + ), + true, + 'Invite should be initially pending' +); + +-- Test: Simulate sign-in to trigger invite update +DO $$ +BEGIN + UPDATE auth.users + SET last_sign_in_at = now(), + updated_at = now() + WHERE id = current_setting('test.temp_user_id')::uuid; +END $$; + +-- Test: Verify invite is_pending was set to false after sign-in +SELECT is( + ( + SELECT is_pending + FROM public.tablo_invites + WHERE invited_email = current_setting('test.temp_user_email') + AND invite_token = 'temp-user-token' + LIMIT 1 + ), + false, + 'Invite should be marked as not pending after temporary user signs in' +); + +-- ============================================================================ +-- Trigger Timing Tests +-- ============================================================================ + +-- Test that tablo access trigger fires AFTER INSERT +SELECT trigger_is( + 'public', 'tablos', 'trigger_create_tablo_access', + 'public', 'create_tablo_access_for_owner', + 'Tablo access trigger should fire after insert' +); + +-- ============================================================================ +-- Security Tests +-- ============================================================================ + +-- Test that trigger functions are SECURITY DEFINER +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'create_tablo_access_for_owner' + LIMIT 1 + ), + true, + 'create_tablo_access_for_owner should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'create_last_signed_in_on_profiles' + LIMIT 1 + ), + true, + 'create_last_signed_in_on_profiles should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'update_tablo_invites_on_login' + LIMIT 1 + ), + true, + 'update_tablo_invites_on_login should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'update_profile_subscription_status' + LIMIT 1 + ), + true, + 'update_profile_subscription_status should be SECURITY DEFINER' +); + +select * from finish(); +rollback; + diff --git a/supabase/tests/database/06_stripe_functions.test.sql b/supabase/tests/database/06_stripe_functions.test.sql new file mode 100644 index 0000000..48e2e23 --- /dev/null +++ b/supabase/tests/database/06_stripe_functions.test.sql @@ -0,0 +1,332 @@ +begin; +select plan(40); -- Total number of tests + +-- ============================================================================ +-- Stripe Schema Tests +-- ============================================================================ + +SELECT has_schema('stripe', 'Stripe schema should exist'); + +-- ============================================================================ +-- Stripe Function Existence Tests +-- ============================================================================ + +SELECT has_function('public', 'get_my_active_subscription', + 'Function get_my_active_subscription should exist'); + +SELECT has_function('public', 'get_user_stripe_customer', + 'Function get_user_stripe_customer should exist'); + +SELECT has_function('public', 'get_user_stripe_subscriptions', + 'Function get_user_stripe_subscriptions should exist'); + +SELECT has_function('public', 'get_stripe_products', + 'Function get_stripe_products should exist'); + +SELECT has_function('public', 'get_stripe_prices', + 'Function get_stripe_prices should exist'); + +SELECT has_function('public', 'is_paying_user', ARRAY['uuid'], + 'Function is_paying_user should exist with uuid parameter'); + +SELECT has_function('public', 'get_user_subscription_status', ARRAY['uuid'], + 'Function get_user_subscription_status should exist with uuid parameter'); + +SELECT has_function('public', 'get_user_stripe_customer_id', ARRAY['uuid'], + 'Function get_user_stripe_customer_id should exist with uuid parameter'); + +-- ============================================================================ +-- Function Security Tests (SECURITY DEFINER) +-- ============================================================================ + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'get_my_active_subscription' + LIMIT 1 + ), + true, + 'get_my_active_subscription should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'get_user_stripe_customer' + LIMIT 1 + ), + true, + 'get_user_stripe_customer should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'get_user_stripe_subscriptions' + LIMIT 1 + ), + true, + 'get_user_stripe_subscriptions should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'get_stripe_products' + LIMIT 1 + ), + true, + 'get_stripe_products should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'get_stripe_prices' + LIMIT 1 + ), + true, + 'get_stripe_prices should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'is_paying_user' + LIMIT 1 + ), + true, + 'is_paying_user should be SECURITY DEFINER' +); + +SELECT is( + ( + SELECT prosecdef + FROM pg_proc + WHERE proname = 'get_user_subscription_status' + LIMIT 1 + ), + true, + 'get_user_subscription_status should be SECURITY DEFINER' +); + +-- ============================================================================ +-- Profile Stripe Columns Tests +-- ============================================================================ + +SELECT has_column('public', 'profiles', 'is_paying', + 'profiles should have is_paying column'); + +SELECT has_column('public', 'profiles', 'subscription_tier', + 'profiles should have subscription_tier column'); + +SELECT col_type_is('public', 'profiles', 'is_paying', 'boolean', + 'profiles.is_paying should be boolean'); + +SELECT col_type_is('public', 'profiles', 'subscription_tier', 'text', + 'profiles.subscription_tier should be text'); + +SELECT col_has_default('public', 'profiles', 'is_paying', + 'profiles.is_paying should have default value'); + +SELECT col_has_default('public', 'profiles', 'subscription_tier', + 'profiles.subscription_tier should have default value'); + +-- ============================================================================ +-- Function Return Type Tests +-- ============================================================================ + +-- Test that is_paying_user returns boolean +SELECT is( + ( + SELECT prorettype::regtype::text + FROM pg_proc + WHERE proname = 'is_paying_user' + LIMIT 1 + ), + 'boolean', + 'is_paying_user should return boolean' +); + +-- Test that get_user_stripe_customer_id returns text +SELECT is( + ( + SELECT prorettype::regtype::text + FROM pg_proc + WHERE proname = 'get_user_stripe_customer_id' + LIMIT 1 + ), + 'text', + 'get_user_stripe_customer_id should return text' +); + +-- ============================================================================ +-- Test Function Behavior +-- ============================================================================ + +-- Create test user for Stripe functions +DO $$ +DECLARE + stripe_user_id uuid := gen_random_uuid(); +BEGIN + -- Insert test user + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (stripe_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'stripeuser@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profile + INSERT INTO public.profiles (id, email, first_name, last_name, is_paying, subscription_tier) + VALUES + (stripe_user_id, 'stripeuser@test.com', 'Stripe', 'User', false, 'free'); + + -- Store test ID + PERFORM set_config('test.stripe_user_id', stripe_user_id::text, true); +END $$; + +-- Test: User has is_paying set to false by default +SELECT is( + ( + SELECT is_paying + FROM public.profiles + WHERE id = current_setting('test.stripe_user_id')::uuid + LIMIT 1 + ), + false, + 'New user should have is_paying set to false' +); + +-- Test: User has subscription_tier set to free by default +SELECT is( + ( + SELECT subscription_tier + FROM public.profiles + WHERE id = current_setting('test.stripe_user_id')::uuid + LIMIT 1 + ), + 'free', + 'New user should have subscription_tier set to free' +); + +-- Test: is_paying_user returns false for non-paying user +SELECT is( + public.is_paying_user(current_setting('test.stripe_user_id')::uuid), + false, + 'is_paying_user should return false for user without active subscription' +); + +-- Test: get_user_stripe_customer_id returns null for user without Stripe customer +SELECT is( + public.get_user_stripe_customer_id(current_setting('test.stripe_user_id')::uuid), + NULL, + 'get_user_stripe_customer_id should return null for user without Stripe customer' +); + +-- ============================================================================ +-- View Tests +-- ============================================================================ + +SELECT has_view('public', 'active_subscriptions', + 'active_subscriptions view should exist'); + +-- Test that the view is secure (note: this view was replaced with a function in migration 37) +-- But we still test for its existence in case it's being used +SELECT ok( + (SELECT COUNT(*) FROM information_schema.views WHERE table_schema = 'public' AND table_name = 'active_subscriptions') >= 0, + 'active_subscriptions view existence check' +); + +-- ============================================================================ +-- Subscription Plan Enum Tests (if exists) +-- ============================================================================ + +-- Check if subscription_plan type exists +SELECT ok( + (SELECT COUNT(*) FROM pg_type WHERE typname = 'subscription_plan') >= 0, + 'Check for subscription_plan type' +); + +-- ============================================================================ +-- Comments and Documentation Tests +-- ============================================================================ + +-- Test that functions have comments for documentation +SELECT ok( + ( + SELECT obj_description(oid) IS NOT NULL + FROM pg_proc + WHERE proname = 'get_my_active_subscription' + LIMIT 1 + ), + 'get_my_active_subscription should have documentation comment' +); + +SELECT ok( + ( + SELECT obj_description(oid) IS NOT NULL + FROM pg_proc + WHERE proname = 'is_paying_user' + LIMIT 1 + ), + 'is_paying_user should have documentation comment' +); + +-- ============================================================================ +-- Profile Update Tests +-- ============================================================================ + +-- Test updating a user's subscription status +DO $$ +DECLARE + paying_user_id uuid := gen_random_uuid(); +BEGIN + -- Insert test user + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (paying_user_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'payinguser@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profile + INSERT INTO public.profiles (id, email, first_name, last_name, is_paying, subscription_tier) + VALUES + (paying_user_id, 'payinguser@test.com', 'Paying', 'User', false, 'free'); + + -- Update to paying + UPDATE public.profiles + SET is_paying = true, subscription_tier = 'standard' + WHERE id = paying_user_id; + + -- Store test ID + PERFORM set_config('test.paying_user_id', paying_user_id::text, true); +END $$; + +-- Test: Verify profile was updated to paying +SELECT is( + ( + SELECT is_paying + FROM public.profiles + WHERE id = current_setting('test.paying_user_id')::uuid + LIMIT 1 + ), + true, + 'Profile should be updated to paying' +); + +SELECT is( + ( + SELECT subscription_tier + FROM public.profiles + WHERE id = current_setting('test.paying_user_id')::uuid + LIMIT 1 + ), + 'standard', + 'Profile subscription_tier should be updated to standard' +); + +select * from finish(); +rollback; + diff --git a/supabase/tests/database/07_views.test.sql b/supabase/tests/database/07_views.test.sql new file mode 100644 index 0000000..6fce190 --- /dev/null +++ b/supabase/tests/database/07_views.test.sql @@ -0,0 +1,182 @@ +begin; +select plan(21); -- Total number of tests + +-- ============================================================================ +-- View Existence Tests +-- ============================================================================ + +SELECT has_view('public', 'user_tablos', + 'user_tablos view should exist'); + +SELECT has_view('public', 'active_subscriptions', + 'active_subscriptions view should exist'); + +-- ============================================================================ +-- User Tablos View Tests +-- ============================================================================ + +-- Test that user_tablos view has expected columns +SELECT has_column('public', 'user_tablos', 'id', + 'user_tablos view should have id column'); + +SELECT has_column('public', 'user_tablos', 'user_id', + 'user_tablos view should have user_id column'); + +SELECT has_column('public', 'user_tablos', 'name', + 'user_tablos view should have name column'); + +SELECT has_column('public', 'user_tablos', 'status', + 'user_tablos view should have status column'); + +SELECT has_column('public', 'user_tablos', 'access_level', + 'user_tablos view should have access_level column'); + +SELECT has_column('public', 'user_tablos', 'is_admin', + 'user_tablos view should have is_admin column'); + +SELECT has_column('public', 'user_tablos', 'position', + 'user_tablos view should have position column'); + +SELECT has_column('public', 'user_tablos', 'deleted_at', + 'user_tablos view should have deleted_at column'); + +-- Test that user_tablos is defined with security_invoker +SELECT ok( + ( + SELECT COUNT(*) + FROM pg_views + WHERE schemaname = 'public' + AND viewname = 'user_tablos' + AND definition LIKE '%security_invoker%' + ) > 0, + 'user_tablos view should use security_invoker' +); + +-- ============================================================================ +-- User Tablos View Behavior Tests +-- ============================================================================ + +-- Create test data for view testing +DO $$ +DECLARE + view_user1_id uuid := gen_random_uuid(); + view_user2_id uuid := gen_random_uuid(); + view_tablo1_id integer; + view_tablo2_id integer; +BEGIN + -- Insert test users + INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) + VALUES + (view_user1_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'viewuser1@test.com', 'encrypted', now(), now(), now()), + (view_user2_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'viewuser2@test.com', 'encrypted', now(), now(), now()); + + -- Insert test profiles + INSERT INTO public.profiles (id, email, first_name, last_name) + VALUES + (view_user1_id, 'viewuser1@test.com', 'View User', 'One'), + (view_user2_id, 'viewuser2@test.com', 'View User', 'Two'); + + -- Insert test tablos + INSERT INTO public.tablos (owner_id, name, status, position) + VALUES + (view_user1_id, 'View User 1 Tablo', 'todo', 0), + (view_user2_id, 'View User 2 Tablo', 'in_progress', 1) + RETURNING id INTO view_tablo1_id; + + -- Store test IDs + PERFORM set_config('test.view_user1_id', view_user1_id::text, true); + PERFORM set_config('test.view_user2_id', view_user2_id::text, true); +END $$; + +-- Test: Verify user_tablos returns tablos for users +SELECT ok( + (SELECT count(*) FROM public.user_tablos WHERE user_id = current_setting('test.view_user1_id')::uuid) > 0, + 'user_tablos should return tablos for user 1' +); + +SELECT ok( + (SELECT count(*) FROM public.user_tablos WHERE user_id = current_setting('test.view_user2_id')::uuid) > 0, + 'user_tablos should return tablos for user 2' +); + +-- Test: Verify access_level is set correctly for owner +SELECT is( + ( + SELECT access_level + FROM public.user_tablos + WHERE user_id = current_setting('test.view_user1_id')::uuid + AND name = 'View User 1 Tablo' + LIMIT 1 + ), + 'admin', + 'Owner should have admin access_level in user_tablos view' +); + +-- Test: Verify is_admin is true for owner +SELECT is( + ( + SELECT is_admin + FROM public.user_tablos + WHERE user_id = current_setting('test.view_user1_id')::uuid + AND name = 'View User 1 Tablo' + LIMIT 1 + ), + true, + 'Owner should have is_admin true in user_tablos view' +); + +-- Test: Verify deleted tablos are filtered out +SELECT is( + (SELECT count(*) FROM public.user_tablos WHERE deleted_at IS NOT NULL), + 0::bigint, + 'user_tablos view should not return deleted tablos' +); + +-- ============================================================================ +-- Active Subscriptions View Tests +-- ============================================================================ + +-- Test that active_subscriptions view has expected columns +SELECT has_column('public', 'active_subscriptions', 'subscription_id', + 'active_subscriptions view should have subscription_id column'); + +SELECT has_column('public', 'active_subscriptions', 'user_id', + 'active_subscriptions view should have user_id column'); + +SELECT has_column('public', 'active_subscriptions', 'status', + 'active_subscriptions view should have status column'); + +-- ============================================================================ +-- View Comments and Documentation +-- ============================================================================ + +-- Test that views have documentation comments +SELECT ok( + ( + SELECT obj_description(c.oid) IS NOT NULL + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' + AND c.relname = 'user_tablos' + AND c.relkind = 'v' + LIMIT 1 + ), + 'user_tablos view should have documentation comment' +); + +SELECT ok( + ( + SELECT obj_description(c.oid) IS NOT NULL + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' + AND c.relname = 'active_subscriptions' + AND c.relkind = 'v' + LIMIT 1 + ), + 'active_subscriptions view should have documentation comment' +); + +select * from finish(); +rollback; + diff --git a/supabase/tests/database/08_indexes_performance.test.sql b/supabase/tests/database/08_indexes_performance.test.sql new file mode 100644 index 0000000..f3ea2d4 --- /dev/null +++ b/supabase/tests/database/08_indexes_performance.test.sql @@ -0,0 +1,146 @@ +begin; +select plan(34); -- Total number of tests + +-- ============================================================================ +-- Tablo Access Indexes +-- ============================================================================ + +SELECT has_index('public', 'tablo_access', 'idx_tablo_access_tablo_id', + 'Index on tablo_access.tablo_id should exist'); + +SELECT has_index('public', 'tablo_access', 'idx_tablo_access_user_id', + 'Index on tablo_access.user_id should exist'); + +-- Test that the indexes are on the correct columns +SELECT index_is_type('public', 'tablo_access', 'idx_tablo_access_tablo_id', 'btree', + 'tablo_access.tablo_id index should be btree'); + +SELECT index_is_type('public', 'tablo_access', 'idx_tablo_access_user_id', 'btree', + 'tablo_access.user_id index should be btree'); + +-- ============================================================================ +-- Events Table Indexes +-- ============================================================================ + +SELECT has_index('public', 'events', 'idx_events_tablo_id', + 'Index on events.tablo_id should exist'); + +SELECT has_index('public', 'events', 'idx_events_created_by', + 'Index on events.created_by should exist'); + +SELECT has_index('public', 'events', 'idx_events_start_date', + 'Index on events.start_date should exist'); + +SELECT has_index('public', 'events', 'idx_events_deleted_at', + 'Index on events.deleted_at should exist'); + +SELECT index_is_type('public', 'events', 'idx_events_tablo_id', 'btree', + 'events.tablo_id index should be btree'); + +SELECT index_is_type('public', 'events', 'idx_events_start_date', 'btree', + 'events.start_date index should be btree'); + +-- ============================================================================ +-- Notes Table Indexes +-- ============================================================================ + +SELECT has_index('public', 'notes', 'idx_notes_user_id', + 'Index on notes.user_id should exist'); + +SELECT has_index('public', 'notes', 'idx_notes_deleted_at', + 'Index on notes.deleted_at should exist'); + +SELECT has_index('public', 'notes', 'idx_notes_created_at', + 'Index on notes.created_at should exist'); + +SELECT index_is_type('public', 'notes', 'idx_notes_user_id', 'btree', + 'notes.user_id index should be btree'); + +SELECT index_is_type('public', 'notes', 'idx_notes_deleted_at', 'btree', + 'notes.deleted_at index should be btree'); + +-- ============================================================================ +-- Shared Notes Table Indexes +-- ============================================================================ + +SELECT has_index('public', 'shared_notes', 'idx_shared_notes_is_public', + 'Index on shared_notes.is_public should exist'); + +SELECT has_index('public', 'shared_notes', 'idx_shared_notes_user_id', + 'Index on shared_notes.user_id should exist'); + +SELECT index_is_type('public', 'shared_notes', 'idx_shared_notes_is_public', 'btree', + 'shared_notes.is_public index should be btree'); + +-- ============================================================================ +-- Note Access Table Indexes +-- ============================================================================ + +SELECT has_index('public', 'note_access', 'idx_note_access_note_id', + 'Index on note_access.note_id should exist'); + +SELECT has_index('public', 'note_access', 'idx_note_access_user_id', + 'Index on note_access.user_id should exist'); + +SELECT has_index('public', 'note_access', 'idx_note_access_tablo_id', + 'Index on note_access.tablo_id should exist'); + +SELECT has_index('public', 'note_access', 'idx_note_access_is_active', + 'Index on note_access.is_active should exist'); + +-- ============================================================================ +-- Unique Indexes for Note Access +-- ============================================================================ + +SELECT has_index('public', 'note_access', 'unique_note_access_with_tablo', + 'Unique index on note_access (note_id, user_id, tablo_id) should exist'); + +SELECT has_index('public', 'note_access', 'unique_note_access_all_tablos', + 'Unique index on note_access (note_id, user_id) for NULL tablo_id should exist'); + +-- ============================================================================ +-- Primary Key Indexes +-- ============================================================================ + +-- Test that primary keys exist (which create implicit indexes) +SELECT has_pk('public', 'tablos', 'tablos should have primary key'); +SELECT has_pk('public', 'tablo_access', 'tablo_access should have primary key'); +SELECT has_pk('public', 'tablo_invites', 'tablo_invites should have primary key'); +SELECT has_pk('public', 'feedbacks', 'feedbacks should have primary key'); +SELECT has_pk('public', 'events', 'events should have primary key'); +SELECT has_pk('public', 'notes', 'notes should have primary key'); +SELECT has_pk('public', 'shared_notes', 'shared_notes should have primary key'); +SELECT has_pk('public', 'note_access', 'note_access should have primary key'); + +-- ============================================================================ +-- Verify Index Coverage for Common Query Patterns +-- ============================================================================ + +-- Test that commonly queried foreign key columns have indexes +-- This helps with JOIN performance and foreign key constraint enforcement + +SELECT ok( + ( + SELECT COUNT(*) + FROM pg_indexes + WHERE schemaname = 'public' + AND tablename = 'tablo_access' + AND indexdef LIKE '%tablo_id%' + ) > 0, + 'tablo_access should have index on tablo_id for foreign key joins' +); + +SELECT ok( + ( + SELECT COUNT(*) + FROM pg_indexes + WHERE schemaname = 'public' + AND tablename = 'events' + AND indexdef LIKE '%tablo_id%' + ) > 0, + 'events should have index on tablo_id for foreign key joins' +); + +select * from finish(); +rollback; +