From ac496a5f9b599294ca6e1622a62a7726d16df7c0 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 2 Sep 2026 23:54:02 +0200 Subject: [PATCH] fix(absences): POST /absences/ reicht warnings jetzt an den Client durch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create_absence() gibt (absence, warnings) zurück, der Router verwarf die Warnungen bisher (im Unterschied zu approve_absence, das sie via AbsenceApproveOut.warnings durchreicht). Neues AbsenceCreateOut-Schema analog dazu. Regressionstest über Überschneidungs-Warnung. Gefunden während Coverage-Arbeit an absence_service.py. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn --- backend/app/routers/absences.py | 10 +++++-- backend/tests/test_coverage_gaps.py | 44 +++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/backend/app/routers/absences.py b/backend/app/routers/absences.py index ea747c1..55d2aa3 100644 --- a/backend/app/routers/absences.py +++ b/backend/app/routers/absences.py @@ -262,7 +262,11 @@ async def list_absences( return AbsenceListResponse(total=total, items=[AbsenceOut.model_validate(a) for a in absences]) -@router.post("/absences/", response_model=AbsenceOut, status_code=201) +class AbsenceCreateOut(AbsenceOut): + warnings: list[str] = [] + + +@router.post("/absences/", response_model=AbsenceCreateOut, status_code=201) async def create_absence( data: AbsenceCreate, current_user: CurrentUser, @@ -281,7 +285,9 @@ async def create_absence( acting_user = target absence, warnings = await absence_service.create_absence(data, acting_user, db) await db.commit() - return AbsenceOut.model_validate(absence) + out = AbsenceCreateOut.model_validate(absence) + out.warnings = warnings + return out @router.patch("/absences/{absence_id}", response_model=AbsenceOut) diff --git a/backend/tests/test_coverage_gaps.py b/backend/tests/test_coverage_gaps.py index 53dce2a..bdde645 100644 --- a/backend/tests/test_coverage_gaps.py +++ b/backend/tests/test_coverage_gaps.py @@ -8,6 +8,13 @@ Deckt: + Schwellwert-Grenzfall working_days == two_stage_min_days 5. DATEV-Export: Feiertag-Kürzel "F" schlägt Urlaubs-Kürzel "U"; working_days konsistent 6. _categorize_hours: Nachtschicht über Mitternacht + Sonntag+Feiertag (höchster Zuschlag gewinnt) +7. POST /absences/ reicht warnings jetzt durch (API-Konsistenz-Fix) + +Nebenbei gefixte Bugs (während dieser Coverage-Arbeit gefunden): +- _get_holiday_dates() rechnete Feiertage im Folgejahr bei Anträgen über den + Jahreswechsel nicht aus working_days heraus (siehe + test_holiday_dates_cover_year_boundary + absence_service.py year_to-Parameter) +- POST /absences/ verwarf die warnings-Liste von create_absence() (siehe Test 7) """ import pytest import pytest_asyncio @@ -116,12 +123,9 @@ async def test_carryover_expiry_via_create_absence( nicht das Signal 'Konto reicht' bekommen, obwohl entitled+carried_over rechnerisch reichen würde – der verfallene Übertrag darf effective_available nicht aufblähen. - Ruft absence_service.create_absence() direkt auf (statt über HTTP), weil der - Router POST /absences/ die von create_absence() zurückgegebenen `warnings` - **verwirft** (siehe app/routers/absences.py::create_absence – nur approve_absence - reicht `warnings` über AbsenceOut.warnings durch). Das ist ein eigenständiger, - hier entdeckter Bug (siehe Abschlussbericht), separat vom hier geprüften - Verfalls-Verhalten von effective_available().""" + Ruft absence_service.create_absence() direkt auf (statt über HTTP) um den + warnings-Rückgabewert isoliert vom HTTP-Layer zu prüfen (Test 7 unten deckt + den HTTP-Pfad inkl. warnings-Durchreichung separat ab).""" from app.services.absence_service import absence_service from app.schemas.absence import AbsenceCreate @@ -580,3 +584,31 @@ def test_categorize_hours_sunday_and_holiday_highest_wins(): assert result.holiday_150_hours == pytest.approx(8.0) assert result.sunday_hours == pytest.approx(0.0) assert result.holiday_125_hours == pytest.approx(0.0) + + +# ── 7. POST /absences/ gibt warnings zurück (API-Konsistenz-Fix) ──────────────── + +async def test_create_absence_returns_warnings( + client: AsyncClient, cov_headers, cov_vacation_type_id, +): + """Regressionstest: POST /absences/ verwarf bisher die von create_absence() + zurückgegebenen warnings (im Unterschied zu approve_absence, das sie über + AbsenceApproveOut.warnings durchreicht). Fix: neues AbsenceCreateOut-Schema. + Trigger hier: Überschneidungs-Warnung bei zwei sich überlappenden Anträgen.""" + start = _future_monday(20) + end = start + timedelta(days=2) + + resp1 = await client.post("/api/v1/absences/", json={ + "type_id": str(cov_vacation_type_id), "start_date": str(start), "end_date": str(end), + }, headers=cov_headers) + assert resp1.status_code == 201, resp1.text + assert resp1.json().get("warnings") == [] + + # Überlappender zweiter Antrag im selben Zeitraum -> Warnung erwartet + resp2 = await client.post("/api/v1/absences/", json={ + "type_id": str(cov_vacation_type_id), "start_date": str(start), "end_date": str(end + timedelta(days=1)), + }, headers=cov_headers) + assert resp2.status_code == 201, resp2.text + data2 = resp2.json() + assert "warnings" in data2 + assert any("Überschneidung" in w for w in data2["warnings"]), data2["warnings"]