"""Tests für die HTML-Seiten, den Health-Check und allgemeine HTTP-Eigenschaften.""" from __future__ import annotations from pathlib import Path from flask.testing import FlaskClient from app import create_app def test_index_serves_game_page(client: FlaskClient) -> None: response = client.get("/") assert response.status_code == 200 html = response.get_data(as_text=True) assert 'id="game"' in html assert "js/lib/phaser.min.js" in html assert 'type="module"' in html # Der Disclaimer-Text ist für die Einblendung im Spiel eingebettet. assert "Satire und reine Fiktion" in html def test_index_passes_urls_to_frontend(client: FlaskClient) -> None: html = client.get("/").get_data(as_text=True) assert 'data-api-highscores="/api/highscores"' in html assert 'data-api-runs="/api/runs"' in html assert 'data-static-base="/static/"' in html def test_index_respects_mount_prefix(client: FlaskClient) -> None: """Unter cPanel kann die App unter einer Unter-URL laufen (SCRIPT_NAME).""" html = client.get("/", base_url="http://localhost/glory").get_data(as_text=True) assert 'data-api-highscores="/glory/api/highscores"' in html assert 'data-static-base="/glory/static/"' in html assert 'src="/glory/static/js/main.js"' in html def test_disclaimer_page(client: FlaskClient) -> None: response = client.get("/disclaimer") assert response.status_code == 200 html = response.get_data(as_text=True) for phrase in ( "Satire und reine Fiktion", "frei erfunden", "nicht beabsichtigt", "keine politische Position", ): assert phrase in html assert 'href="/"' in html def test_healthz(client: FlaskClient) -> None: response = client.get("/healthz") assert response.status_code == 200 assert response.get_json() == {"status": "ok"} def test_security_headers_present(client: FlaskClient) -> None: response = client.get("/") csp = response.headers["Content-Security-Policy"] assert "script-src 'self'" in csp assert "object-src 'none'" in csp assert response.headers["X-Content-Type-Options"] == "nosniff" def test_unknown_page_returns_404_html(client: FlaskClient) -> None: response = client.get("/gibt-es-nicht") assert response.status_code == 404 assert response.mimetype == "text/html" def test_unknown_api_route_returns_json_404(client: FlaskClient) -> None: response = client.get("/api/gibt-es-nicht") assert response.status_code == 404 assert response.is_json assert response.get_json()["error"] == "not_found" def test_wrong_method_on_api_returns_json_405(client: FlaskClient) -> None: response = client.delete("/api/highscores") assert response.status_code == 405 assert response.get_json()["error"] == "method_not_allowed" def test_forwarded_headers_are_ignored_without_trusted_proxy(client: FlaskClient) -> None: """Ohne GLORY_TRUST_PROXY kann ein Client sein Rate-Limit nicht per Header umgehen.""" for i in range(5): client.post("/api/highscores", json={}, headers={"X-Forwarded-For": f"198.51.100.{i}"}) blocked = client.post("/api/highscores", json={}, headers={"X-Forwarded-For": "198.51.100.99"}) assert blocked.status_code == 429 def test_trusted_proxy_uses_client_ip_but_not_host_or_prefix(tmp_path: Path) -> None: app = create_app( { "TESTING": True, "SECRET_KEY": "k", "DATABASE_PATH": str(tmp_path / "db.sqlite3"), "TRUST_PROXY": True, } ) client = app.test_client() # Host und Präfix aus Headern dürfen nicht in die erzeugten URLs gelangen. html = client.get( "/", headers={"X-Forwarded-Prefix": "//evil.example", "X-Forwarded-Host": "evil.example"} ).get_data(as_text=True) assert "evil.example" not in html assert 'src="/static/js/main.js"' in html # Die weitergeleitete Client-IP zählt dagegen fürs Rate-Limit. for _ in range(5): client.post("/api/highscores", json={}, headers={"X-Forwarded-For": "198.51.100.1"}) first = client.post("/api/highscores", json={}, headers={"X-Forwarded-For": "198.51.100.1"}) other = client.post("/api/highscores", json={}, headers={"X-Forwarded-For": "198.51.100.2"}) assert first.status_code == 429 assert other.status_code == 400 def test_api_responses_keep_field_order(client: FlaskClient) -> None: body = client.post("/api/highscores", json={}).get_data(as_text=True) assert body.index('"error"') < body.index('"message"') < body.index('"fields"') def test_static_phaser_and_main_are_served(client: FlaskClient) -> None: for path in ("/static/js/lib/phaser.min.js", "/static/js/main.js", "/static/css/site.css"): response = client.get(path) assert response.status_code == 200, path response.close()