"""Tests for curseforge.py.""" import pytest import responses as responses_lib from modpack_checker.curseforge import ( CurseForgeAuthError, CurseForgeClient, CurseForgeError, CurseForgeNotFoundError, CurseForgeRateLimitError, ) BASE = "https://api.curseforge.com" @pytest.fixture def client(): return CurseForgeClient("test-api-key", timeout=5) # --------------------------------------------------------------------------- # get_mod # --------------------------------------------------------------------------- @responses_lib.activate def test_get_mod_success(client): responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/123456", json={"data": {"id": 123456, "name": "Test Pack", "latestFiles": []}}, status=200, ) mod = client.get_mod(123456) assert mod["name"] == "Test Pack" @responses_lib.activate def test_get_mod_not_found(client): responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/999", status=404) with pytest.raises(CurseForgeNotFoundError): client.get_mod(999) @responses_lib.activate def test_get_mod_invalid_key_401(client): responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/123", status=401) with pytest.raises(CurseForgeAuthError, match="Invalid API key"): client.get_mod(123) @responses_lib.activate def test_get_mod_forbidden_403(client): responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/123", status=403) with pytest.raises(CurseForgeAuthError, match="permission"): client.get_mod(123) @responses_lib.activate def test_get_mod_rate_limit_429(client): responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/123", status=429) with pytest.raises(CurseForgeRateLimitError): client.get_mod(123) @responses_lib.activate def test_get_mod_server_error(client): # responses library doesn't retry by default in tests; just test the exception responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/123", status=500) responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/123", status=500) responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/123", status=500) responses_lib.add(responses_lib.GET, f"{BASE}/v1/mods/123", status=500) with pytest.raises(CurseForgeError): client.get_mod(123) # --------------------------------------------------------------------------- # get_mod_name # --------------------------------------------------------------------------- @responses_lib.activate def test_get_mod_name(client): responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/100", json={"data": {"id": 100, "name": "All The Mods 9", "latestFiles": []}}, status=200, ) assert client.get_mod_name(100) == "All The Mods 9" @responses_lib.activate def test_get_mod_name_fallback(client): """If 'name' key is missing, return generic fallback.""" responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/100", json={"data": {"id": 100, "latestFiles": []}}, status=200, ) assert client.get_mod_name(100) == "Modpack 100" # --------------------------------------------------------------------------- # get_latest_file # --------------------------------------------------------------------------- @responses_lib.activate def test_get_latest_file_uses_latest_files(client): """get_latest_file should prefer the latestFiles field (fast path).""" responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/200", json={ "data": { "id": 200, "name": "Pack", "latestFiles": [ { "id": 9001, "displayName": "Pack 2.0.0", "fileName": "pack-2.0.0.zip", "fileDate": "2026-01-15T00:00:00Z", }, { "id": 9000, "displayName": "Pack 1.0.0", "fileName": "pack-1.0.0.zip", "fileDate": "2025-12-01T00:00:00Z", }, ], } }, status=200, ) file_obj = client.get_latest_file(200) assert file_obj["displayName"] == "Pack 2.0.0" @responses_lib.activate def test_get_latest_file_fallback_files_endpoint(client): """Falls back to the /files endpoint when latestFiles is empty.""" responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/300", json={"data": {"id": 300, "name": "Pack", "latestFiles": []}}, status=200, ) responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/300/files", json={ "data": [ {"id": 8000, "displayName": "Pack 3.0.0", "fileName": "pack-3.0.0.zip"} ] }, status=200, ) file_obj = client.get_latest_file(300) assert file_obj["displayName"] == "Pack 3.0.0" @responses_lib.activate def test_get_latest_file_no_files_returns_none(client): responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/400", json={"data": {"id": 400, "name": "Pack", "latestFiles": []}}, status=200, ) responses_lib.add( responses_lib.GET, f"{BASE}/v1/mods/400/files", json={"data": []}, status=200, ) assert client.get_latest_file(400) is None # --------------------------------------------------------------------------- # extract_version # --------------------------------------------------------------------------- def test_extract_version_prefers_display_name(client): file_obj = {"displayName": "ATM9 1.2.3", "fileName": "atm9-1.2.3.zip", "id": 1} assert client.extract_version(file_obj) == "ATM9 1.2.3" def test_extract_version_falls_back_to_filename(client): file_obj = {"displayName": "", "fileName": "pack-1.0.0.zip", "id": 1} assert client.extract_version(file_obj) == "pack-1.0.0.zip" def test_extract_version_last_resort_file_id(client): file_obj = {"displayName": "", "fileName": "", "id": 9999} assert client.extract_version(file_obj) == "File ID 9999" def test_extract_version_strips_whitespace(client): file_obj = {"displayName": " Pack 1.0 ", "fileName": "pack.zip", "id": 1} assert client.extract_version(file_obj) == "Pack 1.0" # --------------------------------------------------------------------------- # validate_api_key # --------------------------------------------------------------------------- @responses_lib.activate def test_validate_api_key_success(client): responses_lib.add( responses_lib.GET, f"{BASE}/v1/games/432", json={"data": {"id": 432, "name": "Minecraft"}}, status=200, ) assert client.validate_api_key() is True @responses_lib.activate def test_validate_api_key_failure(client): responses_lib.add(responses_lib.GET, f"{BASE}/v1/games/432", status=401) assert client.validate_api_key() is False