Files
climperor/web/tests/test_pro_matches_incremental.py
T
vosonandCursor 0f7e7b68dd Rotate OpenDota pro-match refresh to stay under rate limits.
Daily now refreshes the 15 oldest watchlist pros, keeps prior cache for the
rest, fails fast on consecutive 429s, and still writes a partial result.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 22:59:34 +08:00

145 lines
5.1 KiB
Python

"""Unit tests for pro_matches incremental refresh + 429 circuit breaker."""
from __future__ import annotations
import json
import sys
import tempfile
import unittest
import urllib.error
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "web"))
from web.fetch_pro_matches import ( # noqa: E402
OpenDotaClient,
RateLimitTripped,
load_existing,
retain_cell,
select_refresh_batch,
write_out,
)
class SelectRefreshBatchTests(unittest.TestCase):
def test_oldest_and_missing_first(self) -> None:
picked = [
(1, {"name": "A"}),
(2, {"name": "B"}),
(3, {"name": "C"}),
(4, {"name": "D"}),
]
existing = {
"1": {"fetched_at": "2026-07-28T00:00:00+00:00", "matches": []},
"2": {"fetched_at": "2026-07-20T00:00:00+00:00", "matches": []},
# 3 missing => oldest
"4": {"fetched_at": "2026-07-29T00:00:00+00:00", "matches": []},
}
refresh, retain = select_refresh_batch(picked, existing, 2)
self.assertEqual([aid for aid, _ in refresh], [3, 2])
self.assertEqual([aid for aid, _ in retain], [1, 4])
def test_refresh_limit_zero_means_all(self) -> None:
picked = [(1, {}), (2, {})]
refresh, retain = select_refresh_batch(picked, {}, 0)
self.assertEqual(len(refresh), 2)
self.assertEqual(retain, [])
def test_retain_keeps_prior_matches(self) -> None:
existing = {
"account_id": 9,
"name": "Old",
"matches": [{"match_id": 1}],
"fetched_at": "2026-07-01T00:00:00+00:00",
}
cell = retain_cell(9, {"name": "New"}, existing)
self.assertEqual(cell["matches"], [{"match_id": 1}])
self.assertEqual(cell["fetched_at"], "2026-07-01T00:00:00+00:00")
self.assertEqual(cell["match_count"], 1)
class RateLimitClientTests(unittest.TestCase):
def test_trips_after_consecutive_429(self) -> None:
client = OpenDotaClient(consecutive_limit=2)
def boom(_url: str, **_kwargs):
raise urllib.error.HTTPError(
"https://api.opendota.com/api/x", 429, "Too Many", hdrs=None, fp=None
)
with patch("web.fetch_pro_matches.http_json", side_effect=boom):
with self.assertRaises(urllib.error.HTTPError):
client.json("/players/1/matches")
with self.assertRaises(RateLimitTripped):
client.json("/players/1/matches")
self.assertTrue(client.tripped)
def test_success_resets_streak(self) -> None:
client = OpenDotaClient(consecutive_limit=3)
calls = {"n": 0}
def flaky(url: str, **_kwargs):
calls["n"] += 1
if calls["n"] == 1:
raise urllib.error.HTTPError(url, 429, "Too Many", hdrs=None, fp=None)
return []
with patch("web.fetch_pro_matches.http_json", side_effect=flaky):
with self.assertRaises(urllib.error.HTTPError):
client.json("/players/1/matches")
self.assertEqual(client.consecutive_429, 1)
self.assertEqual(client.json("/players/2/matches"), [])
self.assertEqual(client.consecutive_429, 0)
class WriteOutMergeTests(unittest.TestCase):
def test_write_out_records_refresh_meta(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "pro_matches.json"
by_pro = {
"1": {
"account_id": 1,
"name": "A",
"matches": [],
"match_count": 0,
"fetched_at": datetime.now(timezone.utc).isoformat(),
},
"2": {
"account_id": 2,
"name": "B",
"matches": [{"match_id": 9, "hero_key": "axe"}],
"match_count": 1,
"fetched_at": "2026-07-01T00:00:00+00:00",
},
}
write_out(
path,
by_pro=by_pro,
by_hero={"axe": {"matches": [{"match_id": 9}]}},
pros_meta={"1": {"account_id": 1}, "2": {"account_id": 2}},
item_catalog={},
limit=8,
limit_pros=2,
lobby_types=(1, 2, 7),
player_source="watchlist:x.json",
refreshed_count=1,
retained_count=1,
refresh_limit=15,
rate_limited=True,
)
payload = json.loads(path.read_text(encoding="utf-8"))
meta = payload["meta"]
self.assertEqual(meta["refreshed_count"], 1)
self.assertEqual(meta["retained_count"], 1)
self.assertEqual(meta["refresh_limit"], 15)
self.assertTrue(meta["rate_limited"])
self.assertEqual(load_existing(path)["by_pro"]["2"]["match_count"], 1)
if __name__ == "__main__":
unittest.main()