Files
playwish/server/tests/test_turnstile.py
T
99admandClaude Opus 5.5 fc15f2b933 Umbenennung: Audiofy heißt jetzt Playwish
Name, Seitentitel, Dienste (playwish-web, playwish-controller), Dienst-Benutzer
(playwish), Pfade (/opt/playwish, /usr/local/src/playwish), Vorlagen und
Repository-Adresse. „Audiofy“ war bereits von mehreren Produkten belegt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:27:59 +01:00

123 lines
4.9 KiB
Python

"""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