- LookingWebMain: 强制 IPv4 避免 IPv6 不可达挂起;requests 加超时防卡死 - get_seed_from_element_detail: 修复限流页崩溃与 favicon 误作来源 URL,改用 canonical/传入 URL - QbittorrentDownloader: 修复 main() 中模块/类误用导致的 download_by_seed AttributeError;请求加超时 - MySqlService: 修复数据库不可达时 query/execute/close 的 None 崩溃,close 后置空 connection - 新增 cf_session: Cloudflare 人机认证 cookie 管理 + 限流退避重试 + 自动弹浏览器刷新验证 - 新增 refresh_cf_cookies.py: 弹出浏览器手动验证并保存 cf_clearance - tests/: 真实网络/数据库测试(get_page、seed_is_exist、cf_session)+ run_tests.py runner - 清理无用脚本(final_test2/verify_replace/REPLACEMENT_INFO),.idea 与 cookie 文件入 .gitignore 注: test_qbittorrent_downloader.py 11 个用例为既有失败(mock 目标 session 为实例属性,与实现错配),未在本次范围
111 lines
3.5 KiB
Python
111 lines
3.5 KiB
Python
"""
|
||
测试 cf_session:Cloudflare 限流/人机认证识别与自动重试逻辑
|
||
"""
|
||
import sys
|
||
import os
|
||
|
||
import pytest
|
||
from bs4 import BeautifulSoup
|
||
from loguru import logger
|
||
|
||
# 添加项目路径
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||
|
||
import cf_session
|
||
|
||
|
||
class TestRateLimitDetection:
|
||
"""限流/人机认证页识别(纯函数测试)"""
|
||
|
||
def setup_method(self):
|
||
logger.remove()
|
||
logger.add(sys.stderr, level="DEBUG")
|
||
|
||
def test_detects_429_rate_limit_page(self):
|
||
"""429 限流页(Too Many Requests)应被识别"""
|
||
soup = BeautifulSoup(
|
||
'<html><head><title>Too Many Requests</title></head>'
|
||
'<body><div class="cf-error-title">429</div></body></html>',
|
||
'html.parser')
|
||
assert cf_session.is_rate_limited(soup) is True
|
||
|
||
def test_detects_403_js_challenge_page(self):
|
||
"""403 JS 挑战页(Just a moment)应被识别"""
|
||
soup = BeautifulSoup(
|
||
'<html><head><title>Just a moment...</title></head>'
|
||
'<body><script src="/cdn-cgi/challenge-platform/scripts/jsd/main.js"></script></body></html>',
|
||
'html.parser')
|
||
assert cf_session.is_rate_limited(soup) is True
|
||
|
||
def test_detects_403_attention_required_page(self):
|
||
"""403 阻止页(Attention Required)应被识别"""
|
||
soup = BeautifulSoup(
|
||
'<html><head><title>Attention Required! | Cloudflare</title></head><body>x</body></html>',
|
||
'html.parser')
|
||
assert cf_session.is_rate_limited(soup) is True
|
||
|
||
def test_normal_page_not_rate_limited(self):
|
||
"""正常页面不应被误判"""
|
||
soup = BeautifulSoup(
|
||
'<html><body><div class="card-body">可下</div></body></html>',
|
||
'html.parser')
|
||
assert cf_session.is_rate_limited(soup) is False
|
||
|
||
def test_none_not_rate_limited(self):
|
||
"""None 页面不应被误判"""
|
||
assert cf_session.is_rate_limited(None) is False
|
||
|
||
|
||
class TestParseCookies:
|
||
"""agent-browser cookies 输出解析"""
|
||
|
||
def test_parse_name_value_lines(self):
|
||
stdout = ("cf_clearance=abc.def-123\n"
|
||
"bnState_x=%7B%22a%22%3A1%7D\n"
|
||
"# comment line\n")
|
||
cookies = cf_session._parse_cookies(stdout)
|
||
assert cookies == {
|
||
"cf_clearance": "abc.def-123",
|
||
"bnState_x": "%7B%22a%22%3A1%7D",
|
||
}
|
||
|
||
def test_parse_empty(self):
|
||
assert cf_session._parse_cookies("") == {}
|
||
|
||
|
||
class TestFetchWithRetry:
|
||
"""自动重试的真实请求验证"""
|
||
|
||
def setup_method(self):
|
||
logger.remove()
|
||
logger.add(sys.stderr, level="DEBUG")
|
||
|
||
def test_fetch_real_url_ok(self):
|
||
"""真实 URL 正常返回 (soup, 'ok')"""
|
||
url = "https://freejavbt.com/censored/filter?c=212&page=1"
|
||
soup, status = cf_session.fetch_with_retry(url, (10, 60), retries=2, retry_delay=2)
|
||
assert status == "ok"
|
||
assert soup is not None
|
||
assert len(soup.find_all("div", {"class": "card-body"})) > 0
|
||
|
||
def test_fetch_network_error(self):
|
||
"""不可达 URL 返回 (None, 'error')"""
|
||
soup, status = cf_session.fetch_with_retry(
|
||
"http://invalid-domain-for-test-12345.invalid", (5, 10), retries=2, retry_delay=1)
|
||
assert status == "error"
|
||
assert soup is None
|
||
|
||
|
||
def run_tests():
|
||
"""运行所有测试"""
|
||
pytest.main([
|
||
__file__,
|
||
"-v",
|
||
"--tb=short",
|
||
"--log-cli-level=INFO"
|
||
])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
run_tests()
|