Neuer PDF-Export GET /reports/datev-monthly/export: ein Blatt pro Mitarbeiter/Monat im Layout der DATEV-Stundenaufzeichnungs-Vorlage (Beginn/Pause/Ende/Dauer + K/U/UU/F/SA/SU-Kürzel + Bemerkungen + Summe + Unterschriftsfelder). Führt time_entries, Absences und Feiertage pro Kalendertag zusammen. Außerdem: Mustervorlagen für neue Backend-Module (Model/Schema/Router/ Migration/Test) und eine Frontend-Page-Vorlage, abgeleitet vom hours_payouts-Modul als aktuellstem sauberen Muster. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CviFgc739S89xS97pvjszj
67 lines
2.9 KiB
Python
67 lines
2.9 KiB
Python
"""TEMPLATE – Mustervorlage für Router-Tests. Abgeleitet von test_hours_payouts.py.
|
||
|
||
Anleitung:
|
||
1. Nach backend/tests/test_xxx_things.py kopieren, Endpunkte/Felder anpassen
|
||
2. pytest-Konvention: alle Fixtures scope="session" + loop_scope="session"
|
||
(asyncpg + pytest-asyncio 1.x Anforderung – siehe project_pytest_asyncio)
|
||
3. Ausführen NUR auf dem Server (root@192.168.1.137), nie lokal:
|
||
ssh root@192.168.1.137 'cd /opt/timemaster/backend && source venv/bin/activate && python -m pytest tests/test_xxx_things.py -v'
|
||
"""
|
||
import pytest
|
||
import pytest_asyncio
|
||
from httpx import AsyncClient
|
||
|
||
|
||
@pytest_asyncio.fixture(scope="session", loop_scope="session")
|
||
async def xxx_thing_headers(client: AsyncClient):
|
||
resp = await client.post("/api/v1/auth/register", json={
|
||
"company_name": "XxxThing GmbH",
|
||
"first_name": "Test",
|
||
"last_name": "User",
|
||
"email": "admin@xxxthinggmbh.de",
|
||
"password": "Secret123",
|
||
})
|
||
assert resp.status_code == 201, resp.text
|
||
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
|
||
|
||
|
||
@pytest.mark.asyncio(loop_scope="session")
|
||
async def test_create_item(client: AsyncClient, xxx_thing_headers):
|
||
r = await client.post("/api/v1/xxx-things",
|
||
json={"user_id": "...", "note": "Test"}, headers=xxx_thing_headers)
|
||
assert r.status_code == 201, r.text
|
||
assert r.json()["status"] == "requested"
|
||
|
||
|
||
@pytest.mark.asyncio(loop_scope="session")
|
||
async def test_approve_item(client: AsyncClient, xxx_thing_headers):
|
||
create = await client.post("/api/v1/xxx-things",
|
||
json={"user_id": "...", "note": "Test"}, headers=xxx_thing_headers)
|
||
item_id = create.json()["id"]
|
||
|
||
ap = await client.post(f"/api/v1/xxx-things/{item_id}/approve", json={}, headers=xxx_thing_headers)
|
||
assert ap.status_code == 200, ap.text
|
||
assert ap.json()["status"] == "approved"
|
||
|
||
# Doppel-Approve muss scheitern (409)
|
||
ap2 = await client.post(f"/api/v1/xxx-things/{item_id}/approve", json={}, headers=xxx_thing_headers)
|
||
assert ap2.status_code == 409
|
||
|
||
|
||
@pytest.mark.asyncio(loop_scope="session")
|
||
async def test_company_isolation(client: AsyncClient, xxx_thing_headers):
|
||
"""Cross-Tenant-Zugriff muss 404 liefern, nicht 200/403 (verrät keine Existenz)."""
|
||
other = await client.post("/api/v1/auth/register", json={
|
||
"company_name": "Andere Firma GmbH",
|
||
"first_name": "Other", "last_name": "Admin",
|
||
"email": "admin@andere-firma.de", "password": "Secret123",
|
||
})
|
||
other_headers = {"Authorization": f"Bearer {other.json()['access_token']}"}
|
||
|
||
create = await client.post("/api/v1/xxx-things",
|
||
json={"user_id": "...", "note": "Geheim"}, headers=xxx_thing_headers)
|
||
item_id = create.json()["id"]
|
||
|
||
r = await client.get("/api/v1/xxx-things", headers=other_headers)
|
||
assert all(item["id"] != item_id for item in r.json()["items"])
|