Skip to content

Commit 9947628

Browse files
committed
Mock resproxy tests to avoid dependency on live data
Update both sync and async handler tests to use mocked HTTP responses instead of relying on live API data that may change.
1 parent 7aefc4b commit 9947628

2 files changed

Lines changed: 162 additions & 49 deletions

File tree

tests/handler_async_test.py

Lines changed: 116 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
import os
33
import sys
44

5+
import aiohttp
6+
import ipinfo
7+
import pytest
8+
from ipinfo import handler_utils
59
from ipinfo.cache.default import DefaultCache
610
from ipinfo.details import Details
7-
from ipinfo.handler_async import AsyncHandler
8-
from ipinfo import handler_utils
911
from ipinfo.error import APIError
1012
from ipinfo.exceptions import RequestQuotaExceededError
11-
import ipinfo
12-
import pytest
13-
import aiohttp
13+
from ipinfo.handler_async import AsyncHandler
1414

1515
skip_if_python_3_11_or_later = sys.version_info >= (3, 11)
1616

@@ -78,8 +78,7 @@ async def test_get_details():
7878
assert country_flag["unicode"] == "U+1F1FA U+1F1F8"
7979
country_flag_url = details.country_flag_url
8080
assert (
81-
country_flag_url
82-
== "https://cdn.ipinfo.io/static/images/countries-flags/US.svg"
81+
country_flag_url == "https://cdn.ipinfo.io/static/images/countries-flags/US.svg"
8382
)
8483
country_currency = details.country_currency
8584
assert country_currency["code"] == "USD"
@@ -132,40 +131,84 @@ async def test_get_details():
132131

133132
await handler.deinit()
134133

134+
135135
@pytest.mark.parametrize(
136-
("mock_resp_status_code", "mock_resp_headers", "mock_resp_error_msg", "expected_error_json"),
136+
(
137+
"mock_resp_status_code",
138+
"mock_resp_headers",
139+
"mock_resp_error_msg",
140+
"expected_error_json",
141+
),
137142
[
138-
pytest.param(503, {"Content-Type": "text/plain"}, "Service Unavailable", {"error": "Service Unavailable"}, id="5xx_not_json"),
139-
pytest.param(403, {"Content-Type": "application/json"}, '{"message": "missing token"}', {"message": "missing token"}, id="4xx_json"),
140-
pytest.param(400, {"Content-Type": "application/json"}, '{"message": "missing field"}', {"message": "missing field"}, id="400"),
141-
]
143+
pytest.param(
144+
503,
145+
{"Content-Type": "text/plain"},
146+
"Service Unavailable",
147+
{"error": "Service Unavailable"},
148+
id="5xx_not_json",
149+
),
150+
pytest.param(
151+
403,
152+
{"Content-Type": "application/json"},
153+
'{"message": "missing token"}',
154+
{"message": "missing token"},
155+
id="4xx_json",
156+
),
157+
pytest.param(
158+
400,
159+
{"Content-Type": "application/json"},
160+
'{"message": "missing field"}',
161+
{"message": "missing field"},
162+
id="400",
163+
),
164+
],
142165
)
143166
@pytest.mark.asyncio
144-
async def test_get_details_error(monkeypatch, mock_resp_status_code, mock_resp_headers, mock_resp_error_msg, expected_error_json):
167+
async def test_get_details_error(
168+
monkeypatch,
169+
mock_resp_status_code,
170+
mock_resp_headers,
171+
mock_resp_error_msg,
172+
expected_error_json,
173+
):
145174
async def mock_get(*args, **kwargs):
146-
response = MockResponse(status=mock_resp_status_code, text=mock_resp_error_msg, headers=mock_resp_headers)
175+
response = MockResponse(
176+
status=mock_resp_status_code,
177+
text=mock_resp_error_msg,
178+
headers=mock_resp_headers,
179+
)
147180
return response
148181

149-
monkeypatch.setattr(aiohttp.ClientSession, 'get', lambda *args, **kwargs: aiohttp.client._RequestContextManager(mock_get()))
182+
monkeypatch.setattr(
183+
aiohttp.ClientSession,
184+
"get",
185+
lambda *args, **kwargs: aiohttp.client._RequestContextManager(mock_get()),
186+
)
150187
token = os.environ.get("IPINFO_TOKEN", "")
151188
handler = AsyncHandler(token)
152189
with pytest.raises(APIError) as exc_info:
153190
await handler.getDetails("8.8.8.8")
154191
assert exc_info.value.error_code == mock_resp_status_code
155192
assert exc_info.value.error_json == expected_error_json
156193

194+
157195
@pytest.mark.asyncio
158196
async def test_get_details_quota_error(monkeypatch):
159197
async def mock_get(*args, **kwargs):
160198
response = MockResponse(status=429, text="Quota exceeded", headers={})
161199
return response
162200

163-
monkeypatch.setattr(aiohttp.ClientSession, 'get', lambda *args, **kwargs: aiohttp.client._RequestContextManager(mock_get()))
201+
monkeypatch.setattr(
202+
aiohttp.ClientSession,
203+
"get",
204+
lambda *args, **kwargs: aiohttp.client._RequestContextManager(mock_get()),
205+
)
164206
token = os.environ.get("IPINFO_TOKEN", "")
165207
handler = AsyncHandler(token)
166208
with pytest.raises(RequestQuotaExceededError):
167209
await handler.getDetails("8.8.8.8")
168210

211+
169212
#############
170213
# BATCH TESTS
171214
#############
@@ -198,7 +241,9 @@ def _check_batch_details(ips, details, token):
198241
assert "domains" in d
199242

200243

201-
@pytest.mark.skipif(skip_if_python_3_11_or_later, reason="Requires Python 3.10 or earlier")
244+
@pytest.mark.skipif(
245+
skip_if_python_3_11_or_later, reason="Requires Python 3.10 or earlier"
246+
)
202247
@pytest.mark.parametrize("batch_size", [None, 1, 2, 3])
203248
@pytest.mark.asyncio
204249
async def test_get_batch_details(batch_size):
@@ -229,15 +274,15 @@ async def test_get_iterative_batch_details(batch_size):
229274
_check_iterative_batch_details(ips, details, token)
230275

231276

232-
@pytest.mark.skipif(skip_if_python_3_11_or_later, reason="Requires Python 3.10 or earlier")
277+
@pytest.mark.skipif(
278+
skip_if_python_3_11_or_later, reason="Requires Python 3.10 or earlier"
279+
)
233280
@pytest.mark.parametrize("batch_size", [None, 1, 2, 3])
234281
@pytest.mark.asyncio
235282
async def test_get_batch_details_total_timeout(batch_size):
236283
handler, token, ips = _prepare_batch_test()
237284
with pytest.raises(ipinfo.exceptions.TimeoutExceededError):
238-
await handler.getBatchDetails(
239-
ips, batch_size=batch_size, timeout_total=0.001
240-
)
285+
await handler.getBatchDetails(ips, batch_size=batch_size, timeout_total=0.001)
241286
await handler.deinit()
242287

243288

@@ -260,30 +305,65 @@ async def test_bogon_details():
260305

261306

262307
@pytest.mark.asyncio
263-
async def test_get_resproxy():
264-
token = os.environ.get("IPINFO_TOKEN", "")
265-
if not token:
266-
pytest.skip("token required for resproxy tests")
267-
handler = AsyncHandler(token)
268-
# Use an IP known to be a residential proxy (from API documentation)
308+
async def test_get_resproxy(monkeypatch):
309+
mock_response = MockResponse(
310+
json.dumps(
311+
{
312+
"ip": "175.107.211.204",
313+
"last_seen": "2025-01-20",
314+
"percent_days_seen": 0.85,
315+
"service": "example_service",
316+
}
317+
),
318+
200,
319+
{"Content-Type": "application/json"},
320+
)
321+
322+
def mock_get(*args, **kwargs):
323+
return mock_response
324+
325+
handler = AsyncHandler("test_token")
326+
handler._ensure_aiohttp_ready()
327+
monkeypatch.setattr(handler.httpsess, "get", mock_get)
328+
269329
details = await handler.getResproxy("175.107.211.204")
270330
assert isinstance(details, Details)
271331
assert details.ip == "175.107.211.204"
272-
assert details.last_seen is not None
273-
assert details.percent_days_seen is not None
274-
assert details.service is not None
332+
assert details.last_seen == "2025-01-20"
333+
assert details.percent_days_seen == 0.85
334+
assert details.service == "example_service"
275335
await handler.deinit()
276336

277337

278338
@pytest.mark.asyncio
279-
async def test_get_resproxy_caching():
280-
token = os.environ.get("IPINFO_TOKEN", "")
281-
if not token:
282-
pytest.skip("token required for resproxy tests")
283-
handler = AsyncHandler(token)
339+
async def test_get_resproxy_caching(monkeypatch):
340+
call_count = 0
341+
342+
def mock_get(*args, **kwargs):
343+
nonlocal call_count
344+
call_count += 1
345+
return MockResponse(
346+
json.dumps(
347+
{
348+
"ip": "175.107.211.204",
349+
"last_seen": "2025-01-20",
350+
"percent_days_seen": 0.85,
351+
"service": "example_service",
352+
}
353+
),
354+
200,
355+
{"Content-Type": "application/json"},
356+
)
357+
358+
handler = AsyncHandler("test_token")
359+
handler._ensure_aiohttp_ready()
360+
monkeypatch.setattr(handler.httpsess, "get", mock_get)
361+
284362
# First call should hit the API
285363
details1 = await handler.getResproxy("175.107.211.204")
286364
# Second call should hit the cache
287365
details2 = await handler.getResproxy("175.107.211.204")
288366
assert details1.ip == details2.ip
289-
await handler.deinit()
367+
# Verify only one API call was made (second was cached)
368+
assert call_count == 1
369+
await handler.deinit()

tests/handler_test.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -243,27 +243,60 @@ def test_iterative_bogon_details():
243243
#################
244244

245245

246-
def test_get_resproxy():
247-
token = os.environ.get("IPINFO_TOKEN", "")
248-
if not token:
249-
pytest.skip("token required for resproxy tests")
246+
def test_get_resproxy(monkeypatch):
247+
def mock_get(*args, **kwargs):
248+
response = requests.Response()
249+
response.status_code = 200
250+
response.headers = {"Content-Type": "application/json"}
251+
response._content = b'{"ip": "175.107.211.204", "last_seen": "2025-01-20", "percent_days_seen": 0.85, "service": "example_service"}'
252+
return response
253+
254+
monkeypatch.setattr(requests, "get", mock_get)
255+
token = "test_token"
250256
handler = Handler(token)
251-
# Use an IP known to be a residential proxy (from API documentation)
252257
details = handler.getResproxy("175.107.211.204")
253258
assert isinstance(details, Details)
254259
assert details.ip == "175.107.211.204"
255-
assert details.last_seen is not None
256-
assert details.percent_days_seen is not None
257-
assert details.service is not None
260+
assert details.last_seen == "2025-01-20"
261+
assert details.percent_days_seen == 0.85
262+
assert details.service == "example_service"
258263

259264

260-
def test_get_resproxy_caching():
261-
token = os.environ.get("IPINFO_TOKEN", "")
262-
if not token:
263-
pytest.skip("token required for resproxy tests")
265+
def test_get_resproxy_caching(monkeypatch):
266+
call_count = 0
267+
268+
def mock_get(*args, **kwargs):
269+
nonlocal call_count
270+
call_count += 1
271+
response = requests.Response()
272+
response.status_code = 200
273+
response.headers = {"Content-Type": "application/json"}
274+
response._content = b'{"ip": "175.107.211.204", "last_seen": "2025-01-20", "percent_days_seen": 0.85, "service": "example_service"}'
275+
return response
276+
277+
monkeypatch.setattr(requests, "get", mock_get)
278+
token = "test_token"
264279
handler = Handler(token)
265280
# First call should hit the API
266281
details1 = handler.getResproxy("175.107.211.204")
267282
# Second call should hit the cache
268283
details2 = handler.getResproxy("175.107.211.204")
269-
assert details1.ip == details2.ip
284+
assert details1.ip == details2.ip
285+
# Verify only one API call was made (second was cached)
286+
assert call_count == 1
287+
288+
289+
def test_get_resproxy_empty(monkeypatch):
290+
def mock_get(*args, **kwargs):
291+
response = requests.Response()
292+
response.status_code = 200
293+
response.headers = {"Content-Type": "application/json"}
294+
response._content = b"{}"
295+
return response
296+
297+
monkeypatch.setattr(requests, "get", mock_get)
298+
token = "test_token"
299+
handler = Handler(token)
300+
details = handler.getResproxy("8.8.8.8")
301+
assert isinstance(details, Details)
302+
assert details.all == {}

0 commit comments

Comments
 (0)