"""Tests für agent-02-zeiterfassung""" import pytest import pytest_asyncio from datetime import date, time from httpx import AsyncClient # ── Fixtures ────────────────────────────────────────────────────────────────── @pytest_asyncio.fixture(scope="session", loop_scope="session") async def auth_headers(registered_user): token = registered_user["tokens"]["access_token"] return {"Authorization": f"Bearer {token}"} @pytest_asyncio.fixture(scope="session", loop_scope="session") async def manager_headers(client: AsyncClient): """Zweiten User als Manager registrieren.""" resp = await client.post("/api/v1/auth/register", json={ "company_name": "Time GmbH", "first_name": "Manager", "last_name": "Max", "email": "manager@timegmbh.de", "password": "Secret123", }) assert resp.status_code == 201 token = resp.json()["access_token"] return {"Authorization": f"Bearer {token}"} @pytest_asyncio.fixture(scope="session", loop_scope="session") async def time_approver_headers(client: AsyncClient, manager_headers): """Zweiter Admin in Time GmbH – kann Zeiteinträge anderer genehmigen.""" resp = await client.post("/api/v1/users/invite", json={ "first_name": "Time", "last_name": "Approver", "email": "approver@timegmbh.de", "role": "COMPANY_ADMIN", "initial_password": "Secret123", }, headers=manager_headers) assert resp.status_code == 201, resp.text login = await client.post("/api/v1/auth/login", json={ "email": "approver@timegmbh.de", "password": "Secret123", }) assert login.status_code == 200, login.text return {"Authorization": f"Bearer {login.json()['access_token']}"} # ── Stamp In / Out ───────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_stamp_in(client: AsyncClient, auth_headers): resp = await client.post( "/api/v1/time/stamp-in", json={"source": "web"}, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert "entry" in data assert data["entry"]["end_time"] is None assert data["entry"]["status"] == "pending" assert isinstance(data["warnings"], list) @pytest.mark.asyncio async def test_stamp_in_twice_fails(client: AsyncClient, auth_headers): """Zweimal einzustempeln ohne auszustempeln muss 409 ergeben.""" await client.post("/api/v1/time/stamp-in", json={"source": "web"}, headers=auth_headers) resp = await client.post("/api/v1/time/stamp-in", json={"source": "web"}, headers=auth_headers) assert resp.status_code == 409 @pytest.mark.asyncio async def test_stamp_out(client: AsyncClient, auth_headers): await client.post("/api/v1/time/stamp-in", json={"source": "web"}, headers=auth_headers) resp = await client.post("/api/v1/time/stamp-out", json={}, headers=auth_headers) assert resp.status_code == 200 data = resp.json() assert data["entry"]["end_time"] is not None @pytest.mark.asyncio async def test_stamp_out_without_in_fails(client: AsyncClient, auth_headers): resp = await client.post("/api/v1/time/stamp-out", json={}, headers=auth_headers) assert resp.status_code == 404 @pytest.mark.asyncio async def test_break_workflow(client: AsyncClient, auth_headers): await client.post("/api/v1/time/stamp-in", json={"source": "web"}, headers=auth_headers) resp = await client.post("/api/v1/time/break-start", headers=auth_headers) assert resp.status_code == 200 assert resp.json()["break_start"] is not None resp = await client.post("/api/v1/time/break-end", headers=auth_headers) assert resp.status_code == 200 assert resp.json()["break_start"] is None assert resp.json()["break_minutes"] >= 0 @pytest.mark.asyncio async def test_break_start_twice_fails(client: AsyncClient, auth_headers): await client.post("/api/v1/time/stamp-in", json={"source": "web"}, headers=auth_headers) await client.post("/api/v1/time/break-start", headers=auth_headers) resp = await client.post("/api/v1/time/break-start", headers=auth_headers) assert resp.status_code == 409 # ── Today ────────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_today(client: AsyncClient, auth_headers): resp = await client.get("/api/v1/time/today", headers=auth_headers) assert resp.status_code == 200 assert isinstance(resp.json(), list) # ── Entries ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_list_entries(client: AsyncClient, auth_headers): resp = await client.get("/api/v1/time/entries", headers=auth_headers) assert resp.status_code == 200 data = resp.json() assert "total" in data assert "items" in data @pytest.mark.asyncio async def test_create_manual_entry(client: AsyncClient, auth_headers): resp = await client.post( "/api/v1/time/entries", json={ "date": str(date.today()), "start_time": "09:00:00", "end_time": "17:00:00", "break_minutes": 30, "source": "manual", }, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert data["entry"]["status"] == "pending" assert data["entry"]["source"] == "manual" @pytest.mark.asyncio async def test_manual_entry_arbzg_warning(client: AsyncClient, auth_headers): """Mehr als 6h ohne Pause → ArbZG Warnung.""" resp = await client.post( "/api/v1/time/entries", json={ "date": str(date.today()), "start_time": "08:00:00", "end_time": "15:00:00", "break_minutes": 0, "source": "manual", }, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert len(data["warnings"]) > 0 assert any("Pause" in w for w in data["warnings"]) @pytest.mark.asyncio async def test_update_entry(client: AsyncClient, auth_headers): create_resp = await client.post( "/api/v1/time/entries", json={ "date": str(date.today()), "start_time": "09:00:00", "end_time": "17:00:00", "break_minutes": 30, "source": "manual", }, headers=auth_headers, ) entry_id = create_resp.json()["entry"]["id"] resp = await client.patch( f"/api/v1/time/entries/{entry_id}", json={"break_minutes": 45, "correction_note": "Pause vergessen einzutragen"}, headers=auth_headers, ) assert resp.status_code == 200 assert resp.json()["break_minutes"] == 45 assert resp.json()["correction_note"] == "Pause vergessen einzutragen" @pytest.mark.asyncio async def test_approve_entry(client: AsyncClient, manager_headers, time_approver_headers): """Manager genehmigt einen Eintrag eines anderen Benutzers (kein Self-Approval).""" create_resp = await client.post( "/api/v1/time/entries", json={ "date": str(date.today()), "start_time": "09:00:00", "end_time": "17:30:00", "break_minutes": 30, "source": "manual", }, headers=manager_headers, ) assert create_resp.status_code == 201 entry_id = create_resp.json()["entry"]["id"] resp = await client.post( f"/api/v1/time/entries/{entry_id}/approve", headers=time_approver_headers, ) assert resp.status_code == 200 assert resp.json()["status"] == "approved" @pytest.mark.asyncio async def test_reject_entry(client: AsyncClient, manager_headers): create_resp = await client.post( "/api/v1/time/entries", json={ "date": str(date.today()), "start_time": "08:00:00", "end_time": "20:00:00", "break_minutes": 0, "source": "manual", }, headers=manager_headers, ) entry_id = create_resp.json()["entry"]["id"] resp = await client.post( f"/api/v1/time/entries/{entry_id}/reject", json={"rejection_note": "Zeiten unrealistisch"}, headers=manager_headers, ) assert resp.status_code == 200 assert resp.json()["status"] == "rejected" # ── Balance ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_balance(client: AsyncClient, registered_user, auth_headers): user_id = registered_user["user"]["id"] resp = await client.get(f"/api/v1/time/balance/{user_id}", headers=auth_headers) assert resp.status_code == 200 data = resp.json() assert "total_hours_worked" in data assert "expected_hours" in data assert "overtime_hours" in data # ── Work Schedules ───────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_and_list_schedules(client: AsyncClient, manager_headers): resp = await client.post( "/api/v1/time/schedules", json={ "name": "Vollzeit 40h", "mon_h": "8.00", "tue_h": "8.00", "wed_h": "8.00", "thu_h": "8.00", "fri_h": "8.00", "sat_h": "0.00", "sun_h": "0.00", "valid_from": str(date.today()), }, headers=manager_headers, ) assert resp.status_code == 201 assert resp.json()["name"] == "Vollzeit 40h" list_resp = await client.get("/api/v1/time/schedules", headers=manager_headers) assert list_resp.status_code == 200 assert len(list_resp.json()) >= 1 # ── ArbZG Unit Tests ────────────────────────────────────────────────────────── def test_arbzg_check_ok(): from app.services.time_service import _check_arbzg warnings = _check_arbzg(time(9, 0), time(17, 0), 30) assert len(warnings) == 0 def test_arbzg_check_no_break_over_6h(): from app.services.time_service import _check_arbzg warnings = _check_arbzg(time(9, 0), time(16, 0), 0) assert any("30 min" in w for w in warnings) def test_arbzg_check_break_too_short_over_9h(): from app.services.time_service import _check_arbzg warnings = _check_arbzg(time(8, 0), time(18, 0), 30) assert any("45 min" in w for w in warnings) def test_arbzg_check_over_10h(): from app.services.time_service import _check_arbzg warnings = _check_arbzg(time(6, 0), time(17, 0), 0) assert any("10 Stunden" in w for w in warnings) def test_rest_period_warning(): from app.services.time_service import _check_rest_period from datetime import date, time warnings = _check_rest_period( time(22, 0), date(2026, 3, 26), time(7, 0), date(2026, 3, 27) ) assert any("11h" in w for w in warnings) def test_rest_period_ok(): from app.services.time_service import _check_rest_period from datetime import date, time warnings = _check_rest_period( time(17, 0), date(2026, 3, 26), time(8, 0), date(2026, 3, 27) ) assert len(warnings) == 0