Der Testlauf war zu langsam. Gemessen statt geraten: 0,68 s Vorbereitung
pro Test, bei 81 Tests praktisch die ganze Laufzeit.
Ursache war bcrypt. Pro Test wird ein Hash gebildet und geprüft, und das
ist absichtlich langsam - genau das soll es im Betrieb sein. Der Aufwand
ist jetzt über BCRYPT_ROUNDS einstellbar (Standard bleibt 12) und in den
Tests auf 4 gesetzt: dort geht es um die Ablauflogik, nicht um die Stärke
des Hashes.
Der Dekompressionsbomben-Test erzeugte ein Bild mit 400 Megapixeln, allein
dafür 4,8 s. Jetzt wird stattdessen die Grenze heruntergesetzt und ein
kleines Bild verwendet - dieselbe Codestelle, ohne die Wartezeit.
Ausserdem installierten Test- und Anwendungs-Image dieselben
Abhängigkeiten zweimal. Dockerfile.test setzt nun auf dem gebauten
Anwendungs-Image auf, der Testschritt kommt entsprechend danach.
Dabei zwei Fallen, die beide auffielen, weil die Tests plötzlich wieder
langsam waren:
- Das Basis-Image bringt seinen eigenen Stand von app/ mit. Ohne erneutes
Kopieren prüfen die Tests den Code des Basis-Images - ist es veraltet,
läuft alles gegen alten Code und meldet Erfolg.
- Der ENTRYPOINT des Anwendungs-Images startet eine Datenbank-Migration.
Für Tests weder nötig noch erwünscht, darum geleert.
Ganze Kette lokal durchgespielt: bauen, testen, Image prüfen - 22 s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
Lesen darf jeder, schreiben nur der Betreiber - mit der bewussten Ausnahme
des Reservierens. Dazu Dockerfile und Compose-Datei, damit sich das lokal
ausprobieren lässt.
Beim Bauen sind zwei Fehler aufgefallen, die ohne Test nicht aufgefallen
wären:
1. slowapi zählt pro URL-Pfad. Weil jedes Kleidungsstück eine eigene URL
hat, bekam jedes seinen eigenen Zähler - ein Skript hätte also den
gesamten Bestand reservieren können, ohne je an ein Limit zu stossen.
Genau der Missbrauch, gegen den das Limit gedacht ist. Behoben mit
shared_limit und festem scope.
2. Der erste Anlauf des Tests machte fünf Anfragen gegen ein Limit von
fünf und konnte damit gar nichts zeigen. Geprüft wird jetzt der
tatsächlich ausgelieferte Standardwert, mit mehr Anfragen als erlaubt.
Weiter umgesetzt:
- Passwort als bcrypt-Hash aus der Umgebung, einmal beim Start gebildet
und gemerkt. Bei jeder Anfrage neu gehasht liesse sich die Anwendung
sonst ohne Anmeldung lahmlegen - bcrypt ist absichtlich langsam.
- Ohne hinterlegtes Passwort bleibt der Erfassungsbereich gesperrt (503)
statt offen zu stehen. Kein mitgeliefertes Standardpasswort.
- Sitzung als signiertes Cookie, HttpOnly, SameSite=Lax (blockt
seitenfremde POSTs), Secure abschaltbar nur fürs lokale Testen,
Abmeldung nach zwei Stunden Ruhe.
- CSP mit script-src 'self', nosniff, frame-ancestors none, dazu noindex
und robots.txt: die Galerie ist frei zugänglich, soll aber nicht
dauerhaft im Suchindex stehen.
- Der Betreiber darf Reservierungen ohne Token aufheben, damit sich eine
Missbrauchswelle aufräumen lässt.
- uvicorn mit --proxy-headers: hinter einem Reverse-Proxy zählte sonst
alles auf dessen IP, und ein einzelner Besucher sperrte alle aus.
- Container läuft nicht als root; Code gehört root, nur Daten und Bilder
dem Dienstbenutzer.
Umgebungsvariablen heissen jetzt ausdrücklich englisch (ADMIN_PASSWORD,
SECRET_KEY, ...), passend zur Anleitung und zur Kantone-App.
59 Tests, alle grün. Zusätzlich gegen den laufenden Container geprüft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
FastAPI mit SQLAlchemy und SQLite, Migrationen über Alembic. Die
Endpunkte aus projekt.md für items, categories und sizes stehen samt
Reservierung; Bilder und Anmeldung folgen in Phase 2 und 3.
Umgesetzt wie in Plan.md festgelegt:
- Feste Wertelisten als Enum UND als CHECK in der Datenbank. Die
CHECK-Bedingung ist der eigentliche Schutz: an SQLAlchemy vorbei (Import,
sqlite3 von Hand) käme sonst "Gril" durch, und die Filter griffen still
nicht mehr.
- Status "draft" für den Stapel-Import. Entwürfe erscheinen weder in der
Galerie noch in GET /items, solange nicht ausdrücklich status=draft
angefragt wird, und lassen sich nicht reservieren.
- Titel ist freiwillig und wird sonst beim ANZEIGEN aus Kategorie und
Grösse gebildet ("Jacken 98/104") - nicht beim Speichern, damit er einer
späteren Korrektur der Grösse folgt.
- reservation_token: nur wer es hat, kann die eigene Reservierung aufheben.
Verglichen mit compare_digest, gelöscht beim Freigeben und beim
Erledigen, damit ein alter Link nicht später eine fremde Reservierung
aufhebt.
- Eigene sizes-Tabelle mit sort_order statt SELECT DISTINCT: sonst stünde
"104" vor "56" und jeder Tippfehler würde zur Filteroption.
- Pagination auf GET /items, in projekt.md nicht vorgesehen.
Zwei SQLite-Eigenheiten, die leicht untergehen: foreign_keys ist
standardmässig AUS (ohne PRAGMA greift ON DELETE CASCADE nicht), und
check_same_thread muss für FastAPI abgeschaltet werden. Beides in
database.py, dazu WAL fürs gleichzeitige Lesen.
24 Tests, alle grün. Zusätzlich von Hand geprüft: Migration anwenden,
Eintrag anlegen, reservieren, ohne Token freigeben (403), mit Token
freigeben (200), Swagger UI erreichbar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o