#!/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']}") # --- 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"}) 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", "eingang": "/api/inbox", "dubletten": "/api/transactions/dubletten", "uebertragsvorschlaege": "/api/uebertraege/vorschlaege", } 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())