-- pgTAP: admin RPCs + server_settings + section_enabled() server layer (Fase 13.2) -- Run with: psql -U postgres -d postgres -f supabase/tests/017_admin_rpcs.sql -- -- Verifies schema-level invariants on the new RPCs and the server_settings -- table. Runtime behaviour (denied-for-non-admin, audit writes, last-admin -- guard, hard-delete cascade) lives in the Vitest integration suite — pgTAP -- runs as postgres so the SECURITY DEFINER `is_server_admin` check passes -- vacuously, making these tests structural only. CREATE EXTENSION IF NOT EXISTS pgtap; BEGIN; SELECT plan(22); -- ── server_settings table ────────────────────────────────────────────────── SELECT has_table( 'public', 'server_settings', 'AR-T01: public.server_settings table exists' ); SELECT col_is_pk( 'public', 'server_settings', 'key', 'AR-T02: server_settings.key is the primary key' ); SELECT col_type_is( 'public', 'server_settings', 'value', 'jsonb', 'AR-T03: server_settings.value is jsonb' ); SELECT ok( (SELECT relrowsecurity FROM pg_class WHERE relname = 'server_settings' AND relnamespace = 'public'::regnamespace), 'AR-T04: RLS enabled on server_settings' ); -- Read-allowed to anyone (server-layer flags need to be evaluable from a -- non-admin session inside section_enabled). Writes are admin-only via RPC. SELECT results_eq( $$ SELECT cmd::text FROM pg_policies WHERE schemaname = 'public' AND tablename = 'server_settings' ORDER BY cmd $$, $$ VALUES ('SELECT') $$, 'AR-T05: server_settings has exactly one policy and it is SELECT-only' ); -- ── RPC existence + SECURITY DEFINER posture ─────────────────────────────── SELECT has_function( 'public', 'grant_server_admin', ARRAY['uuid'], 'AR-T06: grant_server_admin(uuid) exists' ); SELECT has_function( 'public', 'revoke_server_admin', ARRAY['uuid'], 'AR-T07: revoke_server_admin(uuid) exists' ); SELECT has_function( 'public', 'admin_list_collectives', ARRAY['text', 'integer'], 'AR-T08: admin_list_collectives(text, int) exists' ); SELECT has_function( 'public', 'admin_soft_delete_collective', ARRAY['uuid', 'text'], 'AR-T09: admin_soft_delete_collective(uuid, text) exists' ); SELECT has_function( 'public', 'admin_restore_collective', ARRAY['uuid'], 'AR-T10: admin_restore_collective(uuid) exists' ); SELECT has_function( 'public', 'admin_hard_delete_collective', ARRAY['uuid', 'boolean'], 'AR-T11: admin_hard_delete_collective(uuid, boolean) exists' ); SELECT has_function( 'public', 'admin_remove_member', ARRAY['uuid', 'uuid', 'text'], 'AR-T12: admin_remove_member(uuid, uuid, text) exists' ); SELECT has_function( 'public', 'admin_set_default_section', ARRAY['text', 'boolean'], 'AR-T13: admin_set_default_section(text, boolean) exists' ); -- Every admin_* function plus grant/revoke MUST be SECURITY DEFINER so the -- `if not is_server_admin() then raise` guard is the gate, not RLS. SELECT results_eq( $$ SELECT proname::text COLLATE "C" FROM pg_proc WHERE pronamespace = 'public'::regnamespace AND (proname LIKE 'admin\_%' OR proname IN ('grant_server_admin','revoke_server_admin')) AND prosecdef = true ORDER BY proname COLLATE "C" $$, $$ VALUES ('admin_hard_delete_collective'::text COLLATE "C"), ('admin_list_collectives'::text COLLATE "C"), ('admin_remove_member'::text COLLATE "C"), ('admin_restore_collective'::text COLLATE "C"), ('admin_set_default_section'::text COLLATE "C"), ('admin_soft_delete_collective'::text COLLATE "C"), ('grant_server_admin'::text COLLATE "C"), ('revoke_server_admin'::text COLLATE "C") $$, 'AR-T14: every privileged RPC is SECURITY DEFINER (exact list)' ); -- ── collectives.deleted_at column added by this migration ────────────────── SELECT has_column( 'public', 'collectives', 'deleted_at', 'AR-T15: collectives.deleted_at column exists' ); SELECT col_type_is( 'public', 'collectives', 'deleted_at', 'timestamp with time zone', 'AR-T16: collectives.deleted_at is timestamptz' ); -- ── section_enabled() extended with server layer ─────────────────────────── -- Behavioural assertions: with no server flag set, falls through to the -- collective/user/default layers (Fase 12 contract preserved). With a server -- flag of false, returns false regardless of any lower layer. -- Ensure a clean slate on every layer. DELETE FROM public.server_settings WHERE key = 'default_sections'; UPDATE public.users SET feature_flags = '{}'::jsonb WHERE id = '11111111-1111-1111-1111-111111111111'; UPDATE public.collectives SET feature_flags = '{}'::jsonb WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; -- AR-T17: server absent + everything else absent → true (default ON preserved). SELECT is( public.section_enabled( 'tasks', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' ), true, 'AR-T17: with no flags anywhere, section_enabled returns true' ); -- AR-T18: server OFF beats collective ON beats user ON. UPDATE public.users SET feature_flags = '{"notes": true}'::jsonb WHERE id = '11111111-1111-1111-1111-111111111111'; UPDATE public.collectives SET feature_flags = '{"notes": true}'::jsonb WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; INSERT INTO public.server_settings (key, value) VALUES ('default_sections', '{"notes": false}'::jsonb) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; SELECT is( public.section_enabled( 'notes', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' ), false, 'AR-T18: server OFF wins over collective ON and user ON' ); -- AR-T19: server ON over collective OFF — server is the top layer; if it -- explicitly turns a section ON we want that to win too. UPDATE public.collectives SET feature_flags = '{"notes": false}'::jsonb WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; UPDATE public.server_settings SET value = '{"notes": true}'::jsonb WHERE key = 'default_sections'; SELECT is( public.section_enabled( 'notes', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' ), true, 'AR-T19: server ON wins over collective OFF' ); -- AR-T20: server "no opinion" (key absent inside the JSONB) → fall through -- to collective layer. UPDATE public.server_settings SET value = '{}'::jsonb WHERE key = 'default_sections'; UPDATE public.collectives SET feature_flags = '{"notes": false}'::jsonb WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; SELECT is( public.section_enabled( 'notes', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' ), false, 'AR-T20: server with no opinion falls through to collective' ); -- AR-T21: a row missing entirely in server_settings still behaves like -- "no opinion". DELETE FROM public.server_settings WHERE key = 'default_sections'; UPDATE public.collectives SET feature_flags = '{}'::jsonb WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; UPDATE public.users SET feature_flags = '{"tasks": false}'::jsonb WHERE id = '11111111-1111-1111-1111-111111111111'; SELECT is( public.section_enabled( 'tasks', '11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' ), false, 'AR-T21: missing server_settings row equals no-opinion, user layer wins' ); -- AR-T22: the function is still STABLE (no behavioural side effects, planner -- can cache within a statement). SELECT volatility_is( 'public', 'section_enabled', ARRAY['text', 'uuid', 'uuid'], 'stable', 'AR-T22: section_enabled() remains STABLE after server-layer extension' ); SELECT * FROM finish(); ROLLBACK;