Shuffle/supabase/tests/database/10_game_reports_user.test.sql
cschlaefke 7186cddac5 feat(db): Fehlendes Spiel melden – Migration, RPCs, RLS und pgTAP-Tests (KAT-04, 13a)
Refs #13

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 19:11:20 +01:00

687 lines
68 KiB
PL/PgSQL

-- KAT-04 (PR 13a): Fehlendes Spiel melden. Normalisierung (strip_leading_article, normalize_game_title),
-- report_day, Rechte/Policies/Trigger auf game_reports, find_similar_catalog_games, report_missing_game,
-- adopt_report_target (security definer), Ergebnis nach admin_resolve_report, Konto-Löschen.
-- Läuft auch mit eingespieltem Startbestand: eigene Fixtures mit festen IDs (Präfix a130/f13) und
-- unwahrscheinlichen Titeln (zyxq...), damit der Katalog nicht hineinspielt.
-- Fixtures legt postgres an; "als Nutzer" läuft wirklich als Rolle authenticated mit gesetztem
-- request.jwt.claims. Regel dieser Datei: Jede Verweigerung steht neben einem Positivfall.
begin;
select plan(198);
insert into auth.users (id, email, raw_user_meta_data) values
('a1300000-0000-4000-8000-0000000000a1', 'kat04-a@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"KA"}'),
('a1300000-0000-4000-8000-0000000000b2', 'kat04-b@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"KB"}'),
('a1300000-0000-4000-8000-0000000000c3', 'kat04-c@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"KC"}'),
('a1300000-0000-4000-8000-0000000000d4', 'kat04-d@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"KD"}'),
('a1300000-0000-4000-8000-0000000000e1', 'kat04-l1@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"L1"}'),
('a1300000-0000-4000-8000-0000000000e2', 'kat04-l2@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"L2"}'),
('a1300000-0000-4000-8000-0000000000e3', 'kat04-l3@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"L3"}'),
('a1300000-0000-4000-8000-0000000000e4', 'kat04-y@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"KY"}');
-- ---------------------------------------------------------------------------------------------
-- 1. Funktionen: Rechte, Attribute (Spec 4.1)
-- ---------------------------------------------------------------------------------------------
create temp table fn (sig text primary key, names text, granted boolean);
grant all on fn to public;
insert into fn values
('public.strip_leading_article(text)', 'strip_leading_article', true),
('public.normalize_game_title(text)', 'normalize_game_title', true),
('public.report_day(timestamptz)', 'report_day', true),
('public.find_similar_catalog_games(text, integer)', 'find_similar_catalog_games', true),
('public.report_missing_game(text, text, integer, text, text, boolean)', 'report_missing_game', true),
('public.adopt_report_target(uuid)', 'adopt_report_target', true),
('public.game_reports_daily_limit()', 'game_reports_daily_limit', false);
select is_empty($$select f.sig, r from fn f, unnest(array['anon', 'service_role']) r
where has_function_privilege(r, f.sig::regprocedure, 'execute')$$,
'Sicherheit Kriterium 2: anon und service_role dürfen keine der neuen Funktionen ausführen');
select is_empty($$select f.sig from fn f
where has_function_privilege('authenticated', f.sig::regprocedure, 'execute') <> f.granted$$,
'KAT-04: authenticated darf genau die RPCs und Hilfsfunktionen ausführen, die Trigger-Funktion nicht');
select is_empty($$select f.sig from fn f join pg_proc p on p.oid = f.sig::regprocedure
where p.proacl is null or exists (select 1 from aclexplode(p.proacl) a where a.grantee = 0)$$,
'KAT-04: keine neue Funktion ist für PUBLIC ausführbar (ACL gesetzt, kein Eintrag für grantee 0)');
select is_empty($$select f.sig from fn f join pg_proc p on p.oid = f.sig::regprocedure
where p.prosecdef <> (f.sig = 'public.adopt_report_target(uuid)')$$,
'KAT-04: nur adopt_report_target ist security definer');
select is((select pg_get_userbyid(proowner) from pg_proc where oid = 'public.adopt_report_target(uuid)'::regprocedure), 'postgres'::name,
'KAT-04 Kriterium 5: adopt_report_target gehört postgres');
select is_empty($$select f.sig from fn f join pg_proc p on p.oid = f.sig::regprocedure
where p.proconfig is null or not (p.proconfig @> array['search_path=""'])$$,
'KAT-04: alle neuen Funktionen haben search_path = "" als Funktionseinstellung');
-- ---------------------------------------------------------------------------------------------
-- 2. Normalisierung (Krit. 2, Klärung 15)
-- ---------------------------------------------------------------------------------------------
select is(public.strip_leading_article('die siedler von catan'), 'siedler von catan', 'KAT-04 Kriterium 2: führendes "die" entfällt');
select is(public.strip_leading_article('the crew'), 'crew', 'KAT-04 Kriterium 2: führendes "the" entfällt');
select is(public.strip_leading_article('a game of thrones'), 'game of thrones', 'KAT-04 Kriterium 2: führendes "a" entfällt');
select is(public.strip_leading_article('an apple'), 'apple', 'KAT-04 Kriterium 2: führendes "an" entfällt');
select is(public.strip_leading_article('einem haus'), 'haus', 'KAT-04 Kriterium 2: auch "einem" entfällt');
select is(public.strip_leading_article('das'), 'das', 'KAT-04 Kriterium 2: ein Titel nur aus dem Artikel bleibt unverändert');
select is(public.strip_leading_article('theater der welt'), 'theater der welt', 'KAT-04 Kriterium 2: kein Artikel, wenn das Wort nur damit beginnt');
select is(public.strip_leading_article('herr der ringe'), 'herr der ringe', 'KAT-04 Kriterium 2: Artikel mitten im Titel bleiben');
select is(public.strip_leading_article('der die das'), 'die das', 'KAT-04 Kriterium 2: nur einmal ein Artikel');
select is(public.strip_leading_article(null), null, 'KAT-04: strip_leading_article(null) ist null');
select is(public.normalize_game_title('Die Siedler von Catan'), public.normalize_game_title('siedler von catan'),
'KAT-04 Kriterium 2: "Die Siedler von Catan" und "siedler von catan" ergeben denselben Wert');
select is(public.normalize_game_title('CATAN!'), public.normalize_game_title('Catan'), 'KAT-04 Kriterium 2: Großbuchstaben und Sonderzeichen sind egal');
select is(public.normalize_game_title('The Crew'), public.normalize_game_title('Crew'), 'KAT-04 Kriterium 2: "The Crew" = "Crew"');
select is(public.normalize_game_title('Das'), 'das', 'KAT-04 Kriterium 2: ein Titel nur aus einem Artikel bleibt nicht leer');
select is(public.normalize_game_title('The'), 'the', 'KAT-04 Kriterium 2: auch "The" allein bleibt nicht leer');
select is(public.normalize_game_title('Mölkky'), public.normalize_game_title('Molkky'), 'KAT-04 Kriterium 2: Umlaute werden über search_normalize gefaltet');
select is(public.normalize_game_title('日本語'), '日本語', 'KAT-04 Kriterium 2: nicht-lateinischer Titel fällt auf lower(btrim) zurück');
select is(public.normalize_game_title(' !!! '), '!!!', 'KAT-04 Kriterium 2: Titel nur aus Sonderzeichen fällt auf lower(btrim) zurück');
select is(public.normalize_game_title(null), null, 'KAT-04: normalize_game_title(null) ist null');
select is(public.report_day('2026-01-15 23:30:00+00'), '2026-01-16'::date, 'KAT-04 Kriterium 7: 23:30 UTC im Winter ist schon der nächste Berliner Tag');
select is(public.report_day('2026-01-15 22:59:00+00'), '2026-01-15'::date, 'KAT-04 Kriterium 7: 22:59 UTC im Winter ist noch derselbe Tag');
select is(public.report_day('2026-07-15 21:59:00+00'), '2026-07-15'::date, 'KAT-04 Kriterium 7: 21:59 UTC im Sommer ist noch derselbe Tag');
select is(public.report_day('2026-07-15 22:00:00+00'), '2026-07-16'::date, 'KAT-04 Kriterium 7: 22:00 UTC im Sommer ist der nächste Tag');
-- ---------------------------------------------------------------------------------------------
-- 3. Rechte, Policies, Trigger auf game_reports (Spec 3.2)
-- ---------------------------------------------------------------------------------------------
select is((select array_agg(a.attname::text order by a.attname) from pg_attribute a
where a.attrelid = 'public.game_reports'::regclass and a.attnum > 0 and not a.attisdropped
and has_column_privilege('authenticated', 'public.game_reports', a.attname, 'insert')),
array['link', 'owner_id', 'provisional_game_id', 'publisher_name', 'title', 'year'],
'Sicherheit Kriterium 1: authenticated darf beim Anlegen genau owner_id, title, publisher_name, year, link, provisional_game_id setzen');
select is((select array_agg(a.attname::text order by a.attname) from pg_attribute a
where a.attrelid = 'public.game_reports'::regclass and a.attnum > 0 and not a.attisdropped
and has_column_privilege('authenticated', 'public.game_reports', a.attname, 'update')),
array['result_seen_at'],
'KAT-04 Kriterium 8: authenticated darf genau result_seen_at ändern');
select is_empty($$select a.attname, p from pg_attribute a, unnest(array['select', 'insert', 'update', 'references']) p
where a.attrelid = 'public.game_reports'::regclass and a.attnum > 0 and not a.attisdropped
and has_column_privilege('anon', 'public.game_reports', a.attname, p)$$,
'Sicherheit Kriterium 2: anon hat auf keine Spalte von game_reports ein Recht');
select is_empty($$select p from unnest(array['delete', 'truncate']) p where has_table_privilege('authenticated', 'public.game_reports', p)$$,
'Sicherheit Kriterium 1: authenticated darf Meldungen nicht löschen');
select is((select array_agg(policyname::text order by policyname) from pg_policies where tablename = 'game_reports' and cmd in ('INSERT', 'UPDATE')),
array['game_reports_insert_own', 'game_reports_update_seen'], 'KAT-04: genau eine Insert- und eine Update-Policy');
select ok((select pg_get_triggerdef(t.oid) ~ 'BEFORE INSERT ON public.game_reports FOR EACH ROW WHEN \(\(?new.origin = ''user''::text\)?\)'
from pg_trigger t where t.tgname = 'game_reports_daily_limit' and t.tgrelid = 'public.game_reports'::regclass),
'KAT-04 Kriterium 7: der Limit-Trigger läuft vor dem Insert und nur für origin = user');
-- ---------------------------------------------------------------------------------------------
-- 4. Katalog-Fixtures (postgres)
-- ---------------------------------------------------------------------------------------------
insert into public.games (id, title, year, visibility) values
('f13c0000-0000-4000-8000-000000000001', 'Quorblaxwitz Towers', 2020, 'catalog'),
('f13c0000-0000-4000-8000-000000000002', 'Die Zyxqwvland Saga', 2018, 'catalog'),
('f13c0000-0000-4000-8000-000000000003', 'Sortzyxq Reihenfolge', 2000, 'catalog'),
('f13c0000-0000-4000-8000-000000000004', 'Sortzyxq Reihenfolg', 2000, 'catalog'),
('f13c0000-0000-4000-8000-000000000005', 'Sortzyxq Reihenfol', 2000, 'catalog'),
('f13c0000-0000-4000-8000-00000000000d', 'Verstecktzyxq Spiel', 2015, 'catalog'),
('f13c0000-0000-4000-8000-00000000000e', 'Verstecktzyxq Spiel', 2015, 'catalog'),
('f13c0000-0000-4000-8000-00000000000f', 'Verstecktzyxq Spiel', 2015, 'catalog'),
('f13c0000-0000-4000-8000-000000000010', 'Jahrlosxyz Spiel', null, 'catalog'),
('f13c0000-0000-4000-8000-000000000011', '日本語ゲーム', 2010, 'catalog'),
('f13c0000-0000-4000-8000-000000000012', 'Das', 2010, 'catalog'),
('f13c0000-0000-4000-8000-000000000013', 'Adoptzyxq Ziel', 2021, 'catalog'),
('f13c0000-0000-4000-8000-000000000014', 'Adoptzyxq Verstecktes Ziel', 2021, 'catalog'),
('f13c0000-0000-4000-8000-000000000015', 'Adoptzyxq Zusammengefuehrtes Ziel', 2021, 'catalog');
insert into public.games (id, title, year, visibility)
select ('f13c0000-0000-4000-8000-' || lpad(to_hex(5 + i), 12, '0'))::uuid, 'Limitzyxq Spiel', 2005, 'catalog'
from generate_series(1, 7) i;
update public.games set hidden_at = now() where id in ('f13c0000-0000-4000-8000-00000000000e', 'f13c0000-0000-4000-8000-000000000014');
update public.games set hidden_at = now(), merged_into_id = 'f13c0000-0000-4000-8000-00000000000d'
where id in ('f13c0000-0000-4000-8000-00000000000f');
update public.games set hidden_at = now(), merged_into_id = 'f13c0000-0000-4000-8000-000000000013'
where id = 'f13c0000-0000-4000-8000-000000000015';
insert into public.game_alt_titles (game_id, title, locale) values
('f13c0000-0000-4000-8000-000000000002', 'The Zyxqwvland Chronicle', 'en');
insert into public.publishers (id, name) values
('f13c0000-0000-4000-8000-0000000000a1', 'Zyxq Verlag B'),
('f13c0000-0000-4000-8000-0000000000a2', 'Zyxq Verlag A');
insert into public.game_publishers (game_id, publisher_id) values
('f13c0000-0000-4000-8000-000000000001', 'f13c0000-0000-4000-8000-0000000000a1'),
('f13c0000-0000-4000-8000-000000000001', 'f13c0000-0000-4000-8000-0000000000a2');
insert into public.game_variants (id, game_id, name) values
('f13c0000-0000-4000-8000-0000000000b1', 'f13c0000-0000-4000-8000-000000000013', 'Standard');
-- private Spiele mit dem Titel des sichtbaren Katalogspiels: Bens und Annas eigener vorläufiger Eintrag
insert into public.games (id, title, year, visibility, owner_id) values
('f13a0000-0000-4000-8000-000000000001', 'Verstecktzyxq Spiel', 2015, 'private', 'a1300000-0000-4000-8000-0000000000b2'),
('f13a0000-0000-4000-8000-000000000002', 'Verstecktzyxq Spiel', 2015, 'private', 'a1300000-0000-4000-8000-0000000000a1');
-- Schwellen vorab nachrechnen, damit die Trigramm-Fälle wirklich an der Grenze von 0,8 liegen
select cmp_ok(extensions.similarity('quorblaxwitz towers', 'quorblaxwitz towe'), '>=', 0.8::real, 'Vorbedingung: "towe" liegt knapp über 0,8');
select cmp_ok(extensions.similarity('quorblaxwitz towers', 'quorblaxwitz tow'), '<', 0.8::real, 'Vorbedingung: "tow" liegt unter 0,8');
select cmp_ok(extensions.similarity('quorblaxwitz towers', 'quorblaxwitz tow'), '>=', 0.7::real, 'Vorbedingung: "tow" liegt über 0,7 (fängt die Schwelle 0,7)');
-- ---------------------------------------------------------------------------------------------
-- 5. find_similar_catalog_games (Krit. 2)
-- ---------------------------------------------------------------------------------------------
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000b2","role":"authenticated"}', true);
set local role authenticated;
select results_eq($$select game_id, title, matched_title, year, publishers, match_kind, similarity
from public.find_similar_catalog_games('Quorblaxwitz Towers', 2020)$$,
$$values ('f13c0000-0000-4000-8000-000000000001'::uuid, 'Quorblaxwitz Towers', 'Quorblaxwitz Towers', 2020::smallint,
array['Zyxq Verlag A', 'Zyxq Verlag B'], 'same_title', 1.0::real)$$,
'KAT-04 Kriterium 2: gleicher Titel liefert Treffer mit Haupttitel, Jahr, sortierten Verlagen, same_title und Ähnlichkeit 1');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towers', 2030)), 1::bigint,
'KAT-04 Kriterium 2: gleicher normalisierter Titel gilt unabhängig vom Jahr');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towers', null)), 1::bigint,
'KAT-04 Kriterium 2: gleicher Titel ohne Jahr trifft');
select is((select count(*) from public.find_similar_catalog_games('The QUORBLAXWITZ-TOWERS!', 1999)), 1::bigint,
'KAT-04 Kriterium 2: Artikel, Großbuchstaben und Sonderzeichen werden ignoriert');
select is((select count(*) from public.find_similar_catalog_games(' Quorblaxwitz Towers ', 2020)), 1::bigint,
'KAT-04 Kriterium 2: Leerraum am Rand ändert nichts');
select results_eq($$select match_kind, similarity > 0.8 and similarity < 1 from public.find_similar_catalog_games('Quorblaxwitz Towe', 2021)$$,
$$values ('similar_title', true)$$,
'KAT-04 Kriterium 2: Ähnlichkeit >= 0,8 bei Jahr + 1 trifft als similar_title mit echter Ähnlichkeit');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towe', 2019)), 1::bigint,
'KAT-04 Kriterium 2: Ähnlichkeit >= 0,8 bei Jahr - 1 trifft');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towe', 2020)), 1::bigint,
'KAT-04 Kriterium 2: Ähnlichkeit >= 0,8 bei gleichem Jahr trifft');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towe', 2022)), 0::bigint,
'KAT-04 Kriterium 2: Ähnlichkeit >= 0,8 bei Jahr + 2 trifft nicht');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towe', 2018)), 0::bigint,
'KAT-04 Kriterium 2: Ähnlichkeit >= 0,8 bei Jahr - 2 trifft nicht');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towe', null)), 1::bigint,
'KAT-04 Kriterium 2 (Spec-F2): ohne Jahr gilt die Trigramm-Regel ohne Jahresbedingung');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Tow', 2020)), 0::bigint,
'KAT-04 Kriterium 2: Ähnlichkeit < 0,8 trifft nicht, auch bei gleichem Jahr');
select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Tow', null)), 0::bigint,
'KAT-04 Kriterium 2: Ähnlichkeit < 0,8 trifft auch ohne Jahr nicht');
select is((select count(*) from public.find_similar_catalog_games('Jahrlosxyz Spie', 2020)), 1::bigint,
'KAT-04 Kriterium 2 (Spec-F2): fehlt das Jahr am Katalogspiel, gilt die Trigramm-Regel ohne Jahresbedingung');
select is((select count(*) from public.find_similar_catalog_games('Jahrlosxyz Spi', 2020)), 0::bigint,
'KAT-04 Kriterium 2: auch ohne Katalogjahr zählt die Schwelle 0,8');
-- alternative Titel und Haupttitel mit Artikel
select results_eq($$select game_id, matched_title, match_kind from public.find_similar_catalog_games('Zyxqwvland Chronicle', 1900)$$,
$$values ('f13c0000-0000-4000-8000-000000000002'::uuid, 'The Zyxqwvland Chronicle', 'same_title')$$,
'KAT-04 Kriterium 2: Treffer über title_search des alternativen Titels (matched_title), Artikel am alternativen Titel ignoriert');
select results_eq($$select title, matched_title, match_kind from public.find_similar_catalog_games('zyxqwvland saga', 1900)$$,
$$values ('Die Zyxqwvland Saga', 'Die Zyxqwvland Saga', 'same_title')$$,
'KAT-04 Kriterium 2: Treffer über den Haupttitel (games.title_search), Artikel am Haupttitel ignoriert');
select results_eq($$select matched_title, match_kind from public.find_similar_catalog_games('Zyxqwvland Chronicl', 2019)$$,
$$values ('The Zyxqwvland Chronicle', 'similar_title')$$,
'KAT-04 Kriterium 2: Trigramm-Treffer über den alternativen Titel (Jahr + 1 zum Spiel)');
select is((select count(*) from public.find_similar_catalog_games('Zyxqwvland Chronicl', 2021)), 0::bigint,
'KAT-04 Kriterium 2: Trigramm-Treffer über den alternativen Titel gilt das Jahr des Spiels (± 2 trifft nicht)');
-- Sonderfälle der Normalisierung
select is((select count(*) from public.find_similar_catalog_games('日本語ゲーム', null)), 1::bigint,
'KAT-04 Kriterium 2: Titel ohne normalisierbare Zeichen trifft über den Rückfall lower(btrim)');
select is((select count(*) from public.find_similar_catalog_games('das', null)), 1::bigint,
'KAT-04 Kriterium 2: ein Titel nur aus einem Artikel trifft das gleichnamige Spiel');
-- nicht sichtbare Spiele (hidden, merged, privat von Ben und Anna) treffen nicht; das sichtbare Spiel schon
select results_eq($$select game_id from public.find_similar_catalog_games('Verstecktzyxq Spiel', 2015)$$,
$$values ('f13c0000-0000-4000-8000-00000000000d'::uuid)$$,
'KAT-04 Kriterium 2: nur das sichtbare Katalogspiel trifft, nicht ausgeblendete, zusammengeführte oder private Spiele (auch nicht Bens und das eigene)');
-- Eingabegrenzen, Reihenfolge, Höchstzahl
select is((select count(*) from public.find_similar_catalog_games(repeat('a', 201), null)), 0::bigint, 'KAT-04 Kriterium 2: Titel über 200 Zeichen liefert eine leere Menge');
select is((select count(*) from public.find_similar_catalog_games(' ', null)), 0::bigint, 'KAT-04 Kriterium 2: leerer Titel liefert eine leere Menge');
select is((select count(*) from public.find_similar_catalog_games(null, null)), 0::bigint, 'KAT-04 Kriterium 2: Titel null liefert eine leere Menge');
select is((select count(*) from public.find_similar_catalog_games('!!!', null)), 0::bigint, 'KAT-04 Kriterium 2: Titel nur aus Sonderzeichen liefert keinen Treffer');
select is((select count(*) from public.find_similar_catalog_games(repeat('a', 200), null)), 0::bigint, 'KAT-04 Kriterium 2: Titel mit genau 200 Zeichen ist erlaubt');
select is((select count(*) from public.find_similar_catalog_games('Limitzyxq Spiel', 2005)), 5::bigint,
'KAT-04 Kriterium 2: höchstens 5 Treffer');
select results_eq($$select game_id, match_kind from public.find_similar_catalog_games('Sortzyxq Reihenfolge', 2000)$$,
$$values ('f13c0000-0000-4000-8000-000000000003'::uuid, 'same_title'),
('f13c0000-0000-4000-8000-000000000004'::uuid, 'similar_title'),
('f13c0000-0000-4000-8000-000000000005'::uuid, 'similar_title')$$,
'KAT-04 Kriterium 2: same_title zuerst, danach nach Ähnlichkeit absteigend');
reset role;
-- ---------------------------------------------------------------------------------------------
-- 6. report_missing_game (Krit. 1, 2, 3, 7, 8)
-- ---------------------------------------------------------------------------------------------
-- nicht angemeldet
select set_config('request.jwt.claims', '{"role":"authenticated"}', true);
set local role authenticated;
select throws_ok($$select public.report_missing_game('Zyxqtest Ohne Anmeldung')$$, 'P0001', 'not_authenticated',
'Sicherheit Kriterium 2: ohne Nutzerkennung (auth.uid() null) wird nichts gemeldet');
reset role;
select set_config('request.jwt.claims', '{"role":"anon"}', true);
set local role anon;
select throws_ok($$select public.report_missing_game('Zyxqtest Anon')$$, '42501', null, 'Sicherheit Kriterium 2: anon darf report_missing_game nicht aufrufen');
select throws_ok($$select * from public.find_similar_catalog_games('Zyxqtest Anon')$$, '42501', null, 'Sicherheit Kriterium 2: anon darf find_similar_catalog_games nicht aufrufen');
select throws_ok($$select public.adopt_report_target('a1300000-0000-4000-8000-0000000000ff')$$, '42501', null, 'Sicherheit Kriterium 2: anon darf adopt_report_target nicht aufrufen');
select throws_ok($$select public.normalize_game_title('x')$$, '42501', null, 'Sicherheit Kriterium 2: anon darf normalize_game_title nicht aufrufen');
select throws_ok($$select public.strip_leading_article('x')$$, '42501', null, 'Sicherheit Kriterium 2: anon darf strip_leading_article nicht aufrufen');
select throws_ok($$select public.report_day(now())$$, '42501', null, 'Sicherheit Kriterium 2: anon darf report_day nicht aufrufen');
select throws_ok($$select 1 from public.game_reports$$, '42501', null, 'Sicherheit Kriterium 2: anon darf keine Meldungen lesen');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000a1', 'x', null)$$, '42501', null,
'Sicherheit Kriterium 2: anon darf keine Meldung anlegen');
select throws_ok($$update public.game_reports set result_seen_at = now()$$, '42501', null, 'Sicherheit Kriterium 2: anon darf keine Meldung ändern');
reset role;
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000a1","role":"authenticated"}', true);
set local role authenticated;
select results_eq($$select game_id from public.find_similar_catalog_games('Verstecktzyxq Spiel', 2015)$$,
$$values ('f13c0000-0000-4000-8000-00000000000d'::uuid)$$,
'KAT-04 Kriterium 2 (Spec-F7): auch Annas eigener vorläufiger Eintrag mit demselben Titel trifft nicht');
-- Validierung (jeder Fehler mit Code, nichts bleibt zurück)
select throws_ok($$select public.report_missing_game('')$$, 'P0001', 'report_title_required', 'KAT-04 Kriterium 1: leerer Titel');
select throws_ok($$select public.report_missing_game(' ')$$, 'P0001', 'report_title_required', 'KAT-04 Kriterium 1: Titel nur aus Leerzeichen');
select throws_ok($$select public.report_missing_game(null)$$, 'P0001', 'report_title_required', 'KAT-04 Kriterium 1: Titel null');
select throws_ok($$select public.report_missing_game(repeat('t', 201))$$, 'P0001', 'report_title_too_long', 'KAT-04 Kriterium 1: Titel über 200 Zeichen');
select throws_ok($$select public.report_missing_game('Zyxqtest Verlag', repeat('v', 201))$$, 'P0001', 'report_publisher_too_long', 'KAT-04 Kriterium 1: Verlag über 200 Zeichen');
select throws_ok($$select public.report_missing_game('Zyxqtest Jahr', null, 1799)$$, 'P0001', 'report_year_invalid', 'KAT-04 Kriterium 1: Jahr 1799');
select throws_ok($$select public.report_missing_game('Zyxqtest Jahr', null, 2101)$$, 'P0001', 'report_year_invalid', 'KAT-04 Kriterium 1: Jahr 2101');
select throws_ok($$select public.report_missing_game('Zyxqtest Jahr', null, 99999)$$, 'P0001', 'report_year_invalid', 'KAT-04 Kriterium 1: Jahr 99999 ergibt report_year_invalid, kein 22003');
select throws_ok($$select public.report_missing_game('Zyxqtest Link', null, null, 'ftp://example.org')$$, 'P0001', 'report_link_invalid', 'KAT-04 Kriterium 1: Link mit anderem Schema');
select throws_ok($$select public.report_missing_game('Zyxqtest Link', null, null, 'example.org/spiel')$$, 'P0001', 'report_link_invalid', 'KAT-04 Kriterium 1: Link ohne Schema');
select throws_ok($$select public.report_missing_game('Zyxqtest Link', null, null, 'HTTP://example.org')$$, 'P0001', 'report_link_invalid', 'KAT-04 Kriterium 1: Schema in Großbuchstaben (wie der Tabellen-Check)');
select throws_ok($$select public.report_missing_game('Zyxqtest Link', null, null, 'https://' || repeat('l', 493))$$, 'P0001', 'report_link_invalid', 'KAT-04 Kriterium 1: Link mit 501 Zeichen');
select throws_ok($$select public.report_missing_game('Zyxqtest Liste', null, null, null, 'foo')$$, 'P0001', 'report_list_invalid', 'KAT-04 Kriterium 3: unbekannte Liste');
select throws_ok($$select public.report_missing_game('Zyxqtest Liste', null, null, null, '')$$, 'P0001', 'report_list_invalid', 'KAT-04 Kriterium 3: leere Liste ist keine Liste');
select is((select count(*) from public.game_reports) + (select count(*) from public.games where owner_id = 'a1300000-0000-4000-8000-0000000000a1' and title like 'Zyxqtest%'),
0::bigint, 'KAT-04 Kriterium 1: nach allen Validierungsfehlern bleibt nichts zurück');
select lives_ok($$select public.report_missing_game('Zyxqtest Grenzwerte ' || repeat('t', 180), repeat('v', 200), 1800, 'https://' || repeat('l', 492))$$,
'KAT-04 Kriterium 1: Grenzwerte (Titel 200, Verlag 200, Jahr 1800, Link 500) sind gültig');
select lives_ok($$select public.report_missing_game('Zyxqtest Jahr 2100', null, 2100, 'http://example.org')$$, 'KAT-04 Kriterium 1: Jahr 2100 und http-Link sind gültig');
reset role;
select is((select count(*) from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000a1'), 2::bigint, 'Vorbedingung: Anna hat 2 Meldungen von den Grenzwert-Fällen');
delete from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000a1';
delete from public.games where owner_id = 'a1300000-0000-4000-8000-0000000000a1' and title like 'Zyxqtest%';
-- Erfolg ohne Liste: Trimmen, Felder, Rückgabe
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000a1","role":"authenticated"}', true);
set local role authenticated;
create temp table res1 as select public.report_missing_game(' Zyxqtest Eins ', ' ', 2019, ' https://example.org/eins ') as j;
grant all on res1 to public;
select ok((select j ? 'report_id' and j ? 'provisional_game_id' and j ? 'collection_item_id' and j->'collection_item_id' = 'null'::jsonb from res1),
'KAT-04 Kriterium 1: Rückgabe enthält report_id, provisional_game_id und collection_item_id null');
select results_eq($$select r.owner_id, r.origin, r.status, r.title, r.publisher_name, r.year, r.link, r.target_game_id, r.resolved_at, r.result_seen_at,
r.provisional_game_id = (res1.j->>'provisional_game_id')::uuid, r.created_at >= now() - interval '1 minute'
from public.game_reports r, res1 where r.id = (res1.j->>'report_id')::uuid$$,
$$values ('a1300000-0000-4000-8000-0000000000a1'::uuid, 'user', 'open', 'Zyxqtest Eins', null::text, 2019::smallint,
'https://example.org/eins', null::uuid, null::timestamptz, null::timestamptz, true, true)$$,
'KAT-04 Kriterium 1 und 8: Meldung gespeichert (getrimmt, Leerverlag null), Status offen, Herkunft user, kein Ziel');
select results_eq($$select g.title, g.year, g.visibility, g.owner_id, g.description, g.image_path, g.bgg_id, g.hidden_at, g.merged_into_id
from public.games g, res1 where g.id = (res1.j->>'provisional_game_id')::uuid$$,
$$values ('Zyxqtest Eins', 2019::smallint, 'private', 'a1300000-0000-4000-8000-0000000000a1'::uuid, null::text, null::text, null::integer, null::timestamptz, null::uuid)$$,
'KAT-04 Kriterium 3: vorläufiger Eintrag ist privat, gehört Anna und hat nur Titel und Jahr');
select is((select count(*) from public.collection_items where owner_id = 'a1300000-0000-4000-8000-0000000000a1'), 0::bigint,
'KAT-04 Kriterium 3: ohne p_list entsteht kein Sammlungseintrag');
-- Erfolg mit Liste
create temp table res2 as select public.report_missing_game('Zyxqtest Zwei', 'Zyxq Eigenverlag', null, null, 'wishlist') as j;
grant all on res2 to public;
select results_eq($$select c.owner_id, c.game_id = (res2.j->>'provisional_game_id')::uuid, c.list, c.variant_id, c.id = (res2.j->>'collection_item_id')::uuid
from public.collection_items c, res2 where c.id = (res2.j->>'collection_item_id')::uuid$$,
$$values ('a1300000-0000-4000-8000-0000000000a1'::uuid, true, 'wishlist', null::uuid, true)$$,
'KAT-04 Kriterium 3: p_list legt den Sammlungseintrag auf den vorläufigen Eintrag an, ohne Edition');
select is((select publisher_name from public.game_reports where id = (select (j->>'report_id')::uuid from res2)), 'Zyxq Eigenverlag',
'KAT-04 Kriterium 1: der Verlag wird als Freitext gespeichert');
create temp table res_own as select public.report_missing_game('Zyxqtest Besitz', null, null, null, 'owned') as j;
grant all on res_own to public;
select is((select list from public.collection_items where id = (select (j->>'collection_item_id')::uuid from res_own)), 'owned', 'KAT-04 Kriterium 3: Liste owned');
select lives_ok($$select public.report_missing_game('Zyxqtest Einkauf', null, null, null, 'shopping')$$, 'KAT-04 Kriterium 3: Liste shopping');
reset role;
-- Isolation: Ben sieht nichts von Anna
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000b2","role":"authenticated"}', true);
set local role authenticated;
select is((select count(*) from public.game_reports where title like 'Zyxqtest%'), 0::bigint, 'Sicherheit Kriterium 1: Ben sieht Annas Meldungen nicht');
select is((select count(*) from public.games where title like 'Zyxqtest%'), 0::bigint, 'Sicherheit Kriterium 3: Ben sieht Annas vorläufige Einträge nicht');
select is((select count(*) from public.collection_items where game_id in (select (j->>'provisional_game_id')::uuid from res1 union all select (j->>'provisional_game_id')::uuid from res2)), 0::bigint,
'Sicherheit Kriterium 3: Ben sieht Annas Listeneinträge nicht');
reset role;
-- Ähnlichkeitsprüfung beim Speichern (Krit. 2)
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000c3","role":"authenticated"}', true);
set local role authenticated;
select throws_ok($$select public.report_missing_game('Die Quorblaxwitz Towers', null, 2020)$$, 'P0001', 'report_similar_exists',
'KAT-04 Kriterium 2: ein sehr ähnlicher Katalogeintrag ohne Bestätigung wird nicht gespeichert');
select throws_ok($$select public.report_missing_game('Quorblaxwitz Towers', null, 2020, null, null, null)$$, 'P0001', 'report_similar_exists',
'KAT-04 Kriterium 2: p_confirmed_distinct = null gilt als nicht bestätigt');
select is((select count(*) from public.game_reports) + (select count(*) from public.games where owner_id = 'a1300000-0000-4000-8000-0000000000c3'), 0::bigint,
'KAT-04 Kriterium 2: ohne Bestätigung bleibt nichts zurück');
select lives_ok($$select public.report_missing_game('Die Quorblaxwitz Towers', null, 2020, null, null, true)$$,
'KAT-04 Kriterium 2: mit ausdrücklicher Bestätigung wird gespeichert');
select is((select count(*) from public.game_reports where title = 'Die Quorblaxwitz Towers'), 1::bigint, 'KAT-04 Kriterium 2: die bestätigte Meldung ist gespeichert');
reset role;
-- Rollback bei Fehler im Listeneintrag: ein Hilfstrigger lässt den Insert in collection_items scheitern
create function pg_temp.fail_ci() returns trigger language plpgsql as $$ begin raise exception 'boom' using errcode = 'P0001'; end $$;
create trigger fail_ci before insert on public.collection_items for each row execute function pg_temp.fail_ci();
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000c3","role":"authenticated"}', true);
set local role authenticated;
select throws_ok($$select public.report_missing_game('Zyxqtest Rollback', null, null, null, 'owned')$$, 'P0001', 'boom',
'Vorbedingung: der Listeneintrag scheitert');
select is((select count(*) from public.game_reports where title = 'Zyxqtest Rollback') + (select count(*) from public.games where title = 'Zyxqtest Rollback'), 0::bigint,
'KAT-04 Kriterium 3: scheitert der Listeneintrag, bleiben weder Meldung noch vorläufiges Spiel zurück');
reset role;
drop trigger fail_ci on public.collection_items;
-- ---------------------------------------------------------------------------------------------
-- 7. Tageslimit (Krit. 7)
-- ---------------------------------------------------------------------------------------------
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000e1","role":"authenticated"}', true);
set local role authenticated;
select lives_ok($$select public.report_missing_game('Tageslimit Meldung ' || i) from generate_series(1, 10) i$$,
'KAT-04 Kriterium 7: 10 Meldungen über report_missing_game sind erlaubt');
select throws_ok($$select public.report_missing_game('Tageslimit Meldung 11')$$, 'P0001', 'report_daily_limit', 'KAT-04 Kriterium 7: die 11. Meldung scheitert am Tageslimit');
select throws_ok($$select public.report_missing_game('Quorblaxwitz Towers', null, 2020)$$, 'P0001', 'report_daily_limit',
'KAT-04 Kriterium 7: das Limit wird vor der Ähnlichkeitsprüfung gemeldet');
select is((select count(*) from public.game_reports), 10::bigint, 'KAT-04 Kriterium 7: es bleiben genau 10 Meldungen');
select is((select count(*) from public.games where owner_id = 'a1300000-0000-4000-8000-0000000000e1'), 10::bigint, 'KAT-04 Kriterium 7: kein vorläufiges Spiel der abgelehnten Meldung bleibt übrig');
reset role;
-- Meldungen von gestern zählen nicht
insert into public.games (id, title, visibility, owner_id)
select ('f13b0000-0000-4000-8000-' || lpad(to_hex(i), 12, '0'))::uuid, 'Gestern ' || i, 'private', 'a1300000-0000-4000-8000-0000000000e4'
from generate_series(1, 13) i;
insert into public.game_reports (owner_id, title, provisional_game_id, created_at)
select 'a1300000-0000-4000-8000-0000000000e4', 'Gestern ' || i, ('f13b0000-0000-4000-8000-' || lpad(to_hex(i), 12, '0'))::uuid,
-- created_at ist nach dem Anlegen unveränderlich: feste Zeitstempel gleich beim Einfügen (Nr. 1 und 2 an der Berliner Tagesgrenze)
case i when 1 then '2026-01-15 22:59:00+00'::timestamptz when 2 then '2026-01-15 23:30:00+00'::timestamptz else '2026-01-01 12:00:00+00'::timestamptz end
from generate_series(1, 10) i;
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000e4","role":"authenticated"}', true);
set local role authenticated;
select lives_ok($$select public.report_missing_game('Heute nach zehn Meldungen von gestern')$$, 'KAT-04 Kriterium 7: Meldungen von gestern zählen nicht');
reset role;
-- Grenze des Berliner Tages mit festem Zeitstempel: 23:30 UTC im Winter (00:30 am Folgetag in Berlin) zählt nicht zu 22:59 UTC desselben Tages
select is((select count(*) from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000e4' and public.report_day(created_at) = '2026-01-15'), 1::bigint,
'KAT-04 Kriterium 7: der Berliner Kalendertag trennt 22:59 und 23:30 UTC im Winter');
-- Direkter Insert als authenticated: Trigger zählt, report_day ist für authenticated ausführbar
insert into public.games (id, title, visibility, owner_id)
select ('f13b0000-0000-4000-8000-' || lpad(to_hex(100 + i), 12, '0'))::uuid, 'Direkt ' || i, 'private', 'a1300000-0000-4000-8000-0000000000e2'
from generate_series(1, 11) i;
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000e2","role":"authenticated"}', true);
set local role authenticated;
select lives_ok($$insert into public.game_reports (owner_id, title, provisional_game_id)
select 'a1300000-0000-4000-8000-0000000000e2', 'Direkt ' || i, ('f13b0000-0000-4000-8000-' || lpad(to_hex(100 + i), 12, '0'))::uuid from generate_series(1, 10) i$$,
'KAT-04 Kriterium 7: 10 direkte Inserts als authenticated sind erlaubt (der Trigger darf report_day ausführen)');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values
('a1300000-0000-4000-8000-0000000000e2', 'Direkt 11', 'f13b0000-0000-4000-8000-00000000006f')$$, 'P0001', 'report_daily_limit',
'KAT-04 Kriterium 7: auch der direkte 11. Insert scheitert am Limit');
reset role;
-- Import-Meldungen zählen nicht und werden nicht begrenzt (N12)
insert into public.game_reports (owner_id, origin, title, bgg_id)
select 'a1300000-0000-4000-8000-0000000000e3', 'import', 'Import ' || i, 9000 + i from generate_series(1, 12) i;
select is((select count(*) from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000e3' and origin = 'import'), 12::bigint,
'KAT-04 Kriterium 7 (N12): mehr als 10 Import-Meldungen werden nicht begrenzt');
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000e3","role":"authenticated"}', true);
set local role authenticated;
select lives_ok($$select public.report_missing_game('Zyxqtest nach Import ' || i) from generate_series(1, 10) i$$,
'KAT-04 Kriterium 7 (N12): Import-Meldungen zählen nicht auf das Limit des Nutzers');
select throws_ok($$select public.report_missing_game('Zyxqtest nach Import 11')$$, 'P0001', 'report_daily_limit',
'KAT-04 Kriterium 7: danach greift das Limit für Nutzer-Meldungen');
reset role;
-- Meldungen von Nutzer B zählen nicht für Nutzer A: Anna hat oben nur wenige, Ben (e1 hat 10) ändert nichts
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000a1","role":"authenticated"}', true);
set local role authenticated;
select lives_ok($$select public.report_missing_game('Zyxqtest Anna nach Limit anderer')$$, 'KAT-04 Kriterium 7: die Meldungen anderer Nutzer zählen nicht für Anna');
reset role;
-- ---------------------------------------------------------------------------------------------
-- 8. Direkter Zugriff auf game_reports: Policies und Spaltenrechte (Krit. 8, Sicherheit 1 und 3)
-- ---------------------------------------------------------------------------------------------
insert into public.games (id, title, visibility, owner_id, hidden_at, merged_into_id) values
('f13a0000-0000-4000-8000-000000000011', 'Direkt ok', 'private', 'a1300000-0000-4000-8000-0000000000c3', null, null),
('f13a0000-0000-4000-8000-000000000012', 'Direkt versteckt', 'private', 'a1300000-0000-4000-8000-0000000000c3', now(), null),
('f13a0000-0000-4000-8000-000000000013', 'Direkt zusammengeführt', 'private', 'a1300000-0000-4000-8000-0000000000c3', now(), 'f13c0000-0000-4000-8000-00000000000d'),
('f13a0000-0000-4000-8000-000000000014', 'Bens Privatspiel', 'private', 'a1300000-0000-4000-8000-0000000000b2', null, null),
('f13a0000-0000-4000-8000-000000000015', 'Direkt ok 2', 'private', 'a1300000-0000-4000-8000-0000000000c3', null, null);
-- Hilfsfunktion (läuft mit den Rechten des Aufrufers): result_seen_at setzen und die Zahl der geänderten Zeilen liefern.
-- (Ein datenändernder WITH-Block ist in einer Unterabfrage nicht erlaubt.)
create function pg_temp.mark_seen(p_id uuid, p_at timestamptz) returns bigint language plpgsql as $$
declare n bigint;
begin
update public.game_reports set result_seen_at = p_at where p_id is null or id = p_id;
get diagnostics n = row_count;
return n;
end $$;
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000c3","role":"authenticated"}', true);
set local role authenticated;
select lives_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'Direkt ok', 'f13a0000-0000-4000-8000-000000000011')$$,
'Sicherheit Kriterium 1: Cara legt direkt eine Meldung zu ihrem eigenen privaten Spiel an (Positivfall)');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000b2', 'Im Namen von Ben', 'f13a0000-0000-4000-8000-000000000014')$$, '42501', null,
'Sicherheit Kriterium 1: Cara kann keine Meldung für Ben anlegen');
select throws_ok($$insert into public.game_reports (owner_id, title) values ('a1300000-0000-4000-8000-0000000000c3', 'Ohne vorläufigen Eintrag')$$, '42501', null,
'Sicherheit Kriterium 1: eine Meldung ohne vorläufigen Eintrag wird abgelehnt');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'Bens Spiel', 'f13a0000-0000-4000-8000-000000000014')$$, '42501', null,
'Sicherheit Kriterium 1: ein fremdes privates Spiel als vorläufiger Eintrag wird abgelehnt');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'Katalogspiel', 'f13c0000-0000-4000-8000-000000000001')$$, '42501', null,
'Sicherheit Kriterium 1: ein Katalogspiel als vorläufiger Eintrag wird abgelehnt');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'Versteckt', 'f13a0000-0000-4000-8000-000000000012')$$, '42501', null,
'Sicherheit Kriterium 1: ein ausgeblendetes Spiel als vorläufiger Eintrag wird abgelehnt');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'Zusammengeführt', 'f13a0000-0000-4000-8000-000000000013')$$, '42501', null,
'Sicherheit Kriterium 1: ein zusammengeführtes Spiel als vorläufiger Eintrag wird abgelehnt');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'Doppelt', 'f13a0000-0000-4000-8000-000000000011')$$, '23505', null,
'Sicherheit Kriterium 1: eine zweite Meldung zum selben vorläufigen Eintrag scheitert am eindeutigen Index');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id, status) values ('a1300000-0000-4000-8000-0000000000c3', 'S', 'f13a0000-0000-4000-8000-000000000015', 'approved')$$, '42501', null,
'KAT-04 Kriterium 8: status lässt sich beim Anlegen nicht setzen');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id, origin) values ('a1300000-0000-4000-8000-0000000000c3', 'S', 'f13a0000-0000-4000-8000-000000000015', 'import')$$, '42501', null,
'KAT-04 Kriterium 7: origin lässt sich beim Anlegen nicht setzen (Import-Meldungen umgehen das Limit nicht)');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id, created_at) values ('a1300000-0000-4000-8000-0000000000c3', 'S', 'f13a0000-0000-4000-8000-000000000015', '2026-01-01')$$, '42501', null,
'KAT-04 Kriterium 7: created_at lässt sich beim Anlegen nicht setzen');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id, target_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'S', 'f13a0000-0000-4000-8000-000000000015', 'f13c0000-0000-4000-8000-000000000001')$$, '42501', null,
'KAT-04 Kriterium 8: target_game_id lässt sich beim Anlegen nicht setzen');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id, resolved_at) values ('a1300000-0000-4000-8000-0000000000c3', 'S', 'f13a0000-0000-4000-8000-000000000015', now())$$, '42501', null,
'KAT-04 Kriterium 8: resolved_at lässt sich beim Anlegen nicht setzen');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id, bgg_id) values ('a1300000-0000-4000-8000-0000000000c3', 'S', 'f13a0000-0000-4000-8000-000000000015', 1)$$, '42501', null,
'KAT-04: bgg_id lässt sich beim Anlegen nicht setzen');
select throws_ok($$insert into public.game_reports (id, owner_id, title, provisional_game_id) values (gen_random_uuid(), 'a1300000-0000-4000-8000-0000000000c3', 'S', 'f13a0000-0000-4000-8000-000000000015')$$, '42501', null,
'KAT-04: id lässt sich beim Anlegen nicht setzen');
select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', '', 'f13a0000-0000-4000-8000-000000000015')$$, '23514', null,
'KAT-04 Kriterium 1: ein leerer Titel scheitert am Tabellen-Check');
select throws_ok($$insert into public.game_reports (owner_id, title, year, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000c3', 'S', 1700, 'f13a0000-0000-4000-8000-000000000015')$$, '23514', null,
'KAT-04 Kriterium 1: ein Jahr außerhalb 1800-2100 scheitert am Tabellen-Check');
select throws_ok($$update public.game_reports set status = 'approved' where owner_id = 'a1300000-0000-4000-8000-0000000000c3'$$, '42501', null,
'KAT-04 Kriterium 8: status lässt sich nicht ändern');
select throws_ok($$update public.game_reports set title = 'Umbenannt' where owner_id = 'a1300000-0000-4000-8000-0000000000c3'$$, '42501', null,
'KAT-04: title lässt sich nicht ändern');
select throws_ok($$update public.game_reports set target_game_id = 'f13c0000-0000-4000-8000-000000000001' where owner_id = 'a1300000-0000-4000-8000-0000000000c3'$$, '42501', null,
'KAT-04 Kriterium 8: target_game_id lässt sich nicht ändern');
select throws_ok($$update public.game_reports set resolved_at = now() where owner_id = 'a1300000-0000-4000-8000-0000000000c3'$$, '42501', null,
'KAT-04 Kriterium 8: resolved_at lässt sich nicht ändern');
select throws_ok($$delete from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000c3'$$, '42501', null,
'Sicherheit Kriterium 1: eigene Meldungen lassen sich nicht löschen');
-- result_seen_at bei offener Meldung: 0 Zeilen
select is(pg_temp.mark_seen(null, now()), 0::bigint,
'KAT-04 Kriterium 8: result_seen_at an einer offenen Meldung ändert keine Zeile');
reset role;
select throws_ok($$update public.game_reports set result_seen_at = now() where owner_id = 'a1300000-0000-4000-8000-0000000000c3'$$, '23514', null,
'KAT-04 Kriterium 8: auch postgres kann result_seen_at bei einer offenen Meldung nicht setzen (Check)');
-- ---------------------------------------------------------------------------------------------
-- 9. Ergebnis nach admin_resolve_report (Krit. 4 und 6), result_seen_at
-- ---------------------------------------------------------------------------------------------
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000a1","role":"authenticated"}', true);
set local role authenticated;
create temp table res_ap as select public.report_missing_game('Freigabezyxq Neu', 'Freigabe Verlag', 2023, null, 'owned') as j;
grant all on res_ap to public;
create temp table res_rej as select public.report_missing_game('Ablehnzyxq Spiel', null, null, null, 'wishlist') as j;
grant all on res_rej to public;
update public.collection_items set note = 'Annas Notiz', acquired_on = '2025-05-05'
where id = (select (j->>'collection_item_id')::uuid from res_ap);
reset role;
insert into public.game_variants (id, game_id, name) values
('f13a0000-0000-4000-8000-0000000000b1', (select (j->>'provisional_game_id')::uuid from res_ap), 'Eigene Edition');
update public.collection_items set variant_id = 'f13a0000-0000-4000-8000-0000000000b1'
where id = (select (j->>'collection_item_id')::uuid from res_ap);
insert into public.games (id, title, year, visibility) values ('f13c0000-0000-4000-8000-000000000016', 'Freigabezyxq Neu', 2023, 'catalog');
select lives_ok($$select public.admin_resolve_report((select (j->>'report_id')::uuid from res_ap), 'approved', 'f13c0000-0000-4000-8000-000000000016')$$,
'Vorbedingung: der Admin gibt die Meldung frei');
select lives_ok($$select public.admin_resolve_report((select (j->>'report_id')::uuid from res_rej), 'rejected')$$,
'Vorbedingung: der Admin lehnt die zweite Meldung ohne Ziel ab');
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000a1","role":"authenticated"}', true);
set local role authenticated;
select results_eq($$select game_id, list, note, acquired_on, variant_id from public.collection_items where id = (select (j->>'collection_item_id')::uuid from res_ap)$$,
$$values ('f13c0000-0000-4000-8000-000000000016'::uuid, 'owned', 'Annas Notiz', '2025-05-05'::date, null::uuid)$$,
'KAT-04 Kriterium 4: der Katalogeintrag ersetzt den vorläufigen; Liste, Notiz und Kaufdatum bleiben, die Edition ohne gleichnamige Variante ist leer');
select results_eq($$select r.status, r.target_game_id, r.editions_cleared, r.result_seen_at from public.game_reports r where r.id = (select (j->>'report_id')::uuid from res_ap)$$,
$$values ('approved', 'f13c0000-0000-4000-8000-000000000016'::uuid, 1, null::timestamptz)$$,
'KAT-04 Kriterium 4: Meldung freigegeben, editions_cleared zählt die geleerte Edition, Hinweis noch ungesehen');
select is((select count(*) from public.game_variants where id = 'f13a0000-0000-4000-8000-0000000000b1'), 1::bigint,
'KAT-04 Kriterium 4: Annas private Variante bleibt für sie lesbar');
select results_eq($$select g.visibility, g.merged_into_id, g.hidden_at is not null from public.games g where g.id = (select (j->>'provisional_game_id')::uuid from res_ap)$$,
$$values ('private', 'f13c0000-0000-4000-8000-000000000016'::uuid, true)$$,
'KAT-04 Kriterium 4: der vorläufige Eintrag ist ausgeblendet und zeigt auf den Katalogeintrag, Anna liest ihn weiter');
select is(pg_temp.mark_seen((select (j->>'report_id')::uuid from res_ap), '2026-11-04 10:00:00+00'), 1::bigint,
'KAT-04 Kriterium 4: Anna kann den Hinweis an der freigegebenen Meldung wegklicken (result_seen_at)');
select is((select result_seen_at from public.game_reports where id = (select (j->>'report_id')::uuid from res_ap)), '2026-11-04 10:00:00+00'::timestamptz,
'KAT-04 Kriterium 4: result_seen_at ist gesetzt');
-- Krit. 6
select results_eq($$select r.status, r.target_game_id, r.provisional_game_id is not null from public.game_reports r where r.id = (select (j->>'report_id')::uuid from res_rej)$$,
$$values ('rejected', null::uuid, true)$$, 'KAT-04 Kriterium 6: abgelehnt ohne Ziel, der vorläufige Eintrag ist noch verknüpft');
select results_eq($$select g.visibility, g.merged_into_id, g.hidden_at from public.games g where g.id = (select (j->>'provisional_game_id')::uuid from res_rej)$$,
$$values ('private', null::uuid, null::timestamptz)$$, 'KAT-04 Kriterium 6: der vorläufige Eintrag bleibt privat, sichtbar und lesbar');
select is((select count(*) from public.collection_items where id = (select (j->>'collection_item_id')::uuid from res_rej) and game_id = (select (j->>'provisional_game_id')::uuid from res_rej)), 1::bigint,
'KAT-04 Kriterium 6: der Listeneintrag bleibt am vorläufigen Eintrag');
select is(pg_temp.mark_seen((select (j->>'report_id')::uuid from res_rej), now()), 1::bigint,
'KAT-04 Kriterium 6: auch an der abgelehnten Meldung lässt sich der Hinweis wegklicken');
reset role;
-- Fremder Nutzer kann nichts wegklicken
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000b2","role":"authenticated"}', true);
set local role authenticated;
select is(pg_temp.mark_seen((select (j->>'report_id')::uuid from res_ap), now()), 0::bigint,
'Sicherheit Kriterium 1: Ben kann bei fremden Meldungen nichts wegklicken (0 Zeilen)');
reset role;
-- ---------------------------------------------------------------------------------------------
-- 10. adopt_report_target (Krit. 5)
-- ---------------------------------------------------------------------------------------------
-- R1: vollständiger Wechsel. Anna hat am vorläufigen Eintrag Sammlung, Favorit, Partien, Wertungsbogen, zwei Varianten.
insert into public.games (id, title, year, visibility, owner_id) values
('f13a0000-0000-4000-8000-000000000101', 'Adoptzyxq Eigen', 2021, 'private', 'a1300000-0000-4000-8000-0000000000a1');
insert into public.game_variants (id, game_id, name) values
('f13a0000-0000-4000-8000-0000000001b1', 'f13a0000-0000-4000-8000-000000000101', ' standard '),
('f13a0000-0000-4000-8000-0000000001b2', 'f13a0000-0000-4000-8000-000000000101', 'Prototyp');
insert into public.collection_items (id, owner_id, game_id, variant_id, list, note, acquired_on) values
('f13a0000-0000-4000-8000-0000000001c1', 'a1300000-0000-4000-8000-0000000000a1', 'f13a0000-0000-4000-8000-000000000101', 'f13a0000-0000-4000-8000-0000000001b2', 'owned', 'Notiz bleibt', '2025-01-02');
insert into public.favorite_games (id, profile_id, game_id, position) values
('f13a0000-0000-4000-8000-0000000001d1', 'a1300000-0000-4000-8000-0000000000a1', 'f13a0000-0000-4000-8000-000000000101', 1);
insert into public.plays (id, owner_id, game_id, variant_id, played_at, client_id) values
('f13a0000-0000-4000-8000-0000000001e1', 'a1300000-0000-4000-8000-0000000000a1', 'f13a0000-0000-4000-8000-000000000101', 'f13a0000-0000-4000-8000-0000000001b1', '2026-02-01', 'f13a0000-0000-4000-8000-0000000001e1'),
('f13a0000-0000-4000-8000-0000000001e2', 'a1300000-0000-4000-8000-0000000000a1', 'f13a0000-0000-4000-8000-000000000101', 'f13a0000-0000-4000-8000-0000000001b2', '2026-02-02', 'f13a0000-0000-4000-8000-0000000001e2');
insert into public.score_sheets (id, owner_id, game_id, name) values
('f13a0000-0000-4000-8000-0000000001f1', 'a1300000-0000-4000-8000-0000000000a1', 'f13a0000-0000-4000-8000-000000000101', 'Bogen');
insert into public.game_reports (id, owner_id, title, provisional_game_id, status, target_game_id, resolved_at, created_at) values
('f13a0000-0000-4000-8000-000000000a01', 'a1300000-0000-4000-8000-0000000000a1', 'Adoptzyxq Eigen', 'f13a0000-0000-4000-8000-000000000101', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01');
-- Hilfsfälle für die Fehlerpfade
insert into public.games (id, title, visibility, owner_id) values
('f13a0000-0000-4000-8000-000000000102', 'Adopt offen', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-000000000103', 'Adopt ohne Ziel', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-000000000104', 'Adopt freigegeben', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-000000000105', 'Adopt Ziel versteckt', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-000000000106', 'Adopt Ziel zusammengefuehrt', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-000000000107', 'Adopt Bens Eintrag', 'private', 'a1300000-0000-4000-8000-0000000000b2'),
('f13a0000-0000-4000-8000-0000000001a1', 'Adopt fremd Partie', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-0000000001a2', 'Adopt Partie ohne Eigentuemer', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-0000000001a3', 'Adopt fremd Sammlung', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-0000000001a4', 'Adopt fremd Favorit', 'private', 'a1300000-0000-4000-8000-0000000000a1'),
('f13a0000-0000-4000-8000-0000000001a5', 'Adopt fremd Bogen', 'private', 'a1300000-0000-4000-8000-0000000000a1');
insert into public.game_reports (id, owner_id, title, provisional_game_id, status, target_game_id, resolved_at, created_at) values
('f13a0000-0000-4000-8000-000000000a02', 'a1300000-0000-4000-8000-0000000000a1', 'offen', 'f13a0000-0000-4000-8000-000000000102', 'open', null, null, '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a03', 'a1300000-0000-4000-8000-0000000000a1', 'ohne Ziel', 'f13a0000-0000-4000-8000-000000000103', 'rejected', null, now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a04', 'a1300000-0000-4000-8000-0000000000a1', 'freigegeben', 'f13a0000-0000-4000-8000-000000000104', 'approved', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a05', 'a1300000-0000-4000-8000-0000000000a1', 'ohne vorläufigen Eintrag', null, 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a06', 'a1300000-0000-4000-8000-0000000000a1', 'Ziel versteckt', 'f13a0000-0000-4000-8000-000000000105', 'rejected', 'f13c0000-0000-4000-8000-000000000014', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a07', 'a1300000-0000-4000-8000-0000000000a1', 'Ziel zusammengeführt', 'f13a0000-0000-4000-8000-000000000106', 'rejected', 'f13c0000-0000-4000-8000-000000000015', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a08', 'a1300000-0000-4000-8000-0000000000a1', 'Eintrag gehört Ben', 'f13a0000-0000-4000-8000-000000000107', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a09', 'a1300000-0000-4000-8000-0000000000a1', 'Eintrag ist Katalog', 'f13c0000-0000-4000-8000-000000000001', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a11', 'a1300000-0000-4000-8000-0000000000a1', 'fremde Partie', 'f13a0000-0000-4000-8000-0000000001a1', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a12', 'a1300000-0000-4000-8000-0000000000a1', 'Partie ohne Eigentümer', 'f13a0000-0000-4000-8000-0000000001a2', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a13', 'a1300000-0000-4000-8000-0000000000a1', 'fremde Sammlung', 'f13a0000-0000-4000-8000-0000000001a3', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a14', 'a1300000-0000-4000-8000-0000000000a1', 'fremder Favorit', 'f13a0000-0000-4000-8000-0000000001a4', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01'),
('f13a0000-0000-4000-8000-000000000a15', 'a1300000-0000-4000-8000-0000000000a1', 'fremder Bogen', 'f13a0000-0000-4000-8000-0000000001a5', 'rejected', 'f13c0000-0000-4000-8000-000000000013', now(), '2026-02-01');
insert into public.plays (id, owner_id, game_id, played_at, client_id) values
('f13a0000-0000-4000-8000-0000000001e3', 'a1300000-0000-4000-8000-0000000000b2', 'f13a0000-0000-4000-8000-0000000001a1', '2026-02-03', 'f13a0000-0000-4000-8000-0000000001e3'),
('f13a0000-0000-4000-8000-0000000001e4', null, 'f13a0000-0000-4000-8000-0000000001a2', '2026-02-04', 'f13a0000-0000-4000-8000-0000000001e4');
insert into public.collection_items (owner_id, game_id) values ('a1300000-0000-4000-8000-0000000000b2', 'f13a0000-0000-4000-8000-0000000001a3');
insert into public.favorite_games (profile_id, game_id, position) values ('a1300000-0000-4000-8000-0000000000b2', 'f13a0000-0000-4000-8000-0000000001a4', 1);
insert into public.score_sheets (owner_id, game_id, name) values ('a1300000-0000-4000-8000-0000000000b2', 'f13a0000-0000-4000-8000-0000000001a5', 'Bogen Ben');
-- Fremde und ungültige Aufrufe
select set_config('request.jwt.claims', '{"role":"authenticated"}', true);
set local role authenticated;
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a01')$$, 'P0001', 'not_authenticated',
'Sicherheit Kriterium 2: ohne Nutzerkennung (auth.uid() null) wird nichts übernommen');
reset role;
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000b2","role":"authenticated"}', true);
set local role authenticated;
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a01')$$, 'P0001', 'report_not_found',
'Sicherheit Kriterium 1: Ben kann Annas Meldung nicht übernehmen');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-00000000ffff')$$, 'P0001', 'report_not_found',
'KAT-04 Kriterium 5: unbekannte Meldung (gleicher Fehler wie fremde)');
select throws_ok($$select public.adopt_report_target(null)$$, 'P0001', 'report_not_found', 'KAT-04 Kriterium 5: Meldung null');
reset role;
select is((select merged_into_id from public.games where id = 'f13a0000-0000-4000-8000-000000000101'), null::uuid,
'Sicherheit Kriterium 1: Bens Versuch hat Annas vorläufigen Eintrag nicht verändert');
select is((select game_id from public.collection_items where id = 'f13a0000-0000-4000-8000-0000000001c1'), 'f13a0000-0000-4000-8000-000000000101'::uuid,
'Sicherheit Kriterium 1: Bens Versuch hat Annas Sammlung nicht umgehängt');
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000a1","role":"authenticated"}', true);
set local role authenticated;
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a02')$$, 'P0001', 'report_not_adoptable', 'KAT-04 Kriterium 5: offene Meldung');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a03')$$, 'P0001', 'report_not_adoptable', 'KAT-04 Kriterium 6: abgelehnt ohne Ziel gibt es nichts zu übernehmen');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a04')$$, 'P0001', 'report_not_adoptable', 'KAT-04 Kriterium 5: freigegebene Meldung');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a05')$$, 'P0001', 'report_no_provisional', 'KAT-04 Kriterium 5: vorläufiger Eintrag fehlt (vom Nutzer gelöscht)');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a06')$$, 'P0001', 'report_target_not_listed', 'KAT-04 Kriterium 5: Ziel ist ausgeblendet');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a07')$$, 'P0001', 'report_target_not_listed', 'KAT-04 Kriterium 5: Ziel ist zusammengeführt');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a08')$$, 'P0001', 'report_not_found', 'Sicherheit Kriterium 3: der vorläufige Eintrag gehört einem anderen Nutzer');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a09')$$, 'P0001', 'report_not_found', 'Sicherheit Kriterium 3: der vorläufige Eintrag ist ein Katalogspiel');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a11')$$, 'P0001', 'report_foreign_references', 'Sicherheit Kriterium 3: Partie eines anderen Eigentümers am vorläufigen Eintrag');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a12')$$, 'P0001', 'report_foreign_references', 'Sicherheit Kriterium 3: Partie ohne Eigentümer (null zählt als fremd)');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a13')$$, 'P0001', 'report_foreign_references', 'Sicherheit Kriterium 3: Sammlungseintrag eines anderen Nutzers');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a14')$$, 'P0001', 'report_foreign_references', 'Sicherheit Kriterium 3: Favorit eines anderen Nutzers');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a15')$$, 'P0001', 'report_foreign_references', 'Sicherheit Kriterium 3: Wertungsbogen eines anderen Nutzers');
reset role;
select is((select count(*) from public.games where id::text like 'f13a0000-0000-4000-8000-0000000001a%' and merged_into_id is null and hidden_at is null), 5::bigint,
'Sicherheit Kriterium 3: nach allen Fehlern ist keiner der geprüften vorläufigen Einträge verändert');
select is((select count(*) from public.plays where id in ('f13a0000-0000-4000-8000-0000000001e3', 'f13a0000-0000-4000-8000-0000000001e4') and game_id::text like 'f13a0000-0000-4000-8000-0000000001a%'), 2::bigint,
'Sicherheit Kriterium 3: fremde Partien wurden nicht umgehängt');
-- Erfolg
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000a1","role":"authenticated"}', true);
set local role authenticated;
create temp table res_adopt as select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a01') as j;
grant all on res_adopt to public;
select results_eq($$select (j->>'collection_items')::int, (j->>'favorite_games')::int, (j->>'plays')::int, (j->>'score_sheets')::int,
(j->>'variants_copied')::int, (j->>'variants_mapped')::int, (j->>'variants_cleared')::int from res_adopt$$,
$$values (1, 1, 2, 1, 0, 1, 2)$$,
'KAT-04 Kriterium 5: Rückgabe zählt Sammlung, Favorit, Partien, Wertungsbogen; keine Variante kopiert, eine abgebildet, zwei geleert');
select results_eq($$select game_id, list, note, acquired_on, variant_id from public.collection_items where id = 'f13a0000-0000-4000-8000-0000000001c1'$$,
$$values ('f13c0000-0000-4000-8000-000000000013'::uuid, 'owned', 'Notiz bleibt', '2025-01-02'::date, null::uuid)$$,
'KAT-04 Kriterium 5: Sammlungseintrag zeigt aufs Ziel, Liste/Notiz/Kaufdatum bleiben, die nicht passende Edition ist geleert');
select results_eq($$select id, game_id, variant_id from public.plays where owner_id = 'a1300000-0000-4000-8000-0000000000a1' and id in ('f13a0000-0000-4000-8000-0000000001e1', 'f13a0000-0000-4000-8000-0000000001e2') order by id$$,
$$values ('f13a0000-0000-4000-8000-0000000001e1'::uuid, 'f13c0000-0000-4000-8000-000000000013'::uuid, 'f13c0000-0000-4000-8000-0000000000b1'::uuid),
('f13a0000-0000-4000-8000-0000000001e2'::uuid, 'f13c0000-0000-4000-8000-000000000013'::uuid, null::uuid)$$,
'KAT-04 Kriterium 5: Partien zeigen aufs Ziel, die gleichnamige Edition wird abgebildet, die andere geleert');
select is((select game_id from public.favorite_games where id = 'f13a0000-0000-4000-8000-0000000001d1'), 'f13c0000-0000-4000-8000-000000000013'::uuid, 'KAT-04 Kriterium 5: Favorit zeigt aufs Ziel');
select is((select game_id from public.score_sheets where id = 'f13a0000-0000-4000-8000-0000000001f1'), 'f13c0000-0000-4000-8000-000000000013'::uuid, 'KAT-04 Kriterium 5: Wertungsbogen zeigt aufs Ziel');
select results_eq($$select visibility, merged_into_id, hidden_at is not null from public.games where id = 'f13a0000-0000-4000-8000-000000000101'$$,
$$values ('private', 'f13c0000-0000-4000-8000-000000000013'::uuid, true)$$,
'KAT-04 Kriterium 5: der vorläufige Eintrag ist ausgeblendet und zeigt aufs Ziel, bleibt privat');
select results_eq($$select status, target_game_id, result_seen_at, editions_cleared from public.game_reports where id = 'f13a0000-0000-4000-8000-000000000a01'$$,
$$values ('rejected', 'f13c0000-0000-4000-8000-000000000013'::uuid, null::timestamptz, 2)$$,
'KAT-04 Kriterium 5: Status bleibt rejected, result_seen_at bleibt null, editions_cleared ist gesetzt');
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a01')$$, 'P0001', 'report_already_adopted', 'KAT-04 Kriterium 5: ein zweiter Wechsel scheitert');
reset role;
-- ---------------------------------------------------------------------------------------------
-- 11. Datenschutz: Konto löschen (QUE-04, Klärung 10)
-- ---------------------------------------------------------------------------------------------
select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000d4","role":"authenticated"}', true);
set local role authenticated;
select lives_ok($$select public.report_missing_game('Zyxqtest Danas Spiel', null, null, null, 'owned')$$, 'Vorbedingung: Dana meldet ein Spiel');
reset role;
select is((select count(*) from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000d4'), 1::bigint, 'Vorbedingung: Dana hat eine Meldung');
select lives_ok($$delete from auth.users where id = 'a1300000-0000-4000-8000-0000000000d4'$$, 'QUE-04: Danas Konto lässt sich löschen');
select is((select count(*) from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000d4')
+ (select count(*) from public.games where owner_id = 'a1300000-0000-4000-8000-0000000000d4')
+ (select count(*) from public.collection_items where owner_id = 'a1300000-0000-4000-8000-0000000000d4'), 0::bigint,
'QUE-04: Meldungen, vorläufige Spiele und Listeneinträge sind mit dem Konto gelöscht');
select cmp_ok((select count(*) from public.game_reports where owner_id = 'a1300000-0000-4000-8000-0000000000a1'), '>', 0::bigint, 'QUE-04: Anna hat weiterhin ihre Meldungen');
select * from finish();
rollback;