"""Tests für die Moderation der Ehrentafel: CLI-Befehle und Sperrliste für Namen.""" from __future__ import annotations from collections.abc import Callable from pathlib import Path import pytest from flask import Flask from flask.testing import FlaskClient from app import create_app from app.models import get_db, insert_score, top_scores from app.validation import is_name_blocked, load_blocklist, normalize_for_blocklist def _seed(app: Flask, entries: list[tuple[str, int]]) -> list[int]: with app.app_context(): return [ insert_score(get_db(), name, score, wave=1, duration_s=60) for name, score in entries ] def _names(app: Flask) -> list[str]: with app.app_context(): return [entry["name"] for entry in top_scores(get_db(), 100)] # --------------------------------------------------------------------------- CLI def test_scores_list_shows_ids_and_names(app: Flask) -> None: ids = _seed(app, [("Ada", 300), ("Grace", 500)]) result = app.test_cli_runner().invoke(args=["scores", "list"]) assert result.exit_code == 0 lines = result.output.splitlines() assert "Name" in lines[0] assert str(ids[1]) in lines[1] and "Grace" in lines[1] assert str(ids[0]) in lines[2] and "Ada" in lines[2] def test_scores_list_when_empty(app: Flask) -> None: result = app.test_cli_runner().invoke(args=["scores", "list"]) assert result.exit_code == 0 assert "Keine Einträge" in result.output def test_scores_delete_by_id(app: Flask) -> None: ids = _seed(app, [("Bleibt", 100), ("Weg", 200), ("AuchWeg", 300)]) result = app.test_cli_runner().invoke( args=["scores", "delete", str(ids[1]), str(ids[2]), "999"] ) assert result.exit_code == 0 assert "2 Eintrag/Einträge gelöscht" in result.output assert _names(app) == ["Bleibt"] def test_scores_delete_requires_ids(app: Flask) -> None: result = app.test_cli_runner().invoke(args=["scores", "delete"]) assert result.exit_code != 0 def test_scores_delete_name_is_case_insensitive(app: Flask) -> None: _seed(app, [("Unerwünscht", 100), ("unerwünscht", 200), ("Ada", 300)]) # Auch Umlaute zählen ohne Groß-/Kleinschreibung (SQLites NOCASE könnte das nicht). result = app.test_cli_runner().invoke(args=["scores", "delete-name", "UNERWÜNSCHT"]) assert result.exit_code == 0 assert "2 Eintrag/Einträge" in result.output assert _names(app) == ["Ada"] # --------------------------------------------------------------------- Sperrliste @pytest.mark.parametrize( ("raw", "expected"), [ ("Max Muster", "maxmuster"), ("M.a.x-Muster!", "maxmuster"), ("MAX", "max"), ("Jürg", "jürg"), ], ) def test_normalize_for_blocklist(raw: str, expected: str) -> None: assert normalize_for_blocklist(raw) == expected def test_load_blocklist_ignores_comments_and_blank_lines(tmp_path: Path) -> None: path = tmp_path / "sperre.txt" path.write_text("# Kommentar\n\n Max Muster \nbeleidigung\n#noch einer\n", encoding="utf-8") assert load_blocklist(path) == ["maxmuster", "beleidigung"] def test_missing_blocklist_blocks_nothing(tmp_path: Path) -> None: assert load_blocklist(tmp_path / "gibt-es-nicht.txt") == [] assert is_name_blocked("Irgendwer", []) is False def test_blocked_terms_cannot_be_bypassed_by_spacing_or_case() -> None: terms = ["maxmuster"] assert is_name_blocked("Max Muster", terms) assert is_name_blocked("xX.MAX.muster.Xx", terms) assert not is_name_blocked("Max Mustang", terms) @pytest.fixture() def blocklist_app(tmp_path: Path) -> Flask: blocklist = tmp_path / "name_blocklist.txt" blocklist.write_text("Max Muster\n", encoding="utf-8") return create_app( { "TESTING": True, "SECRET_KEY": "test-secret-key", "DATABASE_PATH": str(tmp_path / "db.sqlite3"), "NAME_BLOCKLIST_PATH": str(blocklist), } ) def test_api_rejects_blocked_name(blocklist_app: Flask) -> None: client: FlaskClient = blocklist_app.test_client() token = client.post("/api/runs").get_json()["run_token"] payload = {"name": "max.muster", "score": 10, "wave": 1, "duration_s": 0, "run_token": token} response = client.post("/api/highscores", json=payload) assert response.status_code == 400 assert response.get_json()["fields"] == {"name": "Dieser Name ist nicht zulässig."} assert _names(blocklist_app) == [] def test_api_accepts_other_names_with_blocklist(blocklist_app: Flask) -> None: client: FlaskClient = blocklist_app.test_client() token = client.post("/api/runs").get_json()["run_token"] payload = {"name": "Ada", "score": 10, "wave": 1, "duration_s": 0, "run_token": token} assert client.post("/api/highscores", json=payload).status_code == 201 def test_api_without_blocklist_file_accepts_names( client: FlaskClient, valid_payload: Callable[..., dict] ) -> None: assert client.post("/api/highscores", json=valid_payload(name="Max Muster")).status_code == 201