Files
StefanandClaude Opus 5 e0212cfa2c
Docker-Image bauen / build (push) Successful in 1m7s
Tests von 55 auf 12 Sekunden, Abhängigkeiten nur noch einmal installieren
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
2026-08-30 14:45:24 +02:00

81 lines
3.6 KiB
Python

"""Einstellungen, ausschliesslich aus Umgebungsvariablen.
Nichts davon steht im Code oder im Image: Passwort und Pfade kommen beim
Start von aussen. Ohne gesetztes Passwort bleibt der Erfassungsbereich
gesperrt (siehe Phase 3) - ein mitgeliefertes Standardpasswort wäre
öffentlich bekannt und damit keines.
"""
from functools import lru_cache
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Einstellungen(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
# Die Namen der Umgebungsvariablen sind ausdrücklich angegeben und
# englisch - sie stehen in docker-compose.yml und in der Anleitung, und
# dort wären deutsche Namen neben ADMIN_PASSWORD & Co. verwirrend.
# Ablageorte. Im Container zeigen beide auf eingehängte Volumes, damit
# Datenbank und Bilder einen Neustart überleben.
datenbank_pfad: Path = Field(default=Path("data/kleiderboerse.sqlite"),
validation_alias="DATABASE_PATH")
upload_pfad: Path = Field(default=Path("uploads"), validation_alias="UPLOAD_PATH")
# Passwort für den Erfassungsbereich. Leer = gesperrt.
admin_passwort: str = Field(default="", validation_alias="ADMIN_PASSWORD")
admin_passwort_hash: str = Field(default="", validation_alias="ADMIN_PASSWORD_HASH")
# Schlüssel, mit dem das Sitzungs-Cookie signiert wird. Ohne festen Wert
# wird beim Start einer erzeugt - dann ist man nach jedem Neustart
# abgemeldet. Im Betrieb also setzen.
secret_key: str = Field(default="", validation_alias="SECRET_KEY")
# Secure-Kennzeichen am Sitzungs-Cookie. Standard an; nur zum Testen
# über http://localhost abschalten, sonst schickt der Browser das
# Cookie gar nicht erst mit.
cookie_nur_https: bool = Field(default=True, validation_alias="COOKIE_HTTPS_ONLY")
# Grösse eines einzelnen Bild-Uploads. Greift zusätzlich zur Prüfung in
# images.py; ein Handyfoto liegt heute bei 3-12 MB.
max_upload_bytes: int = Field(default=10 * 1024 * 1024,
validation_alias="MAX_UPLOAD_BYTES")
# Rate-Limits (pro IP). Reservieren ist ohne Anmeldung möglich, also
# steht hier nichts anderes zwischen einem Skript und dem Bestand.
limit_reservieren: str = Field(default="5/hour", validation_alias="RATE_LIMIT_RESERVE")
limit_anmelden: str = Field(default="10/hour", validation_alias="RATE_LIMIT_LOGIN")
# Rechenaufwand für das Passwort-Hashing. 12 ist der sinnvolle
# Standard; bcrypt ist absichtlich langsam, damit sich Passwörter
# nicht durchprobieren lassen. In Tests wird der Wert gesenkt -
# sonst kostet jeder Test rund eine halbe Sekunde nur dafür.
bcrypt_runden: int = Field(default=12, validation_alias="BCRYPT_ROUNDS")
# Nach dieser Zeit ohne Aktivität wird abgemeldet.
sitzung_max_ruhe_sekunden: int = Field(default=7200,
validation_alias="SESSION_IDLE_SECONDS")
@property
def datenbank_url(self) -> str:
return f"sqlite:///{self.datenbank_pfad}"
@property
def admin_aktiv(self) -> bool:
"""Ohne hinterlegtes Passwort bleibt der Erfassungsbereich gesperrt.
Bewusst kein mitgeliefertes Standardpasswort: das stünde in der
Anleitung und wäre damit allgemein bekannt.
"""
return bool(self.admin_passwort_hash or self.admin_passwort)
@lru_cache
def einstellungen() -> Einstellungen:
"""Einmal lesen und merken - die Werte ändern sich zur Laufzeit nicht."""
return Einstellungen()