fix: Neue Tabellen ins Backup, toter Transfer-Code raus
Sieben nutzereigene Tabellen aus den Migrationen 0015-0020 standen in keiner Backup-Liste. Am schwersten wiegt inbox_items: Der Eingang haelt fest, welche Zahlungen von aussen schon erfasst wurden. Fehlt er nach dem Wiedereinspielen, erkennt der naechste Kontoauszug sie nicht wieder und bucht sie ein zweites Mal – genau der Fehler, den die Tabelle verhindern soll. Aufgenommen: inbox_items, transfer_links, link_ignored, api_tokens. Bewusst nicht: mail_sources/mail_senders (tragen das verschluesselte IMAP-Passwort; ein Backup ist eine Datei, die man herumreicht) und pending_uploads (fluechtige Rohdateien). Beides steht begruendet im Code. Ein Test laeuft jetzt ueber Base.registry und fordert fuer jedes Modell mit owner_id eine bewusste Entscheidung – sichern oder begruendet ausnehmen. Genau diese Pruefung fehlte, weshalb die sieben unbemerkt durchrutschten. Dazu der tote Code aus dem Transfer-Ausbau: is_counterpart und braucht_ziel waren feste False, _apply_counterpart_amount damit unerreichbar. Die Tests sichern dieselbe Garantie jetzt ueber das tatsaechliche Verhalten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,3 +52,37 @@ def test_db_backup_admin_only(admin):
|
||||
assert other.get("/api/backup/db").status_code == 403
|
||||
# ...aber seinen eigenen Export schon (leer).
|
||||
assert other.get("/api/export/all.json").status_code == 200
|
||||
|
||||
|
||||
def test_jede_nutzertabelle_ist_im_backup_beruecksichtigt():
|
||||
"""Neue Tabellen dürfen nicht stillschweigend aus dem Backup fallen.
|
||||
|
||||
Sieben Tabellen sind genau so entstanden: angelegt, migriert, benutzt – und
|
||||
in ``backup_data`` nie eingetragen. Aufgefallen wäre das erst beim
|
||||
Wiedereinspielen, also im schlechtesten Moment. Dieser Test zwingt jede
|
||||
künftige Tabelle mit ``owner_id`` zu einer bewussten Entscheidung: entweder
|
||||
sichern oder hier begründet ausnehmen.
|
||||
"""
|
||||
from app.models import Base
|
||||
from app.services import backup_data
|
||||
|
||||
gesichert = {
|
||||
spec.model.__tablename__
|
||||
for liste in (backup_data.USER_TABLES, backup_data.SYSTEM_USER_TABLES,
|
||||
backup_data.GLOBAL_TABLES)
|
||||
for spec in liste
|
||||
}
|
||||
# Bewusst ausgenommen – die Begründung steht in backup_data.py.
|
||||
ausgenommen = {"mail_sources", "mail_senders", "pending_uploads"}
|
||||
|
||||
fehlend = sorted(
|
||||
m.class_.__tablename__
|
||||
for m in Base.registry.mappers
|
||||
if "owner_id" in m.class_.__table__.columns
|
||||
and m.class_.__tablename__ not in gesichert
|
||||
and m.class_.__tablename__ not in ausgenommen
|
||||
)
|
||||
assert not fehlend, (
|
||||
f"Diese Tabellen gehören dem Nutzer, stehen aber in keiner Backup-Liste: "
|
||||
f"{fehlend}. Entweder in USER_TABLES aufnehmen oder oben begründet ausnehmen."
|
||||
)
|
||||
|
||||
@@ -341,7 +341,8 @@ def test_umtausch_wird_als_ausgabe_und_einnahme_gebucht(admin):
|
||||
# Der EUR-Auszug bringt den Zugang – als Einnahme, nicht als Gegenbuchung.
|
||||
rows2 = _rev_preview(admin, eur["id"], EUR_UMTAUSCH)
|
||||
assert rows2[0]["type"] == "income"
|
||||
assert rows2[0]["is_counterpart"] is False
|
||||
assert rows2[0]["type"] != "transfer" # keine Kontoverknuepfung mehr
|
||||
assert rows2[0]["include"] is True
|
||||
assert rows2[0]["include"] is True
|
||||
r = admin.post("/api/imports/commit", json={"account_id": eur["id"], "filename": "eur.csv", "rows": rows2})
|
||||
assert r.json()["imported_count"] == 1
|
||||
@@ -367,7 +368,8 @@ def test_uebertrags_schlagwort_setzt_neutrale_kategorie(admin):
|
||||
rows = _rev_preview(admin, chf["id"], CHF_UMTAUSCH)
|
||||
assert rows[0]["type"] == "expense"
|
||||
assert rows[0]["category_id"] == neutral["id"]
|
||||
assert rows[0]["needs_target"] is False
|
||||
assert rows[0]["type"] != "transfer"
|
||||
assert rows[0]["include"] is True
|
||||
|
||||
|
||||
def test_expense_is_not_swallowed_by_incoming_transfer(admin):
|
||||
@@ -399,7 +401,7 @@ def test_expense_is_not_swallowed_by_incoming_transfer(admin):
|
||||
).json()["rows"]
|
||||
|
||||
# Keine Zeile gilt mehr als Gegenbuchung – beide werden gebucht.
|
||||
assert all(r["is_counterpart"] is False for r in rows)
|
||||
assert all(r["type"] != "transfer" for r in rows)
|
||||
assert all(r["include"] is True for r in rows)
|
||||
assert rows[0]["type"] == "expense"
|
||||
assert rows[1]["type"] == "income"
|
||||
@@ -512,7 +514,7 @@ def test_keine_zeile_bleibt_wegen_fehlendem_gegenkonto_liegen(admin):
|
||||
},
|
||||
).json()
|
||||
|
||||
assert not [r for r in res["rows"] if r.get("needs_target")]
|
||||
assert all(r["type"] != "transfer" for r in res["rows"])
|
||||
uebertrag = res["rows"][1]
|
||||
assert uebertrag["type"] == "income" and uebertrag["include"] is True
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ def test_import_verbindet_keine_konten_mehr(admin):
|
||||
rows = _preview_csv(admin, giro["id"], csv, COLMAP)
|
||||
assert [r["type"] for r in rows] == ["expense", "expense", "income"]
|
||||
assert all(r["to_account_id"] is None for r in rows)
|
||||
assert all(r["needs_target"] is False for r in rows)
|
||||
assert all(r["type"] != "transfer" for r in rows)
|
||||
|
||||
|
||||
def test_beide_auszuege_werden_beide_gebucht(admin):
|
||||
@@ -276,7 +276,8 @@ def test_beide_auszuege_werden_beide_gebucht(admin):
|
||||
rowsB = _preview_csv(admin, b["id"],
|
||||
"Datum;Betrag;Empfaenger;Zweck\n07.03.2026;100,00;Gutschrift;von KontoA\n", COLMAP)
|
||||
assert rowsB[0]["type"] == "income"
|
||||
assert rowsB[0]["is_counterpart"] is False
|
||||
assert rowsB[0]["type"] != "transfer"
|
||||
assert rowsB[0]["include"] is True
|
||||
assert rowsB[0]["include"] is True
|
||||
r = admin.post("/api/imports/commit", json={"account_id": b["id"], "filename": "b.csv", "rows": rowsB})
|
||||
assert r.json()["imported_count"] == 1
|
||||
|
||||
Reference in New Issue
Block a user