"""Static export: pages, art downloads, badge slot, privacy. Offline — cover downloads run against a mock CDN transport.""" from __future__ import annotations import json from pathlib import Path import httpx import pytest from bggpipe.config import Config from bggpipe.export import run_export GAMES = { "240:24621": { "bgg_id": 240, "type": "boardgame", "name": "Britannia", "year": 1986, "description": "A game of & settlement.", "image": "https://cf.geekdo-images.com/orig/pic1.jpg", "thumbnail": "https://cf.geekdo-images.com/thumb/pic1.jpg", "min_players": 3, "max_players": 5, "best_player_counts": ["4"], "playtime": 240, "designers": ["Lewis Pulsipher"], "rank": 1210, "weight": 3.16, "version": { "version_id": 24621, "name": "Avalon Hill second edition", "year": 1987, "publishers": ["Avalon Hill"], "languages": ["English"], }, "source_photos": ["IMG_1.jpeg"], }, "311654": { "bgg_id": 311654, "type": "rpgitem", "name": "Alice is Missing", "year": 2020, "image": "https://cf.geekdo-images.com/orig/pic2.png", "thumbnail": "", "source_photos": ["IMG_2.jpeg"], }, "local:homebrew:x.jpg": { "type": "localgame", "name": "Britannia", # deliberate name collision with 240:24621 "image": "/local-art/abc123.jpg", "source_photos": ["x.jpg"], }, } def _cfg(tmp_path: Path) -> Config: cfg = Config(data_dir=tmp_path / "data") cfg.data_dir.mkdir(parents=True) cfg.games_path.write_text(json.dumps(GAMES)) cfg.local_art_dir.mkdir() (cfg.local_art_dir / "abc123.jpg").write_bytes(b"\xff\xd8local") return cfg def _cdn(requests_log: list[str]) -> httpx.Client: def handler(request: httpx.Request) -> httpx.Response: requests_log.append(str(request.url)) return httpx.Response(200, content=b"\xff\xd8fake-image") return httpx.Client(transport=httpx.MockTransport(handler)) def test_export_writes_pages_art_and_no_shelf_photos(tmp_path): cfg = _cfg(tmp_path) out = tmp_path / "site" log: list[str] = [] summary = run_export( cfg, out, title="Test Shelves", fetch=_cdn(log), sleep=lambda s: None ) assert summary == {"games": 3, "downloaded": 3, "failures": 0} index = (out / "index.html").read_text() assert "Test Shelves" in index and "Powered by BGG" in index assert "trademarks of" in index # name collision -> distinct stable slugs assert (out / "britannia" / "index.html").exists() assert (out / "britannia-2" / "index.html").exists() assert (out / "alice-is-missing" / "index.html").exists() detail = (out / "britannia" / "index.html").read_text() assert "Avalon Hill second edition" in detail assert "<invasions> & settlement" in detail # escaped, not raw # BGG double-encodes entities; the page shows the character, not ’ assert "&rsquo" not in detail assert "boardgamegeek.com/boardgame/240" in detail rpg = (out / "alice-is-missing" / "index.html").read_text() assert "rpggeek.com/rpgitem/311654" in rpg # covers landed locally and pages never hotlink the CDN assert len(list((out / "art").glob("*"))) == 4 # 2 full + 1 thumb + local art all_html = index + detail + rpg assert "geekdo-images.com" not in all_html # the privacy rule: shelf photo names appear nowhere in the output assert "IMG_1.jpeg" not in all_html and "IMG_2.jpeg" not in all_html def test_export_is_idempotent_and_offline_on_rerun(tmp_path): cfg = _cfg(tmp_path) out = tmp_path / "site" run_export(cfg, out, fetch=_cdn([]), sleep=lambda s: None) def refuse(request: httpx.Request) -> httpx.Response: raise AssertionError(f"re-run fetched {request.url}") summary = run_export( cfg, out, fetch=httpx.Client(transport=httpx.MockTransport(refuse)), sleep=lambda s: None, ) assert summary["downloaded"] == 0 def test_failed_cover_reports_and_ships_page_anyway(tmp_path): cfg = _cfg(tmp_path) def flaky(request: httpx.Request) -> httpx.Response: if "pic2" in str(request.url): return httpx.Response(503) return httpx.Response(200, content=b"img") summary = run_export( cfg, tmp_path / "site", fetch=httpx.Client(transport=httpx.MockTransport(flaky)), sleep=lambda s: None, ) assert summary["failures"] == 1 page = (tmp_path / "site" / "alice-is-missing" / "index.html").read_text() assert "Alice is Missing" in page # the page exists, just coverless # nothing half-written left behind to poison the idempotent re-run assert not list((tmp_path / "site" / "art").glob("*.part")) def test_official_badge_upgrades_the_footer(tmp_path): cfg = _cfg(tmp_path) (cfg.data_dir / "powered-by-bgg.png").write_bytes(b"\x89PNG-badge") out = tmp_path / "site" run_export(cfg, out, fetch=_cdn([]), sleep=lambda s: None) assert (out / "art" / "powered-by-bgg.png").exists() index = (out / "index.html").read_text() assert 'img src="art/powered-by-bgg.png" alt="Powered by BGG"' in index detail = (out / "britannia" / "index.html").read_text() assert 'img src="../art/powered-by-bgg.png"' in detail def test_export_without_games_json_exits_with_guidance(tmp_path): cfg = Config(data_dir=tmp_path / "data") import typer with pytest.raises(typer.Exit): run_export(cfg, tmp_path / "site", fetch=_cdn([]), sleep=lambda s: None)