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>
191 lines
8.4 KiB
Python
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())
|