gwoe-antragspruefer/tests/test_main_validators.py

92 lines
3.5 KiB
Python
Raw Permalink Normal View History

Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59) Drei verbleibende Audit-Befunde aus #57 in einem Patch: - **#57.3 MEDIUM** Drucksache-Regex-Validation: neue app/validators.py mit validate_drucksache() als gemeinsamer Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu), 23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus Standard-Injection (;, <, &). Drei Endpoints durchgeschleust: /api/assessment, /api/assessment/pdf, /api/analyze-drucksache. - **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte werden NICHT mehr als Volltext geloggt — neue Helper _content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur Forensik ohne Antrag-Inhalte ins Container-Log zu leaken. basicConfig() mit ISO-Format setzt strukturiertes Logging früh, damit logger.exception() auch beim Boot greift. - **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory- Regel "stille excepts in Adaptern" ab). Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache- Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen- Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu). Validators in eigenem Modul (app/validators.py), damit Tests sie ohne slowapi-Dependency direkt importieren können. Refs: #57, #59 (Phase A) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:15:16 +02:00
"""Tests für die FastAPI-Validation-Helper aus app.main.
Issue #57 Befund #3 (Path-Traversal via drucksache) und #7 (Query-DoS).
Wir importieren die Helper direkt keine TestClient-Roundtrip nötig,
weil die Funktionen reine Validierung sind und die FastAPI-Integration
in app.main den richtigen Aufrufpfad bereits vorgibt.
"""
from __future__ import annotations
import pytest
from fastapi import HTTPException
from app.validators import (
MAX_SEARCH_QUERY_LEN,
validate_drucksache,
validate_search_query,
)
# ─────────────────────────────────────────────────────────────────────────────
# validate_drucksache (Befund #3 — Path-Traversal-Härtung)
# ─────────────────────────────────────────────────────────────────────────────
class TestValidateDrucksache:
@pytest.mark.parametrize(
"valid",
[
"8/6390",
"18/12345",
"23/3700",
"20/4309",
"8/6390(neu)",
"23/3700-A",
],
)
def test_accepts_known_real_formats(self, valid):
assert validate_drucksache(valid) == valid
@pytest.mark.parametrize(
"evil",
[
"../../etc/passwd",
"8/6390/../../../etc/shadow",
"8/6390;rm -rf /",
"8/6390?bundesland=NRW",
"<script>alert(1)</script>",
"8/6390 OR 1=1",
"",
"/",
"8//6390",
"abc/def",
"8/abc",
],
)
def test_rejects_path_traversal_and_injection(self, evil):
with pytest.raises(HTTPException) as exc:
validate_drucksache(evil)
assert exc.value.status_code == 400
# ─────────────────────────────────────────────────────────────────────────────
# validate_search_query (Befund #7 — Query-Length-DoS)
# ─────────────────────────────────────────────────────────────────────────────
class TestValidateSearchQuery:
def test_short_query_passes(self):
assert validate_search_query("Klimaschutz") == "Klimaschutz"
def test_at_limit_passes(self):
q = "x" * MAX_SEARCH_QUERY_LEN
assert validate_search_query(q) == q
def test_over_limit_rejected(self):
q = "x" * (MAX_SEARCH_QUERY_LEN + 1)
with pytest.raises(HTTPException) as exc:
validate_search_query(q)
assert exc.value.status_code == 400
assert "zu lang" in exc.value.detail.lower()
def test_none_query_rejected(self):
with pytest.raises(HTTPException) as exc:
validate_search_query(None) # type: ignore[arg-type]
assert exc.value.status_code == 400
def test_empty_string_passes(self):
# Eine leere Query ist semantisch ok (Frontend nutzt sie für
# "alle Drucksachen") — die Längen-Validierung soll nur den
# absurd großen Fall blocken, nicht den leeren.
assert validate_search_query("") == ""