import os
import sys
import unittest
from unittest.mock import MagicMock, patch, call
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from api_client import TheGamesDbClient, ApiError
FAKE_KEY = "fake_api_key"
def _ok(data: dict) -> MagicMock:
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {"code": 200, "status": "Success", "data": data,
"remaining_monthly_allowance": 900,
"extra_allowance": 0}
return resp
def _page(games: list, page: int = 1, total: int = None) -> MagicMock:
total = total or len(games)
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200,
"status": "Success",
"data": {"count": len(games), "games": games},
"pages": {
"previous": None,
"current": f"https://api.thegamesdb.net/v1/Games/ByPlatformID?page={page}",
"next": None if len(games) < 20 else f"https://api.thegamesdb.net/v1/Games/ByPlatformID?page={page+1}",
},
"remaining_monthly_allowance": 900,
"extra_allowance": 0,
}
return resp
class TestTheGamesDbClientConstruction(unittest.TestCase):
def test_raises_if_no_api_key(self):
with self.assertRaises(ValueError):
TheGamesDbClient(api_key="")
def test_accepts_valid_api_key(self):
client = TheGamesDbClient(api_key=FAKE_KEY)
self.assertIsNotNone(client)
class TestTheGamesDbClientRateLimiting(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_sleep_called_between_requests(self, mock_get, mock_sleep):
game = {"id": 5, "game_title": "Donkey Kong", "release_date": "1986-06-01",
"platform": 7, "region_id": 2, "country_id": 50}
mock_get.return_value = _page([game])
client = TheGamesDbClient(api_key=FAKE_KEY, request_delay=0.2)
client.get_games_by_platform(7)
client.get_games_by_platform(7)
self.assertGreaterEqual(mock_sleep.call_count, 1)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_sleep_duration_matches_configured_delay(self, mock_get, mock_sleep):
mock_get.return_value = _page([])
client = TheGamesDbClient(api_key=FAKE_KEY, request_delay=0.123)
client.get_games_by_platform(7)
client.get_games_by_platform(7)
for c in mock_sleep.call_args_list:
self.assertAlmostEqual(c.args[0], 0.123, places=2)
class TestGetGamesByPlatform(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_returns_games_from_single_page(self, mock_get, _sleep):
games = [{"id": 5, "game_title": "Donkey Kong", "release_date": "1986-06-01",
"platform": 7, "region_id": 2, "country_id": 50}]
mock_get.return_value = _page(games)
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_by_platform(7)
self.assertEqual(len(result["games"]), 1)
self.assertEqual(result["games"][0]["id"], 5)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_paginates_through_all_pages(self, mock_get, _sleep):
page1_games = [{"id": i, "game_title": f"Game {i}", "release_date": "",
"platform": 7, "region_id": 0, "country_id": 0} for i in range(20)]
page2_games = [{"id": 20, "game_title": "Game 20", "release_date": "",
"platform": 7, "region_id": 0, "country_id": 0}]
mock_get.side_effect = [_page(page1_games, page=1, total=21),
_page(page2_games, page=2, total=21)]
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_by_platform(7)
self.assertEqual(len(result["games"]), 21)
self.assertEqual(mock_get.call_count, 2)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_includes_all_fields_parameter(self, mock_get, _sleep):
mock_get.return_value = _page([])
client = TheGamesDbClient(api_key=FAKE_KEY)
client.get_games_by_platform(7)
url = mock_get.call_args[0][0]
self.assertIn("fields=", url)
self.assertIn("overview", url)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_includes_boxart_in_include_parameter(self, mock_get, _sleep):
mock_get.return_value = _page([])
client = TheGamesDbClient(api_key=FAKE_KEY)
client.get_games_by_platform(7)
url = mock_get.call_args[0][0]
self.assertIn("include=", url)
self.assertIn("boxart", url)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_raises_api_error_on_http_error(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 429
resp.json.return_value = {"code": 429, "status": "Too Many Requests"}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY)
with self.assertRaises(ApiError):
client.get_games_by_platform(7)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_returns_base_url_when_boxart_present(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {"count": 0, "games": []},
"include": {
"boxart": {
"base_url": {"original": "https://cdn.thegamesdb.net/images/original/"},
"data": {},
}
},
"pages": {"previous": None, "current": "...", "next": None},
"remaining_monthly_allowance": 900,
"extra_allowance": 0,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_by_platform(7)
self.assertIn("base_url", result)
self.assertIn("original", result["base_url"])
class TestGetGamesById(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_fetches_multiple_ids_in_one_call(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {"count": 2, "games": {
"135": {"id": 135, "game_title": "Castlevania"},
"140": {"id": 140, "game_title": "Super Mario Bros."},
}},
"remaining_monthly_allowance": 899,
"extra_allowance": 0,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_by_id([135, 140])
self.assertEqual(len(result["games"]), 2)
self.assertEqual(mock_get.call_count, 1)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_batches_large_id_lists(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {"count": 0, "games": {}},
"remaining_monthly_allowance": 800,
"extra_allowance": 0,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY, batch_size=10)
client.get_games_by_id(list(range(25)))
self.assertEqual(mock_get.call_count, 3)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_handles_list_response_from_api(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {"count": 2, "games": [
{"id": 135, "game_title": "Castlevania"},
{"id": 140, "game_title": "Super Mario Bros."},
]},
"remaining_monthly_allowance": 899,
"extra_allowance": 0,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_by_id([135, 140])
self.assertEqual(len(result["games"]), 2)
class TestGetGamesImages(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_returns_images_for_given_ids(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {
"count": 1,
"base_url": {"original": "https://cdn.thegamesdb.net/images/original/"},
"images": {
"135": [{"id": 718, "type": "boxart", "side": "back",
"filename": "boxart/back/135-2.jpg", "resolution": "1000x1435"}],
},
},
"remaining_monthly_allowance": 890,
"extra_allowance": 0,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_images([135])
self.assertIn("images", result)
self.assertIn("135", result["images"])
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_batches_large_id_lists(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {"count": 0, "base_url": {}, "images": {}},
"remaining_monthly_allowance": 800,
"extra_allowance": 0,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY, batch_size=10)
client.get_games_images(list(range(25)))
self.assertEqual(mock_get.call_count, 3)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_paginates_images_within_a_batch(self, mock_get, _sleep):
base_url = {"original": "https://cdn.thegamesdb.net/images/original/"}
page1 = MagicMock()
page1.status_code = 200
page1.json.return_value = {
"code": 200, "status": "Success",
"data": {
"count": 2,
"base_url": base_url,
"images": {
"135": [{"id": 1, "type": "boxart", "side": "front",
"filename": "boxart/front/135-1.jpg", "resolution": None}],
},
},
"pages": {
"previous": None,
"current": "https://api.thegamesdb.net/v1/Games/Images?games_id=135&page=1",
"next": "https://api.thegamesdb.net/v1/Games/Images?games_id=135&page=2",
},
"remaining_monthly_allowance": 890,
}
page2 = MagicMock()
page2.status_code = 200
page2.json.return_value = {
"code": 200, "status": "Success",
"data": {
"count": 2,
"base_url": base_url,
"images": {
"135": [{"id": 2, "type": "screenshot", "side": None,
"filename": "screenshots/135-1.jpg", "resolution": None}],
},
},
"pages": {
"previous": "https://api.thegamesdb.net/v1/Games/Images?games_id=135&page=1",
"current": "https://api.thegamesdb.net/v1/Games/Images?games_id=135&page=2",
"next": None,
},
"remaining_monthly_allowance": 889,
}
mock_get.side_effect = [page1, page2]
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_images([135])
self.assertEqual(mock_get.call_count, 2)
images_135 = result["images"]["135"]
ids = {img["id"] for img in images_135}
self.assertEqual(ids, {1, 2})
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_merges_images_across_pages_for_same_game(self, mock_get, _sleep):
base_url = {"original": "https://cdn.thegamesdb.net/images/original/"}
def _img_page(img_id, img_type, has_next):
return {
"code": 200, "status": "Success",
"data": {
"count": 1, "base_url": base_url,
"images": {
"135": [{"id": img_id, "type": img_type, "side": None,
"filename": f"{img_type}/{img_id}.jpg", "resolution": None}],
},
},
"pages": {"previous": None, "current": "...",
"next": "...?page=2" if has_next else None},
"remaining_monthly_allowance": 890,
}
resp1, resp2 = MagicMock(), MagicMock()
resp1.status_code = resp2.status_code = 200
resp1.json.return_value = _img_page(10, "fanart", has_next=True)
resp2.json.return_value = _img_page(11, "clearlogo", has_next=False)
mock_get.side_effect = [resp1, resp2]
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_images([135])
images_135 = result["images"]["135"]
ids = {img["id"] for img in images_135}
self.assertEqual(ids, {10, 11})
class TestGetGamesUpdates(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_returns_list_of_updated_game_ids(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {
"count": 2,
"updates": [
{"game_id": 135, "edit_id": 1001},
{"game_id": 140, "edit_id": 1002},
],
},
"remaining_monthly_allowance": 850,
"extra_allowance": 0,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_games_updates(last_edit_id=999)
self.assertIn("updates", result)
self.assertEqual(len(result["updates"]), 2)
class TestReferenceEndpoints(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_get_genres_returns_dict_keyed_by_id(self, mock_get, _sleep):
mock_get.return_value = _ok({
"count": 1,
"genres": {"15": {"id": 15, "name": "Action"}},
})
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_genres()
self.assertIn("15", result)
self.assertEqual(result["15"]["name"], "Action")
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_get_developers_returns_dict_keyed_by_id(self, mock_get, _sleep):
mock_get.return_value = _ok({
"count": 1,
"developers": {"389": {"id": 389, "name": "Nintendo"}},
})
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_developers()
self.assertIn("389", result)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_get_publishers_returns_dict_keyed_by_id(self, mock_get, _sleep):
mock_get.return_value = _ok({
"count": 1,
"publishers": {"252": {"id": 252, "name": "Nintendo of America"}},
})
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_publishers()
self.assertIn("252", result)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_get_regions_returns_list(self, mock_get, _sleep):
mock_get.return_value = _ok({
"count": 1,
"regions": {"1": {"id": 1, "name": "US"}},
})
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_regions()
self.assertIn("1", result)
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_get_countries_returns_list(self, mock_get, _sleep):
mock_get.return_value = _ok({
"count": 1,
"countries": {"50": {"id": 50, "name": "United States"}},
})
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_countries()
self.assertIn("50", result)
class TestGetApiLimit(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_returns_remaining_allowance(self, mock_get, _sleep):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {
"code": 200, "status": "Success",
"data": {"remaining_monthly_allowance": 750, "extra_allowance": 5},
"remaining_monthly_allowance": 750,
"extra_allowance": 5,
}
mock_get.return_value = resp
client = TheGamesDbClient(api_key=FAKE_KEY)
result = client.get_api_limit()
self.assertIn("remaining_monthly_allowance", result)
class TestPaginateProgress(unittest.TestCase):
@patch("api_client.time.sleep")
@patch("api_client.requests.get")
def test_progress_bar_shows_page_and_game_count(self, mock_get, _sleep):
page1_games = [{"id": i} for i in range(20)]
page2_games = [{"id": 20}]
mock_get.side_effect = [_page(page1_games, page=1, total=21),
_page(page2_games, page=2, total=21)]
postfixes = []
class _FakeBar:
def update(self, n=1): pass
def set_postfix_str(self, s, **kw): postfixes.append(s)
def close(self): pass
client = TheGamesDbClient(api_key=FAKE_KEY, verbose=True)
with patch("api_client.tqdm", return_value=_FakeBar()):
client._paginate("/v1/Games/ByPlatformID", {"id": 7})
self.assertEqual(postfixes[0], "20 games fetched")
self.assertEqual(postfixes[1], "21 games fetched")
if __name__ == "__main__":
unittest.main()