diff --git a/src/pyapiary/api_connectors/ipqs.py b/src/pyapiary/api_connectors/ipqs.py index fc9d137..80594ea 100644 --- a/src/pyapiary/api_connectors/ipqs.py +++ b/src/pyapiary/api_connectors/ipqs.py @@ -36,6 +36,19 @@ def malicious_url(self, query: str, **kwargs) -> httpx.Response: """ return self.post("/url/", data={"url": query, "key": self.api_key, **kwargs}) + @log_method_call + def phone_validation(self, query: str, **kwargs) -> httpx.Response: + """The IPQS Phone Number Validation API offers rapid analysis to determine the risk core, + country of origin, carrier, validity, owner information, and connection status of phone numbers + + Args: + query (str): The phone number to look up. + + Returns: + httpx.Response: the httpx.Response object + """ + return self.post("/phone/", data={"phone": query, "key": self.api_key, **kwargs}) + @bubble_broker_init_signature() class AsyncIPQSConnector(AsyncBroker): @@ -63,3 +76,16 @@ async def malicious_url(self, query: str, **kwargs) -> httpx.Response: httpx.Response: the httpx.Response object """ return await self.post("/url/", data={"url": query, "key": self.api_key, **kwargs}) + + @log_method_call + async def phone_validation(self, query: str, **kwargs) -> httpx.Response: + """The IPQS Phone Number Validation API offers rapid analysis to determine the risk core, + country of origin, carrier, validity, owner information, and connection status of phone numbers + + Args: + query (str): The phone number to look up. + + Returns: + httpx.Response: the httpx.Response object + """ + return await self.post("/phone/", data={"phone": query, "key": self.api_key, **kwargs}) diff --git a/src/pyapiary/tests/test_ipqs/cassettes/test_ipqs_phone_validation_vcr.yaml b/src/pyapiary/tests/test_ipqs/cassettes/test_ipqs_phone_validation_vcr.yaml new file mode 100644 index 0000000..7c80bec --- /dev/null +++ b/src/pyapiary/tests/test_ipqs/cassettes/test_ipqs_phone_validation_vcr.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: phone=12024567041&key=HOwg8USe8QTBYpdxDEnfAqWZeb0kpAuQ + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '54' + content-type: + - application/x-www-form-urlencoded + host: + - www.ipqualityscore.com + user-agent: + - REDACTED + method: POST + uri: https://www.ipqualityscore.com/api/json/phone/ + response: + body: + string: !!binary | + H4sIAAAAAAAA/1VUXW/iOBT9K1aedjUM03a7s1KfcAE12aGBSaBVtbOKjGOIi2NnbYcqM+p/3+NA + aXmJcu+5Pvf7/opq4RzbiugmWlRGCyId2TMly2E0iFzLOeDoxttWDKKNsTXzXpQw/nR5dXF1/efX + vy6uL2GpDGeqOBgA/Q3g7wTw5yPeU554LGvLwnFj4fZiEFnBhfYFW7cOig1TDkYP82RxEhorGhYI + dKsUHki3604g417uxRs5Z9ZKYRHEg7Dyp9FkbOq61ZIzL412IVipReG7JiQ9Y7oMMtTctNpb8Ear + PIjSh/9HmsdJerecp9D9lE3BTRkeXl1c/PE1CsFvQQvFZAyplEhUb49Gl2/BFc4z36KQEe1lMoNL + 8pnEclshPr2RpdA8BOFqV5SmZjJQpj++UOiYc4ZLhsIXAogqWFla9EWA8Ff0Tn0yI72ZIxtk1Dfy + IEc3/4QAjVDSi9E26Ibc1MAb1irwWOnZqHWleR5uzR76Z+NEU72gpEKfARZl69bCbv9rpfPnkFnr + TgE/84C2qBEv2V6WzmipXQsKLo7os3QVa6Q1o45Vxhy1O8meTaVhf+bAV6iP0w1TwtbmzEvJNFvb + 1vHzNBSDtx1rENWZfiuM3QrHqxapnGfBKwyZ716k2IZUPkIbBN65GlPVjZhRpxR05yqpR5XZOG/Z + UJRt9O/rIMJQ26Ifg8NATbUXtgE7puD6E7ECJbSib1Ot+XvXa/5BUILtxGmBXMPqOsz4cQMCh3C+ + CBuCLZXXfvfw8lThnWZ1GNa76Ty7m5LbVR4PyIKuZiRf0ixZ0gH5e55PFzF5pFk2TQcko+nkidxO + s7vvqyRfQjG/JenTDOoBGdNsRib0IZnk8zQ8TekypinJY7pIsnnQxCkg8i0B8zKe39Mc7mbT7B7g + hKaU3GarfAxwRlcZJd/oAswDcowvH8cruIbXcZzB+xN5TKZ3vetHfCnJ75MlMoC/x5Tcr7JF/ASn + SYggCfvpZS2w8iFligJh5398mQi9R60wHKbQxhc4VOpUOc8bVqwV4zuFdn84KRwT40VxvAnHfT6i + YUf7jXrvj25rjFCBS9bxcADCYr5f1rRHyQklG4GdtYKU0rG1QvPJAh3GRHCjPWaFuLZpjPXEG9KP + DmIZRm+XUnVHR+J0ERVD+8ML3IgSxm/6g+ogvb7+D6arKrXuBQAA + headers: + CF-RAY: + - 9e51976b889d98d5-ATL + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Tue, 31 Mar 2026 18:59:29 GMT + Nel: + - '{"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}' + Report-To: + - '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=MNIXz1eWc6WlUYSlvt%2FrHoZ5QQ62jQe%2BvrW%2B0dRfxNJU4OAh9YKgXohdqxSO3ED9eYP2ynVDPg4y7nwpTZdHvmVi8GcVjKsApuuMnMiWLjJXwliCb5Yq22rBsNTixyO5F%2BrJesPFCuc%3D"}]}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-LB-ID: + - LB-3 + X-Powered-By: + - IPQualityScore + X-Upstream-ID: + - N-152 + cf-cache-status: + - DYNAMIC + status: + code: 200 + message: OK +version: 1 diff --git a/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py b/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py index 4fb50ea..31aef73 100644 --- a/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py +++ b/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py @@ -10,4 +10,15 @@ def test_ipqs_malicious_url_vcr(vcr_cassette): assert isinstance(result, httpx.Response) assert "domain" in result.json() - assert result.json()["domain"] == "github.com" \ No newline at end of file + assert result.json()["domain"] == "github.com" + + +@pytest.mark.integration +def test_ipqs_phone_validation_vcr(vcr_cassette): + with vcr_cassette.use_cassette("test_ipqs_phone_validation_vcr"): + connector = IPQSConnector(load_env_vars=True, enable_logging=True) + result = connector.phone_validation("+12024567041") + + assert isinstance(result, httpx.Response) + assert "formatted" in result.json() + assert result.json()["formatted"] == "+12024567041" diff --git a/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py b/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py index 88b6895..74f392d 100644 --- a/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py +++ b/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py @@ -51,3 +51,30 @@ async def test_async_malicious_url(mock_post): "/url/", data={"url": "example.com", "key": "test_key", "strictness": 1} ) + + +@patch("pyapiary.api_connectors.ipqs.AsyncIPQSConnector.post", new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_async_phone_validation(mock_post): + import json + + request = httpx.Request("POST", "https://www.ipqualityscore.com/api/json/url/") + payload = {"success": True, "phone": "8888888888"} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_post.return_value = mock_response + + connector = AsyncIPQSConnector(api_key="test_key") + response = await connector.phone_validation("8888888888", strictness=1) + + assert isinstance(response, httpx.Response) + assert response.status_code == 200 + assert response.json() == payload + mock_post.assert_awaited_once_with( + "/phone/", + data={"phone": "8888888888", "key": "test_key", "strictness": 1} + ) \ No newline at end of file diff --git a/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py b/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py index 0953a9b..7064d94 100644 --- a/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py +++ b/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py @@ -46,3 +46,29 @@ def test_malicious_url(mock_post): ) assert isinstance(result, httpx.Response) assert result.json() == payload + + +@patch("pyapiary.api_connectors.ipqs.IPQSConnector.post") +def test_phone_validation(mock_post): + # Build a real httpx.Response to match the new return type + import json + + request = httpx.Request("POST", "https://www.ipqualityscore.com/api/json/phone/") + payload = {"success": True, "phone": "8888888888"} + mock_response = httpx.Response( + 200, + request=request, + content=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + mock_post.return_value = mock_response + + connector = IPQSConnector(api_key="test_key") + result = connector.phone_validation("8888888888") + + mock_post.assert_called_once_with( + "/phone/", + data={"phone": "8888888888", "key": "test_key"}, + ) + assert isinstance(result, httpx.Response) + assert result.json() == payload