"""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()) == {} def test_local_rows_enrich_from_their_own_reads(tmp_path): cfg = Config(data_dir=tmp_path / "data") cfg.data_dir.mkdir(parents=True) write_matches( cfg.matches_path, [ { "title_raw": "Obscure Homebrew", "bgg_id": "", "bgg_name": "", "year": "", "type": "", "match_status": "local", "version_id": "", "version_name": "", "version_status": "", "candidates_json": "[]", "version_candidates_json": "[]", "source_photos": "shelf.jpg", } ], ) cfg.titles_path.write_text( json.dumps( [ { "title_raw": "Obscure Homebrew", "publisher_hint": "Basement Press", "year_hint": 1998, "source_photos": ["shelf.jpg"], } ] ) ) from bggpipe.enrich import run_enrich client = BGGClient( cache_dir=cfg.data_dir / "cache", transport=httpx.MockTransport(_no_network), ) games = run_enrich(cfg, client=client) (key,) = [k for k in games if k.startswith("local:")] entry = games[key] assert entry["name"] == "Obscure Homebrew" assert entry["year"] == 1998 assert entry["publishers"] == ["Basement Press"] assert entry["type"] == "localgame" # idempotent: a second run keeps the entry (no prune, no dupe) games2 = run_enrich(cfg, client=client) assert key in games2 def test_summary_counts_local_and_api_entries_separately(tmp_path, capsys): """Local entries have no API target, so folding them into the fetched tally made "already present or waiting" report a NEGATIVE count.""" from bggpipe.enrich import run_enrich cfg = Config(data_dir=tmp_path / "data") cfg.data_dir.mkdir(parents=True) write_matches( cfg.matches_path, [ { "title_raw": "Homebrew", "bgg_id": "", "bgg_name": "", "year": "", "type": "", "match_status": "local", "version_id": "", "version_name": "", "version_status": "", "candidates_json": "[]", "version_candidates_json": "[]", "source_photos": "shelf.jpg", } ], ) cfg.titles_path.write_text("[]") client = BGGClient( cache_dir=cfg.data_dir / "cache", transport=httpx.MockTransport(_no_network), ) games = run_enrich(cfg, client=client) out = capsys.readouterr().out assert len(games) == 1 assert "1 local-only" in out import re as _re assert not _re.search(r"-\d", out) # no negative tallies