"""Integrationstests der CRUD-Endpunkte.""" from httpx import AsyncClient # --- Konten -------------------------------------------------------------------- async def test_konto_lebenszyklus(auth_client: AsyncClient) -> None: angelegt = await auth_client.post( "/api/accounts", json={ "name": "Girokonto", "type": "checking", "iban_last4": "4711", "opening_balance": "1500.00", "opening_balance_date": "2026-01-01", }, ) assert angelegt.status_code == 201 konto = angelegt.json() assert konto["opening_balance"] == "1500.00" geaendert = await auth_client.patch( f"/api/accounts/{konto['id']}", json={"name": "Gehaltskonto", "sort_order": 3} ) assert geaendert.status_code == 200 assert geaendert.json()["name"] == "Gehaltskonto" assert geaendert.json()["iban_last4"] == "4711" # unverändert liste = (await auth_client.get("/api/accounts")).json() assert [item["name"] for item in liste] == ["Gehaltskonto"] geloescht = await auth_client.delete(f"/api/accounts/{konto['id']}") assert geloescht.status_code == 200 assert (await auth_client.get(f"/api/accounts/{konto['id']}")).status_code == 404 async def test_doppelter_kontoname_wird_abgewiesen(auth_client: AsyncClient) -> None: payload = {"name": "Girokonto", "opening_balance_date": "2026-01-01"} assert (await auth_client.post("/api/accounts", json=payload)).status_code == 201 zweites = await auth_client.post("/api/accounts", json=payload) assert zweites.status_code == 409 assert zweites.json()["code"] == "conflict" async def test_unbekanntes_feld_wird_abgewiesen(auth_client: AsyncClient) -> None: antwort = await auth_client.post( "/api/accounts", json={"name": "X", "opening_balance_date": "2026-01-01", "tippfehler": 1}, ) assert antwort.status_code == 422 assert antwort.json()["code"] == "validation_error" async def test_ungueltige_farbe_wird_abgewiesen(auth_client: AsyncClient) -> None: antwort = await auth_client.post( "/api/accounts", json={"name": "X", "opening_balance_date": "2026-01-01", "color": "blau"}, ) assert antwort.status_code == 422 # --- Kategorien ---------------------------------------------------------------- async def test_kategoriebaum_ist_zweistufig(auth_client: AsyncClient, seeded: dict) -> None: baum = (await auth_client.get("/api/categories")).json() wohnen = next(item for item in baum if item["name"] == "Wohnen") assert wohnen["parent_id"] is None assert {kind["name"] for kind in wohnen["children"]} == { "Miete", "Nebenkosten", "Strom", "Internet", "Rundfunkbeitrag", } assert next(k for k in wohnen["children"] if k["name"] == "Miete")["is_fixed_cost"] is True async def test_dritte_ebene_wird_verweigert(auth_client: AsyncClient, seeded: dict) -> None: antwort = await auth_client.post( "/api/categories", json={"name": "Netflix", "kind": "expense", "parent_id": seeded["streaming"]}, ) assert antwort.status_code == 422 assert antwort.json()["code"] == "category_too_deep" async def test_unterkategorie_erbt_die_richtung(auth_client: AsyncClient, seeded: dict) -> None: baum = (await auth_client.get("/api/categories")).json() wohnen = next(item for item in baum if item["name"] == "Wohnen") # Absichtlich die falsche Richtung mitschicken – der Elternknoten gewinnt. antwort = await auth_client.post( "/api/categories", json={"name": "Gartenpflege", "kind": "income", "parent_id": wohnen["id"]}, ) assert antwort.status_code == 201 assert antwort.json()["kind"] == "expense" async def test_verwendete_kategorie_kann_nicht_geloescht_werden( auth_client: AsyncClient, seeded: dict ) -> None: await auth_client.post( "/api/recurrences", json={ "kind": "expense", "title": "Miete", "category_id": seeded["miete"], "account_id": seeded["account_id"], "amount": "950.00", "rrule": "FREQ=MONTHLY;BYMONTHDAY=1", "dtstart": "2026-01-01", }, ) antwort = await auth_client.delete(f"/api/categories/{seeded['miete']}") assert antwort.status_code == 409 assert antwort.json()["code"] == "category_in_use" # --- Firmen -------------------------------------------------------------------- async def test_firma_wird_normalisiert(auth_client: AsyncClient) -> None: antwort = await auth_client.post( "/api/merchants", json={"name": "Netflix International B.V.", "domain": "netflix.com"} ) assert antwort.status_code == 201 firma = antwort.json() assert firma["normalized_name"] == "netflixinternational" assert firma["logo_status"] == "pending" assert firma["logo_asset_id"] is None async def test_firmensuche(auth_client: AsyncClient) -> None: for name in ["Netflix", "Spotify", "Deutsche Telekom AG"]: await auth_client.post("/api/merchants", json={"name": name}) treffer = (await auth_client.get("/api/merchants", params={"q": "telekom"})).json() assert treffer["total"] == 1 assert treffer["items"][0]["name"] == "Deutsche Telekom AG" alle = (await auth_client.get("/api/merchants")).json() assert alle["total"] == 3 async def test_doppelte_firma_wird_abgewiesen(auth_client: AsyncClient) -> None: await auth_client.post("/api/merchants", json={"name": "Netflix"}) # Andere Schreibweise, gleicher normalisierter Name. antwort = await auth_client.post("/api/merchants", json={"name": "NETFLIX"}) assert antwort.status_code == 409 async def test_firma_loeschen_loest_verweise(auth_client: AsyncClient, seeded: dict) -> None: firma = (await auth_client.post("/api/merchants", json={"name": "Rewe"})).json() buchung = ( await auth_client.post( "/api/transactions", json={ "kind": "expense", "title": "Wocheneinkauf", "category_id": seeded["lebensmittel"], "account_id": seeded["account_id"], "merchant_id": firma["id"], "amount": "84.30", "booking_date": "2026-03-05", }, ) ).json() assert (await auth_client.delete(f"/api/merchants/{firma['id']}")).status_code == 200 danach = (await auth_client.get(f"/api/transactions/{buchung['id']}")).json() assert danach["merchant_id"] is None # --- Buchungen ----------------------------------------------------------------- async def test_buchung_mit_falscher_kategorierichtung( auth_client: AsyncClient, seeded: dict ) -> None: antwort = await auth_client.post( "/api/transactions", json={ "kind": "income", "title": "Falsch einsortiert", "category_id": seeded["lebensmittel"], "account_id": seeded["account_id"], "amount": "10.00", "booking_date": "2026-03-01", }, ) assert antwort.status_code == 422 assert antwort.json()["code"] == "category_kind_mismatch" async def test_buchungen_filtern_und_blaettern(auth_client: AsyncClient, seeded: dict) -> None: for tag in range(1, 6): await auth_client.post( "/api/transactions", json={ "kind": "expense", "title": f"Einkauf {tag}", "category_id": seeded["lebensmittel"], "account_id": seeded["account_id"], "amount": "20.00", "booking_date": f"2026-03-0{tag}", }, ) seite = (await auth_client.get("/api/transactions", params={"limit": 2, "offset": 0})).json() assert seite["total"] == 5 assert len(seite["items"]) == 2 # Neueste zuerst. assert seite["items"][0]["booking_date"] == "2026-03-05" zeitraum = ( await auth_client.get( "/api/transactions", params={"from": "2026-03-02", "to": "2026-03-03"} ) ).json() assert zeitraum["total"] == 2 suche = (await auth_client.get("/api/transactions", params={"q": "Einkauf 4"})).json() assert suche["total"] == 1 # --- Budgets und Sparziele ----------------------------------------------------- async def test_budget_wird_auf_den_monatsersten_normalisiert( auth_client: AsyncClient, seeded: dict ) -> None: antwort = await auth_client.post( "/api/budgets", json={ "category_id": seeded["lebensmittel"], "period_month": "2026-03-17", "limit_amount": "450.00", }, ) assert antwort.status_code == 201 assert antwort.json()["period_month"] == "2026-03-01" async def test_budget_pro_kategorie_und_monat_nur_einmal( auth_client: AsyncClient, seeded: dict ) -> None: payload = { "category_id": seeded["lebensmittel"], "period_month": "2026-03-01", "limit_amount": "450.00", } await auth_client.post("/api/budgets", json=payload) zweites = await auth_client.post("/api/budgets", json=payload) assert zweites.status_code == 409 async def test_budgetvorlage(auth_client: AsyncClient, seeded: dict) -> None: antwort = await auth_client.post( "/api/budget-templates", json={ "category_id": seeded["lebensmittel"], "valid_from": "2026-01-15", "limit_amount": "500.00", }, ) assert antwort.status_code == 201 assert antwort.json()["valid_from"] == "2026-01-01" assert antwort.json()["valid_until"] is None async def test_sparziel_lebenszyklus(auth_client: AsyncClient) -> None: angelegt = await auth_client.post( "/api/savings-goals", json={"name": "Neues Fahrrad", "target_amount": "1800.00", "target_date": "2027-04-01"}, ) assert angelegt.status_code == 201 ziel = angelegt.json() assert ziel["current_amount"] == "0.00" geaendert = await auth_client.patch( f"/api/savings-goals/{ziel['id']}", json={"current_amount": "450.00"} ) assert geaendert.json()["current_amount"] == "450.00" await auth_client.patch(f"/api/savings-goals/{ziel['id']}", json={"is_archived": True}) assert (await auth_client.get("/api/savings-goals")).json() == [] assert ( len((await auth_client.get("/api/savings-goals", params={"include_archived": True})).json()) == 1 )