92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
|
|
"""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("") == ""
|