-- Rauchtest: der Lebenszyklus, einmal wirklich ausgeführt -- -- docker compose run --rm psql -f /repo/db/tests/rauchtest.sql -- -- ═══ Warum es das gibt ════════════════════════════════════════════ -- -- rehire_employee war seit dem 17.09. bei jedem Aufruf kaputt: ein fehlender -- Umwandlungsschritt beim Status. Aufgefallen ist es erst beim Handtest am -- 29.09. Keine Prüfung der Anwendung hat es bemerkt, und keine konnte es: -- die Selbstprüfungen der Migrationen lesen den **Text** der Funktionen, die -- Unit-Tests laufen ohne Datenbank, und die Integrationstests haben keinen -- Bestand, gegen den sie liefen. -- -- Diese Datei schliesst genau diese Lücke: sie **ruft** die Funktionen auf, -- gegen die echte Datenbank, in der Reihenfolge des Lebenszyklus. -- -- ═══ Zwei Eigenschaften, ohne die es gefährlich wäre ══════════════ -- -- 1. **Es wird zurückgerollt.** Alles läuft in einer Transaktion, die am Ende -- verworfen wird. Nach dem Lauf steht kein Testmensch, keine Testplanstelle -- und keine Testeinheit in der Datenbank. Der Anlass steht daneben: beim -- Test vom 29.09. sind reale Personen auf Testplanstellen umgezogen und -- dort geblieben. -- -- 2. **Es läuft als Anwendungsrolle.** psql verbindet sich als Superuser, und -- der geht an jeder Zeilenschutz-Regel und an require_hr_admin() vorbei. -- Ein Lauf als postgres würde beweisen, dass die Funktionen für niemanden -- funktionieren, der sie benutzt. -- -- Fehlschläge brechen mit einer Meldung ab, die den Schritt nennt. Kommt der -- Lauf bis „Rauchtest bestanden" durch, ist der Lebenszyklus gegangen worden. \set ON_ERROR_STOP on begin; select set_config( 'app.user_id', (select p.id::text from profiles p where p.role = 'hr' and p.is_active limit 1), true ) as angemeldet_als; set local role alpenwerk_app; do $rauchtest$ declare v_wurzel uuid; v_einheit uuid; v_p1 uuid; v_p2 uuid; v_p3 uuid; v_p4 uuid; v_a uuid; v_b uuid; v_ort uuid; v_nr int; v_status text; v_stelle uuid; v_offen int; v_meldung text; v_ging boolean; begin select id into v_wurzel from org_units where parent_id is null; select id into v_ort from locations order by name limit 1; select coalesce(max(personnel_number), 0) + 1 into v_nr from employees; if v_wurzel is null or v_ort is null then raise exception 'Kein Bestand: es fehlt die oberste Einheit oder ein Standort.'; end if; -- ── Spielwiese ────────────────────────────────────────────────── v_einheit := create_org_unit(jsonb_build_object( 'parent_id', v_wurzel, 'org_number', 'RAUCHTEST', 'name', 'Rauchtest', 'unit_type', 'Team', 'valid_from', (current_date - 30)::text)); v_p1 := create_position(jsonb_build_object('org_unit_id', v_einheit, 'job_title', 'Rauchtest Eins', 'is_chief', false, 'valid_from', (current_date - 30)::text)); v_p2 := create_position(jsonb_build_object('org_unit_id', v_einheit, 'job_title', 'Rauchtest Zwei', 'is_chief', false, 'valid_from', (current_date - 30)::text)); v_p3 := create_position(jsonb_build_object('org_unit_id', v_einheit, 'job_title', 'Rauchtest Drei', 'is_chief', false, 'valid_from', (current_date - 30)::text)); v_p4 := create_position(jsonb_build_object('org_unit_id', v_einheit, 'job_title', 'Rauchtest Vier', 'is_chief', false, 'valid_from', (current_date - 30)::text)); raise notice '1/8 Einheit und vier Planstellen angelegt'; -- ── Einstellung ───────────────────────────────────────────────── v_a := hire_employee(jsonb_build_object( 'personnel_number', v_nr, 'first_name', 'Rauch', 'last_name', 'Test-A', 'gender', 'w', 'birth_date', '1990-01-01', 'location_id', v_ort, 'position_id', v_p1, 'entry_date', current_date::text)); raise notice '2/8 Einstellung auf Planstelle 1'; -- ── Versetzung am selben Tag (20260929180000) ─────────────────── -- Die Besetzung begann heute; sie auf heute zu schliessen wäre ein -- Intervall von null Tagen. Vorher: chk_assignment_range. perform transfer_employee(jsonb_build_object( 'employee_id', v_a, 'target_position_id', v_p2, 'effective_date', current_date::text)); select position_id into v_stelle from position_assignments where employee_id = v_a and valid_to is null; if v_stelle is distinct from v_p2 then raise exception 'Nach der Versetzung sitzt die Person nicht auf Planstelle 2.'; end if; raise notice '3/8 Versetzung am selben Tag'; -- ── Beförderung mit Planstellenwechsel, ebenfalls heute ───────── perform promote_employee(jsonb_build_object( 'employee_id', v_a, 'effective_date', current_date::text, 'new_title', 'Rauchtest Leitung', 'new_paygrade', 'HG15', 'target_position_id', v_p3)); select position_id into v_stelle from position_assignments where employee_id = v_a and valid_to is null; if v_stelle is distinct from v_p3 then raise exception 'Nach der Beförderung sitzt die Person nicht auf Planstelle 3.'; end if; if (select paygrade::text from employees where id = v_a) <> 'HG15' then raise exception 'Die Beförderung hat den Hay-Grade nicht gesetzt.'; end if; raise notice '4/8 Beförderung am selben Tag, Hay-Grade gesetzt'; -- ── Vorgemerkte Planstelle (20260929200000) ───────────────────── -- Eine Versetzung in die Zukunft belegt die Zielstelle, obwohl dort noch -- keine Besetzung steht. Wer sie vorher besetzt, bringt den Nachtlauf zum -- Stehen — vorher ging genau das durch. perform transfer_employee(jsonb_build_object( 'employee_id', v_a, 'target_position_id', v_p4, 'effective_date', (current_date + 10)::text)); -- Der Fehlschlag wird **ausserhalb** des Blocks ausgewertet. Ein `raise` -- innerhalb landet im eigenen exception-Zweig — und weil seine Meldung das -- Wort „vorgemerkte" enthielt, bestand der Schritt beim ersten Lauf, obwohl -- die Migration gar nicht angewendet war. Ein Test, der sich selbst -- durchwinkt, ist schlimmer als keiner. v_ging := false; begin perform hire_employee(jsonb_build_object( 'personnel_number', v_nr + 1, 'first_name', 'Rauch', 'last_name', 'Test-B', 'gender', 'm', 'birth_date', '1990-01-01', 'location_id', v_ort, 'position_id', v_p4, 'entry_date', current_date::text)); v_ging := true; exception when others then v_meldung := SQLERRM; end; if v_ging then raise exception 'Die vorgemerkte Planstelle liess sich besetzen — die Pruefung greift nicht.'; end if; if v_meldung not like '%bereits eine Versetzung vorgemerkt%' then raise exception 'Erwartet wurde die Meldung zur Vormerkung, kam aber: %', v_meldung; end if; raise notice '5/8 Vorgemerkte Planstelle wird abgewiesen'; -- Die eigene Vormerkung darf nicht im Weg stehen: dieselbe Versetzung noch -- einmal zu erfassen ist eine Berichtigung. perform transfer_employee(jsonb_build_object( 'employee_id', v_a, 'target_position_id', v_p4, 'effective_date', (current_date + 20)::text)); raise notice '6/8 Die eigene Vormerkung steht nicht im Weg'; -- ── Austritt am selben Tag (20260929220000) ───────────────────── perform terminate_employee(jsonb_build_object( 'employee_id', v_a, 'exit_date', current_date::text, 'exit_reason', 'Einvernehmliche Auflösung', 'austrittsart', 'freiwillig')); select count(*) into v_offen from position_assignments where employee_id = v_a and valid_to is null; if v_offen <> 0 then raise exception 'Nach dem Austritt ist die Planstelle nicht frei.'; end if; raise notice '7/8 Austritt am selben Tag, Planstelle frei'; -- ── Wiedereintritt (20260929160000, K.01) ─────────────────────── perform rehire_employee(jsonb_build_object( 'employee_id', v_a, 'rehire_date', current_date::text, 'position_id', v_p1, 'first_name', 'Rauch', 'last_name', 'Test-A', 'gender', 'w', 'birth_date', '1990-01-01', 'nationality', 'Österreich', 'location_id', v_ort)); select status::text into v_status from employees where id = v_a; if v_status <> 'Aktiv' then raise exception 'Nach dem Wiedereintritt steht der Status auf %, erwartet Aktiv.', v_status; end if; select position_id into v_stelle from position_assignments where employee_id = v_a and valid_to is null; if v_stelle is distinct from v_p1 then raise exception 'Nach dem Wiedereintritt sitzt die Person nicht auf Planstelle 1.'; end if; raise notice '8/8 Wiedereintritt'; raise notice ''; raise notice 'Rauchtest bestanden. Alles Angelegte wird gleich verworfen.'; end $rauchtest$; rollback; -- Beweis, dass nichts geblieben ist. Läuft ausserhalb der verworfenen -- Transaktion und muss überall 0 liefern. select (select count(*) from org_units where org_number = 'RAUCHTEST') as einheiten, (select count(*) from employees where last_name like 'Test-%' and first_name = 'Rauch') as personen, (select count(*) from jobs where title like 'Rauchtest %') as taetigkeiten;