test(db): Policy- und Fehlerpfade für KAT-04 nachgeschärft (Refs #13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7186cddac5
commit
0826c97471
2 changed files with 78 additions and 9 deletions
|
|
@ -43,7 +43,7 @@ R1 braucht 22 Tabellen in vier Bereichen (17 aus der Grundplanung, dazu `catalog
|
|||
| Tabelle | Spalten | Zweck |
|
||||
| --- | --- | --- |
|
||||
| `games` | `owner_id` (FK → `profiles`, nur bei privaten Spielen), `title`, `description`, `description_locale`, `year`, `min_players`, `max_players`, `min_minutes`, `max_minutes`, `min_age`, `image_path`, `bgg_id` (optional, eindeutig), `visibility` (`catalog` oder `private`), `hidden_at`, `merged_into_id` (FK → `games`), `manual_fields`, `catalog_source_id` (FK → `catalog_sources`), `source_ref`, `source_url`, `source_synced_at`, `title_search` (generiert) | Ein Eintrag je Spiel; private Einträge sind vorläufig und nur für den Ersteller sichtbar (O5). Die sieben Spalten vor `title_search` gehören der Katalogpflege (KAT-05), `title_search` der Suche (KAT-01), siehe unten |
|
||||
| `game_reports` | `owner_id` (FK → `profiles`), `origin` (`user`/`import`), `title`, `publisher_name`, `year`, `link`, `bgg_id`, `provisional_game_id` (FK → `games`), `status` (`open`/`approved`/`rejected`/`merged`), `target_game_id` (FK → `games`), `resolved_at`, `editions_cleared` | Meldungen fehlender Spiele (KAT-05 PR 2c, Anlegen durch Nutzer kommt mit KAT-04); siehe Katalogpflege |
|
||||
| `game_reports` | `owner_id` (FK → `profiles`), `origin` (`user`/`import`), `title`, `publisher_name`, `year`, `link`, `bgg_id`, `provisional_game_id` (FK → `games`), `status` (`open`/`approved`/`rejected`/`merged`), `target_game_id` (FK → `games`), `resolved_at`, `editions_cleared`, `result_seen_at` | Meldungen fehlender Spiele (KAT-05 PR 2c; Anlegen durch Nutzer und Ergebnishinweis KAT-04 PR 13a); siehe Katalogpflege und Abschnitt „Meldungen (KAT-04)“ |
|
||||
| `catalog_sources` | `key`, `name`, `attribution_text`, `attribution_url`, `images_allowed`, `texts_allowed`, `terms_url`, `terms_checked_at` | Datenquellen des Katalogs mit Anzeige-Erlaubnis für Bilder und Texte je Quelle (KAT-05); die Migration legt `bgg` ohne Erlaubnis an |
|
||||
| `publishers`, `game_publishers` | `name` (eindeutig ohne Beachtung der Schreibweise), `name_search` (generiert); `game_id`, `publisher_id` | Verlage und ihre Zuordnung zu Spielen (Klärung 4) |
|
||||
| `game_alt_titles` | `game_id` (FK → `games`), `title`, `locale` (`de`/`en`), `title_search` (generiert) | Alternative Titel eines Spiels mit Sprache (Klärung 3, KAT-01); die Suche findet das Spiel auch darüber |
|
||||
|
|
@ -78,7 +78,7 @@ Jede Tabelle hat Row Level Security (RLS) und eine Policy-Test-Suite (pgTAP). De
|
|||
| `profiles` | Eigenes Profil; fremde nur `display_name`, `avatar_path`, `city` | Nur eigenes |
|
||||
| `favorite_games`, `bgg_links` | Nur eigene | Nur eigene |
|
||||
| `games`, `game_variants`, Begriffe | Katalog für alle (O6); private Spiele nur der Ersteller | Katalog nur Admin (in R1 nur über Studio/SQL, E10); private Spiele nur der Ersteller, und nur die Spalten laut Spaltenrechten |
|
||||
| `game_reports` | nur der Meldende liest seine Zeilen (Admin-Policy `is_admin()` für R4); Schreiben kein Nutzerrecht | Anlegen ergänzt KAT-04 (#13); Bearbeiten nur über `admin_resolve_report` (Studio) |
|
||||
| `game_reports` | nur der Meldende liest seine Zeilen (Admin-Policy `is_admin()` für R4) | Anlegen: eigene Zeile mit eigenem vorläufigen Eintrag (Spaltenrechte, Policy `game_reports_insert_own`, Tageslimit); Ändern: nur `result_seen_at` an eigenen, bereits bearbeiteten Meldungen; kein Löschen. Bearbeiten (Status, Ziel) nur über `admin_resolve_report` (Studio) |
|
||||
| `game_alt_titles` | für alle, auch ohne Anmeldung; folgt dem Spiel (alternative Titel eines fremden privaten Spiels sind unsichtbar) | **niemand über die API** (kein Recht, keine Policy); Pflege nur über Studio und das Startbestand-Werkzeug |
|
||||
| `catalog_sources`, `publishers`, `game_publishers`, View `catalog_games` | für alle, auch ohne Anmeldung (`game_publishers` folgt dem Spiel) | Quellen und Verlage nur Admin; `game_publishers` Admin oder Ersteller des privaten Spiels; die View ist nicht beschreibbar |
|
||||
| `collection_items`, `score_sheets`, `score_categories` | Nur eigene | Nur eigene |
|
||||
|
|
@ -267,7 +267,8 @@ Eindeutig ist `(game_id, locale, title_search)`: derselbe Titel in derselben Spr
|
|||
| `status` | text | ja | `'open'` | `open`, `approved`, `rejected`, `merged` |
|
||||
| `target_game_id` | uuid | nein | | FK → games (`on delete restrict`) |
|
||||
| `resolved_at` | timestamptz | nein | | gesetzt genau dann, wenn `status <> 'open'` |
|
||||
| `editions_cleared` | integer | ja | `0` | Anzahl der Verweise (Sammlung, Partien), deren Edition beim Umhängen geleert wurde (`variants_cleared`); Check `>= 0`; setzt nur `admin_resolve_report` (Spec-F6) |
|
||||
| `editions_cleared` | integer | ja | `0` | Anzahl der Verweise (Sammlung, Partien), deren Edition beim Umhängen geleert wurde (`variants_cleared`); Check `>= 0`; setzen nur `admin_resolve_report` und `adopt_report_target` (Spec-F6) |
|
||||
| `result_seen_at` | timestamptz | nein | | Der Nutzer hat den Ergebnishinweis weggeklickt (KAT-04, Spec-F5); Check: nur bei `status <> 'open'` |
|
||||
| `created_at`, `updated_at` | timestamptz | ja | `now()` | |
|
||||
|
||||
Checks: offen hat kein Ziel; `approved`/`merged` brauchen ein Ziel. Indizes: `(owner_id, created_at)`, eindeutig `(owner_id, bgg_id)` für offene Import-Meldungen (N12a), `(created_at)` für offene Meldungen, eindeutig `provisional_game_id`, `target_game_id`.
|
||||
|
|
@ -382,7 +383,7 @@ Die Pflege läuft in R1 nur über Supabase Studio bzw. `psql` als Datenbankrolle
|
|||
|
||||
- **Ausblenden:** `games.hidden_at` gesetzt = ausgeblendet. Die View `catalog_games` enthält nur Katalogspiele (`visibility = 'catalog'`) ohne `hidden_at` und ohne `merged_into_id`. **Suche und Entdecken lesen nur diese View.** Ein ausgeblendetes Spiel bleibt per ID lesbar (Sammlungen, Partien und Detailseite verweisen darauf).
|
||||
- **Zusammenführen** (Migration `catalog_merge`, PR 2b): `games.merged_into_id` zeigt auf das Zielspiel; ein Trigger lässt nur Katalogspiele als Ziel zu (Fehler `merged_into_not_catalog`). **Regel: `merged_into_id` wird nur über `public.admin_merge_games(p_source, p_target) returns jsonb` gesetzt, nie von Hand.** (Ablauf und Ergebnis-JSON: `docs/katalog-pflege.md`, Abschnitt 5). Sie ist `security invoker`, prüft `current_user in ('postgres', 'supabase_admin')` (sonst `admin_only`) und ist für `anon`, `authenticated` und `service_role` nicht ausführbar; einen API-Aufruf gibt es nicht. Beide Spiele müssen Katalogspiele sein, das Ziel sichtbar. Die Funktion hängt über die interne Funktion `public.repoint_game_references(p_from, p_to)` Sammlung, Favoriten, Partien (mit Edition) und Wertungsbögen um, kopiert die Varianten der Quelle ans Ziel (gleichnamige Zielvarianten werden abgebildet: `lower(btrim(name))`, bei mehreren die älteste, dann die kleinste `id`) und löscht danach die alten Varianten. Bei Doppelbesitz bleibt der Zieleintrag, leere Felder füllt die Quelle, die Liste folgt `owned` vor `wishlist` vor `shopping`, der Quell-Eintrag wird gelöscht; ein Favorit der Quelle entfällt, wenn der Nutzer das Ziel schon als Favorit hat. `bgg_id` und `catalog_source_id`/`source_*` gehen aufs Ziel über, wenn es keine hat; sonst werden sie an der Quelle trotzdem geleert und der Konflikt mit den alten Werten im Ergebnis gemeldet. Die Quelle wird nie gelöscht, sondern ausgeblendet und verwiesen (`hidden_at`, `merged_into_id`); Ketten werden geglättet. Zuordnungen (Genre, Mechanik, Kategorie, Verlag) bleiben am Quell-Eintrag. Ein pgTAP-Wächter (`07`) listet alle Fremdschlüssel auf `games` und lässt neue Verweise erst nach einer Entscheidung zu.
|
||||
- **Meldungen** (Migration `game_reports`, PR 2c): Tabelle `game_reports` (siehe Tabellenstruktur). Lesen darf nur der Meldende (Policy auf `owner_id`; die Policy für `is_admin()` ist für R4 vorbereitet); Schreibrechte haben `anon` und `authenticated` nicht, #13 ergänzt Insert-Recht und -Policy. Bearbeitet wird in R1 nur über `public.admin_resolve_report(p_report, p_status, p_target) returns jsonb` (Ablauf, Ergebnis und Fehler: `docs/katalog-pflege.md`, Abschnitt 6): `security invoker`, prüft `current_user in ('postgres', 'supabase_admin')` (sonst `admin_only`), für `anon`, `authenticated` und `service_role` nicht ausführbar. `approved` und `merged` hängen den vorläufigen privaten Eintrag über `repoint_game_references` auf das Ziel um (Varianten werden nicht kopiert, gleichnamige abgebildet, sonst geleert, `variants_cleared`) und blenden ihn aus (`hidden_at`, `merged_into_id`); `rejected` speichert höchstens den Verweis. `admin_merge_games` hängt zusätzlich `game_reports.target_game_id` von der Quelle auf das Ziel (`reports_repointed`). Der Trigger `games_visibility_immutable` lehnt jede Änderung von `visibility` und `owner_id` an bestehenden Spielen ab (Fehler `visibility_immutable`, für alle Rollen auch `postgres`; vor der RLS-Prüfung, ein Nutzer bekommt also P0001 statt 42501). `admin_resolve_report` schreibt außerdem `editions_cleared` (Anzahl geleerter Editionen, 0 bei `rejected` und ohne Umhängen), damit die App den Nutzer zum Neuwählen auffordern kann (Spec-F6). Beim Konto-Löschen werden Meldungen und vorläufige Einträge (auch nach der Freigabe) hart gelöscht, Katalogspiele und Daten anderer Nutzer bleiben. Fremdschlüssel-Wächter: `07` und `08`.
|
||||
- **Meldungen** (Migration `game_reports`, PR 2c): Tabelle `game_reports` (siehe Tabellenstruktur). Lesen darf nur der Meldende (Policy auf `owner_id`; die Policy für `is_admin()` ist für R4 vorbereitet); `anon` hat keine Rechte; `authenticated` bekommt ab KAT-04 (Migration `game_reports_user`) Insert- und Update-Rechte nur spaltenweise (siehe Abschnitt „Meldungen (KAT-04)“). Bearbeitet wird in R1 nur über `public.admin_resolve_report(p_report, p_status, p_target) returns jsonb` (Ablauf, Ergebnis und Fehler: `docs/katalog-pflege.md`, Abschnitt 6): `security invoker`, prüft `current_user in ('postgres', 'supabase_admin')` (sonst `admin_only`), für `anon`, `authenticated` und `service_role` nicht ausführbar. `approved` und `merged` hängen den vorläufigen privaten Eintrag über `repoint_game_references` auf das Ziel um (Varianten werden nicht kopiert, gleichnamige abgebildet, sonst geleert, `variants_cleared`) und blenden ihn aus (`hidden_at`, `merged_into_id`); `rejected` speichert höchstens den Verweis. `admin_merge_games` hängt zusätzlich `game_reports.target_game_id` von der Quelle auf das Ziel (`reports_repointed`). Der Trigger `games_visibility_immutable` lehnt jede Änderung von `visibility` und `owner_id` an bestehenden Spielen ab (Fehler `visibility_immutable`, für alle Rollen auch `postgres`; vor der RLS-Prüfung, ein Nutzer bekommt also P0001 statt 42501). `admin_resolve_report` schreibt außerdem `editions_cleared` (Anzahl geleerter Editionen, 0 bei `rejected` und ohne Umhängen), damit die App den Nutzer zum Neuwählen auffordern kann (Spec-F6). Beim Konto-Löschen werden Meldungen und vorläufige Einträge (auch nach der Freigabe) hart gelöscht, Katalogspiele und Daten anderer Nutzer bleiben. Fremdschlüssel-Wächter: `07` und `08`.
|
||||
- **Manuell gepflegte Felder:** Der Trigger `games_mark_manual_fields` trägt jedes Feld eines Katalogspiels, das ein Mensch setzt oder ändert, in `manual_fields` ein (beim Anlegen alle gesetzten Felder). Der Trigger `game_links_mark_manual` ergänzt `genres`, `mechanics`, `categories` und `publishers`, wenn Zuordnungen angelegt, geändert oder gelöscht werden. Ausgenommen ist nur ein späterer Import: Rolle `catalog_importer` **und** Einstellung `shuffle.catalog_import = 'on'` (der Import setzt sie per `set local`; das ist Konvention, die Rolle ist die Sperre). Private Spiele werden nicht gekennzeichnet. Eine bewusst entfernte Markierung entfernt der Trigger nicht wieder.
|
||||
- **Anzeige-Erlaubnis:** `catalog_games.description` und `image_path` sind leer, wenn die Quelle (`catalog_sources`) Texte bzw. Bilder nicht erlaubt und das Feld nicht in `manual_fields` steht. Das ist nur die zweite Sicherung: Daten ohne Erlaubnis dürfen gar nicht erst gespeichert werden (Pflicht für den Import).
|
||||
- **Spaltenrechte auf `games`:** Nutzer dürfen nur `owner_id` (nur beim Anlegen), `title`, `description`, `description_locale`, `year`, `min_players`, `max_players`, `min_minutes`, `max_minutes`, `min_age`, `image_path`, `bgg_id` und `visibility` schreiben, nicht `id`, `hidden_at`, `merged_into_id`, `manual_fields`, `catalog_source_id` und `source_*`.
|
||||
|
|
@ -390,6 +391,38 @@ Die Pflege läuft in R1 nur über Supabase Studio bzw. `psql` als Datenbankrolle
|
|||
- **Konto löschen:** Neue Nutzerdaten entstehen nicht; die Zuordnungen privater Spiele (`game_publishers` usw.) werden mit dem Spiel gelöscht.
|
||||
- **API-Änderungen für das Frontend:** neu lesbar für alle: View `catalog_games`, Tabellen `catalog_sources`, `publishers`, `game_publishers`; neu für den Meldenden lesbar: `game_reports`. Das Ändern von `visibility` an einem Spiel ist nicht mehr möglich (P0001 `visibility_immutable`); wer eine Sichtbarkeit mitsendet, muss den alten Wert senden oder die Spalte weglassen. Beim Anlegen und Ändern eines privaten Spiels dürfen Clients keine `id`, `created_at` und `updated_at` mehr mitsenden (42501; die ID kommt per `returning`); ein `toJson()` eines Modells darf nie ungefiltert an `insert`/`update` gehen (Merkpunkt für #3 und #13). Der `is_admin()`-Zweig der Zuordnungs-Policies (`game_*_write`) greift für Katalogspiele faktisch nicht mehr, weil der Trigger `game_links_mark_manual` `manual_fields` schreiben muss; ein späterer Admin-Pfad (QUE-08) braucht eigene RPCs. Es gibt keine generierten Datenbanktypen im Repo; das Domänenmodell `CatalogGame` legt KAT-01 (#3) auf Basis der Spalten von `catalog_games` an.
|
||||
|
||||
## Meldungen (KAT-04, Migration `game_reports_user`)
|
||||
|
||||
Spec: `docs/specs/13-fehlendes-spiel-melden.md` (Umsetzungsnachtrag: `docs/specs/13-umsetzung.md`). Nur additiv zu `game_reports` aus KAT-05; keine neue Tabelle, kein neuer Fremdschlüssel.
|
||||
|
||||
**Ablauf (Nutzer):** Die App ruft zuerst `find_similar_catalog_games(titel, jahr)`; gibt es Treffer, bestätigt der Nutzer ausdrücklich, dass sein Spiel ein anderes ist. Danach `report_missing_game(...)` mit `p_confirmed_distinct = true`. Die RPC legt in einer Transaktion den vorläufigen privaten Eintrag (nur Titel und Jahr), die Meldung (`status = 'open'`, `origin = 'user'`) und optional den Listeneintrag an. Der Admin bearbeitet die Meldung manuell (`admin_resolve_report`, `docs/katalog-pflege.md` Abschnitt 6). Bei „abgelehnt mit Verweis“ entscheidet der Nutzer selbst über den Wechsel auf den Zieleintrag (`adopt_report_target`). Den Ergebnishinweis klickt die App mit einem Update von `result_seen_at` weg.
|
||||
|
||||
**Funktionen** (alle mit `search_path = ''`; jede zuerst für `public`, `anon`, `authenticated`, `service_role` entzogen, danach nur der genannte Grant):
|
||||
|
||||
| Funktion | Art | `execute` | Zweck |
|
||||
| --- | --- | --- | --- |
|
||||
| `strip_leading_article(text)`, `normalize_game_title(text)` | sql, immutable | `authenticated` | Titel normalisieren; `normalize_game_title` = `search_normalize` plus ein führender Artikel (de/en), Rückfall `lower(btrim)` |
|
||||
| `report_day(timestamptz)` | sql, immutable | `authenticated` | Berliner Kalendertag (Tageslimit); `authenticated` braucht das Recht, weil der Trigger als `security invoker` läuft |
|
||||
| `find_similar_catalog_games(p_title, p_year)` | RPC, `security invoker`, stable | `authenticated` | höchstens 5 sichtbare Katalogspiele mit gleichem normalisierten Titel (Regel A, jedes Jahr) oder Titel-Ähnlichkeit >= 0,8 und Jahr ± 1 (Regel B; fehlt ein Jahr, ohne Jahresbedingung); Kandidatentitel sind der Haupttitel und alle alternativen Titel (`title_search`); Rückgabe `game_id, title, matched_title, year, publishers, match_kind ('same_title'/'similar_title'), similarity`; Titel leer oder über 200 Zeichen ergibt eine leere Menge |
|
||||
| `report_missing_game(p_title, p_publisher_name, p_year, p_link, p_list, p_confirmed_distinct)` | RPC, `security invoker` | `authenticated` | Meldung + vorläufiger Eintrag + optional Listeneintrag; Rückgabe `{"report_id", "provisional_game_id", "collection_item_id"}` |
|
||||
| `adopt_report_target(p_report)` | RPC, **`security definer`**, Eigentümer `postgres` | `authenticated` | Wechsel auf den Zieleintrag einer mit Verweis abgelehnten Meldung; Rückgabe: Zähler aus `repoint_game_references` |
|
||||
| `game_reports_daily_limit()` | Trigger-Funktion, `security invoker` | niemand | Tageslimit |
|
||||
|
||||
**Fehlercodes** (`raise exception '<code>' using errcode = 'P0001'`; die App bildet sie auf Domänenfehler ab):
|
||||
`report_missing_game`: `not_authenticated`, `report_title_required`, `report_title_too_long`, `report_publisher_too_long`, `report_year_invalid`, `report_link_invalid`, `report_list_invalid`, `report_daily_limit`, `report_similar_exists`, `report_provisional_in_use` (nur bei gleichzeitigem Konflikt auf den Teilindex; über die RPC praktisch nicht erreichbar, ein direkter Insert liefert 23505).
|
||||
`adopt_report_target`: `not_authenticated`, `report_not_found` (unbekannt, fremd oder vorläufiger Eintrag gehört nicht dem Aufrufer), `report_not_adoptable`, `report_no_provisional`, `report_already_adopted`, `report_target_not_listed`, `report_foreign_references`.
|
||||
Tageslimit im Trigger: `report_daily_limit`.
|
||||
|
||||
**Rechte und Policies auf `game_reports`:** Insert-Recht nur auf `owner_id, title, publisher_name, year, link, provisional_game_id`; Update-Recht nur auf `result_seen_at`; kein Löschrecht. Nicht schreibbar (42501): `id`, `origin`, `status`, `bgg_id`, `target_game_id`, `resolved_at`, `editions_cleared`, `created_at`, `updated_at`. Policy `game_reports_insert_own`: `owner_id = auth.uid()`, `origin = 'user'`, `status = 'open'`, kein Ziel, `provisional_game_id` verweist auf ein eigenes privates, nicht ausgeblendetes und nicht zusammengeführtes Spiel. Policy `game_reports_update_seen`: nur eigene Meldungen mit `status <> 'open'`. Lesen unverändert (nur eigene).
|
||||
|
||||
**Tageslimit (Krit. 7):** Trigger `game_reports_daily_limit` (`before insert ... when (new.origin = 'user')`) lässt höchstens 10 Nutzer-Meldungen je Nutzer und Berliner Kalendertag zu (Advisory-Lock je Nutzer gegen gleichzeitige Inserts, setzt READ COMMITTED voraus, den Standard bei PostgREST). Import-Meldungen (`origin = 'import'`) werden weder gezählt noch begrenzt (N12). Der Trigger gilt für jede Rolle: Wer als `postgres` viele Nutzer-Meldungen „von heute“ anlegt (z. B. in Tests), stößt ebenfalls auf das Limit.
|
||||
|
||||
**`adopt_report_target` (security definer, Begründung):** Nutzer dürfen `merged_into_id` und `hidden_at` nicht schreiben und `repoint_game_references` nicht ausführen. Die Funktion bestimmt den Aufrufer nur über `auth.uid()`, prüft Eigentum an Meldung und vorläufigem Eintrag, dass das Ziel ein sichtbares Katalogspiel ist und dass keine Verweise mit fremdem oder fehlendem Eigentümer (`collection_items`, `favorite_games`, `plays`, `score_sheets`) am vorläufigen Eintrag hängen; sie ruft nie eine `admin_*`-Funktion auf. Sperrreihenfolge: erst die Meldung, dann beide Spiele nach `id`. `result_seen_at` bleibt unverändert, damit der Hinweis am Zieleintrag erscheint, bis der Nutzer ihn wegklickt. `editions_cleared` wird wie in `admin_resolve_report` gesetzt.
|
||||
|
||||
**Normalisierung (gemeinsam mit KAT-01):** `search_normalize(text)` (Migration `catalog_search`) ist die einzige Basis und entfernt nie Artikel. `normalize_game_title` und der Kandidatenschlüssel in `find_similar_catalog_games` entfernen zusätzlich einen führenden Artikel. Ein Titel, der nach `search_normalize` leer ist (nur nicht-lateinische Zeichen oder Sonderzeichen), fällt auf `lower(btrim(titel))` zurück.
|
||||
|
||||
**API-Änderungen für das Frontend (13b):** neue RPCs `find_similar_catalog_games`, `report_missing_game`, `adopt_report_target`; `game_reports` ist für `authenticated` jetzt beschreibbar, aber nur wie oben (nie mit `id`, `created_at`, `status`, `origin`, `target_game_id`, `resolved_at`, `bgg_id`, `editions_cleared` senden; die App schreibt über die RPCs und das Update von `result_seen_at` mit `{'result_seen_at': <ISO-8601 UTC>}`). Es gibt keine generierten Datenbanktypen im Repo.
|
||||
|
||||
## Katalogsuche (KAT-01, Migration `catalog_search`)
|
||||
|
||||
Spec: `docs/specs/3-katalog-suche.md`. Die Suche liest ausschließlich über die RPC `search_catalog_games`, die ihrerseits die View `catalog_games` nutzt.
|
||||
|
|
|
|||
|
|
@ -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(198);
|
||||
select plan(206);
|
||||
|
||||
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"}'),
|
||||
|
|
@ -122,7 +122,8 @@ insert into public.games (id, title, year, visibility) values
|
|||
('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');
|
||||
('f13c0000-0000-4000-8000-000000000015', 'Adoptzyxq Zusammengefuehrtes Ziel', 2021, 'catalog'),
|
||||
('f13c0000-0000-4000-8000-000000000017', repeat('x', 200), 2001, '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;
|
||||
|
|
@ -217,11 +218,11 @@ select results_eq($$select game_id from public.find_similar_catalog_games('Verst
|
|||
'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(repeat('x', 200), null)), 1::bigint, 'KAT-04 Kriterium 2: Titel mit genau 200 Zeichen ist erlaubt und trifft das gleichlange Katalogspiel');
|
||||
select is((select count(*) from public.find_similar_catalog_games(repeat('x', 201), null)), 0::bigint, 'KAT-04 Kriterium 2: Titel über 200 Zeichen liefert eine leere Menge (obwohl dieselben Trigramme ein Katalogspiel träfen)');
|
||||
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)$$,
|
||||
|
|
@ -339,11 +340,31 @@ select throws_ok($$select public.report_missing_game('Quorblaxwitz Towers', null
|
|||
'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 throws_ok($$select public.report_missing_game('Quorblaxwitz Towe', null, 2021)$$, 'P0001', 'report_similar_exists',
|
||||
'KAT-04 Kriterium 2: die RPC prüft die Trigramm-Regel mit dem gemeldeten Jahr (± 1 trifft)');
|
||||
select lives_ok($$select public.report_missing_game('Quorblaxwitz Towe', null, 2022)$$,
|
||||
'KAT-04 Kriterium 2: die RPC prüft die Trigramm-Regel mit dem gemeldeten Jahr (± 2 trifft nicht, ohne Bestätigung gespeichert)');
|
||||
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');
|
||||
select is((select count(*) from public.game_reports where title in ('Die Quorblaxwitz Towers', 'Quorblaxwitz Towe')), 2::bigint, 'KAT-04 Kriterium 2: die bestätigte und die nicht ähnliche Meldung sind gespeichert');
|
||||
reset role;
|
||||
|
||||
-- 23505 am eindeutigen Index auf provisional_game_id: über die RPC nicht erreichbar (jeder Aufruf legt ein frisches Spiel an).
|
||||
-- Ein Hilfstrigger lenkt den Insert in game_reports auf ein schon verwendetes Spiel um.
|
||||
create function pg_temp.reuse_prov() returns trigger language plpgsql as $$
|
||||
begin
|
||||
new.provisional_game_id := (select r.provisional_game_id from public.game_reports r where r.title = 'Die Quorblaxwitz Towers' and r.owner_id = new.owner_id);
|
||||
return new;
|
||||
end $$;
|
||||
create trigger zz_reuse_prov before insert on public.game_reports for each row execute function pg_temp.reuse_prov();
|
||||
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 Doppelt belegt')$$, 'P0001', 'report_provisional_in_use',
|
||||
'KAT-04 Kriterium 3: ein schon verwendeter vorläufiger Eintrag wird als report_provisional_in_use gemeldet');
|
||||
select is((select count(*) from public.games where title = 'Zyxqtest Doppelt belegt'), 0::bigint, 'KAT-04 Kriterium 3: der Konflikt lässt kein vorläufiges Spiel zurück');
|
||||
reset role;
|
||||
drop trigger zz_reuse_prov on public.game_reports;
|
||||
|
||||
-- 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();
|
||||
|
|
@ -430,6 +451,13 @@ insert into public.games (id, title, visibility, owner_id, hidden_at, merged_int
|
|||
('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_all(p_at timestamptz) returns bigint language plpgsql as $$
|
||||
declare n bigint;
|
||||
begin
|
||||
update public.game_reports set result_seen_at = p_at; -- bewusst ohne WHERE: nur die Update-Policy begrenzt die Zeilen
|
||||
get diagnostics n = row_count;
|
||||
return n;
|
||||
end $$;
|
||||
create function pg_temp.mark_seen(p_id uuid, p_at timestamptz) returns bigint language plpgsql as $$
|
||||
declare n bigint;
|
||||
begin
|
||||
|
|
@ -443,6 +471,8 @@ select lives_ok($$insert into public.game_reports (owner_id, title, provisional_
|
|||
'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, provisional_game_id) values ('a1300000-0000-4000-8000-0000000000b2', 'Fremder Besitzer, eigenes Spiel', 'f13a0000-0000-4000-8000-000000000015')$$, '42501', null,
|
||||
'Sicherheit Kriterium 1: owner_id eines anderen Nutzers wird abgelehnt, auch wenn das vorläufige Spiel Cara gehört');
|
||||
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,
|
||||
|
|
@ -544,6 +574,8 @@ select set_config('request.jwt.claims', '{"sub":"a1300000-0000-4000-8000-0000000
|
|||
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)');
|
||||
select is(pg_temp.mark_seen_all(now()), 0::bigint,
|
||||
'Sicherheit Kriterium 1: auch ein Update ohne WHERE trifft bei Ben keine fremde Meldung (Update-Policy)');
|
||||
reset role;
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------
|
||||
|
|
@ -614,7 +646,11 @@ select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-00
|
|||
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');
|
||||
select throws_ok($$select public.adopt_report_target('f13a0000-0000-4000-8000-000000000a08')$$, 'P0001', 'report_not_found',
|
||||
'Sicherheit Kriterium 3: Ben kann Annas Meldung nicht mit seinem eigenen privaten Spiel als vorläufigem Eintrag übernehmen');
|
||||
reset role;
|
||||
select is((select merged_into_id from public.games where id = 'f13a0000-0000-4000-8000-000000000107'), null::uuid,
|
||||
'Sicherheit Kriterium 3: Bens Spiel wurde dabei nicht zusammengeführt');
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue