test(#134): app/og_card.py Coverage 44% → 100%

10 Tests in test_og_card.py:
- TestCacheKey: deterministisch, aenderungs-empfindlich, 16 Zeichen lang
- TestGetCached: Pfad-Lookup mit/ohne Datei
- TestRenderOgCard: Cache-Hit vs Cache-Miss, URL-Encoding der DS,
  Playwright-Exception → None, cache_dir wird angelegt

Playwright wird ueber sys.modules-Stub eingehaengt, sync_playwright()
liefert einen ContextManager mit gemocktem Browser/Page-Stack — keine
echte Chromium-Installation noetig fuer den lokalen Run.

cache_key/get_cached-Tests waren bisher in test_wahlprogramm_fetch.py
verstreut; bleiben dort als Smoke, das eigentliche Modul-Test-File ist
jetzt test_og_card.py.
This commit is contained in:
Dotty Dotter 2026-04-28 08:40:20 +02:00
parent 50442f203a
commit 8f3a811a83

168
tests/test_og_card.py Normal file
View File

@ -0,0 +1,168 @@
"""Tests fuer app/og_card.py — render_og_card mit Cache + Playwright (#134, #141).
Tests fuer cache_key + get_cached lebten vorher in test_wahlprogramm_fetch.py;
hier kommt der Render-Pfad mit gemocktem Playwright dazu, sodass die volle
Coverage von render_og_card lokal lauft.
"""
from __future__ import annotations
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from app.og_card import (
cache_key,
get_cached,
render_og_card,
)
class TestCacheKey:
def test_deterministic(self):
a = cache_key("NRW-18/1", "2026-04-01T00:00:00")
b = cache_key("NRW-18/1", "2026-04-01T00:00:00")
assert a == b
def test_changes_with_updated_at(self):
a = cache_key("NRW-18/1", "2026-04-01T00:00:00")
b = cache_key("NRW-18/1", "2026-04-02T00:00:00")
assert a != b
def test_length_16(self):
assert len(cache_key("NRW-18/1", "x")) == 16
class TestGetCached:
def test_returns_none_when_missing(self, tmp_path):
assert get_cached("NRW-18/1", "2026-04-01T00:00:00", cache_dir=tmp_path) is None
def test_returns_path_when_exists(self, tmp_path):
ds = "NRW-18/1"
upd = "2026-04-01T00:00:00"
key = cache_key(ds, upd)
safe = ds.replace("/", "_")
target = tmp_path / f"{safe}_{key}.png"
target.write_bytes(b"\x89PNG dummy")
result = get_cached(ds, upd, cache_dir=tmp_path)
assert result == target
class TestRenderOgCard:
"""Tests fuer den Render-Pfad. Playwright wird ueber sys.modules-Stub
eingehaengt sync_playwright() liefert einen ContextManager, der
einen gemockten Browser/Page-Stack zurueckgibt."""
def _make_playwright_stub(self, png_bytes: bytes = b"\x89PNG fake"):
"""Erstellt ein Stub-Modul 'playwright.sync_api' mit
``sync_playwright`` als ContextManager, dessen __enter__ einen Mock
liefert, der die Chain pw.chromium.launch().new_page().screenshot()
liefert."""
mod = types.ModuleType("playwright")
sub = types.ModuleType("playwright.sync_api")
page_mock = MagicMock()
page_mock.screenshot.return_value = png_bytes
page_mock.goto.return_value = None
browser_mock = MagicMock()
browser_mock.new_page.return_value = page_mock
browser_mock.close.return_value = None
pw_mock = MagicMock()
pw_mock.chromium.launch.return_value = browser_mock
ctx_mgr = MagicMock()
ctx_mgr.__enter__.return_value = pw_mock
ctx_mgr.__exit__.return_value = False
sub.sync_playwright = MagicMock(return_value=ctx_mgr)
mod.sync_api = sub
return mod, sub, page_mock
def test_cache_hit_skips_playwright(self, tmp_path):
"""Existierender Cache → Playwright wird gar nicht angerufen."""
ds = "NRW-18/1"
upd = "2026-04-01T00:00:00"
key = cache_key(ds, upd)
safe = ds.replace("/", "_")
cache_file = tmp_path / f"{safe}_{key}.png"
cache_file.write_bytes(b"\x89CACHED")
# Wenn der Cache hit ist, sollte playwright NICHT importiert werden.
# Dafuer setzen wir einen Stub, der bei Aufruf einen Test-Fehler triggert.
sys.modules.pop("playwright", None)
sys.modules.pop("playwright.sync_api", None)
with patch.dict(sys.modules, {}, clear=False):
result = render_og_card(ds, upd, cache_dir=tmp_path)
assert result == b"\x89CACHED"
def test_cache_miss_renders_via_playwright(self, tmp_path):
ds = "NRW-18/2"
upd = "2026-04-02T00:00:00"
png = b"\x89PNG rendered"
mod, sub, page_mock = self._make_playwright_stub(png)
with patch.dict(sys.modules, {"playwright": mod, "playwright.sync_api": sub}):
result = render_og_card(ds, upd, cache_dir=tmp_path,
base_url="http://test.example")
assert result == png
# Cache-Datei muss geschrieben sein
key = cache_key(ds, upd)
safe = ds.replace("/", "_")
cache_file = tmp_path / f"{safe}_{key}.png"
assert cache_file.exists()
assert cache_file.read_bytes() == png
def test_cache_miss_passes_drucksache_to_playwright_url(self, tmp_path):
"""URL-Kodierung des Drucksachen-Namens muss ans og-template gehen."""
ds = "NRW-18/123 (neu)" # Sonderzeichen
upd = "2026-04-03T00:00:00"
mod, sub, page_mock = self._make_playwright_stub()
with patch.dict(sys.modules, {"playwright": mod, "playwright.sync_api": sub}):
render_og_card(ds, upd, cache_dir=tmp_path,
base_url="http://internal:8000")
# page.goto wurde aufgerufen — URL-Argument analysieren
call = page_mock.goto.call_args
url = call.args[0]
assert url.startswith("http://internal:8000/v2/og-template?drucksache=")
# / und Klammern muessen URL-encoded sein
assert "%2F" in url
assert "(" not in url # encoded as %28
def test_playwright_exception_returns_none(self, tmp_path):
"""Renderer-Fehler darf den Caller nicht crashen."""
ds = "NRW-18/3"
upd = "2026-04-04T00:00:00"
mod = types.ModuleType("playwright")
sub = types.ModuleType("playwright.sync_api")
def _broken(*a, **kw):
raise RuntimeError("Browser launch failed")
sub.sync_playwright = _broken
mod.sync_api = sub
with patch.dict(sys.modules, {"playwright": mod, "playwright.sync_api": sub}):
result = render_og_card(ds, upd, cache_dir=tmp_path)
assert result is None
# Cache-Datei darf NICHT existieren
key = cache_key(ds, upd)
safe = ds.replace("/", "_")
cache_file = tmp_path / f"{safe}_{key}.png"
assert not cache_file.exists()
def test_cache_dir_created_if_missing(self, tmp_path):
"""render_og_card muss das cache_dir auch anlegen, wenn es fehlt."""
sub_dir = tmp_path / "deep" / "nested" / "cache"
# Existiert noch nicht
assert not sub_dir.exists()
mod, sub, page_mock = self._make_playwright_stub()
with patch.dict(sys.modules, {"playwright": mod, "playwright.sync_api": sub}):
render_og_card("NRW-18/4", "2026-04-05T00:00:00", cache_dir=sub_dir)
assert sub_dir.exists()