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