test: Die iOS-Modelle werden gegen echte Serverantworten geprueft
Die Swift-Modelle sind aus den Pydantic-Schemas abgeschrieben. Ein vertippter Feldname uebersetzt anstandslos und faellt erst dem Nutzer auf, wenn eine Liste leer bleibt - das ist beim blinden Schreiben die einzige Luecke, die kein Uebersetzer schliesst. backend/scripts/erzeuge_ios_fixture.py legt eine Wegwerfdatenbank an, befuellt sie so, dass jedes Feld einmal belegt ist (Tags, Uebertrag am Stueck, verknuepfter Uebertrag, Vertragszuordnung, gebundenes Konto, jaehrliches Abo) und schreibt 15 echte Antworten nach ios/Tests/Fixtures/antworten.json. Der Swift-Test "Serverantworten" entschluesselt daraus jedes Modell. Nach jeder Schema-Aenderung neu laufen lassen und das Ergebnis mit committen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/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) -> None:
|
||||
"""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']}")
|
||||
|
||||
|
||||
PFADE = {
|
||||
"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",
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
_seed(c)
|
||||
|
||||
heraus = {}
|
||||
for name, pfad in PFADE.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())
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user