572 lines
51 KiB
PL/PgSQL
572 lines
51 KiB
PL/PgSQL
-- KAT-05 (PR 2b): Zusammenführen. repoint_game_references, admin_merge_games, Rechte,
|
|
-- Fremdschlüssel-Wächter und die SQL-Bausteine aus docs/katalog-pflege.md, Abschnitt 5
|
|
-- (unverändert, ohne begin/commit; Auswahl immer per id).
|
|
-- Läuft auch mit eingespieltem Startbestand: eigene Fixtures mit festen IDs (Präfix f7/7a1b), gezählt wird nur darüber.
|
|
-- Ab 2c kennt der Fremdschlüssel-Wächter am Ende auch game_reports.target_game_id (behandelt) und
|
|
-- game_reports.provisional_game_id (bewusst ausgelassen); admin_merge_games meldet reports_repointed.
|
|
begin;
|
|
select plan(107);
|
|
|
|
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"}'),
|
|
('22222222-2222-2222-2222-222222222222', 'ben@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Ben"}'),
|
|
('33333333-3333-3333-3333-333333333333', 'cara@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Cara"}'),
|
|
('44444444-4444-4444-4444-444444444444', 'dora@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Dora"}');
|
|
|
|
-- ---------------------------------------------------------------------------------------------
|
|
-- Rechte (KAT-05 Kriterium 9)
|
|
-- ---------------------------------------------------------------------------------------------
|
|
select is_empty($$select p.proname, r from pg_proc p join pg_namespace n on n.oid = p.pronamespace,
|
|
unnest(array['anon', 'authenticated', 'service_role']) as r
|
|
where n.nspname = 'public' and p.proname in ('repoint_game_references', 'admin_merge_games')
|
|
and has_function_privilege(r, p.oid, 'execute')$$,
|
|
'KAT-05 Kriterium 9: repoint_game_references und admin_merge_games sind für anon, authenticated und service_role nicht ausführbar');
|
|
select is((select count(*) from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
where n.nspname = 'public' and p.proname in ('repoint_game_references', 'admin_merge_games')
|
|
and not p.prosecdef and p.proconfig = array['search_path=""']), 2::bigint,
|
|
'Beide Funktionen sind security invoker mit leerem search_path');
|
|
|
|
-- Aufruf wirklich versuchen, auch mit Admin-Claims im JWT (E10: kein is_admin()-Pfad)
|
|
select set_config('request.jwt.claims',
|
|
'{"sub":"11111111-1111-1111-1111-111111111111","role":"authenticated","app_metadata":{"role":"admin"}}', true);
|
|
set local role authenticated;
|
|
select throws_ok($$select public.admin_merge_games(gen_random_uuid(), gen_random_uuid())$$, '42501', null,
|
|
'KAT-05 Kriterium 9: authenticated (auch mit app_metadata.role = admin) kann admin_merge_games nicht aufrufen');
|
|
select throws_ok($$select public.repoint_game_references(gen_random_uuid(), gen_random_uuid())$$, '42501', null,
|
|
'KAT-05 Kriterium 9: authenticated kann repoint_game_references nicht aufrufen');
|
|
reset role;
|
|
set local role anon;
|
|
select throws_ok($$select public.admin_merge_games(gen_random_uuid(), gen_random_uuid())$$, '42501', null,
|
|
'KAT-05 Kriterium 9: anon kann admin_merge_games nicht aufrufen');
|
|
reset role;
|
|
set local role service_role;
|
|
select throws_ok($$select public.admin_merge_games(gen_random_uuid(), gen_random_uuid())$$, '42501', null,
|
|
'KAT-05 Kriterium 9: service_role kann admin_merge_games nicht aufrufen');
|
|
select throws_ok($$select public.repoint_game_references(gen_random_uuid(), gen_random_uuid())$$, '42501', null,
|
|
'KAT-05 Kriterium 9: service_role kann repoint_game_references nicht aufrufen');
|
|
reset role;
|
|
|
|
-- Die Rollenprüfung im Funktionskörper: eine Rolle MIT execute-Recht, aber ohne postgres/supabase_admin
|
|
create role merge_tester nologin;
|
|
grant merge_tester to postgres with set true;
|
|
grant usage on schema public to merge_tester;
|
|
grant execute on function public.admin_merge_games(uuid, uuid) to merge_tester;
|
|
set local role merge_tester;
|
|
do $$
|
|
begin
|
|
perform public.admin_merge_games('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-000000000002');
|
|
perform set_config('t.merge_err', 'kein Fehler', true);
|
|
exception when others then
|
|
perform set_config('t.merge_err', sqlstate || ':' || sqlerrm, true);
|
|
end
|
|
$$;
|
|
reset role;
|
|
select is(current_setting('t.merge_err'), 'P0001:admin_only',
|
|
'KAT-05 Kriterium 9: admin_merge_games prüft die Datenbankrolle (admin_only)');
|
|
|
|
-- ---------------------------------------------------------------------------------------------
|
|
-- Fehlerfälle von admin_merge_games
|
|
-- ---------------------------------------------------------------------------------------------
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-000000000001', 'Fehler Eins', 'catalog'),
|
|
('f7000000-0000-0000-0000-000000000002', 'Fehler Zwei', 'catalog');
|
|
insert into public.games (id, title, visibility, hidden_at) values
|
|
('f7000000-0000-0000-0000-000000000003', 'Fehler Versteckt', 'catalog', '2020-01-01');
|
|
insert into public.games (id, title, visibility, hidden_at, merged_into_id) values
|
|
('f7000000-0000-0000-0000-000000000004', 'Fehler Zusammengeführt', 'catalog', '2020-01-01', 'f7000000-0000-0000-0000-000000000001');
|
|
insert into public.games (id, title, visibility, owner_id) values
|
|
('f7000000-0000-0000-0000-0000000000a1', 'Fehler Privat', 'private', '11111111-1111-1111-1111-111111111111');
|
|
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-000000000001')$$,
|
|
'P0001', 'merge_same_game', 'KAT-05 Kriterium 4: Quelle gleich Ziel wird abgelehnt (merge_same_game)');
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-0000000000ff', 'f7000000-0000-0000-0000-000000000001')$$,
|
|
'P0001', 'merge_game_not_found', 'KAT-05 Kriterium 4: unbekannte Quelle (merge_game_not_found)');
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-0000000000ff')$$,
|
|
'P0001', 'merge_game_not_found', 'KAT-05 Kriterium 4: unbekanntes Ziel (merge_game_not_found)');
|
|
select throws_ok($$select public.admin_merge_games(null, 'f7000000-0000-0000-0000-000000000001')$$,
|
|
'P0001', 'merge_game_not_found', 'KAT-05 Kriterium 4: fehlende Quelle (null) ist merge_game_not_found');
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-0000000000a1', 'f7000000-0000-0000-0000-000000000001')$$,
|
|
'P0001', 'merge_not_catalog', 'KAT-05 Kriterium 4: private Quelle wird abgelehnt (merge_not_catalog)');
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-0000000000a1')$$,
|
|
'P0001', 'merge_not_catalog', 'KAT-05 Kriterium 4: privates Ziel wird abgelehnt (merge_not_catalog)');
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-000000000003')$$,
|
|
'P0001', 'merge_target_not_listed', 'KAT-05 Kriterium 4: ausgeblendetes Ziel wird abgelehnt (merge_target_not_listed)');
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000002', 'f7000000-0000-0000-0000-000000000004')$$,
|
|
'P0001', 'merge_target_not_listed', 'KAT-05 Kriterium 4: zusammengeführtes Ziel wird abgelehnt (merge_target_not_listed)');
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000004', 'f7000000-0000-0000-0000-000000000002')$$,
|
|
'P0001', 'merge_source_already_merged', 'KAT-05 Kriterium 4: bereits zusammengeführte Quelle wird abgelehnt (merge_source_already_merged)');
|
|
select is((select count(*) from public.games where id in ('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-000000000002')
|
|
and hidden_at is null and merged_into_id is null), 2::bigint,
|
|
'Nach Fehlern ist nichts verändert');
|
|
|
|
-- repoint_game_references: eigene Prüfungen (admin_merge_games kommt ihnen sonst zuvor)
|
|
select throws_ok($$select public.repoint_game_references('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-000000000001')$$,
|
|
'P0001', 'repoint_same_game', 'KAT-05 Kriterium 4: repoint mit p_from = p_to (repoint_same_game)');
|
|
select throws_ok($$select public.repoint_game_references('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-000000000003')$$,
|
|
'P0001', 'repoint_target_not_listed', 'KAT-05 Kriterium 4: repoint auf ein ausgeblendetes Ziel');
|
|
select throws_ok($$select public.repoint_game_references('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-000000000004')$$,
|
|
'P0001', 'repoint_target_not_listed', 'KAT-05 Kriterium 4: repoint auf ein zusammengeführtes Ziel');
|
|
select throws_ok($$select public.repoint_game_references('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-0000000000a1')$$,
|
|
'P0001', 'repoint_target_not_listed', 'KAT-05 Kriterium 4: repoint auf ein privates Ziel');
|
|
select throws_ok($$select public.repoint_game_references('f7000000-0000-0000-0000-000000000001', 'f7000000-0000-0000-0000-0000000000ff')$$,
|
|
'P0001', 'repoint_target_not_listed', 'KAT-05 Kriterium 4: repoint auf ein unbekanntes Ziel');
|
|
|
|
-- ---------------------------------------------------------------------------------------------
|
|
-- Hauptfall nach Runbook Abschnitt 5: zwei Katalogspiele (Quelle ...0002, Ziel ...0003)
|
|
-- ---------------------------------------------------------------------------------------------
|
|
insert into public.catalog_sources (key, name) values ('test_src', 'Testquelle');
|
|
|
|
-- Quelle ist älter als das Ziel; Titel der Quelle mit Kleinschreibung und Leerzeichen am Rand
|
|
insert into public.games (id, title, year, visibility, bgg_id, catalog_source_id, source_ref, source_url, source_synced_at, created_at) values
|
|
('7a1b2c3d-0000-4000-8000-000000000002', 'duplikat runbook ', 2021, 'catalog', 13,
|
|
(select id from public.catalog_sources where key = 'test_src'), 'ref-13', 'https://example.org/13', '2026-01-01', '2020-01-01');
|
|
insert into public.games (id, title, year, visibility, created_at) values
|
|
('7a1b2c3d-0000-4000-8000-000000000003', 'Duplikat Runbook', 2021, 'catalog', '2021-01-01');
|
|
insert into public.genres (id, name_de, name_en) values ('f7000000-0000-0000-0000-0000000000e1', 'Merge-Genre', 'Merge genre');
|
|
insert into public.game_genres (game_id, genre_id) values ('7a1b2c3d-0000-4000-8000-000000000002', 'f7000000-0000-0000-0000-0000000000e1');
|
|
|
|
-- Varianten. Ziel: zwei gleichnamige ("deluxe"/"DELUXE"); die ÄLTERE hat die größere id (...0f9 > ...0f0).
|
|
insert into public.game_variants (id, game_id, name, year, language, bgg_id, created_at) values
|
|
('f7000000-0000-0000-0000-0000000000f1', '7a1b2c3d-0000-4000-8000-000000000002', ' Deluxe ', 2021, 'de', null, '2020-01-01'),
|
|
('f7000000-0000-0000-0000-0000000000f2', '7a1b2c3d-0000-4000-8000-000000000002', 'Collector', 2022, 'en', 777, '2020-01-02'),
|
|
('f7000000-0000-0000-0000-0000000000f9', '7a1b2c3d-0000-4000-8000-000000000003', 'deluxe', null, null, null, '2020-01-01'),
|
|
('f7000000-0000-0000-0000-0000000000f0', '7a1b2c3d-0000-4000-8000-000000000003', 'DELUXE', null, null, null, '2021-01-01');
|
|
|
|
-- Sammlung: Anna (Wunschliste an Quelle, Sammlung am Ziel), Ben (nur Quelle), Cara (Einkauf/Wunsch, Notizen), Dora (Quelle besser)
|
|
insert into public.collection_items (id, owner_id, game_id, variant_id, list, note, acquired_on) values
|
|
('f7000000-0000-0000-0000-0000000000c1', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000002', 'f7000000-0000-0000-0000-0000000000f1', 'wishlist', 'Annas Notiz', '2020-02-02'),
|
|
('f7000000-0000-0000-0000-0000000000c2', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000003', null, 'owned', null, null),
|
|
('f7000000-0000-0000-0000-0000000000c3', '22222222-2222-2222-2222-222222222222', '7a1b2c3d-0000-4000-8000-000000000002', 'f7000000-0000-0000-0000-0000000000f2', 'owned', 'Bens Notiz', null),
|
|
('f7000000-0000-0000-0000-0000000000c4', '33333333-3333-3333-3333-333333333333', '7a1b2c3d-0000-4000-8000-000000000002', null, 'shopping', 'Caras Quellnotiz', '2020-03-03'),
|
|
('f7000000-0000-0000-0000-0000000000c5', '33333333-3333-3333-3333-333333333333', '7a1b2c3d-0000-4000-8000-000000000003', null, 'wishlist', 'Caras Zielnotiz', '2019-03-03'),
|
|
('f7000000-0000-0000-0000-0000000000c6', '44444444-4444-4444-4444-444444444444', '7a1b2c3d-0000-4000-8000-000000000002', null, 'owned', 'Doras Quellnotiz', null),
|
|
('f7000000-0000-0000-0000-0000000000c7', '44444444-4444-4444-4444-444444444444', '7a1b2c3d-0000-4000-8000-000000000003', null, 'shopping', '', null);
|
|
|
|
-- Favoriten: Anna hat beide (Quelle Platz 1, Ziel Platz 2), Ben nur die Quelle (Platz 3)
|
|
insert into public.favorite_games (id, profile_id, game_id, position) values
|
|
('f7000000-0000-0000-0000-0000000000d1', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000002', 1),
|
|
('f7000000-0000-0000-0000-0000000000d2', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000003', 2),
|
|
('f7000000-0000-0000-0000-0000000000d3', '22222222-2222-2222-2222-222222222222', '7a1b2c3d-0000-4000-8000-000000000002', 3);
|
|
|
|
-- Partien (mit zwei Editionen und ohne) und ein Wertungsbogen
|
|
insert into public.plays (id, owner_id, game_id, variant_id, played_at, client_id) values
|
|
('f7000000-0000-0000-0000-0000000000b1', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000002', 'f7000000-0000-0000-0000-0000000000f1', '2026-01-01', 'f7000000-0000-0000-0000-0000000000b1'),
|
|
('f7000000-0000-0000-0000-0000000000b2', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000002', 'f7000000-0000-0000-0000-0000000000f2', '2026-01-02', 'f7000000-0000-0000-0000-0000000000b2'),
|
|
('f7000000-0000-0000-0000-0000000000b3', '22222222-2222-2222-2222-222222222222', '7a1b2c3d-0000-4000-8000-000000000002', null, '2026-01-03', 'f7000000-0000-0000-0000-0000000000b3');
|
|
insert into public.score_sheets (id, owner_id, game_id, name) values
|
|
('f7000000-0000-0000-0000-0000000000b5', '11111111-1111-1111-1111-111111111111', '7a1b2c3d-0000-4000-8000-000000000002', 'Bogen');
|
|
|
|
-- Unbeteiligtes Spiel mit Verweisen: darf vom Zusammenführen nicht berührt werden
|
|
insert into public.games (id, title, visibility) values ('f7000000-0000-0000-0000-000000000501', 'Unbeteiligt', 'catalog');
|
|
insert into public.game_variants (id, game_id, name) values ('f7000000-0000-0000-0000-0000000005e1', 'f7000000-0000-0000-0000-000000000501', 'Deluxe');
|
|
insert into public.collection_items (id, owner_id, game_id, variant_id, list, note) values
|
|
('f7000000-0000-0000-0000-0000000005c1', '44444444-4444-4444-4444-444444444444', 'f7000000-0000-0000-0000-000000000501', 'f7000000-0000-0000-0000-0000000005e1', 'wishlist', 'bleibt');
|
|
insert into public.plays (id, owner_id, game_id, variant_id, played_at, client_id) values
|
|
('f7000000-0000-0000-0000-0000000005b1', '44444444-4444-4444-4444-444444444444', 'f7000000-0000-0000-0000-000000000501', 'f7000000-0000-0000-0000-0000000005e1', '2026-04-01', 'f7000000-0000-0000-0000-0000000005b1');
|
|
insert into public.favorite_games (id, profile_id, game_id, position) values
|
|
('f7000000-0000-0000-0000-0000000005d1', '44444444-4444-4444-4444-444444444444', 'f7000000-0000-0000-0000-000000000501', 1);
|
|
|
|
-- Runbook 5, Schritt 1: Duplikate finden (global; geprüft wird nur die Fixture-Zeile)
|
|
create temp table rb_dup as
|
|
select lower(btrim(title)) as titel_normalisiert, year,
|
|
array_agg(id order by created_at, id) as ids, count(*) as anzahl
|
|
from public.catalog_games
|
|
group by lower(btrim(title)), year
|
|
having count(*) > 1
|
|
order by 1, 2;
|
|
select results_eq($$select titel_normalisiert, year, ids, anzahl from rb_dup where ids && array['7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003']::uuid[]$$,
|
|
$$values ('duplikat runbook'::text, 2021::smallint, array['7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003']::uuid[], 2::bigint)$$,
|
|
'KAT-05 Kriterium 8: Runbook 5, Duplikate finden (Titel normalisiert, älteste ID zuerst)');
|
|
|
|
-- Runbook 5, Schritt 1: Kandidaten per Titel, IDs ablesen
|
|
create temp table rb_lookup as
|
|
select id, title, year, hidden_at, merged_into_id, bgg_id, created_at from public.games
|
|
where visibility = 'catalog' and lower(btrim(title)) = lower(btrim('Duplikat Runbook'))
|
|
order by created_at, id;
|
|
select results_eq($$select id from rb_lookup$$,
|
|
$$values ('7a1b2c3d-0000-4000-8000-000000000002'::uuid), ('7a1b2c3d-0000-4000-8000-000000000003'::uuid)$$,
|
|
'KAT-05 Kriterium 8: Runbook 5, Kandidaten per Titel (IDs, älteste zuerst)');
|
|
|
|
-- Runbook 5, Schritt 2: Vorschau Verweise an der Quelle
|
|
create temp table rb_preview as
|
|
select 'collection_items' as tabelle, count(*) as anzahl from public.collection_items where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'favorite_games', count(*) from public.favorite_games where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'plays', count(*) from public.plays where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'score_sheets', count(*) from public.score_sheets where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'game_variants', count(*) from public.game_variants where game_id = '7a1b2c3d-0000-4000-8000-000000000002';
|
|
select set_eq($$select tabelle, anzahl from rb_preview$$,
|
|
$$values ('collection_items'::text, 4::bigint), ('favorite_games', 2), ('plays', 3), ('score_sheets', 1), ('game_variants', 2)$$,
|
|
'KAT-05 Kriterium 8: Runbook 5, Vorschau der Verweise an der Quelle');
|
|
|
|
-- Runbook 5, Schritt 2: Vorschau Doppelbesitz
|
|
create temp table rb_dbl as
|
|
select s.owner_id, s.list as liste_quelle, t.list as liste_ziel
|
|
from public.collection_items s
|
|
join public.collection_items t on t.owner_id = s.owner_id and t.game_id = '7a1b2c3d-0000-4000-8000-000000000003'
|
|
where s.game_id = '7a1b2c3d-0000-4000-8000-000000000002';
|
|
select set_eq($$select owner_id, liste_quelle, liste_ziel from rb_dbl$$,
|
|
$$values ('11111111-1111-1111-1111-111111111111'::uuid, 'wishlist'::text, 'owned'::text),
|
|
('33333333-3333-3333-3333-333333333333', 'shopping', 'wishlist'),
|
|
('44444444-4444-4444-4444-444444444444', 'owned', 'shopping')$$,
|
|
'KAT-05 Kriterium 8: Runbook 5, Vorschau Doppelbesitz');
|
|
|
|
-- Runbook 5, Schritt 3: Zusammenführen
|
|
create temp table rb_merge as
|
|
select public.admin_merge_games('7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003');
|
|
|
|
select is((select admin_merge_games from rb_merge),
|
|
jsonb_build_object(
|
|
'source_id', '7a1b2c3d-0000-4000-8000-000000000002', 'target_id', '7a1b2c3d-0000-4000-8000-000000000003',
|
|
'collection_items', 1, 'collection_conflicts', 3, 'favorite_games', 1, 'favorite_conflicts', 1,
|
|
'plays', 3, 'score_sheets', 1, 'variants_copied', 1, 'variants_mapped', 1, 'variants_cleared', 0,
|
|
'bgg_id_transferred', true, 'source_transferred', true, 'chain_flattened', 0, 'reports_repointed', 0),
|
|
'KAT-05 Kriterium 8: Runbook 5, das Ergebnis-JSON zählt alles richtig');
|
|
|
|
-- Runbook 5, Schritt 4: Nachprüfen (der Vorschau-Baustein aus Schritt 2 liefert überall 0)
|
|
drop table rb_preview;
|
|
create temp table rb_preview as
|
|
select 'collection_items' as tabelle, count(*) as anzahl from public.collection_items where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'favorite_games', count(*) from public.favorite_games where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'plays', count(*) from public.plays where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'score_sheets', count(*) from public.score_sheets where game_id = '7a1b2c3d-0000-4000-8000-000000000002'
|
|
union all select 'game_variants', count(*) from public.game_variants where game_id = '7a1b2c3d-0000-4000-8000-000000000002';
|
|
select set_eq($$select tabelle, anzahl from rb_preview$$,
|
|
$$values ('collection_items'::text, 0::bigint), ('favorite_games', 0), ('plays', 0), ('score_sheets', 0), ('game_variants', 0)$$,
|
|
'KAT-05 Kriterium 4: nach dem Zusammenführen hängt nichts mehr an der Quelle');
|
|
|
|
-- Runbook 5, Schritt 4: Zustand beider Einträge
|
|
create temp table rb_state as
|
|
select id, title, hidden_at is not null as ausgeblendet, merged_into_id, bgg_id, catalog_source_id
|
|
from public.games where id in ('7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003');
|
|
select set_eq($$select id, ausgeblendet, merged_into_id, bgg_id, catalog_source_id is not null from rb_state$$,
|
|
$$values ('7a1b2c3d-0000-4000-8000-000000000002'::uuid, true, '7a1b2c3d-0000-4000-8000-000000000003'::uuid, null::integer, false),
|
|
('7a1b2c3d-0000-4000-8000-000000000003', false, null, 13, true)$$,
|
|
'KAT-05 Kriterium 4: Quelle ausgeblendet und verwiesen, Ziel sichtbar und mit BGG-Kennung und Quelle der Quelle');
|
|
|
|
-- Runbook 5, Schritt 4: Nachprüfung auf Ketten (muss leer sein)
|
|
create temp table rb_ketten as
|
|
select id from public.games where merged_into_id = '7a1b2c3d-0000-4000-8000-000000000002';
|
|
select is_empty($$select * from rb_ketten$$, 'KAT-05 Kriterium 8: Runbook 5, kein Spiel verweist mehr auf die Quelle');
|
|
|
|
-- Runbook 5, Fehlertext merge_target_not_listed: wohin verweist ein zusammengeführtes Spiel?
|
|
create temp table rb_verweis as
|
|
select id, title, hidden_at, merged_into_id from public.games where id = '7a1b2c3d-0000-4000-8000-000000000002';
|
|
select results_eq($$select merged_into_id from rb_verweis$$, $$values ('7a1b2c3d-0000-4000-8000-000000000003'::uuid)$$,
|
|
'KAT-05 Kriterium 8: Runbook 5, Nachschlagen des endgültigen Ziels über merged_into_id');
|
|
|
|
-- Weitere Prüfungen am Ergebnis des Hauptfalls
|
|
select is((select count(*) from public.catalog_games where id in ('7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003')), 1::bigint,
|
|
'KAT-05 Kriterium 4: catalog_games enthält nur noch das Ziel');
|
|
select is((select count(*) from public.games where id = '7a1b2c3d-0000-4000-8000-000000000002'), 1::bigint,
|
|
'Die Quelle wird nicht gelöscht, ihre ID bleibt gültig');
|
|
select is((select row(catalog_source_id, source_ref, source_url, source_synced_at)::text from public.games where id = '7a1b2c3d-0000-4000-8000-000000000002'),
|
|
'(,,,)', 'Die Quellen-Angaben sind an der Quelle geleert');
|
|
select is((select row(catalog_source_id = (select id from public.catalog_sources where key = 'test_src'), source_ref, source_url, source_synced_at)::text
|
|
from public.games where id = '7a1b2c3d-0000-4000-8000-000000000003'),
|
|
'(t,ref-13,https://example.org/13,"2026-01-01 00:00:00+00")', 'Das Ziel hat Quelle, Kennung, URL und Abgleichszeit übernommen');
|
|
select is((select title from public.games where id = '7a1b2c3d-0000-4000-8000-000000000003'), 'Duplikat Runbook',
|
|
'Das Ziel behält seine Fakten');
|
|
select is((select count(*) from public.game_genres where game_id = '7a1b2c3d-0000-4000-8000-000000000002'), 1::bigint,
|
|
'Zuordnungen bleiben am ausgeblendeten Quell-Eintrag');
|
|
select is((select count(*) from public.game_genres where game_id = '7a1b2c3d-0000-4000-8000-000000000003'), 0::bigint,
|
|
'Zuordnungen werden nicht auf das Ziel kopiert');
|
|
|
|
-- Sammlung (Spec-F1): Kollisionsregel
|
|
select is((select row(list, note, acquired_on, variant_id)::text from public.collection_items where id = 'f7000000-0000-0000-0000-0000000000c2'),
|
|
'(owned,"Annas Notiz",2020-02-02,f7000000-0000-0000-0000-0000000000f9)',
|
|
'Spec-F1: Anna: Ziel gewinnt die Liste (owned vor wishlist), leere Felder aus der Quelle, Edition über die Variantenzuordnung (älteste gleichnamige Zielvariante)');
|
|
select is((select count(*) from public.collection_items where id = 'f7000000-0000-0000-0000-0000000000c1'), 0::bigint,
|
|
'Spec-F1: der Quell-Eintrag der Kollision ist gelöscht');
|
|
select isnt((select variant_id from public.collection_items where id = 'f7000000-0000-0000-0000-0000000000c3'), 'f7000000-0000-0000-0000-0000000000f2'::uuid,
|
|
'Ben: die Edition zeigt nicht mehr auf die alte Variante');
|
|
select is((select row(c.game_id, c.list, c.note)::text || ':' || v.name || ':' || v.game_id::text || ':' || v.year::text || ':' || v.language || ':' || v.bgg_id::text
|
|
from public.collection_items c join public.game_variants v on v.id = c.variant_id where c.id = 'f7000000-0000-0000-0000-0000000000c3'),
|
|
'(7a1b2c3d-0000-4000-8000-000000000003,owned,"Bens Notiz"):Collector:7a1b2c3d-0000-4000-8000-000000000003:2022:en:777',
|
|
'Ben: ohne Kollision wechselt der Eintrag auf das Ziel, die Edition ist eine Kopie am Ziel (Name, Jahr, Sprache, BGG-Kennung)');
|
|
select is((select row(list, note, acquired_on)::text from public.collection_items where id = 'f7000000-0000-0000-0000-0000000000c5'),
|
|
'(wishlist,"Caras Zielnotiz",2019-03-03)',
|
|
'Spec-F1: Cara: wishlist vor shopping; eine vorhandene Notiz und ein vorhandenes Datum am Ziel werden nicht überschrieben');
|
|
select is((select row(list, note, acquired_on)::text from public.collection_items where id = 'f7000000-0000-0000-0000-0000000000c7'),
|
|
'(owned,"Doras Quellnotiz",)',
|
|
'Spec-F1: Dora: owned (Quelle) vor shopping (Ziel); eine leere Notiz am Ziel wird aus der Quelle gefüllt');
|
|
select is((select count(*) from public.collection_items where owner_id = '44444444-4444-4444-4444-444444444444'
|
|
and game_id in ('7a1b2c3d-0000-4000-8000-000000000002', '7a1b2c3d-0000-4000-8000-000000000003')), 1::bigint,
|
|
'Spec-F1: Doras Quell-Eintrag ist gelöscht');
|
|
select is((select row(c.game_id, c.variant_id, c.list, c.note)::text || '|' || p.game_id::text || p.variant_id::text || '|' || f.game_id::text || f.position::text
|
|
from public.collection_items c, public.plays p, public.favorite_games f
|
|
where c.id = 'f7000000-0000-0000-0000-0000000005c1' and p.id = 'f7000000-0000-0000-0000-0000000005b1' and f.id = 'f7000000-0000-0000-0000-0000000005d1'),
|
|
'(f7000000-0000-0000-0000-000000000501,f7000000-0000-0000-0000-0000000005e1,wishlist,bleibt)|f7000000-0000-0000-0000-000000000501f7000000-0000-0000-0000-0000000005e1|f7000000-0000-0000-0000-0000000005011',
|
|
'Daten unbeteiligter Spiele bleiben unverändert (Sammlung, Partie, Favorit, Variante)');
|
|
select is((select count(*) from public.game_variants where game_id = 'f7000000-0000-0000-0000-000000000501'), 1::bigint,
|
|
'Varianten unbeteiligter Spiele bleiben erhalten');
|
|
select is((select count(*) from public.collection_items where game_id = '7a1b2c3d-0000-4000-8000-000000000003'), 4::bigint,
|
|
'Jeder der vier Nutzer hat genau einen Eintrag am Ziel');
|
|
|
|
-- Favoriten
|
|
select is((select array_agg(position order by position) from public.favorite_games
|
|
where profile_id = '11111111-1111-1111-1111-111111111111'), array[2::smallint],
|
|
'Favoriten: Anna hatte beide; nur der Favorit am Ziel (Platz 2) bleibt');
|
|
select is((select row(game_id, position)::text from public.favorite_games where id = 'f7000000-0000-0000-0000-0000000000d3'),
|
|
'(7a1b2c3d-0000-4000-8000-000000000003,3)', 'Favoriten: Ben wechselt auf das Ziel und behält seinen Platz');
|
|
|
|
-- Partien
|
|
select is((select row(game_id, variant_id)::text from public.plays where id = 'f7000000-0000-0000-0000-0000000000b1'),
|
|
'(7a1b2c3d-0000-4000-8000-000000000003,f7000000-0000-0000-0000-0000000000f9)',
|
|
'Partien: Edition wird auf die älteste gleichnamige Zielvariante abgebildet');
|
|
select is((select p.game_id::text || ':' || v.game_id::text || ':' || v.name
|
|
from public.plays p join public.game_variants v on v.id = p.variant_id where p.id = 'f7000000-0000-0000-0000-0000000000b2'),
|
|
'7a1b2c3d-0000-4000-8000-000000000003:7a1b2c3d-0000-4000-8000-000000000003:Collector',
|
|
'Partien: die Edition "Collector" zeigt auf die Kopie am Ziel');
|
|
select is((select row(game_id, variant_id)::text from public.plays where id = 'f7000000-0000-0000-0000-0000000000b3'),
|
|
'(7a1b2c3d-0000-4000-8000-000000000003,)', 'Partien: ohne Edition bleibt es ohne Edition');
|
|
select is((select game_id from public.score_sheets where id = 'f7000000-0000-0000-0000-0000000000b5'), '7a1b2c3d-0000-4000-8000-000000000003'::uuid,
|
|
'Wertungsbogen am Ziel');
|
|
|
|
-- Varianten
|
|
select is((select count(*) from public.game_variants where game_id = '7a1b2c3d-0000-4000-8000-000000000002'), 0::bigint,
|
|
'Die Varianten der Quelle sind gelöscht');
|
|
select is((select count(*) from public.game_variants where game_id = '7a1b2c3d-0000-4000-8000-000000000003'), 3::bigint,
|
|
'Das Ziel hat 2 eigene + 1 kopierte Variante (die gleichnamige wurde abgebildet, nicht kopiert)');
|
|
select is((select count(*) from public.game_variants where id in ('f7000000-0000-0000-0000-0000000000f9', 'f7000000-0000-0000-0000-0000000000f0')), 2::bigint,
|
|
'Eigene Zielvarianten bleiben erhalten');
|
|
select is((select count(distinct variant_id) from (
|
|
select variant_id from public.plays where id = 'f7000000-0000-0000-0000-0000000000b2'
|
|
union all select variant_id from public.collection_items where id = 'f7000000-0000-0000-0000-0000000000c3') x), 1::bigint,
|
|
'Dieselbe Quellvariante wird nur einmal kopiert (Partie und Sammlung zeigen auf dieselbe Kopie)');
|
|
|
|
-- Löschen eines Ziels mit Verweis (nur der Verweis merged_into_id, ohne andere Verweise)
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-0000000000b8', 'Löschziel', 'catalog'), ('f7000000-0000-0000-0000-0000000000b9', 'Löschquelle', 'catalog');
|
|
select lives_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-0000000000b9', 'f7000000-0000-0000-0000-0000000000b8')$$, 'Zusammenführen läuft ohne Fehler (0000b8)');
|
|
select throws_ok($$delete from public.games where id = 'f7000000-0000-0000-0000-0000000000b8'$$, '23503',
|
|
null, 'KAT-05 Kriterium 4: Ein Ziel mit Verweis lässt sich nicht löschen (on delete restrict)');
|
|
|
|
-- ---------------------------------------------------------------------------------------------
|
|
-- Konflikte bei BGG-Kennung und Quelle (W6), Ketten, bereits ausgeblendete Quelle
|
|
-- ---------------------------------------------------------------------------------------------
|
|
insert into public.games (id, title, visibility, bgg_id, catalog_source_id, source_ref, source_url, source_synced_at) values
|
|
('f7000000-0000-0000-0000-000000000101', 'Konflikt Quelle', 'catalog', 14,
|
|
(select id from public.catalog_sources where key = 'test_src'), 'quelle-ref', 'https://example.org/q', '2026-02-02'),
|
|
('f7000000-0000-0000-0000-000000000102', 'Konflikt Ziel', 'catalog', 42,
|
|
(select id from public.catalog_sources where key = 'test_src'), 'ziel-ref', 'https://example.org/z', '2026-03-03');
|
|
create temp table rb_conflict as select public.admin_merge_games('f7000000-0000-0000-0000-000000000101', 'f7000000-0000-0000-0000-000000000102') as r;
|
|
select is((select r -> 'bgg_id_conflict' from rb_conflict), '{"source": 14, "target": 42}'::jsonb,
|
|
'KAT-05 Kriterium 4: bgg_id-Konflikt wird mit den alten Werten gemeldet');
|
|
select is((select r -> 'source_conflict' from rb_conflict), jsonb_build_object(
|
|
'source', jsonb_build_object('catalog_source_id', (select id from public.catalog_sources where key = 'test_src'), 'source_ref', 'quelle-ref', 'source_url', 'https://example.org/q', 'source_synced_at', '2026-02-02T00:00:00+00:00'),
|
|
'target', jsonb_build_object('catalog_source_id', (select id from public.catalog_sources where key = 'test_src'), 'source_ref', 'ziel-ref', 'source_url', 'https://example.org/z', 'source_synced_at', '2026-03-03T00:00:00+00:00')),
|
|
'KAT-05 Kriterium 4: Quellen-Konflikt wird mit den alten Werten gemeldet');
|
|
select is((select r -> 'bgg_id_transferred' from rb_conflict), 'false'::jsonb, 'Bei Konflikt wird nichts übertragen (bgg_id)');
|
|
select is((select r -> 'source_transferred' from rb_conflict), 'false'::jsonb, 'Bei Konflikt wird nichts übertragen (Quelle)');
|
|
select is((select row(bgg_id, source_ref, source_url)::text from public.games where id = 'f7000000-0000-0000-0000-000000000102'),
|
|
'(42,ziel-ref,https://example.org/z)', 'Das Ziel behält bei Konflikt seine Werte');
|
|
select is((select row(bgg_id, catalog_source_id, source_ref, source_url, source_synced_at)::text from public.games where id = 'f7000000-0000-0000-0000-000000000101'),
|
|
'(,,,,)', 'Die Quelle ist bei Konflikt trotzdem geleert');
|
|
|
|
-- Ohne Konflikt gibt es keine Konfliktschlüssel; nur BGG-Kennung auf der Quelle
|
|
insert into public.games (id, title, visibility, bgg_id) values
|
|
('f7000000-0000-0000-0000-000000000111', 'Ohne Konflikt Quelle', 'catalog', 99),
|
|
('f7000000-0000-0000-0000-000000000112', 'Ohne Konflikt Ziel', 'catalog', null);
|
|
create temp table rb_noconf as select public.admin_merge_games('f7000000-0000-0000-0000-000000000111', 'f7000000-0000-0000-0000-000000000112') as r;
|
|
select is((select (r ? 'bgg_id_conflict') or (r ? 'source_conflict') from rb_noconf), false, 'Ohne Konflikt gibt es keine Konfliktschlüssel');
|
|
select is((select bgg_id from public.games where id = 'f7000000-0000-0000-0000-000000000112'), 99, 'BGG-Kennung geht auf das Ziel über, ohne den Unique-Index zu verletzen');
|
|
select is((select r -> 'bgg_id_transferred' from rb_noconf), 'true'::jsonb, 'bgg_id_transferred ist true');
|
|
select is((select r -> 'source_transferred' from rb_noconf), 'false'::jsonb, 'source_transferred ist false (die Quelle hatte keine)');
|
|
|
|
-- Ziel hat BGG-Kennung, Quelle nicht: Ziel bleibt, kein Konflikt
|
|
insert into public.games (id, title, visibility, bgg_id) values
|
|
('f7000000-0000-0000-0000-000000000121', 'Ziel hat BGG Quelle', 'catalog', null),
|
|
('f7000000-0000-0000-0000-000000000122', 'Ziel hat BGG Ziel', 'catalog', 55);
|
|
create temp table rb_tgt as select public.admin_merge_games('f7000000-0000-0000-0000-000000000121', 'f7000000-0000-0000-0000-000000000122') as r;
|
|
select is((select bgg_id from public.games where id = 'f7000000-0000-0000-0000-000000000122'), 55, 'Ziel behält seine BGG-Kennung');
|
|
select is((select r ? 'bgg_id_conflict' from rb_tgt), false, 'Kein Konflikt, wenn nur das Ziel eine BGG-Kennung hat');
|
|
|
|
-- Bereits ausgeblendete Quelle behält ihren Zeitpunkt (coalesce)
|
|
insert into public.games (id, title, visibility, hidden_at) values
|
|
('f7000000-0000-0000-0000-000000000131', 'Versteckte Quelle', 'catalog', '2020-05-05 00:00:00+00'),
|
|
('f7000000-0000-0000-0000-000000000132', 'Ziel zu Versteckt', 'catalog', null);
|
|
select lives_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000131', 'f7000000-0000-0000-0000-000000000132')$$, 'Zusammenführen läuft ohne Fehler (000132)');
|
|
select is((select hidden_at from public.games where id = 'f7000000-0000-0000-0000-000000000131'), '2020-05-05 00:00:00+00'::timestamptz,
|
|
'Eine bereits ausgeblendete Quelle behält ihr hidden_at');
|
|
select is((select merged_into_id from public.games where id = 'f7000000-0000-0000-0000-000000000131'), 'f7000000-0000-0000-0000-000000000132'::uuid,
|
|
'Die ausgeblendete Quelle ist zusammengeführt');
|
|
select is((select hidden_at is not null from public.games where id = 'f7000000-0000-0000-0000-000000000101'), true,
|
|
'Eine sichtbare Quelle wird ausgeblendet');
|
|
|
|
-- Ketten glätten: C -> A, dann A -> B ergibt C -> B
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-000000000141', 'Kette A', 'catalog'),
|
|
('f7000000-0000-0000-0000-000000000142', 'Kette B', 'catalog'),
|
|
('f7000000-0000-0000-0000-000000000143', 'Kette C', 'catalog');
|
|
select lives_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000143', 'f7000000-0000-0000-0000-000000000141')$$, 'Zusammenführen läuft ohne Fehler (000141)');
|
|
select is((select merged_into_id from public.games where id = 'f7000000-0000-0000-0000-000000000143'), 'f7000000-0000-0000-0000-000000000141'::uuid, 'Kette: C zeigt zuerst auf A');
|
|
create temp table rb_chain as select public.admin_merge_games('f7000000-0000-0000-0000-000000000141', 'f7000000-0000-0000-0000-000000000142') as r;
|
|
select is((select merged_into_id from public.games where id = 'f7000000-0000-0000-0000-000000000143'), 'f7000000-0000-0000-0000-000000000142'::uuid,
|
|
'KAT-05 Kriterium 4: Ketten werden geglättet (C zeigt danach direkt auf B)');
|
|
select is((select merged_into_id from public.games where id = 'f7000000-0000-0000-0000-000000000141'), 'f7000000-0000-0000-0000-000000000142'::uuid, 'A zeigt auf B');
|
|
select is((select r -> 'chain_flattened' from rb_chain), '1'::jsonb, 'chain_flattened zählt die geglättete Kette');
|
|
|
|
-- ---------------------------------------------------------------------------------------------
|
|
-- Namensabgleich: Gleichstand bei created_at -> kleinste id
|
|
-- ---------------------------------------------------------------------------------------------
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-000000000151', 'Gleichstand Quelle', 'catalog'),
|
|
('f7000000-0000-0000-0000-000000000152', 'Gleichstand Ziel', 'catalog');
|
|
-- zuerst die größeren ids einfügen, die kleinste zuletzt (die Zeilenreihenfolge darf nicht entscheiden)
|
|
insert into public.game_variants (id, game_id, name, created_at) values
|
|
('f7000000-0000-0000-0000-0000000001a2', 'f7000000-0000-0000-0000-000000000152', 'Edition', '2022-01-01'),
|
|
('f7000000-0000-0000-0000-0000000001a3', 'f7000000-0000-0000-0000-000000000152', 'EDITION', '2022-01-01'),
|
|
('f7000000-0000-0000-0000-0000000001a1', 'f7000000-0000-0000-0000-000000000152', ' edition', '2022-01-01'),
|
|
('f7000000-0000-0000-0000-0000000001b1', 'f7000000-0000-0000-0000-000000000151', 'edition ', '2020-01-01');
|
|
insert into public.plays (id, owner_id, game_id, variant_id, played_at, client_id) values
|
|
('f7000000-0000-0000-0000-0000000001c1', '11111111-1111-1111-1111-111111111111', 'f7000000-0000-0000-0000-000000000151',
|
|
'f7000000-0000-0000-0000-0000000001b1', '2026-02-01', 'f7000000-0000-0000-0000-0000000001c1');
|
|
select lives_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000151', 'f7000000-0000-0000-0000-000000000152')$$, 'Zusammenführen läuft ohne Fehler (000152)');
|
|
select is((select variant_id from public.plays where id = 'f7000000-0000-0000-0000-0000000001c1'), 'f7000000-0000-0000-0000-0000000001a1'::uuid,
|
|
'KAT-05 Kriterium 4: bei Gleichstand von created_at gilt die kleinste id');
|
|
select is((select count(*) from public.game_variants where game_id = 'f7000000-0000-0000-0000-000000000152'), 3::bigint,
|
|
'Eine gleichnamige Variante (ohne Groß-/Kleinschreibung und Randleerzeichen) wird nicht kopiert');
|
|
|
|
-- Quelle ohne Verweise: Zähler 0
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-000000000161', 'Leer Quelle', 'catalog'), ('f7000000-0000-0000-0000-000000000162', 'Leer Ziel', 'catalog');
|
|
select is(public.admin_merge_games('f7000000-0000-0000-0000-000000000161', 'f7000000-0000-0000-0000-000000000162') - 'source_id' - 'target_id',
|
|
'{"collection_items":0,"collection_conflicts":0,"favorite_games":0,"favorite_conflicts":0,"plays":0,"score_sheets":0,"variants_copied":0,"variants_mapped":0,"variants_cleared":0,"bgg_id_transferred":false,"source_transferred":false,"chain_flattened":0,"reports_repointed":0}'::jsonb,
|
|
'Zusammenführen ohne Verweise liefert lauter Nullen');
|
|
|
|
-- ---------------------------------------------------------------------------------------------
|
|
-- repoint_game_references: privater Eintrag -> Katalogspiel (RB1, Klärung N15): nichts kopieren
|
|
-- ---------------------------------------------------------------------------------------------
|
|
insert into public.games (id, title, visibility, owner_id) values
|
|
('f7000000-0000-0000-0000-0000000002a1', 'Vorläufig Anna', 'private', '11111111-1111-1111-1111-111111111111');
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-0000000002b1', 'Katalogziel', 'catalog');
|
|
insert into public.game_variants (id, game_id, name) values
|
|
('f7000000-0000-0000-0000-0000000002e1', 'f7000000-0000-0000-0000-0000000002a1', 'Deluxe'),
|
|
('f7000000-0000-0000-0000-0000000002e2', 'f7000000-0000-0000-0000-0000000002a1', 'Prototyp'),
|
|
('f7000000-0000-0000-0000-0000000002e3', 'f7000000-0000-0000-0000-0000000002b1', ' deluxe');
|
|
insert into public.collection_items (id, owner_id, game_id, variant_id, list) values
|
|
('f7000000-0000-0000-0000-0000000002c1', '11111111-1111-1111-1111-111111111111', 'f7000000-0000-0000-0000-0000000002a1', 'f7000000-0000-0000-0000-0000000002e2', 'owned');
|
|
insert into public.plays (id, owner_id, game_id, variant_id, played_at, client_id) values
|
|
('f7000000-0000-0000-0000-0000000002d1', '11111111-1111-1111-1111-111111111111', 'f7000000-0000-0000-0000-0000000002a1', 'f7000000-0000-0000-0000-0000000002e1', '2026-03-01', 'f7000000-0000-0000-0000-0000000002d1'),
|
|
('f7000000-0000-0000-0000-0000000002d2', '11111111-1111-1111-1111-111111111111', 'f7000000-0000-0000-0000-0000000002a1', 'f7000000-0000-0000-0000-0000000002e2', '2026-03-02', 'f7000000-0000-0000-0000-0000000002d2');
|
|
create temp table rb_priv as select public.repoint_game_references('f7000000-0000-0000-0000-0000000002a1', 'f7000000-0000-0000-0000-0000000002b1') as r;
|
|
select is((select r from rb_priv),
|
|
'{"collection_items":1,"collection_conflicts":0,"favorite_games":0,"favorite_conflicts":0,"plays":2,"score_sheets":0,"variants_copied":0,"variants_mapped":1,"variants_cleared":2}'::jsonb,
|
|
'KAT-05 Kriterium 4: privater Eintrag: nichts kopiert, eine Variante abgebildet, zwei Verweise geleert (variants_cleared)');
|
|
select is((select count(*) from public.game_variants where game_id = 'f7000000-0000-0000-0000-0000000002b1'), 1::bigint,
|
|
'Privater Eintrag: das Katalogziel bekommt keine Variante (RB1)');
|
|
select is((select count(*) from public.game_variants where game_id = 'f7000000-0000-0000-0000-0000000002a1'), 2::bigint,
|
|
'Privater Eintrag: die privaten Varianten bleiben am vorläufigen Eintrag');
|
|
select is((select row(game_id, variant_id)::text from public.collection_items where id = 'f7000000-0000-0000-0000-0000000002c1'),
|
|
'(f7000000-0000-0000-0000-0000000002b1,)', 'Privater Eintrag: Edition ohne gleichnamige Zielvariante wird am Sammlungseintrag geleert');
|
|
select is((select row(game_id, variant_id)::text from public.plays where id = 'f7000000-0000-0000-0000-0000000002d1'),
|
|
'(f7000000-0000-0000-0000-0000000002b1,f7000000-0000-0000-0000-0000000002e3)', 'Privater Eintrag: gleichnamige Zielvariante wird abgebildet');
|
|
select is((select row(game_id, variant_id)::text from public.plays where id = 'f7000000-0000-0000-0000-0000000002d2'),
|
|
'(f7000000-0000-0000-0000-0000000002b1,)', 'Privater Eintrag: Edition der Partie ohne Gegenstück wird geleert');
|
|
|
|
-- Kollision bei privatem Eintrag: das Ziel hat die Edition schon; sie bleibt, nichts gilt als geleert
|
|
insert into public.games (id, title, visibility, owner_id) values
|
|
('f7000000-0000-0000-0000-0000000002a2', 'Vorläufig Ben', 'private', '22222222-2222-2222-2222-222222222222');
|
|
insert into public.game_variants (id, game_id, name) values ('f7000000-0000-0000-0000-0000000002e4', 'f7000000-0000-0000-0000-0000000002a2', 'Nur privat');
|
|
insert into public.collection_items (id, owner_id, game_id, variant_id, list) values
|
|
('f7000000-0000-0000-0000-0000000002c2', '22222222-2222-2222-2222-222222222222', 'f7000000-0000-0000-0000-0000000002a2', 'f7000000-0000-0000-0000-0000000002e4', 'wishlist'),
|
|
('f7000000-0000-0000-0000-0000000002c3', '22222222-2222-2222-2222-222222222222', 'f7000000-0000-0000-0000-0000000002b1', 'f7000000-0000-0000-0000-0000000002e3', 'owned');
|
|
select is((select (r ->> 'variants_cleared')::int from (select public.repoint_game_references('f7000000-0000-0000-0000-0000000002a2', 'f7000000-0000-0000-0000-0000000002b1') as r) x), 0,
|
|
'Kollision: hat das Ziel schon eine Edition, geht keine verloren');
|
|
select is((select row(list, variant_id)::text from public.collection_items where id = 'f7000000-0000-0000-0000-0000000002c3'),
|
|
'(owned,f7000000-0000-0000-0000-0000000002e3)', 'Kollision: die Edition des Zieleintrags bleibt');
|
|
|
|
-- Kollision mit leerer Zieledition: die Variante der Quelle kommt über die Zuordnung in den Zieleintrag (Spec-F1)
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-0000000003a1', 'Kollision Quelle', 'catalog'), ('f7000000-0000-0000-0000-0000000003b1', 'Kollision Ziel', 'catalog');
|
|
insert into public.game_variants (id, game_id, name) values
|
|
('f7000000-0000-0000-0000-0000000003e1', 'f7000000-0000-0000-0000-0000000003a1', 'Jubiläum');
|
|
insert into public.collection_items (id, owner_id, game_id, variant_id, list) values
|
|
('f7000000-0000-0000-0000-0000000003c1', '33333333-3333-3333-3333-333333333333', 'f7000000-0000-0000-0000-0000000003a1', 'f7000000-0000-0000-0000-0000000003e1', 'owned'),
|
|
('f7000000-0000-0000-0000-0000000003c2', '33333333-3333-3333-3333-333333333333', 'f7000000-0000-0000-0000-0000000003b1', null, 'owned');
|
|
select lives_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-0000000003a1', 'f7000000-0000-0000-0000-0000000003b1')$$, 'Zusammenführen läuft ohne Fehler (0003b1)');
|
|
select is((select v.name || '/' || v.game_id::text from public.collection_items c join public.game_variants v on v.id = c.variant_id
|
|
where c.id = 'f7000000-0000-0000-0000-0000000003c2'),
|
|
'Jubiläum/f7000000-0000-0000-0000-0000000003b1', 'Spec-F1: Kollision: die Edition der Quelle kommt über die Zuordnung (Kopie am Ziel) in den Zieleintrag');
|
|
|
|
-- Spec-F1 Kernregel (W1): Doppelbesitz, beide Einträge haben eine eigene Edition -> der Zieleintrag behält seine
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-000000000401', 'Edition Quelle', 'catalog'), ('f7000000-0000-0000-0000-000000000402', 'Edition Ziel', 'catalog');
|
|
insert into public.game_variants (id, game_id, name) values
|
|
('f7000000-0000-0000-0000-0000000004e1', 'f7000000-0000-0000-0000-000000000401', 'Y-Edition'),
|
|
('f7000000-0000-0000-0000-0000000004e2', 'f7000000-0000-0000-0000-000000000402', 'X-Edition');
|
|
insert into public.collection_items (id, owner_id, game_id, variant_id, list) values
|
|
('f7000000-0000-0000-0000-0000000004c1', '11111111-1111-1111-1111-111111111111', 'f7000000-0000-0000-0000-000000000401', 'f7000000-0000-0000-0000-0000000004e1', 'owned'),
|
|
('f7000000-0000-0000-0000-0000000004c2', '11111111-1111-1111-1111-111111111111', 'f7000000-0000-0000-0000-000000000402', 'f7000000-0000-0000-0000-0000000004e2', 'owned');
|
|
select lives_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000401', 'f7000000-0000-0000-0000-000000000402')$$,
|
|
'Zusammenführen mit Doppelbesitz und zwei Editionen läuft');
|
|
select is((select variant_id from public.collection_items where id = 'f7000000-0000-0000-0000-0000000004c2'),
|
|
'f7000000-0000-0000-0000-0000000004e2'::uuid, 'Spec-F1: Doppelbesitz: der Zieleintrag behält seine eigene Edition (X), die der Quelle (Y) verdrängt sie nicht');
|
|
|
|
-- N15 (W2): privater Eintrag -> Katalog, Doppelbesitz, Ziel ohne Edition, Quelle mit Edition ohne Gegenstück -> geleert und gezählt
|
|
insert into public.games (id, title, visibility, owner_id) values
|
|
('f7000000-0000-0000-0000-000000000411', 'Vorläufig Cara', 'private', '33333333-3333-3333-3333-333333333333');
|
|
insert into public.games (id, title, visibility) values ('f7000000-0000-0000-0000-000000000412', 'Ziel Cara', 'catalog');
|
|
insert into public.game_variants (id, game_id, name) values
|
|
('f7000000-0000-0000-0000-0000000004e3', 'f7000000-0000-0000-0000-000000000411', 'Nur bei Cara');
|
|
insert into public.collection_items (id, owner_id, game_id, variant_id, list) values
|
|
('f7000000-0000-0000-0000-0000000004c3', '33333333-3333-3333-3333-333333333333', 'f7000000-0000-0000-0000-000000000411', 'f7000000-0000-0000-0000-0000000004e3', 'owned'),
|
|
('f7000000-0000-0000-0000-0000000004c4', '33333333-3333-3333-3333-333333333333', 'f7000000-0000-0000-0000-000000000412', null, 'owned');
|
|
create temp table rb_n15 as select public.repoint_game_references('f7000000-0000-0000-0000-000000000411', 'f7000000-0000-0000-0000-000000000412') as r;
|
|
select is((select r from rb_n15),
|
|
'{"collection_items":0,"collection_conflicts":1,"favorite_games":0,"favorite_conflicts":0,"plays":0,"score_sheets":0,"variants_copied":0,"variants_mapped":0,"variants_cleared":1}'::jsonb,
|
|
'KAT-05 Kriterium 4 (N15): Doppelbesitz mit Edition ohne Gegenstück: variants_cleared = 1');
|
|
select is((select variant_id from public.collection_items where id = 'f7000000-0000-0000-0000-0000000004c4'), null::uuid,
|
|
'KAT-05 Kriterium 4 (N15): die Edition am Zieleintrag bleibt leer (nichts kopiert)');
|
|
select is((select count(*) from public.game_variants where game_id = 'f7000000-0000-0000-0000-000000000412'), 0::bigint,
|
|
'KAT-05 Kriterium 4 (N15): das Katalogspiel bekommt keine Variante');
|
|
|
|
-- Zwei gleichnamige Quellvarianten werden einzeln kopiert (nur gegen vorher vorhandene Zielvarianten abgeglichen)
|
|
insert into public.games (id, title, visibility) values
|
|
('f7000000-0000-0000-0000-000000000421', 'Zwilling Quelle', 'catalog'), ('f7000000-0000-0000-0000-000000000422', 'Zwilling Ziel', 'catalog');
|
|
insert into public.game_variants (id, game_id, name, created_at) values
|
|
('f7000000-0000-0000-0000-0000000004f1', 'f7000000-0000-0000-0000-000000000421', 'Twin', '2020-01-01'),
|
|
('f7000000-0000-0000-0000-0000000004f2', 'f7000000-0000-0000-0000-000000000421', 'twin ', '2021-01-01');
|
|
create temp table rb_twin as select public.admin_merge_games('f7000000-0000-0000-0000-000000000421', 'f7000000-0000-0000-0000-000000000422') as r;
|
|
select is((select (r ->> 'variants_copied') || '/' || (r ->> 'variants_mapped') from rb_twin), '2/0',
|
|
'Zwei gleichnamige Quellvarianten werden beide kopiert, keine auf die eben kopierte abgebildet');
|
|
select is((select count(*) from public.game_variants where game_id = 'f7000000-0000-0000-0000-000000000422'), 2::bigint,
|
|
'Das Ziel hat danach beide Kopien');
|
|
|
|
-- Ziel null
|
|
select throws_ok($$select public.admin_merge_games('f7000000-0000-0000-0000-000000000001', null)$$,
|
|
'P0001', 'merge_game_not_found', 'KAT-05 Kriterium 4: fehlendes Ziel (null) ist merge_game_not_found');
|
|
|
|
-- ---------------------------------------------------------------------------------------------
|
|
-- Fremdschlüssel-Wächter: jeder Verweis auf games ist entweder behandelt oder bewusst ausgelassen
|
|
-- ---------------------------------------------------------------------------------------------
|
|
create temp table fk_found as
|
|
select (select relname from pg_class where oid = c.conrelid) || '.' ||
|
|
(select attname from pg_attribute where attrelid = c.conrelid and attnum = c.conkey[1]) as fk
|
|
from pg_constraint c
|
|
where c.contype = 'f' and c.confrelid = 'public.games'::regclass;
|
|
select is((select count(*) from pg_constraint c where c.contype = 'f' and c.confrelid = 'public.games'::regclass and cardinality(c.conkey) > 1), 0::bigint,
|
|
'Fremdschlüssel-Wächter: alle Verweise auf games sind einspaltig');
|
|
select set_eq($$select fk from fk_found$$,
|
|
$$values
|
|
-- behandelt von repoint_game_references / admin_merge_games
|
|
('collection_items.game_id'), ('favorite_games.game_id'), ('plays.game_id'), ('score_sheets.game_id'),
|
|
('game_variants.game_id'), ('games.merged_into_id'), ('game_reports.target_game_id'),
|
|
-- bewusst ausgelassen: bleiben am ausgeblendeten Quell-Eintrag
|
|
('game_genres.game_id'), ('game_mechanics.game_id'), ('game_categories.game_id'), ('game_publishers.game_id'),
|
|
-- bewusst ausgelassen: zeigt auf ein privates Spiel, das Zusammenführen zweier Katalogspiele betrifft ihn nicht
|
|
('game_reports.provisional_game_id')$$,
|
|
'KAT-05 Kriterium 4: Fremdschlüssel-Wächter: neue Verweise auf games erzwingen eine Entscheidung im Zusammenführen');
|
|
|
|
-- Zweiter Wächter: Verweise auf game_variants. Sie werden über die Variantenzuordnung umgesetzt;
|
|
-- ein neuer Verweis (z. B. aus SAM-02/SAM-04) erzwingt eine Entscheidung in repoint_game_references.
|
|
select set_eq($$select (select relname from pg_class where oid = c.conrelid) || '(' ||
|
|
(select string_agg(a.attname, ', ' order by k.ord) from unnest(c.conkey) with ordinality as k(attnum, ord)
|
|
join pg_attribute a on a.attrelid = c.conrelid and a.attnum = k.attnum) || ')'
|
|
from pg_constraint c where c.contype = 'f' and c.confrelid = 'public.game_variants'::regclass$$,
|
|
$$values ('collection_items(variant_id, game_id)'), ('plays(variant_id, game_id)')$$,
|
|
'KAT-05 Kriterium 4: Fremdschlüssel-Wächter: Verweise auf game_variants sind genau Sammlung und Partien (beide über die Variantenzuordnung)');
|
|
|
|
select * from finish();
|
|
rollback;
|