from datetime import datetime, timedelta, timezone import re from fastapi.testclient import TestClient from sqlalchemy import select from sqlalchemy.orm import Session from app.config import Settings from app.database import get_engine from app.main import create_app from app.models import SupportTicket def _csrf_from_html(html: str) -> str: match = re.search(r'name="csrf_token" value="([^"]+)"', html) assert match is not None return match.group(1) def _started_at_from_html(html: str) -> str: match = re.search(r'name="started_at" value="([^"]+)"', html) assert match is not None return match.group(1) def _build_admin_app(db_url: str) -> object: return create_app( Settings( APP_ENV="test", DB_URL=db_url, SESSION_SECRET="test-secret", COOKIE_SECURE=False, COOKIE_SAMESITE="lax", LOGIN_RATE_LIMIT_ATTEMPTS=5, LOGIN_RATE_LIMIT_WINDOW_MINUTES=15, BOOTSTRAP_ADMIN_EMAIL="admin@example.com", ) ) def test_public_footer_and_legal_pages_render(app): with TestClient(app) as client: response = client.get("/login") assert response.status_code == 200 assert 'href="/kontakt"' in response.text assert 'href="/impressum"' in response.text assert 'href="/datenschutz"' in response.text impressum = client.get("/impressum") assert impressum.status_code == 200 assert "Impressum" in impressum.text privacy = client.get("/datenschutz") assert privacy.status_code == 200 assert "Datenschutz" in privacy.text def test_contact_form_creates_ticket(monkeypatch, app): import app.main as main_module base_time = datetime(2026, 3, 22, 12, 0, tzinfo=timezone.utc) monkeypatch.setattr(main_module, "utc_now", lambda: base_time) with TestClient(app) as client: form = client.get("/kontakt") assert form.status_code == 200 csrf = _csrf_from_html(form.text) started_at = _started_at_from_html(form.text) monkeypatch.setattr(main_module, "utc_now", lambda: base_time + timedelta(seconds=5)) submit = client.post( "/kontakt", data={ "csrf_token": csrf, "started_at": started_at, "website": "", "category": "feature", "name": "Max Beispiel", "email": "max@example.com", "subject": "Bitte Monatsfilter erweitern", "message": "Ich wünsche mir eine bessere Filterung in der Monatsansicht.", }, follow_redirects=False, ) assert submit.status_code == 303 assert submit.headers["location"] == "/kontakt?msg=sent" with Session(get_engine()) as db: tickets = db.execute(select(SupportTicket)).scalars().all() assert len(tickets) == 1 assert tickets[0].category == "feature" assert tickets[0].status == "open" assert tickets[0].subject == "Bitte Monatsfilter erweitern" def test_contact_form_honeypot_blocks_submission(monkeypatch, app): import app.main as main_module base_time = datetime(2026, 3, 22, 12, 0, tzinfo=timezone.utc) monkeypatch.setattr(main_module, "utc_now", lambda: base_time) with TestClient(app) as client: form = client.get("/kontakt") csrf = _csrf_from_html(form.text) started_at = _started_at_from_html(form.text) monkeypatch.setattr(main_module, "utc_now", lambda: base_time + timedelta(seconds=5)) submit = client.post( "/kontakt", data={ "csrf_token": csrf, "started_at": started_at, "website": "spam", "category": "problem", "name": "", "email": "spam@example.com", "subject": "Spamversuch", "message": "Das sollte blockiert werden.", }, ) assert submit.status_code == 429 assert "nicht versendet" in submit.text with Session(get_engine()) as db: tickets = db.execute(select(SupportTicket)).scalars().all() assert tickets == [] def test_admin_can_manage_legal_content_and_tickets(tmp_path, monkeypatch): import app.main as main_module db_path = tmp_path / "legal-support.db" app = _build_admin_app(f"sqlite:///{db_path}") base_time = datetime(2026, 3, 22, 12, 0, tzinfo=timezone.utc) monkeypatch.setattr(main_module, "utc_now", lambda: base_time) with TestClient(app) as admin_client: register = admin_client.post( "/auth/register", json={"email": "admin@example.com", "password": "verystrongPass123"}, ) assert register.status_code == 200 csrf = register.json()["csrf_token"] update_legal = admin_client.post( "/settings/admin/site-content", data={ "csrf_token": csrf, "impressum_markdown": "# Impressum\n\n**Stage Test**", "privacy_markdown": "# Datenschutz\n\nBitte Datenschutz beachten.", }, follow_redirects=False, ) assert update_legal.status_code == 303 assert update_legal.headers["location"] == "/settings?tab=admin&msg=site_content_updated" impressum = admin_client.get("/impressum") assert impressum.status_code == 200 assert "Stage Test" in impressum.text form = admin_client.get("/kontakt") started_at = _started_at_from_html(form.text) monkeypatch.setattr(main_module, "utc_now", lambda: base_time + timedelta(seconds=5)) submit = admin_client.post( "/kontakt", data={ "csrf_token": csrf, "started_at": started_at, "website": "", "category": "problem", "name": "Admin Test", "email": "admin@example.com", "subject": "Ticket bitte schließen", "message": "Dieses Ticket wird direkt im Adminbereich geschlossen.", }, follow_redirects=False, ) assert submit.status_code == 303 with Session(get_engine()) as db: ticket = db.execute(select(SupportTicket).where(SupportTicket.subject == "Ticket bitte schließen")).scalar_one() ticket_id = ticket.id with TestClient(app) as admin_client: login = admin_client.post( "/login", data={ "email": "admin@example.com", "password": "verystrongPass123", "csrf_token": _csrf_from_html(admin_client.get("/login").text), }, follow_redirects=False, ) assert login.status_code == 303 settings_page = admin_client.get("/settings?tab=admin") settings_csrf = _csrf_from_html(settings_page.text) update_ticket = admin_client.post( f"/settings/admin/tickets/{ticket_id}", data={ "csrf_token": settings_csrf, "status": "closed", "admin_notes": "Geschlossen im Test", }, follow_redirects=False, ) assert update_ticket.status_code == 303 assert update_ticket.headers["location"] == "/settings?tab=admin&msg=ticket_updated" with Session(get_engine()) as db: ticket = db.get(SupportTicket, ticket_id) assert ticket is not None assert ticket.status == "closed" assert ticket.admin_notes == "Geschlossen im Test" assert ticket.closed_at is not None