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>
289 lines
10 KiB
Python
289 lines
10 KiB
Python
"""Tests der sichtbaren Seiten."""
|
|
|
|
import io
|
|
import re
|
|
|
|
from PIL import Image
|
|
|
|
|
|
def bild_bytes() -> bytes:
|
|
puffer = io.BytesIO()
|
|
Image.new("RGB", (600, 800), (120, 160, 200)).save(puffer, "JPEG")
|
|
return puffer.getvalue()
|
|
|
|
|
|
def csrf_aus(html: str) -> str:
|
|
treffer = re.search(r'name="csrf" value="([^"]+)"', html)
|
|
assert treffer, "kein CSRF-Token im Formular"
|
|
return treffer.group(1)
|
|
|
|
|
|
def erfasse(client, kategorie_id, **felder):
|
|
"""Legt über das Formular ein Kleidungsstück an."""
|
|
seite = client.get("/erfassen")
|
|
daten = {
|
|
"csrf": csrf_aus(seite.text),
|
|
"size": "98/104",
|
|
"category_id": str(kategorie_id),
|
|
"gender": "unisex",
|
|
"season": "all_year",
|
|
"condition": "good",
|
|
"title": "",
|
|
"description": "",
|
|
}
|
|
daten.update(felder)
|
|
return client.post("/erfassen", data=daten, follow_redirects=True)
|
|
|
|
|
|
# --------------------------------------------------------------- Galerie ---
|
|
|
|
def test_galerie_ist_ohne_anmeldung_sichtbar(gast):
|
|
antwort = gast.get("/")
|
|
assert antwort.status_code == 200
|
|
assert "Kleider-Börse" in antwort.text
|
|
|
|
|
|
def test_galerie_zeigt_erfasstes(client, gast, kategorie_id):
|
|
erfasse(client, kategorie_id, title="Rote Winterjacke")
|
|
client.post("/abmelden", data={"csrf": csrf_aus(client.get("/").text)})
|
|
|
|
seite = gast.get("/")
|
|
assert "Rote Winterjacke" in seite.text
|
|
|
|
|
|
def test_abgeleiteter_titel_erscheint(client, kategorie_id):
|
|
"""Titel leer gelassen - dann bildet ihn die Anwendung."""
|
|
erfasse(client, kategorie_id, size="80")
|
|
seite = client.get("/")
|
|
assert "80" in seite.text
|
|
|
|
|
|
def test_filter_liefert_nur_die_liste(client, kategorie_id):
|
|
"""HTMX tauscht nur den Ausschnitt aus, nicht die ganze Seite."""
|
|
erfasse(client, kategorie_id, size="80", title="Kleines Teil")
|
|
erfasse(client, kategorie_id, size="122", title="Grosses Teil")
|
|
|
|
ausschnitt = client.get("/teil/liste?size=80")
|
|
assert ausschnitt.status_code == 200
|
|
assert "Kleines Teil" in ausschnitt.text
|
|
assert "Grosses Teil" not in ausschnitt.text
|
|
# Kein vollständiges Dokument
|
|
assert "<html" not in ausschnitt.text.lower()
|
|
|
|
|
|
def test_entwuerfe_tauchen_nicht_in_der_galerie_auf(client, db_sitzung):
|
|
from app import crud
|
|
|
|
crud.entwuerfe_anlegen(db_sitzung, 3)
|
|
assert "Nachtragen" not in client.get("/").text
|
|
|
|
|
|
# ------------------------------------------------------------- Erfassung ---
|
|
|
|
def test_erfassen_braucht_anmeldung(gast):
|
|
assert gast.get("/erfassen").status_code == 401
|
|
|
|
|
|
def test_erfassen_und_anzeigen(client, kategorie_id):
|
|
antwort = erfasse(client, kategorie_id, title="Blaue Hose")
|
|
assert antwort.status_code == 200
|
|
assert "gespeichert" in antwort.text
|
|
|
|
detail = client.get("/kleid/1")
|
|
assert detail.status_code == 200
|
|
assert "Blaue Hose" in detail.text
|
|
|
|
|
|
def test_werte_werden_fuer_das_naechste_teil_gemerkt(client, kategorie_id):
|
|
"""Beim Abarbeiten einer Kiste ändert sich meist nur der Titel."""
|
|
erfasse(client, kategorie_id, size="122", gender="girl")
|
|
formular = client.get("/erfassen").text
|
|
|
|
assert re.search(r'<option value="122"\s+selected', formular)
|
|
assert re.search(r'<option value="girl"\s+selected', formular)
|
|
|
|
|
|
def test_formular_ohne_token_wird_abgewiesen(client, kategorie_id):
|
|
antwort = client.post("/erfassen", data={
|
|
"csrf": "erfunden", "size": "80", "category_id": str(kategorie_id),
|
|
"gender": "unisex", "season": "all_year", "condition": "good",
|
|
"title": "", "description": "",
|
|
})
|
|
assert antwort.status_code == 400
|
|
|
|
|
|
# ----------------------------------------------------------- Stapelweise ---
|
|
|
|
def test_stapel_import_ueber_das_formular(client):
|
|
seite = client.get("/erfassen")
|
|
antwort = client.post(
|
|
"/stapel",
|
|
data={"csrf": csrf_aus(seite.text)},
|
|
files=[("dateien", (f"f{i}.jpg", bild_bytes(), "image/jpeg")) for i in range(4)],
|
|
follow_redirects=True,
|
|
)
|
|
assert antwort.status_code == 200
|
|
assert "4 Fotos hochgeladen" in antwort.text
|
|
assert "Unfertige Einträge (4)" in antwort.text
|
|
|
|
|
|
def test_entwurf_nachtragen_macht_ihn_sichtbar(client, kategorie_id):
|
|
seite = client.get("/erfassen")
|
|
client.post(
|
|
"/stapel",
|
|
data={"csrf": csrf_aus(seite.text)},
|
|
files={"dateien": ("f.jpg", bild_bytes(), "image/jpeg")},
|
|
follow_redirects=True,
|
|
)
|
|
|
|
formular = client.get("/kleid/1/bearbeiten")
|
|
assert formular.status_code == 200
|
|
assert "Entwurf nachtragen" in formular.text
|
|
|
|
fertig = client.post("/kleid/1/bearbeiten", data={
|
|
"csrf": csrf_aus(formular.text), "size": "92",
|
|
"category_id": str(kategorie_id), "gender": "boy",
|
|
"season": "all_year", "condition": "good", "title": "", "description": "",
|
|
}, follow_redirects=True)
|
|
assert fertig.status_code == 200
|
|
|
|
# Jetzt in der Galerie
|
|
galerie = client.get("/")
|
|
assert "92" in galerie.text
|
|
|
|
|
|
# ---------------------------------------------------------- Reservierung ---
|
|
|
|
def test_reservieren_zeigt_den_freigabe_link(client, gast, kategorie_id):
|
|
erfasse(client, kategorie_id, title="Warme Jacke")
|
|
client.post("/abmelden", data={"csrf": csrf_aus(client.get("/").text)})
|
|
|
|
detail = gast.get("/kleid/1")
|
|
antwort = gast.post("/kleid/1/reservieren", data={
|
|
"csrf": csrf_aus(detail.text), "reserved_by": "Familie Meier",
|
|
})
|
|
assert antwort.status_code == 200
|
|
assert "Reserviert für dich" in antwort.text
|
|
|
|
# Absolute Adresse - der Link soll sich aufbewahren und weiterschicken
|
|
# lassen, ein reiner Pfad nützte dabei nichts.
|
|
link = re.search(r'value="(https?://[^"]*/kleid/1/freigeben\?token=[^"]+)"',
|
|
antwort.text)
|
|
assert link, f"kein Freigabe-Link angezeigt:\n{antwort.text[-600:]}"
|
|
|
|
# Der Link hebt die Reservierung wieder auf
|
|
auf = gast.get(link.group(1), follow_redirects=True)
|
|
assert "aufgehoben" in auf.text
|
|
|
|
|
|
def test_name_bleibt_oeffentlich_verborgen(client, gast, kategorie_id):
|
|
"""Die Galerie ist frei zugänglich - der Name gehört nicht hinein."""
|
|
erfasse(client, kategorie_id, title="Warme Jacke")
|
|
detail = client.get("/kleid/1")
|
|
client.post("/kleid/1/reservieren", data={
|
|
"csrf": csrf_aus(detail.text), "reserved_by": "Familie Geheim",
|
|
})
|
|
client.post("/abmelden", data={"csrf": csrf_aus(client.get("/").text)})
|
|
|
|
assert "Familie Geheim" not in gast.get("/").text
|
|
assert "Familie Geheim" not in gast.get("/kleid/1").text
|
|
assert "reserviert" in gast.get("/kleid/1").text
|
|
|
|
|
|
def test_betreiber_sieht_den_namen(client, kategorie_id):
|
|
erfasse(client, kategorie_id, title="Warme Jacke")
|
|
detail = client.get("/kleid/1")
|
|
client.post("/kleid/1/reservieren", data={
|
|
"csrf": csrf_aus(detail.text), "reserved_by": "Familie Meier",
|
|
})
|
|
assert "Familie Meier" in client.get("/kleid/1").text
|
|
|
|
|
|
def test_falscher_freigabe_link_aendert_nichts(client, gast, kategorie_id):
|
|
erfasse(client, kategorie_id)
|
|
detail = client.get("/kleid/1")
|
|
client.post("/kleid/1/reservieren", data={
|
|
"csrf": csrf_aus(detail.text), "reserved_by": "Wer auch immer",
|
|
})
|
|
client.post("/abmelden", data={"csrf": csrf_aus(client.get("/").text)})
|
|
|
|
antwort = gast.get("/kleid/1/freigeben?token=daneben", follow_redirects=True)
|
|
assert "passt nicht" in antwort.text
|
|
assert "reserviert" in gast.get("/kleid/1").text
|
|
|
|
|
|
# ---------------------------------------------------------------- Seiten ---
|
|
|
|
def test_seiten_laden_nichts_von_fremden_servern(client, kategorie_id):
|
|
"""Sonst blockiert die CSP es - und die Seite bliebe leer."""
|
|
erfasse(client, kategorie_id)
|
|
for pfad in ["/", "/kleid/1", "/erfassen", "/anmelden", "/entwuerfe"]:
|
|
html = client.get(pfad).text
|
|
fremde = re.findall(r'(?:src|href)="(?:https?:)?//[^"]+"', html)
|
|
assert not fremde, f"{pfad} lädt von aussen: {fremde}"
|
|
|
|
|
|
# --------------------------------------------------------- Darstellung ---
|
|
|
|
def test_fusszeile_zeigt_die_version(client, monkeypatch):
|
|
"""Die CI reicht Tag und Commit durch; lokal steht der Build-Zeitpunkt."""
|
|
from app.version import version_info
|
|
|
|
version_info.cache_clear()
|
|
monkeypatch.setenv("APP_VERSION", "1.2.3")
|
|
monkeypatch.setenv("APP_REVISION", "abcdef1234567")
|
|
try:
|
|
assert "Version 1.2.3" in client.get("/").text
|
|
finally:
|
|
version_info.cache_clear()
|
|
|
|
|
|
def test_ohne_tag_erscheint_der_commit(client, monkeypatch):
|
|
from app.version import version_info
|
|
|
|
version_info.cache_clear()
|
|
monkeypatch.setenv("APP_VERSION", "")
|
|
monkeypatch.setenv("APP_REVISION", "abcdef1234567")
|
|
try:
|
|
assert "Commit abcdef1" in client.get("/").text
|
|
finally:
|
|
version_info.cache_clear()
|
|
|
|
|
|
def test_farbschema_skript_laeuft_vor_dem_zeichnen(client):
|
|
"""Ohne das blitzt beim Laden kurz die helle Fassung auf."""
|
|
html = client.get("/").text
|
|
theme = html.index('src="/static/theme.js"')
|
|
koerper = html.index("<body")
|
|
assert theme < koerper, "theme.js muss vor <body> stehen"
|
|
# und darf nicht verzögert werden
|
|
zeile = html[html.rindex("<script", 0, theme):html.index(">", theme) + 1]
|
|
assert "defer" not in zeile and "async" not in zeile
|
|
|
|
|
|
def test_umschalter_ist_vorhanden(client):
|
|
assert 'id="schema-knopf"' in client.get("/").text
|
|
|
|
|
|
# ------------------------------------------------- Unsinnige Eingaben ---
|
|
|
|
def test_unsinniges_limit_ist_kein_serverfehler(gast):
|
|
"""?limit= kommt aus der URL - Buchstaben oder negative Werte dürfen
|
|
keinen 500er auslösen, sondern fallen auf die Seitengrösse zurück."""
|
|
assert gast.get("/?limit=abc").status_code == 200
|
|
assert gast.get("/teil/liste?limit=-5").status_code == 200
|
|
|
|
|
|
def test_unsinnige_kategorie_gibt_meldung_statt_500(client, kategorie_id):
|
|
antwort = erfasse(client, kategorie_id, category_id="abc")
|
|
assert antwort.status_code == 200
|
|
assert "Bitte die Angaben prüfen" in antwort.text
|
|
|
|
|
|
def test_zu_langer_titel_gibt_meldung_statt_500(client, kategorie_id):
|
|
"""maxlength im HTML schützt nur den Browserweg - die Anwendung muss
|
|
es selbst abfangen."""
|
|
antwort = erfasse(client, kategorie_id, title="x" * 101)
|
|
assert antwort.status_code == 200
|
|
assert "Bitte die Angaben prüfen" in antwort.text
|