Files
MABEA/backend/tests/test_personal.py
T
patrickandClaude Sonnet 5 7dc6446a56
CI / backend-tests (push) Successful in 2m29s
CI / frontend-build (push) Successful in 28s
feat(personal): PERS-001/002 Person fachlich von Benutzer getrennt
Neue Tabelle person (Migration 0034, Backfill bestehender Benutzer 1:1
per Zeilennummer, keine Datenverluste). Benutzer.person_id optional -
Login kann bestehende Person verknüpfen oder legt automatisch eine neue
an (Rückwärtskompatibilität zum bisherigen Anlegen-Flow). /personen-CRUD
(admin-only), neue PersonSection.tsx im Personal-Tab, BenutzerSection
um Personen-Auswahl erweitert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-08 22:31:46 +02:00

365 lines
13 KiB
Python

from datetime import date, timedelta
import pytest
from tests.conftest import auth_header, login
@pytest.mark.asyncio
async def test_einheit_anlegen_und_hierarchie(client, admin_user):
token = await login(client, "admin1")
zug = await client.post("/api/v1/einheiten", json={"name": "Sanitätszug 1"}, headers=auth_header(token))
assert zug.status_code == 201
zug_id = zug.json()["id"]
gruppe = await client.post(
"/api/v1/einheiten",
json={"name": "Gruppe 1", "uebergeordnete_einheit_id": zug_id},
headers=auth_header(token),
)
assert gruppe.status_code == 201
assert gruppe.json()["uebergeordnete_einheit_id"] == zug_id
@pytest.mark.asyncio
async def test_mitarbeiter_darf_einheit_nicht_anlegen(client, mitarbeiter_user):
token = await login(client, "mitarbeiter1")
response = await client.post("/api/v1/einheiten", json={"name": "X"}, headers=auth_header(token))
assert response.status_code == 403
@pytest.mark.asyncio
async def test_qualifikationstyp_anlegen(client, admin_user):
token = await login(client, "admin1")
response = await client.post(
"/api/v1/qualifikationstypen",
json={"name": "Führerschein C1", "kategorie": "fuehrerschein"},
headers=auth_header(token),
)
assert response.status_code == 201
assert response.json()["gueltigkeit_monate"] is None
@pytest.mark.asyncio
async def test_wer_darf_fahrzeug_fahren(
client, db_session, objekttyp_factory, standort_factory, admin_user, mitarbeiter_user, hauptserver_id
):
"""Nutzer-Beispiel Personal-Modul: Objekttyp verlangt Führerschein C1,
Benutzer ohne gültigen Nachweis darf nicht, mit gültigem Nachweis schon."""
from app.services.vorlagen import erstelle_vorlage
from app.services.objekte import objekt_aus_vorlage
objekttyp = await objekttyp_factory("GW-San", ist_zugfahrzeug=True)
standort = await standort_factory("Wache-Personal")
vorlage = await erstelle_vorlage(db_session, objekttyp_id=objekttyp.id, name="GW-San Standard", positionen=[])
fahrzeug = await objekt_aus_vorlage(
db_session,
code="GW-SAN-01",
name="GW-San 01",
standort_id=standort.id,
vorlage=vorlage,
zustaendiger_server_id=hauptserver_id,
)
token = await login(client, "admin1")
quali = await client.post(
"/api/v1/qualifikationstypen",
json={"name": "Führerschein C1", "kategorie": "fuehrerschein"},
headers=auth_header(token),
)
quali_id = quali.json()["id"]
await client.post(
f"/api/v1/objekttypen/{objekttyp.id}/qualifikationsanforderungen",
json={"objekttyp_id": objekttyp.id, "qualifikationstyp_id": quali_id},
headers=auth_header(token),
)
mitarbeiter_id = mitarbeiter_user.id
# ohne Nachweis: nicht berechtigt
pruefung = await client.get(
f"/api/v1/objekte/{fahrzeug.id}/berechtigung/{mitarbeiter_id}", headers=auth_header(token)
)
assert pruefung.json()["berechtigt"] is False
assert len(pruefung.json()["fehlende_qualifikationstypen"]) == 1
# abgelaufener Nachweis zählt nicht
await client.post(
f"/api/v1/benutzer/{mitarbeiter_id}/qualifikationen",
json={
"benutzer_id": mitarbeiter_id,
"qualifikationstyp_id": quali_id,
"erworben_am": (date.today() - timedelta(days=1000)).isoformat(),
"gueltig_bis": (date.today() - timedelta(days=1)).isoformat(),
},
headers=auth_header(token),
)
abgelaufen = await client.get(
f"/api/v1/objekte/{fahrzeug.id}/berechtigung/{mitarbeiter_id}", headers=auth_header(token)
)
assert abgelaufen.json()["berechtigt"] is False
# gültiger Nachweis: berechtigt
await client.post(
f"/api/v1/benutzer/{mitarbeiter_id}/qualifikationen",
json={
"benutzer_id": mitarbeiter_id,
"qualifikationstyp_id": quali_id,
"erworben_am": date.today().isoformat(),
"gueltig_bis": None,
},
headers=auth_header(token),
)
berechtigt = await client.get(
f"/api/v1/objekte/{fahrzeug.id}/berechtigung/{mitarbeiter_id}", headers=auth_header(token)
)
assert berechtigt.json()["berechtigt"] is True
assert berechtigt.json()["fehlende_qualifikationstypen"] == []
@pytest.mark.asyncio
async def test_benutzer_einheit_zuordnung(client, admin_user):
token = await login(client, "admin1")
einheit = await client.post("/api/v1/einheiten", json={"name": "Zug 2"}, headers=auth_header(token))
einheit_id = einheit.json()["id"]
benutzer = await client.post(
"/api/v1/benutzer",
json={"name": "Neuer Helfer", "login": "helfer1", "passwort": "test-passwort-123", "einheit_id": einheit_id},
headers=auth_header(token),
)
assert benutzer.status_code == 201
assert benutzer.json()["einheit_id"] == einheit_id
benutzer_id = benutzer.json()["id"]
geloest = await client.patch(
f"/api/v1/benutzer/{benutzer_id}", json={"einheit_id": None}, headers=auth_header(token)
)
assert geloest.json()["einheit_id"] is None
@pytest.mark.asyncio
async def test_einsatzfunktion_anlegen_und_benutzer_zuordnen(client, admin_user):
"""PERS-004: Einsatzfunktion unabhängig von der technischen App-Rolle."""
token = await login(client, "admin1")
funktion = await client.post(
"/api/v1/einsatzfunktionen", json={"name": "Gruppenführer"}, headers=auth_header(token)
)
assert funktion.status_code == 201
funktion_id = funktion.json()["id"]
duplikat = await client.post(
"/api/v1/einsatzfunktionen", json={"name": "Gruppenführer"}, headers=auth_header(token)
)
assert duplikat.status_code == 409
benutzer = await client.post(
"/api/v1/benutzer",
json={
"name": "Neuer Gruppenführer",
"login": "gf1",
"passwort": "test-passwort-123",
"einsatzfunktion_id": funktion_id,
},
headers=auth_header(token),
)
assert benutzer.status_code == 201
assert benutzer.json()["einsatzfunktion_id"] == funktion_id
benutzer_id = benutzer.json()["id"]
geloest = await client.patch(
f"/api/v1/benutzer/{benutzer_id}", json={"einsatzfunktion_id": None}, headers=auth_header(token)
)
assert geloest.json()["einsatzfunktion_id"] is None
@pytest.mark.asyncio
async def test_mitarbeiter_darf_einsatzfunktion_nicht_anlegen(client, mitarbeiter_user):
token = await login(client, "mitarbeiter1")
response = await client.post(
"/api/v1/einsatzfunktionen", json={"name": "Melder"}, headers=auth_header(token)
)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_bald_ablaufende_qualifikation_erscheint_im_dashboard(
client, admin_user, mitarbeiter_user
):
from datetime import date, timedelta
token = await login(client, "admin1")
quali = await client.post(
"/api/v1/qualifikationstypen",
json={"name": "Führerschein C1", "kategorie": "fuehrerschein"},
headers=auth_header(token),
)
quali_id = quali.json()["id"]
await client.post(
f"/api/v1/benutzer/{mitarbeiter_user.id}/qualifikationen",
json={
"benutzer_id": mitarbeiter_user.id,
"qualifikationstyp_id": quali_id,
"erworben_am": (date.today() - timedelta(days=365)).isoformat(),
"gueltig_bis": (date.today() + timedelta(days=10)).isoformat(),
},
headers=auth_header(token),
)
response = await client.get("/api/v1/dashboard/qualifikationsablaeufe", headers=auth_header(token))
body = response.json()
assert len(body) == 1
assert body[0]["status"] == "bald_ablaufend"
assert body[0]["benutzer_id"] == mitarbeiter_user.id
@pytest.mark.asyncio
async def test_unbefristete_qualifikation_erscheint_nicht_im_dashboard(client, admin_user, mitarbeiter_user):
from datetime import date, timedelta
token = await login(client, "admin1")
quali = await client.post(
"/api/v1/qualifikationstypen",
json={"name": "Sanitätshelfer", "kategorie": "lehrgang"},
headers=auth_header(token),
)
quali_id = quali.json()["id"]
await client.post(
f"/api/v1/benutzer/{mitarbeiter_user.id}/qualifikationen",
json={
"benutzer_id": mitarbeiter_user.id,
"qualifikationstyp_id": quali_id,
"erworben_am": (date.today() - timedelta(days=365)).isoformat(),
"gueltig_bis": None,
},
headers=auth_header(token),
)
response = await client.get("/api/v1/dashboard/qualifikationsablaeufe", headers=auth_header(token))
assert response.json() == []
@pytest.mark.asyncio
async def test_person_ohne_benutzer_anlegbar(client, admin_user):
"""PERS-001: Person kann ohne jeden Systemzugang existieren (z.B. Jugendgruppe)."""
token = await login(client, "admin1")
person = await client.post(
"/api/v1/personen",
json={"name": "Max Mustermann (Jugendgruppe)", "geburtsdatum": "2012-04-01"},
headers=auth_header(token),
)
assert person.status_code == 201
body = person.json()
assert body["name"] == "Max Mustermann (Jugendgruppe)"
assert body["geburtsdatum"] == "2012-04-01"
liste = await client.get("/api/v1/personen", headers=auth_header(token))
assert any(p["id"] == body["id"] for p in liste.json())
geaendert = await client.patch(
f"/api/v1/personen/{body['id']}", json={"kontakt": "Erziehungsberechtigter: 0170..."}, headers=auth_header(token)
)
assert geaendert.json()["kontakt"] == "Erziehungsberechtigter: 0170..."
@pytest.mark.asyncio
async def test_mitarbeiter_darf_person_nicht_anlegen(client, mitarbeiter_user):
token = await login(client, "mitarbeiter1")
response = await client.post("/api/v1/personen", json={"name": "Jemand"}, headers=auth_header(token))
assert response.status_code == 403
@pytest.mark.asyncio
async def test_benutzer_bekommt_automatisch_verknuepfte_person(client, admin_user):
"""PERS-002: bestehender Flow (Benutzer ohne explizite person_id anlegen)
bleibt unverändert - Person wird automatisch mitangelegt."""
token = await login(client, "admin1")
benutzer = await client.post(
"/api/v1/benutzer",
json={"name": "Auto-Person Helfer", "login": "autoperson1", "passwort": "test-passwort-123"},
headers=auth_header(token),
)
assert benutzer.status_code == 201
person_id = benutzer.json()["person_id"]
assert person_id is not None
personen = await client.get("/api/v1/personen", headers=auth_header(token))
assert any(p["id"] == person_id and p["name"] == "Auto-Person Helfer" for p in personen.json())
@pytest.mark.asyncio
async def test_benutzer_mit_bestehender_person_verknuepfen(client, admin_user):
"""PERS-002: bereits erfasste Person (z.B. aus der Jugendgruppe) bekommt
nachträglich einen Login, ohne dass eine zweite Person entsteht."""
token = await login(client, "admin1")
person = await client.post("/api/v1/personen", json={"name": "Bestehende Person"}, headers=auth_header(token))
person_id = person.json()["id"]
benutzer = await client.post(
"/api/v1/benutzer",
json={
"name": "Bestehende Person",
"login": "bestehendeperson1",
"passwort": "test-passwort-123",
"person_id": person_id,
},
headers=auth_header(token),
)
assert benutzer.status_code == 201
assert benutzer.json()["person_id"] == person_id
personen = await client.get("/api/v1/personen", headers=auth_header(token))
assert len([p for p in personen.json() if p["id"] == person_id]) == 1
@pytest.mark.asyncio
async def test_benutzer_mit_unbekannter_person_id_abgelehnt(client, admin_user):
token = await login(client, "admin1")
response = await client.post(
"/api/v1/benutzer",
json={
"name": "X",
"login": "unbekanntperson1",
"passwort": "test-passwort-123",
"person_id": 999999,
},
headers=auth_header(token),
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_qualifikation_erfassen_steht_in_historie(client, admin_user, mitarbeiter_user):
from datetime import date
token = await login(client, "admin1")
quali = await client.post(
"/api/v1/qualifikationstypen",
json={"name": "Maschinist", "kategorie": "lehrgang"},
headers=auth_header(token),
)
quali_id = quali.json()["id"]
erfasst = await client.post(
f"/api/v1/benutzer/{mitarbeiter_user.id}/qualifikationen",
json={
"benutzer_id": mitarbeiter_user.id,
"qualifikationstyp_id": quali_id,
"erworben_am": date.today().isoformat(),
},
headers=auth_header(token),
)
qualifikation_id = erfasst.json()["id"]
historie = await client.get(
f"/api/v1/historie?entitaet_typ=benutzer_qualifikation&entitaet_id={qualifikation_id}",
headers=auth_header(token),
)
body = historie.json()
assert len(body) == 1
assert body[0]["ereignistyp"] == "qualifikation_erfasst"