Files
collective-lists/supabase/tests/012_trash_rpcs.sql
Oier Bravo Urtasun 5b4ba9aaef feat(fase-10): trash drawer — restore + hard-delete RPCs (10.4)
Migration 020 (019 reserved per plan) introduces two SECURITY DEFINER
RPCs that back the trash drawer's per-row actions:

- restore_list(uuid): clears deleted_at; requires caller to be admin
  or member of the owning collective; rejects if the list is not
  currently in trash (defensive)
- hard_delete_list(uuid): permanently deletes a soft-deleted list;
  same role guard; rejects if the list is still active

Both close the existing UI gap where guest could in principle issue
the equivalent UPDATE (the FROM-UPDATE policy is is_active_member,
which excludes guest, but the surface area is broader than these two
intents — the RPCs make the contract explicit).

The lists drawer already exposed the Restore + Delete-for-ever
buttons; only the store calls swap from chained PostgREST writes to
.rpc() — the UI itself is unchanged.

pgTAP 012 (8 assertions) covers existence, restore happy path,
non-member rejection, hard-delete happy path, active-list guard.
Integration T-10..T-14 (5 vitest) verifies the wire format the store
relies on (rpc errors, row state, active-listing visibility).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 02:14:10 +02:00

88 lines
3.4 KiB
PL/PgSQL

-- pgTAP: restore_list / hard_delete_list RPCs — Fase 10.4
-- Run with: psql -U postgres -d postgres -f supabase/tests/012_trash_rpcs.sql
CREATE EXTENSION IF NOT EXISTS pgtap;
BEGIN;
SELECT plan(8);
-- ── Existence ────────────────────────────────────────────────────────────────
SELECT has_function(
'public', 'restore_list', ARRAY['uuid'],
'public.restore_list(uuid) RPC exists'
);
SELECT has_function(
'public', 'hard_delete_list', ARRAY['uuid'],
'public.hard_delete_list(uuid) RPC exists'
);
-- ── Setup: a soft-deleted list in the seed collective ────────────────────────
-- Owned by Borja so it has a valid created_by (RESTRICT FK).
INSERT INTO public.shopping_lists (id, collective_id, name, status, created_by, deleted_at)
VALUES ('cccccccc-4444-4444-4444-4444deadbeef',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'Trash test list', 'active',
'22222222-2222-2222-2222-222222222222', now() - interval '1 hour');
INSERT INTO public.shopping_lists (id, collective_id, name, status, created_by)
VALUES ('cccccccc-4444-4444-4444-4444cafebabe',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'Active list', 'active',
'22222222-2222-2222-2222-222222222222');
-- ── Case 1: restore as Borja (member) ────────────────────────────────────────
SELECT set_config('request.jwt.claim.sub', '22222222-2222-2222-2222-222222222222', true);
SELECT lives_ok(
$$ SELECT public.restore_list('cccccccc-4444-4444-4444-4444deadbeef') $$,
'T-01: member can restore a soft-deleted list in their collective'
);
SELECT is(
(SELECT deleted_at FROM public.shopping_lists
WHERE id = 'cccccccc-4444-4444-4444-4444deadbeef'),
NULL,
'T-01: deleted_at cleared after restore'
);
-- ── Case 2: restore by non-member (Eva) → reject ─────────────────────────────
-- Soft-delete it again first
UPDATE public.shopping_lists SET deleted_at = now() - interval '1 hour'
WHERE id = 'cccccccc-4444-4444-4444-4444deadbeef';
SELECT set_config('request.jwt.claim.sub', '55555555-5555-5555-5555-555555555555', true); -- Eva
SELECT throws_ok(
$$ SELECT public.restore_list('cccccccc-4444-4444-4444-4444deadbeef') $$,
NULL,
NULL,
'T-02: non-member rejected'
);
-- ── Case 3: hard delete a soft-deleted list (Borja) ──────────────────────────
SELECT set_config('request.jwt.claim.sub', '22222222-2222-2222-2222-222222222222', true);
SELECT lives_ok(
$$ SELECT public.hard_delete_list('cccccccc-4444-4444-4444-4444deadbeef') $$,
'T-03: member can hard-delete a soft-deleted list'
);
SELECT is(
(SELECT count(*)::int FROM public.shopping_lists
WHERE id = 'cccccccc-4444-4444-4444-4444deadbeef'),
0,
'T-03: row gone after hard delete'
);
-- ── Case 4: hard delete an active list → rejected (guard) ────────────────────
SELECT throws_ok(
$$ SELECT public.hard_delete_list('cccccccc-4444-4444-4444-4444cafebabe') $$,
NULL,
NULL,
'T-04: hard delete of non-trashed list rejected'
);
SELECT * FROM finish();
ROLLBACK;