diff --git a/README.md b/README.md index a2bdcc3..d5af61d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ After calling `verify_captcha_response` with the captcha response there are two Below are some examples of this behaviour. -#### Verifying a correct captcha response without issues when veryfing: +#### Verifying a correct captcha response without issues when verifying: ```python result = client.verify_captcha_response("CORRECT?CAPTCHA_RESPONSE_HERE") @@ -46,7 +46,7 @@ print(result.was_able_to_verify) # True print(result.should_accept) # True ``` -#### Verifying an incorrect captcha response without issues when veryfing: +#### Verifying an incorrect captcha response without issues when verifying: ```python result = client.verify_captcha_response("INCORRECT_CAPTCHA_RESPONSE_HERE") @@ -54,7 +54,7 @@ print(result.was_able_to_verify) # True print(result.should_accept) # False ``` -#### Verifying an incorrect captcha response with issues (network issues or bad configuration) when veryfing in non-strict mode (default): +#### Verifying an incorrect captcha response with issues (network issues or bad configuration) when verifying in non-strict mode (default): ```python result = client.verify_captcha_response("INCORRECT_CAPTCHA_RESPONSE_HERE") @@ -62,7 +62,9 @@ print(result.was_able_to_verify) # False print(result.should_accept) # True ``` -#### Verifying an incorrect captcha response with issues (network/service issues or bad configuration) when veryfing in strict mode: +Network errors don't raise an exception. Check `result.error.error_code` for `request_failed` or `request_failed_due_to_timeout`. + +#### Verifying an incorrect captcha response with issues (network/service issues or bad configuration) when verifying in strict mode: ```python client.strict = True diff --git a/friendly_captcha_client/client.py b/friendly_captcha_client/client.py index 1b3634d..844a871 100644 --- a/friendly_captcha_client/client.py +++ b/friendly_captcha_client/client.py @@ -230,11 +230,19 @@ def _is_error_loose(self, error, status_code): or all( # unknown errors where we allow loose verification error_code != _error.value for _error in DefaultErrorCodes ) - and status_code in [200, 500] + and (status_code == 200 or status_code >= 500) ): return True return False + def _request_failed_error(self, e: requests.RequestException) -> Error: + if self.verbose: + self.logger.error("Error requesting Friendly Captcha API: %s", e) + error_code = DefaultErrorCodes.REQUEST_FAILED + if isinstance(e, requests.Timeout): + error_code = DefaultErrorCodes.REQUEST_FAILED_TIMEOUT + return Error(error_code=error_code.value, detail=str(e)) + @staticmethod def _is_decode_response_failed(error: Union[Error, None]) -> bool: return ( @@ -326,17 +334,25 @@ def verify_captcha_response( was_able_to_verify=True, ) - response = requests.post( - url=self.siteverify_endpoint, - json={"response": captcha_response, "sitekey": self.sitekey}, - headers={ - "Content-Type": "application/json", - "Accept": "application/json", - "X-Api-Key": self.api_key, - "Frc-Sdk": f"friendly-captcha-python@{self._get_current_version()}", - }, - timeout=timeout, - ) + try: + response = requests.post( + url=self.siteverify_endpoint, + json={"response": captcha_response, "sitekey": self.sitekey}, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Api-Key": self.api_key, + "Frc-Sdk": f"friendly-captcha-python@{self._get_current_version()}", + }, + timeout=timeout, + ) + except requests.RequestException as e: + return FriendlyCaptchaResult( + should_accept=not self.strict, + was_able_to_verify=False, + is_client_error=False, + error=self._request_failed_error(e), + ) return self._handle_verify_captcha_response(response) def retrieve_risk_intelligence( @@ -356,20 +372,27 @@ def retrieve_risk_intelligence( """ if not isinstance(token, str): return RiskIntelligenceRetrieveResult( + is_valid=False, was_able_to_retrieve=False, ) - print(f"friendly-captcha-python@{self._get_current_version()}") - - response = requests.post( - url=self.risk_intelligence_retrieve_endpoint, - json={"token": token, "sitekey": self.sitekey}, - headers={ - "Content-Type": "application/json", - "Accept": "application/json", - "X-Api-Key": self.api_key, - "Frc-Sdk": f"friendly-captcha-python@{self._get_current_version()}", - }, - timeout=timeout, - ) + try: + response = requests.post( + url=self.risk_intelligence_retrieve_endpoint, + json={"token": token, "sitekey": self.sitekey}, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Api-Key": self.api_key, + "Frc-Sdk": f"friendly-captcha-python@{self._get_current_version()}", + }, + timeout=timeout, + ) + except requests.RequestException as e: + return RiskIntelligenceRetrieveResult( + is_valid=False, + was_able_to_retrieve=False, + is_client_error=False, + error=self._request_failed_error(e), + ) return self._handle_risk_intelligence_retrieve_response(response) diff --git a/friendly_captcha_client/schemas.py b/friendly_captcha_client/schemas.py index b0b4b45..5baf883 100644 --- a/friendly_captcha_client/schemas.py +++ b/friendly_captcha_client/schemas.py @@ -32,6 +32,8 @@ class DefaultErrorCodes(str, Enum): RESPONSE_TIMEOUT = "response_timeout" # 200 RESPONSE_DUPLICATE = "response_duplicate" # 200 CLIENT_ERROR = "request_failed_due_to_client_error" + REQUEST_FAILED = "request_failed" + REQUEST_FAILED_TIMEOUT = "request_failed_due_to_timeout" @staticmethod def contains(value: str) -> bool: diff --git a/pyproject.toml b/pyproject.toml index e3c3c45..a36631d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "friendly-captcha-client" -version = "0.2.1" +version = "0.3.0" description = "A client for Friendly Captcha." authors = [ "Antal Nagy ", diff --git a/tests/test_friendly_client.py b/tests/test_friendly_client.py index 1be94ca..c00c003 100644 --- a/tests/test_friendly_client.py +++ b/tests/test_friendly_client.py @@ -1,6 +1,7 @@ from unittest.mock import Mock, patch import pytest +import requests import requests_mock from friendly_captcha_client.client import ( @@ -419,3 +420,54 @@ def test_retrieve_risk_intelligence_bad_response_with_500(client): ) assert result.was_able_to_retrieve is False assert result.is_client_error is False + + +@pytest.mark.parametrize( + "exception,expected_error_code", + [ + (requests.ConnectionError("refused"), DefaultErrorCodes.REQUEST_FAILED), + (requests.Timeout("timed out"), DefaultErrorCodes.REQUEST_FAILED_TIMEOUT), + ], +) +def test_verify_captcha_response_request_failed(client, exception, expected_error_code): + with patch("requests.post", side_effect=exception): + result: FriendlyCaptchaResult = client.verify_captcha_response(CAPTCHA_RESPONSE) + assert result.should_accept is True + assert result.was_able_to_verify is False + assert result.is_client_error is False + assert result.error.error_code == expected_error_code + + +def test_verify_captcha_response_request_failed_strict(strict_client): + with patch("requests.post", side_effect=requests.ConnectionError("refused")): + result: FriendlyCaptchaResult = strict_client.verify_captcha_response( + CAPTCHA_RESPONSE + ) + assert result.should_accept is False + assert result.was_able_to_verify is False + assert result.is_client_error is False + + +def test_retrieve_risk_intelligence_request_failed(client): + with patch("requests.post", side_effect=requests.ConnectionError("refused")): + result: RiskIntelligenceRetrieveResult = client.retrieve_risk_intelligence( + "token" + ) + assert result.is_valid is False + assert result.was_able_to_retrieve is False + assert result.is_client_error is False + assert result.error.error_code == DefaultErrorCodes.REQUEST_FAILED + + +def test_verify_captcha_response_bad_response_with_503(client): + with requests_mock.Mocker() as m: + m.post( + client.siteverify_endpoint, + text="Service unavailable", + status_code=503, + headers={"Content-Type": "text/html"}, + ) + result: FriendlyCaptchaResult = client.verify_captcha_response(CAPTCHA_RESPONSE) + assert result.should_accept is True + assert result.was_able_to_verify is False + assert result.is_client_error is False