533 lines
16 KiB
PL/PgSQL
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;
|