141 lines
5.0 KiB
Python
141 lines
5.0 KiB
Python
"""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
|