d3ro-voice/server/supabase/tests/content-reporting.integration.sql
2026-08-29 18:33:45 +09:00

533 lines
16 KiB
PL/PgSQL

\set ON_ERROR_STOP on
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
(
'29000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
'content-reporter@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
),
(
'29000000-0000-4000-8000-000000000002', 'authenticated', 'authenticated',
'content-other@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
),
(
'29000000-0000-4000-8000-000000000003', 'authenticated', 'authenticated',
'content-manager@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"],"role":"manager"}'::jsonb, '{}'::jsonb, now(), now()
),
(
'29000000-0000-4000-8000-000000000004', 'authenticated', 'authenticated',
'content-rate@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
);
UPDATE public.profiles
SET role = 'manager'
WHERE id = '29000000-0000-4000-8000-000000000003';
SELECT pg_temp.assert_true(
NOT has_table_privilege('authenticated', 'public.content_generation_receipts', 'SELECT'),
'authenticated users cannot read generation receipts'
);
SELECT pg_temp.assert_true(
NOT has_table_privilege('authenticated', 'public.content_reports', 'SELECT'),
'authenticated users cannot read moderation snapshots directly'
);
SELECT pg_temp.assert_true(
NOT has_table_privilege('authenticated', 'public.content_reports', 'INSERT'),
'authenticated users cannot bypass the submission RPC'
);
SELECT pg_temp.assert_true(
NOT has_function_privilege(
'authenticated',
'public.submit_content_report_v1(uuid,uuid,text,text,uuid,text,text,text)',
'EXECUTE'
),
'authenticated users cannot forge the service submission actor'
);
SELECT pg_temp.assert_true(
NOT has_function_privilege(
'authenticated',
'public.purge_expired_content_reporting_data_v1()',
'EXECUTE'
),
'authenticated users cannot purge moderation evidence'
);
SELECT pg_temp.assert_true(
has_function_privilege(
'authenticated',
'public.admin_list_content_reports_v1(text,integer,timestamptz)',
'EXECUTE'
),
'moderation reads cross an explicit role-checking RPC'
);
SELECT pg_temp.assert_true(
NOT has_column_privilege('authenticated', 'public.profiles', 'role', 'UPDATE'),
'authenticated users cannot self-promote through profiles_update_own'
);
SELECT pg_temp.assert_true(
NOT has_column_privilege('authenticated', 'public.profiles', 'tier', 'UPDATE'),
'authenticated users cannot self-assign an entitlement tier'
);
SELECT pg_temp.assert_true(
has_column_privilege('authenticated', 'public.profiles', 'name', 'UPDATE')
AND has_column_privilege('authenticated', 'public.profiles', 'avatar_url', 'UPDATE')
AND has_column_privilege('authenticated', 'public.profiles', 'locale', 'UPDATE'),
'authenticated users retain the intended self-service profile fields'
);
SET LOCAL ROLE authenticated;
SELECT set_config('request.jwt.claim.role', 'authenticated', true);
SELECT set_config('request.jwt.claim.sub', '29000000-0000-4000-8000-000000000002', true);
DO $$
BEGIN
BEGIN
UPDATE public.profiles
SET role = 'manager'
WHERE id = '29000000-0000-4000-8000-000000000002';
RAISE EXCEPTION 'expected_profile_role_update_denied';
EXCEPTION WHEN insufficient_privilege THEN
NULL;
END;
BEGIN
UPDATE public.profiles
SET tier = 'pro_plus'
WHERE id = '29000000-0000-4000-8000-000000000002';
RAISE EXCEPTION 'expected_profile_tier_update_denied';
EXCEPTION WHEN insufficient_privilege THEN
NULL;
END;
UPDATE public.profiles
SET name = 'Safe self-service profile', locale = 'en'
WHERE id = '29000000-0000-4000-8000-000000000002';
END;
$$;
RESET ROLE;
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.profiles
WHERE id = '29000000-0000-4000-8000-000000000002'
AND name = 'Safe self-service profile'
AND locale = 'en'
AND role = 'user'
AND tier = 'free'
),
'profile ACL preserves public edits while managed fields remain unchanged'
);
SELECT set_config('request.jwt.claim.role', 'service_role', true);
DO $$
DECLARE
receipt jsonb;
first_result jsonb;
replay_result jsonb;
command_receipt jsonb;
action_receipt jsonb;
BEGIN
receipt := public.issue_content_generation_receipt_v1(
'29000000-0000-4000-8000-000000000001',
'talk_response',
'claude-haiku-4-5-20251001'
);
PERFORM pg_temp.assert_true(receipt->>'generationId' IS NOT NULL,
'Talk generation receives a server-side receipt');
first_result := public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000001',
'29100000-0000-4000-8000-000000000001',
'ai_output',
'talk_response',
(receipt->>'generationId')::uuid,
'harmful',
'Please review this output',
'Reporter-selected AI response evidence'
);
PERFORM pg_temp.assert_true(
first_result->>'status' = 'submitted'
AND NOT (first_result->>'idempotent')::boolean,
'first submission creates a report'
);
replay_result := public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000001',
'29100000-0000-4000-8000-000000000001',
'ai_output',
'talk_response',
(receipt->>'generationId')::uuid,
'harmful',
'Please review this output',
'Reporter-selected AI response evidence'
);
PERFORM pg_temp.assert_true(
(replay_result->>'idempotent')::boolean
AND replay_result->>'reportId' = first_result->>'reportId',
'same key and body replay without a duplicate row'
);
BEGIN
PERFORM public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000001',
'29100000-0000-4000-8000-000000000001',
'ai_output', 'talk_response', (receipt->>'generationId')::uuid,
'privacy', NULL, 'Different body'
);
RAISE EXCEPTION 'expected_idempotency_conflict';
EXCEPTION WHEN SQLSTATE 'PT409' THEN
NULL;
END;
BEGIN
PERFORM public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000002',
'29100000-0000-4000-8000-000000000002',
'ai_output', 'talk_response', (receipt->>'generationId')::uuid,
'privacy', NULL, 'Cross-user attempt'
);
RAISE EXCEPTION 'expected_source_not_found';
EXCEPTION WHEN SQLSTATE 'P0002' THEN
NULL;
END;
command_receipt := public.issue_content_generation_receipt_v1(
'29000000-0000-4000-8000-000000000001',
'command_response',
'claude-haiku-4-5-20251001'
);
action_receipt := public.issue_content_generation_receipt_v1(
'29000000-0000-4000-8000-000000000001',
'action_response',
'claude-haiku-4-5-20251001'
);
PERFORM pg_temp.assert_true(
command_receipt->>'generationId' IS NOT NULL
AND action_receipt->>'generationId' IS NOT NULL,
'Commands and Actions share the implemented receipt boundary'
);
BEGIN
PERFORM public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000001',
'29100000-0000-4000-8000-000000000003',
'ai_output', 'talk_response', (command_receipt->>'generationId')::uuid,
'other', NULL, 'Surface mismatch'
);
RAISE EXCEPTION 'expected_surface_mismatch';
EXCEPTION WHEN SQLSTATE 'P0002' THEN
NULL;
END;
END;
$$;
INSERT INTO public.meetings(id, user_id, title, status)
VALUES (
'29700000-0000-4000-8000-000000000001',
'29000000-0000-4000-8000-000000000001',
'Generated document report fixture',
'completed'
);
INSERT INTO public.meeting_documents(
id, meeting_id, user_id, template_type, title, content, prompt_used,
llm_model, llm_latency_ms, generation_idempotency_key
) VALUES
(
'29800000-0000-4000-8000-000000000001',
'29700000-0000-4000-8000-000000000001',
'29000000-0000-4000-8000-000000000001',
'minutes',
'Generated minutes',
'Generated meeting document evidence',
'template:fixture@1',
'claude-haiku-4-5-20251001',
12,
'29900000-0000-4000-8000-000000000001'
),
(
'29800000-0000-4000-8000-000000000002',
'29700000-0000-4000-8000-000000000001',
'29000000-0000-4000-8000-000000000001',
'custom',
'Manual document',
'This row has no generation provenance',
NULL,
NULL,
NULL,
NULL
);
INSERT INTO public.meeting_document_generation_audit(
user_id, meeting_id, document_id, idempotency_key, model,
template_revision, transcript_hash, latency_ms
) VALUES (
'29000000-0000-4000-8000-000000000001',
'29700000-0000-4000-8000-000000000001',
'29800000-0000-4000-8000-000000000001',
'29900000-0000-4000-8000-000000000001',
'claude-haiku-4-5-20251001',
1,
repeat('a', 64),
12
);
DO $$
DECLARE
result jsonb;
BEGIN
result := public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000001',
'29100000-0000-4000-8000-000000000007',
'ai_output',
'meeting_document',
'29800000-0000-4000-8000-000000000001',
'misinformation',
NULL,
'Generated meeting document evidence'
);
PERFORM pg_temp.assert_true(
result->>'status' = 'submitted'
AND EXISTS (
SELECT 1 FROM public.content_reports
WHERE id = (result->>'reportId')::uuid
AND source_type = 'meeting_document'
AND generation_receipt_id IS NULL
),
'owned meeting document with generation audit is reportable'
);
BEGIN
PERFORM public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000002',
'29100000-0000-4000-8000-000000000008',
'ai_output', 'meeting_document',
'29800000-0000-4000-8000-000000000001',
'privacy', NULL, 'Cross-user generated document'
);
RAISE EXCEPTION 'expected_cross_user_document_not_found';
EXCEPTION WHEN SQLSTATE 'P0002' THEN
NULL;
END;
BEGIN
PERFORM public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000001',
'29100000-0000-4000-8000-000000000009',
'ai_output', 'meeting_document',
'29800000-0000-4000-8000-000000000002',
'other', NULL, 'Manual document masquerading as AI output'
);
RAISE EXCEPTION 'expected_generation_audit_required';
EXCEPTION WHEN SQLSTATE 'P0002' THEN
NULL;
END;
END;
$$;
DO $$
DECLARE
purge_result jsonb;
BEGIN
INSERT INTO public.content_generation_receipts(
id, user_id, purpose, model, created_at, expires_at
) VALUES (
'29600000-0000-4000-8000-000000000001',
'29000000-0000-4000-8000-000000000002',
'talk_response',
'expired-fixture',
now() - interval '31 days',
now() - interval '1 day'
);
purge_result := public.purge_expired_content_reporting_data_v1();
PERFORM pg_temp.assert_true(
(purge_result->>'purgedReceipts')::integer >= 1
AND NOT EXISTS (
SELECT 1 FROM public.content_generation_receipts
WHERE id = '29600000-0000-4000-8000-000000000001'
),
'service cleanup removes an expired content-free receipt'
);
END;
$$;
-- Fill the hourly window for a separate actor through service-only rows, then
-- prove that the atomic RPC denies the next otherwise valid receipt.
INSERT INTO public.content_generation_receipts(id, user_id, purpose, model)
SELECT
('29200000-0000-4000-8000-' || lpad(ordinal::text, 12, '0'))::uuid,
'29000000-0000-4000-8000-000000000004'::uuid,
'talk_response',
'fixture-model'
FROM generate_series(1, 11) AS ordinal;
INSERT INTO public.content_reports(
reporter_id, idempotency_key, request_hash, kind, source_type, source_id,
generation_receipt_id, reason, reported_snapshot, snapshot_sha256
)
SELECT
'29000000-0000-4000-8000-000000000004'::uuid,
gen_random_uuid(),
encode(extensions.digest(('fixture-' || ordinal)::text, 'sha256'), 'hex'),
'ai_output',
'talk_response',
('29200000-0000-4000-8000-' || lpad(ordinal::text, 12, '0'))::uuid,
('29200000-0000-4000-8000-' || lpad(ordinal::text, 12, '0'))::uuid,
'spam',
'fixture evidence ' || ordinal,
encode(extensions.digest(('fixture evidence ' || ordinal)::text, 'sha256'), 'hex')
FROM generate_series(1, 10) AS ordinal;
DO $$
DECLARE
remaining_receipt uuid;
BEGIN
SELECT receipt.id INTO remaining_receipt
FROM public.content_generation_receipts AS receipt
WHERE receipt.user_id = '29000000-0000-4000-8000-000000000004'
AND NOT EXISTS (
SELECT 1 FROM public.content_reports AS report
WHERE report.generation_receipt_id = receipt.id
)
LIMIT 1;
BEGIN
PERFORM public.submit_content_report_v1(
'29000000-0000-4000-8000-000000000004',
'29100000-0000-4000-8000-000000000004',
'ai_output', 'talk_response', remaining_receipt,
'spam', NULL, 'Rate-limited evidence'
);
RAISE EXCEPTION 'expected_rate_limit';
EXCEPTION WHEN SQLSTATE 'PT429' THEN
NULL;
END;
END;
$$;
SELECT set_config('request.jwt.claim.role', 'authenticated', true);
SELECT set_config('request.jwt.claim.sub', '29000000-0000-4000-8000-000000000003', true);
DO $$
DECLARE
queued record;
acted jsonb;
replay jsonb;
resolved jsonb;
evidence_expiry timestamptz;
BEGIN
SELECT * INTO queued
FROM public.admin_list_content_reports_v1('pending', 100, NULL)
WHERE reporter_id = '29000000-0000-4000-8000-000000000001'
LIMIT 1;
PERFORM pg_temp.assert_true(
queued.report_id IS NOT NULL
AND queued.reported_snapshot = 'Reporter-selected AI response evidence',
'manager can read the bounded moderation evidence through the RPC'
);
acted := public.admin_act_on_content_report_v1(
queued.report_id,
'29100000-0000-4000-8000-000000000005',
'begin_review',
'Initial policy review'
);
PERFORM pg_temp.assert_true(
acted->>'status' = 'reviewing' AND NOT (acted->>'idempotent')::boolean,
'manager can start review through the action RPC'
);
replay := public.admin_act_on_content_report_v1(
queued.report_id,
'29100000-0000-4000-8000-000000000005',
'begin_review',
'Initial policy review'
);
PERFORM pg_temp.assert_true(
(replay->>'idempotent')::boolean,
'moderation action replay is idempotent'
);
resolved := public.admin_act_on_content_report_v1(
queued.report_id,
'29100000-0000-4000-8000-000000000006',
'dismiss',
'No policy violation after review'
);
SELECT evidence_expires_at INTO evidence_expiry
FROM public.content_reports WHERE id = queued.report_id;
PERFORM pg_temp.assert_true(
resolved->>'status' = 'dismissed'
AND evidence_expiry BETWEEN now() + interval '179 days' AND now() + interval '181 days',
'terminal moderation sets the implemented 180-day evidence window'
);
END;
$$;
SELECT set_config('request.jwt.claim.sub', '29000000-0000-4000-8000-000000000002', true);
DO $$
BEGIN
BEGIN
PERFORM public.admin_list_content_reports_v1(NULL, 10, NULL);
RAISE EXCEPTION 'expected_manager_required';
EXCEPTION WHEN insufficient_privilege THEN
NULL;
END;
END;
$$;
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.audit_log
WHERE action = 'content_report.dismiss'
AND target_type = 'content_report'
AND admin_id = '29000000-0000-4000-8000-000000000003'
),
'moderation resolution records metadata-only audit evidence before manager deletion'
);
DELETE FROM auth.users WHERE id = '29000000-0000-4000-8000-000000000003';
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.audit_log
WHERE action = 'content_report.dismiss'
AND target_type = 'content_report'
AND admin_id IS NULL
),
'manager deletion preserves audit evidence while unlinking the manager identity'
);
-- Account deletion removes the generation ledger identity but retains a
-- de-identified moderation record and its selected evidence.
DELETE FROM auth.users WHERE id = '29000000-0000-4000-8000-000000000001';
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.content_reports
WHERE reporter_id IS NULL
AND reported_snapshot = 'Reporter-selected AI response evidence'
AND generation_receipt_id IS NULL
),
'moderation evidence survives account deletion without the reporter identity'
);
ROLLBACK;