"""Forum-Antworten: Idempotenz gegen Doppelposts bei verlorener Antwort (Funkloch). Szenario aus der Praxis: Eine Antwort wird serverseitig erstellt, aber die HTTP- Antwort erreicht das Handy nicht (schlechtes Netz). Der Nutzer tippt erneut auf „Antworten". Mit stabiler client_uuid liefert der Retry denselben Post zurück — statt 30-Sekunden-Cooldown-Fehler (429) oder Doppelpost. Cooldown/Duplikat-Checks sind NICHT gestubbt → client_time steuert die Zeitbasis. """ from __future__ import annotations import secrets def _mk_thread(client, headers, client_time): r = client.post( "/api/forum/threads", headers=headers, json={ "kategorie": "allgemein", "titel": "Idempotenz-Thread", "text": f"Genug langer Thread-Text {secrets.token_hex(6)}.", "client_time": client_time, }, ) assert r.status_code == 201, f"thread create: {r.status_code} {r.text}" return r.json() def _reply(client, headers, thread_id, text, client_time, client_uuid=None): body = {"text": text, "client_time": client_time} if client_uuid is not None: body["client_uuid"] = client_uuid return client.post(f"/api/forum/threads/{thread_id}/posts", headers=headers, json=body) def test_retry_same_uuid_returns_same_post_no_cooldown(client, user): h = user["headers"] t = _mk_thread(client, h, "2026-06-19T10:00:00") # 1. Antwort (60s nach Thread → kein Cooldown) r1 = _reply(client, h, t["id"], "Oh ja das stimmt", "2026-06-19T10:01:00", client_uuid="uuid-A") assert r1.status_code == 201, r1.text pid = r1.json()["id"] # Retry mit SELBER UUID, nur 5s später (läge im 30s-Cooldown) → idempotent, # liefert denselben Post zurück, KEIN 429. r2 = _reply(client, h, t["id"], "Oh ja das stimmt", "2026-06-19T10:01:05", client_uuid="uuid-A") assert r2.status_code == 201, r2.text assert r2.json()["id"] == pid, "Retry muss den bereits erstellten Post zurückliefern" # Es darf nur EINE Antwort existieren (kein Doppelpost). detail = client.get(f"/api/forum/threads/{t['id']}", headers=h).json() assert detail["antworten"] == 1 assert len([p for p in detail["posts"] if not p.get("is_deleted")]) == 1 def test_cooldown_still_blocks_genuinely_new_reply(client, user): h = user["headers"] t = _mk_thread(client, h, "2026-06-19T10:00:00") r1 = _reply(client, h, t["id"], "Erste Antwort", "2026-06-19T10:01:00", client_uuid="uuid-1") assert r1.status_code == 201, r1.text # Andere UUID + anderer Text, nur 10s später → echter Doppel-Post-Versuch → 429 r2 = _reply(client, h, t["id"], "Zweite Antwort", "2026-06-19T10:01:10", client_uuid="uuid-2") assert r2.status_code == 429, f"Cooldown muss greifen: {r2.status_code} {r2.text}" def test_reply_without_uuid_still_works(client, user): """Rückwärtskompatibel: Antwort ohne client_uuid bleibt 201.""" h = user["headers"] t = _mk_thread(client, h, "2026-06-19T10:00:00") r = _reply(client, h, t["id"], "Antwort ohne UUID", "2026-06-19T10:01:00") assert r.status_code == 201, r.text