62ef6c2a11
Frontend (TimeTrackingPage): - Live-Arbeitsuhr (HH:MM:SS) während eingestempelt - Break-Start/End-Buttons mit laufender Pausenuhr - Wochen-Balance-Widget (gearbeitet / erwartet / überstunden) - Approval-Queue Tab für Manager/HR/Admin (pending entries genehmigen/ablehnen) Backend (Reports): - weasyprint>=61.0 in requirements.txt - 3 neue PDF-Export-Tests (Zeit, Abwesenheit, Überstunden) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
355 lines
13 KiB
Python
355 lines
13 KiB
Python
"""Tests für agent-04-dashboard"""
|
|
import pytest
|
|
import pytest_asyncio
|
|
from datetime import date, timedelta
|
|
from httpx import AsyncClient
|
|
|
|
|
|
# ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
|
|
@pytest_asyncio.fixture(scope="session", loop_scope="session")
|
|
async def report_headers(registered_user):
|
|
"""Admin-Headers aus dem bestehenden registered_user Fixture."""
|
|
return {"Authorization": f"Bearer {registered_user['tokens']['access_token']}"}
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="session", loop_scope="session")
|
|
async def report_vacation_type(client: AsyncClient, report_headers):
|
|
resp = await client.get("/api/v1/absence-types/", headers=report_headers)
|
|
types = resp.json()
|
|
vacation = next((t for t in types if t["name"] == "Urlaub"), types[0])
|
|
return vacation["id"]
|
|
|
|
|
|
# ── Employee Dashboard ─────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_employee_dashboard(client: AsyncClient, report_headers):
|
|
resp = await client.get("/api/v1/dashboard/me", headers=report_headers)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "today_open" in data
|
|
assert "week_hours_worked" in data
|
|
assert "week_hours_expected" in data
|
|
assert "week_overtime" in data
|
|
assert "vacation_entitled_days" in data
|
|
assert "pending_absences" in data
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_employee_dashboard_unauthenticated(client: AsyncClient):
|
|
resp = await client.get("/api/v1/dashboard/me")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
# ── Team Dashboard ─────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_team_dashboard(client: AsyncClient, report_headers):
|
|
"""COMPANY_ADMIN kann Team-Dashboard abrufen."""
|
|
resp = await client.get("/api/v1/dashboard/team", headers=report_headers)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "present_count" in data
|
|
assert "on_leave_count" in data
|
|
assert "absent_count" in data
|
|
assert "pending_time_approvals" in data
|
|
assert "pending_absence_approvals" in data
|
|
assert "members" in data
|
|
assert isinstance(data["members"], list)
|
|
# Mindestens der Admin selbst
|
|
assert len(data["members"]) >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_team_dashboard_member_fields(client: AsyncClient, report_headers):
|
|
resp = await client.get("/api/v1/dashboard/team", headers=report_headers)
|
|
data = resp.json()
|
|
member = data["members"][0]
|
|
assert "user_id" in member
|
|
assert "user_name" in member
|
|
assert "status" in member
|
|
assert member["status"] in ("present", "on_leave", "absent")
|
|
|
|
|
|
# ── Company Dashboard ──────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_company_dashboard(client: AsyncClient, report_headers):
|
|
"""COMPANY_ADMIN kann Unternehmens-Dashboard abrufen."""
|
|
resp = await client.get("/api/v1/dashboard/company", headers=report_headers)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["total_employees"] >= 1
|
|
assert "active_today" in data
|
|
assert "attendance_rate" in data
|
|
assert "month_hours_worked" in data
|
|
assert "month_hours_expected" in data
|
|
assert "pending_time_approvals" in data
|
|
assert "pending_absence_approvals" in data
|
|
assert "upcoming_absences" in data
|
|
|
|
|
|
# ── Time Report ────────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_time_report(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
month_start = today.replace(day=1)
|
|
resp = await client.get(
|
|
"/api/v1/reports/time",
|
|
params={"date_from": str(month_start), "date_to": str(today)},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "total_rows" in data
|
|
assert "total_hours" in data
|
|
assert "rows" in data
|
|
assert isinstance(data["rows"], list)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_time_report_row_structure(client: AsyncClient, report_headers):
|
|
"""Falls Einträge vorhanden, Struktur prüfen."""
|
|
today = date.today()
|
|
month_start = today.replace(day=1)
|
|
resp = await client.get(
|
|
"/api/v1/reports/time",
|
|
params={"date_from": str(month_start), "date_to": str(today)},
|
|
headers=report_headers,
|
|
)
|
|
data = resp.json()
|
|
if data["rows"]:
|
|
row = data["rows"][0]
|
|
assert "date" in row
|
|
assert "user_name" in row
|
|
assert "start_time" in row
|
|
assert "status" in row
|
|
assert "worked_hours" in row
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_time_report_default_params(client: AsyncClient, report_headers):
|
|
"""Report ohne date_from/date_to → default = aktueller Monat."""
|
|
resp = await client.get("/api/v1/reports/time", headers=report_headers)
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ── Absence Report ─────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_absence_report(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/absences",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today)},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "total_rows" in data
|
|
assert "total_days" in data
|
|
assert "rows" in data
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_absence_report_with_data(client: AsyncClient, report_headers, report_vacation_type):
|
|
"""Urlaub beantragen und im Report prüfen."""
|
|
next_monday = date.today() + timedelta(days=(7 - date.today().weekday()) + 56)
|
|
await client.post(
|
|
"/api/v1/absences/",
|
|
json={
|
|
"type_id": str(report_vacation_type),
|
|
"start_date": str(next_monday),
|
|
"end_date": str(next_monday + timedelta(days=4)),
|
|
},
|
|
headers=report_headers,
|
|
)
|
|
|
|
resp = await client.get(
|
|
"/api/v1/reports/absences",
|
|
params={"date_from": str(next_monday), "date_to": str(next_monday + timedelta(days=4))},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["total_rows"] >= 1
|
|
assert data["total_days"] > 0
|
|
|
|
|
|
# ── Overtime Report ────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_overtime_report(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/overtime",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today)},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "total_employees" in data
|
|
assert "total_overtime" in data
|
|
assert "rows" in data
|
|
assert data["total_employees"] >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_overtime_report_row_fields(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/overtime",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today)},
|
|
headers=report_headers,
|
|
)
|
|
data = resp.json()
|
|
row = data["rows"][0]
|
|
assert "user_name" in row
|
|
assert "hours_worked" in row
|
|
assert "hours_expected" in row
|
|
assert "overtime_hours" in row
|
|
|
|
|
|
# ── Export ─────────────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_time_csv(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/time/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "csv"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "text/csv" in resp.headers["content-type"]
|
|
assert "attachment" in resp.headers.get("content-disposition", "")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_time_xlsx(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/time/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "xlsx"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "spreadsheetml" in resp.headers["content-type"]
|
|
assert len(resp.content) > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_absence_csv(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/absences/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "csv"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "text/csv" in resp.headers["content-type"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_overtime_xlsx(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/overtime/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "xlsx"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "spreadsheetml" in resp.headers["content-type"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_invalid_format(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/time/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "xml"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 422
|
|
|
|
|
|
# ── Unit Tests: Export-Helpers ─────────────────────────────────────────────────
|
|
|
|
def test_to_csv_empty():
|
|
from app.services.report_service import ReportService
|
|
svc = ReportService()
|
|
assert svc.to_csv([]) == ""
|
|
|
|
|
|
def test_to_csv_with_data():
|
|
from app.services.report_service import ReportService
|
|
svc = ReportService()
|
|
rows = [{"Name": "Max", "Stunden": 8.0}, {"Name": "Anna", "Stunden": 7.5}]
|
|
csv_str = svc.to_csv(rows)
|
|
assert "Name" in csv_str
|
|
assert "Max" in csv_str
|
|
assert "Anna" in csv_str
|
|
|
|
|
|
def test_to_xlsx_empty():
|
|
from app.services.report_service import ReportService
|
|
svc = ReportService()
|
|
result = svc.to_xlsx([])
|
|
assert isinstance(result, bytes)
|
|
assert len(result) > 0 # Leere XLSX-Datei hat noch Header
|
|
|
|
|
|
def test_to_xlsx_with_data():
|
|
from app.services.report_service import ReportService
|
|
svc = ReportService()
|
|
rows = [{"Mitarbeiter": "Max", "Stunden": 8.0}]
|
|
result = svc.to_xlsx(rows, sheet_name="Test")
|
|
assert isinstance(result, bytes)
|
|
assert len(result) > 1000 # XLSX ist ZIP-basiert
|
|
|
|
|
|
# ── PDF Export ─────────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_time_pdf(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/time/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "pdf"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "pdf" in resp.headers["content-type"]
|
|
assert "attachment" in resp.headers.get("content-disposition", "")
|
|
assert len(resp.content) > 1000
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_absence_pdf(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/absences/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "pdf"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "pdf" in resp.headers["content-type"]
|
|
assert "attachment" in resp.headers.get("content-disposition", "")
|
|
assert len(resp.content) > 1000
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_overtime_pdf(client: AsyncClient, report_headers):
|
|
today = date.today()
|
|
resp = await client.get(
|
|
"/api/v1/reports/overtime/export",
|
|
params={"date_from": str(today.replace(day=1)), "date_to": str(today), "format": "pdf"},
|
|
headers=report_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "pdf" in resp.headers["content-type"]
|
|
assert "attachment" in resp.headers.get("content-disposition", "")
|
|
assert len(resp.content) > 1000
|