Files
flow2api/tests/test_recaptcha_runtime.py
TheSmallHanCat 5644df2481 fix(flow): 适配新版图片接口并统一验证码与协议提交链路
- 更新 BELUGA、HARBOR_SEAL 模型映射,移除失效模型
  - 修正图片 RPC 参数布局及 XSRF 认证,明确提交模型、比例和参考图
  - 浏览器仅负责按对应 action 取码,取消本地打码对第三方密钥的强制依赖
  - 修复 Cookie 导入与作用域、空账号预热异常,防止重复生成并完善资源释放
  - 补充回归测试:171 项通过,真实浏览器打码与 BELUGA 图片生成验证成功
2026-10-12 16:15:50 +08:00

120 lines
4.8 KiB
Python

import json
import shutil
import subprocess
import types
import unittest
from unittest.mock import AsyncMock
from src.services.browser_captcha import TokenBrowser
from src.services.recaptcha_runtime import (
TRUSTED_RECAPTCHA_HOOK_SOURCE,
build_recaptcha_execute_script,
)
class LocalCaptchaActionTests(unittest.IsolatedAsyncioTestCase):
async def test_protocol_actions_execute_without_native_generation(self):
for action in ("IMAGE_GENERATION", "VIDEO_GENERATION", "UPLOAD_IMAGE"):
with self.subTest(action=action):
browser = object.__new__(TokenBrowser)
browser.token_id = 7
browser._apply_browser_environment_patch = AsyncMock()
browser._prepare_flow_runtime_page = AsyncMock(return_value=True)
browser._harvest_token_via_ui_submit = AsyncMock()
page = types.SimpleNamespace(
add_init_script=AsyncMock(),
evaluate=AsyncMock(return_value={"ok": True, "token": "captcha"}),
close=AsyncMock(),
)
context = types.SimpleNamespace(new_page=AsyncMock(return_value=page))
token = await browser._execute_captcha(
context, "project", "site-key", action
)
self.assertEqual(token, "captcha")
self.assertIn(json.dumps(action), page.evaluate.call_args.args[0])
browser._harvest_token_via_ui_submit.assert_not_awaited()
self.assertIsNone(browser._last_harvest_session_id)
page.close.assert_awaited_once()
async def test_execution_failure_closes_page_without_chat_fallback(self):
browser = object.__new__(TokenBrowser)
browser.token_id = 7
browser._apply_browser_environment_patch = AsyncMock()
browser._prepare_flow_runtime_page = AsyncMock(return_value=True)
browser._harvest_token_via_ui_submit = AsyncMock()
page = types.SimpleNamespace(
add_init_script=AsyncMock(),
evaluate=AsyncMock(side_effect=RuntimeError("execution failed")),
close=AsyncMock(),
)
token = await browser._execute_captcha(
types.SimpleNamespace(new_page=AsyncMock(return_value=page)),
"project",
"site-key",
"IMAGE_GENERATION",
)
self.assertIsNone(token)
browser._harvest_token_via_ui_submit.assert_not_awaited()
page.close.assert_awaited_once()
@unittest.skipUnless(
shutil.which("node"), "Node.js is required for the JavaScript contract"
)
def test_shared_execute_script_preserves_requested_action(self):
source = r"""
const vm = require('vm');
const input = JSON.parse(require('fs').readFileSync(0, 'utf8'));
(async () => {
const sandbox = { setTimeout, clearTimeout, location: { href: 'https://flow.google.com/project/example' } };
sandbox.window = sandbox;
vm.createContext(sandbox);
vm.runInContext(input.hook, sandbox);
vm.runInContext(`
globalThis.calls = [];
globalThis.grecaptcha = { enterprise: {
execute: function(key, options) { calls.push({ key, action: options.action }); return Promise.resolve('captcha'); }
}};
const original = grecaptcha.enterprise.execute.bind(grecaptcha.enterprise);
grecaptcha.enterprise.execute = function(key, options) {
return original(key, Object.assign({}, options, { action: 'extension_hijack_detected' }));
};
`, sandbox);
for (const script of input.scripts) {
const result = await vm.runInContext(script, sandbox);
if (!result.ok || result.token !== 'captcha') throw Error('captcha execution failed');
}
const actual = sandbox.calls.map(call => call.action);
if (JSON.stringify(actual) !== JSON.stringify(input.actions)) throw Error('action mismatch');
if (sandbox.calls.some(call => call.key !== input.siteKey)) throw Error('site key mismatch');
})().catch(error => { process.stderr.write(error.message); process.exitCode = 1; });
"""
actions = [
"IMAGE_GENERATION",
"VIDEO_GENERATION",
"UPLOAD_IMAGE",
"CHAT_GENERATION",
]
site_key = 'site-key"\\value'
result = subprocess.run(
[shutil.which("node"), "-e", source],
input=json.dumps(
{
"hook": TRUSTED_RECAPTCHA_HOOK_SOURCE,
"scripts": [
build_recaptcha_execute_script(site_key, action, 1000)
for action in actions
],
"actions": actions,
"siteKey": site_key,
}
),
text=True,
capture_output=True,
timeout=15,
)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()