fix(tests): year-boundary + response-shape issues in coverage gap tests
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's warnings are discarded by the router (only approve_absence
passes them through AbsenceOut.warnings) — test now calls the service
directly and documents this as a separate finding. DATEV test kept within
a single year since absence_service._get_holiday_dates only looks up
holidays for start_date.year (cross-year ranges miss the boundary holiday).

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:30:35 +02:00
co-authored by Claude Sonnet 5
parent cbb5f467fe
commit dc4982bb37
+41 -25
View File
@@ -114,9 +114,20 @@ async def test_carryover_expiry_via_create_absence(
):
"""Ein User mit abgelaufenem carried_over-Betrag darf im create_absence-Pfad
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
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()."""
from app.services.absence_service import absence_service
from app.schemas.absence import AbsenceCreate
user_id = cov_company["user"]["id"]
year = date.today().year
user = await db_session.get(User, user_id)
# Firma: Verfallsdatum in der Vergangenheit setzen (z.B. 31.01. dieses Jahr)
r = await client.patch("/api/v1/companies/me", json={
@@ -138,24 +149,24 @@ async def test_carryover_expiry_via_create_absence(
balance.carried_over = 20
balance.used_days = 0
balance.special_days = 0
await db_session.commit()
await db_session.flush()
# Antrag über 10 Arbeitstage (mehr als die 5 "echten" Tage, aber weniger als
# der rechnerische Gesamtsaldo 25 -- Verfall muss dennoch greifen und warnen)
start = _future_monday(23)
resp = await client.post("/api/v1/absences/", json={
"type_id": str(cov_vacation_type_id),
"start_date": str(start), "end_date": str(start + timedelta(days=13)), # 2 volle Wochen = 10 Werktage
}, headers=cov_headers)
assert resp.status_code == 201, resp.text
data = resp.json()
assert data["working_days"] == 10
# Antrag über 10 Arbeitstage im selben Jahr wie die präparierte Bilanz (nah
# in der Zukunft, damit wir nicht ins nächste Jahr rutschen)
start = _future_monday(1)
assert start.year == year, "Testannahme verletzt: Zeitraum muss im selben Jahr wie die Bilanz liegen"
create_data = AbsenceCreate(
type_id=cov_vacation_type_id,
start_date=start, end_date=start + timedelta(days=13), # 2 volle Wochen = 10 Werktage
)
absence, warnings = await absence_service.create_absence(create_data, user, db_session)
assert absence.working_days == 10
warnings = data.get("warnings", [])
assert any("Urlaubskonto reicht" in w for w in warnings), (
f"Erwartete Verfalls-Warnung fehlt (Bug: effective_available berücksichtigt Verfall nicht "
f"im create_absence-Pfad?), warnings={warnings}"
)
await db_session.commit()
# Reset für nachfolgende Tests
await client.patch("/api/v1/companies/me", json={"settings": {}}, headers=cov_headers)
@@ -436,22 +447,28 @@ async def test_datev_holiday_takes_precedence_over_vacation(
r2 = await client.patch(f"/api/v1/users/{user_id}", json={"personnel_number": "4711"}, headers=cov_headers)
assert r2.status_code == 200, r2.text
# Feiertage für 2026/2027 vorab befüllen, damit absence_service._get_holiday_dates
# Feiertage für 2027 vorab befüllen, damit absence_service._get_holiday_dates
# (fragt PublicHoliday direkt ab, ohne Auto-Generierung) sie kennt.
# NB: _get_holiday_dates(company_id, data.start_date.year, db) holt Feiertage
# NUR für das Jahr von start_date -- ein Zeitraum über den Jahreswechsel (z.B.
# 28.12.-01.01.) würde den Neujahrs-Feiertag daher NICHT rausrechnen (eigener,
# hier entdeckter Bug, siehe Abschlussbericht). Um genau den hier zu testenden
# Feiertag-vs-Urlaub-Vorrang isoliert zu prüfen, bleibt der Zeitraum bewusst
# innerhalb eines einzigen Jahres (Januar 2027).
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
await ensure_holidays_for_year(2026, "BY", db_session)
await ensure_holidays_for_year(2027, "BY", db_session)
await db_session.commit()
# Mo 28.12.2026 - Fr 01.01.2027: 5 Wochentage, davon Neujahr (Fr 01.01.) Feiertag
start = date(2026, 12, 28)
end = date(2027, 1, 1)
# Fr 01.01.2027 (Neujahr, Feiertag) - Di 05.01.2027: 3 Wochentage (Fr/Mo/Di),
# davon Neujahr ein Feiertag -> 2 Werktage
start = date(2027, 1, 1)
end = date(2027, 1, 5)
resp = 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 resp.status_code == 201, resp.text
data = resp.json()
assert data["working_days"] == 4, "Feiertag (Neujahr) sollte working_days rausrechnen"
assert data["working_days"] == 2, "Feiertag (Neujahr) sollte working_days rausrechnen"
# Selbst genehmigen geht nicht -> zweiter Admin holen wäre Overkill hier;
# DATEV-Report berücksichtigt PENDING nicht (nur APPROVED/FIRST_APPROVED),
@@ -473,12 +490,11 @@ async def test_datev_holiday_takes_precedence_over_vacation(
assert jan1_row.code == "F", f"Feiertag muss Vorrang vor Urlaubs-Kürzel haben, got code={jan1_row.code!r}"
assert "Neujahr" in (jan1_row.note or "")
# working_days-Konsistenz: Feiertag zählt nicht als Urlaubstag -> im Dezember-Sheet
# muss der 28.-31.12. (4 Werktage) als 'U' erscheinen, nicht Jan 1.
sheet_dec = await report_service.datev_monthly_report(company_id, user_id, 2026, 12, db_session)
dec_codes = {row.day: row.code for row in sheet_dec.rows if row.day >= 28}
assert dec_codes.get(28) == "U"
assert dec_codes.get(31) == "U"
# working_days-Konsistenz: Feiertag zählt nicht als Urlaubstag -> Mo/Di (4./5.1.)
# müssen als 'U' erscheinen (die tatsächlich abgezogenen Werktage).
day_codes = {row.day: row.code for row in sheet.rows}
assert day_codes.get(4) == "U"
assert day_codes.get(5) == "U"
await client.patch("/api/v1/companies/me", json={"state": None}, headers=cov_headers)