Seit PSD2 muessen EU-Banken eine Schnittstelle anbieten, aber nur fuer lizenzierte Dienste mit eIDAS-Zertifikat. Enable Banking haelt die Lizenz und reicht sie weiter: eigene Anwendung registrieren, eigene Konten verknuepfen, danach genau diese lesen. Deckt N26 und Revolut ab; die Schweiz faellt nicht unter PSD2, Raiffeisen bleibt beim CSV-Weg. Es gibt keinen API-Schluessel. Jede Anfrage traegt ein selbst gebautes JWT, signiert mit dem privaten Schluessel der Anwendung (RS256). Signiert wird mit cryptography, das ohnehin schon fuer die Verschluesselung im Projekt ist – der Test prueft die Signatur echt gegen den oeffentlichen Schluessel, denn genau daran scheitert so eine Anbindung sonst als Erstes. Abgerufene Umsaetze werden NICHT gebucht. Sie laufen durch detect_rows und landen als Vorschlag im Eingang – dieselbe Strecke wie Mail und Kurzbefehl, also greifen Duplikatschutz und Schlagwortregeln ohne Sonderbehandlung. Eine dritte Quelle, kein dritter Sonderweg. Bewusst eng gehalten: nur Lesen; Zuordnung Bankkonto -> Konto trifft der Nutzer; die Zuordnung verlangt dieselbe Waehrung; die Rueckleitungs-Adresse wird eingetragen statt geraten (hinter einem Proxy sieht der Dienst "http", obwohl der Browser HTTPS spricht) und muss HTTPS sein. Der Ablauf der Zustimmung (bis 180 Tage) wird sichtbar gemeldet, damit der Abruf nicht eines Tages stumm aufhoert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90 lines
3.1 KiB
Python
90 lines
3.1 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",
|
||
"bank_apps", "bank_connections", "bank_account_links"}
|
||
|
||
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."
|
||
)
|