Files
kleiderboerse/tests/test_reservierung.py
StefanandClaude Fable 5 c0a18ac735 Wettlauf beim Reservieren, 500er bei unsinnigen Eingaben
Drei P3-Funde aus der Durchsicht vom 14.09.:

Reservieren: Statusprüfung und Schreiben waren getrennt - zwei
gleichzeitige Anfragen konnten beide passieren, die zweite überschrieb
Name und Token der ersten, ohne dass die es erfuhr. Jetzt entscheidet
ein UPDATE mit Status-Bedingung; der Verlierer bekommt None und die
Route meldet "schon weg" (Seite) bzw. 409 (API).

/?limit=abc lieferte jedem anonymen Besucher einen internen
Serverfehler, limit=-1 hiess in SQLite "alles". _limit_lesen() fällt
bei Unsinn auf die Seitengrösse zurück und deckelt bei 500.

Erfassen/Bearbeiten: int(category_id) und die Pydantic-Prüfung warfen
im Handler - 500 statt Fehlermeldung. Jetzt Meldung; ausserdem werden
die Angaben VOR den Bildern geprüft, damit bei abgelehnten Angaben
keine verwaisten Bilddateien liegen bleiben.

Vier neue Tests, jeder einmal gegen den alten Code gelaufen und dabei
rot geworden. 84 lokal grün (HEIC-Test braucht pillow-heif, Docker).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-14 17:22:54 +02:00

108 lines
3.9 KiB
Python

"""Tests rund ums Reservieren.
Schwerpunkt ist die Berechtigung: die Galerie ist frei zugänglich, also
darf ein offener release-Endpunkt nicht fremde Reservierungen löschen
können.
"""
import pytest
@pytest.fixture
def item_id(client, kategorie_id):
return client.post(
"/api/v1/items", json={"size": "98/104", "category_id": kategorie_id}
).json()["id"]
def test_reservieren(client, item_id):
antwort = client.post(
f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "Familie Meier"}
)
assert antwort.status_code == 200
daten = antwort.json()
assert daten["item"]["status"] == "reserved"
assert daten["item"]["reserved_by"] == "Familie Meier"
assert len(daten["reservation_token"]) > 20
def test_zweimal_reservieren_geht_nicht(client, item_id):
client.post(f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "A"})
zweiter = client.post(f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "B"})
assert zweiter.status_code == 409
def test_freigeben_mit_token(client, item_id):
token = client.post(
f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "Familie Meier"}
).json()["reservation_token"]
antwort = client.post(f"/api/v1/items/{item_id}/release?token={token}")
assert antwort.status_code == 200
assert antwort.json()["status"] == "available"
assert antwort.json()["reserved_by"] is None
def test_freigeben_ohne_token_wird_abgewiesen(client, gast, item_id):
"""Der Kern der Entscheidung: sonst löscht jeder Besucher fremde
Reservierungen.
Bewusst als Gast geprüft - der angemeldete Betreiber darf das sehr wohl
ohne Token, damit er eine Missbrauchswelle aufräumen kann.
"""
client.post(f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "Familie Meier"})
client.post("/api/v1/auth/logout")
assert gast.post(f"/api/v1/items/{item_id}/release").status_code == 403
assert gast.post(f"/api/v1/items/{item_id}/release?token=falsch").status_code == 403
# Reservierung steht unverändert
item = gast.get(f"/api/v1/items/{item_id}").json()
assert item["status"] == "reserved"
assert item["reserved_by"] == "Familie Meier"
def test_token_gilt_nach_dem_freigeben_nicht_mehr(client, gast, item_id):
"""Sonst könnte ein alter Link später die Reservierung einer anderen
Person aufheben."""
token = client.post(
f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "Erste"}
).json()["reservation_token"]
client.post(f"/api/v1/items/{item_id}/release?token={token}")
client.post(f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "Zweite"})
client.post("/api/v1/auth/logout")
assert gast.post(f"/api/v1/items/{item_id}/release?token={token}").status_code == 403
def test_abgegeben_loescht_den_namen(client, item_id):
"""Datensparsam - die Galerie ist öffentlich."""
client.post(f"/api/v1/items/{item_id}/reserve", json={"reserved_by": "Familie Meier"})
antwort = client.post(f"/api/v1/items/{item_id}/mark-given").json()
assert antwort["status"] == "given_away"
assert antwort["reserved_by"] is None
def test_name_ohne_werbung(client, item_id):
antwort = client.post(
f"/api/v1/items/{item_id}/reserve",
json={"reserved_by": "Jetzt billig kaufen http://spam.example"},
)
assert antwort.status_code == 422
def test_wettlauf_beim_reservieren_gewinnt_nur_einer(client, item_id, db_sitzung):
"""Zwei Anfragen können beide die Statusprüfung in der Route passieren,
bevor eine schreibt. Das UPDATE mit Status-Bedingung lässt trotzdem nur
eine gewinnen - die zweite bekommt None und überschreibt nichts."""
from app import crud
item = crud.item_holen(db_sitzung, item_id)
erster = crud.reservieren(db_sitzung, item, "A")
zweiter = crud.reservieren(db_sitzung, item, "B")
assert erster is not None
assert zweiter is None
assert item.reserved_by == "A"
assert item.reservation_token is not None