"""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()