Files
sysops 1fedd683e0 Initial commit – TimeMaster Zeiterfassung & HR-Tool
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:27 +02:00

228 lines
8.1 KiB
Python
Raw Permalink 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 Busylight-Pull-Endpoint und Token-Verwaltung."""
import pytest
import pytest_asyncio
from datetime import date
from httpx import AsyncClient
# ── Fixtures: eigene Company für Busylight-Tests ─────────────────────────────
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def bl_company(client: AsyncClient):
resp = await client.post("/api/v1/auth/register", json={
"company_name": "Busylight GmbH",
"first_name": "Light",
"last_name": "Admin",
"email": "admin@busylight.de",
"password": "Secret123",
})
assert resp.status_code == 201
tokens = resp.json()
me = await client.get(
"/api/v1/auth/me",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
return {"tokens": tokens, "user": me.json()}
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def bl_headers(bl_company):
return {"Authorization": f"Bearer {bl_company['tokens']['access_token']}"}
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def bl_user_with_personnel_number(client: AsyncClient, bl_headers, bl_company):
"""Setzt personnel_number am Admin-User."""
user_id = bl_company["user"]["id"]
resp = await client.patch(
f"/api/v1/users/{user_id}",
json={"personnel_number": "0001"},
headers=bl_headers,
)
assert resp.status_code == 200
return resp.json()
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def bl_token(client: AsyncClient, bl_headers):
"""Generiert Token via Rotate-Endpoint und gibt Klartext zurück."""
resp = await client.post("/api/v1/companies/me/busylight-token/rotate", headers=bl_headers)
assert resp.status_code == 200
return resp.json()["token"]
# ── Token-Verwaltung ─────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_rotate_returns_plaintext_once(client: AsyncClient, bl_headers):
resp = await client.post("/api/v1/companies/me/busylight-token/rotate", headers=bl_headers)
assert resp.status_code == 200
body = resp.json()
assert "token" in body and len(body["token"]) >= 32
assert "created_at" in body
@pytest.mark.asyncio
async def test_token_status_after_rotate(client: AsyncClient, bl_headers, bl_token):
resp = await client.get("/api/v1/companies/me/busylight-token", headers=bl_headers)
assert resp.status_code == 200
body = resp.json()
assert body["configured"] is True
assert body["created_at"] is not None
# ── Pull-Endpoint Auth ───────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_pull_without_token_returns_401(client: AsyncClient):
resp = await client.get("/api/v1/busylight/users")
assert resp.status_code == 401
@pytest.mark.asyncio
async def test_pull_with_invalid_token_returns_401(client: AsyncClient):
resp = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": "Bearer not-a-valid-token-abc"},
)
assert resp.status_code == 401
@pytest.mark.asyncio
async def test_pull_with_valid_token_returns_200(
client: AsyncClient, bl_token, bl_user_with_personnel_number
):
resp = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": f"Bearer {bl_token}"},
)
assert resp.status_code == 200
body = resp.json()
assert body["date"] == str(date.today())
assert isinstance(body["users"], list)
pn = {u["personnel_number"] for u in body["users"]}
assert "0001" in pn
@pytest.mark.asyncio
async def test_pull_omits_users_without_personnel_number(
client: AsyncClient, bl_token, bl_headers
):
"""User ohne personnel_number tauchen nicht in der Liste auf."""
invite = await client.post(
"/api/v1/users/invite",
json={
"email": "ohne_nr@busylight.de",
"first_name": "Ohne",
"last_name": "Nummer",
"role": "EMPLOYEE",
},
headers=bl_headers,
)
assert invite.status_code in (200, 201)
resp = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": f"Bearer {bl_token}"},
)
assert resp.status_code == 200
names = {u["full_name"] for u in resp.json()["users"]}
assert "Ohne Nummer" not in names
@pytest.mark.asyncio
async def test_pull_includes_today_absence_with_category(
client: AsyncClient, bl_token, bl_headers, bl_user_with_personnel_number
):
"""Quick-Sick erstellt approved-Krank-Absence für heute → muss in absences_today auftauchen."""
today = date.today()
sick_resp = await client.post(
"/api/v1/absences/quick-sick",
json={"start_date": str(today), "end_date": str(today)},
headers=bl_headers,
)
assert sick_resp.status_code in (200, 201), sick_resp.text
resp = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": f"Bearer {bl_token}"},
)
body = resp.json()
me_entry = next((u for u in body["users"] if u["personnel_number"] == "0001"), None)
assert me_entry is not None
cats = {a["category"] for a in me_entry["absences_today"]}
assert "sick" in cats
# ── Tenant-Isolation ─────────────────────────────────────────────────────────
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def other_company_token(client: AsyncClient):
"""Zweite Firma mit eigenem Token + eigenem User mit personnel_number 9999."""
reg = await client.post("/api/v1/auth/register", json={
"company_name": "Other GmbH",
"first_name": "Other",
"last_name": "Boss",
"email": "other@boss.de",
"password": "Secret123",
})
assert reg.status_code == 201
headers = {"Authorization": f"Bearer {reg.json()['access_token']}"}
me = await client.get("/api/v1/auth/me", headers=headers)
user_id = me.json()["id"]
patch = await client.patch(
f"/api/v1/users/{user_id}",
json={"personnel_number": "9999"},
headers=headers,
)
assert patch.status_code == 200
rot = await client.post("/api/v1/companies/me/busylight-token/rotate", headers=headers)
assert rot.status_code == 200
return rot.json()["token"]
@pytest.mark.asyncio
async def test_tenant_isolation(client: AsyncClient, other_company_token, bl_token):
"""Token von Firma B liefert nur deren User keine User von Firma A."""
resp_b = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": f"Bearer {other_company_token}"},
)
assert resp_b.status_code == 200
pns_b = {u["personnel_number"] for u in resp_b.json()["users"]}
assert "9999" in pns_b
assert "0001" not in pns_b
resp_a = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": f"Bearer {bl_token}"},
)
pns_a = {u["personnel_number"] for u in resp_a.json()["users"]}
assert "0001" in pns_a
assert "9999" not in pns_a
# ── Token-Delete ─────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_delete_token_invalidates_pull(client: AsyncClient, bl_headers):
rot = await client.post("/api/v1/companies/me/busylight-token/rotate", headers=bl_headers)
fresh = rot.json()["token"]
pull_ok = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": f"Bearer {fresh}"},
)
assert pull_ok.status_code == 200
dl = await client.delete("/api/v1/companies/me/busylight-token", headers=bl_headers)
assert dl.status_code == 204
pull_after = await client.get(
"/api/v1/busylight/users",
headers={"Authorization": f"Bearer {fresh}"},
)
assert pull_after.status_code == 401