diff --git a/docs/conf.py b/docs/conf.py index cb276aa4..38108420 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,6 +25,7 @@ text_captcha, image_captcha, lemin_captcha, + yidun_captcha, rotate_captcha, binance_captcha, datadome_captcha, diff --git a/docs/index.rst b/docs/index.rst index fd9e801c..a26664d5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -56,6 +56,8 @@ Check our other projects here - `RedPandaDev group >> YandexSmartCaptcha( + ... rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteURL="https://example.com/", + ... websiteKey="Y5Lh0ti...", + ... ).captcha_handler() + {"errorId": 0, "status": "ready", "solution": {"token": "..."}, "taskId": ...} + + >>> await YandexSmartCaptcha( + ... rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteURL="https://example.com/", + ... websiteKey="Y5Lh0ti...", + ... method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask, + ... proxyType="http", + ... proxyAddress="1.2.3.4", + ... proxyPort=8080, + ... ).aio_captcha_handler() + {"errorId": 0, "status": "ready", "solution": {"token": "..."}, "taskId": ...} + + >>> YandexSmartCaptcha( + ... rucaptcha_key="aa9011f31111181111168611f1151122", + ... method=CoordinatesCaptchaEnm.CoordinatesTask, + ... imgType="smart_captcha", + ... comment="select objects in the order of the instruction", + ... ).captcha_handler( + ... captcha_file="src/examples/088636.png", + ... imgInstructions_file="src/examples/bounding_box_start.png", + ... ) + {"errorId": 0, "status": "ready", "solution": {"coordinates": [{"x": 57, "y": 82}, ...]}, "taskId": ...} + + Notes: + https://2captcha.com/api-docs/yandex-smart-captcha + + https://rucaptcha.com/api-docs/yandex-smart-captcha + """ + + _VALID_METHODS = YandexSmartCaptchaEnm.list_values() + [CoordinatesCaptchaEnm.CoordinatesTask.value] + + def __init__( + self, + websiteURL: Optional[str] = None, + websiteKey: Optional[str] = None, + method: Union[str, YandexSmartCaptchaEnm, CoordinatesCaptchaEnm] = ( + YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless + ), + userAgent: Optional[str] = None, + cookies: Optional[str] = None, + proxyType: Optional[str] = None, + proxyAddress: Optional[str] = None, + proxyPort: Optional[int] = None, + proxyLogin: Optional[str] = None, + proxyPassword: Optional[str] = None, + imgType: Optional[str] = None, + comment: Optional[str] = None, + save_format: Union[str, SaveFormatsEnm] = SaveFormatsEnm.TEMP, + img_clearing: bool = True, + img_path: str = "PythonRuCaptchaYandexSmart", + *args, + **kwargs, + ): + method_str = method.value if hasattr(method, "value") else method + + if method_str not in self._VALID_METHODS: + raise ValueError(f"Invalid method parameter set, available - {self._VALID_METHODS}") + + is_token = method_str in YandexSmartCaptchaEnm.list_values() + is_image = method_str == CoordinatesCaptchaEnm.CoordinatesTask.value + + # token-method-specific validation + if is_token: + if not (websiteURL and websiteKey): + raise ValueError( + "websiteURL and websiteKey are required for token methods " + f"({YandexSmartCaptchaEnm.list_values()})" + ) + + # proxy-method-specific validation + if method_str == YandexSmartCaptchaEnm.YandexSmartCaptchaTask.value: + if not all([proxyType, proxyAddress, proxyPort]): + raise ValueError( + "proxyType, proxyAddress, and proxyPort are required for YandexSmartCaptchaTask" + ) + + # image-method-specific validation + if is_image: + if not imgType: + raise ValueError("imgType is required for CoordinatesTask") + if imgType == "smart_captcha" and not comment: + raise ValueError('comment is required for CoordinatesTask with imgType="smart_captcha"') + + # Build task payload + task_data: dict[str, Any] = {} + if is_token: + task_data["websiteURL"] = websiteURL + task_data["websiteKey"] = websiteKey + if userAgent is not None: + task_data["userAgent"] = userAgent + if cookies is not None: + task_data["cookies"] = cookies + if method_str == YandexSmartCaptchaEnm.YandexSmartCaptchaTask.value: + task_data["proxyType"] = proxyType + task_data["proxyAddress"] = proxyAddress + task_data["proxyPort"] = proxyPort + if proxyLogin is not None: + task_data["proxyLogin"] = proxyLogin + if proxyPassword is not None: + task_data["proxyPassword"] = proxyPassword + elif is_image: + task_data["imgType"] = imgType + if comment is not None: + task_data["comment"] = comment + + super().__init__(method=method, *args, **kwargs) + self.method = method_str + self.save_format = save_format + self.img_clearing = img_clearing + self.img_path = img_path + self.create_task_payload["task"].update(task_data) + + def captcha_handler( + self, + captcha_link: Optional[str] = None, + captcha_file: Optional[str] = None, + captcha_base64: Optional[bytes] = None, + imgInstructions_link: Optional[str] = None, + imgInstructions_file: Optional[str] = None, + imgInstructions_base64: Optional[bytes] = None, + **kwargs: dict[str, Any], + ) -> dict[str, Any]: + """ + Sync solving method. + """ + if self.method == CoordinatesCaptchaEnm.CoordinatesTask.value: + self._body_file_processing( + save_format=self.save_format, + file_path=self.img_path, + image_key="body", + captcha_link=captcha_link, + captcha_file=captcha_file, + captcha_base64=captcha_base64, + **kwargs, + ) + if any([imgInstructions_link, imgInstructions_file, imgInstructions_base64]): + self._body_file_processing( + save_format=self.save_format, + file_path=self.img_path, + image_key="imgInstructions", + captcha_link=imgInstructions_link, + captcha_file=imgInstructions_file, + captcha_base64=imgInstructions_base64, + **kwargs, + ) + if not self.result.errorId: + return self._processing_response(**kwargs) + return self.result.to_dict() + + return self._processing_response(**kwargs) + + async def aio_captcha_handler( + self, + captcha_link: Optional[str] = None, + captcha_file: Optional[str] = None, + captcha_base64: Optional[bytes] = None, + imgInstructions_link: Optional[str] = None, + imgInstructions_file: Optional[str] = None, + imgInstructions_base64: Optional[bytes] = None, + **kwargs: dict[str, Any], + ) -> dict[str, Any]: + """ + Async solving method. + """ + if self.method == CoordinatesCaptchaEnm.CoordinatesTask.value: + await self._aio_body_file_processing( + save_format=self.save_format, + file_path=self.img_path, + image_key="body", + captcha_link=captcha_link, + captcha_file=captcha_file, + captcha_base64=captcha_base64, + **kwargs, + ) + if any([imgInstructions_link, imgInstructions_file, imgInstructions_base64]): + await self._aio_body_file_processing( + save_format=self.save_format, + file_path=self.img_path, + image_key="imgInstructions", + captcha_link=imgInstructions_link, + captcha_file=imgInstructions_file, + captcha_base64=imgInstructions_base64, + **kwargs, + ) + if not self.result.errorId: + return await self._aio_processing_response() + return self.result.to_dict() + + return await self._aio_processing_response() + + def __del__(self): + if ( + hasattr(self, "save_format") + and self.save_format == SaveFormatsEnm.CONST.value + and hasattr(self, "img_clearing") + and self.img_clearing + ): + try: + shutil.rmtree(self.img_path) + except OSError: + pass diff --git a/src/python_rucaptcha/yidun_captcha.py b/src/python_rucaptcha/yidun_captcha.py new file mode 100644 index 00000000..8e773c61 --- /dev/null +++ b/src/python_rucaptcha/yidun_captcha.py @@ -0,0 +1,171 @@ +from typing import Union, Optional + +from .core.base import BaseCaptcha +from .core.enums import YidunEnm + + +class YidunCaptcha(BaseCaptcha): + def __init__( + self, + websiteURL: str, + websiteKey: str, + method: Union[str, YidunEnm] = YidunEnm.YidunTaskProxyless, + userAgent: Optional[str] = None, + yidunGetLib: Optional[str] = None, + yidunApiServerSubdomain: Optional[str] = None, + challenge: Optional[str] = None, + hcg: Optional[str] = None, + hct: Optional[int] = None, + proxyType: Optional[str] = None, + proxyAddress: Optional[str] = None, + proxyPort: Optional[int] = None, + proxyLogin: Optional[str] = None, + proxyPassword: Optional[str] = None, + *args, + **kwargs, + ): + """ + The class is used to work with Yidun NECaptcha. + + Args: + rucaptcha_key: User API key + websiteURL: Full URL of the page where the captcha is loaded + websiteKey: Value of the `id` (or `sitekey`) parameter from the page source + or from the `get?referer=` / `check?referer=` network request + method: Captcha type (YidunTaskProxyless or YidunTask) + userAgent: Browser User-Agent used to open the page + yidunGetLib: Full URL of the JS file that loads the captcha. + Recommended for Enterprise version. + yidunApiServerSubdomain: Yidun API server subdomain without `https://`. + Specify if using a custom server. + challenge: Dynamic challenge parameter from network requests (Enterprise) + hcg: Captcha hash used when forming the request (Enterprise) + hct: Numeric timestamp identifier for Enterprise version validation (Unix milliseconds) + proxyType: Proxy type (http, socks4, socks5) - required for YidunTask + proxyAddress: Proxy IP or hostname - required for YidunTask + proxyPort: Proxy port - required for YidunTask + proxyLogin: Proxy login (optional) + proxyPassword: Proxy password (optional) + + Examples: + >>> YidunCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteURL="https://example.com/page-with-yidun", + ... websiteKey="0f743r3g1g...rz3grz0ym5", + ... method=YidunEnm.YidunTaskProxyless.value, + ... ).captcha_handler() + { + "errorId":0, + "status":"ready", + "solution":{ + "token":"D19scz7n4VCU7b_...fRyEY-tXQ0cmS6laRKp_tZEyei_EUzc5M1IW0oxUHnZ4fBMH2a0jMPjOReiHVWBgkrcRYaOkQRasHlFejEToe7HZJy2jaGkxiB9b" + }, + "cost":"0.003", + "ip":"1.2.3.4", + "createTime":1692863536, + "endTime":1692863556, + "solveCount":1, + "taskId": 73243152973, + } + + >>> await YidunCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteURL="https://example.com/page-with-yidun", + ... websiteKey="0f743r3g1g...rz3grz0ym5", + ... method=YidunEnm.YidunTask.value, + ... proxyType="http", + ... proxyAddress="1.2.3.4", + ... proxyPort=8080, + ... ).aio_captcha_handler() + { + "errorId":0, + "status":"ready", + "solution":{ + "token":"D19scz7n4VCU7b_...fRyEY-tXQ0cmS6laRKp_tZEyei_EUzc5M1IW0oxUHnZ4fBMH2a0jMPjOReiHVWBgkrcRYaOkQRasHlFejEToe7HZJy2jaGkxiB9b" + }, + "cost":"0.003", + "ip":"1.2.3.4", + "createTime":1692863536, + "endTime":1692863556, + "solveCount":1, + "taskId": 73243152973, + } + + Returns: + Dict with full server response + + Notes: + https://2captcha.com/api-docs/yidun-necaptcha + + https://rucaptcha.com/api-docs/yidun-necaptcha + """ + super().__init__(method=method, *args, **kwargs) + + # Validate method + if method not in YidunEnm.list_values(): + raise ValueError(f"Invalid method parameter set, available - {YidunEnm.list_values()}") + + # Build task payload + task_data = { + "websiteURL": websiteURL, + "websiteKey": websiteKey, + } + + # Optional Enterprise-version and userAgent fields (only include if non-None) + if userAgent is not None: + task_data["userAgent"] = userAgent + if yidunGetLib is not None: + task_data["yidunGetLib"] = yidunGetLib + if yidunApiServerSubdomain is not None: + task_data["yidunApiServerSubdomain"] = yidunApiServerSubdomain + if challenge is not None: + task_data["challenge"] = challenge + if hcg is not None: + task_data["hcg"] = hcg + if hct is not None: + task_data["hct"] = hct + + # Add proxy params only for non-proxyless methods + if method == YidunEnm.YidunTask.value: + if not all([proxyType, proxyAddress, proxyPort]): + raise ValueError( + "Proxy parameters (proxyType, proxyAddress, proxyPort) are required for YidunTask" + ) + task_data.update( + { + "proxyType": proxyType, + "proxyAddress": proxyAddress, + "proxyPort": proxyPort, + } + ) + if proxyLogin is not None: + task_data["proxyLogin"] = proxyLogin + if proxyPassword is not None: + task_data["proxyPassword"] = proxyPassword + + self.create_task_payload["task"].update(task_data) + + def captcha_handler(self, **kwargs) -> dict: + """ + Sync solving method + + Args: + kwargs: Parameters for the `requests` library + + Returns: + Dict with full server response + + Notes: + Check class docstring for more info + """ + return self._processing_response(**kwargs) + + async def aio_captcha_handler(self) -> dict: + """ + Async solving method + + Returns: + Dict with full server response + + Notes: + Check class docstring for more info + """ + return await self._aio_processing_response() diff --git a/tests/test_yandex_smart_captcha.py b/tests/test_yandex_smart_captcha.py new file mode 100644 index 00000000..412014c3 --- /dev/null +++ b/tests/test_yandex_smart_captcha.py @@ -0,0 +1,236 @@ +import pytest + +from tests.conftest import BaseTest +from python_rucaptcha.core.enums import YandexSmartCaptchaEnm, CoordinatesCaptchaEnm, SaveFormatsEnm +from python_rucaptcha.core.serializer import GetTaskResultResponseSer +from python_rucaptcha.yandex_smart_captcha import YandexSmartCaptcha + + +class TestYandexSmartCaptcha(BaseTest): + websiteURL = "https://example.com/" + websiteKey = "Y5Lh0ti..." + userAgent = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" + ) + comment = "select objects in the order of the instruction" + captcha_file = "src/examples/088636.png" + instruction_file = "src/examples/bounding_box_start.png" + kwargs_params = { + "proxyType": "http", + "proxyAddress": "1.2.3.4", + "proxyPort": 8080, + } + + def test_methods_exists(self): + assert "captcha_handler" in YandexSmartCaptcha.__dict__.keys() + assert "aio_captcha_handler" in YandexSmartCaptcha.__dict__.keys() + + @pytest.mark.parametrize("method", YandexSmartCaptchaEnm.list_values() + [CoordinatesCaptchaEnm.CoordinatesTask.value]) + def test_args(self, method: str): + kwargs = {} + if method == YandexSmartCaptchaEnm.YandexSmartCaptchaTask.value: + kwargs = {"proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080} + elif method == CoordinatesCaptchaEnm.CoordinatesTask.value: + kwargs = {"imgType": "smart_captcha", "comment": self.comment} + + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=method, + **kwargs, + ) + assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY + assert instance.create_task_payload["task"]["type"] == method + + def test_kwargs(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask, + **self.kwargs_params, + ) + assert set(self.kwargs_params.keys()).issubset(set(instance.create_task_payload["task"].keys())) + assert set(self.kwargs_params.values()).issubset(set(instance.create_task_payload["task"].values())) + + def test_no_useragent(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless, + ) + assert "userAgent" not in instance.create_task_payload["task"] + + def test_proxy_params_in_payload(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask, + proxyType="http", + proxyAddress="1.2.3.4", + proxyPort=8080, + ) + assert instance.create_task_payload["task"]["proxyType"] == "http" + assert instance.create_task_payload["task"]["proxyAddress"] == "1.2.3.4" + assert instance.create_task_payload["task"]["proxyPort"] == 8080 + + def test_missing_proxy_for_proxy_method(self): + with pytest.raises(ValueError, match="proxyType|proxyAddress|proxyPort"): + YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask, + ) + + def test_missing_required_token_fields(self): + with pytest.raises(ValueError, match="websiteURL and websiteKey"): + YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless, + ) + + def test_wrong_method(self): + with pytest.raises(ValueError): + YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=self.get_random_string(5), + ) + + def test_smart_captcha_missing_comment(self): + with pytest.raises(ValueError, match="comment"): + YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + method=CoordinatesCaptchaEnm.CoordinatesTask, + imgType="smart_captcha", + ) + + def test_smart_captcha_with_instructions(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + method=CoordinatesCaptchaEnm.CoordinatesTask, + imgType="smart_captcha", + comment=self.comment, + ) + result = instance.captcha_handler( + captcha_file=self.captcha_file, + imgInstructions_file=self.instruction_file, + ) + assert isinstance(result, dict) is True + # The handler will attempt to call the API + # We just verify it constructs without exception and returns a dict + + def test_pazl_smart_captcha_minimal(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + method=CoordinatesCaptchaEnm.CoordinatesTask, + imgType="pazl_smart_captcha", + ) + assert instance.create_task_payload["task"]["imgType"] == "pazl_smart_captcha" + assert "comment" not in instance.create_task_payload["task"] + + """ + Success tests + """ + + def test_basic_data(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value, + ) + + result = instance.captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] in ("ready", "processing") + assert isinstance(result["taskId"], int) is True + else: + assert result["errorId"] in (1, 12) + assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE" + + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + async def test_aio_basic_data(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value, + ) + + result = await instance.aio_captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] in ("ready", "processing") + assert isinstance(result["taskId"], int) is True + else: + assert result["errorId"] in (1, 12) + assert result["errorCode"] in ("ERROR_CAPTCHA_UNSOLVABLE", YandexSmartCaptcha.NO_CAPTCHA_ERR) + + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + def test_context_basic_data(self): + with YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value, + ) as instance: + assert instance + + async def test_context_aio_basic_data(self): + async with YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value, + ) as instance: + assert instance + + def test_image_with_url(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + method=CoordinatesCaptchaEnm.CoordinatesTask, + imgType="smart_captcha", + comment=self.comment, + ) + result = instance.captcha_handler( + captcha_link=self.captcha_file, + imgInstructions_link=self.instruction_file, + ) + assert isinstance(result, dict) is True + + def test_no_captcha(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + method=CoordinatesCaptchaEnm.CoordinatesTask, + imgType="pazl_smart_captcha", + ) + result = instance.captcha_handler() + assert isinstance(result, dict) is True + assert result["errorId"] == 12 + assert isinstance(result["errorCode"], str) is True + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + async def test_aio_no_captcha(self): + instance = YandexSmartCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + method=CoordinatesCaptchaEnm.CoordinatesTask, + imgType="pazl_smart_captcha", + ) + result = await instance.aio_captcha_handler() + assert isinstance(result, dict) is True + assert result["errorId"] == 12 + assert isinstance(result["errorCode"], str) is True + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() diff --git a/tests/test_yidun_captcha.py b/tests/test_yidun_captcha.py new file mode 100644 index 00000000..6b367cc2 --- /dev/null +++ b/tests/test_yidun_captcha.py @@ -0,0 +1,192 @@ +import pytest + +from tests.conftest import BaseTest +from python_rucaptcha.core.enums import YidunEnm +from python_rucaptcha.yidun_captcha import YidunCaptcha +from python_rucaptcha.core.serializer import GetTaskResultResponseSer + + +class TestYidunCaptcha(BaseTest): + websiteURL = "https://example.com/page-with-yidun" + websiteKey = "0f743r3g1grz3grz0ym5" + userAgent = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" + ) + kwargs_params = { + "proxyType": "socks5", + "proxyAddress": BaseTest.proxyAddress, + "proxyPort": BaseTest.proxyPort, + } + + def test_methods_exists(self): + assert "captcha_handler" in YidunCaptcha.__dict__.keys() + assert "aio_captcha_handler" in YidunCaptcha.__dict__.keys() + + @pytest.mark.parametrize("method", YidunEnm.list_values()) + def test_args(self, method: str): + kwargs = {} + if method == YidunEnm.YidunTask.value: + kwargs = {"proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080} + instance = YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + userAgent=self.userAgent, + method=method, + **kwargs, + ) + assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY + assert instance.create_task_payload["task"]["type"] == method + assert instance.create_task_payload["task"]["websiteURL"] == self.websiteURL + assert instance.create_task_payload["task"]["websiteKey"] == self.websiteKey + assert instance.create_task_payload["task"]["userAgent"] == self.userAgent + + def test_kwargs(self): + instance = YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTask, + **self.kwargs_params, + ) + assert set(self.kwargs_params.keys()).issubset(set(instance.create_task_payload["task"].keys())) + assert set(self.kwargs_params.values()).issubset(set(instance.create_task_payload["task"].values())) + + def test_enterprise_params(self): + enterprise_params = { + "yidunGetLib": "https://example.com/yidun/load.min.js", + "yidunApiServerSubdomain": "c.dun.163.com", + "challenge": "0c59ba0d1b2349f9b2c1a2b3c4d5e6f7", + "hcg": "2c78a7731e2345f6a7b8c9d0e1f2a3b4", + "hct": 1779358333191, + } + instance = YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + **enterprise_params, + ) + for key, value in enterprise_params.items(): + assert instance.create_task_payload["task"][key] == value + + def test_no_useragent(self): + instance = YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTaskProxyless, + ) + assert "userAgent" not in instance.create_task_payload["task"] + + def test_proxy_params_in_payload(self): + instance = YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTask, + proxyType="http", + proxyAddress="1.2.3.4", + proxyPort=8080, + ) + assert instance.create_task_payload["task"]["proxyType"] == "http" + assert instance.create_task_payload["task"]["proxyAddress"] == "1.2.3.4" + assert instance.create_task_payload["task"]["proxyPort"] == 8080 + + def test_missing_proxy_for_proxy_method(self): + with pytest.raises(ValueError, match="proxyType|proxyAddress|proxyPort"): + YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTask, + ) + + def test_wrong_method(self): + with pytest.raises(ValueError): + YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=self.get_random_string(5), + ) + + """ + Success tests + """ + + def test_basic_data(self): + instance = YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTaskProxyless.value, + ) + + result = instance.captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] in ("ready", "processing") + assert isinstance(result["taskId"], int) is True + else: + assert result["errorId"] in (1, 12) + assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE" + + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + async def test_aio_basic_data(self): + instance = YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTaskProxyless.value, + ) + + result = await instance.aio_captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] in ("ready", "processing") + assert isinstance(result["taskId"], int) is True + else: + assert result["errorId"] in (1, 12) + assert result["errorCode"] in ("ERROR_CAPTCHA_UNSOLVABLE", YidunCaptcha.NO_CAPTCHA_ERR) + + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + def test_context_basic_data(self): + with YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTaskProxyless.value, + ) as instance: + assert instance + + async def test_context_aio_basic_data(self): + async with YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=YidunEnm.YidunTaskProxyless.value, + ) as instance: + assert instance + + """ + Fail tests + """ + + def test_no_websiteURL(self): + with pytest.raises(TypeError): + YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteKey=self.websiteKey, + ) + + def test_no_websiteKey(self): + with pytest.raises(TypeError): + YidunCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + )