gwoe-antragspruefer/tests/test_feedback_endpoint.py

160 lines
6.1 KiB
Python
Raw Permalink Normal View History

"""Unit-Tests für /api/feedback — gemockter Gitea-Call.
Prüft:
- Issue-Body wird korrekt aus Eingaben + Audit-Trail zusammengebaut
- Endpoint antwortet mit issue_id und issue_url
- Rate-Limit-Decorator ist deklariert
- Kein Token 503
"""
import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
# Skip falls die App-Abhängigkeiten nicht importierbar sind
try:
from fastapi.testclient import TestClient
from app.main import app, _strip_html, _gitea_ensure_label
_HAS_APP = True
except ImportError:
_HAS_APP = False
pytestmark = pytest.mark.skipif(not _HAS_APP, reason="app.main not importable")
# ── _strip_html ──────────────────────────────────────────────────────────────
class TestStripHtml:
def test_removes_tags(self):
assert _strip_html("<b>hallo</b>", 200) == "hallo"
def test_max_len(self):
assert len(_strip_html("a" * 300, 100)) == 100
def test_empty(self):
assert _strip_html("", 200) == ""
def test_no_tags(self):
assert _strip_html("plain text", 200) == "plain text"
# ── /api/feedback Endpoint ───────────────────────────────────────────────────
class TestFeedbackEndpoint:
"""Smoke-Tests mit gemocktem httpx-Client + gemocktem gitea_token."""
@pytest.fixture(autouse=True)
def _mock_gitea(self):
"""Patches settings.gitea_token und httpx.AsyncClient."""
# settings.gitea_token setzen
with patch("app.main.settings") as mock_settings:
mock_settings.gitea_token = "fake-token-123"
mock_settings.gitea_api_url = "https://repo.example.com/api/v1"
mock_settings.gitea_repo_owner = "testowner"
mock_settings.gitea_repo_name = "testrepo"
# httpx.AsyncClient mocken
mock_resp_labels = MagicMock()
mock_resp_labels.status_code = 200
mock_resp_labels.json.return_value = [{"id": 7, "name": "feedback"}]
mock_resp_issue = MagicMock()
mock_resp_issue.status_code = 201
mock_resp_issue.json.return_value = {
"number": 42,
"html_url": "https://repo.example.com/testowner/testrepo/issues/42",
}
async_client = AsyncMock()
async_client.get.return_value = mock_resp_labels
async_client.post.return_value = mock_resp_issue
async_client.__aenter__ = AsyncMock(return_value=async_client)
async_client.__aexit__ = AsyncMock(return_value=False)
with patch("httpx.AsyncClient", return_value=async_client):
self._async_client = async_client
yield
def test_happy_path_returns_issue_url(self):
client = TestClient(app)
resp = client.post("/api/feedback", data={
"titel": "Test-Bug",
"beschreibung": "Etwas ist kaputt",
"url": "https://gwoe.toppyr.de/antrag/NRW-18/1234",
"drucksache": "NRW-18/1234",
"viewport": "1440x900",
"user_agent": "TestAgent/1.0",
"klicks_json": json.dumps([{"t": "2026-04-25T10:00:00Z", "el": "button.v2-nav-item", "txt": "Durchsuchen"}]),
"errors_json": json.dumps([]),
})
assert resp.status_code == 200
data = resp.json()
assert data["issue_id"] == 42
assert "issues/42" in data["issue_url"]
def test_issue_body_contains_drucksache(self):
"""Stellt sicher, dass die Drucksachen-Nummer im POST-Payload auftaucht."""
client = TestClient(app)
client.post("/api/feedback", data={
"titel": "Body-Check",
"beschreibung": "Details",
"drucksache": "BY-18/9999",
"klicks_json": "[]",
"errors_json": "[]",
})
# Zweiter Post-Call ist der Issue-Create-Call
calls = self._async_client.post.call_args_list
issue_call = next((c for c in calls if "/issues" in str(c)), None)
assert issue_call is not None
body_arg = issue_call.kwargs.get("json", {}).get("body", "")
assert "BY-18/9999" in body_arg
def test_missing_titel_returns_422(self):
client = TestClient(app)
resp = client.post("/api/feedback", data={
"beschreibung": "Ohne Titel",
"klicks_json": "[]",
"errors_json": "[]",
})
assert resp.status_code == 422
def test_missing_beschreibung_returns_422(self):
client = TestClient(app)
resp = client.post("/api/feedback", data={
"titel": "Ohne Beschreibung",
"klicks_json": "[]",
"errors_json": "[]",
})
assert resp.status_code == 422
def test_html_stripped_from_titel(self):
"""XSS im Titel wird entfernt."""
client = TestClient(app)
client.post("/api/feedback", data={
"titel": "<script>alert(1)</script>Bug",
"beschreibung": "XSS-Test",
"klicks_json": "[]",
"errors_json": "[]",
})
calls = self._async_client.post.call_args_list
issue_call = next((c for c in calls if "/issues" in str(c)), None)
if issue_call:
title_arg = issue_call.kwargs.get("json", {}).get("title", "")
assert "<script>" not in title_arg
def test_no_token_returns_503(self):
"""Ohne konfiguriertes Token gibt es 503."""
with patch("app.main.settings") as s:
s.gitea_token = ""
s.gitea_api_url = "https://repo.example.com/api/v1"
s.gitea_repo_owner = "testowner"
s.gitea_repo_name = "testrepo"
client = TestClient(app, raise_server_exceptions=False)
resp = client.post("/api/feedback", data={
"titel": "Test",
"beschreibung": "Kein Token",
"klicks_json": "[]",
"errors_json": "[]",
})
assert resp.status_code == 503