xtablo-source/supabase/tests/database/09_notifications.test.sql
2025-11-16 20:34:33 +01:00

864 lines
35 KiB
PL/PgSQL

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;