Rauchtest: der Lebenszyklus, einmal wirklich ausgefuehrt -- und die fuenfte Fundstelle
Some checks failed
CI / Migrationen auf leerer Datenbank (push) Has been cancelled
CI / Lint, Typen, Tests, Build (push) Has been cancelled

rehire_employee war seit dem 17.09. bei jedem Aufruf kaputt, und keine Pruefung
konnte das bemerken: die Selbstpruefungen der Migrationen lesen den Text der
Funktionen, die Unit-Tests laufen ohne Datenbank, und die Integrationstests
haben keinen Bestand, gegen den sie liefen. db/tests/rauchtest.sql schliesst
die Luecke -- es ruft die Funktionen auf, gegen die echte Datenbank, in der
Reihenfolge des Lebenszyklus: anlegen, versetzen, befoerdern, vormerken,
austreten, wiedereintreten.

Zwei Eigenschaften, ohne die es gefaehrlich waere. Es laeuft als
Anwendungsrolle statt als Superuser -- sonst bewiese es nur, dass die
Funktionen fuer niemanden gehen, der sie benutzt. Und es rollt am Ende zurueck,
mit einer Zaehlung danach, die das belegt: beim Test vom 29.09. sind reale
Personen auf Testplanstellen umgezogen und dort geblieben.

Beim Schreiben fiel die fuenfte Stelle mit dem Paar aus Schliessen und
Einfuegen auf: terminate_employee. Wer heute eingestellt und heute wieder
ausgetragen wird -- ohne den Grund "No Show" --, lief in chk_assignment_range.
Die No-Show-Haelfte derselben Funktion macht es laengst richtig und erklaert
auch, warum; es galt nur fuer genau einen Austrittsgrund.
This commit is contained in:
2026-09-29 19:17:33 +02:00
parent 89ba34c901
commit 194b6d8916
2 changed files with 261 additions and 0 deletions

185
db/tests/rauchtest.sql Normal file
View File

@@ -0,0 +1,185 @@
-- 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;
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));
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));
raise exception 'Die vorgemerkte Planstelle liess sich besetzen.';
exception
when others then
v_meldung := SQLERRM;
if v_meldung not like '%vorgemerkt%' then
raise exception 'Erwartet wurde die Meldung zur Vormerkung, kam aber: %', v_meldung;
end if;
end;
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;