38e20f2c30
Correctness: review vetoes persist via a dedupe_veto column (resolve re-runs no longer overturn humans); diff emits second copies whose confident version matches no owned copy (spec: pairs own only on both ids) and fetches the live collection with refresh; resolve pairs titles.json entries to rows by title so a reshoot photo updates provenance instead of duplicating rows; version lookups survive empty /thing results; publisher tie-break now honors the mixed base/expansion veto and refuses multi-candidate picks; empty-normalized (non-Latin) titles never count as exact. Upload: LoginError aborts a run instead of logging N bogus failures (and 3 identical consecutive failures abort as systemic); Cloudflare interstitials are detected; added-without-version gets its own logged status that verify understands; same-game updates run one per pass so the name-targeted row edit can't overwrite a fresh version; absent diff outputs fail loudly; pagination clicks are paced. Web review: a lock serializes freshen/decide (threadpool race dropped decisions); failed saves roll memory back and always alert the browser (non-JSON 500s included); session warnings reach the page instead of a StringIO; state-load failures and dead servers show banners instead of a blank page; duplicate (title, photos) rows are addressable by ordinal. Consistency: shared CONFIDENT_VERSION_STATUSES, client_for(), Config paths for every artifact, one review-port constant, named matching thresholds, strict collection-id parsing, error-doc responses never cached, unknown config keys warn, extract reports dropped vision entries, fixture generators share escaping + marker text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
119 lines
3.8 KiB
Python
119 lines
3.8 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)
|