345 lines
13 KiB
Python
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 & 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
|