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>
123 lines
4.9 KiB
Python
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
|