Docker-Image bauen / build (push) Successful in 1m7s
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
114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
"""Gemeinsame Test-Vorbereitung.
|
|
|
|
Jeder Test bekommt eine eigene, frische SQLite-Datei in einem temporären
|
|
Verzeichnis - keine geteilten Zustände zwischen Tests, und die echte
|
|
Datenbank unter data/ wird nie angefasst.
|
|
"""
|
|
|
|
import os
|
|
|
|
TEST_PASSWORT = "test-passwort-123"
|
|
|
|
# MUSS vor dem Import von app.main stehen: dort wird die SessionMiddleware
|
|
# beim Import aufgebaut und liest cookie_nur_https genau einmal. Käme das
|
|
# erst als Fixture, bliebe das Cookie auf "Secure" - und der TestClient
|
|
# spricht über http://testserver, verwürfe es also stillschweigend. Die
|
|
# Anmeldung schlüge dann ohne erkennbaren Grund fehl.
|
|
os.environ.setdefault("COOKIE_HTTPS_ONLY", "false")
|
|
os.environ.setdefault("ADMIN_PASSWORD", TEST_PASSWORT)
|
|
os.environ.setdefault("SECRET_KEY", "nur-fuer-tests")
|
|
# bcrypt ist absichtlich langsam. Bei rund 160 Hash-Vorgängen über
|
|
# alle Tests kostete der Standardaufwand knapp eine Minute; hier
|
|
# geht es um die Ablauflogik, nicht um die Stärke des Hashes.
|
|
os.environ.setdefault("BCRYPT_ROUNDS", "4")
|
|
|
|
import pytest # noqa: E402
|
|
from fastapi.testclient import TestClient # noqa: E402
|
|
from sqlalchemy.orm import sessionmaker # noqa: E402
|
|
|
|
from app import security # noqa: E402
|
|
from app.config import einstellungen # noqa: E402
|
|
from app.database import Basis, engine_erzeugen, get_db # noqa: E402
|
|
from app.main import app # noqa: E402
|
|
from app.seed import stammdaten_anlegen # noqa: E402
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def admin_eingerichtet(monkeypatch):
|
|
"""Setzt ein Passwort für die Tests und leert die Zwischenspeicher.
|
|
|
|
autouse, weil sonst jeder Schreibzugriff mit 503 ("nicht eingerichtet")
|
|
scheitern würde - das ist das gewollte Verhalten ohne Passwort.
|
|
"""
|
|
monkeypatch.setenv("ADMIN_PASSWORD", TEST_PASSWORT)
|
|
monkeypatch.setenv("COOKIE_HTTPS_ONLY", "false")
|
|
einstellungen.cache_clear()
|
|
security.hash_zwischenspeicher_leeren()
|
|
# Rate-Limits zwischen Tests zurücksetzen, sonst schlägt der zehnte
|
|
# Anmeldeversuch über alle Tests hinweg zu.
|
|
security.limiter.reset()
|
|
yield
|
|
einstellungen.cache_clear()
|
|
security.hash_zwischenspeicher_leeren()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def upload_ordner(tmp_path, monkeypatch):
|
|
"""Jeder Test lädt in ein eigenes Verzeichnis hoch.
|
|
|
|
Ohne das schrieben die Tests in das echte uploads/ und liessen dort
|
|
ihre Bilder liegen. autouse, damit das auch für Tests gilt, die den
|
|
Ordner nicht selbst anfordern.
|
|
"""
|
|
ziel = tmp_path / "uploads"
|
|
ziel.mkdir()
|
|
monkeypatch.setenv("UPLOAD_PATH", str(ziel))
|
|
einstellungen.cache_clear()
|
|
yield ziel
|
|
einstellungen.cache_clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def db_sitzung(tmp_path):
|
|
engine = engine_erzeugen(f"sqlite:///{tmp_path/'test.sqlite'}")
|
|
Basis.metadata.create_all(engine)
|
|
Sitzung = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
|
|
with Sitzung() as db:
|
|
stammdaten_anlegen(db)
|
|
yield db
|
|
engine.dispose()
|
|
|
|
|
|
@pytest.fixture
|
|
def gast(db_sitzung):
|
|
"""Nicht angemeldeter Besucher - sieht die Galerie, darf reservieren.
|
|
|
|
Bewusst ohne "with": nur dann startet Starlette den Lifespan, und der
|
|
würde die echte Datenbank unter data/ anlegen und befüllen. Die
|
|
Stammdaten stehen in der Testdatenbank ohnehin schon (db_sitzung).
|
|
"""
|
|
|
|
def _db():
|
|
yield db_sitzung
|
|
|
|
app.dependency_overrides[get_db] = _db
|
|
yield TestClient(app)
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def client(gast):
|
|
"""Angemeldeter Betreiber - darf alles.
|
|
|
|
Die meisten Tests brauchen das, weil Anlegen und Ändern seit Phase 3
|
|
eine Anmeldung voraussetzen.
|
|
"""
|
|
antwort = gast.post("/api/v1/auth/login", json={"passwort": TEST_PASSWORT})
|
|
assert antwort.status_code == 200, antwort.text
|
|
return gast
|
|
|
|
|
|
@pytest.fixture
|
|
def kategorie_id(gast):
|
|
return gast.get("/api/v1/categories").json()[0]["id"]
|