"""Die Bild-Route ist die einzige, die je Tabellenzeile einmal aufgerufen wird. Was hier schiefgeht, vervielfacht sich also mit der Länge der Liste – und weil ein Browser nur ~6 Verbindungen je Host offen hält, warten alle übrigen Abfragen der Seite dahinter. Genau daran lag das halb geladene Bild der Artikelseite. Deshalb halten diese Tests drei Zusagen fest: 1. Die Auslieferung geht **nie** ins Netz. 2. Ein unverändertes Bild wird **nicht erneut** übertragen. 3. Ein Vorschaubild ist ein **kleines** Bild, nicht das Original. """ import io import pytest from fastapi import BackgroundTasks, HTTPException from app.models import BaseUnit, Product, ProductImage from app.routers.products import get_product_image from app.services import images class FakeRequest: """Nur die Kopfzeilen – mehr braucht die Route nicht.""" def __init__(self, **kopf): self.headers = {k.lower().replace("_", "-"): v for k, v in kopf.items()} def _artikel(db, url=None): p = Product(name="Pesto", base_unit=BaseUnit.gram, image_url=url) db.add(p) db.commit() db.refresh(p) return p def _jpeg(breite=1600, hoehe=1200) -> bytes: """Ein Bild in Handy-Größe – kein Platzhalter, sonst misst der Test nichts.""" Image = pytest.importorskip("PIL.Image") bild = Image.new("RGB", (breite, hoehe)) # Rauschen, damit JPEG nicht auf ein paar Byte zusammenfällt. bild.putdata([(x * 7 % 256, x * 13 % 256, x * 29 % 256) for x in range(breite * hoehe)]) puffer = io.BytesIO() bild.save(puffer, format="JPEG", quality=90) return puffer.getvalue() def _hole(db, artikel, **kwargs): return get_product_image( artikel.id, request=kwargs.pop("request", FakeRequest()), background_tasks=kwargs.pop("background_tasks", BackgroundTasks()), thumb=kwargs.pop("thumb", False), db=db, _=None, ) def test_auslieferung_geht_nie_ins_netz(db, monkeypatch): """Der eigentliche Fehler: ``ensure`` holte das Bild MITTEN in der Anfrage. Bei einem toten Link kostete das bis zu 15 Sekunden – je Tabellenzeile, und bei jedem Aufruf aufs Neue, weil ein Fehlschlag nirgends vermerkt wurde. """ monkeypatch.setattr( images, "fetch", lambda url: pytest.fail("Die Auslieferung darf nicht ins Netz") ) artikel = _artikel(db, "https://example.org/pesto.jpg") with pytest.raises(HTTPException) as fehler: _hole(db, artikel) assert fehler.value.status_code == 404 def test_fehlende_kopie_wird_im_hintergrund_nachgeholt(db, monkeypatch): """Nachgeholt wird weiterhin – nur eben nach der Antwort.""" monkeypatch.setattr(images, "fetch", lambda url: (b"x", "image/png")) artikel = _artikel(db, "https://example.org/pesto.jpg") aufgaben = BackgroundTasks() with pytest.raises(HTTPException): _hole(db, artikel, background_tasks=aufgaben) assert [t.func for t in aufgaben.tasks] == [images.nachholen] def test_ohne_bildadresse_gibt_es_nichts_nachzuholen(db, monkeypatch): monkeypatch.setattr( images, "fetch", lambda url: pytest.fail("Es gibt keine Adresse zum Holen") ) aufgaben = BackgroundTasks() with pytest.raises(HTTPException): _hole(db, _artikel(db), background_tasks=aufgaben) assert aufgaben.tasks == [] def test_toter_link_wird_nicht_bei_jedem_aufruf_erneut_versucht(db, monkeypatch): """Ohne Sperre klopft jede Tabellenzeile bei jedem Seitenaufruf erneut an.""" images.sperre_zuruecksetzen() monkeypatch.setattr(images, "fetch", lambda url: None) # Abruf schlägt fehl artikel = _artikel(db, "https://example.org/weg.jpg") erste = BackgroundTasks() with pytest.raises(HTTPException): _hole(db, artikel, background_tasks=erste) assert len(erste.tasks) == 1 # Die Hintergrundaufgabe oeffnet sonst eine eigene Sitzung – hier soll es # die des Fixtures sein, und schliessen darf sie sie nicht. monkeypatch.setattr(db, "close", lambda: None) monkeypatch.setattr("app.database.SessionLocal", lambda: db) images.nachholen(artikel.id, artikel.image_url) # …und scheitert zweite = BackgroundTasks() with pytest.raises(HTTPException): _hole(db, artikel, background_tasks=zweite) assert zweite.tasks == [] def test_unveraendertes_bild_wird_nicht_erneut_uebertragen(db): """Ohne 304 lädt der Browser nach Ablauf der Frist jedes Bild komplett neu.""" artikel = _artikel(db) db.add(ProductImage(product_id=artikel.id, content_type="image/jpeg", data=_jpeg())) db.commit() erste = _hole(db, artikel) marke = erste.headers["etag"] assert len(erste.body) > 0 zweite = _hole(db, artikel, request=FakeRequest(if_none_match=marke)) assert zweite.status_code == 304 assert zweite.body == b"" def test_vorschaubild_ist_ein_bruchteil_des_originals(db): """34x34 Pixel auf dem Schirm dürfen nicht 2 MB auf der Leitung sein.""" original = _jpeg() artikel = _artikel(db) db.add(ProductImage(product_id=artikel.id, content_type="image/jpeg", data=original)) db.commit() gross = _hole(db, artikel) klein = _hole(db, artikel, thumb=True) assert len(gross.body) == len(original) assert len(klein.body) < len(original) / 20 # Verschiedene Inhalte brauchen verschiedene Marken, sonst liefert der # Zwischenspeicher des Browsers das eine fuer das andere aus. assert klein.headers["etag"] != gross.headers["etag"] def test_vorschau_bleibt_erhalten_und_ruehrt_den_zeitstempel_nicht_an(db): """Sie wird beim ersten Abruf nachgezogen – aber das Bild ist nicht neu.""" artikel = _artikel(db) bild = ProductImage(product_id=artikel.id, content_type="image/jpeg", data=_jpeg()) db.add(bild) db.commit() db.refresh(bild) vorher = bild.updated_at _hole(db, artikel, thumb=True) db.expire_all() frisch = db.get(ProductImage, artikel.id) assert frisch.thumb is not None assert frisch.updated_at == vorher