"""Das Buch der Abende: Wertungen als Momentaufnahme, Kommentare als Diskussion — verschachtelt, so tief man will.""" def abend(client, film, movie_id=1): """Host trägt einen Film ein, gibt die watched_id zurück.""" return client.post("/api/watched", json={"movie_id": film(movie_id)}).json()["id"] def wertung(client, watched_id, stars=4): client.post(f"/api/watched/{watched_id}/rating", json={"stars": stars}) film = next(f for f in client.get("/api/watched").json() if f["id"] == watched_id) return next(r for r in film["ratings"] if r["mine"]) def film_von(client, watched_id): return next(f for f in client.get("/api/watched").json() if f["id"] == watched_id) def kommentar(client, watched_id, text, parent_id=None): antwort = client.post( f"/api/watched/{watched_id}/notes", json={"text": text, "parent_id": parent_id} ) assert antwort.status_code == 200, antwort.text return antwort.json()["id"] def test_laufende_nummer_zaehlt_vom_ersten_abend(client, film): erster = abend(client, film, 1) client.patch(f"/api/watched/{erster}", json={"watched_at": "2026-01-01"}) zweiter = abend(client, film, 2) client.patch(f"/api/watched/{zweiter}", json={"watched_at": "2026-01-08"}) liste = client.get("/api/watched").json() # Jüngster zuerst, trägt aber die höchste Nummer assert [(f["id"], f["nummer"]) for f in liste] == [(zweiter, 2), (erster, 1)] def test_katalogdaten_haengen_am_abend(client): from app import db with db.connect() as conn: db.upsert(conn, {"id": 5, "title": "Alien", "backdrop_path": "/b.jpg", "images": ["/s1.jpg"], "runtime": 117, "genres": ["Horror"]}) watched_id = client.post("/api/watched", json={"movie_id": 5}).json()["id"] f = film_von(client, watched_id) assert f["backdrop_path"] == "/b.jpg" assert f["images"] == ["/s1.jpg"] assert f["runtime"] == 117 assert f["genres"] == ["Horror"] def test_kommentare_bilden_einen_baum(client, client2, film): watched_id = abend(client, film) oben = kommentar(client, watched_id, "War super.") antwort = kommentar(client2, watched_id, "Fand ich nicht.", oben) tiefer = kommentar(client, watched_id, "Doch!", antwort) noch_einer = kommentar(client2, watched_id, "Anderes Thema.") notes = film_von(client, watched_id)["notes"] assert [n["text"] for n in notes] == ["War super.", "Anderes Thema."] assert notes[0]["replies"][0]["text"] == "Fand ich nicht." assert notes[0]["replies"][0]["name"] == "Ben" assert notes[0]["replies"][0]["mine"] is False assert notes[0]["replies"][0]["replies"][0]["text"] == "Doch!" assert notes[0]["replies"][0]["replies"][0]["mine"] is True assert notes[1]["replies"] == [] assert (oben, antwort, tiefer, noch_einer) == tuple(sorted((oben, antwort, tiefer, noch_einer))) def test_zehn_ebenen_tief(client, film): watched_id = abend(client, film) vater = None for stufe in range(10): vater = kommentar(client, watched_id, f"Stufe {stufe}", vater) knoten = film_von(client, watched_id)["notes"][0] tiefe = 0 while knoten["replies"]: knoten = knoten["replies"][0] tiefe += 1 assert tiefe == 9 assert knoten["text"] == "Stufe 9" def test_leerer_text_oder_fremder_vater_werden_abgelehnt(client, film): watched_id = abend(client, film, 1) anderer = abend(client, film, 2) dort = kommentar(client, anderer, "woanders") assert client.post(f"/api/watched/{watched_id}/notes", json={"text": " "}).status_code == 400 # Der Vater gehört zu einem anderen Abend assert client.post( f"/api/watched/{watched_id}/notes", json={"text": "x", "parent_id": dort} ).status_code == 400 assert client.post( f"/api/watched/{watched_id}/notes", json={"text": "x", "parent_id": 9999} ).status_code == 400 def test_wertungen_und_kommentare_sind_unabhaengig(client, film): watched_id = abend(client, film) wertung(client, watched_id, 5) kommentar(client, watched_id, "Diskussion.") f = film_von(client, watched_id) assert f["ratings"][0]["stars"] == 5 assert "text" not in f["ratings"][0] assert [n["text"] for n in f["notes"]] == ["Diskussion."] def test_eigenen_kommentar_loeschen_fremden_nicht(client, client2, film): watched_id = abend(client, film) meiner = kommentar(client, watched_id, "meiner") assert client2.delete(f"/api/watched-notes/{meiner}").status_code == 404 assert client.delete(f"/api/watched-notes/{meiner}").json()["removed"] == 1 assert film_von(client, watched_id)["notes"] == [] def test_loeschen_nimmt_den_ganzen_ast_mit(client, client2, film): """Eine Antwort ohne ihren Anlass ergäbe keinen Sinn.""" watched_id = abend(client, film) oben = kommentar(client, watched_id, "a") mitte = kommentar(client2, watched_id, "b", oben) unten = kommentar(client, watched_id, "c", mitte) daneben = kommentar(client2, watched_id, "d") assert client.delete(f"/api/watched-notes/{oben}").json()["removed"] == 3 notes = film_von(client, watched_id)["notes"] assert [n["id"] for n in notes] == [daneben] def test_host_loescht_jeden_kommentar(client, zuschauer, hostmodus, film): watched_id = abend(client, film) note_id = kommentar(client, watched_id, "x") hostmodus(zuschauer) assert zuschauer.delete(f"/api/watched-notes/{note_id}").json()["removed"] == 1 def test_film_loeschen_raeumt_alles(client, client2, film): from app import db watched_id = abend(client, film) wertung(client, watched_id, 5) oben = kommentar(client2, watched_id, "frei") kommentar(client, watched_id, "antwort", oben) client.delete(f"/api/watched/{watched_id}") with db.connect() as conn: for tabelle in ("ratings", "watched_notes"): assert conn.execute(f"SELECT COUNT(*) FROM {tabelle}").fetchone()[0] == 0, tabelle def test_zuschauer_darf_nicht_schreiben(zuschauer, client, film): watched_id = abend(client, film) assert zuschauer.post(f"/api/watched/{watched_id}/notes", json={"text": "x"}).status_code == 403