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
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- uses: nanasess/setup-chromedriver@v2

- name: Run integration tests
run: make test
run: make test_e2e
env:
KEY: ${{ secrets.anticaptcha_key }}
PROXY_URL: "${{ secrets.proxy_url }}"
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CHROMEDRIVER_VERSION=99.0.4844.17
CHROMEDRIVER_DIR=${PWD}/geckodriver

.PHONY: lint fmt build docs install test gecko
.PHONY: lint fmt build docs install test test_e2e gecko

build:
python setup.py sdist bdist_wheel
Expand All @@ -25,7 +25,10 @@ gecko:
rm ${CHROMEDRIVER_DIR}/chromedriver_linux64.zip

test:
PATH=$$PWD/geckodriver:$$PATH nose2 --verbose
pytest

test_e2e:
PATH=$$PWD/geckodriver:$$PATH pytest -m e2e --override-ini="addopts="

clean:
rm -r build geckodriver
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"e2e: end-to-end tests requiring API keys and network access",
]
addopts = "-m 'not e2e'"
3 changes: 0 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,3 @@ current_version = 0.4.2
commit = True
tag = True
tag_name = {new_version}

[nosetests]
process-timeout = 600
4 changes: 1 addition & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
long_description = f.read()


tests_deps = ["retry", "nose2", "selenium"]
tests_deps = ["retry", "pytest", "selenium"]

extras = {"tests": tests_deps, "docs": "sphinx"}

setup(
test_suite="nose2.collector.collector",
name="python-anticaptcha",
description="Client library for solve captchas with Anticaptcha.com support.",
long_description=long_description,
Expand All @@ -39,6 +38,5 @@
keywords="recaptcha captcha development",
packages=["python_anticaptcha"],
install_requires=["requests"],
tests_require=tests_deps,
extras_require=extras,
)
149 changes: 149 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from unittest.mock import patch, MagicMock
import pytest

from python_anticaptcha.base import AnticaptchaClient, Job, SLEEP_EVERY_CHECK_FINISHED
from python_anticaptcha.exceptions import AnticaptchaException


class TestAnticaptchaClientInit:
def test_https_url(self):
client = AnticaptchaClient("key123")
assert client.base_url == "https://api.anti-captcha.com/"
assert client.client_key == "key123"

def test_http_url(self):
client = AnticaptchaClient("key123", use_ssl=False)
assert client.base_url == "http://api.anti-captcha.com/"

def test_custom_host(self):
client = AnticaptchaClient("key123", host="custom.host.com")
assert client.base_url == "https://custom.host.com/"

def test_language_pool(self):
client = AnticaptchaClient("key123", language_pool="rn")
assert client.language_pool == "rn"


class TestCheckResponse:
def setup_method(self):
self.client = AnticaptchaClient("key123")

def test_success_passthrough(self):
response = {"errorId": 0, "taskId": 42}
self.client._check_response(response)

def test_error_raises(self):
response = {
"errorId": 1,
"errorCode": "ERROR_KEY_DOES_NOT_EXIST",
"errorDescription": "Account authorization key not found",
}
with pytest.raises(AnticaptchaException) as exc_info:
self.client._check_response(response)
assert exc_info.value.error_id == 1
assert exc_info.value.error_code == "ERROR_KEY_DOES_NOT_EXIST"

def test_error_id_11_appends_ip(self):
response = {
"errorId": 11,
"errorCode": "ERROR_IP_NOT_ALLOWED",
"errorDescription": "IP not allowed",
}
with patch.object(
type(self.client), "client_ip", new_callable=lambda: property(lambda self: "5.6.7.8")
):
with pytest.raises(AnticaptchaException) as exc_info:
self.client._check_response(response)
assert "5.6.7.8" in exc_info.value.error_description


class TestCreateTask:
def test_payload_structure(self):
client = AnticaptchaClient("key123")
mock_task = MagicMock()
mock_task.serialize.return_value = {"type": "NoCaptchaTaskProxyless", "websiteURL": "https://example.com"}

mock_response = MagicMock()
mock_response.json.return_value = {"errorId": 0, "taskId": 99}

with patch.object(client.session, "post", return_value=mock_response) as mock_post:
job = client.createTask(mock_task)

call_kwargs = mock_post.call_args
payload = call_kwargs[1]["json"] if "json" in call_kwargs[1] else call_kwargs.kwargs["json"]
assert payload["clientKey"] == "key123"
assert payload["task"] == {"type": "NoCaptchaTaskProxyless", "websiteURL": "https://example.com"}
assert payload["softId"] == 847
assert payload["languagePool"] == "en"
assert isinstance(job, Job)
assert job.task_id == 99


class TestGetBalance:
def test_returns_balance(self):
client = AnticaptchaClient("key123")
mock_response = MagicMock()
mock_response.json.return_value = {"errorId": 0, "balance": 3.21}

with patch.object(client.session, "post", return_value=mock_response):
balance = client.getBalance()
assert balance == 3.21


class TestJobCheckIsReady:
def test_ready(self):
client = MagicMock()
client.getTaskResult.return_value = {"status": "ready", "solution": {}}
job = Job(client, task_id=1)
assert job.check_is_ready() is True

def test_processing(self):
client = MagicMock()
client.getTaskResult.return_value = {"status": "processing"}
job = Job(client, task_id=1)
assert job.check_is_ready() is False


class TestJobSolutionGetters:
def setup_method(self):
self.client = MagicMock()
self.job = Job(self.client, task_id=1)
self.job._last_result = {
"status": "ready",
"solution": {
"gRecaptchaResponse": "recaptcha-token",
"token": "funcaptcha-token",
"text": "captcha text",
"cellNumbers": [1, 3, 5],
"answers": {"q1": "a1"},
},
}

def test_get_solution_response(self):
assert self.job.get_solution_response() == "recaptcha-token"

def test_get_token_response(self):
assert self.job.get_token_response() == "funcaptcha-token"

def test_get_captcha_text(self):
assert self.job.get_captcha_text() == "captcha text"

def test_get_cells_numbers(self):
assert self.job.get_cells_numbers() == [1, 3, 5]

def test_get_answers(self):
assert self.job.get_answers() == {"q1": "a1"}

def test_get_solution(self):
assert self.job.get_solution() == self.job._last_result["solution"]


class TestJobJoinTimeout:
@patch("python_anticaptcha.base.time.sleep")
def test_timeout_raises(self, mock_sleep):
client = MagicMock()
client.getTaskResult.return_value = {"status": "processing"}
job = Job(client, task_id=1)
with pytest.raises(AnticaptchaException) as exc_info:
job.join(maximum_time=SLEEP_EVERY_CHECK_FINISHED)
assert "exceeded" in str(exc_info.value).lower()
11 changes: 9 additions & 2 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
from retry import retry

import os
import pytest

from python_anticaptcha import AnticatpchaException
from contextlib import contextmanager

_multiprocess_can_split_ = True


def missing_key(*args, **kwargs):
return skipIf(
Expand All @@ -23,6 +22,7 @@ def missing_proxy(*args, **kwargs):
)(*args, **kwargs)


@pytest.mark.e2e
@missing_key
class AntiGateTestCase(TestCase):
@retry(tries=3)
Expand All @@ -34,6 +34,7 @@ def test_process_antigate(self):
self.assertIn(key, solution)


@pytest.mark.e2e
@missing_key
@missing_proxy
class FuncaptchaTestCase(TestCase):
Expand All @@ -46,6 +47,7 @@ def test_funcaptcha(self):
self.assertIn("Solved!", funcaptcha_request.process())


@pytest.mark.e2e
@missing_key
class RecaptchaRequestTestCase(TestCase):
# Anticaptcha responds is not fully reliable.
Expand All @@ -56,6 +58,7 @@ def test_process(self):
self.assertIn(recaptcha_request.EXPECTED_RESULT, recaptcha_request.process())


@pytest.mark.e2e
@missing_key
@skipIf(True, "Anti-captcha unable to provide required score, but we tests via proxy")
class RecaptchaV3ProxylessTestCase(TestCase):
Expand All @@ -78,6 +81,7 @@ def open_driver(*args, **kwargs):
driver.quit()


@pytest.mark.e2e
@missing_key
class RecaptchaSeleniumtTestCase(TestCase):
# Anticaptcha responds is not fully reliable.
Expand All @@ -98,6 +102,7 @@ def test_process(self):
)


@pytest.mark.e2e
@missing_key
class TextTestCase(TestCase):
def test_process(self):
Expand All @@ -106,6 +111,7 @@ def test_process(self):
self.assertEqual(text.process(text.IMAGE).lower(), text.EXPECTED_RESULT.lower())


@pytest.mark.e2e
@missing_key
@skipIf(True, "We testing via proxy for performance reason.")
class HCaptchaTaskProxylessTestCase(TestCase):
Expand All @@ -116,6 +122,7 @@ def test_process(self):
self.assertIn(hcaptcha_request.EXPECTED_RESULT, hcaptcha_request.process())


@pytest.mark.e2e
@missing_key
@missing_proxy
class HCaptchaTaskTestCase(TestCase):
Expand Down
42 changes: 42 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from python_anticaptcha.exceptions import (
AnticaptchaException,
AnticatpchaException,
InvalidWidthException,
MissingNameException,
)


class TestAnticaptchaException:
def test_attributes(self):
exc = AnticaptchaException(1, "ERROR_KEY", "Some description")
assert exc.error_id == 1
assert exc.error_code == "ERROR_KEY"
assert exc.error_description == "Some description"

def test_str_format(self):
exc = AnticaptchaException(1, "ERROR_KEY", "Some description")
assert str(exc) == "[ERROR_KEY:1]Some description"

def test_typo_alias(self):
assert AnticatpchaException is AnticaptchaException


class TestInvalidWidthException:
def test_message(self):
exc = InvalidWidthException(75)
assert exc.width == 75
assert "75" in str(exc)
assert "100, 50, 33, 25" in str(exc)
assert exc.error_id == "AC-1"
assert exc.error_code == 1


class TestMissingNameException:
def test_message(self):
exc = MissingNameException("MyClass")
assert exc.cls == "MyClass"
assert "MyClass" in str(exc)
assert '__init__(name="X")' in str(exc)
assert 'serialize(name="X")' in str(exc)
assert exc.error_id == "AC-2"
assert exc.error_code == 2
Loading
Loading