d3ro-voice/server/supabase/tests/audio-retention-on-delete.integration.sql

256 lines
11 KiB
PL/PgSQL

\set ON_ERROR_STOP on
-- Regression (redteam r2-24): deleting a history entry or meeting (e.g. from the
-- phone, which deletes only the parent row) must queue its raw audio for removal
-- instead of leaving an unreachable object in the audio bucket forever.
-- Requires 20260929000004_audio_retention_on_delete.sql.
BEGIN;
CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF condition IS NOT TRUE THEN
RAISE EXCEPTION 'assertion_failed: %', message;
END IF;
END;
$$;
INSERT INTO auth.users (
id, aud, role, email, encrypted_password, email_confirmed_at,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES
(
'24000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
'audio-retention-owner@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
),
(
'24000000-0000-4000-8000-000000000002', 'authenticated', 'authenticated',
'audio-retention-leaver@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
);
INSERT INTO public.history (id, user_id, original_text, duration)
VALUES
('24100000-0000-4000-8000-000000000001', '24000000-0000-4000-8000-000000000001', 'dictation one', 1),
('24100000-0000-4000-8000-000000000002', '24000000-0000-4000-8000-000000000001', 'dictation two', 1),
('24100000-0000-4000-8000-000000000003', '24000000-0000-4000-8000-000000000002', 'leaver dictation', 1);
INSERT INTO public.meetings (id, user_id, title, status)
VALUES ('24200000-0000-4000-8000-000000000001', '24000000-0000-4000-8000-000000000001', 'Retention meeting', 'completed');
INSERT INTO public.audio_files (
id, user_id, history_id, meeting_id, source, storage_key, mime_type, size_bytes, sha256, upload_status
) VALUES
('24300000-0000-4000-8000-000000000001', '24000000-0000-4000-8000-000000000001',
'24100000-0000-4000-8000-000000000001', NULL, 'recording',
'24000000-0000-4000-8000-000000000001/history/one.wav', 'audio/wav', 10, repeat('a', 64), 'uploaded'),
('24300000-0000-4000-8000-000000000002', '24000000-0000-4000-8000-000000000001',
NULL, '24200000-0000-4000-8000-000000000001', 'recording',
'24000000-0000-4000-8000-000000000001/meetings/m.wav', 'audio/wav', 10, repeat('b', 64), 'uploaded'),
('24300000-0000-4000-8000-000000000003', '24000000-0000-4000-8000-000000000001',
'24100000-0000-4000-8000-000000000002', NULL, 'file-picker',
'24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a', 'audio/mp4', 10, repeat('c', 64), 'uploaded'),
('24300000-0000-4000-8000-000000000004', '24000000-0000-4000-8000-000000000002',
'24100000-0000-4000-8000-000000000003', NULL, 'recording',
'24000000-0000-4000-8000-000000000002/history/leaver.wav', 'audio/wav', 10, repeat('d', 64), 'uploaded');
-- 1) Clients cannot see or drive the purge queue. Checked through the catalog:
-- calling a function whose EXECUTE is denied segfaults the local
-- supabase/postgres 17.6.1.104 image, which would take the shared DB down.
SELECT pg_temp.assert_true(
NOT has_table_privilege('authenticated', 'public.audio_purge_queue', 'SELECT')
AND NOT has_table_privilege('anon', 'public.audio_purge_queue', 'SELECT')
AND NOT has_table_privilege('authenticated', 'public.audio_purge_queue', 'INSERT')
AND NOT has_function_privilege('authenticated', 'public.claim_audio_purge_batch_v1(integer, integer)', 'EXECUTE')
AND NOT has_function_privilege('anon', 'public.claim_audio_purge_batch_v1(integer, integer)', 'EXECUTE')
AND NOT has_function_privilege('authenticated', 'public.complete_audio_purge_v1(bigint[], uuid)', 'EXECUTE')
AND NOT has_function_privilege('authenticated', 'public.dispatch_audio_purge_v1()', 'EXECUTE')
AND has_function_privilege('service_role', 'public.claim_audio_purge_batch_v1(integer, integer)', 'EXECUTE')
AND has_function_privilege('service_role', 'public.complete_audio_purge_v1(bigint[], uuid)', 'EXECUTE'),
'purge queue and worker RPCs are service-only'
);
SET LOCAL ROLE authenticated;
SELECT set_config(
'request.jwt.claims',
'{"sub":"24000000-0000-4000-8000-000000000001","role":"authenticated"}',
true
);
-- 2) The phone deletes only the parent rows (RLS path, like deleteHistoryRevisionSafe).
DELETE FROM public.history WHERE id = '24100000-0000-4000-8000-000000000001';
DELETE FROM public.meetings WHERE id = '24200000-0000-4000-8000-000000000001';
DELETE FROM public.history WHERE id = '24100000-0000-4000-8000-000000000002';
RESET ROLE;
SELECT pg_temp.assert_true(
(SELECT count(*) FROM public.audio_files
WHERE id IN ('24300000-0000-4000-8000-000000000001',
'24300000-0000-4000-8000-000000000002',
'24300000-0000-4000-8000-000000000003')
AND upload_status = 'deleted'
AND history_id IS NULL AND meeting_id IS NULL) = 3,
'orphaned audio rows are marked deleted'
);
SELECT pg_temp.assert_true(
(SELECT array_agg(storage_key ORDER BY storage_key) FROM public.audio_purge_queue
WHERE user_id = '24000000-0000-4000-8000-000000000001')
= ARRAY[
'24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a',
'24000000-0000-4000-8000-000000000001/history/one.wav',
'24000000-0000-4000-8000-000000000001/meetings/m.wav'
],
'history and meeting deletes queue their audio keys'
);
-- 3) Re-importing the same content-addressed file reclaims the key: the pending
-- purge is cancelled and the stale deleted row no longer blocks the UNIQUE key.
SET LOCAL ROLE authenticated;
INSERT INTO public.audio_files (
id, user_id, source, storage_key, mime_type, size_bytes, sha256, upload_status
) VALUES (
'24300000-0000-4000-8000-000000000005', '24000000-0000-4000-8000-000000000001', 'file-picker',
'24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a', 'audio/mp4', 10, repeat('c', 64), 'pending'
);
RESET ROLE;
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM public.audio_purge_queue
WHERE storage_key LIKE '%/memo.m4a'),
'reclaimed key is no longer queued for purge'
);
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM public.audio_files WHERE id = '24300000-0000-4000-8000-000000000003'),
'stale deleted row for the reclaimed key is dropped'
);
-- 4) A key referenced by a live row again is dropped at claim time, never removed.
INSERT INTO public.audio_purge_queue (user_id, storage_key)
VALUES ('24000000-0000-4000-8000-000000000001',
'24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a');
SET LOCAL ROLE service_role;
CREATE TEMP TABLE claimed ON COMMIT DROP AS
SELECT * FROM public.claim_audio_purge_batch_v1(100, 600);
RESET ROLE;
SELECT pg_temp.assert_true(
(SELECT count(*) FROM claimed) = 2
AND NOT EXISTS (SELECT 1 FROM claimed WHERE storage_key LIKE '%/memo.m4a')
AND (SELECT count(DISTINCT lease_token) FROM claimed) = 1
AND (SELECT bool_and(leased_until > now() AND attempts = 1) FROM claimed),
'claim leases only keys with no live owner'
);
-- 5) While a worker holds the lease, a live row cannot reclaim that key.
SET LOCAL ROLE authenticated;
DO $$
BEGIN
BEGIN
INSERT INTO public.audio_files (
user_id, meeting_id, source, storage_key, mime_type, size_bytes, sha256, upload_status
) VALUES (
'24000000-0000-4000-8000-000000000001', NULL, 'recording',
'24000000-0000-4000-8000-000000000001/meetings/m.wav', 'audio/wav', 10, repeat('e', 64), 'pending'
);
RAISE EXCEPTION 'assertion_failed: leased key was reclaimed';
EXCEPTION WHEN object_in_use THEN NULL;
END;
END;
$$;
RESET ROLE;
-- 6) A second claim does not hand out leased work.
SET LOCAL ROLE service_role;
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM public.claim_audio_purge_batch_v1(100, 600)),
'leased entries are not claimed twice'
);
-- 7) Completing with a wrong lease does nothing; the right lease clears queue and rows.
SELECT pg_temp.assert_true(
public.complete_audio_purge_v1(
(SELECT array_agg(id) FROM claimed), gen_random_uuid()
) = 0,
'foreign lease cannot complete'
);
SELECT pg_temp.assert_true(
public.complete_audio_purge_v1(
(SELECT array_agg(id) FROM claimed), (SELECT min(lease_token::text)::uuid FROM claimed)
) = 2,
'lease holder completes the batch'
);
RESET ROLE;
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM public.audio_purge_queue
WHERE user_id = '24000000-0000-4000-8000-000000000001'),
'completed entries leave the queue'
);
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM public.audio_files
WHERE id IN ('24300000-0000-4000-8000-000000000001',
'24300000-0000-4000-8000-000000000002')),
'purged audio metadata is removed'
);
SELECT pg_temp.assert_true(
EXISTS (SELECT 1 FROM public.audio_files
WHERE id = '24300000-0000-4000-8000-000000000005' AND upload_status = 'pending'),
'the reclaimed live row survives completion'
);
-- 8) Desktop upsert (ON CONFLICT on the UNIQUE key) over a deleted row reclaims it.
INSERT INTO public.history (id, user_id, original_text, duration)
VALUES ('24100000-0000-4000-8000-000000000004', '24000000-0000-4000-8000-000000000001', 'desktop', 1);
INSERT INTO public.audio_files (
id, user_id, history_id, source, storage_key, mime_type, size_bytes, sha256, upload_status
) VALUES (
'24300000-0000-4000-8000-000000000006', '24000000-0000-4000-8000-000000000001',
'24100000-0000-4000-8000-000000000004', 'recording',
'24000000-0000-4000-8000-000000000001/history/desk.wav', 'audio/wav', 10, repeat('f', 64), 'uploaded'
);
DELETE FROM public.history WHERE id = '24100000-0000-4000-8000-000000000004';
INSERT INTO public.history (id, user_id, original_text, duration)
VALUES ('24100000-0000-4000-8000-000000000004', '24000000-0000-4000-8000-000000000001', 'desktop again', 1);
INSERT INTO public.audio_files (
user_id, history_id, source, storage_key, mime_type, size_bytes, sha256, upload_status
) VALUES (
'24000000-0000-4000-8000-000000000001', '24100000-0000-4000-8000-000000000004', 'recording',
'24000000-0000-4000-8000-000000000001/history/desk.wav', 'audio/wav', 10, repeat('f', 64), 'uploaded'
)
ON CONFLICT (user_id, sha256, storage_key) DO UPDATE
SET history_id = EXCLUDED.history_id, upload_status = EXCLUDED.upload_status;
SELECT pg_temp.assert_true(
(SELECT count(*) FROM public.audio_files
WHERE storage_key = '24000000-0000-4000-8000-000000000001/history/desk.wav') = 1
AND EXISTS (SELECT 1 FROM public.audio_files
WHERE storage_key = '24000000-0000-4000-8000-000000000001/history/desk.wav'
AND upload_status = 'uploaded'
AND history_id = '24100000-0000-4000-8000-000000000004')
AND NOT EXISTS (SELECT 1 FROM public.audio_purge_queue WHERE storage_key LIKE '%/desk.wav'),
'desktop upsert reclaims a deleted key'
);
-- 9) Account deletion still cascades (the queue has no FK to auth.users).
DELETE FROM auth.users WHERE id = '24000000-0000-4000-8000-000000000002';
SELECT pg_temp.assert_true(
NOT EXISTS (SELECT 1 FROM public.audio_files WHERE user_id = '24000000-0000-4000-8000-000000000002'),
'account deletion removes the user audio rows'
);
-- 10) The dispatcher is a no-op when there is no ready work.
SELECT pg_temp.assert_true(
public.dispatch_audio_purge_v1() IS NULL OR NOT EXISTS (
SELECT 1 FROM public.audio_purge_queue WHERE user_id = '24000000-0000-4000-8000-000000000001'
),
'dispatcher does not fail'
);
ROLLBACK;