Skip to content

Commit f0e4663

Browse files
test: achieve 97.24% coverage with comprehensive test suite
Add 68 new tests across 7 test files to increase coverage from 68.20% to 97.24%. New test files: - test_api_response_storage.py: File I/O, path generation, gzip, metadata (15 tests) - test_endpoint_method_introspection.py: Schema introspection, parameters, docs (11 tests) - test_registry_errors.py: Registry error handling and caching (5 tests) - test_http_retry.py: HTTP retry logic, backoff, error handling (7 tests) - test_edge_cases.py: Property accessors, validation, enum handling (17 tests) - test_final_coverage.py: Parent dirs, repr, case-insensitive enums (6 tests) - test_complete_coverage.py: Wrappers, formatters, factory functions (7 tests) Coverage achievements: - 100% coverage: registry.py, log.py, schema_loader.py, all __init__.py - 96.51% coverage: factory.py (only defensive code paths uncovered) - Overall: 97.24% (505 statements, 98 passing tests) Remaining 2.76% uncovered consists of defensive code paths that cannot be triggered without breaking the API contract.
1 parent 368476b commit f0e4663

File tree

7 files changed

+1808
-0
lines changed

7 files changed

+1808
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
"""
2+
Unit tests for APIResponse storage methods (get_path, get_uri, save_json, gzip).
3+
"""
4+
5+
import gzip as gzip_module
6+
import json
7+
import os
8+
from unittest.mock import Mock
9+
from urllib.parse import ParseResult
10+
11+
import pytest
12+
import requests
13+
14+
from pymlb_statsapi.model.factory import APIResponse
15+
16+
17+
class TestAPIResponseStorage:
18+
"""Test APIResponse storage and path generation methods."""
19+
20+
@pytest.fixture
21+
def mock_response(self):
22+
"""Create a mock response for testing."""
23+
mock_resp = Mock(spec=requests.Response)
24+
mock_resp.url = "https://statsapi.mlb.com/api/v1/schedule?sportId=1&date=2024-07-04"
25+
mock_resp.status_code = 200
26+
mock_resp.ok = True
27+
mock_resp.headers = {"Content-Type": "application/json"}
28+
mock_resp.content = b'{"dates": []}'
29+
mock_resp.elapsed = Mock()
30+
mock_resp.elapsed.total_seconds.return_value = 0.245
31+
mock_resp.json.return_value = {"dates": [{"date": "2024-07-04"}]}
32+
return mock_resp
33+
34+
@pytest.fixture
35+
def api_response(self, mock_response):
36+
"""Create an APIResponse for testing."""
37+
return APIResponse(
38+
response=mock_response,
39+
endpoint_name="schedule",
40+
method_name="schedule",
41+
path_params={},
42+
query_params={"sportId": "1", "date": "2024-07-04"},
43+
)
44+
45+
def test_get_path_no_prefix(self, api_response):
46+
"""Test get_path without prefix."""
47+
path = api_response.get_path()
48+
assert path == "schedule/schedule/date=2024-07-04&sportId=1"
49+
50+
def test_get_path_with_prefix(self, api_response):
51+
"""Test get_path with prefix."""
52+
path = api_response.get_path(prefix="mlb-data")
53+
assert path == "mlb-data/schedule/schedule/date=2024-07-04&sportId=1"
54+
55+
def test_get_path_with_path_params(self, mock_response):
56+
"""Test get_path with path parameters."""
57+
response = APIResponse(
58+
response=mock_response,
59+
endpoint_name="game",
60+
method_name="boxscore",
61+
path_params={"game_pk": "747175"},
62+
query_params={"fields": "gameData"},
63+
)
64+
path = response.get_path(prefix="data")
65+
assert path == "data/game/boxscore/game_pk=747175/fields=gameData"
66+
67+
def test_get_path_no_params(self, mock_response):
68+
"""Test get_path with no parameters."""
69+
response = APIResponse(
70+
response=mock_response,
71+
endpoint_name="sports",
72+
method_name="sports",
73+
path_params={},
74+
query_params={},
75+
)
76+
path = response.get_path()
77+
assert path == "sports/sports"
78+
79+
def test_get_uri_default(self, api_response):
80+
"""Test get_uri with defaults."""
81+
uri = api_response.get_uri()
82+
83+
assert isinstance(uri, ParseResult)
84+
assert uri.scheme == "file"
85+
assert uri.netloc == ""
86+
assert uri.path.endswith("schedule/schedule/date=2024-07-04&sportId=1.json")
87+
assert ".var/local/mlb_statsapi" in uri.path
88+
89+
def test_get_uri_with_prefix(self, api_response):
90+
"""Test get_uri with prefix."""
91+
uri = api_response.get_uri(prefix="test-data")
92+
93+
assert uri.scheme == "file"
94+
assert "test-data/schedule/schedule" in uri.path
95+
assert uri.path.endswith(".json")
96+
97+
def test_get_uri_with_gzip(self, api_response):
98+
"""Test get_uri with gzip extension."""
99+
uri = api_response.get_uri(gzip=True)
100+
101+
assert uri.path.endswith(".json.gz")
102+
103+
def test_get_uri_custom_base_path(self, api_response, tmp_path):
104+
"""Test get_uri with custom base path from environment."""
105+
os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"] = str(tmp_path)
106+
107+
try:
108+
uri = api_response.get_uri()
109+
assert str(tmp_path) in uri.path
110+
finally:
111+
del os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"]
112+
113+
def test_save_json_auto_path(self, api_response, tmp_path):
114+
"""Test save_json with auto-generated path."""
115+
os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"] = str(tmp_path)
116+
117+
try:
118+
result = api_response.save_json(prefix="test")
119+
120+
assert "path" in result
121+
assert "bytes_written" in result
122+
assert "timestamp" in result
123+
assert "uri" in result
124+
assert isinstance(result["uri"], ParseResult)
125+
126+
# Verify file was created
127+
assert os.path.exists(result["path"])
128+
129+
# Verify content structure
130+
with open(result["path"]) as f:
131+
data = json.load(f)
132+
assert "metadata" in data
133+
assert "data" in data
134+
assert data["data"]["dates"] == [{"date": "2024-07-04"}]
135+
finally:
136+
del os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"]
137+
138+
def test_save_json_explicit_path(self, api_response, tmp_path):
139+
"""Test save_json with explicit file path."""
140+
file_path = tmp_path / "test.json"
141+
142+
result = api_response.save_json(file_path=str(file_path))
143+
144+
assert result["path"] == str(file_path)
145+
assert "uri" not in result # URI only included when auto-generating
146+
assert os.path.exists(file_path)
147+
148+
def test_save_json_gzipped(self, api_response, tmp_path):
149+
"""Test save_json with gzip compression."""
150+
os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"] = str(tmp_path)
151+
152+
try:
153+
result = api_response.save_json(gzip=True, prefix="compressed")
154+
155+
assert result["path"].endswith(".json.gz")
156+
assert os.path.exists(result["path"])
157+
158+
# Verify gzipped content
159+
with gzip_module.open(result["path"], "rt", encoding="utf-8") as f:
160+
data = json.load(f)
161+
assert "metadata" in data
162+
assert "data" in data
163+
finally:
164+
del os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"]
165+
166+
def test_save_json_creates_parent_dirs(self, api_response, tmp_path):
167+
"""Test save_json creates parent directories."""
168+
file_path = tmp_path / "nested" / "dirs" / "test.json"
169+
170+
api_response.save_json(file_path=str(file_path))
171+
172+
assert os.path.exists(file_path)
173+
assert file_path.parent.exists()
174+
175+
def test_gzip_convenience_method(self, api_response, tmp_path):
176+
"""Test gzip() convenience method."""
177+
os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"] = str(tmp_path)
178+
179+
try:
180+
result = api_response.gzip(prefix="data")
181+
182+
assert result["path"].endswith(".json.gz")
183+
assert os.path.exists(result["path"])
184+
185+
# Verify it's properly gzipped
186+
with gzip_module.open(result["path"], "rt", encoding="utf-8") as f:
187+
data = json.load(f)
188+
assert "metadata" in data
189+
assert "data" in data
190+
finally:
191+
del os.environ["PYMLB_STATSAPI__BASE_FILE_PATH"]
192+
193+
def test_gzip_with_explicit_path(self, api_response, tmp_path):
194+
"""Test gzip() with explicit file path."""
195+
file_path = tmp_path / "explicit.json.gz"
196+
197+
result = api_response.gzip(file_path=str(file_path))
198+
199+
assert result["path"] == str(file_path)
200+
assert os.path.exists(file_path)
201+
202+
def test_metadata_in_saved_file(self, api_response, tmp_path):
203+
"""Test that saved files include complete metadata."""
204+
file_path = tmp_path / "metadata_test.json"
205+
206+
api_response.save_json(file_path=str(file_path))
207+
208+
with open(file_path) as f:
209+
data = json.load(f)
210+
211+
# Check metadata structure
212+
assert "metadata" in data
213+
assert "request" in data["metadata"]
214+
assert "response" in data["metadata"]
215+
216+
# Check request metadata
217+
req = data["metadata"]["request"]
218+
assert req["endpoint_name"] == "schedule"
219+
assert req["method_name"] == "schedule"
220+
assert req["query_params"] == {"sportId": "1", "date": "2024-07-04"}
221+
assert "timestamp" in req
222+
223+
# Check response metadata
224+
resp = data["metadata"]["response"]
225+
assert resp["status_code"] == 200
226+
assert resp["ok"] is True
227+
assert "elapsed_ms" in resp

0 commit comments

Comments
 (0)