"""Tests für Cloudflare Turnstile (siteverify wird durch einen Platzhalter ersetzt).""" import httpx import pytest from argon2 import PasswordHasher from fastapi.testclient import TestClient from app import admin, config, gaeste, turnstile from app.spotify import spotify from tests.test_gaeste import URI, FakeSpotify, eintreten PASSWORT = "richtig-langes-passwort" @pytest.fixture def antworten(monkeypatch): """Steuert, was siteverify antwortet; merkt sich die Anfragen.""" zustand = {"ergebnis": {"success": True, "hostname": "musik.example.de"}, "anfragen": []} def siteverify(daten): zustand["anfragen"].append(daten) if isinstance(zustand["ergebnis"], Exception): raise zustand["ergebnis"] return zustand["ergebnis"] monkeypatch.setattr(turnstile, "_siteverify", siteverify) return zustand @pytest.fixture def client(tmp_path, monkeypatch, antworten): env = tmp_path / ".env" env.write_text(f"DB_PATH={tmp_path / 'playwish.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n" f"PUBLIC_URL=https://musik.example.de\nADMIN_PASSWORD_HASH={PasswordHasher().hash(PASSWORT)}\n" "TURNSTILE_SITE_KEY=site-oeffentlich\nTURNSTILE_SECRET_KEY=secret-geheim\n") monkeypatch.setenv("PLAYWISH_ENV", str(env)) config.konfig.cache_clear() for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"): monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60)) monkeypatch.setattr(admin, "login_drossel", gaeste.Drossel(1000, 900)) from app.main import app app.dependency_overrides[spotify] = FakeSpotify with TestClient(app) as c: yield c app.dependency_overrides.clear() config.konfig.cache_clear() def test_site_key_oeffentlich(client): assert client.get("/api/turnstile").json() == {"site_key": "site-oeffentlich"} def test_wunsch_ohne_token_abgelehnt(client): eintreten(client) r = client.post("/api/wish", json={"track_uri": URI["A"]}) assert r.status_code == 400 and "Sicherheitsprüfung" in r.json()["detail"] def test_wunsch_mit_gueltigem_token(client, antworten): eintreten(client) assert client.post("/api/wish", json={"track_uri": URI["A"], "turnstile": "tok"}).status_code == 200 anfrage = antworten["anfragen"][0] assert anfrage["secret"] == "secret-geheim" and anfrage["response"] == "tok" def test_wunsch_ungueltiges_token(client, antworten): eintreten(client) antworten["ergebnis"] = {"success": False, "error-codes": ["invalid-input-response"]} assert client.post("/api/wish", json={"track_uri": URI["A"], "turnstile": "tok"}).status_code == 403 def test_falscher_hostname(client, antworten): eintreten(client) antworten["ergebnis"] = {"success": True, "hostname": "boese.example"} assert client.post("/api/wish", json={"track_uri": URI["A"], "turnstile": "tok"}).status_code == 403 def test_falsche_aktion(client, antworten): """Ein Token vom Login-Formular darf keinen Wunsch abschicken (und umgekehrt).""" eintreten(client) antworten["ergebnis"] = {"success": True, "hostname": "musik.example.de", "action": "login"} assert client.post("/api/wish", json={"track_uri": URI["A"], "turnstile": "tok"}).status_code == 403 def test_wunsch_bei_cloudflare_ausfall_erlaubt(client, antworten): eintreten(client) antworten["ergebnis"] = httpx.ConnectError("weg") assert client.post("/api/wish", json={"track_uri": URI["A"], "turnstile": "tok"}).status_code == 200 def test_limit_greift_vor_turnstile(client, antworten): """Abgewiesene Anfragen (z. B. Limit) kosten keine Turnstile-Prüfung.""" eintreten(client) client.post("/api/wish", json={"track_uri": URI["A"], "turnstile": "t1"}) client.post("/api/wish", json={"track_uri": URI["B"], "turnstile": "t2"}) antworten["anfragen"].clear() assert client.post("/api/wish", json={"track_uri": URI["E"], "turnstile": "t3"}).status_code == 429 assert antworten["anfragen"] == [] def test_login_ohne_token(client): assert client.post("/admin/login", json={"password": PASSWORT}).status_code == 400 def test_login_mit_token(client): assert client.post("/admin/login", json={"password": PASSWORT, "turnstile": "tok"}).status_code == 200 def test_login_ungueltiges_token(client, antworten): antworten["ergebnis"] = {"success": False} assert client.post("/admin/login", json={"password": PASSWORT, "turnstile": "tok"}).status_code == 403 def test_login_bei_cloudflare_ausfall_gesperrt(client, antworten): antworten["ergebnis"] = httpx.ConnectError("weg") assert client.post("/admin/login", json={"password": PASSWORT, "turnstile": "tok"}).status_code == 503 def test_csp_erlaubt_turnstile(client): csp = client.get("/admin/login").headers["content-security-policy"] assert "script-src 'self' https://challenges.cloudflare.com" in csp assert "frame-src https://challenges.cloudflare.com" in csp