steam-backlog-enforcer/steam_backlog_enforcer/tests/test_hltb_search.py
Krzysztof kuhy Rudnicki 8cc31a2c87
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
HLTB rate limit (429) wipes confidence data in hl…
Todo: 9b9342ba
2026-11-09 08:02:55 +01:00

191 lines
5.9 KiB
Python

"""Tests for HLTB search, batch-fetch, and page parsing — part 2."""
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING, Any, Self
from unittest.mock import MagicMock
import aiohttp
from steam_backlog_enforcer._hltb_search import (
_search_one,
_SearchCtx,
)
if TYPE_CHECKING:
from collections.abc import Callable
class _FakeResponse:
"""Async context manager mimicking aiohttp response."""
def __init__(self, status: int, json_data: dict[str, Any] | None = None) -> None:
"""Test init."""
self.status = status
self._json_data = json_data or {}
async def __aenter__(self) -> Self:
"""Test aenter."""
return self
async def __aexit__(self, *args: object) -> None:
"""Test aexit."""
async def json(self) -> dict[str, Any]:
"""Test json."""
return self._json_data
def _make_session(resp: _FakeResponse) -> MagicMock:
session = MagicMock()
session.post.return_value = resp
return session
def _make_ctx(
session: MagicMock,
*,
cache: dict[int, float] | None = None,
progress_cb: Callable[..., object] | None = None,
) -> _SearchCtx:
return _SearchCtx(
session=session,
search_url="https://example.com/search",
headers={},
cache=cache if cache is not None else {},
counter={"done": 0, "found": 0, "failed": 0},
total=1,
progress_cb=progress_cb,
)
class TestSearchOne:
"""Tests for _search_one."""
def test_found(self) -> None:
"""Test found."""
resp = _FakeResponse(
200,
{
"data": [
{
"game_name": "TF2",
"game_alias": "",
"comp_100": 180000,
"game_id": 12345,
}
],
},
)
ctx = _make_ctx(_make_session(resp))
result = asyncio.run(_search_one(asyncio.Semaphore(1), ctx, 440, "TF2"))
assert result is not None
assert result.app_id == 440
def test_found_without_game_id(self) -> None:
"""Found result with hltb_game_id=0 does not populate ctx.hltb_game_id."""
resp = _FakeResponse(
200,
{
"data": [
{
"game_name": "TF2",
"game_alias": "",
"comp_100": 180000,
"game_id": 0,
}
],
},
)
ctx = _make_ctx(_make_session(resp))
result = asyncio.run(_search_one(asyncio.Semaphore(1), ctx, 440, "TF2"))
assert result is not None
assert 440 not in ctx.extras.hltb_game_id
def test_not_found(self) -> None:
"""Test not found."""
resp = _FakeResponse(200, {"data": []})
ctx = _make_ctx(_make_session(resp))
result = asyncio.run(_search_one(asyncio.Semaphore(1), ctx, 440, "TF2"))
assert result is None
assert ctx.cache[440] == -1
def test_error(self) -> None:
"""Test error."""
session = MagicMock()
session.post.side_effect = aiohttp.ClientError("fail")
ctx = _make_ctx(session)
result = asyncio.run(_search_one(asyncio.Semaphore(1), ctx, 440, "TF2"))
assert result is None
assert 440 not in ctx.cache # a failed lookup is not a miss
assert ctx.counter["failed"] == 1
def test_non_200(self) -> None:
"""Test non 200."""
resp = _FakeResponse(500)
ctx = _make_ctx(_make_session(resp))
result = asyncio.run(_search_one(asyncio.Semaphore(1), ctx, 440, "TF2"))
assert result is None
assert 440 not in ctx.cache # a failed lookup is not a miss
assert ctx.counter["failed"] == 1
def test_fallback_name_without_year_suffix(self) -> None:
"""Test fallback name without year suffix."""
session = MagicMock()
session.post.side_effect = [
_FakeResponse(200, {"data": []}),
_FakeResponse(
200,
{
"data": [
{
"game_name": "Final Fantasy VII",
"game_alias": "",
"game_type": "game",
"comp_100": 141120,
"game_id": 435,
"comp_100_count": 746,
"count_comp": 10450,
}
]
},
),
]
ctx = _make_ctx(session)
result = asyncio.run(
_search_one(asyncio.Semaphore(1), ctx, 39140, "Final Fantasy VII (2013)")
)
assert result is not None
assert result.app_id == 39140
assert result.comp_100_count == 746
assert result.count_comp == 10450
assert session.post.call_count == 2
def test_with_progress_cb(self) -> None:
"""Test with progress cb."""
resp = _FakeResponse(200, {"data": []})
cb = MagicMock()
ctx = _make_ctx(_make_session(resp), progress_cb=cb)
asyncio.run(_search_one(asyncio.Semaphore(1), ctx, 440, "TF2"))
cb.assert_called_once()
def test_low_similarity_skipped(self) -> None:
"""Test low similarity skipped."""
resp = _FakeResponse(
200,
{
"data": [
{
"game_name": "Completely Different Name",
"game_alias": "",
"comp_100": 3600,
"game_id": 1,
}
],
},
)
ctx = _make_ctx(_make_session(resp))
result = asyncio.run(_search_one(asyncio.Semaphore(1), ctx, 440, "TF2"))
assert result is None