"""Enrich-stage tests: full-metadata parsing plus run_enrich orchestration (batched, cache-keyed, idempotent, token-degrading). No network.""" from __future__ import annotations import json import httpx from bggpipe.bgg_client import BGGClient, cache_key from bggpipe.config import Config from bggpipe.models import parse_things_full from bggpipe.resolve import write_matches FULL_THING_XML = """ https://cf.example/thumb.jpg https://cf.example/full.jpg A bird-collection engine builder. """ def test_parse_things_full_extracts_everything(): (game,) = parse_things_full(FULL_THING_XML) assert game["name"] == "Wingspan" assert game["year"] == 2019 assert game["description"] == "A bird-collection engine builder." assert (game["min_players"], game["max_players"]) == (1, 5) # 3 is Best-majority; 4 loses to Recommended+NotRec? no — Best(90) >= Rec(80) # and > NotRec(5), so 4 qualifies too; 1 does not (Best < Recommended) assert game["best_player_counts"] == ["3", "4"] assert game["designers"] == ["Elizabeth Hargrave"] assert game["artists"] == ["Natalia Rojas"] assert game["publishers"] == ["Stonemaier Games"] assert game["categories"] == ["Animals", "Card Game"] assert game["mechanics"] == ["Engine Building"] assert game["rating"] == 8.05 assert game["weight"] == 2.45 assert game["rank"] == 30 assert game["playtime"] == 70 assert game["min_age"] == 10 assert game["image"].endswith("full.jpg") CATAN_MINIMAL = ( '' '' "" ) def _matches_rows(): base = { "year": "", "type": "boardgame", "candidates_json": "[]", "source_photos": "x.jpg", "version_id": "", "version_name": "", "version_status": "version_unknown", "version_candidates_json": "[]", } return [ { **base, "title_raw": "Catan", "bgg_id": "13", "bgg_name": "CATAN", "match_status": "auto", }, { **base, "title_raw": "Wingspan", "bgg_id": "266192", "bgg_name": "Wingspan", "match_status": "auto", "version_id": "465063", "version_name": "English first edition", "version_status": "version_auto", "version_candidates_json": json.dumps( [ { "version_id": 465063, "name": "English first edition", "year": 2019, "publishers": ["Stonemaier Games"], "languages": ["English"], "score": 5, } ] ), }, { **base, "title_raw": "Junk", "bgg_id": "", "bgg_name": "", "match_status": "rejected", }, ] def _no_network(request: httpx.Request) -> httpx.Response: raise AssertionError(f"test hit the network: {request.url}") def _cfg_with_matches(tmp_path): cfg = Config(data_dir=tmp_path / "data") write_matches(cfg.matches_path, _matches_rows()) return cfg def _seed_batch_fixture(cache_dir): """The batch cache entry run_enrich will ask for: sorted ids 13,266192.""" cache_dir.mkdir(parents=True, exist_ok=True) combined = FULL_THING_XML.replace("", "" + CATAN_MINIMAL[7:-8], 1) key = cache_key("thing", {"id": "13,266192", "stats": "1"}) (cache_dir / key).write_text(combined) def test_run_enrich_writes_games_json_with_versions(tmp_path): from bggpipe.enrich import run_enrich cfg = _cfg_with_matches(tmp_path) cache = tmp_path / "cache" _seed_batch_fixture(cache) client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(_no_network)) games = run_enrich(cfg, client=client) assert set(games) == {"13", "266192:465063"} wingspan = games["266192:465063"] assert wingspan["designers"] == ["Elizabeth Hargrave"] assert wingspan["version"]["name"] == "English first edition" assert wingspan["version"]["publishers"] == ["Stonemaier Games"] assert games["13"]["version"] is None saved = json.loads((cfg.data_dir / "games.json").read_text()) assert saved == games def test_run_enrich_skips_already_enriched(tmp_path): from bggpipe.enrich import run_enrich cfg = _cfg_with_matches(tmp_path) cache = tmp_path / "cache" _seed_batch_fixture(cache) client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(_no_network)) run_enrich(cfg, client=client) # empty cache + network-refusing transport: passes only if enrich # doesn't need to fetch anything at all bare = BGGClient( cache_dir=tmp_path / "empty", transport=httpx.MockTransport(_no_network) ) games = run_enrich(cfg, client=bare) assert set(games) == {"13", "266192:465063"} def test_refresh_bypasses_cache_read(tmp_path): from bggpipe.enrich import run_enrich cfg = _cfg_with_matches(tmp_path) cache = tmp_path / "cache" _seed_batch_fixture(cache) requests = [] def handler(request): requests.append(str(request.url)) return httpx.Response(200, text=FULL_THING_XML.replace("8.05", "7.5")) client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(handler)) games = run_enrich(cfg, refresh=True, client=client) assert len(requests) == 1 # cache read skipped, live fetch happened assert games["266192:465063"]["rating"] == 7.5 def test_enrich_degrades_without_token(tmp_path, capsys): from bggpipe.enrich import run_enrich cfg = _cfg_with_matches(tmp_path) client = BGGClient( cache_dir=tmp_path / "empty", transport=httpx.MockTransport( lambda req: httpx.Response(401, text="Unauthorized") ), ) games = run_enrich(cfg, client=client) assert games == {} assert "waiting on the BGG API" in capsys.readouterr().out assert json.loads((cfg.data_dir / "games.json").read_text()) == {}