Audiofy – Musikwünsche für Partys
Server (Website, Admin-Bereich, Player-API) und Player (raspotify + Controller) mit Install-Skripten für Debian-Systeme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""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 / 'audiofy.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("AUDIOFY_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
|
||||
Reference in New Issue
Block a user