begin; select plan(71); -- Total number of tests (54 original + 9 multiple notifications tests + 2 invite edge cases + 6 null actor tests) -- ============================================================================ -- Table Existence Tests -- ============================================================================ SELECT has_table('public', 'notifications', 'notifications table should exist'); -- ============================================================================ -- Notifications Table Structure -- ============================================================================ SELECT has_column('public', 'notifications', 'id', 'notifications should have id column'); SELECT has_column('public', 'notifications', 'user_id', 'notifications should have user_id column'); SELECT has_column('public', 'notifications', 'actor_id', 'notifications should have actor_id column'); SELECT has_column('public', 'notifications', 'entity_type', 'notifications should have entity_type column'); SELECT has_column('public', 'notifications', 'entity_id', 'notifications should have entity_id column'); SELECT has_column('public', 'notifications', 'action_type', 'notifications should have action_type column'); SELECT has_column('public', 'notifications', 'message', 'notifications should have message column'); SELECT has_column('public', 'notifications', 'metadata', 'notifications should have metadata column'); SELECT has_column('public', 'notifications', 'read_at', 'notifications should have read_at column'); SELECT has_column('public', 'notifications', 'created_at', 'notifications should have created_at column'); SELECT col_type_is('public', 'notifications', 'id', 'uuid', 'notifications.id should be uuid'); SELECT col_type_is('public', 'notifications', 'user_id', 'uuid', 'notifications.user_id should be uuid'); SELECT col_type_is('public', 'notifications', 'actor_id', 'uuid', 'notifications.actor_id should be uuid'); SELECT col_type_is('public', 'notifications', 'entity_type', 'text', 'notifications.entity_type should be text'); SELECT col_type_is('public', 'notifications', 'entity_id', 'text', 'notifications.entity_id should be text'); SELECT col_type_is('public', 'notifications', 'action_type', 'text', 'notifications.action_type should be text'); SELECT col_type_is('public', 'notifications', 'message', 'text', 'notifications.message should be text'); SELECT col_type_is('public', 'notifications', 'metadata', 'jsonb', 'notifications.metadata should be jsonb'); SELECT col_type_is('public', 'notifications', 'read_at', 'timestamp with time zone', 'notifications.read_at should be timestamptz'); SELECT col_type_is('public', 'notifications', 'created_at', 'timestamp with time zone', 'notifications.created_at should be timestamptz'); SELECT col_not_null('public', 'notifications', 'id', 'notifications.id should be NOT NULL'); SELECT col_not_null('public', 'notifications', 'user_id', 'notifications.user_id should be NOT NULL'); SELECT col_not_null('public', 'notifications', 'entity_type', 'notifications.entity_type should be NOT NULL'); SELECT col_not_null('public', 'notifications', 'entity_id', 'notifications.entity_id should be NOT NULL'); SELECT col_not_null('public', 'notifications', 'action_type', 'notifications.action_type should be NOT NULL'); SELECT col_not_null('public', 'notifications', 'message', 'notifications.message should be NOT NULL'); SELECT col_not_null('public', 'notifications', 'created_at', 'notifications.created_at should be NOT NULL'); SELECT col_has_default('public', 'notifications', 'id', 'notifications.id should have default'); SELECT col_has_default('public', 'notifications', 'metadata', 'notifications.metadata should have default'); SELECT col_has_default('public', 'notifications', 'created_at', 'notifications.created_at should have default'); -- Check action_type constraint SELECT ok( (SELECT COUNT(*) FROM information_schema.check_constraints WHERE constraint_schema = 'public' AND constraint_name LIKE '%action_type%') > 0, 'notifications.action_type should have check constraint' ); -- ============================================================================ -- Index Existence Tests -- ============================================================================ SELECT ok( (SELECT COUNT(*) FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'notifications' AND indexname = 'idx_notifications_user_id') > 0, 'Index idx_notifications_user_id should exist' ); SELECT ok( (SELECT COUNT(*) FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'notifications' AND indexname = 'idx_notifications_entity_type') > 0, 'Index idx_notifications_entity_type should exist' ); SELECT ok( (SELECT COUNT(*) FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'notifications' AND indexname = 'idx_notifications_entity_id') > 0, 'Index idx_notifications_entity_id should exist' ); SELECT ok( (SELECT COUNT(*) FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'notifications' AND indexname = 'idx_notifications_read_at') > 0, 'Index idx_notifications_read_at should exist' ); SELECT ok( (SELECT COUNT(*) FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'notifications' AND indexname = 'idx_notifications_created_at') > 0, 'Index idx_notifications_created_at should exist' ); -- ============================================================================ -- Trigger Function Tests -- ============================================================================ SELECT has_function('public', 'notify_users', 'Function notify_users should exist'); -- ============================================================================ -- Trigger Existence Tests -- ============================================================================ SELECT has_trigger('public', 'tablos', 'notify_users_on_tablos', 'Trigger notify_users_on_tablos should exist on tablos table'); SELECT has_trigger('public', 'tasks', 'notify_users_on_tasks', 'Trigger notify_users_on_tasks should exist on tasks table'); SELECT has_trigger('public', 'events', 'notify_users_on_events', 'Trigger notify_users_on_events should exist on events table'); SELECT has_trigger('public', 'notes', 'notify_users_on_notes', 'Trigger notify_users_on_notes should exist on notes table'); SELECT has_trigger('public', 'tablo_access', 'notify_users_on_tablo_access', 'Trigger notify_users_on_tablo_access should exist on tablo_access table'); SELECT has_trigger('public', 'tablo_invites', 'notify_users_on_tablo_invites', 'Trigger notify_users_on_tablo_invites should exist on tablo_invites table'); -- ============================================================================ -- Trigger Behavior Tests - Setup Test Data -- ============================================================================ DO $$ DECLARE test_user1_id uuid := gen_random_uuid(); test_user2_id uuid := gen_random_uuid(); test_tablo_id text; test_task_id text; test_event_id text; test_note_id 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 (test_user1_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notiftest1_' || test_user1_id::text || '@test.com', 'encrypted', now(), now(), now()), (test_user2_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notiftest2_' || test_user2_id::text || '@test.com', 'encrypted', now(), now(), now()) ON CONFLICT DO NOTHING; -- Insert test profiles INSERT INTO public.profiles (id, email, first_name, last_name, short_user_id) VALUES (test_user1_id, 'notiftest1_' || test_user1_id::text || '@test.com', 'NotifTest', 'User1', substring(test_user1_id::text from 1 for 8)), (test_user2_id, 'notiftest2_' || test_user2_id::text || '@test.com', 'NotifTest', 'User2', substring(test_user2_id::text from 1 for 8)) ON CONFLICT DO NOTHING; -- Set auth context to user1 PERFORM set_config('request.jwt.claims', json_build_object('sub', test_user1_id::text)::text, true); -- Create a tablo as user1 INSERT INTO public.tablos (owner_id, name, status, position) VALUES (test_user1_id, 'Notification Test Tablo', 'todo', 0) RETURNING id INTO test_tablo_id; -- Grant access to user2 INSERT INTO public.tablo_access (tablo_id, user_id, granted_by, is_active, is_admin) VALUES (test_tablo_id, test_user2_id, test_user1_id, true, false); -- Create a task INSERT INTO public.tasks (tablo_id, title, description, status, assignee_id, position) VALUES (test_tablo_id, 'Test Task', 'Test Description', 'todo', test_user2_id, 0) RETURNING id INTO test_task_id; -- Create an event INSERT INTO public.events (tablo_id, title, description, start_date, start_time, created_by) VALUES (test_tablo_id, 'Test Event', 'Test Event Description', CURRENT_DATE, '10:00', test_user1_id) RETURNING id INTO test_event_id; -- Create a note INSERT INTO public.notes (user_id, title, content) VALUES (test_user1_id, 'Test Note', 'Test Note Content') RETURNING id INTO test_note_id; -- Share note with user2 INSERT INTO public.note_access (note_id, user_id, is_active) VALUES (test_note_id, test_user2_id, true); -- Store test IDs for use in tests PERFORM set_config('test.user1_id', test_user1_id::text, true); PERFORM set_config('test.user2_id', test_user2_id::text, true); PERFORM set_config('test.tablo_id', test_tablo_id, true); PERFORM set_config('test.task_id', test_task_id, true); PERFORM set_config('test.event_id', test_event_id, true); PERFORM set_config('test.note_id', test_note_id, true); END $$; -- ============================================================================ -- Tablo Trigger Tests -- ============================================================================ -- Update tablo and check notification for user2 DO $$ BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); UPDATE public.tablos SET name = 'Updated Test Tablo' WHERE id = current_setting('test.tablo_id'); END $$; SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE entity_type = 'tablos' AND entity_id = current_setting('test.tablo_id') AND user_id = current_setting('test.user2_id')::uuid AND action_type = 'updated') > 0, 'Notification should be created for collaborator when tablo is updated' ); -- ============================================================================ -- Task Trigger Tests -- ============================================================================ -- Update task status DO $$ BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); UPDATE public.tasks SET status = 'in_progress' WHERE id = current_setting('test.task_id'); END $$; SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE entity_type = 'tasks' AND entity_id = current_setting('test.task_id') AND action_type = 'updated') > 0, 'Notification should be created when task is updated' ); -- ============================================================================ -- Event Trigger Tests -- ============================================================================ -- Update event DO $$ BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); UPDATE public.events SET title = 'Updated Test Event' WHERE id = current_setting('test.event_id'); END $$; SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE entity_type = 'events' AND entity_id = current_setting('test.event_id') AND action_type = 'updated') > 0, 'Notification should be created when event is updated' ); -- ============================================================================ -- Note Trigger Tests -- ============================================================================ -- Update note DO $$ BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); UPDATE public.notes SET title = 'Updated Test Note' WHERE id = current_setting('test.note_id'); END $$; SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE entity_type = 'notes' AND entity_id = current_setting('test.note_id') AND action_type = 'updated') > 0, 'Notification should be created when note is updated' ); -- ============================================================================ -- Tablo Access Trigger Tests -- ============================================================================ -- Grant access to a new user and verify notification DO $$ DECLARE test_user3_id uuid := gen_random_uuid(); BEGIN -- Insert test user3 INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) VALUES (test_user3_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notiftest3_' || test_user3_id::text || '@test.com', 'encrypted', now(), now(), now()) ON CONFLICT DO NOTHING; INSERT INTO public.profiles (id, email, first_name, last_name, short_user_id) VALUES (test_user3_id, 'notiftest3_' || test_user3_id::text || '@test.com', 'NotifTest', 'User3', substring(test_user3_id::text from 1 for 8)) ON CONFLICT DO NOTHING; PERFORM set_config('test.user3_id', test_user3_id::text, true); PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); -- Grant access INSERT INTO public.tablo_access (tablo_id, user_id, granted_by, is_active, is_admin) VALUES (current_setting('test.tablo_id'), test_user3_id, current_setting('test.user1_id')::uuid, true, false); END $$; SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE entity_type = 'tablo_access' AND user_id = current_setting('test.user3_id')::uuid AND action_type = 'created') > 0, 'Notification should be created when user is granted tablo access' ); -- ============================================================================ -- Tablo Invite Trigger Tests -- ============================================================================ -- Create an invite DO $$ BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); INSERT INTO public.tablo_invites (tablo_id, invited_email, invited_by, invite_token, is_pending) VALUES ( current_setting('test.tablo_id'), 'notiftest2_' || current_setting('test.user2_id') || '@test.com', current_setting('test.user1_id')::uuid, 'test-token-' || gen_random_uuid()::text, true ); END $$; SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE entity_type = 'tablo_invites' AND user_id = current_setting('test.user2_id')::uuid) > 0, 'Notification should be created when existing user is invited' ); -- Test: No notification for non-existent user DO $$ DECLARE count_before bigint; count_after bigint; BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); SELECT COUNT(*) INTO count_before FROM public.notifications; -- Invite a non-existent user INSERT INTO public.tablo_invites (tablo_id, invited_email, invited_by, invite_token, is_pending) VALUES ( current_setting('test.tablo_id'), 'nonexistent@example.com', current_setting('test.user1_id')::uuid, 'test-token-' || gen_random_uuid()::text, true ); SELECT COUNT(*) INTO count_after FROM public.notifications; PERFORM set_config('test.nonexistent_notif_diff', (count_after - count_before)::text, true); END $$; SELECT is( current_setting('test.nonexistent_notif_diff', true)::integer, 0, 'No notification should be created when inviting non-existent user' ); -- Test: Case-insensitive email matching DO $$ DECLARE count_before bigint; count_after bigint; BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); -- Count notifications for user2 before SELECT COUNT(*) INTO count_before FROM public.notifications WHERE entity_type = 'tablo_invites' AND user_id = current_setting('test.user2_id')::uuid; -- Invite with uppercase email to test case-insensitivity INSERT INTO public.tablo_invites (tablo_id, invited_email, invited_by, invite_token, is_pending) VALUES ( current_setting('test.tablo_id'), UPPER('notiftest2_' || current_setting('test.user2_id') || '@test.com'), current_setting('test.user1_id')::uuid, 'test-token-uppercase-' || gen_random_uuid()::text, true ); -- Count after SELECT COUNT(*) INTO count_after FROM public.notifications WHERE entity_type = 'tablo_invites' AND user_id = current_setting('test.user2_id')::uuid; PERFORM set_config('test.case_insensitive_notif_diff', (count_after - count_before)::text, true); END $$; SELECT is( current_setting('test.case_insensitive_notif_diff', true)::integer, 1, 'Notification should be created with case-insensitive email matching' ); -- ============================================================================ -- Multiple Notifications Tests (Exact Count Verification) -- ============================================================================ -- Setup: Create additional users with access to test tablo DO $$ DECLARE test_user4_id uuid := gen_random_uuid(); test_user5_id uuid := gen_random_uuid(); BEGIN -- Insert test users 4 and 5 INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) VALUES (test_user4_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notiftest4_' || test_user4_id::text || '@test.com', 'encrypted', now(), now(), now()), (test_user5_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notiftest5_' || test_user5_id::text || '@test.com', 'encrypted', now(), now(), now()) ON CONFLICT DO NOTHING; INSERT INTO public.profiles (id, email, first_name, last_name, short_user_id) VALUES (test_user4_id, 'notiftest4_' || test_user4_id::text || '@test.com', 'NotifTest', 'User4', substring(test_user4_id::text from 1 for 8)), (test_user5_id, 'notiftest5_' || test_user5_id::text || '@test.com', 'NotifTest', 'User5', substring(test_user5_id::text from 1 for 8)) ON CONFLICT DO NOTHING; PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); -- Grant access to users 4 and 5 on the test tablo INSERT INTO public.tablo_access (tablo_id, user_id, granted_by, is_active, is_admin) VALUES (current_setting('test.tablo_id'), test_user4_id, current_setting('test.user1_id')::uuid, true, false), (current_setting('test.tablo_id'), test_user5_id, current_setting('test.user1_id')::uuid, true, false); PERFORM set_config('test.user4_id', test_user4_id::text, true); PERFORM set_config('test.user5_id', test_user5_id::text, true); END $$; -- Test: Multiple users notified when tablo is updated -- Expected: user2, user3, user4, user5 should be notified (4 notifications), but NOT user1 (the actor) DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); -- Count notifications before update SELECT COUNT(*) INTO notif_count_before FROM public.notifications; -- Update tablo (user1 is the actor) UPDATE public.tablos SET name = 'Tablo Updated for Multiple Users Test' WHERE id = current_setting('test.tablo_id'); -- Count notifications after update SELECT COUNT(*) INTO notif_count_after FROM public.notifications; PERFORM set_config('test.multi_notif_count', (notif_count_after - notif_count_before)::text, true); END $$; SELECT is( current_setting('test.multi_notif_count', true)::integer, 4, 'Exactly 4 notifications should be created when tablo with 4 collaborators (excluding actor) is updated' ); -- Test: Verify each specific user received the notification SELECT is( (SELECT COUNT(*)::integer FROM public.notifications WHERE entity_type = 'tablos' AND entity_id = current_setting('test.tablo_id') AND message = 'Tablo "Tablo Updated for Multiple Users Test" was updated' AND user_id = current_setting('test.user2_id')::uuid), 1, 'User2 should receive exactly 1 notification for tablo update' ); SELECT is( (SELECT COUNT(*)::integer FROM public.notifications WHERE entity_type = 'tablos' AND entity_id = current_setting('test.tablo_id') AND message = 'Tablo "Tablo Updated for Multiple Users Test" was updated' AND user_id = current_setting('test.user4_id')::uuid), 1, 'User4 should receive exactly 1 notification for tablo update' ); SELECT is( (SELECT COUNT(*)::integer FROM public.notifications WHERE entity_type = 'tablos' AND entity_id = current_setting('test.tablo_id') AND message = 'Tablo "Tablo Updated for Multiple Users Test" was updated' AND user_id = current_setting('test.user5_id')::uuid), 1, 'User5 should receive exactly 1 notification for tablo update' ); SELECT is( (SELECT COUNT(*)::integer FROM public.notifications WHERE entity_type = 'tablos' AND entity_id = current_setting('test.tablo_id') AND message = 'Tablo "Tablo Updated for Multiple Users Test" was updated' AND user_id = current_setting('test.user3_id')::uuid), 1, 'User3 should receive exactly 1 notification for tablo update' ); -- Test: Actor (user1) should NOT receive a notification SELECT is( (SELECT COUNT(*)::integer FROM public.notifications WHERE entity_type = 'tablos' AND entity_id = current_setting('test.tablo_id') AND message = 'Tablo "Tablo Updated for Multiple Users Test" was updated' AND actor_id = current_setting('test.user1_id')::uuid AND user_id = current_setting('test.user1_id')::uuid), 0, 'Actor should not receive notification for their own action' ); -- Test: Multiple users notified when new task is created -- Expected: user1 (owner), user2, user4, user5 should be notified, but NOT user3 (who created the task) DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; new_task_id text; BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user3_id'))::text, true); -- Count notifications before creating task SELECT COUNT(*) INTO notif_count_before FROM public.notifications; -- User3 creates a new task INSERT INTO public.tasks (tablo_id, title, description, status, position) VALUES (current_setting('test.tablo_id'), 'Multi-User Test Task', 'Task to test multiple notifications', 'todo', 1) RETURNING id INTO new_task_id; -- Count notifications after creating task SELECT COUNT(*) INTO notif_count_after FROM public.notifications; PERFORM set_config('test.multi_task_notif_count', (notif_count_after - notif_count_before)::text, true); PERFORM set_config('test.multi_task_id', new_task_id, true); END $$; SELECT is( current_setting('test.multi_task_notif_count', true)::integer, 4, 'Exactly 4 notifications should be created when task is added to tablo with 4 collaborators (owner + 3 others, excluding actor)' ); -- Test: Multiple users notified when task is assigned -- Expected: Assignee (user5) + owner (user1) + collaborators (user2, user4) should be notified, but NOT user3 (the actor) DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user3_id'))::text, true); -- Count notifications before update SELECT COUNT(*) INTO notif_count_before FROM public.notifications; -- User3 assigns the task to user5 (without changing status to avoid message priority issues) UPDATE public.tasks SET assignee_id = current_setting('test.user5_id')::uuid WHERE id = current_setting('test.multi_task_id'); -- Count notifications after update SELECT COUNT(*) INTO notif_count_after FROM public.notifications; PERFORM set_config('test.task_assign_notif_count', (notif_count_after - notif_count_before)::text, true); END $$; SELECT is( current_setting('test.task_assign_notif_count', true)::integer, 4, 'Exactly 4 notifications should be created when task is assigned (owner + assignee + 2 other collaborators, excluding actor)' ); -- Test: Assignee (user5) receives notification about being assigned SELECT is( (SELECT COUNT(*)::integer FROM public.notifications WHERE entity_type = 'tasks' AND entity_id = current_setting('test.multi_task_id') AND user_id = current_setting('test.user5_id')::uuid AND message LIKE '%assigned%'), 1, 'Assignee should receive exactly 1 notification about task assignment' ); -- ============================================================================ -- Metadata and Message Tests -- ============================================================================ -- Test: Notification metadata should be valid JSONB SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE jsonb_typeof(metadata) = 'object') > 0, 'Notification metadata should be valid JSONB objects' ); -- Test: Messages should be non-empty SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE length(message) = 0) = 0, 'All notification messages should be non-empty' ); -- Test: Action types should be valid SELECT ok( (SELECT COUNT(*) FROM public.notifications WHERE action_type NOT IN ('created', 'updated')) = 0, 'All notifications should have valid action_type (created or updated)' ); -- ============================================================================ -- Edge Cases -- ============================================================================ -- Test: Soft delete should not create notification DO $$ DECLARE soft_delete_tablo_id text; count_before bigint; count_after bigint; BEGIN PERFORM set_config('request.jwt.claims', json_build_object('sub', current_setting('test.user1_id'))::text, true); -- Create a tablo and immediately soft delete it INSERT INTO public.tablos (owner_id, name, status, position) VALUES (current_setting('test.user1_id')::uuid, 'Soft Delete Test', 'todo', 0) RETURNING id INTO soft_delete_tablo_id; -- Count notifications before soft delete SELECT COUNT(*) INTO count_before FROM public.notifications WHERE entity_id = soft_delete_tablo_id; -- Soft delete UPDATE public.tablos SET deleted_at = now() WHERE id = soft_delete_tablo_id; -- Count notifications after soft delete SELECT COUNT(*) INTO count_after FROM public.notifications WHERE entity_id = soft_delete_tablo_id; -- Store results PERFORM set_config('test.notif_count_before', count_before::text, true); PERFORM set_config('test.notif_count_after', count_after::text, true); END $$; SELECT is( current_setting('test.notif_count_before', true), current_setting('test.notif_count_after', true), 'Soft delete should not create additional notifications' ); -- ============================================================================ -- NULL Actor Tests (Service Role / API Operations) -- ============================================================================ -- Test: Tablo update with NULL actor should notify all collaborators DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; BEGIN -- Clear auth context (simulates service role call with no auth.uid()) PERFORM set_config('request.jwt.claims', NULL, true); -- Count notifications before update SELECT COUNT(*) INTO notif_count_before FROM public.notifications; -- Update tablo without actor context UPDATE public.tablos SET name = 'Updated by Service Role' WHERE id = current_setting('test.tablo_id'); -- Count notifications after update SELECT COUNT(*) INTO notif_count_after FROM public.notifications; PERFORM set_config('test.null_actor_notif_count', (notif_count_after - notif_count_before)::text, true); END $$; SELECT is( current_setting('test.null_actor_notif_count', true)::integer, 5, 'With NULL actor, all collaborators (including owner) should be notified on tablo update (5 total: user1-5)' ); -- Test: Task creation with NULL actor DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; new_task_id text; BEGIN PERFORM set_config('request.jwt.claims', NULL, true); SELECT COUNT(*) INTO notif_count_before FROM public.notifications; -- Create task without actor context INSERT INTO public.tasks (tablo_id, title, description, status, position) VALUES (current_setting('test.tablo_id'), 'Service Role Task', 'Created via service role', 'todo', 2) RETURNING id INTO new_task_id; SELECT COUNT(*) INTO notif_count_after FROM public.notifications; PERFORM set_config('test.null_actor_task_notif_count', (notif_count_after - notif_count_before)::text, true); PERFORM set_config('test.null_actor_task_id', new_task_id, true); END $$; SELECT is( current_setting('test.null_actor_task_notif_count', true)::integer, 5, 'With NULL actor, all tablo members should be notified on task creation (owner + 4 collaborators)' ); -- Test: Tablo access grant with NULL actor DO $$ DECLARE test_user6_id uuid := gen_random_uuid(); notif_count_before bigint; notif_count_after bigint; BEGIN -- Insert test user6 INSERT INTO auth.users (id, instance_id, aud, role, email, encrypted_password, email_confirmed_at, created_at, updated_at) VALUES (test_user6_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notiftest6_' || test_user6_id::text || '@test.com', 'encrypted', now(), now(), now()) ON CONFLICT DO NOTHING; INSERT INTO public.profiles (id, email, first_name, last_name, short_user_id) VALUES (test_user6_id, 'notiftest6_' || test_user6_id::text || '@test.com', 'NotifTest', 'User6', substring(test_user6_id::text from 1 for 8)) ON CONFLICT DO NOTHING; PERFORM set_config('test.user6_id', test_user6_id::text, true); PERFORM set_config('request.jwt.claims', NULL, true); SELECT COUNT(*) INTO notif_count_before FROM public.notifications WHERE user_id = test_user6_id; -- Grant access without actor context INSERT INTO public.tablo_access (tablo_id, user_id, granted_by, is_active, is_admin) VALUES (current_setting('test.tablo_id'), test_user6_id, current_setting('test.user1_id')::uuid, true, false); SELECT COUNT(*) INTO notif_count_after FROM public.notifications WHERE user_id = test_user6_id; PERFORM set_config('test.null_actor_access_notif_diff', (notif_count_after - notif_count_before)::text, true); END $$; SELECT is( current_setting('test.null_actor_access_notif_diff', true)::integer, 1, 'With NULL actor, user should still be notified when granted tablo access' ); -- Test: Tablo invite with NULL actor (simulating API invite) DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; BEGIN PERFORM set_config('request.jwt.claims', NULL, true); -- Count notifications for user6 before invite SELECT COUNT(*) INTO notif_count_before FROM public.notifications WHERE user_id = current_setting('test.user6_id')::uuid AND entity_type = 'tablo_invites'; -- Invite user6 without actor context (via service role) INSERT INTO public.tablo_invites (tablo_id, invited_email, invited_by, invite_token, is_pending) VALUES ( current_setting('test.tablo_id'), 'notiftest6_' || current_setting('test.user6_id') || '@test.com', current_setting('test.user1_id')::uuid, 'test-token-null-actor-' || gen_random_uuid()::text, true ); -- Count after SELECT COUNT(*) INTO notif_count_after FROM public.notifications WHERE user_id = current_setting('test.user6_id')::uuid AND entity_type = 'tablo_invites'; PERFORM set_config('test.null_actor_invite_notif_diff', (notif_count_after - notif_count_before)::text, true); END $$; SELECT is( current_setting('test.null_actor_invite_notif_diff', true)::integer, 1, 'With NULL actor, invited user should still receive notification (API-initiated invite)' ); -- Test: Event creation with NULL actor DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; new_event_id text; BEGIN PERFORM set_config('request.jwt.claims', NULL, true); SELECT COUNT(*) INTO notif_count_before FROM public.notifications; -- Create event without actor context INSERT INTO public.events (tablo_id, title, description, start_date, start_time, created_by) VALUES (current_setting('test.tablo_id'), 'Service Role Event', 'Event via service role', CURRENT_DATE, '14:00', current_setting('test.user1_id')::uuid) RETURNING id INTO new_event_id; SELECT COUNT(*) INTO notif_count_after FROM public.notifications; PERFORM set_config('test.null_actor_event_notif_count', (notif_count_after - notif_count_before)::text, true); END $$; SELECT is( current_setting('test.null_actor_event_notif_count', true)::integer, 6, 'With NULL actor, all tablo members should be notified on event creation (owner + 5 collaborators including user6)' ); -- Test: Note update with NULL actor DO $$ DECLARE notif_count_before bigint; notif_count_after bigint; BEGIN PERFORM set_config('request.jwt.claims', NULL, true); SELECT COUNT(*) INTO notif_count_before FROM public.notifications; -- Update note without actor context UPDATE public.notes SET content = 'Updated by service role' WHERE id = current_setting('test.note_id'); SELECT COUNT(*) INTO notif_count_after FROM public.notifications; PERFORM set_config('test.null_actor_note_notif_count', (notif_count_after - notif_count_before)::text, true); END $$; SELECT is( current_setting('test.null_actor_note_notif_count', true)::integer, 2, 'With NULL actor, note owner (user1) and collaborator (user2) should both be notified on note update' ); select * from finish(); rollback;