feature: действие captcha value=image — капча-картинка через 2captcha (#109)
Снимок элемента картинки на текущей странице (без повторной загрузки src) уходит в 2captcha как обычная капча (method=base64), ответ набирается в поле target как у typing и возвращается в result действия. Необязательные подсказки: min_len, max_len, numeric, lang, case_sensitive. Клиент: Action.captcha_image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
502b6e1003
commit
5ed2cfbb3d
15 files changed
+301
-9
No files matched your search
@@ -1,4 +1,6 @@
|
||||
"""api.actions.execute на поддельной странице: порядок, остановка на ошибке, антибот, длительность."""
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
|
||||
from antibot.orchestrator import ChallengeOutcome
|
||||
@@ -113,3 +115,93 @@ async def test_pass_challenge_action(challenge):
|
||||
result = await actions.execute(FakePage(), acts({"type": "pass_challenge"}), antibot_name="cloudflare")
|
||||
assert result[0]["ok"] and result[0]["result"] == "passed"
|
||||
assert challenge["calls"] == 2
|
||||
|
||||
|
||||
class FakeImage:
|
||||
"""Locator картинки капчи: скриншот элемента — заранее заданные байты."""
|
||||
|
||||
def __init__(self, png):
|
||||
self.png = png
|
||||
self.waited = False
|
||||
|
||||
@property
|
||||
def first(self):
|
||||
return self
|
||||
|
||||
async def wait_for(self):
|
||||
self.waited = True
|
||||
|
||||
async def evaluate(self, script):
|
||||
return True
|
||||
|
||||
async def screenshot(self, type):
|
||||
return self.png
|
||||
|
||||
|
||||
class ImagePage(FakePage):
|
||||
def __init__(self, image):
|
||||
super().__init__()
|
||||
self.image = image
|
||||
self.locators = []
|
||||
|
||||
def locator(self, selector):
|
||||
self.locators.append(selector)
|
||||
return self.image
|
||||
|
||||
|
||||
async def test_captcha_image(challenge, monkeypatch):
|
||||
"""Снимок элемента уходит в 2captcha base64 с подсказками, ответ набирается в target и попадает в result."""
|
||||
from captcha import image as image_mod
|
||||
from twocaptcha import TwoCaptcha
|
||||
|
||||
sent, typed = {}, {}
|
||||
|
||||
def fake_solve(self, **kwargs):
|
||||
sent.update(kwargs)
|
||||
return {"captchaId": "1", "code": "x7Kp2"}
|
||||
|
||||
async def fake_typing(page, locator, text):
|
||||
typed[locator] = text
|
||||
|
||||
monkeypatch.setattr(TwoCaptcha, "solve", fake_solve)
|
||||
monkeypatch.setattr(image_mod, "human_typing", fake_typing)
|
||||
|
||||
page = ImagePage(FakeImage(b"\x89PNG-captcha"))
|
||||
result = await actions.execute(page, acts(
|
||||
{"type": "captcha", "selector": "#popup-signup img.captcha", "value": "image",
|
||||
"target": "#popup-signup input[name=\"captcha\"]", "min_len": 5, "numeric": 4, "case_sensitive": True},
|
||||
), captcha_key="KEY")
|
||||
|
||||
assert [(r["ok"], r.get("result")) for r in result] == [(True, "x7Kp2")]
|
||||
assert page.locators == ["#popup-signup img.captcha"]
|
||||
assert page.image.waited
|
||||
assert sent == {
|
||||
"method": "base64", "body": base64.b64encode(b"\x89PNG-captcha").decode(),
|
||||
"minLen": 5, "numeric": 4, "caseSensitive": 1,
|
||||
}
|
||||
assert typed == {"#popup-signup input[name=\"captcha\"]": "x7Kp2"}
|
||||
|
||||
|
||||
async def test_captcha_image_unsolved(challenge, monkeypatch):
|
||||
"""2captcha не решила — действие падает, в target ничего не вводится."""
|
||||
from captcha import image as image_mod
|
||||
from twocaptcha import TwoCaptcha
|
||||
from twocaptcha.solver import ApiException
|
||||
|
||||
def fake_solve(self, **kwargs):
|
||||
raise ApiException("ERROR_CAPTCHA_UNSOLVABLE")
|
||||
|
||||
typed = []
|
||||
|
||||
async def fake_typing(page, locator, text):
|
||||
typed.append(text)
|
||||
|
||||
monkeypatch.setattr(TwoCaptcha, "solve", fake_solve)
|
||||
monkeypatch.setattr(image_mod, "human_typing", fake_typing)
|
||||
|
||||
result = await actions.execute(ImagePage(FakeImage(b"png")), acts(
|
||||
{"type": "captcha", "selector": "img", "value": "image", "target": "#in"},
|
||||
), captcha_key="KEY")
|
||||
|
||||
assert [(r["ok"], r["result"]) for r in result] == [(False, "ERROR_CAPTCHA_UNSOLVABLE")]
|
||||
assert typed == []
|
||||
@@ -38,6 +38,9 @@ def test_action_builders():
|
||||
assert Action.pass_challenge().to_dict() == {"type": "pass_challenge"}
|
||||
with pytest.raises(ValueError):
|
||||
Action.captcha("#c", "funcaptcha")
|
||||
assert Action.captcha_image("img", "#in", numeric=1, case_sensitive=False).to_dict() == {
|
||||
"type": "captcha", "selector": "img", "value": "image", "target": "#in", "numeric": 1, "case_sensitive": False,
|
||||
}
|
||||
|
||||
|
||||
def test_settings_from_env(monkeypatch):
|
||||
|
||||
@@ -21,6 +21,13 @@ def solve(**kwargs) -> SolveRequest:
|
||||
({"type": "wait_for", "selector": "a", "timeout": 0}, "between 1 and 7200"),
|
||||
({"type": "wait_for", "selector": "a", "timeout": 7201}, "between 1 and 7200"),
|
||||
({"type": "captcha", "selector": "#c", "value": "datadome"}, "captcha value must be one of"),
|
||||
({"type": "captcha", "selector": "img", "value": "image"}, "requires: target"),
|
||||
({"type": "captcha", "selector": "#c", "value": "hcaptcha", "target": "#in"}, "only for action 'captcha'"),
|
||||
({"type": "typing", "selector": "#q", "value": "x", "numeric": 1}, "only for action 'captcha'"),
|
||||
({"type": "captcha", "selector": "img", "value": "image", "target": "#in", "numeric": 5}, "numeric"),
|
||||
({"type": "captcha", "selector": "img", "value": "image", "target": "#in", "max_len": 21}, "max_len"),
|
||||
({"type": "captcha", "selector": "img", "value": "image", "target": "#in", "min_len": 6, "max_len": 4},
|
||||
"min_len must not exceed max_len"),
|
||||
])
|
||||
def test_invalid_action(action, error):
|
||||
with pytest.raises(ValidationError, match=error):
|
||||
@@ -35,6 +42,9 @@ def test_invalid_action(action, error):
|
||||
{"type": "delay", "value": "0"},
|
||||
{"type": "pass_challenge"},
|
||||
{"type": "captcha", "selector": "#c", "value": "hcaptcha"},
|
||||
{"type": "captcha", "selector": "img", "value": "image", "target": "#in"},
|
||||
{"type": "captcha", "selector": "img", "value": "image", "target": "#in",
|
||||
"min_len": 4, "max_len": 6, "numeric": 1, "lang": "en", "case_sensitive": True},
|
||||
{"type": "inner_html", "selector": "h1"},
|
||||
])
|
||||
def test_valid_action(action):
|
||||
|
||||
Reference in new issue
Block a user