diff --git a/app/schemas.py b/app/schemas.py index 7b2190c..ac7f96c 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,5 +1,8 @@ +import re +from datetime import date from typing import Literal -from pydantic import BaseModel, Field + +from pydantic import BaseModel, Field, field_validator class EndpointCapabilities(BaseModel): @@ -127,8 +130,37 @@ class EmergencyRequest(BaseModel): device: DeviceInfo | None = None +# Format-Validierung der Notfall-Profilfelder (spiegelt die Frontend-Prüfung). +_PLZ_RE = re.compile(r"^\d{5}$") +_PHONE_RE = re.compile(r"^\+?[\d\s/().\-]{6,20}$") +_EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$") +_DATE_RE = re.compile(r"^(\d{2})\.(\d{2})\.(\d{4})$") + + +def _valid_german_date(v: str) -> bool: + m = _DATE_RE.match(v) + if not m: + return False + d, mo, y = int(m.group(1)), int(m.group(2)), int(m.group(3)) + if not (1900 <= y <= 2100): + return False + try: + date(y, mo, d) + except ValueError: + return False + return True + + +def _csv_all(v: str, rx: "re.Pattern") -> bool: + return all(rx.match(p.strip()) for p in v.split(",") if p.strip()) + + class AdminUserPrefsUpdate(BaseModel): - """Admin setzt Nutzer-Einstellungen: erlaubte Sprachen + Notfall-Profil.""" + """Admin setzt Nutzer-Einstellungen: erlaubte Sprachen + Notfall-Profil. + + Optionale Felder dürfen leer/None sein; nicht-leere Werte werden auf ihr + Format geprüft (serverseitige Härtung zusätzlich zur Frontend-Validierung). + """ allowed_languages: str | None = None # Notfall-Profil (in user.prefs gespeichert, fließt in die Notruf-Meldungen ein) full_name: str | None = None @@ -143,6 +175,41 @@ class AdminUserPrefsUpdate(BaseModel): emergency_phones: str | None = None # Kontakt-Telefon(e), CSV location_consent: bool | None = None # Standort im Notfall mitsenden? + @field_validator("postal_code") + @classmethod + def _check_plz(cls, v): + if v and not _PLZ_RE.match(v.strip()): + raise ValueError("PLZ muss genau 5 Ziffern sein") + return v + + @field_validator("phone_mobile", "phone_landline") + @classmethod + def _check_phone(cls, v): + if v and not _PHONE_RE.match(v.strip()): + raise ValueError("ungültige Telefonnummer") + return v + + @field_validator("birth_date") + @classmethod + def _check_date(cls, v): + if v and not _valid_german_date(v.strip()): + raise ValueError("Geburtsdatum muss TT.MM.JJJJ (gültiges Datum) sein") + return v + + @field_validator("emergency_contacts") + @classmethod + def _check_emails(cls, v): + if v and not _csv_all(v, _EMAIL_RE): + raise ValueError("Kontakt-E-Mail(s): ungültige Adresse") + return v + + @field_validator("emergency_phones") + @classmethod + def _check_phones_csv(cls, v): + if v and not _csv_all(v, _PHONE_RE): + raise ValueError("Kontakt-Telefon(e): ungültige Nummer") + return v + class UserAdminUpdate(BaseModel): """Admin schaltet Admin-Rechte für einen Nutzer ein/aus.""" diff --git a/tests/test_admin_prefs_validation.py b/tests/test_admin_prefs_validation.py new file mode 100644 index 0000000..a62c211 --- /dev/null +++ b/tests/test_admin_prefs_validation.py @@ -0,0 +1,36 @@ +import pytest +from pydantic import ValidationError + +from app.schemas import AdminUserPrefsUpdate + + +def test_valid_profile_passes(): + AdminUserPrefsUpdate( + full_name="Frieda Schlüter", postal_code="50670", + phone_mobile="+4915127747511", phone_landline="040 123456", + birth_date="14.02.1976", + emergency_contacts="a@b.de, c@d.com", + emergency_phones="+4915127747511, 040 12345", + location_consent=True, + ) + + +def test_empty_and_none_pass(): + # Leere/None-Felder gelten als konform (optional). + AdminUserPrefsUpdate(postal_code="", phone_mobile="", birth_date="", + emergency_contacts="", emergency_phones=None) + + +@pytest.mark.parametrize("field,value", [ + ("postal_code", "123"), + ("postal_code", "abcde"), + ("phone_mobile", "hallo"), + ("phone_landline", "xx"), + ("birth_date", "1976-02-14"), + ("birth_date", "31.02.2020"), + ("emergency_contacts", "a@b.de, nope"), + ("emergency_phones", "kein-telefon!!"), +]) +def test_invalid_rejected(field, value): + with pytest.raises(ValidationError): + AdminUserPrefsUpdate(**{field: value})