from datetime import date, timedelta from decimal import Decimal from app.models.tables import Account H = {"Authorization": "Bearer test-key"} def test_pages_require_login(client): for path in ("/", "/buchungen", "/import", "/planung", "/hilfe"): r = client.get(path, follow_redirects=False) assert r.status_code in (302, 303), path assert r.headers["location"] == "/login" def test_pages_render_after_login(client): client.post("/login", data={"username": "admin", "password": "geheim"}) for path in ("/", "/buchungen", "/import", "/planung", "/hilfe"): r = client.get(path) assert r.status_code == 200 assert "Finanzberatung" in r.text assert "Gebrauchsanleitung" in client.get("/hilfe").text def test_index_shows_account_anchor(client, db): client.post("/login", data={"username": "admin", "password": "geheim"}) with_anchor = Account(bank="dkb", iban="DE-GUI-1", name="Mit Anker", type="giro") without_anchor = Account(bank="vr", iban="DE-GUI-2", name="Ohne Anker", type="giro") db.add_all([with_anchor, without_anchor]) db.flush() with_anchor.anchor_date = date(2026, 6, 30) with_anchor.anchor_balance = Decimal("321.00") db.commit() r = client.get("/") assert r.status_code == 200 assert "Anker: 321.00" in r.text assert "30.06.2026" in r.text assert "kein Anker" in r.text # Inline-Pflegeformular fuer den Anker muss je Konto vorhanden sein. assert 'name="anchor_date"' in r.text assert 'name="anchor_balance"' in r.text def test_import_page_has_dropzone_and_list(client): client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get("/import") assert r.status_code == 200 assert 'id="dropzone"' in r.text assert 'id="imports"' in r.text def test_import_list_fragment_requires_login(client): r = client.get("/import/list", follow_redirects=False) assert r.status_code in (302, 303) def test_import_list_fragment_renders_after_login(client): client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get("/import/list") assert r.status_code == 200 assert "Noch keine Imports" in r.text def test_preview_fragment_shows_balance_ok_none_for_format_without_saldo( client, tmp_path, monkeypatch): # UX-Regel: Saldo-Status ist IMMER sichtbar, auch wenn das Format (hier # HVB-CSV) keine Saldodaten liefert - dreiwertig statt faelschlich "rot". monkeypatch.setenv("FB_INBOX_DIR", str(tmp_path / "inbox")) monkeypatch.setenv("FB_UPLOADS_DIR", str(tmp_path / "uploads")) from app.config import get_settings get_settings.cache_clear() from tests.test_import_api import _hvb_csv_bytes sid = client.post("/api/imports/upload", headers=H, files={"file": ("HVB_gui.csv", _hvb_csv_bytes(), "text/csv")} ).json()["id"] client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get(f"/import/{sid}/preview-fragment") assert r.status_code == 200 assert "Saldo-Prüfung: nicht verfügbar (Format ohne Saldodaten)" in r.text def test_preview_fragment_shows_balance_ok_true_for_vr_csv(client, tmp_path, monkeypatch): monkeypatch.setenv("FB_INBOX_DIR", str(tmp_path / "inbox")) monkeypatch.setenv("FB_UPLOADS_DIR", str(tmp_path / "uploads")) from app.config import get_settings get_settings.cache_clear() from tests.test_import_api import _vr_csv_bytes sid = client.post("/api/imports/upload", headers=H, files={"file": ("VR_gui.csv", _vr_csv_bytes(), "text/csv")} ).json()["id"] client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get(f"/import/{sid}/preview-fragment") assert r.status_code == 200 assert "Saldo plausibel" in r.text def test_version_visible(client): from pathlib import Path version = (Path(__file__).resolve().parent.parent / "VERSION").read_text().strip() r = client.get("/api/version", headers={"Authorization": "Bearer test-key"}) assert r.status_code == 200 and r.json()["version"] == version client.post("/login", data={"username": "admin", "password": "geheim"}) assert f"v{version}" in client.get("/").text def test_buchungen_pagination(client, db): from app.models.tables import Account, Transaction acc = Account(bank="dkb", iban="DE01", name="G", type="giro") db.add(acc) db.flush() base = date(2026, 1, 1) for i in range(1, 61): purpose = "MARKER51" if i == 51 else f"TX{i}" db.add(Transaction( account_id=acc.id, booking_date=base + timedelta(days=i), amount=Decimal("10.00"), purpose=purpose, counterparty="X", status="confirmed", dedup_hash=f"hash-{i}", )) db.commit() client.post("/login", data={"username": "admin", "password": "geheim"}) r1 = client.get("/buchungen") assert r1.status_code == 200 assert "Seite 1 von 2" in r1.text assert "Weiter" in r1.text assert "MARKER51" not in r1.text r2 = client.get("/buchungen?page=2") assert r2.status_code == 200 assert "Seite 2 von 2" in r2.text assert "Zurück" in r2.text assert "MARKER51" in r2.text def test_buchungen_empty_filter_fields_no_422(client): # HTML-Formulare senden bei nicht ausgefüllten Feldern leere Strings, nicht # "kein Parameter" — die GUI-Route muss das tolerant behandeln statt mit # einem rohen 422 zu scheitern. client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get( "/buchungen" "?account_id=2&date_from=&date_to=&category_id=&q=&status=confirmed" ) assert r.status_code == 200 def test_buchungen_filled_filters_scope_to_account(client, db): from app.models.tables import Account, Transaction acc_a = Account(bank="dkb", iban="DE01", name="Konto A", type="giro") acc_b = Account(bank="dkb", iban="DE02", name="Konto B", type="giro") db.add_all([acc_a, acc_b]) db.flush() db.add(Transaction( account_id=acc_a.id, booking_date=date(2026, 6, 15), amount=Decimal("10.00"), purpose="MARKER-A", counterparty="X", status="confirmed", dedup_hash="hash-a", )) db.add(Transaction( account_id=acc_b.id, booking_date=date(2026, 6, 15), amount=Decimal("20.00"), purpose="MARKER-B", counterparty="Y", status="confirmed", dedup_hash="hash-b", )) db.commit() client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get( f"/buchungen?account_id={acc_a.id}&date_from=2026-06-01&date_to=2026-06-30" ) assert r.status_code == 200 assert "MARKER-A" in r.text assert "MARKER-B" not in r.text def test_planning_page_has_all_sections(client): client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get("/planung") assert r.status_code == 200 for heading in ("Wiederkehrende Posten", "Einmalposten", "Kredite", "Szenarien"): assert heading in r.text def test_planning_page_shows_empty_suggestions_hint(client): # UX-Regel: das "Vorschläge aus Buchungen"-Fieldset ist immer sichtbar, # auch ohne Daten - statt komplett zu verschwinden zeigt es einen Hinweis. client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get("/planung") assert r.status_code == 200 assert "Vorschläge aus Buchungen" in r.text assert "Keine Vorschläge" in r.text