F1: GUI-Login-Hash gegen $-Korruption gehaertet. .env-Werte einfach-gequotet
geschrieben + Sanity-Check nach Sourcing. Zusaetzlich systemd-Ebene:
podman escapt $->$$ in Environment=, systemd 252 kollabiert das nicht ->
sed-Post-Processing der Unit-Dateien. Live-.env: GUI-Passwort rotiert.
F2: Explizite Dependent-Deletes gegen FK-500 (delete_scenario/loan), 409 bei
bestaetigtem Import-Delete. Test-Engine erzwingt PRAGMA foreign_keys=ON.
F3: Enum/Range-Validierung (Literal + Field ge/le) fuer Recurring, Loan,
Modifier inkl. PATCH-Pfade.
F4: Confirm nur fuer draft-Statements (sonst 409) + Dedup-Re-Check gegen
bestaetigte Buchungen vor dem Promoten.
F5: v_balance_history/v_balance_total addieren pro Konto den opening_balance
des fruehesten bestaetigten Statements (approved deviation vom Plan-SQL).
F6: patch_scenario Namenskollision -> 409; create_transaction unbekanntes
Konto -> 404 (statt 500).
F7: Generierte systemd-Unit-Dateien chmod 600 (enthalten Env-Secrets).
F8: Globaler htmx:responseError-Handler in base.html.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
94 lines
3.8 KiB
Python
94 lines
3.8 KiB
Python
"""Tests fuer die Final-Review-Findings F2, F3, F6."""
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
|
|
from app.models.tables import (ProjectionPoint, ProjectionResult, Scenario,
|
|
Statement, Transaction)
|
|
|
|
H = {"Authorization": "Bearer test-key"}
|
|
|
|
|
|
def _account(client, iban="DE01"):
|
|
return client.post("/api/accounts", headers=H, json={
|
|
"bank": "DKB", "iban": iban, "name": "G", "type": "giro"}).json()
|
|
|
|
|
|
# ----------------------------------------------------------------- F2 cascades
|
|
def test_delete_projected_scenario_removes_dependent_rows(client, db):
|
|
sc = client.post("/api/scenarios", headers=H, json={"name": "Basis"}).json()
|
|
sid = sc["id"]
|
|
r = client.post(f"/api/scenarios/{sid}/project", headers=H,
|
|
params={"horizon_days": 10, "start_date": "2026-07-15"})
|
|
assert r.status_code == 200
|
|
assert db.query(ProjectionPoint).filter_by(scenario_id=sid).count() > 0
|
|
assert db.get(ProjectionResult, sid) is not None
|
|
|
|
assert client.delete(f"/api/scenarios/{sid}", headers=H).status_code == 204
|
|
db.expire_all()
|
|
assert db.get(Scenario, sid) is None
|
|
assert db.query(ProjectionPoint).filter_by(scenario_id=sid).count() == 0
|
|
assert db.get(ProjectionResult, sid) is None
|
|
|
|
|
|
def test_delete_loan_assigned_to_scenario(client):
|
|
loan = client.post("/api/loans", headers=H, json={
|
|
"name": "K1", "principal": "5000.00", "annual_rate_pct": "6.0",
|
|
"term_months": 48, "payout_date": "2026-07-20"}).json()
|
|
sc = client.post("/api/scenarios", headers=H, json={"name": "Kredit"}).json()
|
|
assert client.post(f"/api/scenarios/{sc['id']}/loans/{loan['id']}",
|
|
headers=H).status_code == 204
|
|
assert client.delete(f"/api/loans/{loan['id']}", headers=H).status_code == 204
|
|
|
|
|
|
def test_delete_confirmed_import_returns_409(client, db):
|
|
acc = _account(client)
|
|
db.add(Statement(filename="s.pdf", bank="dkb", account_id=acc["id"],
|
|
status="confirmed"))
|
|
db.commit()
|
|
sid = db.query(Statement).one().id
|
|
assert client.delete(f"/api/imports/{sid}", headers=H).status_code == 409
|
|
|
|
|
|
# --------------------------------------------------------------- F3 validation
|
|
def test_recurring_invalid_rhythm_422(client):
|
|
r = client.post("/api/recurring", headers=H, json={
|
|
"name": "X", "amount": "-1.00", "rhythm": "weekly", "due_day": 1})
|
|
assert r.status_code == 422
|
|
|
|
|
|
def test_recurring_due_day_zero_422(client):
|
|
r = client.post("/api/recurring", headers=H, json={
|
|
"name": "X", "amount": "-1.00", "rhythm": "monthly", "due_day": 0})
|
|
assert r.status_code == 422
|
|
|
|
|
|
def test_loan_invalid_repayment_type_422(client):
|
|
r = client.post("/api/loans", headers=H, json={
|
|
"name": "K", "principal": "1000.00", "annual_rate_pct": "3.0",
|
|
"term_months": 12, "payout_date": "2026-08-01",
|
|
"repayment_type": "balloon"})
|
|
assert r.status_code == 422
|
|
|
|
|
|
def test_modifier_invalid_kind_422(client):
|
|
sc = client.post("/api/scenarios", headers=H, json={"name": "S"}).json()
|
|
r = client.post(f"/api/scenarios/{sc['id']}/modifiers", headers=H, json={
|
|
"target_type": "category", "target_id": 1, "kind": "double",
|
|
"value": "10"})
|
|
assert r.status_code == 422
|
|
|
|
|
|
# ------------------------------------------------------- F6 409/404 statt 500
|
|
def test_patch_scenario_rename_clash_409(client):
|
|
client.post("/api/scenarios", headers=H, json={"name": "A"})
|
|
b = client.post("/api/scenarios", headers=H, json={"name": "B"}).json()
|
|
r = client.patch(f"/api/scenarios/{b['id']}", headers=H, json={"name": "A"})
|
|
assert r.status_code == 409
|
|
|
|
|
|
def test_create_transaction_unknown_account_404(client):
|
|
r = client.post("/api/transactions", headers=H, json={
|
|
"account_id": 9999, "booking_date": "2026-07-01",
|
|
"amount": "-10.00", "purpose": "X", "counterparty": ""})
|
|
assert r.status_code == 404
|