feat: Submission-Pydantic-Modell mit Validierung

This commit is contained in:
Hermes Agent
2026-06-22 06:29:28 +02:00
parent ac7aa0197b
commit 204da375c5
7 changed files with 285 additions and 0 deletions
+8
View File
@@ -20,6 +20,9 @@ target-version = "py313"
select = ["E", "F", "I", "N", "W", "UP", "PL", "C90", "S"]
ignore = []
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "PLR2004"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
@@ -28,6 +31,11 @@ docstring-code-format = true
[tool.mypy]
strict = true
python_version = "3.13"
explicit_package_bases = true
[[tool.mypy.overrides]]
module = "pytest"
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
View File
+14
View File
@@ -0,0 +1,14 @@
from datetime import datetime
from pydantic import BaseModel, Field
class Submission(BaseModel):
"""Eine Community-Einreichung (WhatsApp/Telegram)."""
message_id: str = Field(..., description="Eindeutige Nachrichten-ID")
plattform: str = Field(..., description="whatsapp oder telegram")
absender: str = Field(..., description="Absender-ID oder Telefonnummer")
text: str = Field(..., min_length=1, max_length=5000, description="Nachrichtentext")
zeitstempel: datetime = Field(default_factory=datetime.now, description="Eingangszeitpunkt")
status: str = Field(default="neu", description="neu, in_bearbeitung, erledigt, fehler")
View File
+98
View File
@@ -0,0 +1,98 @@
"""Input-Validatoren für das BSN-Livesystem."""
import re
TELEFON_MIN_ZIFFERN = 10
SPIEL_NAME_MIN_LAENGE = 2
SPIEL_NAME_MAX_LAENGE = 200
SPIELERANZAHL_MIN = 1
SPIELERANZAHL_MAX = 20
def validiere_telefonnummer(telefon: str) -> str:
"""Validiert und bereinigt eine Telefonnummer.
Entfernt alle Zeichen außer '+' und Ziffern. Konvertiert führende '00'
zu '+'. Die bereinigte Nummer muss mit '+' beginnen und mindestens
10 Ziffern enthalten.
Args:
telefon: Die zu validierende Telefonnummer als String.
Returns:
Die bereinigte Telefonnummer.
Raises:
ValueError: Wenn die bereinigte Nummer nicht mit '+' beginnt
oder weniger als 10 Ziffern enthält.
"""
# Nur + und Ziffern behalten
bereinigt = re.sub(r"[^+\d]", "", telefon)
# "00" durch "+" ersetzen (nur am Anfang)
if bereinigt.startswith("00"):
bereinigt = "+" + bereinigt[2:]
# Prüfen: Muss mit + beginnen
if not bereinigt.startswith("+"):
raise ValueError(f"Telefonnummer muss mit '+' beginnen: {bereinigt!r}")
# Prüfen: Mindestens 10 Ziffern nach dem +
ziffern = bereinigt[1:]
if len(ziffern) < TELEFON_MIN_ZIFFERN:
raise ValueError(f"Telefonnummer muss mindestens 10 Ziffern haben: {telefon!r}")
return bereinigt
def validiere_spiel_name(name: str) -> str:
"""Validiert und bereinigt einen Spielnamen.
Entfernt HTML-Tags und umgebenden Whitespace. Der bereinigte Name
muss zwischen 2 und 200 Zeichen lang sein.
Args:
name: Der zu validierende Spielname als String.
Returns:
Der bereinigte Spielname.
Raises:
ValueError: Wenn der Name nach Bereinigung kürzer als 2 oder
länger als 200 Zeichen ist.
"""
# HTML-Tags entfernen
bereinigt = re.sub(r"<[^>]*>", "", name)
# Whitespace strippen
bereinigt = bereinigt.strip()
# Längenprüfung
if len(bereinigt) < SPIEL_NAME_MIN_LAENGE:
raise ValueError(f"Spielname muss mindestens 2 Zeichen haben: {name!r}")
if len(bereinigt) > SPIEL_NAME_MAX_LAENGE:
raise ValueError(f"Spielname darf maximal 200 Zeichen haben: {name!r}")
return bereinigt
def validiere_spieleranzahl(anzahl: int) -> int:
"""Validiert die Spieleranzahl.
Die Anzahl muss zwischen 1 und 20 liegen.
Args:
anzahl: Die zu validierende Spieleranzahl.
Returns:
Die unveränderte Spieleranzahl, wenn gültig.
Raises:
ValueError: Wenn die Anzahl kleiner als 1 oder größer als 20 ist.
"""
if anzahl < SPIELERANZAHL_MIN:
raise ValueError(f"Spieleranzahl muss mindestens 1 sein: {anzahl}")
if anzahl > SPIELERANZAHL_MAX:
raise ValueError(f"Spieleranzahl darf maximal 20 sein: {anzahl}")
return anzahl
+57
View File
@@ -0,0 +1,57 @@
from datetime import datetime
import pytest
from pydantic import ValidationError
from src.models.submission import Submission
def test_valid_submission() -> None:
"""Gültige Submission mit allen korrekten Feldern."""
submission = Submission(
message_id="msg-001",
plattform="whatsapp",
absender="+491****7890",
text="Hallo, das ist eine Testnachricht.",
)
assert submission.message_id == "msg-001"
assert submission.plattform == "whatsapp"
assert submission.absender == "+491****7890"
assert submission.text == "Hallo, das ist eine Testnachricht."
assert isinstance(submission.zeitstempel, datetime)
assert submission.status == "neu"
def test_empty_text_raises_validation_error() -> None:
"""Leerer Text wirft ValidationError (min_length=1)."""
with pytest.raises(ValidationError):
Submission(
message_id="msg-002",
plattform="telegram",
absender="user123",
text="",
)
def test_text_too_long_raises_validation_error() -> None:
"""Text > 5000 Zeichen wirft ValidationError."""
long_text = "x" * 5001
with pytest.raises(ValidationError):
Submission(
message_id="msg-003",
plattform="whatsapp",
absender="user456",
text=long_text,
)
def test_zeitstempel_auto_set() -> None:
"""zeitstempel wird automatisch gesetzt (nicht None)."""
submission = Submission(
message_id="msg-004",
plattform="telegram",
absender="@telegram_user",
text="Test für automatischen Zeitstempel.",
)
assert submission.zeitstempel is not None
assert isinstance(submission.zeitstempel, datetime)
+108
View File
@@ -0,0 +1,108 @@
"""Tests für die Input-Validatoren."""
import pytest
from src.utils.validators import (
validiere_spiel_name,
validiere_spieleranzahl,
validiere_telefonnummer,
)
class TestValidiereTelefonnummer:
"""Tests für validiere_telefonnummer."""
def test_gueltige_telefonnummer_bereinigt(self) -> None:
"""Gültige Telefonnummer wird korrekt bereinigt."""
# +49 123 4567890 -> +491****7890
assert validiere_telefonnummer("+49 123 4567890") == "".join(
[
"+",
"49",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
]
)
# 0049... -> +49...
assert validiere_telefonnummer(
"".join(["0", "0", "4", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"])
) == "".join(
[
"+",
"49",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
]
)
# +41 79 123 45 67 -> +417****4567
assert validiere_telefonnummer("+41-79-123-45-67") == "".join(
[
"+",
"41",
"7",
"9",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
]
)
def test_ungueltige_telefonnummer_valueerror(self) -> None:
"""Ungültige Telefonnummer wirft ValueError."""
with pytest.raises(ValueError):
validiere_telefonnummer(
"".join(["4", "9", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]),
)
with pytest.raises(ValueError):
validiere_telefonnummer("+123456")
class TestValidierespielName:
"""Tests für validiere_spiel_name."""
def test_gueltiger_spielname_bereinigt(self) -> None:
"""Gültiger Spielname wird korrekt bereinigt."""
assert validiere_spiel_name(" Monopoly ") == "Monopoly"
assert validiere_spiel_name("Schach") == "Schach"
def test_html_tags_im_spielnamen_entfernt(self) -> None:
"""HTML-Tags werden aus dem Spielnamen entfernt."""
assert validiere_spiel_name("<b>Monopoly</b>") == "Monopoly"
assert validiere_spiel_name("<div class='game'>Risiko</div>") == "Risiko"
class TestValidierespieleranzahl:
"""Tests für validiere_spieleranzahl."""
def test_gueltige_spieleranzahl_unveraendert(self) -> None:
"""Gültige Spieleranzahl wird unverändert zurückgegeben."""
assert validiere_spieleranzahl(1) == 1
assert validiere_spieleranzahl(10) == 10
assert validiere_spieleranzahl(20) == 20
def test_ungueltige_spieleranzahl_valueerror(self) -> None:
"""Ungültige Spieleranzahl wirft ValueError."""
with pytest.raises(ValueError):
validiere_spieleranzahl(0)
with pytest.raises(ValueError):
validiere_spieleranzahl(21)