Files
certain/backend/scripts/erzeuge_ios_fixture.py
GuidoandClaude Opus 5 bab5cc5060 test: Die Pruefdatei deckt auch die Nebenraeume ab
Erkennungswoerter einer Kategorie, markierte Woerter, der Stand des zweiten
Faktors und die Bankverbindungen kommen jetzt mit in
ios/Tests/Fixtures/antworten.json. Das Skript gibt die IDs aus der Saat
zurueck, damit ein Pfad wie /categories/{id}/keywords ueberhaupt gebaut
werden kann.

Zwei davon bleiben leer, und das steht auch dort: Eine Markierung entsteht
erst nach fuenf Anwendungen mit Korrekturen, eine Bankanbindung ohne echten
Anbieter gar nicht. Die Felder von BankConnectionOut deckt der Test also
NICHT ab - wer sie aendert, prueft von Hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-21 22:03:28 +02:00

191 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Echte Antworten des Backends als Prüfdatei für die iOS-Modelle.
Aufruf aus ``backend/``::
.venv/bin/python -m scripts.erzeuge_ios_fixture
Schreibt ``ios/Tests/Fixtures/antworten.json``. Der Swift-Test
``Serverantworten`` entschlüsselt daraus jedes Modell und schlägt fehl, sobald
ein Feldname nicht mehr passt.
**Warum es das gibt.** Die iOS-App wird gegen Modelle geschrieben, die jemand
aus den Pydantic-Schemas abgeschrieben hat. Ein vertippter Feldname fällt beim
Übersetzen nicht auf -- er fällt erst dem Nutzer auf, wenn die Liste leer
bleibt. Diese Datei ist die einzige Prüfung, die das fängt.
Nach jeder Änderung an einem Schema neu laufen lassen und das Ergebnis
mit committen. Die Datenbank ist eine Wegwerfdatenbank; es landen keine
echten Daten in der Datei.
"""
import json
import os
import tempfile
from pathlib import Path
# Muss vor dem Import von app stehen -- die Konfiguration wird beim Import gelesen.
os.environ["APP_DATA_DIR"] = tempfile.mkdtemp(prefix="cf-fixture-")
os.environ.setdefault("APP_ECB_FETCH_ON_START", "false")
os.environ.setdefault("APP_ADMIN_USERNAME", "admin")
os.environ.setdefault("APP_ADMIN_PASSWORD", "AdminPass1")
os.environ.setdefault("APP_SESSION_SECRET", "fixture-secret")
from datetime import date # noqa: E402
from fastapi.testclient import TestClient # noqa: E402
from app.main import app # noqa: E402
ZIEL = Path(__file__).resolve().parents[2] / "ios" / "Tests" / "Fixtures" / "antworten.json"
def _seed(c: TestClient) -> dict:
"""Genug Daten, dass jedes Feld einmal belegt ist.
Leere Listen pruefen nichts: Ein falscher Feldname in einer Zeile faellt
nur auf, wenn es eine Zeile gibt.
"""
c.put("/api/settings/budget_start_day", params={"value": "1"})
giro = c.post("/api/accounts", json={
"name": "Girokonto", "type": "girokonto", "currency": "EUR",
"opening_balance_cents": 120000}).json()
sparen = c.post("/api/accounts", json={
"name": "Sparen", "type": "sparkonto", "currency": "CHF",
"opening_balance_cents": 500000, "is_locked": True}).json()
gruppen = c.get("/api/categories").json()
ausgabe = next(g for g in gruppen if g["kind"] == "expense")["categories"][0]
einnahme = next(g for g in gruppen if g["kind"] == "income")["categories"][0]
tag = c.post("/api/tags", json={"name": "Urlaub", "color": "#2a78d6"}).json()
abo = c.post("/api/subscriptions", json={
"name": "Streaming", "amount_cents": 1299, "currency": "EUR",
"interval_months": 12, "category_id": ausgabe["id"]}).json()
heute = str(date.today())
kauf = c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 4250, "category_id": ausgabe["id"], "payee": "Supermarkt",
"description": "Wocheneinkauf", "notes": "mit Pfand"}).json()
c.patch(f"/api/transactions/{kauf['id']}", json={"tag_ids": [tag["id"]]})
c.post("/api/transactions", json={
"type": "income", "date": heute, "account_id": giro["id"],
"amount_cents": 350000, "category_id": einnahme["id"], "payee": "Arbeitgeber"})
c.post("/api/transactions", json={
"type": "transfer", "date": heute, "account_id": giro["id"],
"to_account_id": sparen["id"], "amount_cents": 20000})
# Zwei getrennte Buchungen, von Hand zu einem Uebertrag verknuepft -- der
# zweite Mechanismus, den die App nachbauen muss.
raus = c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 15000, "payee": "Ueberweisung"}).json()
rein = c.post("/api/transactions", json={
"type": "income", "date": heute, "account_id": sparen["id"],
"amount_cents": 15000, "payee": "Gutschrift"}).json()
c.post("/api/uebertraege/verbinden",
json={"a_id": raus["id"], "b_id": rein["id"]})
c.put(f"/api/budget/template/category/{ausgabe['id']}",
params={"planned_cents": 50000})
c.post(f"/api/subscriptions/{abo['id']}/buchungen/{kauf['id']}")
# --- Arbeitsliste -----------------------------------------------------
# Ein Eingangseintrag entsteht nur ueber die Schnellerfassung; von Hand
# legt man keinen an. Also eine Marke ausstellen und damit buchen.
marke = c.post("/api/inbox/tokens",
json={"name": "Fixture", "default_account_id": giro["id"]}).json()
kopf = {"X-Certain-Token": marke["token"]}
c.post("/api/quick/booking", headers=kopf,
json={"amount": "12.90", "payee": "Baeckerei", "date": heute,
"external_id": "fixture-1"})
# Zwei gleich aussehende Buchungen: gleiches Konto, gleicher Tag,
# gleicher krummer Betrag. Genau das meldet /dubletten.
for _ in range(2):
c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 1043, "payee": "Kiosk", "category_id": ausgabe["id"]})
# Ein Abgang und ein Zugang ueber verschiedene Konten, gleicher Betrag,
# unverknuepft - daraus macht /uebertraege/vorschlaege einen Vorschlag.
# Beide Konten muessen dieselbe Waehrung haben: 75 CHF und 75 EUR sind
# nicht derselbe Betrag, nur dieselbe Zahl (siehe _kandidaten).
zweitgiro = c.post("/api/accounts", json={
"name": "Zweitkonto", "type": "girokonto", "currency": "EUR",
"opening_balance_cents": 0}).json()
c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 7500, "payee": "Dauerauftrag"})
c.post("/api/transactions", json={
"type": "income", "date": heute, "account_id": zweitgiro["id"],
"amount_cents": 7500, "payee": "Dauerauftrag"})
# --- Nebenraeume ------------------------------------------------------
# Ein Schlagwort, damit die Liste nicht leer ist. Die Markierung
# ("wird oft korrigiert") entsteht erst nach fuenf Anwendungen und laesst
# sich hier nicht erzwingen - dort bleibt die Liste leer.
c.post(f"/api/categories/{ausgabe['id']}/keywords", json={"word": "supermarkt"})
return {"kategorie": ausgabe["id"]}
def pfade(ids: dict) -> dict[str, str]:
"""Welche Antwort unter welchem Namen in die Pruefdatei kommt."""
return {
"version": "/api/version",
"ichSelbst": "/api/auth/me",
"einstellungen": "/api/settings",
"konten": "/api/accounts",
"kategoriegruppen": "/api/categories",
"schlagwoerter": "/api/tags",
"buchungen": "/api/transactions",
"uebertragsverknuepfungen": "/api/transactions/links",
"budget": "/api/budget/current",
"budgetvorlage": "/api/budget/template",
"abos": "/api/subscriptions",
"kontostaende": "/api/dashboard/balances",
"budgetfortschritt": "/api/dashboard/budget-progress",
"einnahmenAusgaben": "/api/dashboard/income-expense",
"letzteBuchungen": "/api/dashboard/recent",
"eingang": "/api/inbox",
"dubletten": "/api/transactions/dubletten",
"uebertragsvorschlaege": "/api/uebertraege/vorschlaege",
"schlagwoerter_der_kategorie": f"/api/categories/{ids['kategorie']}/keywords",
"markierteSchlagwoerter": "/api/categories/keywords/flagged",
"zweiterFaktor": "/api/auth/2fa/status",
# Bleibt leer: Eine Bankanbindung laesst sich ohne echten Anbieter nicht
# herstellen. Der Test prueft hier nur, dass sich die leere Liste lesen
# laesst - die Felder von BankConnectionOut deckt er nicht ab.
"bankverbindungen": "/api/bank/connections",
}
def main() -> int:
with TestClient(app) as c:
r = c.post("/api/auth/login",
data={"username": "admin", "password": "AdminPass1"})
if r.status_code != 200:
print("Anmeldung fehlgeschlagen:", r.text)
return 1
ids = _seed(c)
heraus = {}
for name, pfad in pfade(ids).items():
antwort = c.get(pfad)
if antwort.status_code != 200:
print(f"{pfad} antwortet {antwort.status_code}: {antwort.text[:200]}")
return 1
heraus[name] = antwort.json()
ZIEL.parent.mkdir(parents=True, exist_ok=True)
ZIEL.write_text(json.dumps(heraus, indent=1, ensure_ascii=False) + "\n",
encoding="utf-8")
print(f"{ZIEL} geschrieben, {len(heraus)} Antworten.")
return 0
if __name__ == "__main__":
raise SystemExit(main())