Files
bin/finance/tests/test_gui.py

196 lines
7.4 KiB
Python

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