Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,33 @@ 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")
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")
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")
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
Expand Down
73 changes: 48 additions & 25 deletions friendly_captcha_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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(
Expand All @@ -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)
2 changes: 2 additions & 0 deletions friendly_captcha_client/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dev@friendlycaptcha.com>",
Expand Down
52 changes: 52 additions & 0 deletions tests/test_friendly_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest.mock import Mock, patch

import pytest
import requests
import requests_mock

from friendly_captcha_client.client import (
Expand Down Expand Up @@ -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="<html><body>Service unavailable</body></html>",
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
Loading