fix: Eine Sicherung mit Transfer-Schlagwort laesst sich wieder einspielen
Das feste Zielkonto eines Transfer-Schlagworts behielt beim Wiederherstellen die alte Konto-Nummer. Die Nummern werden dabei aber neu vergeben, also lief es ins Leere – und zwar so, dass die GANZE Wiederherstellung mit einem Fremdschluessel-Fehler abbrach, nicht nur diese eine Zeile. Wer je ein Zielkonto gewaehlt hatte, kam an seine Sicherung nicht mehr heran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e8c18adafb
commit
a9ea0432d8
@@ -55,7 +55,9 @@ from ..models import (
|
|||||||
LinkIgnored,
|
LinkIgnored,
|
||||||
PlannedIncomeLine,
|
PlannedIncomeLine,
|
||||||
RecoveryCode,
|
RecoveryCode,
|
||||||
|
Tag,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
TransactionTag,
|
||||||
TransferLink,
|
TransferLink,
|
||||||
User,
|
User,
|
||||||
UserSetting,
|
UserSetting,
|
||||||
@@ -135,8 +137,14 @@ USER_TABLES: list[TableSpec] = [
|
|||||||
{"budget_period_id": "budget_periods", "category_id": "categories"},
|
{"budget_period_id": "budget_periods", "category_id": "categories"},
|
||||||
label="Budget-Monatswerte"),
|
label="Budget-Monatswerte"),
|
||||||
TableSpec("dashboard_layouts", DashboardLayout, "owner", label="Dashboard-Layout"),
|
TableSpec("dashboard_layouts", DashboardLayout, "owner", label="Dashboard-Layout"),
|
||||||
|
# target_account_id gehört dazu: Ein Transfer-Schlagwort mit festem
|
||||||
|
# Zielkonto trug sonst die ALTE Konto-Nummer in die neue Datenbank, und die
|
||||||
|
# Wiederherstellung brach mit einem Fremdschlüssel-Fehler ab – nicht nur
|
||||||
|
# diese Zeile, die ganze Sicherung liess sich nicht mehr einspielen.
|
||||||
|
# accounts steht weiter oben, die Übersetzung greift also.
|
||||||
TableSpec("categorization_rules", CategorizationRule, "owner",
|
TableSpec("categorization_rules", CategorizationRule, "owner",
|
||||||
{"category_id": "categories"}, label="Schlagwortregeln"),
|
{"category_id": "categories", "target_account_id": "accounts"},
|
||||||
|
label="Schlagwortregeln"),
|
||||||
TableSpec("user_settings", UserSetting, "user", has_id=False,
|
TableSpec("user_settings", UserSetting, "user", has_id=False,
|
||||||
label="Persönliche Einstellungen"),
|
label="Persönliche Einstellungen"),
|
||||||
# Der Eingang MUSS mitgesichert werden: Er hält fest, welche Zahlungen
|
# Der Eingang MUSS mitgesichert werden: Er hält fest, welche Zahlungen
|
||||||
@@ -154,6 +162,12 @@ USER_TABLES: list[TableSpec] = [
|
|||||||
TableSpec("link_ignored", LinkIgnored, "owner",
|
TableSpec("link_ignored", LinkIgnored, "owner",
|
||||||
{"a_txn_id": "transactions", "b_txn_id": "transactions"},
|
{"a_txn_id": "transactions", "b_txn_id": "transactions"},
|
||||||
label="Abgelehnte Übertrags-Vorschläge"),
|
label="Abgelehnte Übertrags-Vorschläge"),
|
||||||
|
# Erst die Tags, dann die Zuordnung – ein refs-Verweis darf nur auf eine
|
||||||
|
# schon eingefügte Tabelle zeigen.
|
||||||
|
TableSpec("tags", Tag, "owner", label="Tags"),
|
||||||
|
TableSpec("transaction_tags", TransactionTag, "owner",
|
||||||
|
{"transaction_id": "transactions", "tag_id": "tags"},
|
||||||
|
label="Tag-Zuordnungen"),
|
||||||
# Ohne die Geräteschlüssel hört der Kurzbefehl auf dem Telefon nach einem
|
# Ohne die Geräteschlüssel hört der Kurzbefehl auf dem Telefon nach einem
|
||||||
# Wiedereinspielen stillschweigend auf zu funktionieren. Gesichert wird nur
|
# Wiedereinspielen stillschweigend auf zu funktionieren. Gesichert wird nur
|
||||||
# der Hash – der Klartext existiert nirgends.
|
# der Hash – der Klartext existiert nirgends.
|
||||||
@@ -412,6 +426,10 @@ _MERGE_KEYS: dict[str, Callable[[dict], tuple | None]] = {
|
|||||||
# Vertrags ein zweites Mal an – und scheitert an seinem Schluessel, womit
|
# Vertrags ein zweites Mal an – und scheitert an seinem Schluessel, womit
|
||||||
# die ganze Wiederherstellung kippt. Es gibt genau eins je Vertrag.
|
# die ganze Wiederherstellung kippt. Es gibt genau eins je Vertrag.
|
||||||
"subscription_logos": lambda r: ("logo", r.get("subscription_id")),
|
"subscription_logos": lambda r: ("logo", r.get("subscription_id")),
|
||||||
|
# Ohne die beiden Einträge scheitert ein zweiter Ergänzen-Lauf an
|
||||||
|
# (owner_id, name) bzw. (transaction_id, tag_id).
|
||||||
|
"tags": lambda r: ("name", (r.get("name") or "").strip().lower()),
|
||||||
|
"transaction_tags": lambda r: ("zuordnung", r.get("transaction_id"), r.get("tag_id")),
|
||||||
"transactions": lambda r: (
|
"transactions": lambda r: (
|
||||||
("uid", r["source_uid"]) if r.get("source_uid")
|
("uid", r["source_uid"]) if r.get("source_uid")
|
||||||
else ("hash", r["import_hash"]) if r.get("import_hash")
|
else ("hash", r["import_hash"]) if r.get("import_hash")
|
||||||
|
|||||||
@@ -87,3 +87,44 @@ def test_jede_nutzertabelle_ist_im_backup_beruecksichtigt():
|
|||||||
f"Diese Tabellen gehören dem Nutzer, stehen aber in keiner Backup-Liste: "
|
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."
|
f"{fehlend}. Entweder in USER_TABLES aufnehmen oder oben begründet ausnehmen."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_transfer_schlagwort_behaelt_sein_zielkonto(admin):
|
||||||
|
"""Ein Transfer-Schlagwort mit festem Zielkonto trug beim Wiedereinspielen
|
||||||
|
die ALTE Konto-Nummer in die neue Datenbank. Beim Einspielen brach dann die
|
||||||
|
ganze Sicherung mit einem Fremdschluessel-Fehler ab - nicht nur diese eine
|
||||||
|
Zeile. Wer je ein Zielkonto gewaehlt hatte, kam an seine Sicherung also gar
|
||||||
|
nicht mehr heran."""
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
def _konto(name):
|
||||||
|
return admin.post("/api/accounts", json={
|
||||||
|
"name": name, "type": "sparkonto", "currency": "CHF",
|
||||||
|
"opening_balance_cents": 0}).json()
|
||||||
|
|
||||||
|
# Erst Luecken in die Konto-Nummern reissen, dann das Zielkonto anlegen.
|
||||||
|
# Ohne Luecken vergibt SQLite beim Einspielen zufaellig dieselben Nummern
|
||||||
|
# und der Fehler bliebe unsichtbar - genau deshalb ist er so lange
|
||||||
|
# unbemerkt geblieben.
|
||||||
|
weg = [_konto(f"Weg {secrets.token_hex(3)}") for _ in range(5)]
|
||||||
|
ziel = _konto(f"Sparen {secrets.token_hex(3)}")
|
||||||
|
for k in weg:
|
||||||
|
assert admin.delete(f"/api/accounts/{k['id']}").status_code in (200, 204)
|
||||||
|
|
||||||
|
wort = f"um chf sparen {secrets.token_hex(5)}"
|
||||||
|
r = admin.post("/api/categories/transfer-keywords",
|
||||||
|
json={"word": wort, "target_account_id": ziel["id"]})
|
||||||
|
assert r.status_code == 201, r.text
|
||||||
|
|
||||||
|
daten = json.loads(admin.get("/api/export/backup.json").content)
|
||||||
|
r = admin.post("/api/backup/restore",
|
||||||
|
files={"file": ("backup.json", json.dumps(daten).encode("utf-8"),
|
||||||
|
"application/json")},
|
||||||
|
data={"mode": "replace"})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
|
||||||
|
konten = {a["name"]: a["id"] for a in admin.get("/api/accounts").json()}
|
||||||
|
regel = next(w for w in admin.get("/api/categories/transfer-keywords").json()
|
||||||
|
if w["word"] == wort)
|
||||||
|
assert regel["target_account_id"] == konten[ziel["name"]], \
|
||||||
|
"das Zielkonto muss auf die NEUE Nummer zeigen"
|
||||||
|
|||||||
Reference in New Issue
Block a user