"""Tests für Kiosk-Stempel-Endpunkte (Login -> stamp/in|out|status).""" import base64 import hashlib import time import uuid import pytest import pytest_asyncio from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from httpx import AsyncClient pytestmark = pytest.mark.asyncio DEVICES_URL = "/api/v1/kiosk/devices" def _headers(device_id: str, private_key: Ed25519PrivateKey, path: str, body: bytes) -> dict: timestamp = str(int(time.time())) nonce = str(uuid.uuid4()) body_hash = hashlib.sha256(body).hexdigest() message = f"POST {path} {timestamp} {nonce} {body_hash}".encode() signature = private_key.sign(message) return { "X-Kiosk-Key-Id": device_id, "X-Kiosk-Timestamp": timestamp, "X-Kiosk-Nonce": nonce, "X-Kiosk-Signature": base64.b64encode(signature).decode(), "Content-Type": "application/json", } @pytest_asyncio.fixture(scope="session", loop_scope="session") async def stamp_keypair(): private_key = Ed25519PrivateKey.generate() public_key_pem = ( private_key.public_key() .public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo) .decode() ) return private_key, public_key_pem @pytest_asyncio.fixture(scope="session", loop_scope="session") async def stamp_user_headers(client: AsyncClient): resp = await client.post("/api/v1/auth/register", json={ "company_name": "Stamp GmbH", "first_name": "Stan", "last_name": "Pel", "email": "admin@stampgmbh.de", "password": "Secret123", }) assert resp.status_code == 201, resp.text return {"Authorization": f"Bearer {resp.json()['access_token']}"} @pytest_asyncio.fixture(scope="session", loop_scope="session") async def stamp_device(client: AsyncClient, stamp_user_headers, stamp_keypair): _, public_key_pem = stamp_keypair resp = await client.post(DEVICES_URL, json={"name": "Stamp-Terminal", "location": "Test", "public_key": public_key_pem}, headers=stamp_user_headers) assert resp.status_code == 201, resp.text device = resp.json() ap = await client.post(f"{DEVICES_URL}/{device['id']}/approve", headers=stamp_user_headers) assert ap.status_code == 200, ap.text return device async def test_pin_login_then_stamp_in_and_out( client: AsyncClient, stamp_user_headers, stamp_keypair, stamp_device ): private_key, _ = stamp_keypair device_id = stamp_device["id"] me = await client.get("/api/v1/users/me", headers=stamp_user_headers) user_id = me.json()["id"] personnel_number = me.json().get("personnel_number") if not personnel_number: pn = await client.patch(f"/api/v1/users/{user_id}", json={"personnel_number": "9001"}, headers=stamp_user_headers) assert pn.status_code == 200, pn.text personnel_number = "9001" pin_r = await client.post(f"/api/v1/users/{user_id}/kiosk-pin", json={"pin": "4321"}, headers=stamp_user_headers) assert pin_r.status_code == 200, pin_r.text login_body = f'{{"personnel_number": "{personnel_number}", "pin": "4321"}}'.encode() login_resp = await client.post( "/api/v1/kiosk/auth/pin", content=login_body, headers=_headers(device_id, private_key, "/api/v1/kiosk/auth/pin", login_body), ) assert login_resp.status_code == 200, login_resp.text session_token = login_resp.json()["session_token"] status_body = f'{{"session_token": "{session_token}"}}'.encode() status_resp = await client.post( "/api/v1/kiosk/stamp/status", content=status_body, headers=_headers(device_id, private_key, "/api/v1/kiosk/stamp/status", status_body), ) assert status_resp.status_code == 200, status_resp.text assert status_resp.json()["stamped_in"] is False in_body = f'{{"session_token": "{session_token}"}}'.encode() in_resp = await client.post( "/api/v1/kiosk/stamp/in", content=in_body, headers=_headers(device_id, private_key, "/api/v1/kiosk/stamp/in", in_body), ) assert in_resp.status_code == 200, in_resp.text assert in_resp.json()["status"] == "stamped_in" out_body = f'{{"session_token": "{session_token}"}}'.encode() out_resp = await client.post( "/api/v1/kiosk/stamp/out", content=out_body, headers=_headers(device_id, private_key, "/api/v1/kiosk/stamp/out", out_body), ) assert out_resp.status_code == 200, out_resp.text assert out_resp.json()["status"] == "stamped_out" async def test_stamp_rejects_session_from_other_device( client: AsyncClient, stamp_user_headers, stamp_keypair, stamp_device ): private_key, public_key_pem = stamp_keypair other = await client.post(DEVICES_URL, json={"name": "Anderes-Terminal", "location": "Test", "public_key": public_key_pem}, headers=stamp_user_headers) assert other.status_code == 201 other_device = other.json() ap = await client.post(f"{DEVICES_URL}/{other_device['id']}/approve", headers=stamp_user_headers) assert ap.status_code == 200 me = await client.get("/api/v1/users/me", headers=stamp_user_headers) user_id = me.json()["id"] login_body = f'{{"user_id": "{user_id}"}}'.encode() login_resp = await client.post( "/api/v1/kiosk/auth/list", content=login_body, headers=_headers(stamp_device["id"], private_key, "/api/v1/kiosk/auth/list", login_body), ) assert login_resp.status_code == 200, login_resp.text session_token = login_resp.json()["session_token"] # Session wurde für stamp_device erstellt - Stempeln über other_device muss scheitern body = f'{{"session_token": "{session_token}"}}'.encode() resp = await client.post( "/api/v1/kiosk/stamp/in", content=body, headers=_headers(other_device["id"], private_key, "/api/v1/kiosk/stamp/in", body), ) assert resp.status_code == 401, resp.text