Tests: pytest-Suite für Konten, Haushalt, Kanban, Reisen und Migrationen

- backend/tests: jede Test-Funktion bekommt eine frische Datenbank;
  test_migrationen.py baut ältere Datenbank-Stände nach und prüft die
  verlustfreie Umstellung (auch zweiter Start ohne Änderung)
- requirements-dev.txt mit pytest
- Fix (von den Tests gefunden): Eine neue, leere Datenbank bekam keine
  erste Liste "Zuhause"; jetzt einmalig beim ersten Start

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-15 18:07:54 +02:00
1 parent dc4ce18593
commit 79cbaab092
10 files changed
+478

No files matched your search

+8
View File
@@ -273,6 +273,13 @@ def _migrate(conn) -> None:
"""
info = {r["name"]: r for r in conn.execute("PRAGMA table_info(household_items)")}
if "list_id" in info and info["list_id"]["notnull"]:
# Neue Datenbank: einmalig eine erste Liste anlegen (wer später alle löscht, bekommt keine zurück)
if get_meta(conn, "haushalt_start") is None:
if not conn.execute("SELECT 1 FROM shopping_lists").fetchone():
conn.execute(
"INSERT INTO shopping_lists (name, everyone, position, created_at) VALUES ('Zuhause', 1, 1, ?)", (now_iso(),)
)
conn.execute("INSERT INTO meta (key, value) VALUES ('haushalt_start', '1')")
return
row = conn.execute("SELECT id FROM shopping_lists ORDER BY position, id LIMIT 1").fetchone()
if row:
@@ -295,6 +302,7 @@ def _migrate(conn) -> None:
(default,),
)
conn.execute("DROP TABLE household_items_alt")
conn.execute("INSERT OR REPLACE INTO meta (key, value) VALUES ('haushalt_start', '1')")
def _migrate_checked(conn) -> None:
+3
View File
@@ -0,0 +1,3 @@
# Nur für die Entwicklung: Tests (cd backend && .venv/bin/python -m pytest)
-r requirements.txt
pytest==8.4.2
+50
View File
@@ -0,0 +1,50 @@
"""Gemeinsame Test-Hilfen: jede Test-Funktion bekommt eine frische, leere Datenbank.
Die App liest den Datenordner beim Import; deshalb zeigt P42_DATA_DIR schon vorher auf einen
Wegwerf-Ordner. Pro Test wird db.DB_FILE auf eine neue Datei umgebogen und das Schema angelegt.
Hintergrund-Aufgaben (lifespan) laufen nicht, weil TestClient ohne "with" benutzt wird.
"""
import os
import sys
import tempfile
from pathlib import Path
os.environ["P42_DATA_DIR"] = tempfile.mkdtemp(prefix="p42-test-")
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import pytest # noqa: E402
from fastapi.testclient import TestClient # noqa: E402
from app import auth, db # noqa: E402
from app.main import app # noqa: E402
PASSWORD = "passwort123"
@pytest.fixture(autouse=True)
def fresh_db(tmp_path, monkeypatch):
monkeypatch.setattr(db, "DB_FILE", tmp_path / "p42.db")
auth._failures.clear()
db.init()
return tmp_path / "p42.db"
@pytest.fixture
def conn(fresh_db):
c = db.connect()
yield c
c.close()
@pytest.fixture
def login():
"""login("anna", admin=True) → angemeldeter TestClient mit .user (id, name)."""
def make(username: str, admin: bool = False) -> TestClient:
with db.transaction() as c:
user_id = auth._create_user(c, username, username.capitalize(), PASSWORD, admin)
client = TestClient(app)
res = client.post("/api/auth/login", json={"username": username, "password": PASSWORD})
assert res.status_code == 200, res.text
client.user = {"id": user_id, "name": username.capitalize()}
return client
return make
+28
View File
@@ -0,0 +1,28 @@
from fastapi.testclient import TestClient
from app.main import app
def test_ohne_anmeldung_gesperrt():
assert TestClient(app).get("/api/haushalt").status_code == 401
def test_erstes_konto_wird_admin():
client = TestClient(app)
assert client.get("/api/auth/status").json()["setupRequired"] is True
res = client.post("/api/auth/setup", json={"username": "erste", "password": "passwort123"})
assert res.status_code == 200
assert res.json()["isAdmin"] is True
# Danach gibt es kein zweites "erstes Konto"
assert TestClient(app).post("/api/auth/setup", json={"username": "zweite", "password": "passwort123"}).status_code == 403
def test_falsches_passwort(login):
login("anna")
res = TestClient(app).post("/api/auth/login", json={"username": "anna", "password": "falsch"})
assert res.status_code == 401
def test_benutzerverwaltung_nur_admin(login):
assert login("anna").get("/api/users").status_code == 403
assert login("chef", admin=True).get("/api/users").status_code == 200
+140
View File
@@ -0,0 +1,140 @@
def state(client):
return client.get("/api/haushalt").json()
def add(client, list_id, name, area="einkauf", quantity=1, unit="stk"):
return client.post("/api/haushalt/items", json={"list": area, "name": name, "quantity": quantity, "unit": unit, "listId": list_id})
def items(data, list_id, area=None):
return [i for i in data["items"] if i["listId"] == list_id and (area is None or i["list"] == area)]
def new_list(client, name, everyone=True, members=()):
res = client.post("/api/haushalt/listen", json={"name": name, "everyone": everyone, "members": list(members)})
assert res.status_code == 201, res.text
return res.json()["created"]
def test_neue_db_hat_liste_zuhause(login):
data = state(login("anna"))
assert [l["name"] for l in data["lists"]] == ["Zuhause"]
assert data["lists"][0]["everyone"] is True
def test_hinzufuegen_fuehrt_zusammen_und_vereinheitlicht_namen(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
add(anna, home, "milch", quantity=2)
data = add(anna, home, "Milch", quantity=1).json()
assert [(i["name"], i["quantity"]) for i in items(data, home, "einkauf")] == [("Milch", 3)]
# Andere Einheit = eigene Zeile
data = add(anna, home, "Milch", quantity=1.5, unit="l").json()
assert len(items(data, home, "einkauf")) == 2
def test_listen_haben_eigenen_vorrat(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
urlaub = new_list(anna, "Urlaub")
add(anna, urlaub, "Brot", area="vorrat")
data = state(anna)
assert [i["name"] for i in items(data, urlaub, "vorrat")] == ["Brot"]
assert items(data, home, "vorrat") == []
def test_private_liste_nur_fuer_mitglieder(login):
anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True)
geheim = new_list(anna, "Geheim", everyone=False)
item = add(anna, geheim, "Kuchen").json()
item_id = items(item, geheim)[0]["id"]
for other in (ben, chef): # auch Admins sehen fremde private Listen nicht
assert all(l["id"] != geheim for l in state(other)["lists"])
assert add(other, geheim, "x").status_code == 404
assert other.patch(f"/api/haushalt/items/{item_id}", json={"quantity": 5}).status_code == 404
anna.patch(f"/api/haushalt/listen/{geheim}", json={"name": "Geheim", "everyone": False, "members": [ben.user["id"]]})
lst = next(l for l in state(ben)["lists"] if l["id"] == geheim)
assert lst["canManage"] is False
assert ben.patch(f"/api/haushalt/listen/{geheim}", json={"name": "x"}).status_code == 403
assert ben.delete(f"/api/haushalt/listen/{geheim}").status_code == 403
def test_liste_loeschen_entfernt_eintraege(login, conn):
anna = login("anna")
urlaub = new_list(anna, "Urlaub")
add(anna, urlaub, "Brot", area="vorrat")
add(anna, urlaub, "Wasser")
assert anna.delete(f"/api/haushalt/listen/{urlaub}").status_code == 200
assert conn.execute("SELECT COUNT(*) FROM household_items WHERE list_id = ?", (urlaub,)).fetchone()[0] == 0
def test_ungueltiges_mitglied(login):
res = login("anna").post("/api/haushalt/listen", json={"name": "x", "everyone": False, "members": [9999]})
assert res.status_code == 400
def test_abhaken_und_einkauf_beenden(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
add(anna, home, "Milch", quantity=2)
add(anna, home, "Brot")
add(anna, home, "Milch", area="vorrat", quantity=1)
milch = next(i for i in items(state(anna), home, "einkauf") if i["name"] == "Milch")
data = anna.patch(f"/api/haushalt/items/{milch['id']}", json={"checked": True}).json()
# Abgehakt bleibt auf der Einkaufsliste
assert {i["name"]: i["checked"] for i in items(data, home, "einkauf")} == {"Milch": True, "Brot": False}
res = anna.post(f"/api/haushalt/listen/{home}/einkauf-beenden")
assert res.json()["moved"] == 1
data = res.json()
assert [i["name"] for i in items(data, home, "einkauf")] == ["Brot"]
assert [(i["name"], i["quantity"]) for i in items(data, home, "vorrat")] == [("Milch", 3)]
# Nichts mehr abgehakt
assert anna.post(f"/api/haushalt/listen/{home}/einkauf-beenden").status_code == 400
def test_vorrat_laesst_sich_nicht_abhaken(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
vorrat = items(add(anna, home, "Reis", area="vorrat").json(), home, "vorrat")[0]
assert anna.patch(f"/api/haushalt/items/{vorrat['id']}", json={"checked": True}).status_code == 400
def test_erneut_hinzufuegen_hebt_haken_auf(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
milch = items(add(anna, home, "Milch").json(), home)[0]
anna.patch(f"/api/haushalt/items/{milch['id']}", json={"checked": True})
data = add(anna, home, "Milch").json()
assert items(data, home)[0]["checked"] is False
def test_aufgebraucht_kommt_mit_menge_1_auf_einkauf(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
reis = items(add(anna, home, "Reis", area="vorrat", quantity=4).json(), home)[0]
data = anna.post(f"/api/haushalt/items/{reis['id']}/move").json()
assert [(i["list"], i["quantity"]) for i in items(data, home)] == [("einkauf", 1)]
def test_rezept_uebernehmen_in_gewaehlte_liste(login):
anna = login("anna")
urlaub = new_list(anna, "Urlaub")
res = anna.post("/api/haushalt/rezept/uebernehmen", json={
"items": [{"name": "Mehl", "quantity": 0.5, "unit": "kg"}], "aliases": [], "listId": urlaub,
})
assert res.status_code == 200
assert [(i["name"], i["quantity"], i["unit"]) for i in items(res.json(), urlaub, "einkauf")] == [("Mehl", 0.5, "kg")]
def test_einheitenwechsel_fuehrt_zusammen(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
add(anna, home, "Mehl", quantity=1, unit="kg")
stk = items(add(anna, home, "Mehl", quantity=2).json(), home)
stk = next(i for i in stk if i["unit"] == "stk")
data = anna.patch(f"/api/haushalt/items/{stk['id']}", json={"unit": "kg"}).json()
assert [(i["quantity"], i["unit"]) for i in items(data, home)] == [(3, "kg")]
+65
View File
@@ -0,0 +1,65 @@
from datetime import datetime, timedelta, timezone
def ago(days):
return (datetime.now(timezone.utc) - timedelta(days=days)).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def board(client, project_id):
return client.get(f"/api/kanban/projekte/{project_id}").json()
def make_board(client):
return client.post("/api/kanban/projekte", json={"name": "Test"}).json()["id"]
def task(client, project_id, title, status="offen"):
data = client.post(f"/api/kanban/projekte/{project_id}/aufgaben", json={"title": title, "status": status}).json()
return next(t for t in data["tasks"] if t["title"] == title)
def test_erledigt_setzt_und_loescht_zeitpunkt(login):
anna = login("anna")
pid = make_board(anna)
t = task(anna, pid, "A")
assert t["doneAt"] is None
data = anna.patch(f"/api/kanban/aufgaben/{t['id']}", json={"status": "erledigt"}).json()
assert next(x for x in data["tasks"] if x["id"] == t["id"])["doneAt"]
data = anna.patch(f"/api/kanban/aufgaben/{t['id']}", json={"status": "arbeit"}).json()
assert next(x for x in data["tasks"] if x["id"] == t["id"])["doneAt"] is None
assert task(anna, pid, "Gleich erledigt", status="erledigt")["doneAt"]
def test_archiv_nach_2_tagen_loeschen_nach_14(login, conn):
anna = login("anna")
pid = make_board(anna)
for title in ("frisch", "alt", "uralt"):
task(anna, pid, title, status="erledigt")
with conn:
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE title = 'alt'", (ago(3),))
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE title = 'uralt'", (ago(15),))
data = board(anna, pid)
assert [t["title"] for t in data["tasks"]] == ["frisch"]
assert [t["title"] for t in data["archived"]] == ["alt"]
assert data["archived"][0]["deleteAt"] > ago(-10)
assert conn.execute("SELECT COUNT(*) FROM kanban_tasks WHERE title = 'uralt'").fetchone()[0] == 0
def test_zurueckholen_startet_fristen_neu(login, conn):
anna = login("anna")
pid = make_board(anna)
t = task(anna, pid, "alt", status="erledigt")
with conn:
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE id = ?", (ago(5), t["id"]))
data = anna.post(f"/api/kanban/aufgaben/{t['id']}/zurueckholen").json()
assert data["archived"] == []
assert [x["title"] for x in data["tasks"]] == ["alt"]
def test_loeschen_nur_ersteller_oder_admin(login):
anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True)
pid = make_board(anna)
t1, t2 = task(anna, pid, "eins"), task(anna, pid, "zwei")
assert ben.delete(f"/api/kanban/aufgaben/{t1['id']}").status_code == 403
assert anna.delete(f"/api/kanban/aufgaben/{t1['id']}").status_code == 200
assert chef.delete(f"/api/kanban/aufgaben/{t2['id']}").status_code == 200
+138
View File
@@ -0,0 +1,138 @@
"""Alte Datenbank-Stände nachbauen und prüfen, dass der Start sie verlustfrei auf den aktuellen Stand bringt."""
import sqlite3
import pytest
from app import db
T = "2026-01-01T00:00:00.000Z"
KANBAN_PROJECTS = """CREATE TABLE kanban_projects (
id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', position REAL NOT NULL,
completed_at TEXT, created_at TEXT NOT NULL, created_by INTEGER, updated_at TEXT NOT NULL, updated_by INTEGER)"""
# Kanban-Aufgaben vor "erledigt seit"
KANBAN_TASKS_OHNE_DONE = """CREATE TABLE kanban_tasks (
id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL REFERENCES kanban_projects(id) ON DELETE CASCADE,
title TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', status TEXT NOT NULL, assignee INTEGER,
priority INTEGER NOT NULL DEFAULT 2, position REAL NOT NULL, created_at TEXT NOT NULL, created_by INTEGER,
updated_at TEXT NOT NULL, updated_by INTEGER)"""
# Reise-Städte vor Höhe, Zeitzone und Hauptstadt
TRAVEL_ALT = """CREATE TABLE travel_cities (
id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, geoname_id INTEGER NOT NULL, name TEXT NOT NULL,
admin1 TEXT NOT NULL DEFAULT '', country TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL,
population INTEGER, created_at TEXT NOT NULL, UNIQUE (user_id, geoname_id))"""
# Vorrat/Einkauf ganz alt: ohne Einheit
HAUSHALT_OHNE_EINHEIT = """CREATE TABLE household_items (
id INTEGER PRIMARY KEY, key TEXT NOT NULL, name TEXT NOT NULL, list TEXT NOT NULL, quantity INTEGER NOT NULL,
updated_at TEXT NOT NULL, updated_by INTEGER, UNIQUE (key, list))"""
# Vorrat/Einkauf mit Einheit, aber ohne Listen (Stand auf dem Server vor den Listen)
HAUSHALT_OHNE_LISTEN = """CREATE TABLE household_items (
id INTEGER PRIMARY KEY, key TEXT NOT NULL, name TEXT NOT NULL, list TEXT NOT NULL, quantity REAL NOT NULL,
unit TEXT NOT NULL DEFAULT 'stk', updated_at TEXT NOT NULL, updated_by INTEGER, UNIQUE (key, list, unit))"""
# Zwischenstand: mehrere Einkaufslisten, Vorrat ohne Liste (list_id NULL)
ZWISCHENSTAND = [
"""CREATE TABLE shopping_lists (id INTEGER PRIMARY KEY, name TEXT NOT NULL, everyone INTEGER NOT NULL DEFAULT 1,
position REAL NOT NULL, created_at TEXT NOT NULL, created_by INTEGER)""",
"""CREATE TABLE household_items (
id INTEGER PRIMARY KEY, key TEXT NOT NULL, name TEXT NOT NULL, list TEXT NOT NULL,
list_id INTEGER REFERENCES shopping_lists(id) ON DELETE CASCADE, quantity REAL NOT NULL,
unit TEXT NOT NULL DEFAULT 'stk', updated_at TEXT NOT NULL, updated_by INTEGER,
CHECK ((list = 'einkauf') = (list_id IS NOT NULL)))""",
"CREATE UNIQUE INDEX household_vorrat ON household_items (key, unit) WHERE list_id IS NULL",
"CREATE UNIQUE INDEX household_einkauf ON household_items (list_id, key, unit) WHERE list_id IS NOT NULL",
f"INSERT INTO shopping_lists VALUES (1, 'Einkaufsliste', 1, 1, '{T}', NULL)",
]
@pytest.fixture
def old_db(tmp_path, monkeypatch):
"""Leere Datei mit den angegebenen alten Tabellen; danach db.init() wie beim Serverstart."""
path = tmp_path / "alt.db"
monkeypatch.setattr(db, "DB_FILE", path)
def build(*statements):
c = sqlite3.connect(path)
for sql in statements:
c.execute(sql)
c.commit()
c.close()
db.init()
conn = db.connect()
return conn
return build
def household(conn):
return sorted(tuple(r) for r in conn.execute("SELECT name, list, list_id, quantity, unit, checked FROM household_items"))
def test_haushalt_ohne_einheit(old_db):
conn = old_db(
HAUSHALT_OHNE_EINHEIT,
f"INSERT INTO household_items VALUES (1, 'milch', 'Milch', 'vorrat', 2, '{T}', NULL)",
f"INSERT INTO household_items VALUES (2, 'brot', 'Brot', 'einkauf', 1, '{T}', NULL)",
)
assert [tuple(r) for r in conn.execute("SELECT id, name FROM shopping_lists")] == [(1, "Zuhause")]
assert household(conn) == [("Brot", "einkauf", 1, 1, "stk", 0), ("Milch", "vorrat", 1, 2, "stk", 0)]
def test_haushalt_ohne_listen(old_db):
conn = old_db(
HAUSHALT_OHNE_LISTEN,
f"INSERT INTO household_items VALUES (1, 'mehl', 'Mehl', 'vorrat', 1.5, 'kg', '{T}', NULL)",
f"INSERT INTO household_items VALUES (2, 'mehl', 'Mehl', 'einkauf', 2, 'stk', '{T}', NULL)",
)
assert [r["name"] for r in conn.execute("SELECT name FROM shopping_lists")] == ["Zuhause"]
assert household(conn) == [("Mehl", "einkauf", 1, 2, "stk", 0), ("Mehl", "vorrat", 1, 1.5, "kg", 0)]
def test_haushalt_zwischenstand(old_db):
conn = old_db(
*ZWISCHENSTAND,
f"INSERT INTO household_items VALUES (1, 'reis', 'Reis', 'vorrat', NULL, 1, 'stk', '{T}', NULL)",
f"INSERT INTO household_items VALUES (2, 'reis', 'Reis', 'einkauf', 1, 3, 'stk', '{T}', NULL)",
)
# "Einkaufsliste" heißt jetzt "Zuhause" und enthält auch den Vorrat
assert [tuple(r) for r in conn.execute("SELECT id, name FROM shopping_lists")] == [(1, "Zuhause")]
assert household(conn) == [("Reis", "einkauf", 1, 3, "stk", 0), ("Reis", "vorrat", 1, 1, "stk", 0)]
indexes = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type = 'index'")}
assert not {"household_vorrat", "household_einkauf"} & indexes
def test_kanban_ohne_erledigt_zeitpunkt(old_db):
conn = old_db(
KANBAN_PROJECTS,
KANBAN_TASKS_OHNE_DONE,
f"INSERT INTO kanban_projects VALUES (1, 'P', '', 1, NULL, '{T}', NULL, '{T}', NULL)",
f"INSERT INTO kanban_tasks VALUES (1, 1, 'fertig', '', 'erledigt', NULL, 2, 1, '{T}', NULL, '{T}', NULL)",
f"INSERT INTO kanban_tasks VALUES (2, 1, 'offen', '', 'offen', NULL, 2, 2, '{T}', NULL, '{T}', NULL)",
)
rows = dict(conn.execute("SELECT title, done_at FROM kanban_tasks").fetchall())
# Erledigte zählen ab dem Update – nicht ab dem alten Änderungsdatum (sonst sofort gelöscht)
assert rows["fertig"] > T and rows["offen"] is None
def test_reisen_ohne_neue_spalten(old_db):
conn = old_db(
TRAVEL_ALT,
f"INSERT INTO travel_cities VALUES (1, 1, 2950159, 'Berlin', '', 'DE', 52.5, 13.4, NULL, '{T}')",
)
row = conn.execute("SELECT name, elevation, timezone, capital FROM travel_cities").fetchone()
assert tuple(row) == ("Berlin", None, None, None)
def test_zweiter_start_aendert_nichts(old_db):
conn = old_db(
HAUSHALT_OHNE_LISTEN,
f"INSERT INTO household_items VALUES (1, 'mehl', 'Mehl', 'vorrat', 1, 'stk', '{T}', NULL)",
)
before = household(conn), [tuple(r) for r in conn.execute("SELECT * FROM shopping_lists")]
db.init()
assert (household(conn), [tuple(r) for r in conn.execute("SELECT * FROM shopping_lists")]) == before
def test_geloeschte_listen_kommen_nicht_zurueck(conn):
with conn:
conn.execute("DELETE FROM shopping_lists")
db.init()
assert conn.execute("SELECT COUNT(*) FROM shopping_lists").fetchone()[0] == 0
+34
View File
@@ -0,0 +1,34 @@
BERLIN = {"geonameId": 2950159, "name": "Berlin", "country": "DE", "lat": 52.52, "lon": 13.41, "population": 3426354, "capital": True}
LEIDEN = {"geonameId": 2751773, "name": "Leiden", "country": "NL", "lat": 52.16, "lon": 4.49}
def test_staedte_anderer_nur_lesend(login):
anna, ben = login("anna"), login("ben")
anna.post("/api/reisen/staedte", json=BERLIN)
data = ben.get("/api/reisen").json()
assert data["cities"] == []
assert [(p["name"], [c["name"] for c in p["cities"]]) for p in data["others"]] == [("Anna", ["Berlin"])]
city_id = data["others"][0]["cities"][0]["id"]
assert ben.delete(f"/api/reisen/staedte/{city_id}").status_code == 404
assert anna.get("/api/reisen").json()["others"] == []
def test_hauptstadt_kennzeichen(login):
anna = login("anna")
anna.post("/api/reisen/staedte", json=BERLIN)
data = anna.post("/api/reisen/staedte", json=LEIDEN).json()
# Mit Angabe aus der Suche gespeichert, ohne Angabe "unbekannt" (wird im Hintergrund nachgetragen)
assert {c["name"]: c["capital"] for c in data["cities"]} == {"Berlin": True, "Leiden": None}
def test_doppelte_stadt_wird_ignoriert(login):
anna = login("anna")
anna.post("/api/reisen/staedte", json=BERLIN)
assert len(anna.post("/api/reisen/staedte", json=BERLIN).json()["cities"]) == 1
def test_land_markieren(login):
anna = login("anna")
assert anna.put("/api/reisen/laender/IS").json()["countries"] == ["IS"]
assert anna.delete("/api/reisen/laender/IS").json()["countries"] == []
assert anna.put("/api/reisen/laender/xx").status_code == 400