From fd752878b0da0dcbd211c38bad86c1e4f102f266 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 3 Sep 2026 02:10:19 +0200 Subject: [PATCH] =?UTF-8?q?test:=20P1-Coverage=20f=C3=BCr=20Scheduler-Idem?= =?UTF-8?q?potenz,=20Bradford-Fensterrand,=20FZA-Faktor+Kappung,=20Cross-C?= =?UTF-8?q?ompany-RLS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn --- DEVLOG.md | 549 ++++++++++++++++++ ...eichnungen Muster blanko DATEV ab 2015.pdf | Bin 0 -> 51393 bytes backend/tests/test_coverage_gaps.py | 350 +++++++++++ 3 files changed, 899 insertions(+) create mode 100644 Stundenaufzeichnungen Muster blanko DATEV ab 2015.pdf diff --git a/DEVLOG.md b/DEVLOG.md index 214e348..a4c38ac 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -3349,3 +3349,552 @@ Keine Commits in dieser Session. - frontend/src/pages/KioskStampPage.tsx | 279 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- --- +## 2026-09-02 15:07 – 15:08 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- b8073fa docs(devlog): Sessions vom 2026-08-05 bis 2026-09-02 nachtragen + +### Geänderte Dateien +- DEVLOG.md | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 15:09 – 15:09 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 15:09 – 15:10 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 15:11 – 15:11 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 22:20 – 22:20 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 22:21 – 22:23 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 22:27 – 22:28 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- 52ecd9e chore(routers): toten Router absence.py entfernen + +### Geänderte Dateien +- backend/app/routers/absence.py | 159 --------------------------------------------------------------------------------------- + +--- +## 2026-09-02 22:28 – 22:29 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- c733ddf fix(redis): gepoolten async-Redis-Client statt Connect/Close pro Request + +### Geänderte Dateien +- backend/app/core/kiosk_security.py | 5 ++--- +- backend/app/core/redis.py | 30 ++++++++++++++++++++++++++++-- +- backend/app/main.py | 2 ++ +- backend/app/routers/auth.py | 32 ++++++++++++++------------------ +- frontend/src/api/client.ts | 6 ++++++ + +--- +## 2026-09-02 22:32 – 22:37 (5m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/kiosk_security.py | 5 ++--- +- backend/app/core/redis.py | 30 ++++++++++++++++++++++++++++-- +- backend/app/main.py | 2 ++ +- backend/app/routers/auth.py | 32 ++++++++++++++------------------ +- frontend/src/api/client.ts | 6 ++++++ + +--- +## 2026-09-02 22:38 – 22:38 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/kiosk_security.py | 5 ++--- +- backend/app/core/redis.py | 30 ++++++++++++++++++++++++++++-- +- backend/app/main.py | 2 ++ +- backend/app/routers/auth.py | 32 ++++++++++++++------------------ +- frontend/src/api/client.ts | 6 ++++++ + +--- +## 2026-09-02 22:38 – 22:41 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/kiosk_security.py | 5 ++--- +- backend/app/core/redis.py | 30 ++++++++++++++++++++++++++++-- +- backend/app/main.py | 2 ++ +- backend/app/routers/auth.py | 32 ++++++++++++++------------------ +- frontend/src/api/client.ts | 6 ++++++ + +--- +## 2026-09-02 22:41 – 22:41 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/kiosk_security.py | 5 ++--- +- backend/app/core/redis.py | 30 ++++++++++++++++++++++++++++-- +- backend/app/main.py | 2 ++ +- backend/app/routers/auth.py | 32 ++++++++++++++------------------ +- frontend/src/api/client.ts | 6 ++++++ + +--- +## 2026-09-02 22:42 – 22:42 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/kiosk_security.py | 5 ++--- +- backend/app/core/redis.py | 30 ++++++++++++++++++++++++++++-- +- backend/app/main.py | 2 ++ +- backend/app/routers/auth.py | 32 ++++++++++++++------------------ +- frontend/src/api/client.ts | 6 ++++++ + +--- +## 2026-09-02 22:42 – 22:46 (3m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/kiosk_security.py | 5 ++--- +- backend/app/core/redis.py | 30 ++++++++++++++++++++++++++++-- +- backend/app/main.py | 2 ++ +- backend/app/routers/auth.py | 32 ++++++++++++++------------------ +- frontend/src/api/client.ts | 6 ++++++ + +--- +## 2026-09-02 22:46 – 22:47 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- ad0bc62 ci(frontend): Build-Job als Gate im Security-Workflow ergänzen + +### Geänderte Dateien +- .gitea/workflows/security.yml | 23 +++++++++++++++++++++++ + +--- +## 2026-09-02 22:47 – 22:49 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- 538cbb0 test(coverage): pytest-cov einbauen (term-missing Report) + +### Geänderte Dateien +- .gitignore | 3 +++ +- backend/pytest.ini | 2 +- + +--- +## 2026-09-02 22:53 – 22:53 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitignore | 3 +++ +- backend/pytest.ini | 2 +- + +--- +## 2026-09-02 22:54 – 22:54 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitignore | 3 +++ +- backend/pytest.ini | 2 +- + +--- +## 2026-09-02 23:03 – 23:03 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitignore | 3 +++ +- backend/pytest.ini | 2 +- + +--- +## 2026-09-02 23:03 – 23:04 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitignore | 3 +++ +- backend/pytest.ini | 2 +- + +--- +## 2026-09-02 23:05 – 23:05 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- c1572c3 docs(api): 7 fehlende Router nachgezogen + +### Geänderte Dateien +- docs/api.md | 427 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 23:07 – 23:08 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- docs/api.md | 427 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 23:09 – 23:09 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- docs/api.md | 427 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-02 23:09 – 23:13 (3m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- aba0b53 fix(docs): StaticFiles-Mount-Pfad korrigieren (static statt app/static) +- 4e30c55 fix(docs): Swagger-Assets nach backend/static/ verschoben (nginx-Alias) +- 9f9dfb5 fix(docs): Swagger/ReDoc-Assets lokal statt von cdn.jsdelivr.net + +### Geänderte Dateien +- backend/app/main.py | 2 +- + +--- +## 2026-09-02 23:15 – 23:17 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/main.py | 2 +- + +--- +## 2026-09-02 23:17 – 23:17 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- b7bec79 fix(setup): nginx-Template im Installer auf Prod-Stand bringen + +### Geänderte Dateien +- setup_server.sh | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-02 23:17 – 23:17 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- setup_server.sh | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-02 23:19 – 23:19 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- setup_server.sh | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-02 23:22 – 23:22 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- setup_server.sh | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-02 23:24 – 23:24 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- setup_server.sh | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-02 23:25 – 23:25 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- setup_server.sh | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- + +--- +## 2026-09-02 23:35 – 23:35 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/tests/test_coverage_gaps.py | 5 +++-- + +--- +## 2026-09-02 23:49 – 23:55 (5m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- ac496a5 fix(absences): POST /absences/ reicht warnings jetzt an den Client durch +- 3cf396b fix(tests): Testdaten für Jahreswechsel-Regressionstest korrigieren +- 548738c fix(absences): Feiertage über Jahreswechsel korrekt aus working_days rausrechnen + +### Geänderte Dateien +- backend/app/routers/absences.py | 10 ++++++++-- +- backend/tests/test_coverage_gaps.py | 44 ++++++++++++++++++++++++++++++++++++++------ + +--- +## 2026-09-02 23:55 – 23:55 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/routers/absences.py | 10 ++++++++-- +- backend/tests/test_coverage_gaps.py | 44 ++++++++++++++++++++++++++++++++++++++------ + +--- +## 2026-09-02 23:56 – 00:01 (5m) +**Beschreibung:** Claude Code Session +**Projekt:** nexarch + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/routers/absences.py | 10 ++++++++-- +- backend/tests/test_coverage_gaps.py | 44 ++++++++++++++++++++++++++++++++++++++------ + +--- +## 2026-09-03 00:02 – 00:02 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/routers/absences.py | 10 ++++++++-- +- backend/tests/test_coverage_gaps.py | 44 ++++++++++++++++++++++++++++++++++++++------ + +--- +## 2026-09-03 00:03 – 00:03 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- 5ba5a99 docs: README erstellt, Debian statt Ubuntu korrigiert + +### Geänderte Dateien +- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- setup_server.sh | 2 +- + +--- +## 2026-09-03 00:03 – 00:03 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- setup_server.sh | 2 +- + +--- +## 2026-09-03 00:04 – 00:04 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- setup_server.sh | 2 +- + +--- +## 2026-09-03 00:04 – 00:04 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- setup_server.sh | 2 +- + +--- +## 2026-09-03 00:05 – 00:05 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- setup_server.sh | 2 +- + +--- +## 2026-09-03 00:06 – 00:09 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/routers/auth.py | 29 +++++---- +- backend/app/services/auth_service.py | 210 ++++++++++++++++++++++++++++++++--------------------------------- +- backend/app/services/kiosk_auth_service.py | 191 ++++++++++++++++++++++++++++------------------------------- +- backend/app/services/kiosk_session_service.py | 26 +++----- +- backend/app/services/public_stamp_session_service.py | 16 ++--- + +--- +## 2026-09-03 00:12 – 00:14 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- 107cba9 fix(redis): INCR+EXPIRE atomar via Pipeline (MULTI/EXEC) + +### Geänderte Dateien +- backend/app/routers/auth.py | 8 ++++++-- +- backend/app/services/auth_service.py | 8 ++++++-- +- backend/app/services/kiosk_auth_service.py | 8 ++++++-- + +--- +## 2026-09-03 01:59 – 02:00 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/redis.py | 14 +++++++++++++- +- backend/app/main.py | 11 +++++++++++ + +--- +## 2026-09-03 02:04 – 02:05 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/core/redis.py | 14 +++++++++++++- +- backend/app/main.py | 11 +++++++++++ + +--- diff --git a/Stundenaufzeichnungen Muster blanko DATEV ab 2015.pdf b/Stundenaufzeichnungen Muster blanko DATEV ab 2015.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5d2ea771988457d7a6074d7eefeffc0e9340a1b1 GIT binary patch literal 51393 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 OrO6&Vc&M#L_J07uzlLo9 literal 0 HcmV?d00001 diff --git a/backend/tests/test_coverage_gaps.py b/backend/tests/test_coverage_gaps.py index bdde645..c88b575 100644 --- a/backend/tests/test_coverage_gaps.py +++ b/backend/tests/test_coverage_gaps.py @@ -20,6 +20,7 @@ import pytest import pytest_asyncio from datetime import date, time, timedelta from decimal import Decimal +from uuid import UUID from httpx import AsyncClient from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession @@ -612,3 +613,352 @@ async def test_create_absence_returns_warnings( data2 = resp2.json() assert "warnings" in data2 assert any("Überschneidung" in w for w in data2["warnings"]), data2["warnings"] + + +# ── P1-Rest: postgres-expert Audit, 4 gezielte Tests ──────────────────────────── +# +# 1. Scheduler-Idempotenz Überstunden-Verfall (report_service.apply_overtime_expiry_if_needed) +# 2. Bradford-Faktor Fensterrand + MANAGER-Department-Scope (absence_service.get_sick_stats) +# 3. Sondervertretungs-Faktor + Überstunden-Kappung kombiniert (report_service._recalculate_overtime_balance) +# 4. Cross-Company-RLS bei Vertretern (absence_service.list_absences as_substitute=True) + +from types import SimpleNamespace + + +# ── 1. Scheduler-Idempotenz (Überstunden-Verfall) ─────────────────────────────── + +@pytest.mark.asyncio(loop_scope="session") +async def test_overtime_expiry_idempotent_second_call_noop(db_session: AsyncSession): + """Ein zweiter Aufruf am selben Tag darf keinen zweiten Abzug auslösen + (verhindert Doppel-Kappung bei doppeltem Scheduler-Lauf).""" + from app.services.report_service import apply_overtime_expiry_if_needed + + company = SimpleNamespace( + overtime_expiry_enabled=True, + overtime_expiry_month=1, + overtime_expiry_day=1, + overtime_max_carryover_hours=5, + ) + bal = SimpleNamespace(total_hours=Decimal("20"), taken_hours=Decimal("0"), last_expiry_applied_at=None) + + changed_first = await apply_overtime_expiry_if_needed(bal, company, db_session) + assert changed_first is True + assert bal.total_hours == Decimal("5"), "Kappung auf max_carryover_hours erwartet" + + # Zweiter Aufruf am selben Tag (last_expiry_applied_at wurde eben gesetzt) -> no-op + changed_second = await apply_overtime_expiry_if_needed(bal, company, db_session) + assert changed_second is False + assert bal.total_hours == Decimal("5"), "Kein zweiter Abzug bei erneutem Aufruf am selben Tag" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_overtime_expiry_invalid_date_silent_skip(db_session: AsyncSession): + """Ungültiges Verfallsdatum (29. Februar in einem Nicht-Schaltjahr) darf nicht + crashen (kein propagierter ValueError), sondern still übersprungen werden. + + Hinweis: nutzt das reale Systemdatum (Funktion ruft intern date.today() auf, + kein Mock-Punkt vorhanden). Das aktuelle Jahr (Testlauf 2026) ist kein + Schaltjahr, ebenso das Vorjahr 2025 -- Test bleibt bis 2027 gültig + (2028 ist wieder ein Schaltjahr, dann müsste der Test angepasst werden).""" + from app.services.report_service import apply_overtime_expiry_if_needed + + company = SimpleNamespace( + overtime_expiry_enabled=True, + overtime_expiry_month=2, + overtime_expiry_day=29, + overtime_max_carryover_hours=5, + ) + bal = SimpleNamespace(total_hours=Decimal("20"), taken_hours=Decimal("0"), last_expiry_applied_at=None) + + changed = await apply_overtime_expiry_if_needed(bal, company, db_session) + assert changed is False + assert bal.total_hours == Decimal("20"), "Bei ungültigem Datum darf keine Kappung stattfinden" + + +# ── 2. Bradford-Faktor Fensterrand + MANAGER-Department-Scope ────────────────── + +@pytest.mark.asyncio(loop_scope="session") +async def test_bradford_window_boundary_and_manager_department_scope( + client: AsyncClient, db_session: AsyncSession, +): + """(a) Eine Krank-Episode die GENAU am Rand des 365-Tage-Fensters endet + (end_date == window_start) muss mitgezählt werden, eine die einen Tag davor + endet nicht (Off-by-one, quadratisch empfindlich für den Bradford-Faktor). + (b) Ein MANAGER mit department_id sieht nur Bradford-Stats des eigenen + Departments, nicht firmenweit (DSGVO-Datensparsamkeit).""" + from app.models.absence import Absence, AbsenceStatus + from app.models.absence_type import AbsenceCategory, AbsenceType + from app.models.user import UserRole + from app.services.absence_service import absence_service + + reg = await client.post("/api/v1/auth/register", json={ + "company_name": "Bradford Edge GmbH", + "first_name": "Brad", "last_name": "Admin", + "email": "admin@bradfordedge.de", "password": "Secret123", + }) + assert reg.status_code == 201, reg.text + admin_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"} + me = (await client.get("/api/v1/auth/me", headers=admin_headers)).json() + company_id = me["company_id"] + + dep_a = await client.post("/api/v1/companies/me/departments", json={"name": "Abteilung A"}, headers=admin_headers) + assert dep_a.status_code == 201, dep_a.text + dep_b = await client.post("/api/v1/companies/me/departments", json={"name": "Abteilung B"}, headers=admin_headers) + assert dep_b.status_code == 201, dep_b.text + dep_a_id = dep_a.json()["id"] + dep_b_id = dep_b.json()["id"] + + mgr = await client.post("/api/v1/users/invite", json={ + "first_name": "Manager", "last_name": "A", "email": "manager.a@bradfordedge.de", + "role": "MANAGER", "department_id": dep_a_id, "initial_password": "Secret123", + }, headers=admin_headers) + assert mgr.status_code == 201, mgr.text + mgr_id = mgr.json()["id"] + + emp_a = await client.post("/api/v1/users/invite", json={ + "first_name": "Emp", "last_name": "A", "email": "emp.a@bradfordedge.de", + "role": "EMPLOYEE", "department_id": dep_a_id, "initial_password": "Secret123", + }, headers=admin_headers) + assert emp_a.status_code == 201, emp_a.text + emp_a_id = emp_a.json()["id"] + + emp_b = await client.post("/api/v1/users/invite", json={ + "first_name": "Emp", "last_name": "B", "email": "emp.b@bradfordedge.de", + "role": "EMPLOYEE", "department_id": dep_b_id, "initial_password": "Secret123", + }, headers=admin_headers) + assert emp_b.status_code == 201, emp_b.text + emp_b_id = emp_b.json()["id"] + + types_resp = await client.get("/api/v1/absence-types/", headers=admin_headers) + sick_type = next(t for t in types_resp.json() if t["category"] == "sick") + sick_type_id = sick_type["id"] + + ref_date = date.today() + window_start = ref_date - timedelta(days=365) + + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) + + # Emp A: eine Episode GENAU am Fensterrand (muss zählen) + eine Episode + # einen Tag VOR dem Fensterrand (darf nicht zählen). + in_window = Absence( + user_id=emp_a_id, type_id=sick_type_id, + start_date=window_start, end_date=window_start, + working_days=Decimal("1"), status=AbsenceStatus.APPROVED, + ) + out_of_window = Absence( + user_id=emp_a_id, type_id=sick_type_id, + start_date=window_start - timedelta(days=3), end_date=window_start - timedelta(days=1), + working_days=Decimal("3"), status=AbsenceStatus.APPROVED, + ) + # Emp B (anderes Department): Episode innerhalb des Fensters. + emp_b_absence = Absence( + user_id=emp_b_id, type_id=sick_type_id, + start_date=ref_date - timedelta(days=10), end_date=ref_date - timedelta(days=8), + working_days=Decimal("3"), status=AbsenceStatus.APPROVED, + ) + db_session.add_all([in_window, out_of_window, emp_b_absence]) + await db_session.flush() + + # (a) Fensterrand: Admin sieht firmenweit -> Emp A hat genau 1 Episode + # (nur die am Rand liegende zählt, die davor liegende fällt raus). + admin_ns = SimpleNamespace(role=UserRole.COMPANY_ADMIN, department_id=None) + stats_admin = await absence_service.get_sick_stats( + UUID(company_id) if not isinstance(company_id, UUID) else company_id, + admin_ns, ref_date, db_session, + ) + emp_a_stats = next(s for s in stats_admin if str(s["user_id"]) == emp_a_id) + assert emp_a_stats["episodes"] == 1, ( + f"Off-by-one am Fensterrand: erwartet genau 1 Episode (window_start selbst zählt, " + f"ein Tag davor nicht), got {emp_a_stats['episodes']}" + ) + assert emp_a_stats["total_days"] == pytest.approx(1.0) + # Admin sieht auch Emp B (firmenweit, keine Department-Einschränkung). + assert any(str(s["user_id"]) == emp_b_id for s in stats_admin) + + # (b) MANAGER mit department_id sieht nur sein eigenes Department. + mgr_ns = SimpleNamespace(role=UserRole.MANAGER, department_id=UUID(dep_a_id)) + stats_mgr = await absence_service.get_sick_stats( + UUID(company_id) if not isinstance(company_id, UUID) else company_id, + mgr_ns, ref_date, db_session, + ) + assert any(str(s["user_id"]) == emp_a_id for s in stats_mgr), "Manager muss eigenes Department sehen" + assert not any(str(s["user_id"]) == emp_b_id for s in stats_mgr), ( + "DSGVO-Lücke: MANAGER sieht Bradford-Stats von Mitarbeitern außerhalb des eigenen Departments" + ) + + await db_session.commit() + + +# ── 3. Sondervertretungs-Faktor + Überstunden-Kappung kombiniert ─────────────── + +@pytest.mark.asyncio(loop_scope="session") +async def test_special_assignment_factor_combined_with_overtime_cap( + client: AsyncClient, db_session: AsyncSession, +): + """SpecialAssignment mit Faktor 1.5 (mode=fza) UND company.overtime_cap_hours + gleichzeitig: prüft anhand des tatsächlichen Codes, dass die Kappung auf den + FAKTORISIERTEN Wert angewendet wird (hours_by_date wird bereits mit dem Faktor + multipliziert berechnet, siehe report_service.py Zeile ~239, bevor die Kappung + in Zeile ~253 greift). Das ist kein Bug, sondern dokumentiertes Ist-Verhalten: + die Sondervertretungs-Gewichtung fließt VOR die Kappung ein.""" + from app.models.special_assignment import AssignmentMode, SpecialAssignment + from app.models.time_entry import EntryStatus, TimeEntry + from app.services.report_service import _recalculate_overtime_balance + + reg = await client.post("/api/v1/auth/register", json={ + "company_name": "FZA Cap Edge GmbH", + "first_name": "Cap", "last_name": "Admin", + "email": "admin@fzacapedge.de", "password": "Secret123", + }) + assert reg.status_code == 201, reg.text + admin_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"} + + cap_resp = await client.patch("/api/v1/companies/me", json={"overtime_cap_hours": 5}, headers=admin_headers) + assert cap_resp.status_code == 200, cap_resp.text + + emp = await client.post("/api/v1/users/invite", json={ + "first_name": "Cap", "last_name": "Employee", "email": "emp@fzacapedge.de", + "role": "EMPLOYEE", "initial_password": "Secret123", + }, headers=admin_headers) + assert emp.status_code == 201, emp.text + emp_id = emp.json()["id"] + + # Vergangener Werktag (Mo-Fr), weit genug in der Vergangenheit um nicht + # mit anderen Tests zu kollidieren. + workday = date.today() - timedelta(days=90) + while workday.weekday() >= 5: + workday -= timedelta(days=1) + + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) + + user = await db_session.get(User, emp_id) + assert user is not None + + # 10h gearbeitet (08:00-18:00, keine Pause) an einem Standard-8h-Tag. + entry = TimeEntry( + user_id=emp_id, date=workday, + start_time=time(8, 0), end_time=time(18, 0), + break_minutes=0, status=EntryStatus.APPROVED, + ) + db_session.add(entry) + + sa = SpecialAssignment( + user_id=emp_id, company_id=UUID(str(user.company_id)), + date_from=workday, date_to=workday, + factor=Decimal("1.5"), mode=AssignmentMode.fza, + label="Nachtschicht-Zuschlag Test", + ) + db_session.add(sa) + await db_session.flush() + + bal = await _recalculate_overtime_balance(user, None, db_session) + + # Roh: 10h - 8h Soll = 2h Überstunden. Faktorisiert: 10h*1.5=15h - 8h = 7h. + # Kappung bei 5h -> falls auf faktorisierten Wert angewendet (Ist-Verhalten + # laut Code) landet total_hours bei genau 5, NICHT bei den rohen 2h. + assert bal.total_hours == Decimal("5"), ( + f"Erwartetes Ist-Verhalten (Kappung greift auf den faktorisierten Wert, " + f"nicht auf den rohen ArbZG-Überstundenwert) nicht bestätigt, got {bal.total_hours}. " + f"Falls dies fachlich falsch ist, ist das ein Bug in report_service._recalculate_overtime_balance " + f"(Kappung sollte ggf. vor Faktor-Anwendung greifen) -- hier nur dokumentiert, nicht gefixt." + ) + + await db_session.commit() + + +# ── 4. Cross-Company-RLS bei Vertretern (list_absences as_substitute) ────────── + +@pytest.mark.asyncio(loop_scope="session") +async def test_list_absences_as_substitute_cross_company_isolation( + client: AsyncClient, db_session: AsyncSession, +): + """Defense-in-Depth auf Service-Ebene (nicht nur Erstell-Validierung): + list_absences(..., as_substitute=True) mit company_id von Firma B darf + KEINE Anträge aus Firma A liefern, selbst wenn (hypothetisch, z.B. durch + einen Bug anderswo) ein User aus Firma A als substitute_id auf einen + Antrag aus Firma A gesetzt wurde und ein gleich benannter/verwechselter + User aus Firma B abgefragt wird. Der Query joint über User.company_id == + company_id -- geprüft wird hier direkt auf Service-Ebene.""" + from app.models.absence import Absence, AbsenceStatus + from app.models.user import UserRole + from app.services.absence_service import absence_service + + # Firma A: Antragsteller + Vertreter, beide in Firma A. + reg_a = await client.post("/api/v1/auth/register", json={ + "company_name": "Substitute Cross A GmbH", + "first_name": "SubA", "last_name": "Admin", + "email": "admin@subcrossa.de", "password": "Secret123", + }) + assert reg_a.status_code == 201, reg_a.text + headers_a = {"Authorization": f"Bearer {reg_a.json()['access_token']}"} + me_a = (await client.get("/api/v1/auth/me", headers=headers_a)).json() + company_a_id = me_a["company_id"] + + sub_a = await client.post("/api/v1/users/invite", json={ + "first_name": "Sub", "last_name": "InA", "email": "sub.a@subcrossa.de", + "role": "EMPLOYEE", "initial_password": "Secret123", + }, headers=headers_a) + assert sub_a.status_code == 201, sub_a.text + sub_a_id = sub_a.json()["id"] + + types_a = await client.get("/api/v1/absence-types/", headers=headers_a) + vac_type_a = next(t for t in types_a.json() if t["name"] == "Urlaub")["id"] + + # Firma B: eigener Vertreter-Kandidat (gleiche Rolle, andere Firma). + reg_b = await client.post("/api/v1/auth/register", json={ + "company_name": "Substitute Cross B GmbH", + "first_name": "SubB", "last_name": "Admin", + "email": "admin@subcrossb.de", "password": "Secret123", + }) + assert reg_b.status_code == 201, reg_b.text + headers_b = {"Authorization": f"Bearer {reg_b.json()['access_token']}"} + me_b = (await client.get("/api/v1/auth/me", headers=headers_b)).json() + company_b_id = me_b["company_id"] + + # Absence in Firma A, mit sub_a (Firma A) als Vertretung -- über Model + # direkt angelegt um Overlap-/Validierungslogik des Create-Endpunkts zu + # umgehen und die Service-Query isoliert zu prüfen. + start = date.today() + timedelta(days=200) + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) + absence_a = Absence( + user_id=me_a["id"], type_id=vac_type_a, + start_date=start, end_date=start, + working_days=Decimal("1"), status=AbsenceStatus.PENDING, + substitute_id=sub_a_id, + ) + db_session.add(absence_a) + await db_session.flush() + + # Query 1: sub_a (echter Vertreter, Firma A) mit company_id=Firma A -> sieht den Antrag. + sub_a_user = await db_session.get(User, sub_a_id) + total_a, absences_a = await absence_service.list_absences( + UUID(company_a_id), sub_a_user, db_session, as_substitute=True, + ) + assert total_a == 1 + assert absences_a[0].id == absence_a.id + + # Query 2: Admin von Firma B (current_user.id != sub_a_id, aber selbst wenn + # ein Aufrufer aus Firma B versucht mit fremder company_id_a zu fragen, ist + # der Sicherheits-Layer die current_user.id im WHERE Absence.substitute_id + # == current_user.id -- entscheidend ist aber der company_id-Join). Hier: + # regulärer Aufruf mit Firma B's eigener company_id liefert 0 Treffer, + # obwohl der Antrag aus Firma A existiert. + admin_b_user = await db_session.get(User, me_b["id"]) + # current_user hier künstlich auf sub_a_id "verfälscht" simulieren ist nicht + # möglich ohne DB-User -- stattdessen der eigentliche Defense-in-Depth-Check: + # company_id explizit auf Firma B gesetzt, obwohl ein (hypothetischer) + # Vertreter mit gleicher User-Id in Firma A existieren würde. Da UUIDs global + # eindeutig sind, testen wir die Kernaussage direkt: Query mit company_b_id + # + sub_a_user (der in Wahrheit zu Firma A gehört, aber hier als current_user + # übergeben wird) liefert trotzdem 0 Treffer, weil der Join User.company_id + # == company_id (Firma B) den Antrag aus Firma A aussortiert. + total_cross, absences_cross = await absence_service.list_absences( + UUID(company_b_id), sub_a_user, db_session, as_substitute=True, + ) + assert total_cross == 0, ( + "RLS-Lücke: Vertreter-Query lieferte einen Antrag aus einer anderen Firma zurück " + "(company_id-Join in list_absences greift nicht als Defense-in-Depth)" + ) + assert absences_cross == [] + + await db_session.commit()