Files
bggpipe/tests/test_models.py
T
Eric Wagoner 92aaa91a49 Re-audit round 3: 5 blind reviewers, 15 fixes, +18 tests — converging
Round 3's two HIGHs: _fill_version resolved versions with the LAST
same-title entry's cues (photo-aware lookup existed since round 1 but
this caller never used it), and the round-2 diff rework let an earlier
row's disagreement consume the exact-version copy a later row matched.
Diff claims now settle strongest-first across all rows (exact matches,
then versionless upgrades, then disagreement/second-copy), unvetoed
bare duplicates stay owned per spec, and updates are withheld with a
manual-fix note whenever any copy of the game already carries a version
(the row edit targets by name and could hit the wrong copy).

Also: entry-to-row pairing matches by photo overlap before position
(titles.json order churn from reshoot filenames could swap editions);
BGGQueueTimeout defers a title like a missing token; DismissStore
writes atomically, mutates memory only after the write, and
quarantines a torn file instead of bricking the server; version-picker
page-limit exhaustion stays retryable; verify's copy-count shortfall
reports once per game (the old guard was dead code); the upload log
header is created atomically; transient version-lookup failures record
a retryable version_error, not terminal version_unknown; extract
isolates per-photo failures and salvages JSON followed by prose; a
state revision counter stops stale poll responses reverting decisions;
plus the shared-predicate/fsio/docstring consolidation and CLI wiring,
live-diff, verify-wiring, and search-guard tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:55:04 -04:00

145 lines
4.5 KiB
Python

"""XML parser tests against hand-built API2 response shapes."""
from __future__ import annotations
import pytest
from bggpipe.models import (
BGGResponseError,
parse_collection,
parse_search,
parse_things,
)
SEARCH_XML = """<items total="2">
<item type="boardgame" id="13">
<name type="primary" value="CATAN"/><yearpublished value="1995"/>
</item>
<item type="boardgameexpansion" id="290448">
<name type="alternate" value="Wingspan: Europa"/>
</item>
</items>"""
THING_XML = """<items>
<item type="boardgame" id="266192">
<name type="primary" sortindex="1" value="Wingspan"/>
<name type="alternate" sortindex="1" value="Flügelschlag"/>
<yearpublished value="2019"/>
<statistics page="1"><ratings>
<owned value="123456"/>
<ranks>
<rank type="subtype" id="1" name="boardgame" value="30"/>
<rank type="family" id="5497" name="strategygames" value="25"/>
</ranks>
</ratings></statistics>
<versions>
<item type="boardgameversion" id="465063">
<name type="primary" value="English edition"/>
<yearpublished value="2019"/>
<link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
<link type="language" id="2184" value="English"/>
</item>
<item type="boardgameversion" id="465064">
<name type="primary" value="German edition"/>
<yearpublished value="2019"/>
<link type="boardgamepublisher" id="22160" value="Feuerland Spiele"/>
<link type="language" id="2188" value="German"/>
</item>
</versions>
</item>
</items>"""
COLLECTION_XML = """<items totalitems="2">
<item objecttype="thing" objectid="13" subtype="boardgame" collid="101">
<name sortindex="1">Catan</name>
<yearpublished>1995</yearpublished>
<status own="1" wanttoplay="0"/>
</item>
<item objecttype="thing" objectid="266192" subtype="boardgame" collid="102">
<name sortindex="1">Wingspan</name>
<status own="0" wishlist="1"/>
<version><item type="boardgameversion" id="465063"/></version>
</item>
</items>"""
NOT_RANKED_XML = """<items>
<item type="boardgame" id="99999">
<name type="primary" value="Obscurity"/>
<statistics><ratings>
<owned value="12"/>
<ranks><rank type="subtype" id="1" name="boardgame" value="Not Ranked"/></ranks>
</ratings></statistics>
</item>
</items>"""
ERROR_XML = (
"<errors><error><message>Invalid username specified</message></error></errors>"
)
def test_parse_search():
results = parse_search(SEARCH_XML)
assert [r.bgg_id for r in results] == [13, 290448]
assert results[0].name == "CATAN"
assert results[0].year == 1995
assert results[1].type == "boardgameexpansion"
assert results[1].name_type == "alternate"
assert results[1].year is None
def test_parse_things_with_stats_and_versions():
(thing,) = parse_things(THING_XML)
assert thing.name == "Wingspan" # primary, not alternate
assert thing.owned == 123456
assert thing.rank == 30 # boardgame rank, not the family rank
assert len(thing.versions) == 2
english = thing.versions[0]
assert english.version_id == 465063
assert english.publishers == ("Stonemaier Games",)
assert english.languages == ("English",)
def test_parse_things_not_ranked_is_none():
(thing,) = parse_things(NOT_RANKED_XML)
assert thing.rank is None
assert thing.owned == 12
def test_parse_collection():
catan, wingspan = parse_collection(COLLECTION_XML)
assert (catan.object_id, catan.coll_id, catan.own) == (13, 101, True)
assert catan.version_id is None
assert wingspan.own is False # wishlist item must not count as owned
assert wingspan.version_id == 465063
def test_error_document_raises():
with pytest.raises(BGGResponseError, match="Invalid username"):
parse_collection(ERROR_XML)
def test_search_all_items_malformed_raises():
import pytest
from bggpipe.models import BGGResponseError, parse_search
xml = '<items total="2"><item type="boardgame"/><item type="boardgame"/></items>'
with pytest.raises(BGGResponseError):
parse_search(xml)
def test_search_partial_malformed_tolerated_with_warning():
import pytest
from bggpipe.models import parse_search
xml = (
'<items total="2">'
'<item type="boardgame" id="13"><name type="primary" value="CATAN"/></item>'
'<item type="boardgame"/>'
"</items>"
)
with pytest.warns(UserWarning, match="unparseable"):
results = parse_search(xml)
assert [r.bgg_id for r in results] == [13]