diff --git a/docs/specs/13-umsetzung.md b/docs/specs/13-umsetzung.md index 76256d8..8075265 100644 --- a/docs/specs/13-umsetzung.md +++ b/docs/specs/13-umsetzung.md @@ -14,6 +14,10 @@ Ergänzt `docs/specs/13-fehlendes-spiel-melden.md` (die Spec selbst wird hier ni - **Eigentümer von `adopt_report_target`:** Die Migration ändert den Eigentümer nicht ausdrücklich (`alter function … owner to`); sie läuft als `postgres` (lokal und per Supabase CLI). Der Test prüft `proowner = postgres`. - **Testplan:** `00_schema` unverändert (22 Tabellen), `07`/`08`-Fremdschlüssel-Wächter unberührt (kein neuer Fremdschlüssel), `08` nur um das Abschalten des Limit-Triggers ergänzt (Erwartungen und Beschreibungen unverändert gültig: die Spaltenrechte aus 08 waren schon auf `result_seen_at` und die Insert-Spalten ausgelegt). +- **Review-Nachbesserungen:** `find_similar_catalog_games` behandelt ein Jahr außerhalb 1800-2100 wie `null` (kein Überlauf, kein 22003). Zusätzliche Tests: Tagesgrenze Berlin (10 Meldungen kurz nach Mitternacht Berlin, Trigger und Vorprüfung), Trimmen von Tab/Zeilenumbruch. +- **08-Fixtures (N3) bleiben bei `disable trigger`:** Ben und weitere Nutzer haben dort über 20 Fixture-Inserts mit Standard-`created_at`; sie alle auf feste alte Zeitstempel umzustellen wäre ein großer, fehleranfälliger Eingriff in 2c-Tests. Das Limit prüft `10` ausführlich. +- **Zielsperre (N1) unverändert `for update`:** nicht umgestellt, weil `admin_resolve_report` und `admin_merge_games` ebenfalls `for update` sperren und eine gemischte Sperrstufe die Verklemmungsanalyse (Spec R2) ändern würde. + ## 2. Mutationsprüfung Die Mutanten sind veränderte Kopien der Migration, jeweils in einer Transaktion zusammen mit dem Testlauf angewendet und mit dem Rollback des Tests zurückgenommen (am echten Schema bleibt nichts). Die Skripte liegen nicht im Repo. Ergebnis siehe Bericht an Chris; die überlebenden Mutanten sind entweder Doppelabsicherungen (zum Beispiel `origin = 'user'` in der Insert-Policy, die schon durch die Spaltenrechte gesichert ist) oder nur mit Parallelität testbar (Advisory-Lock). diff --git a/supabase/migrations/20261104100000_game_reports_user.sql b/supabase/migrations/20261104100000_game_reports_user.sql index 700a631..474afc4 100644 --- a/supabase/migrations/20261104100000_game_reports_user.sql +++ b/supabase/migrations/20261104100000_game_reports_user.sql @@ -150,7 +150,9 @@ stable set search_path = '' as $$ with q as ( - select public.normalize_game_title(btrim(p_title)) as nk + -- Jahr außerhalb 1800-2100 gilt wie null (kein Überlauf bei integer-Arithmetik, kein 22003) + select public.normalize_game_title(btrim(p_title)) as nk, + case when p_year between 1800 and 2100 then p_year end as y where p_title is not null and char_length(btrim(p_title)) between 1 and 200 ), @@ -172,7 +174,7 @@ as $$ ) k ), scored as ( - select cd.*, (cd.cand_key = q.nk) as is_same, + select cd.*, q.y, (cd.cand_key = q.nk) as is_same, extensions.similarity(cd.cand_key, q.nk) as sim from cand cd cross join q where q.nk <> '' @@ -183,7 +185,7 @@ as $$ from scored s where s.is_same or (s.sim >= 0.8 - and (p_year is null or s.game_year is null or abs(s.game_year - p_year) <= 1)) + and (s.y is null or s.game_year is null or abs(s.game_year - s.y) <= 1)) order by s.game_id, s.is_same desc, s.sim desc, s.ord, s.cand_title ) select h.game_id, diff --git a/supabase/tests/database/10_game_reports_user.test.sql b/supabase/tests/database/10_game_reports_user.test.sql index f66c964..766afd4 100644 --- a/supabase/tests/database/10_game_reports_user.test.sql +++ b/supabase/tests/database/10_game_reports_user.test.sql @@ -6,7 +6,7 @@ -- 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(206); +select plan(213); 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"}'), @@ -16,7 +16,9 @@ insert into auth.users (id, email, raw_user_meta_data) values ('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"}'); + ('a1300000-0000-4000-8000-0000000000e4', 'kat04-y@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"KY"}'), + ('a1300000-0000-4000-8000-0000000000e5', 'kat04-t1@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"T1"}'), + ('a1300000-0000-4000-8000-0000000000e6', 'kat04-t2@example.test', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"T2"}'); -- --------------------------------------------------------------------------------------------- -- 1. Funktionen: Rechte, Attribute (Spec 4.1) @@ -184,6 +186,12 @@ select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz '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 Towe', 2147483647)), 1::bigint, + 'KAT-04 Kriterium 2: ein Jahr weit außerhalb 1800-2100 gilt wie ein fehlendes Jahr (kein Überlauf)'); +select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towe', -2147483648)), 1::bigint, + 'KAT-04 Kriterium 2: ein stark negatives Jahr gilt wie ein fehlendes Jahr (kein 22003)'); +select is((select count(*) from public.find_similar_catalog_games('Quorblaxwitz Towe', 1700)), 1::bigint, + 'KAT-04 Kriterium 2: Jahr 1700 gilt wie ein fehlendes Jahr'); 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, @@ -307,6 +315,11 @@ select results_eq($$select g.title, g.year, g.visibility, g.owner_id, g.descript 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'); +create temp table res_tab as select public.report_missing_game(E'\tZyxqtest Tab\n') as j; +grant all on res_tab to public; +select is((select title from public.games where id = (select (j->>'provisional_game_id')::uuid from res_tab)), 'Zyxqtest Tab', + 'KAT-04 Kriterium 1: Tab und Zeilenumbruch am Rand des Titels werden getrimmt'); + -- 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; @@ -408,6 +421,30 @@ reset role; 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'); +-- Tagesgrenze: Meldungen kurz nach Mitternacht Berlin zählen als heute (auch wenn das UTC-Datum ein anderes ist) +insert into public.games (id, title, visibility, owner_id) + select ('f13b0000-0000-4000-8000-' || lpad(to_hex(200 + i), 12, '0'))::uuid, 'Mitternacht ' || i, 'private', + case when i <= 11 then 'a1300000-0000-4000-8000-0000000000e5'::uuid else 'a1300000-0000-4000-8000-0000000000e6'::uuid end + from generate_series(1, 22) i; +insert into public.game_reports (owner_id, title, provisional_game_id, created_at) + select case when i <= 10 then 'a1300000-0000-4000-8000-0000000000e5'::uuid else 'a1300000-0000-4000-8000-0000000000e6'::uuid end, + 'Mitternacht ' || i, ('f13b0000-0000-4000-8000-' || lpad(to_hex(200 + i), 12, '0'))::uuid, + (date_trunc('day', now() at time zone 'Europe/Berlin') at time zone 'Europe/Berlin') + interval '1 minute' + from generate_series(1, 20) i; +select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000e5","role":"authenticated"}', true); +set local role authenticated; +select throws_ok($$select public.report_missing_game('Mitternacht Vorprüfung')$$, 'P0001', 'report_daily_limit', + 'KAT-04 Kriterium 7: 10 Meldungen kurz nach Mitternacht Berlin zählen für heute (Vorprüfung der RPC)'); +select throws_ok($$select public.report_missing_game('Quorblaxwitz Towers', null, 2020)$$, 'P0001', 'report_daily_limit', + 'KAT-04 Kriterium 7: die Vorprüfung zählt Meldungen nach Mitternacht Berlin auch vor der Ähnlichkeitsprüfung'); +reset role; +select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000000e6","role":"authenticated"}', true); +set local role authenticated; +select throws_ok($$insert into public.game_reports (owner_id, title, provisional_game_id) values + ('a1300000-0000-4000-8000-0000000000e6', 'Mitternacht 21', 'f13b0000-0000-4000-8000-0000000000d9')$$, 'P0001', 'report_daily_limit', + 'KAT-04 Kriterium 7: 10 Meldungen kurz nach Mitternacht Berlin zählen für heute (Trigger)'); +reset role; + -- 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'