Files
timemaster/backend/tests/test_time.py
T
patrick eb122802b2 fix: 8 pre-existing Test-Fehler behoben
1. Krankmeldungen an Wochenenden erlaubt (working_days=0 für SICK)
   Medizinisch korrekt: Krankmeldungen können auch Wochenendtage umfassen.
   Behebt: test_create_absence_sick_auto_approved, test_quick_sick_*,
           test_pull_includes_today_absence_with_category, test_sick_stats_*

2. Self-Approval-Schutz in Tests berücksichtigt
   abs_approver_headers / time_approver_headers: zweiter Admin je Company.
   Behebt: test_approve_absence, test_balance_deducted_after_approve, test_approve_entry

3. test_export_invalid_format: "pdf" → "xml" (pdf ist jetzt valides Format)

Ergebnis: 134/134 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:51:50 +02:00

329 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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