fix(db): Jahr-Grenzen in find_similar_catalog_games, Tests für Tagesgrenze und Trimmen (Refs #13)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
cschlaefke 2026-11-04 19:44:02 +01:00
parent 3135ed6381
commit 154e5e4bb5
3 changed files with 48 additions and 5 deletions

View file

@ -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`. - **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). - **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 ## 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). 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).

View file

@ -150,7 +150,9 @@ stable
set search_path = '' set search_path = ''
as $$ as $$
with q 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 where p_title is not null
and char_length(btrim(p_title)) between 1 and 200 and char_length(btrim(p_title)) between 1 and 200
), ),
@ -172,7 +174,7 @@ as $$
) k ) k
), ),
scored as ( 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 extensions.similarity(cd.cand_key, q.nk) as sim
from cand cd cross join q from cand cd cross join q
where q.nk <> '' where q.nk <> ''
@ -183,7 +185,7 @@ as $$
from scored s from scored s
where s.is_same where s.is_same
or (s.sim >= 0.8 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 order by s.game_id, s.is_same desc, s.sim desc, s.ord, s.cand_title
) )
select h.game_id, select h.game_id,

View file

@ -6,7 +6,7 @@
-- Fixtures legt postgres an; "als Nutzer" läuft wirklich als Rolle authenticated mit gesetztem -- 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. -- request.jwt.claims. Regel dieser Datei: Jede Verweigerung steht neben einem Positivfall.
begin; begin;
select plan(206); select plan(213);
insert into auth.users (id, email, raw_user_meta_data) values 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-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-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-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-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) -- 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'); '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, 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'); '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, 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'); '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, 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, 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'); '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 -- Erfolg mit Liste
create temp table res2 as select public.report_missing_game('Zyxqtest Zwei', 'Zyxq Eigenverlag', null, null, 'wishlist') as j; 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; 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, 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'); '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 -- Direkter Insert als authenticated: Trigger zählt, report_day ist für authenticated ausführbar
insert into public.games (id, title, visibility, owner_id) 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' select ('f13b0000-0000-4000-8000-' || lpad(to_hex(100 + i), 12, '0'))::uuid, 'Direkt ' || i, 'private', 'a1300000-0000-4000-8000-0000000000e2'