test(catalog): address review findings on PR 2a
All checks were successful
All checks were successful
Replace tests that could not fail (manual_fields stability, no-op update), select runbook games by id, add grant matrix and forged-admin tests, assert vocabulary seeding and Deno checksum in CI, clarify the set local convention and API notes. Refs #2 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
52cbd79fb4
commit
9952efa2fe
6 changed files with 103 additions and 35 deletions
|
|
@ -152,8 +152,14 @@ jobs:
|
|||
# Startbestand-Werkzeug (KAT-05, Spec 2 D1): Deno-Tests, erzeugte Dateien aktuell, Schlüsselliste wächst nur.
|
||||
- name: Deno installieren
|
||||
run: |
|
||||
arch=$(uname -m); [ "$arch" = "aarch64" ] && target=aarch64-unknown-linux-gnu || target=x86_64-unknown-linux-gnu
|
||||
# SHA256 fest eingetragen (aus den veröffentlichten .sha256sum-Dateien von v2.9.8); bei neuer DENO_VERSION mit ändern.
|
||||
if [ "$(uname -m)" = "aarch64" ]; then
|
||||
target=aarch64-unknown-linux-gnu; sha=d49596c20b9788cf8f5d8ddf644103d4bde56ed239753f0bdb92ba9361d51905
|
||||
else
|
||||
target=x86_64-unknown-linux-gnu; sha=e890975bc2549807bbcc0b584ffafda72cc7c93349dd99c80483dd784f6c4609
|
||||
fi
|
||||
curl -fsSL "https://github.com/denoland/deno/releases/download/v${DENO_VERSION}/deno-${target}.zip" -o /tmp/deno.zip
|
||||
echo "${sha} /tmp/deno.zip" | sha256sum -c -
|
||||
unzip -q -o /tmp/deno.zip -d /usr/local/bin
|
||||
deno --version
|
||||
- name: Startbestand-Werkzeug testen
|
||||
|
|
@ -212,6 +218,22 @@ jobs:
|
|||
test -f supabase/config.toml || { echo "supabase/config.toml fehlt"; exit 1; }
|
||||
test -d supabase/migrations || { echo "Keine Migrationen – übersprungen"; exit 0; }
|
||||
npx --yes supabase@${{ env.SUPABASE_CLI_VERSION }} start -x studio,imgproxy,edge-runtime,logflare,vector
|
||||
- name: Katalog-Vokabular eingespielt? (KAT-05)
|
||||
# Schlägt fehl, wenn der Seed (sql_paths) das Vokabular nicht geladen hat, zum Beispiel wegen eines Pfad-Tippfehlers
|
||||
# (die CLI überspringt eine fehlende Datei nur mit Warnung). IDs werden aus vocabulary.csv abgeleitet.
|
||||
run: |
|
||||
expected=$(awk -F, 'NR>1 && NF>=4 {printf "%s(\x27%s\x27,\x27%s\x27)", sep, $1, $2; sep=","}' supabase/catalog/vocabulary.csv)
|
||||
test -n "$expected" || { echo "vocabulary.csv ist leer"; exit 1; }
|
||||
missing=$(docker exec -i supabase_db_shuffle psql -U postgres -d postgres -At -v ON_ERROR_STOP=1 <<SQL
|
||||
select count(*) from (values $expected) as v(kind, vocab_key)
|
||||
where (v.kind = 'genre' and not exists (select 1 from public.genres t
|
||||
where t.id = extensions.uuid_generate_v5('6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4', 'genre:' || v.vocab_key)))
|
||||
or (v.kind = 'mechanic' and not exists (select 1 from public.mechanics t
|
||||
where t.id = extensions.uuid_generate_v5('6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4', 'mechanic:' || v.vocab_key)));
|
||||
SQL
|
||||
)
|
||||
echo "Fehlende Vokabular-Einträge: $missing"
|
||||
test "$missing" = "0"
|
||||
- name: Datenbanktests (pgTAP)
|
||||
run: |
|
||||
test -d supabase/migrations || exit 0
|
||||
|
|
|
|||
|
|
@ -343,9 +343,9 @@ 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:** `games.merged_into_id` zeigt auf das Zielspiel (Spalte seit 2a; die Funktion `admin_merge_games` kommt mit PR 2b). Ein Trigger lässt nur Katalogspiele als Ziel zu (Fehler `merged_into_not_catalog`).
|
||||
- **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** `set local shuffle.catalog_import = 'on'`. Private Spiele werden nicht gekennzeichnet. Eine bewusst entfernte Markierung entfernt der Trigger nicht wieder.
|
||||
- **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_*`.
|
||||
- **Startbestand:** `supabase/catalog/` (CSV, Werkzeug, erzeugte SQL-Dateien); `supabase db reset` spielt Vokabular und Startbestand ein. Der Startbestand enthält nur Fakten und einen eigenen deutschen Kurztext, kein Cover, keine Quelle, keine BGG-Kennung.
|
||||
- **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`. Beim Anlegen und Ändern eines privaten Spiels dürfen Clients keine `id` mehr mitsenden (die ID kommt per `returning`). Es gibt keine generierten Datenbanktypen im Repo; das Domänenmodell `CatalogGame` legt KAT-01 (#3) auf Basis der Spalten von `catalog_games` an.
|
||||
- **API-Änderungen für das Frontend:** neu lesbar für alle: View `catalog_games`, Tabellen `catalog_sources`, `publishers`, `game_publishers`. 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.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Stand: PR 2a (Datenmodell, Startbestand, Ausblenden). Die Abschnitte **Zusammenf
|
|||
- Studio läuft als Datenbankrolle `postgres` (bzw. `supabase_admin`) und **umgeht Row Level Security**. Wer Studio erreicht, kann alles lesen und ändern, auch private Nutzerdaten. Mit `select current_user;` im SQL-Editor prüfen, unter welcher Rolle man arbeitet.
|
||||
- **Nicht öffentlich erreichbar** sein dürfen: Studio, der Postgres-Port 5432, der Pooler (Supavisor, Standard 6543) und die Kong-Route `/pg/` (pg-meta). Zugriff nur über SSH-Tunnel oder VPN; die Basic-Auth des Gateways (`DASHBOARD_USERNAME`/`DASHBOARD_PASSWORD`) bleibt mit starkem Passwort als zweite Schicht. Gilt ebenso für Staging. (Einrichtung und Bestätigung: Aufgabe D1, devops.)
|
||||
- **Immer in einer Transaktion arbeiten:** `begin; … commit;` (bei Fehler `rollback;`). Vor größeren Änderungen den Stand des Backups prüfen.
|
||||
- **Niemals `set …` ohne `local` verwenden** (zum Beispiel `set shuffle.catalog_import`): Ohne `local` gilt die Einstellung für die ganze Sitzung. Der Import-Schalter `shuffle.catalog_import` ist ohnehin nur für die Rolle `catalog_importer` wirksam und gehört nicht ins Studio.
|
||||
- **Niemals `set …` ohne `local` verwenden** (zum Beispiel `set shuffle.catalog_import`): Ohne `local` gilt die Einstellung für die ganze Sitzung. Der Import-Schalter `shuffle.catalog_import` ist ohnehin nur für die Rolle `catalog_importer` wirksam und gehört nicht ins Studio. Hinweis: `set local` statt `set` ist eine Konvention des Imports; ein sitzungsweites `set` bleibt hinter einem Pooler bis zum Ende der Verbindung bestehen und kann auf die nächste Anfrage durchschlagen.
|
||||
- Die Rolle `app_metadata.role = 'admin'` (JWT) wird in R1 **niemandem vergeben** (siehe 7).
|
||||
|
||||
## 2. Startbestand pflegen, einspielen und abnehmen
|
||||
|
|
@ -119,16 +119,23 @@ Je Regel eine Zeile (Soll, Ist, Status `ok`/`Verstoß`/`Information`). **Abnahme
|
|||
|
||||
Jede Änderung an einem Fakt eines Katalogspiels (und jede Zuordnung zu Verlag, Genre, Mechanik oder Kategorie) trägt der Trigger automatisch in `games.manual_fields` ein, damit ein späterer Import sie nicht überschreibt. Im Studio muss dafür nichts zusätzlich gepflegt werden. Als Rolle `postgres` bleibt das Kennzeichnen auch dann aktiv, wenn jemand `shuffle.catalog_import` setzt.
|
||||
|
||||
**Neues Katalogspiel mit Verlag und Genre** (Werte ersetzen; in einer Transaktion):
|
||||
**Spiel finden, bevor man es ändert.** Titel sind nicht eindeutig (ab PR 2b kann es gleichnamige Duplikate geben). Deshalb zuerst die ID nachschlagen und alle weiteren Bausteine über `id` ausführen:
|
||||
|
||||
```sql
|
||||
select id, title, year, hidden_at from public.games
|
||||
where visibility = 'catalog' and lower(title) = lower('Beispielspiel Runbook');
|
||||
```
|
||||
|
||||
**Neues Katalogspiel mit Verlag und Genre** (Werte ersetzen; in einer Transaktion). Die ID vorher mit `select gen_random_uuid();` erzeugen und in diesem und den folgenden Bausteinen einsetzen (hier die Beispiel-ID):
|
||||
|
||||
```sql
|
||||
insert into public.publishers (name) values ('Beispielverlag')
|
||||
on conflict ((lower(name))) do nothing;
|
||||
|
||||
with neu as (
|
||||
insert into public.games (title, year, min_players, max_players, min_minutes, max_minutes, min_age,
|
||||
insert into public.games (id, title, year, min_players, max_players, min_minutes, max_minutes, min_age,
|
||||
description, description_locale, visibility)
|
||||
values ('Beispielspiel Runbook', 2020, 2, 4, 30, 60, 8, 'Eigener Kurztext.', 'de', 'catalog')
|
||||
values ('7a1b2c3d-0000-4000-8000-000000000001', 'Beispielspiel Runbook', 2020, 2, 4, 30, 60, 8, 'Eigener Kurztext.', 'de', 'catalog')
|
||||
returning id
|
||||
), verlag as (
|
||||
insert into public.game_publishers (game_id, publisher_id)
|
||||
|
|
@ -144,14 +151,14 @@ Weitere Mechaniken, Kategorien und Verlage ordnet man mit `insert into public.ga
|
|||
|
||||
```sql
|
||||
update public.games set min_age = 10, max_minutes = 90
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog';
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog';
|
||||
```
|
||||
|
||||
**Markierung „manuell gepflegt“ bewusst entfernen** (nur per SQL; eine Änderung allein an `manual_fields` wird vom Trigger nicht zurückgesetzt):
|
||||
|
||||
```sql
|
||||
update public.games set manual_fields = array_remove(manual_fields, 'min_age')
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog';
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog';
|
||||
```
|
||||
|
||||
Regeln:
|
||||
|
|
@ -168,11 +175,11 @@ Ausgeblendete Spiele (`hidden_at` gesetzt) erscheinen nicht mehr in Suche und En
|
|||
```sql
|
||||
-- ausblenden
|
||||
update public.games set hidden_at = now()
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog' and hidden_at is null;
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog' and hidden_at is null;
|
||||
|
||||
-- wieder einblenden
|
||||
update public.games set hidden_at = null
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog';
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog';
|
||||
```
|
||||
|
||||
Hinweis: Der Eintrag bleibt über die rohe Schnittstelle per ID lesbar (`games` ist öffentlich lesbar). Muss ein Spiel aus rechtlichen Gründen wirklich verschwinden, zusätzlich Titel und Texte inhaltlich leeren.
|
||||
|
|
@ -193,4 +200,6 @@ In R1 gibt es **kein App-Konto mit Admin-Rolle** (E10, Klärung 44). Die vorhand
|
|||
update auth.users set raw_app_meta_data = raw_app_meta_data || '{"role":"admin"}' where id = '<nutzer-id>';
|
||||
```
|
||||
|
||||
**Nicht in R1 ausführen.** Nie `user_metadata` dafür verwenden (vom Client änderbar).
|
||||
**Nicht in R1 ausführen und nicht in pgTAP getestet.** Nie `user_metadata` dafür verwenden (vom Client änderbar).
|
||||
|
||||
Hinweis für QUE-08 (R4): Der `is_admin()`-Zweig der Policies `game_*_write` (Genre, Mechanik, Kategorie, Verlag) funktioniert für Katalogspiele faktisch nicht mehr, weil der Trigger `game_links_mark_manual` (security invoker) `games.manual_fields` schreiben muss und `authenticated` dafür kein Spaltenrecht hat. Ein Admin-Pfad über die API braucht eigene RPCs (security definer).
|
||||
|
|
|
|||
|
|
@ -693,7 +693,7 @@ Umgesetzt: B1, B2, B3a, B3, B4 und die beiden CI-Schritte aus D1 (Deno-Tests und
|
|||
- **Branch:** `feature/2a-katalog-datenmodell` statt `feature/2-katalog-fundament` (Vorgabe des Auftrags).
|
||||
- **CLI-Annahmen (R11) geprüft** mit Supabase CLI 3.0.0, Ergebnis in `docs/katalog-pflege.md` 2.7: Reihenfolge und `begin`/`commit` funktionieren; ein Fehler rollt die ganze Datei zurück (Exit 1); ein Pfad ohne Treffer ist nur eine Warnung (Exit 0). Die erzeugten Dateien enthalten kein `begin`/`commit`.
|
||||
- **Nicht in 2a:** `games_visibility_immutable` (2c, wie in 3.4).
|
||||
- **API-Hinweis für das Frontend:** `catalog_games` ist über PostgREST nicht schreibbar (Fehler 55000 „cannot insert/update view“, zusätzlich fehlen die Rechte). Clients dürfen beim Anlegen oder Ändern von `games` keine `id`, `created_at` und `updated_at` mehr senden (42501; bisher setzte der Trigger `set_updated_at` diese Werte stillschweigend zurück). Ein `toJson()` eines Modells darf deshalb nicht unverändert an `insert`/`update` gehen, sondern nur die erlaubten Spalten (3.2). Es gibt keine generierten Datenbanktypen im Repo.
|
||||
- **API-Hinweis für das Frontend:** `catalog_games` ist über PostgREST nicht schreibbar (Fehler 55000 „cannot insert/update view“, zusätzlich fehlen die Rechte). Clients dürfen beim Anlegen oder Ändern von `games` keine `id`, `created_at` und `updated_at` mehr senden (42501; bisher setzte der Trigger `set_updated_at` diese Werte nur bei UPDATE stillschweigend zurück, bei INSERT wurden `id` und `created_at` vom Client übernommen). Ein `toJson()` eines Modells darf deshalb nicht unverändert an `insert`/`update` gehen, sondern nur die erlaubten Spalten (3.2); Merkpunkt für KAT-01 (#3) und KAT-04 (#13). Außerdem funktioniert der `is_admin()`-Zweig der `game_*_write`-Policies für Katalogspiele faktisch nicht mehr (der Trigger `game_links_mark_manual` braucht Schreibrecht auf `manual_fields`; R8), relevant für QUE-08. Es gibt keine generierten Datenbanktypen im Repo.
|
||||
- **Ergänzung `startbestand.sql`:** Ein Wächter am Anfang bricht ab, wenn ein Genre oder eine Mechanik aus der CSV in der Datenbank fehlt; sonst würde der Abgleich der Zuordnungen still Verknüpfungen löschen.
|
||||
- **Ergänzung Testdaten:** `supabase/catalog/testdata/` (fiktives Vokabular und drei Testspiele) erzeugt `supabase/tests/support/*.inc`, die `06_catalog_foundation.test.sql` per `\ir` einbindet; so laufen Idempotenz und `checks.sql` im pgTAP-Test mit den echten erzeugten Skripten.
|
||||
- **`game_links_mark_manual`:** prüft zuerst per `select`, ob das Spiel ein Katalogspiel ist, und schreibt nur dann `manual_fields`. Ohne diese Prüfung scheiterte jede Zuordnung zu einem privaten Spiel mit 42501, weil Nutzer kein Schreibrecht auf `manual_fields` haben.
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ begin
|
|||
if new.visibility <> 'catalog' then
|
||||
return new;
|
||||
end if;
|
||||
-- Ausnahme für den späteren Import: nur als Rolle catalog_importer UND mit set local.
|
||||
-- Ausnahme für den späteren Import: nur als Rolle catalog_importer UND mit Einstellung 'on' (der Import setzt sie per set local; das ist Konvention).
|
||||
-- Ein bloßes set im Studio (Rolle postgres) schaltet die Kennzeichnung nicht ab (W7).
|
||||
if current_user = 'catalog_importer'
|
||||
and coalesce(current_setting('shuffle.catalog_import', true), '') = 'on' then
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
-- Läuft auch mit eingespieltem Startbestand: eigene Fixtures mit festen IDs, gezählt wird nur darüber.
|
||||
-- Die Include-Dateien in ../support/ (\ir) erzeugt supabase/catalog/build_startbestand.ts aus catalog/testdata/.
|
||||
begin;
|
||||
select plan(133);
|
||||
select plan(148);
|
||||
|
||||
insert into auth.users (id, email, raw_user_meta_data) values
|
||||
('11111111-1111-1111-1111-111111111111', 'anna@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Anna"}'),
|
||||
|
|
@ -120,13 +120,12 @@ update public.games set min_age = 8 where id = 'f6000000-0000-0000-0000-00000000
|
|||
select is((select manual_fields from public.games where id = 'f6000000-0000-0000-0000-0000000000d1'),
|
||||
array['description', 'description_locale', 'min_age', 'min_players', 'title', 'year'],
|
||||
'KAT-05 Kriterium 1: Update als postgres ergänzt das geänderte Feld');
|
||||
update public.games set year = year where id = 'f6000000-0000-0000-0000-0000000000d1';
|
||||
select is((select cardinality(manual_fields) from public.games where id = 'f6000000-0000-0000-0000-0000000000d1'),
|
||||
6, 'Ein Update ohne Wertänderung markiert nichts zusätzlich');
|
||||
|
||||
update public.games set manual_fields = array_remove(manual_fields, 'min_age') where id = 'f6000000-0000-0000-0000-0000000000d1';
|
||||
select is((select 'min_age' = any(manual_fields) from public.games where id = 'f6000000-0000-0000-0000-0000000000d1'),
|
||||
false, 'KAT-05 Kriterium 4: Eine bewusst entfernte Markierung bleibt entfernt');
|
||||
update public.games set min_age = min_age where id = 'f6000000-0000-0000-0000-0000000000d1';
|
||||
select is((select 'min_age' = any(manual_fields) from public.games where id = 'f6000000-0000-0000-0000-0000000000d1'),
|
||||
false, 'Ein Update ohne Wertänderung markiert das Feld nicht wieder');
|
||||
update public.games set min_age = 10, manual_fields = array_remove(manual_fields, 'min_age') where id = 'f6000000-0000-0000-0000-0000000000d1';
|
||||
select is((select 'min_age' = any(manual_fields) from public.games where id = 'f6000000-0000-0000-0000-0000000000d1'),
|
||||
true, 'Wer das Feld ändert, markiert es wieder, auch wenn die Markierung im selben Update entfernt wird');
|
||||
|
|
@ -146,7 +145,7 @@ insert into public.games (id, title, year, visibility, owner_id) values
|
|||
select is((select manual_fields from public.games where id = 'f6000000-0000-0000-0000-0000000000a2'),
|
||||
'{}'::text[], 'Private Spiele werden nicht gekennzeichnet');
|
||||
|
||||
-- Import-Ausnahme: Rolle catalog_importer UND set local
|
||||
-- Import-Ausnahme: Rolle catalog_importer UND Einstellung on (der Import setzt sie per set local)
|
||||
create role catalog_importer nologin;
|
||||
grant catalog_importer to postgres with set true;
|
||||
grant usage on schema public to catalog_importer;
|
||||
|
|
@ -167,13 +166,13 @@ insert into public.game_genres (game_id, genre_id) values
|
|||
reset shuffle.catalog_import;
|
||||
reset role;
|
||||
select is((select manual_fields from public.games where id = 'f6000000-0000-0000-0000-0000000000f1'),
|
||||
'{}'::text[], 'KAT-05 Kriterium 1: Rolle catalog_importer mit set local markiert nichts (Insert und Zuordnung)');
|
||||
'{}'::text[], 'KAT-05 Kriterium 1: Rolle catalog_importer mit Einstellung on (der Import setzt sie per set local) markiert nichts (Insert und Zuordnung)');
|
||||
|
||||
set local role catalog_importer;
|
||||
update public.games set year = 2011 where id = 'f6000000-0000-0000-0000-0000000000f1';
|
||||
reset role;
|
||||
select is((select manual_fields from public.games where id = 'f6000000-0000-0000-0000-0000000000f1'),
|
||||
array['year'], 'KAT-05 Kriterium 1: Rolle catalog_importer ohne set local wird gekennzeichnet');
|
||||
array['year'], 'KAT-05 Kriterium 1: Rolle catalog_importer ohne die Einstellung wird gekennzeichnet');
|
||||
|
||||
-- Zuordnungen als Pseudo-Felder
|
||||
update public.games set manual_fields = '{}' where id = 'f6000000-0000-0000-0000-0000000000f1';
|
||||
|
|
@ -197,14 +196,22 @@ delete from public.game_publishers where game_id = 'f6000000-0000-0000-0000-0000
|
|||
select is((select manual_fields from public.games where id = 'f6000000-0000-0000-0000-000000000002'),
|
||||
array['genres', 'publishers', 'title'], 'KAT-05 Kriterium 1: Zuordnung löschen markiert genres und publishers');
|
||||
|
||||
-- Stabilität: eine bereits gesetzte Markierung ändert die Spielzeile nicht noch einmal
|
||||
-- Stabilität: ist 'genres' schon markiert, schreibt eine neue oder gelöschte Zuordnung die Spielzeile nicht noch einmal.
|
||||
-- (ctid statt updated_at, weil now() in der Transaktion konstant ist; jedes UPDATE erzeugt eine neue ctid.)
|
||||
create temp table snap_game on commit drop as
|
||||
select ctid::text as c, updated_at from public.games where id = 'f6000000-0000-0000-0000-000000000002';
|
||||
insert into public.game_mechanics (game_id, mechanic_id) values ('f6000000-0000-0000-0000-000000000002', 'f6000000-0000-0000-0000-0000000000e3')
|
||||
on conflict do nothing;
|
||||
delete from public.game_genres where game_id = 'f6000000-0000-0000-0000-000000000002';
|
||||
select ctid::text as c from public.games where id = 'f6000000-0000-0000-0000-000000000002';
|
||||
insert into public.game_genres (game_id, genre_id) values ('f6000000-0000-0000-0000-000000000002', 'f6000000-0000-0000-0000-0000000000e1');
|
||||
select is((select count(*) from public.game_genres where game_id = 'f6000000-0000-0000-0000-000000000002'), 1::bigint,
|
||||
'Neue Zuordnung ist angelegt (Trigger hat gefeuert)');
|
||||
select is((select 'genres' = any(manual_fields) from public.games where id = 'f6000000-0000-0000-0000-000000000002'), true,
|
||||
'genres ist bereits markiert');
|
||||
select is((select g.ctid::text from public.games g where g.id = 'f6000000-0000-0000-0000-000000000002'),
|
||||
(select c from snap_game), 'Eine schon vorhandene Markierung schreibt die Spielzeile nicht erneut');
|
||||
(select c from snap_game), 'Eine neue Zuordnung schreibt die Spielzeile nicht erneut, wenn die Markierung schon da ist');
|
||||
delete from public.game_genres where game_id = 'f6000000-0000-0000-0000-000000000002';
|
||||
select is((select count(*) from public.game_genres where game_id = 'f6000000-0000-0000-0000-000000000002'), 0::bigint,
|
||||
'Die Zuordnung ist gelöscht (Trigger hat gefeuert)');
|
||||
select is((select g.ctid::text from public.games g where g.id = 'f6000000-0000-0000-0000-000000000002'),
|
||||
(select c from snap_game), 'Eine gelöschte Zuordnung schreibt die Spielzeile nicht erneut, wenn die Markierung schon da ist');
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------
|
||||
-- View catalog_games: Ausblenden, Zusammenführen, Anzeige-Erlaubnis (Kriterien 4 und 6)
|
||||
|
|
@ -301,6 +308,16 @@ select is_empty($$select p.proname from pg_proc p join pg_namespace n on n.oid =
|
|||
select is_empty($$select t from unnest(array['catalog_sources', 'publishers', 'game_publishers']) as t
|
||||
where not (select relrowsecurity from pg_class where oid = ('public.' || t)::regclass)$$,
|
||||
'Die neuen Tabellen haben Row Level Security');
|
||||
select is_empty($$select t, p from unnest(array['catalog_sources', 'publishers', 'game_publishers']) as t,
|
||||
unnest(array['insert', 'update', 'delete', 'truncate', 'references', 'trigger']) as p
|
||||
where has_table_privilege('anon', 'public.' || t, p)
|
||||
or (p in ('truncate', 'references', 'trigger') and has_table_privilege('authenticated', 'public.' || t, p))$$,
|
||||
'Grant-Matrix: anon hat auf den neuen Tabellen nur select, niemand hat truncate, references oder trigger');
|
||||
select is_empty($$select t, p from unnest(array['catalog_sources', 'publishers', 'game_publishers']) as t,
|
||||
unnest(array['select', 'insert', 'update', 'delete']) as p
|
||||
where not has_table_privilege('authenticated', 'public.' || t, p)
|
||||
or (p = 'select' and not has_table_privilege('anon', 'public.' || t, p))$$,
|
||||
'Grant-Matrix: authenticated hat select, insert, update, delete (Schutz durch Policies), anon select');
|
||||
|
||||
-- Besucher
|
||||
set local role anon;
|
||||
|
|
@ -318,6 +335,10 @@ select throws_ok($$update public.games set title = 'Gehackt' where id = 'f600000
|
|||
'KAT-05 Kriterium 9: Besucher ändern keine Katalogspiele');
|
||||
select throws_ok($$delete from public.game_publishers where game_id = 'f6000000-0000-0000-0000-000000000001'$$, '42501', null,
|
||||
'Besucher löschen keine Verlagszuordnungen');
|
||||
select throws_ok($$update public.catalog_sources set texts_allowed = true$$, '42501', null, 'Besucher ändern keine Quellen');
|
||||
select throws_ok($$delete from public.catalog_sources$$, '42501', null, 'Besucher löschen keine Quellen');
|
||||
select throws_ok($$update public.publishers set name = 'Gehackt'$$, '42501', null, 'Besucher ändern keine Verlage');
|
||||
select throws_ok($$delete from public.publishers$$, '42501', null, 'Besucher löschen keine Verlage');
|
||||
select throws_ok($$update public.catalog_games set title = 'Gehackt'$$, '55000', null, 'Die View catalog_games ist für Besucher nicht schreibbar (nicht aktualisierbar)');
|
||||
select is_empty($$select p from unnest(array['insert', 'update', 'delete']) as p
|
||||
where has_table_privilege('anon', 'public.catalog_games', p) or has_table_privilege('authenticated', 'public.catalog_games', p)$$,
|
||||
|
|
@ -385,6 +406,13 @@ select throws_ok($$insert into public.publishers (name) values ('Admin-Verlag')$
|
|||
'KAT-05 Kriterium 9: user_metadata.role = admin gibt keine Rechte');
|
||||
select is_empty($$update public.games set title = 'Gehackt' where id = 'f6000000-0000-0000-0000-000000000001' returning 1$$,
|
||||
'KAT-05 Kriterium 9: user_metadata.role = admin ändert keine Katalogspiele');
|
||||
select throws_ok($$insert into public.catalog_sources (key, name) values ('admin', 'Admin')$$, '42501', null,
|
||||
'KAT-05 Kriterium 9: user_metadata.role = admin legt keine Quellen an');
|
||||
select is_empty($$update public.catalog_sources set texts_allowed = true returning 1$$,
|
||||
'KAT-05 Kriterium 9: user_metadata.role = admin ändert keine Quellen');
|
||||
select throws_ok($$insert into public.game_publishers (game_id, publisher_id)
|
||||
values ('f6000000-0000-0000-0000-000000000002', 'f6000000-0000-0000-0000-0000000000c1')$$, '42501', null,
|
||||
'KAT-05 Kriterium 9: user_metadata.role = admin ordnet Katalogspielen keinen Verlag zu');
|
||||
reset role;
|
||||
|
||||
-- Ben
|
||||
|
|
@ -412,14 +440,19 @@ select is((select count(*) from public.games where id = 'f6000000-0000-0000-0000
|
|||
-- ---------------------------------------------------------------------------------------------
|
||||
insert into public.genres (name_de, name_en) values ('Strategiespiel', 'Strategy game') on conflict do nothing;
|
||||
|
||||
-- Abschnitt 3: Spiel finden (vorab nachschlagen; das Spiel gibt es hier noch nicht)
|
||||
select is((select count(*) from (
|
||||
select id, title, year, hidden_at from public.games
|
||||
where visibility = 'catalog' and lower(title) = lower('Beispielspiel Runbook')) f), 0::bigint, 'Runbook 3: Suche vor dem Anlegen findet nichts');
|
||||
|
||||
-- Abschnitt 3: Neues Katalogspiel mit Verlag und Genre
|
||||
insert into public.publishers (name) values ('Beispielverlag')
|
||||
on conflict ((lower(name))) do nothing;
|
||||
|
||||
with neu as (
|
||||
insert into public.games (title, year, min_players, max_players, min_minutes, max_minutes, min_age,
|
||||
insert into public.games (id, title, year, min_players, max_players, min_minutes, max_minutes, min_age,
|
||||
description, description_locale, visibility)
|
||||
values ('Beispielspiel Runbook', 2020, 2, 4, 30, 60, 8, 'Eigener Kurztext.', 'de', 'catalog')
|
||||
values ('7a1b2c3d-0000-4000-8000-000000000001', 'Beispielspiel Runbook', 2020, 2, 4, 30, 60, 8, 'Eigener Kurztext.', 'de', 'catalog')
|
||||
returning id
|
||||
), verlag as (
|
||||
insert into public.game_publishers (game_id, publisher_id)
|
||||
|
|
@ -428,7 +461,7 @@ with neu as (
|
|||
insert into public.game_genres (game_id, genre_id)
|
||||
select neu.id, g.id from neu, public.genres g where g.name_de = 'Strategiespiel';
|
||||
|
||||
select is((select count(*) from public.games where title = 'Beispielspiel Runbook' and visibility = 'catalog' and owner_id is null), 1::bigint,
|
||||
select is((select count(*) from public.games where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog' and owner_id is null), 1::bigint,
|
||||
'KAT-05 Kriterium 8: Runbook 3, Spiel anlegen');
|
||||
select is((select manual_fields from public.games where title = 'Beispielspiel Runbook'),
|
||||
array['description', 'description_locale', 'genres', 'max_minutes', 'max_players', 'min_age', 'min_minutes', 'min_players',
|
||||
|
|
@ -439,25 +472,29 @@ select is((select count(*) from public.game_publishers gp join public.games g on
|
|||
select is((select count(*) from public.game_genres gg join public.games g on g.id = gg.game_id
|
||||
where g.title = 'Beispielspiel Runbook'), 1::bigint, 'Runbook 3: Genre zugeordnet');
|
||||
|
||||
select is((select count(*) from (
|
||||
select id, title, year, hidden_at from public.games
|
||||
where visibility = 'catalog' and lower(title) = lower('Beispielspiel Runbook')) f), 1::bigint, 'Runbook 3: Suche findet das Spiel');
|
||||
|
||||
-- Abschnitt 3: Fakten ändern
|
||||
update public.games set min_age = 10, max_minutes = 90
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog';
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog';
|
||||
select is((select min_age from public.games where title = 'Beispielspiel Runbook'), 10::smallint, 'Runbook 3: Fakten ändern');
|
||||
|
||||
-- Abschnitt 3: Markierung bewusst entfernen
|
||||
update public.games set manual_fields = array_remove(manual_fields, 'min_age')
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog';
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog';
|
||||
select is((select 'min_age' = any(manual_fields) from public.games where title = 'Beispielspiel Runbook'), false,
|
||||
'Runbook 3: Markierung entfernen');
|
||||
|
||||
-- Abschnitt 4: ausblenden
|
||||
update public.games set hidden_at = now()
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog' and hidden_at is null;
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog' and hidden_at is null;
|
||||
select is((select count(*) from public.catalog_games where title = 'Beispielspiel Runbook'), 0::bigint,
|
||||
'KAT-05 Kriterium 8: Runbook 4, ausgeblendet');
|
||||
-- Abschnitt 4: wieder einblenden
|
||||
update public.games set hidden_at = null
|
||||
where title = 'Beispielspiel Runbook' and visibility = 'catalog';
|
||||
where id = '7a1b2c3d-0000-4000-8000-000000000001' and visibility = 'catalog';
|
||||
select is((select count(*) from public.catalog_games where title = 'Beispielspiel Runbook'), 1::bigint,
|
||||
'KAT-05 Kriterium 8: Runbook 4, wieder eingeblendet');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue