fix(absences): POST /absences/ reicht warnings jetzt an den Client durch
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
This commit is contained in:
2026-09-02 23:54:02 +02:00
co-authored by Claude Sonnet 5
parent 3cf396b972
commit ac496a5f9b
2 changed files with 46 additions and 8 deletions
+8 -2
View File
@@ -262,7 +262,11 @@ async def list_absences(
return AbsenceListResponse(total=total, items=[AbsenceOut.model_validate(a) for a in 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( async def create_absence(
data: AbsenceCreate, data: AbsenceCreate,
current_user: CurrentUser, current_user: CurrentUser,
@@ -281,7 +285,9 @@ async def create_absence(
acting_user = target acting_user = target
absence, warnings = await absence_service.create_absence(data, acting_user, db) absence, warnings = await absence_service.create_absence(data, acting_user, db)
await db.commit() 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) @router.patch("/absences/{absence_id}", response_model=AbsenceOut)
+38 -6
View File
@@ -8,6 +8,13 @@ Deckt:
+ Schwellwert-Grenzfall working_days == two_stage_min_days + Schwellwert-Grenzfall working_days == two_stage_min_days
5. DATEV-Export: Feiertag-Kürzel "F" schlägt Urlaubs-Kürzel "U"; working_days konsistent 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) 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
import pytest_asyncio 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 nicht das Signal 'Konto reicht' bekommen, obwohl entitled+carried_over rechnerisch
reichen würde der verfallene Übertrag darf effective_available nicht aufblähen. reichen würde der verfallene Übertrag darf effective_available nicht aufblähen.
Ruft absence_service.create_absence() direkt auf (statt über HTTP), weil der Ruft absence_service.create_absence() direkt auf (statt über HTTP) um den
Router POST /absences/ die von create_absence() zurückgegebenen `warnings` warnings-Rückgabewert isoliert vom HTTP-Layer zu prüfen (Test 7 unten deckt
**verwirft** (siehe app/routers/absences.py::create_absence nur approve_absence den HTTP-Pfad inkl. warnings-Durchreichung separat ab)."""
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()."""
from app.services.absence_service import absence_service from app.services.absence_service import absence_service
from app.schemas.absence import AbsenceCreate 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.holiday_150_hours == pytest.approx(8.0)
assert result.sunday_hours == pytest.approx(0.0) assert result.sunday_hours == pytest.approx(0.0)
assert result.holiday_125_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"]