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:
parent
50442f203a
commit
8f3a811a83
168
tests/test_og_card.py
Normal file
168
tests/test_og_card.py
Normal 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()
|
||||||
Loading…
Reference in New Issue
Block a user