22import os
33import sys
44
5+ import aiohttp
6+ import ipinfo
7+ import pytest
8+ from ipinfo import handler_utils
59from ipinfo .cache .default import DefaultCache
610from ipinfo .details import Details
7- from ipinfo .handler_async import AsyncHandler
8- from ipinfo import handler_utils
911from ipinfo .error import APIError
1012from ipinfo .exceptions import RequestQuotaExceededError
11- import ipinfo
12- import pytest
13- import aiohttp
13+ from ipinfo .handler_async import AsyncHandler
1414
1515skip_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
158196async 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
204249async 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
235282async 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 ()
0 commit comments