Files
MABEA/backend/tests/test_dashboard.py
T
patrickandClaude Sonnet 5 65203a039c
CI / backend-tests (push) Failing after 2m3s
CI / frontend-build (push) Successful in 19s
fix(dashboard): "nie kontrolliert" ging bei zusätzlichen Gründen verloren
_kategorie_und_gruende() zählte "noch_nie_kontrolliert" bisher nur,
wenn es der EINZIGE Grund war - ein Objekt mit z.B. offenem
Fehlbestand UND nie kontrolliert verlor den Hinweis komplett, landete
nur unter "nicht einsatzbereit" ohne diesen Grund zu nennen. Jetzt
immer als Zusatzgrund angehängt. Neues Feld nie_kontrolliert_gesamt
liefert die tatsächliche Gesamtzahl über alle Kategorien hinweg,
Dashboard nutzt sie statt der (bewusst engeren) "unbekannt"-Kategorie.

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

307 lines
13 KiB
Python

from datetime import date, timedelta
import pytest
from sqlalchemy import select
from app.models.geraet_instanz import GeraetInstanz
from app.models.objektposition import Objektposition
from tests.conftest import auth_header, login
async def _fehlbestand(client, token, objekt, material, istmenge: str, abschliessen=True):
start = await client.post(
f"/api/v1/objekte/{objekt.id}/kontrollen", json={"uebernehmen": False}, headers=auth_header(token)
)
kontrolle_id = start.json()["id"]
put_response = await client.put(
f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}",
json={"istmenge": istmenge},
headers=auth_header(token),
)
if abschliessen:
await client.post(f"/api/v1/kontrollen/{kontrolle_id}/abschliessen", headers=auth_header(token))
return put_response.json()["fehlbestand_id"]
@pytest.mark.asyncio
async def test_offene_stati_werden_gemeinsam_gezaehlt(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
"""Prompt 12.2: offen + in_bearbeitung + nachgefuellt_teilweise = "offen"."""
objekt, material = objekt_mit_position
mitarbeiter_token = await login(client, "mitarbeiter1")
fehlbestand_id = await _fehlbestand(client, mitarbeiter_token, objekt, material, "6")
# Teilnachfüllung -> Status nachgefuellt_teilweise, zählt trotzdem als offen
await client.post(
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
json={"menge": "1"},
headers=auth_header(mitarbeiter_token),
)
verantwortlicher_token = await login(client, "materialverantwortlicher1")
response = await client.get("/api/v1/dashboard/kennzahlen", headers=auth_header(verantwortlicher_token))
assert response.status_code == 200
assert response.json()["anzahl_offener_fehlbestaende"] == 1
@pytest.mark.asyncio
async def test_genehmigte_mindermenge_zaehlt_als_offen_und_separat(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
objekt, material = objekt_mit_position
mitarbeiter_token = await login(client, "mitarbeiter1")
fehlbestand_id = await _fehlbestand(client, mitarbeiter_token, objekt, material, "6")
verantwortlicher_token = await login(client, "materialverantwortlicher1")
await client.post(
f"/api/v1/fehlbestaende/{fehlbestand_id}/mindermenge",
json={"begruendung": "Lieferengpass"},
headers=auth_header(verantwortlicher_token),
)
response = await client.get("/api/v1/dashboard/kennzahlen", headers=auth_header(verantwortlicher_token))
body = response.json()
assert body["anzahl_genehmigter_mindermengen"] == 1
assert body["anzahl_offener_fehlbestaende"] == 1 # Genehmigung ist keine Erledigung
@pytest.mark.asyncio
async def test_kuerzlich_erledigt_wird_gezaehlt(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
objekt, material = objekt_mit_position
mitarbeiter_token = await login(client, "mitarbeiter1")
fehlbestand_id = await _fehlbestand(client, mitarbeiter_token, objekt, material, "6")
await client.post(
f"/api/v1/fehlbestaende/{fehlbestand_id}/nachfuellungen",
json={"menge": "4"},
headers=auth_header(mitarbeiter_token),
)
verantwortlicher_token = await login(client, "materialverantwortlicher1")
response = await client.get("/api/v1/dashboard/kennzahlen", headers=auth_header(verantwortlicher_token))
body = response.json()
assert body["anzahl_kuerzlich_erledigter_fehlbestaende"] == 1
assert body["anzahl_offener_fehlbestaende"] == 0
@pytest.mark.asyncio
async def test_mitarbeiter_darf_dashboard_nicht_sehen(client, mitarbeiter_user):
token = await login(client, "mitarbeiter1")
response = await client.get("/api/v1/dashboard/kennzahlen", headers=auth_header(token))
assert response.status_code == 403
@pytest.mark.asyncio
async def test_ablaufdaten_liste_sortiert_und_gefiltert(
client, db_session, objekt_mit_position, materialverantwortlicher_user
):
"""Prompt 14: nur bald_ablaufend/abgelaufen, sortiert nach verbleibenden Tagen aufsteigend."""
from datetime import date, timedelta
objekt, material = objekt_mit_position
verantwortlicher_token = await login(client, "materialverantwortlicher1")
positionen = (
await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(verantwortlicher_token))
).json()
position_id = positionen[0]["id"]
# weit in der Zukunft -> "gueltig", darf NICHT in der Liste auftauchen
await client.patch(
f"/api/v1/objekte/{objekt.id}/positionen/{position_id}",
json={"ablaufdatum": (date.today() + timedelta(days=365)).isoformat()},
headers=auth_header(verantwortlicher_token),
)
response = await client.get("/api/v1/dashboard/ablaufdaten", headers=auth_header(verantwortlicher_token))
assert response.json() == []
# bald ablaufend (Standard-Warnzeitraum 30 Tage, kein warnzeitraum_tage auf Material gesetzt)
await client.patch(
f"/api/v1/objekte/{objekt.id}/positionen/{position_id}",
json={"ablaufdatum": (date.today() + timedelta(days=5)).isoformat()},
headers=auth_header(verantwortlicher_token),
)
response = await client.get("/api/v1/dashboard/ablaufdaten", headers=auth_header(verantwortlicher_token))
body = response.json()
assert len(body) == 1
assert body[0]["status"] == "bald_ablaufend"
@pytest.mark.asyncio
async def test_abgelaufenes_material_wird_als_abgelaufen_markiert(
client, objekt_mit_position, materialverantwortlicher_user
):
from datetime import date, timedelta
objekt, material = objekt_mit_position
token = await login(client, "materialverantwortlicher1")
positionen = (await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token))).json()
position_id = positionen[0]["id"]
await client.patch(
f"/api/v1/objekte/{objekt.id}/positionen/{position_id}",
json={"ablaufdatum": (date.today() - timedelta(days=3)).isoformat()},
headers=auth_header(token),
)
response = await client.get("/api/v1/dashboard/ablaufdaten", headers=auth_header(token))
body = response.json()
assert len(body) == 1
assert body[0]["status"] == "abgelaufen"
assert body[0]["verbleibende_tage"] == -3
@pytest.mark.asyncio
async def test_objekt_ohne_maengel_und_kontrolliert_ist_einsatzbereit(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
"""Kontrolliert (Ist=Soll, kein Fehlbestand) und ohne sonstige Mängel -> bereit.
Unterscheidet sich bewusst von test_nie_kontrolliertes_objekt_ist_unbekannt."""
objekt, material = objekt_mit_position
mitarbeiter_token = await login(client, "mitarbeiter1")
await _fehlbestand(client, mitarbeiter_token, objekt, material, "10") # = Sollmenge, kein Fehlbestand
token = await login(client, "materialverantwortlicher1")
response = await client.get("/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(token))
body = response.json()
assert body["einsatzbereit"] == 1
assert body["eingeschraenkt_einsatzbereit"] == 0
assert body["unbekannt"] == 0
assert body["nicht_einsatzbereit"] == 0
assert body["details"] == []
@pytest.mark.asyncio
async def test_nie_kontrolliertes_objekt_ist_unbekannt(
client, objekt_mit_position, materialverantwortlicher_user
):
"""Nutzer-Fund: ein nie kontrolliertes Objekt darf NICHT als einsatzbereit zählen
(unterscheidet "nie geprüft" von "geprüft und in Ordnung")."""
objekt, _material = objekt_mit_position
token = await login(client, "materialverantwortlicher1")
response = await client.get("/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(token))
body = response.json()
assert body["einsatzbereit"] == 0
assert body["unbekannt"] == 1
assert body["nicht_einsatzbereit"] == 0
assert body["details"][0]["gruende"] == ["noch_nie_kontrolliert"]
assert body["nie_kontrolliert_gesamt"] == 1
@pytest.mark.asyncio
async def test_offener_fehlbestand_macht_objekt_nicht_einsatzbereit(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
objekt, material = objekt_mit_position
mitarbeiter_token = await login(client, "mitarbeiter1")
await _fehlbestand(client, mitarbeiter_token, objekt, material, "6")
verantwortlicher_token = await login(client, "materialverantwortlicher1")
response = await client.get(
"/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(verantwortlicher_token)
)
body = response.json()
assert body["einsatzbereit"] == 0
assert body["nicht_einsatzbereit"] == 1
# Bug-Fund 2026-09-06: "nie kontrolliert" darf nicht verloren gehen, nur
# weil das Objekt zusätzlich einen anderen Grund hat (objekt_mit_position
# hat nie eine abgeschlossene Kontrolle).
assert body["details"][0]["gruende"] == ["fehlbestand_offen", "noch_nie_kontrolliert"]
assert body["nie_kontrolliert_gesamt"] == 1
@pytest.mark.asyncio
async def test_genehmigte_mindermenge_macht_objekt_eingeschraenkt_einsatzbereit(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
"""U2/Karte 08: Genehmigung erledigt den Fehlbestand fachlich nicht -
eingeschränkt einsatzbereit, nicht (voll) einsatzbereit."""
objekt, material = objekt_mit_position
mitarbeiter_token = await login(client, "mitarbeiter1")
fehlbestand_id = await _fehlbestand(client, mitarbeiter_token, objekt, material, "6")
verantwortlicher_token = await login(client, "materialverantwortlicher1")
await client.post(
f"/api/v1/fehlbestaende/{fehlbestand_id}/mindermenge",
json={"begruendung": "Lieferengpass"},
headers=auth_header(verantwortlicher_token),
)
response = await client.get(
"/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(verantwortlicher_token)
)
body = response.json()
assert body["einsatzbereit"] == 0
assert body["eingeschraenkt_einsatzbereit"] == 1
assert body["nicht_einsatzbereit"] == 0
assert body["details"][0]["gruende"] == ["mindermenge_genehmigt", "noch_nie_kontrolliert"]
assert body["nie_kontrolliert_gesamt"] == 1
@pytest.mark.asyncio
async def test_ueberfaellige_pruefung_macht_objekt_nicht_einsatzbereit(
client, db_session, objekt_mit_position, materialverantwortlicher_user
):
objekt, material = objekt_mit_position
result = await db_session.execute(
select(Objektposition).where(
Objektposition.objekt_id == objekt.id, Objektposition.material_id == material.id
)
)
position = result.scalar_one()
db_session.add(
GeraetInstanz(
objektposition_id=position.id,
seriennummer="SN-DASH-1",
naechste_pruefung=date.today() - timedelta(days=1),
)
)
await db_session.flush()
token = await login(client, "materialverantwortlicher1")
response = await client.get("/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(token))
body = response.json()
assert body["nicht_einsatzbereit"] == 1
assert body["details"][0]["gruende"] == ["pruefung_ueberfaellig", "noch_nie_kontrolliert"]
assert body["nie_kontrolliert_gesamt"] == 1
prueftermine = await client.get("/api/v1/dashboard/prueftermine", headers=auth_header(token))
prueftermine_body = prueftermine.json()
assert len(prueftermine_body) == 1
assert prueftermine_body[0]["status"] == "abgelaufen"
assert prueftermine_body[0]["seriennummer"] == "SN-DASH-1"
@pytest.mark.asyncio
async def test_gueltige_pruefung_taucht_nicht_in_prueftermine_auf(
client, db_session, objekt_mit_position, materialverantwortlicher_user
):
objekt, material = objekt_mit_position
result = await db_session.execute(
select(Objektposition).where(
Objektposition.objekt_id == objekt.id, Objektposition.material_id == material.id
)
)
position = result.scalar_one()
db_session.add(
GeraetInstanz(
objektposition_id=position.id,
seriennummer="SN-DASH-2",
naechste_pruefung=date.today() + timedelta(days=365),
)
)
await db_session.flush()
token = await login(client, "materialverantwortlicher1")
response = await client.get("/api/v1/dashboard/prueftermine", headers=auth_header(token))
assert response.json() == []
@pytest.mark.asyncio
async def test_mitarbeiter_darf_neue_dashboard_endpunkte_nicht_sehen(client, mitarbeiter_user):
token = await login(client, "mitarbeiter1")
for pfad in ("/api/v1/dashboard/einsatzbereitschaft", "/api/v1/dashboard/prueftermine"):
response = await client.get(pfad, headers=auth_header(token))
assert response.status_code == 403