Add notifications
This commit is contained in:
parent
a49dec34ff
commit
766364def4
4 changed files with 1185 additions and 4 deletions
|
|
@ -0,0 +1,331 @@
|
|||
-- Create notifications table
|
||||
CREATE TABLE IF NOT EXISTS public.notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
actor_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id TEXT NOT NULL,
|
||||
action_type TEXT NOT NULL CHECK (action_type IN ('created', 'updated')),
|
||||
message TEXT NOT NULL,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
read_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.notifications IS 'Stores user notifications triggered by changes to major tables';
|
||||
COMMENT ON COLUMN public.notifications.user_id IS 'Recipient of the notification';
|
||||
COMMENT ON COLUMN public.notifications.actor_id IS 'User who triggered the action (can be NULL if user deleted)';
|
||||
COMMENT ON COLUMN public.notifications.entity_type IS 'Type of entity: tablo, task, event, note, tablo_access, tablo_invite';
|
||||
COMMENT ON COLUMN public.notifications.entity_id IS 'ID of the entity that changed';
|
||||
COMMENT ON COLUMN public.notifications.action_type IS 'Operation performed: created or updated';
|
||||
COMMENT ON COLUMN public.notifications.message IS 'Human-readable notification message';
|
||||
COMMENT ON COLUMN public.notifications.metadata IS 'Additional context as JSONB (old/new values, etc.)';
|
||||
COMMENT ON COLUMN public.notifications.read_at IS 'When the user read the notification (NULL = unread)';
|
||||
|
||||
-- Create indexes for efficient querying
|
||||
CREATE INDEX idx_notifications_user_id ON public.notifications(user_id);
|
||||
CREATE INDEX idx_notifications_entity_type ON public.notifications(entity_type);
|
||||
CREATE INDEX idx_notifications_entity_id ON public.notifications(entity_id);
|
||||
CREATE INDEX idx_notifications_read_at ON public.notifications(read_at) WHERE read_at IS NULL;
|
||||
CREATE INDEX idx_notifications_created_at ON public.notifications(created_at DESC);
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS Policy: Users can only view their own notifications
|
||||
CREATE POLICY "Users can view their own notifications"
|
||||
ON public.notifications
|
||||
FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- RLS Policy: Users can mark their own notifications as read
|
||||
CREATE POLICY "Users can mark their own notifications as read"
|
||||
ON public.notifications
|
||||
FOR UPDATE
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id AND read_at IS NOT NULL);
|
||||
|
||||
-- Create a single generic trigger function to handle all table notifications
|
||||
CREATE OR REPLACE FUNCTION public.notify_users()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
affected_users UUID[];
|
||||
affected_user UUID;
|
||||
actor UUID;
|
||||
action TEXT;
|
||||
msg TEXT;
|
||||
meta JSONB;
|
||||
entity_type_name TEXT;
|
||||
entity_identifier TEXT;
|
||||
BEGIN
|
||||
-- Determine if this is an INSERT or UPDATE
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
action := 'created';
|
||||
ELSE
|
||||
action := 'updated';
|
||||
-- Skip if soft delete happened (only for tables with deleted_at column)
|
||||
IF TG_TABLE_NAME IN ('tablos', 'events', 'notes', 'event_types') THEN
|
||||
IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Get the actor (current user)
|
||||
actor := auth.uid();
|
||||
|
||||
-- Set entity type and ID based on the table
|
||||
entity_type_name := TG_TABLE_NAME;
|
||||
|
||||
-- Determine entity ID and affected users based on table
|
||||
CASE TG_TABLE_NAME
|
||||
WHEN 'tablos' THEN
|
||||
entity_identifier := NEW.id;
|
||||
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
msg := 'New tablo "' || NEW.name || '" was created';
|
||||
-- Notify owner (but not if they are the creator)
|
||||
IF NEW.owner_id != actor THEN
|
||||
affected_users := ARRAY[NEW.owner_id];
|
||||
END IF;
|
||||
ELSE
|
||||
msg := 'Tablo "' || NEW.name || '" was updated';
|
||||
-- Notify owner and all collaborators
|
||||
affected_users := ARRAY(
|
||||
SELECT DISTINCT user_id
|
||||
FROM public.tablo_access
|
||||
WHERE tablo_id = NEW.id
|
||||
AND is_active = true
|
||||
AND user_id != actor
|
||||
UNION
|
||||
SELECT NEW.owner_id
|
||||
WHERE NEW.owner_id != actor
|
||||
);
|
||||
END IF;
|
||||
|
||||
meta := jsonb_build_object(
|
||||
'tablo_name', NEW.name,
|
||||
'status', NEW.status,
|
||||
'color', NEW.color
|
||||
);
|
||||
|
||||
WHEN 'tasks' THEN
|
||||
entity_identifier := NEW.id;
|
||||
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
msg := 'New task "' || NEW.title || '" was created';
|
||||
ELSE
|
||||
IF OLD.status != NEW.status THEN
|
||||
msg := 'Task "' || NEW.title || '" status changed to ' || NEW.status;
|
||||
ELSIF OLD.assignee_id != NEW.assignee_id OR (OLD.assignee_id IS NULL AND NEW.assignee_id IS NOT NULL) THEN
|
||||
msg := 'You were assigned to task "' || NEW.title || '"';
|
||||
ELSE
|
||||
msg := 'Task "' || NEW.title || '" was updated';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Notify tablo collaborators and assignee
|
||||
affected_users := ARRAY(
|
||||
SELECT DISTINCT user_id
|
||||
FROM public.tablo_access
|
||||
WHERE tablo_id = NEW.tablo_id
|
||||
AND is_active = true
|
||||
AND user_id != actor
|
||||
UNION
|
||||
SELECT t.owner_id
|
||||
FROM public.tablos t
|
||||
WHERE t.id = NEW.tablo_id
|
||||
AND t.owner_id != actor
|
||||
UNION
|
||||
SELECT NEW.assignee_id
|
||||
WHERE NEW.assignee_id IS NOT NULL
|
||||
AND NEW.assignee_id != actor
|
||||
);
|
||||
|
||||
meta := jsonb_build_object(
|
||||
'task_title', NEW.title,
|
||||
'status', NEW.status,
|
||||
'tablo_id', NEW.tablo_id,
|
||||
'assignee_id', NEW.assignee_id
|
||||
);
|
||||
|
||||
WHEN 'events' THEN
|
||||
entity_identifier := NEW.id;
|
||||
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
msg := 'New event "' || NEW.title || '" was created';
|
||||
ELSE
|
||||
msg := 'Event "' || NEW.title || '" was updated';
|
||||
END IF;
|
||||
|
||||
-- Notify tablo collaborators
|
||||
affected_users := ARRAY(
|
||||
SELECT DISTINCT user_id
|
||||
FROM public.tablo_access
|
||||
WHERE tablo_id = NEW.tablo_id
|
||||
AND is_active = true
|
||||
AND user_id != actor
|
||||
UNION
|
||||
SELECT t.owner_id
|
||||
FROM public.tablos t
|
||||
WHERE t.id = NEW.tablo_id
|
||||
AND t.owner_id != actor
|
||||
UNION
|
||||
SELECT NEW.created_by
|
||||
WHERE NEW.created_by != actor
|
||||
);
|
||||
|
||||
meta := jsonb_build_object(
|
||||
'event_title', NEW.title,
|
||||
'start_date', NEW.start_date,
|
||||
'start_time', NEW.start_time,
|
||||
'tablo_id', NEW.tablo_id
|
||||
);
|
||||
|
||||
WHEN 'notes' THEN
|
||||
entity_identifier := NEW.id;
|
||||
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
msg := 'New note "' || NEW.title || '" was created';
|
||||
ELSE
|
||||
msg := 'Note "' || NEW.title || '" was updated';
|
||||
END IF;
|
||||
|
||||
-- Notify note owner and users with access
|
||||
affected_users := ARRAY(
|
||||
SELECT DISTINCT user_id
|
||||
FROM public.note_access
|
||||
WHERE note_id = NEW.id
|
||||
AND is_active = true
|
||||
AND user_id != actor
|
||||
UNION
|
||||
SELECT NEW.user_id
|
||||
WHERE NEW.user_id != actor
|
||||
);
|
||||
|
||||
meta := jsonb_build_object(
|
||||
'note_title', NEW.title,
|
||||
'note_owner', NEW.user_id
|
||||
);
|
||||
|
||||
WHEN 'tablo_access' THEN
|
||||
entity_identifier := NEW.id::TEXT;
|
||||
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
msg := 'You were granted access to a tablo';
|
||||
ELSE
|
||||
IF OLD.is_admin != NEW.is_admin AND NEW.is_admin = true THEN
|
||||
msg := 'You were promoted to admin on a tablo';
|
||||
ELSIF OLD.is_active != NEW.is_active AND NEW.is_active = false THEN
|
||||
msg := 'Your access to a tablo was revoked';
|
||||
ELSE
|
||||
msg := 'Your tablo access was updated';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Notify the user being granted/modified access
|
||||
IF NEW.user_id != actor THEN
|
||||
affected_users := ARRAY[NEW.user_id];
|
||||
END IF;
|
||||
|
||||
meta := jsonb_build_object(
|
||||
'tablo_id', NEW.tablo_id,
|
||||
'is_admin', NEW.is_admin,
|
||||
'is_active', NEW.is_active,
|
||||
'granted_by', NEW.granted_by
|
||||
);
|
||||
|
||||
WHEN 'tablo_invites' THEN
|
||||
entity_identifier := NEW.id::TEXT;
|
||||
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
msg := 'You were invited to collaborate on a tablo';
|
||||
ELSE
|
||||
IF OLD.is_pending != NEW.is_pending AND NEW.is_pending = false THEN
|
||||
msg := 'Your tablo invitation status changed';
|
||||
ELSE
|
||||
msg := 'Your tablo invitation was updated';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Find the user by email and notify them
|
||||
affected_users := ARRAY(
|
||||
SELECT id
|
||||
FROM auth.users
|
||||
WHERE email = NEW.invited_email
|
||||
AND id != actor
|
||||
);
|
||||
|
||||
meta := jsonb_build_object(
|
||||
'tablo_id', NEW.tablo_id,
|
||||
'invited_email', NEW.invited_email,
|
||||
'invited_by', NEW.invited_by,
|
||||
'is_pending', NEW.is_pending
|
||||
);
|
||||
|
||||
ELSE
|
||||
-- Unknown table, skip
|
||||
RETURN NEW;
|
||||
END CASE;
|
||||
|
||||
-- Insert notifications for all affected users
|
||||
IF affected_users IS NOT NULL AND array_length(affected_users, 1) > 0 THEN
|
||||
FOREACH affected_user IN ARRAY affected_users
|
||||
LOOP
|
||||
INSERT INTO public.notifications (
|
||||
user_id,
|
||||
actor_id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
action_type,
|
||||
message,
|
||||
metadata
|
||||
) VALUES (
|
||||
affected_user,
|
||||
actor,
|
||||
entity_type_name,
|
||||
entity_identifier,
|
||||
action,
|
||||
msg,
|
||||
meta
|
||||
);
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
COMMENT ON FUNCTION public.notify_users() IS 'Single trigger function that handles notifications for all major tables';
|
||||
|
||||
-- Create triggers on all major tables using the same function
|
||||
CREATE TRIGGER notify_users_on_tablos
|
||||
AFTER INSERT OR UPDATE ON public.tablos
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.notify_users();
|
||||
|
||||
CREATE TRIGGER notify_users_on_tasks
|
||||
AFTER INSERT OR UPDATE ON public.tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.notify_users();
|
||||
|
||||
CREATE TRIGGER notify_users_on_events
|
||||
AFTER INSERT OR UPDATE ON public.events
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.notify_users();
|
||||
|
||||
CREATE TRIGGER notify_users_on_notes
|
||||
AFTER INSERT OR UPDATE ON public.notes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.notify_users();
|
||||
|
||||
CREATE TRIGGER notify_users_on_tablo_access
|
||||
AFTER INSERT OR UPDATE ON public.tablo_access
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.notify_users();
|
||||
|
||||
CREATE TRIGGER notify_users_on_tablo_invites
|
||||
AFTER INSERT OR UPDATE ON public.tablo_invites
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.notify_users();
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ This directory contains comprehensive pgTAP tests for the Xtablo database, cover
|
|||
|
||||
## Test Files
|
||||
|
||||
The tests are organized into 8 files, covering different aspects of the database:
|
||||
The tests are organized into 10 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)
|
||||
|
|
@ -19,6 +19,8 @@ The tests are organized into 8 files, covering different aspects of the database
|
|||
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
|
||||
9. **09_notifications.test.sql** - Tests notifications table structure, triggers, and trigger behavior
|
||||
10. **10_rls_policies_notifications.test.sql** - Tests RLS policies for notifications table
|
||||
|
||||
## Running Tests
|
||||
|
||||
|
|
@ -47,6 +49,7 @@ supabase test db --verbose
|
|||
## Test Coverage
|
||||
|
||||
### Tables Tested
|
||||
|
||||
- profiles
|
||||
- feedbacks
|
||||
- tablos
|
||||
|
|
@ -56,32 +59,40 @@ supabase test db --verbose
|
|||
- notes
|
||||
- shared_notes
|
||||
- note_access
|
||||
- notifications
|
||||
|
||||
### 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
|
||||
- ✅ Notification visibility and update permissions
|
||||
|
||||
### Triggers Tested
|
||||
|
||||
- ✅ Auto-creation of tablo_access for owners
|
||||
- ✅ Profile last_signed_in updates
|
||||
- ✅ Tablo invite status updates on login
|
||||
- ✅ Stripe subscription profile updates
|
||||
- ✅ Notification creation for all major table changes (tablos, tasks, events, notes, tablo_access, tablo_invites)
|
||||
|
||||
### 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
|
||||
|
|
@ -95,8 +106,10 @@ 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
|
||||
supabase/tests/database/09_notifications.test.sql .. ok
|
||||
supabase/tests/database/10_rls_policies_notifications.test.sql .. ok
|
||||
All tests successful.
|
||||
Files=8, Tests=295, 5 wallclock secs
|
||||
Files=10, Tests=374, 1 wallclock secs
|
||||
Result: PASS
|
||||
```
|
||||
|
||||
|
|
@ -105,6 +118,7 @@ Result: PASS
|
|||
### 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
|
||||
|
|
@ -131,6 +145,7 @@ 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`
|
||||
|
|
@ -140,7 +155,7 @@ Use the pgTAP documentation for available test functions: https://pgtap.org/docu
|
|||
|
||||
## Total Test Count
|
||||
|
||||
- **295 tests** across 8 test files
|
||||
- **374 tests** across 10 test files (63 notifications schema/triggers + 19 notifications RLS)
|
||||
- Comprehensive coverage of all database components
|
||||
- Security-focused testing for RLS and permissions
|
||||
|
||||
- Full notification system coverage including triggers and RLS
|
||||
|
|
|
|||
599
supabase/tests/database/09_notifications.test.sql
Normal file
599
supabase/tests/database/09_notifications.test.sql
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
begin;
|
||||
select plan(63); -- Total number of tests (54 original + 9 multiple notifications 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 user is invited'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- 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'
|
||||
);
|
||||
|
||||
select * from finish();
|
||||
rollback;
|
||||
236
supabase/tests/database/10_rls_policies_notifications.test.sql
Normal file
236
supabase/tests/database/10_rls_policies_notifications.test.sql
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
begin;
|
||||
select plan(19); -- Total number of tests
|
||||
|
||||
-- ============================================================================
|
||||
-- RLS Enabled Test
|
||||
-- ============================================================================
|
||||
|
||||
SELECT is(
|
||||
(SELECT relrowsecurity FROM pg_class WHERE relname = 'notifications' AND relnamespace = 'public'::regnamespace),
|
||||
true,
|
||||
'RLS should be enabled on notifications table'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Notifications Table RLS Policies
|
||||
-- ============================================================================
|
||||
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM pg_policies WHERE tablename = 'notifications' AND policyname = 'Users can view their own notifications') > 0,
|
||||
'Policy for viewing own notifications should exist'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM pg_policies WHERE tablename = 'notifications' AND policyname = 'Users can mark their own notifications as read') > 0,
|
||||
'Policy for marking notifications as read should exist'
|
||||
);
|
||||
|
||||
-- Test policy commands
|
||||
SELECT is(
|
||||
(SELECT cmd FROM pg_policies WHERE tablename = 'notifications' AND policyname = 'Users can view their own notifications' LIMIT 1),
|
||||
'SELECT',
|
||||
'View notifications policy should be for SELECT'
|
||||
);
|
||||
|
||||
SELECT is(
|
||||
(SELECT cmd FROM pg_policies WHERE tablename = 'notifications' AND policyname = 'Users can mark their own notifications as read' LIMIT 1),
|
||||
'UPDATE',
|
||||
'Mark as read policy should be for UPDATE'
|
||||
);
|
||||
|
||||
-- Test policy uses auth.uid() for isolation
|
||||
SELECT ok(
|
||||
(SELECT qual::text FROM pg_policies
|
||||
WHERE tablename = 'notifications'
|
||||
AND policyname = 'Users can view their own notifications'
|
||||
LIMIT 1) LIKE '%auth.uid()%',
|
||||
'SELECT policy should use auth.uid() for user isolation'
|
||||
);
|
||||
|
||||
-- Test UPDATE policy allows updating read_at
|
||||
SELECT ok(
|
||||
(SELECT with_check::text FROM pg_policies
|
||||
WHERE tablename = 'notifications'
|
||||
AND policyname = 'Users can mark their own notifications as read'
|
||||
LIMIT 1) LIKE '%read_at%',
|
||||
'UPDATE policy should check read_at field'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Notifications Behavior Tests with Mock Data
|
||||
-- ============================================================================
|
||||
|
||||
-- Create test users and generate some notifications
|
||||
DO $$
|
||||
DECLARE
|
||||
user1_id uuid := gen_random_uuid();
|
||||
user2_id uuid := gen_random_uuid();
|
||||
test_tablo_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
|
||||
(user1_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notifrlsuser1_' || user1_id::text || '@test.com', 'encrypted', now(), now(), now()),
|
||||
(user2_id, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'notifrlsuser2_' || 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
|
||||
(user1_id, 'notifrlsuser1_' || user1_id::text || '@test.com', 'NotifRLS', 'User1', substring(user1_id::text from 1 for 8)),
|
||||
(user2_id, 'notifrlsuser2_' || user2_id::text || '@test.com', 'NotifRLS', 'User2', substring(user2_id::text from 1 for 8))
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Set auth context to user1 for creating test data
|
||||
PERFORM set_config('request.jwt.claims', json_build_object('sub', user1_id::text)::text, true);
|
||||
|
||||
-- Create a tablo as user1
|
||||
INSERT INTO public.tablos (owner_id, name, status, position)
|
||||
VALUES (user1_id, 'RLS 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, user2_id, user1_id, true, false);
|
||||
|
||||
-- Update tablo to trigger notification for user2
|
||||
UPDATE public.tablos
|
||||
SET name = 'RLS Test Tablo Updated'
|
||||
WHERE id = test_tablo_id;
|
||||
|
||||
-- Store test IDs
|
||||
PERFORM set_config('test.rls_user1_id', user1_id::text, true);
|
||||
PERFORM set_config('test.rls_user2_id', user2_id::text, true);
|
||||
PERFORM set_config('test.rls_tablo_id', test_tablo_id, true);
|
||||
END $$;
|
||||
|
||||
-- Test: Notifications exist for test users
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM public.notifications
|
||||
WHERE user_id = current_setting('test.rls_user2_id')::uuid) > 0,
|
||||
'Notifications should exist for test user2'
|
||||
);
|
||||
|
||||
-- Test: User2 received notification about tablo update
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM public.notifications
|
||||
WHERE user_id = current_setting('test.rls_user2_id')::uuid
|
||||
AND entity_type = 'tablos'
|
||||
AND entity_id = current_setting('test.rls_tablo_id')) > 0,
|
||||
'User2 should have notification for tablo update'
|
||||
);
|
||||
|
||||
-- Test: Actor (user1) did not receive notification for their own action
|
||||
SELECT is(
|
||||
(SELECT COUNT(*)::integer FROM public.notifications
|
||||
WHERE user_id = current_setting('test.rls_user1_id')::uuid
|
||||
AND actor_id = current_setting('test.rls_user1_id')::uuid
|
||||
AND entity_id = current_setting('test.rls_tablo_id')),
|
||||
0,
|
||||
'User should not receive notifications for their own actions'
|
||||
);
|
||||
|
||||
-- Test: Notifications have proper structure
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM public.notifications
|
||||
WHERE user_id = current_setting('test.rls_user2_id')::uuid
|
||||
AND message IS NOT NULL
|
||||
AND length(message) > 0) > 0,
|
||||
'Notifications should have non-empty messages'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM public.notifications
|
||||
WHERE user_id = current_setting('test.rls_user2_id')::uuid
|
||||
AND metadata IS NOT NULL
|
||||
AND jsonb_typeof(metadata) = 'object') > 0,
|
||||
'Notifications should have valid JSONB metadata'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM public.notifications
|
||||
WHERE user_id = current_setting('test.rls_user2_id')::uuid
|
||||
AND action_type IN ('created', 'updated')) > 0,
|
||||
'Notifications should have valid action_type'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Test Notification Updates (Mark as Read)
|
||||
-- ============================================================================
|
||||
|
||||
-- Test: Update read_at on a notification
|
||||
DO $$
|
||||
DECLARE
|
||||
test_notif_id uuid;
|
||||
BEGIN
|
||||
-- Get an unread notification for user2
|
||||
SELECT id INTO test_notif_id
|
||||
FROM public.notifications
|
||||
WHERE user_id = current_setting('test.rls_user2_id')::uuid
|
||||
AND read_at IS NULL
|
||||
LIMIT 1;
|
||||
|
||||
IF test_notif_id IS NOT NULL THEN
|
||||
-- Update the notification
|
||||
UPDATE public.notifications
|
||||
SET read_at = now()
|
||||
WHERE id = test_notif_id;
|
||||
|
||||
PERFORM set_config('test.updated_notif_id', test_notif_id::text, true);
|
||||
PERFORM set_config('test.notif_was_updated', 'true', true);
|
||||
ELSE
|
||||
PERFORM set_config('test.notif_was_updated', 'false', true);
|
||||
PERFORM set_config('test.updated_notif_id', '00000000-0000-0000-0000-000000000000', true);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Verify the update worked
|
||||
SELECT ok(
|
||||
CASE
|
||||
WHEN current_setting('test.notif_was_updated', true) = 'true' THEN
|
||||
(SELECT read_at IS NOT NULL
|
||||
FROM public.notifications
|
||||
WHERE id = current_setting('test.updated_notif_id', true)::uuid
|
||||
LIMIT 1)
|
||||
ELSE true -- Skip if no notification was found
|
||||
END,
|
||||
'Notification should be updatable with read_at timestamp'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Test Data Isolation
|
||||
-- ============================================================================
|
||||
|
||||
-- Test: Count notifications for user1 vs user2
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM public.notifications WHERE user_id = current_setting('test.rls_user1_id')::uuid) >= 0,
|
||||
'User1 notifications count should be valid'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT COUNT(*) FROM public.notifications WHERE user_id = current_setting('test.rls_user2_id')::uuid) > 0,
|
||||
'User2 should have at least one notification'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- Foreign Key Constraints Tests
|
||||
-- ============================================================================
|
||||
|
||||
SELECT has_fk('public', 'notifications', 'notifications should have foreign key constraints');
|
||||
|
||||
-- Test that notifications.user_id references auth.users.id
|
||||
SELECT fk_ok(
|
||||
'public', 'notifications', 'user_id',
|
||||
'auth', 'users', 'id',
|
||||
'notifications.user_id should reference auth.users.id'
|
||||
);
|
||||
|
||||
-- Test that notifications.actor_id references auth.users.id
|
||||
SELECT fk_ok(
|
||||
'public', 'notifications', 'actor_id',
|
||||
'auth', 'users', 'id',
|
||||
'notifications.actor_id should reference auth.users.id'
|
||||
);
|
||||
|
||||
select * from finish();
|
||||
rollback;
|
||||
Loading…
Reference in a new issue