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>
89 lines
3.0 KiB
Python
89 lines
3.0 KiB
Python
import json
|
||
from datetime import date
|
||
|
||
from conftest import login_ready, new_user
|
||
|
||
|
||
def _setup(admin):
|
||
acc = admin.post("/api/accounts", json={"name": "Exp", "currency": "EUR"}).json()
|
||
admin.post("/api/transactions", json={
|
||
"type": "expense", "date": str(date.today()), "account_id": acc["id"],
|
||
"amount_cents": 4200, "payee": "Testladen", "description": "Etwas",
|
||
})
|
||
return acc
|
||
|
||
|
||
def test_export_transactions_csv(admin):
|
||
_setup(admin)
|
||
r = admin.get("/api/export/transactions.csv")
|
||
assert r.status_code == 200
|
||
assert "text/csv" in r.headers["content-type"]
|
||
body = r.content.decode("utf-8-sig")
|
||
assert "Datum;Typ;Konto" in body.splitlines()[0]
|
||
assert "Testladen" in body
|
||
|
||
|
||
def test_export_transactions_json(admin):
|
||
_setup(admin)
|
||
r = admin.get("/api/export/transactions.json")
|
||
assert r.status_code == 200
|
||
data = json.loads(r.content)
|
||
assert any(t["payee"] == "Testladen" for t in data)
|
||
|
||
|
||
def test_export_all_json(admin):
|
||
_setup(admin)
|
||
r = admin.get("/api/export/all.json")
|
||
assert r.status_code == 200
|
||
data = json.loads(r.content)
|
||
assert data["app"] == "Certain Finance"
|
||
assert len(data["accounts"]) >= 1
|
||
assert len(data["transactions"]) >= 1
|
||
|
||
|
||
def test_db_backup_admin_only(admin):
|
||
# Admin darf.
|
||
r = admin.get("/api/backup/db")
|
||
assert r.status_code == 200
|
||
assert r.content[:16].startswith(b"SQLite format 3")
|
||
|
||
# Normaler Nutzer darf nicht.
|
||
other, _ = login_ready(*new_user(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."
|
||
)
|