Bisher schrieb promote_employee den neuen Titel nach employees.job_title -- und nur dorthin. Gesehen hat ihn niemand: Akte, Organigramm und die Exporte zeigen alle die Taetigkeit der Planstelle. Haltbar war er auch nicht, denn hire_employee, transfer_employee und change_employee_data setzen dieselbe Spalte aus der Planstelle; die naechste Adressaenderung ueberschrieb ihn wieder. Gemeldet von Lara und im Testprotokoll als H.10. Max hat entschieden: die Befoerderung benennt die Planstelle um. Umbenannt wird die Stelle, auf der die Person danach sitzt -- die Zielstelle, sonst die bisherige. Im Formular ist das ohne Ueberraschung, weil es beim Waehlen einer Zielplanstelle deren Taetigkeit als Vorschlag eintraegt: wer nichts aendert, benennt auch nichts um. Und umbenannt wird nicht der Katalogeintrag, sondern die Planstelle zeigt auf einen anderen -- den Eintrag selbst umzubenennen traefe jede Planstelle mit derselben Taetigkeit. Die Regel "gleiche Taetigkeit, ein Katalogeintrag" stand nur in create_position und wandert nach job_fuer_titel(); zwei Fassungen derselben Regel laufen hier erfahrungsgemaess auseinander. Die Nummernvergabe nimmt dabei die erste freie statt count(*) + 1 -- gezaehlt wurde bisher, und das vergibt eine belegte Nummer, sobald ein Eintrag geloescht wurde oder Codes aus einer fremden Quelle danebenstehen, wie bei der Uebernahme der Manner-Daten. Der Nachtlauf tut dasselbe, sonst benennt eine datierte Befoerderung am Stichtag nichts um. Der Rauchtest prueft beide Wege und dass der alte Katalogeintrag stehen bleibt.
224 lines
11 KiB
PL/PgSQL
224 lines
11 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;
|
|
v_titel 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;
|
|
|
|
-- Die Bezeichnung gehört zur Planstelle: dort lesen sie Akte, Organigramm
|
|
-- und die Exporte. Vorher stand der neue Titel allein an der Person und war
|
|
-- nirgends zu sehen.
|
|
select j.title into v_titel
|
|
from om_positions p join jobs j on j.id = p.job_id where p.id = v_p3;
|
|
if v_titel <> 'Rauchtest Leitung' then
|
|
raise exception 'Die Zielplanstelle heisst nach der Beförderung „%", erwartet „Rauchtest Leitung".', v_titel;
|
|
end if;
|
|
raise notice '4/8 Beförderung am selben Tag: Hay-Grade und Planstellenbezeichnung';
|
|
|
|
-- Dieselbe Planstelle, nur ein neuer Name — der Fall aus dem Testtag.
|
|
perform promote_employee(jsonb_build_object(
|
|
'employee_id', v_a, 'effective_date', current_date::text,
|
|
'new_title', 'Rauchtest Leitung Senior', 'new_paygrade', 'HG16'));
|
|
|
|
select j.title into v_titel
|
|
from om_positions p join jobs j on j.id = p.job_id where p.id = v_p3;
|
|
if v_titel <> 'Rauchtest Leitung Senior' then
|
|
raise exception 'Ohne Stellenwechsel heisst die Planstelle „%", erwartet „Rauchtest Leitung Senior".', v_titel;
|
|
end if;
|
|
|
|
-- Der Katalogeintrag der alten Bezeichnung bleibt stehen: an ihm können
|
|
-- andere Planstellen hängen. Umbenannt wird die Stelle, nicht der Katalog.
|
|
if not exists (select 1 from jobs where title = 'Rauchtest Leitung') then
|
|
raise exception 'Der alte Katalogeintrag wurde umbenannt statt die Planstelle umgehängt.';
|
|
end if;
|
|
raise notice '4b/8 Beförderung ohne Stellenwechsel benennt dieselbe Planstelle um';
|
|
|
|
-- ── 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;
|