Files
bin/finance/tests/test_aufteilung_gui.py
T

345 lines
13 KiB
Python

import re
from datetime import date
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
from app.engine.loans import add_months
from app.models.tables import (Category, Loan, RecurringItem, Scenario,
ScenarioModifier)
D = Decimal
MONAT = "2030-03"
def _login(client):
client.post("/login", data={"username": "admin", "password": "geheim"})
def _daten(db, gehalt="3000.00"):
wohnen, eink, vers = Category(name="Wohnen"), Category(name="Einkommen"), Category(name="Versicherung")
db.add_all([wohnen, eink, vers])
db.flush()
db.add_all([
RecurringItem(name="Gehalt", amount=D(gehalt), rhythm="monthly", due_day=1, category_id=eink.id),
RecurringItem(name="Miete", amount=D("-1000.00"), rhythm="monthly", due_day=1, category_id=wohnen.id),
RecurringItem(name="Police", amount=D("-1200.00"), rhythm="yearly", due_day=1, category_id=vers.id),
])
db.commit()
return wohnen
def _get(client, **params):
return client.get("/aufteilung", params=params)
def test_menuepunkt(client):
_login(client)
assert '<a href="/aufteilung">Aufteilung</a>' in client.get("/").text
def test_standardauswahl(client):
_login(client)
r = _get(client)
assert r.status_code == 200
assert f'value="{date.today().strftime("%Y-%m")}"' in r.text
assert re.search(r'<option value="">System-Plan</option>', r.text)
assert len(re.findall(r'type="checkbox"[^>]*checked', r.text)) == 3
def test_tabelle_kennzahlen_torte(client, db):
_login(client)
_daten(db)
t = _get(client, monat=MONAT).text
assert "03.2030" in t
assert "Fixposten (jährlich)" in t
assert "-100,00 €" in t # Police 1.200 / 12
assert "33,3 %" in t # Miete 1.000 von 3.000
assert "Gesamte Einkünfte: <strong>3.000,00 €</strong>" in t
tfoot = re.search(r"<tfoot>(.*?)</tfoot>", t, re.S).group(1)
assert "Frei verfügbar" in tfoot and "1.900,00 €" in tfoot and "63,3 %" in tfoot
assert '<svg class="torte-svg"' in t
# Präfix ohne schließendes Anführungszeichen: ab Task 6 folgt „ klickbar".
assert 'class="seg s1' in t and 'class="seg frei"' in t
assert "Anteile an den Einkünften" in t
def test_einnahmezeile_ohne_prozent(client, db):
_login(client)
_daten(db)
t = _get(client, monat=MONAT).text
zeile = re.search(r"<tr>\s*<td[^>]*>Gehalt</td>.*?</tr>", t, re.S).group(0)
assert "%" not in zeile
def test_alle_filter_aus_zeigt_keine_posten(client, db):
_login(client)
_daten(db)
t = _get(client, monat=MONAT, gesendet="1").text
assert "Für diese Auswahl gibt es keine Posten." in t
assert '<form method="get" action="/aufteilung"' in t
assert not re.findall(r'type="checkbox"[^>]*checked', t)
def test_unterdeckung(client, db):
_login(client)
_daten(db, gehalt="500.00")
t = _get(client, monat=MONAT).text
assert "Kosten übersteigen die Einkünfte um 600,00 €" in t
assert "Anteile an den Gesamtkosten" in t
assert 'class="seg frei"' not in t
tfoot = re.search(r"<tfoot>(.*?)</tfoot>", t, re.S).group(1)
assert "Unterdeckung" in tfoot and "-600,00 €" in tfoot
def test_szenario_mit_modifikator(client, db):
_login(client)
wohnen = _daten(db)
sc = Scenario(name="Sparen", description="")
db.add(sc)
db.flush()
db.add(ScenarioModifier(scenario_id=sc.id, target_type="category", target_id=wohnen.id,
kind="percent", value=D("50")))
db.commit()
t = _get(client, monat=MONAT, szenario=str(sc.id)).text
assert "-500,00 €" in t and "Sparen" in t
assert f'<option value="{sc.id}" selected>Sparen</option>' in t
def test_szenario_vergangenheit_fehler_inline(client, db):
_login(client)
_daten(db)
sc = Scenario(name="S", description="")
db.add(sc)
db.commit()
vormonat = add_months(date.today().replace(day=1), -1).strftime("%Y-%m")
r = _get(client, monat=vormonat, szenario=str(sc.id))
assert r.status_code == 200
assert "Szenarien sind erst ab dem laufenden Monat wählbar" in r.text
assert '<form method="get" action="/aufteilung"' in r.text
assert "<table" not in r.text
def test_system_plan_in_vergangenheit_erlaubt(client, db):
_login(client)
_daten(db)
r = _get(client, monat="2020-01")
assert r.status_code == 200 and "<table" in r.text
def test_ungueltiger_monat_und_unbekanntes_szenario(client):
_login(client)
for params, text in (({"monat": "2030-13"}, "Ungültiger Monat"),
({"monat": "abc"}, "Ungültiger Monat"),
({"monat": "0000-01"}, "Ungültiger Monat"),
({"monat": MONAT, "szenario": "²"}, "Unbekanntes Szenario"),
({"monat": MONAT, "szenario": "999"}, "Unbekanntes Szenario"),
({"monat": MONAT, "szenario": "x"}, "Unbekanntes Szenario"),
({"monat": MONAT, "szenario": "9" * 30}, "Unbekanntes Szenario")):
r = _get(client, **params)
assert r.status_code == 200 and text in r.text
assert '<form method="get" action="/aufteilung"' in r.text
def test_doppelzaehlungs_hinweis(client, db):
_login(client)
_daten(db)
db.add_all([
RecurringItem(name="Autorate", amount=D("-100.00"), rhythm="monthly", due_day=15),
Loan(name="Autokredit", principal=D("1200.00"), annual_rate_pct=D("0"), term_months=12,
payout_date=date(2030, 1, 15), repayment_type="annuity"),
])
db.commit()
assert "⚠ Fixposten „Autorate“ zählt evtl. doppelt mit Kredit „Autokredit“" in _get(client, monat=MONAT).text
assert "⚠" not in _get(client, monat=MONAT, gesendet="1", fixposten="1", einmal="1").text
def test_tabelle_scrollbar_torte_darunter(client, db):
_login(client)
_daten(db)
t = _get(client, monat=MONAT).text
assert re.search(r'<div class="tabelle-scroll">\s*<table', t)
assert t.index('id="aufteilung-torte"') > t.index("</table>")
def _drill(client, pfad="/aufteilung/kategorie", **params):
return client.get(pfad, params={"monat": MONAT, **params})
def test_hinweistext_und_segment_links(client, db):
_login(client)
_daten(db)
t = _get(client, monat=MONAT).text
assert "Für Kategorie-Aufteilung auf Segment klicken" in t
assert 'hx-get="/aufteilung/kategorie?' in t
assert "kategorie=Wohnen" in t
assert 'hx-target="#aufteilung-kategorie"' in t
assert '<div id="aufteilung-kategorie">' in t
def test_frei_verfuegbar_ohne_link(client, db):
_login(client)
_daten(db)
t = _get(client, monat=MONAT).text
assert "kategorie=Frei" not in t
frei = re.search(r'<path[^>]*class="seg frei[^"]*"[^>]*>', t).group(0)
assert "hx-get" not in frei
def test_fragment_zweite_torte_und_oob(client, db):
_login(client)
_daten(db)
r = _drill(client, kategorie="Wohnen")
assert r.status_code == 200
assert "Wohnen: 1.000,00 € / Monat" in r.text
assert "Miete" in r.text and "33,3 % der Einkünfte" in r.text
assert '<div id="aufteilung-torte" hx-swap-oob="true">' in r.text
assert "gewaehlt" in r.text
assert "<html" not in r.text
def test_seite_mit_kategorie_zeigt_zweite_torte(client, db):
_login(client)
_daten(db)
t = _get(client, monat=MONAT, kategorie="Versicherung").text
assert "Versicherung: 100,00 € / Monat" in t
assert 'hx-swap-oob' not in t
def test_unbekannte_kategorie_hinweis(client, db):
_login(client)
_daten(db)
for pfad in ("/aufteilung", "/aufteilung/kategorie"):
r = _drill(client, pfad, kategorie="Gibtsnicht")
assert r.status_code == 200
assert "Kategorie „Gibtsnicht“ hat in dieser Auswahl keine Kosten." in r.text
def test_fragment_bei_fehler_nur_hinweis(client):
_login(client)
r = client.get("/aufteilung/kategorie", params={"monat": "2030-13", "kategorie": "X"})
assert r.status_code == 200
assert "Ungültiger Monat" in r.text and "hx-swap-oob" not in r.text
def _viele_kategorien(db):
eink = Category(name="Einkommen")
kleine = [Category(name="Klein A"), Category(name="Klein B")]
gross = Category(name="Groß")
db.add_all([eink, gross, *kleine])
db.flush()
db.add_all([
RecurringItem(name="Gehalt", amount=D("3000.00"), rhythm="monthly", due_day=1, category_id=eink.id),
RecurringItem(name="Posten G", amount=D("-1000.00"), rhythm="monthly", due_day=1, category_id=gross.id),
RecurringItem(name="Posten A", amount=D("-10.00"), rhythm="monthly", due_day=1, category_id=kleine[0].id),
RecurringItem(name="Posten B", amount=D("-20.00"), rhythm="monthly", due_day=1, category_id=kleine[1].id),
])
db.commit()
def test_sammelgruppe_uebrige(client, db):
_login(client)
_viele_kategorien(db)
t = _get(client, monat=MONAT).text
assert "sammel=1" in t
r = _drill(client, sammel="1")
assert "Übrige: 30,00 € / Monat" in r.text
assert "Enthält: Klein B, Klein A" in r.text
assert "Posten A" in r.text and "Posten B" in r.text
def test_echte_kategorie_uebrige_nicht_verwechselt(client, db):
_login(client)
_viele_kategorien(db)
echte = Category(name="Übrige")
db.add(echte)
db.flush()
db.add(RecurringItem(name="Echt", amount=D("-500.00"), rhythm="monthly", due_day=1,
category_id=echte.id))
db.commit()
r = _drill(client, kategorie="Übrige")
assert "Übrige: 500,00 € / Monat" in r.text
assert "Echt" in r.text and "Posten A" not in r.text
r = _drill(client, sammel="1")
assert "Übrige: 30,00 € / Monat" in r.text and "Echt" not in r.text
def test_drilldown_sonderzeichen(client, db):
_login(client)
eink, haus = Category(name="Einkommen"), Category(name="Haus & Garten")
db.add_all([eink, haus])
db.flush()
db.add_all([
RecurringItem(name="Gehalt", amount=D("3000.00"), rhythm="monthly", due_day=1, category_id=eink.id),
RecurringItem(name="Rasen", amount=D("-300.00"), rhythm="monthly", due_day=1, category_id=haus.id),
])
db.commit()
t = _get(client, monat=MONAT).text
assert "kategorie=Haus+%26+Garten" in t
r = _drill(client, kategorie="Haus & Garten")
assert "Haus &amp; Garten: 300,00 € / Monat" in r.text and "Rasen" in r.text
def test_monat_vor_jahr_1000_vierstellig(client, db):
_login(client)
eink, wohnen = Category(name="Einkommen"), Category(name="Wohnen")
db.add_all([eink, wohnen])
db.flush()
db.add_all([
RecurringItem(name="Gehalt", amount=D("3000.00"), rhythm="monthly", due_day=1, category_id=eink.id),
RecurringItem(name="Miete", amount=D("-1000.00"), rhythm="monthly", due_day=1, category_id=wohnen.id),
])
db.commit()
r = _get(client, monat="0001-01")
assert r.status_code == 200
assert 'value="0001-01"' in r.text
assert "01.0001" in r.text
assert "monat=0001-01" in r.text
def test_uebrige_mitglieder_in_legende_der_zweiten_torte(client, db):
_login(client)
eink, kat = Category(name="Einkommen"), Category(name="Vielfalt")
db.add_all([eink, kat])
db.flush()
db.add(RecurringItem(name="Gehalt", amount=D("3000.00"), rhythm="monthly", due_day=1, category_id=eink.id))
db.add(RecurringItem(name="Großposten", amount=D("-1000.00"), rhythm="monthly", due_day=1, category_id=kat.id))
for i in range(10):
db.add(RecurringItem(name=f"Kleinposten {i}", amount=D("-5.00"), rhythm="monthly",
due_day=1, category_id=kat.id))
db.commit()
t = _drill(client, kategorie="Vielfalt").text
legende = re.search(r'<ul class="legende">(.*?)</ul>', t, re.S).group(1)
assert 'class="legende-zusatz"' in legende
assert "Kleinposten 3" in legende
CSS = (Path(__file__).resolve().parent.parent / "app" / "static" / "style.css").read_text()
def test_nav_bricht_um():
nav = re.search(r"(?m)^nav\s*\{([^}]*)\}", CSS).group(1)
assert re.search(r"flex-wrap:\s*wrap", nav)
def test_legende_zusatz_nicht_fett_und_figcaption_bleibt_fett():
assert re.search(r"\.legende-zusatz\s*\{[^}]*font-weight:\s*normal", CSS)
# Nutzerwunsch A14: Bildunterschrift der Torte bleibt fett.
assert re.search(r"\.torte figcaption\s*\{[^}]*font-weight:\s*bold", CSS)
def test_gewaehltes_segment_zuletzt_gezeichnet():
from app.routers.gui import templates
def seg(label, gewaehlt):
s = SimpleNamespace(path=f"M0 0 L{label}", color="s1", label=label, merged=False, members=[],
value=Decimal("1.00"), share=Decimal("50.0"))
return {"s": s, "q": None, "gewaehlt": gewaehlt, "zusatz": None}
torte = templates.env.get_template("_torte_macros.html").module.torte
html = str(torte([seg("Alpha", True), seg("Beta", False)], "Titel"))
svg = html.split("<svg", 1)[1].split("</svg>", 1)[0]
assert svg.index("M0 0 LBeta") < svg.index("M0 0 LAlpha")
legende = html.split('<ul class="legende">', 1)[1]
assert legende.index("Alpha") < legende.index("Beta") # Legende behält Reihenfolge