Files
alpenwerk-hr/db/tests/rauchtest.sql
Andrei Laas a0b9f6dc80
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m33s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m15s
Anker aus der laufenden Definition, und ein Test, der sich selbst durchwinkt
Zwei Fehler aus dem ersten Lauf des Rauchtests, beide von mir.

Der Anker fuer transfer_employee stammte aus 20260727120200 und fand nichts:
die Funktion ist seither dynamisch gepatcht worden, die Belegungsabfrage ist
zweizeilig und beruecksichtigt den Stichtag. Dieselbe Falle wie immer, nur
diesmal nicht im Rumpf einer Funktion, sondern im Anker auf sie -- fuer
hire_employee und rehire_employee stimmten die Anker, weil sie aus der zuletzt
erzeugten Fassung kamen. Die Migration war nie angewendet (sie bricht als
Ganzes ab), deshalb die Datei korrigiert statt eine neue geschrieben.

Und der Rauchtest hat sich an einer Stelle selbst durchgewunken: das `raise`
fuer den Fehlschlag stand innerhalb des Blocks mit exception-Zweig, wurde also
vom eigenen Handler gefangen -- und weil seine Meldung das Wort "vorgemerkte"
enthielt, bestand die Pruefung auf die erwartete Fehlermeldung. Der Schritt
galt als bestanden, obwohl die Migration gar nicht angewendet war. Jetzt merkt
sich der Block nur, ob es ging, und ausgewertet wird danach.
2026-09-29 19:24:58 +02:00

196 lines
9.2 KiB
PL/PgSQL

-- 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;