Migration 018 adds public.dissolve_collective(uuid): - admin-only; non-admin (member/guest/non-member) → errcode P0002 - single DELETE from public.collectives; all child tables cascade (collective_members, collective_invitations, shopping_lists → shopping_items, task_lists → tasks, notes, item_frequency, sync_conflicts — verified by audit, no schema change needed) /collective/manage gains a "Danger zone" card (admin-only) with a Dissolve button. The confirmation modal loads live counts of lists, tasks and notes, displays them in the warning, and requires the user to type the collective name exactly before the confirm button is enabled. On success, locals stores drop the collective and the user is sent to /lists (next collective) or /onboarding (none left). pgTAP 011 (8 assertions): RPC exists, member/guest/non-member all rejected with P0002, admin succeeds, collective row gone, child list cascade-deleted, members cascade-deleted. Playwright D-01..D-03 cover the happy path, the type-the-name guard, and member visibility. Auxiliary collective fixture (NOT the seed) per test — the seed collective must survive every run for downstream suites. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
111 lines
4.6 KiB
PL/PgSQL
111 lines
4.6 KiB
PL/PgSQL
-- pgTAP: dissolve_collective() RPC — Fase 10.3 (CU-H08)
|
|
-- Run with: psql -U postgres -d postgres -f supabase/tests/011_dissolve_collective.sql
|
|
--
|
|
-- Covers:
|
|
-- * admin disolves: collective + all content cascade-deleted
|
|
-- * member rejected (errcode P0002)
|
|
-- * guest rejected
|
|
-- * non-member rejected
|
|
|
|
CREATE EXTENSION IF NOT EXISTS pgtap;
|
|
|
|
BEGIN;
|
|
SELECT plan(8);
|
|
|
|
-- ── Existence ────────────────────────────────────────────────────────────────
|
|
SELECT has_function(
|
|
'public', 'dissolve_collective', ARRAY['uuid'],
|
|
'public.dissolve_collective(uuid) RPC exists'
|
|
);
|
|
|
|
-- ── Setup: one collective with content, three members ────────────────────────
|
|
INSERT INTO public.users (id, email, display_name, language, avatar_type)
|
|
VALUES
|
|
('e2000000-0000-0000-0000-000000000001', 'diss-admin@local', 'Diss Admin', 'es', 'initials'),
|
|
('e2000000-0000-0000-0000-000000000002', 'diss-member@local', 'Diss Member', 'es', 'initials'),
|
|
('e2000000-0000-0000-0000-000000000003', 'diss-guest@local', 'Diss Guest', 'es', 'initials')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
-- Collective owned by Borja so we can delete the test admin user later.
|
|
INSERT INTO public.collectives (id, name, emoji, created_by)
|
|
VALUES ('cccccccc-3333-3333-3333-333333333333', 'Dissolve test', '💥',
|
|
'22222222-2222-2222-2222-222222222222')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
INSERT INTO public.collective_members (collective_id, user_id, role)
|
|
VALUES
|
|
('cccccccc-3333-3333-3333-333333333333', 'e2000000-0000-0000-0000-000000000001', 'admin'),
|
|
('cccccccc-3333-3333-3333-333333333333', 'e2000000-0000-0000-0000-000000000002', 'member'),
|
|
('cccccccc-3333-3333-3333-333333333333', 'e2000000-0000-0000-0000-000000000003', 'guest')
|
|
ON CONFLICT (collective_id, user_id) DO NOTHING;
|
|
|
|
INSERT INTO public.shopping_lists (id, collective_id, name, status, created_by)
|
|
VALUES ('cccccccc-3333-3333-3333-3333deadbeef', 'cccccccc-3333-3333-3333-333333333333',
|
|
'List to nuke', 'active', 'e2000000-0000-0000-0000-000000000001')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
INSERT INTO public.shopping_items (list_id, name, sort_order, created_by)
|
|
VALUES ('cccccccc-3333-3333-3333-3333deadbeef', 'Milk', 1, 'e2000000-0000-0000-0000-000000000001');
|
|
|
|
-- ── Case 1: member tries to dissolve → P0002 ─────────────────────────────────
|
|
SELECT set_config('request.jwt.claim.sub', 'e2000000-0000-0000-0000-000000000002', true);
|
|
|
|
SELECT throws_ok(
|
|
$$ SELECT public.dissolve_collective('cccccccc-3333-3333-3333-333333333333') $$,
|
|
'P0002',
|
|
NULL,
|
|
'D-pgtap-a: member rejected with P0002'
|
|
);
|
|
|
|
-- ── Case 2: guest tries to dissolve → P0002 ──────────────────────────────────
|
|
SELECT set_config('request.jwt.claim.sub', 'e2000000-0000-0000-0000-000000000003', true);
|
|
|
|
SELECT throws_ok(
|
|
$$ SELECT public.dissolve_collective('cccccccc-3333-3333-3333-333333333333') $$,
|
|
'P0002',
|
|
NULL,
|
|
'D-pgtap-b: guest rejected with P0002'
|
|
);
|
|
|
|
-- ── Case 3: non-member rejected ──────────────────────────────────────────────
|
|
SELECT set_config('request.jwt.claim.sub', '55555555-5555-5555-5555-555555555555', true); -- Eva
|
|
|
|
SELECT throws_ok(
|
|
$$ SELECT public.dissolve_collective('cccccccc-3333-3333-3333-333333333333') $$,
|
|
'P0002',
|
|
NULL,
|
|
'D-pgtap-c: non-member rejected with P0002'
|
|
);
|
|
|
|
-- ── Case 4: admin succeeds + cascade ─────────────────────────────────────────
|
|
SELECT set_config('request.jwt.claim.sub', 'e2000000-0000-0000-0000-000000000001', true);
|
|
|
|
SELECT lives_ok(
|
|
$$ SELECT public.dissolve_collective('cccccccc-3333-3333-3333-333333333333') $$,
|
|
'D-pgtap-d: admin can dissolve without error'
|
|
);
|
|
|
|
SELECT is(
|
|
(SELECT count(*)::int FROM public.collectives
|
|
WHERE id = 'cccccccc-3333-3333-3333-333333333333'),
|
|
0,
|
|
'D-pgtap-d: collective row gone'
|
|
);
|
|
|
|
SELECT is(
|
|
(SELECT count(*)::int FROM public.shopping_lists
|
|
WHERE id = 'cccccccc-3333-3333-3333-3333deadbeef'),
|
|
0,
|
|
'D-pgtap-d: child shopping_list cascade-deleted'
|
|
);
|
|
|
|
SELECT is(
|
|
(SELECT count(*)::int FROM public.collective_members
|
|
WHERE collective_id = 'cccccccc-3333-3333-3333-333333333333'),
|
|
0,
|
|
'D-pgtap-d: collective_members cascade-deleted'
|
|
);
|
|
|
|
SELECT * FROM finish();
|
|
ROLLBACK;
|