mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 22:04:57 +08:00
Merge pull request #7260 from jxxghp/codex/issue-7258-confirm-redirect
fix(torrent): 修复首次下载确认后的重定向
This commit is contained in:
@@ -63,6 +63,21 @@ class TorrentResponsePort(Protocol):
|
||||
reason: str
|
||||
|
||||
|
||||
def _torrent_magnet_link(response: Optional[TorrentResponsePort], url: str) -> Optional[str]:
|
||||
"""
|
||||
从最终跳转地址或响应正文提取磁力链接,GET 和首次下载确认共用该判断。
|
||||
|
||||
:param response: 最终 HTTP 响应;直接跳到磁力链接时为空
|
||||
:param url: 最终下载地址或磁力链接
|
||||
:return: 磁力链接;响应仍需按种子或错误处理时返回 None
|
||||
"""
|
||||
if url.startswith("magnet:"):
|
||||
return url
|
||||
if response is not None and response.status_code == 200 and response.content.startswith(b"magnet:"):
|
||||
return response.text
|
||||
return None
|
||||
|
||||
|
||||
def torrent_rate_limit_error(response: Optional[TorrentResponsePort]) -> Optional[str]:
|
||||
"""
|
||||
在种子解析前提取 JSON 或 HTML 中明确的限流提示,保留站点原文。
|
||||
@@ -366,9 +381,14 @@ class TorrentHelper:
|
||||
proxies: Optional[dict[str, Any]],
|
||||
retry: bool,
|
||||
site: Mapping[str, Any],
|
||||
method: str = "GET",
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
) -> tuple[Optional[TorrentResponsePort], str, Optional[str]]:
|
||||
"""
|
||||
有界处理GET跳转,跨来源后永久移除原站点凭据和Referer。
|
||||
有界处理种子 GET 和首次确认 POST 的跳转,跨来源后永久移除凭据和 Referer。
|
||||
|
||||
POST 收到 301/302/303 后改用 GET;307/308 仅在同来源保留表单。
|
||||
确认 POST 不自动重试,后续 GET 沿用原下载地址的重试策略。
|
||||
|
||||
:param http_port: 已装配HTTP端口
|
||||
:param url: 当前下载地址
|
||||
@@ -378,19 +398,27 @@ class TorrentHelper:
|
||||
:param proxies: 本次请求代理
|
||||
:param retry: 是否对普通GET执行既有瞬时失败重试
|
||||
:param site: 原始资源所属站点的流控配置,跳转到其他主机后仍共用该额度
|
||||
:param method: 起始请求方法,只使用 GET 或 POST
|
||||
:param data: 首次确认表单;改用 GET 后永久移除
|
||||
:return: 最终响应、最终地址及允许在该地址使用的Cookie
|
||||
:raises ValueError: 跳转缺少地址、目标非法、重复或超过上限
|
||||
:raises ValueError: 跳转缺少地址、目标非法、重复、超过上限或要求跨来源提交表单
|
||||
"""
|
||||
seen = set()
|
||||
for _hop in range(9):
|
||||
if url in seen:
|
||||
if (method, url) in seen:
|
||||
raise ValueError("种子下载重定向出现环路")
|
||||
seen.add(url)
|
||||
response = self._request_torrent_get(
|
||||
http_port, retry and not torrent_rules.is_expiring_download_url(url), site,
|
||||
url=url, cookie=cookie, ua=ua, referer=referer,
|
||||
proxies=proxies, allow_redirects=False,
|
||||
)
|
||||
seen.add((method, url))
|
||||
if method == "POST":
|
||||
response = self._request_torrent(
|
||||
http_port, site, method=method, url=url, cookie=cookie, ua=ua,
|
||||
referer=referer, proxies=proxies, data=data, allow_redirects=False,
|
||||
)
|
||||
else:
|
||||
response = self._request_torrent_get(
|
||||
http_port, retry and not torrent_rules.is_expiring_download_url(url), site,
|
||||
url=url, cookie=cookie, ua=ua, referer=referer,
|
||||
proxies=proxies, allow_redirects=False,
|
||||
)
|
||||
if response is None or response.status_code not in (301, 302, 303, 307, 308):
|
||||
return response, url, cookie
|
||||
location = response.headers.get("Location") or response.headers.get("location")
|
||||
@@ -405,7 +433,11 @@ class TorrentHelper:
|
||||
target = urlsplit(destination)
|
||||
if target.scheme not in ("http", "https") or not target.hostname or target.username or target.password:
|
||||
raise ValueError("种子下载重定向目标无效")
|
||||
if response.status_code in (301, 302, 303):
|
||||
method, data = "GET", None
|
||||
if not UrlUtils.same_origin(url, destination):
|
||||
if method == "POST":
|
||||
raise ValueError("首次下载确认重定向不能向其他来源提交表单")
|
||||
cookie, referer = None, None
|
||||
url = destination
|
||||
raise ValueError("种子下载重定向次数超过上限")
|
||||
@@ -487,15 +519,13 @@ class TorrentHelper:
|
||||
)
|
||||
except (ValueError, RateLimitExceededException) as err:
|
||||
return cache_path, None, "", [], str(err)
|
||||
if url.startswith("magnet:"):
|
||||
return None, url, "", [], "获取到磁力链接"
|
||||
if magnet := _torrent_magnet_link(req, url):
|
||||
return cache_path if req is not None else None, magnet, "", [], "获取到磁力链接"
|
||||
if rate_limit_error := torrent_rate_limit_error(req):
|
||||
return cache_path, None, "", [], rate_limit_error
|
||||
if req is not None and req.status_code == 200:
|
||||
if not req.content:
|
||||
return cache_path, None, "", [], "未下载到种子数据"
|
||||
if req.content.startswith(b"magnet:"):
|
||||
return cache_path, req.text, "", [], "获取到磁力链接"
|
||||
# 优先按原始二进制解析,避免合法 torrent 被误判为首次下载 HTML。
|
||||
folder_name, file_list = _cache_valid_torrent_content(
|
||||
self.get_fileinfo_from_torrent_content, cache_backend, cache_path, req.content
|
||||
@@ -511,12 +541,14 @@ class TorrentHelper:
|
||||
return cache_path, None, "", [], "种子数据有误,请确认链接是否正确,如为PT站点则需手工在站点下载一次种子"
|
||||
action, data = confirmation
|
||||
try:
|
||||
req = self._request_torrent(
|
||||
http_port, site, method="POST", url=action, cookie=cookie, ua=ua,
|
||||
referer=url, proxies=config.proxy if proxy else None, data=data, allow_redirects=False,
|
||||
req, url, cookie = self._request_torrent_redirects(
|
||||
http_port, site=site, method="POST", url=action, cookie=cookie, ua=ua,
|
||||
referer=url, proxies=config.proxy if proxy else None, data=data, retry=retry,
|
||||
)
|
||||
except RateLimitExceededException as err:
|
||||
except (ValueError, RateLimitExceededException) as err:
|
||||
return cache_path, None, "", [], str(err)
|
||||
if magnet := _torrent_magnet_link(req, url):
|
||||
return cache_path if req is not None else None, magnet, "", [], "获取到磁力链接"
|
||||
if rate_limit_error := torrent_rate_limit_error(req):
|
||||
return cache_path, None, "", [], rate_limit_error
|
||||
if req is None:
|
||||
@@ -526,14 +558,13 @@ class TorrentHelper:
|
||||
if not req.content:
|
||||
return cache_path, None, "", [], "未下载到种子数据"
|
||||
logger.info("已提交站点首次种子下载确认,正在校验种子内容")
|
||||
# 首次下载提示页面完成后,重新按原始二进制解析 POST 响应。
|
||||
# 首次下载提示页面完成后,校验确认请求及其后续跳转取得的内容。
|
||||
assert req is not None
|
||||
if req.content:
|
||||
folder_name, file_list = _cache_valid_torrent_content(
|
||||
self.get_fileinfo_from_torrent_content, cache_backend, cache_path, req.content
|
||||
)
|
||||
if file_list:
|
||||
return cache_path, req.content, folder_name, file_list, ""
|
||||
folder_name, file_list = _cache_valid_torrent_content(
|
||||
self.get_fileinfo_from_torrent_content, cache_backend, cache_path, req.content
|
||||
)
|
||||
if file_list:
|
||||
return cache_path, req.content, folder_name, file_list, ""
|
||||
# 解析失败时终止,避免把 HTML 继续下沉到下载器或缓存。
|
||||
if cache_invalid:
|
||||
self.add_invalid(original_url)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"app/application/rss.py:RssHelper._parse_impl": 37,
|
||||
"app/application/security/cookie.py:CookieHelper._get_site_cookie_ua_impl": 45,
|
||||
"app/application/security/cookie.py:CookieHelper._get_site_cookie_ua_impl.__page_handler": 43,
|
||||
"app/application/torrent/download.py:TorrentHelper.download_torrent": 23,
|
||||
"app/application/torrent/download.py:TorrentHelper.download_torrent": 22,
|
||||
"app/application/torrent/download.py:TorrentHelper.match_torrent": 16,
|
||||
"app/application/transfer/models.py:TransferPlanCheckpoint.__post_init__": 18,
|
||||
"app/chain/download/existence.py:DownloadExistenceOwner.get_no_exists_info": 23,
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_search_ticket_and_content_share_interval_and_window(boundary):
|
||||
boundary.port.request.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("first", ["redirect", "retry", "confirm"])
|
||||
@pytest.mark.parametrize("first", ["redirect", "retry", "confirm", "confirm-redirect"])
|
||||
def test_each_actual_http_request_consumes_one_slot(boundary, first):
|
||||
"""
|
||||
跳转、普通重试及首次确认 POST 均需重新申请同一站点请求额度。
|
||||
@@ -119,9 +119,14 @@ def test_each_actual_http_request_consumes_one_slot(boundary, first):
|
||||
first_response = _response(status=302 if first == "redirect" else 503)
|
||||
if first == "redirect":
|
||||
first_response.headers["Location"] = "https://cdn.example/file.torrent"
|
||||
if first == "confirm":
|
||||
if first in ("confirm", "confirm-redirect"):
|
||||
first_response = _response('下载种子文件<form method="post"><input name="confirm" value="yes"></form>'.encode())
|
||||
responses = iter([first_response, _response()])
|
||||
response_sequence = [first_response, _response()]
|
||||
if first == "confirm-redirect":
|
||||
redirect = _response(status=302)
|
||||
redirect.headers["Location"] = "https://cdn.example/file.torrent"
|
||||
response_sequence.insert(1, redirect)
|
||||
responses = iter(response_sequence)
|
||||
starts = []
|
||||
|
||||
def request(**_kwargs):
|
||||
@@ -134,9 +139,12 @@ def test_each_actual_http_request_consumes_one_slot(boundary, first):
|
||||
boundary.port.request.side_effect = request
|
||||
|
||||
assert download.TorrentHelper().download_torrent(URL, site_id=42)[1] == CONTENT
|
||||
assert starts == [100, 110]
|
||||
assert starts == ([100, 110, 160] if first == "confirm-redirect" else [100, 110])
|
||||
methods = [call.kwargs["method"] for call in boundary.port.request.call_args_list]
|
||||
assert methods == ["GET", "POST" if first == "confirm" else "GET"]
|
||||
assert methods == {
|
||||
"redirect": ["GET", "GET"], "retry": ["GET", "GET"],
|
||||
"confirm": ["GET", "POST"], "confirm-redirect": ["GET", "POST", "GET"],
|
||||
}[first]
|
||||
|
||||
|
||||
def test_remote_cooldown_blocks_other_resources_and_search_until_expiry(boundary):
|
||||
|
||||
@@ -174,6 +174,249 @@ def test_first_download_confirmation_empty_response_is_transient(boundary):
|
||||
helper.add_invalid.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [301, 302, 303])
|
||||
@pytest.mark.parametrize("location", ["/file.torrent", "?id=39&confirmed=1", URL])
|
||||
def test_confirmation_redirect_switches_to_get_and_caches_original_url(boundary, status, location):
|
||||
"""
|
||||
首次确认后的跳转改用 GET,兼容相对地址及原地址并保留原始缓存键。
|
||||
"""
|
||||
helper, port, cache, sleep = boundary
|
||||
page = '下载种子文件<form action="?"><input name="confirm" value="yes"></form>'.encode()
|
||||
redirect = _response(status)
|
||||
redirect.headers["Location"] = location
|
||||
redirect.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), redirect, _response()]
|
||||
|
||||
result = helper.download_torrent(URL, cookie="session=test", ua="TestAgent")
|
||||
|
||||
assert result[1:5] == (CONTENT, "", ["Series.mkv"], "")
|
||||
calls = [call.kwargs for call in port.request.call_args_list]
|
||||
assert [call["method"] for call in calls] == ["GET", "POST", "GET"]
|
||||
assert calls[1]["data"] == {"confirm": "yes"}
|
||||
assert calls[2]["url"] == download.urljoin(URL, location)
|
||||
assert calls[2].get("data") is None
|
||||
assert [call["cookie"] for call in calls] == ["session=test"] * 3
|
||||
assert calls[2]["referer"] == URL
|
||||
assert all(call["ua"] == "TestAgent" and call["allow_redirects"] is False for call in calls)
|
||||
cache.set.assert_called_once_with(result[0].as_posix(), CONTENT, region="torrents")
|
||||
assert result[0] == Path(download.HashUtils.md5(URL)).with_suffix(".torrent")
|
||||
redirect.close.assert_called_once()
|
||||
helper.add_invalid.assert_not_called()
|
||||
sleep.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [307, 308])
|
||||
@pytest.mark.parametrize("final_status", [200, 503])
|
||||
def test_confirmation_redirect_preserves_post_without_retry(boundary, status, final_status):
|
||||
"""
|
||||
307/308 在同来源保留确认表单,目标瞬时失败也不能重试 POST。
|
||||
"""
|
||||
helper, port, cache, sleep = boundary
|
||||
page = '下载种子文件<form action="?"><input name="token" value="secret"></form>'.encode()
|
||||
redirect = _response(status)
|
||||
redirect.headers["Location"] = "/confirmed.php?id=39"
|
||||
redirect.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), redirect, _response(final_status)]
|
||||
|
||||
result = helper.download_torrent(URL, cookie="session=test")
|
||||
|
||||
assert result[1] == (CONTENT if final_status == 200 else None)
|
||||
assert result[4] == ("" if final_status == 200 else "下载种子出错,状态码:503")
|
||||
calls = [call.kwargs for call in port.request.call_args_list]
|
||||
assert [call["method"] for call in calls] == ["GET", "POST", "POST"]
|
||||
assert calls[2]["url"] == "https://tracker.example/confirmed.php?id=39"
|
||||
assert calls[1]["data"] == calls[2]["data"] == {"token": "secret"}
|
||||
assert calls[2]["cookie"] == "session=test"
|
||||
assert calls[2]["allow_redirects"] is False
|
||||
assert cache.set.call_count == (1 if final_status == 200 else 0)
|
||||
redirect.close.assert_called_once()
|
||||
helper.add_invalid.assert_not_called()
|
||||
sleep.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [307, 308])
|
||||
def test_confirmation_redirect_refuses_cross_origin_post(boundary, status):
|
||||
"""
|
||||
需要保留 POST 的跨来源跳转必须终止,不能外发 Cookie 或隐藏表单凭据。
|
||||
"""
|
||||
helper, port, cache, sleep = boundary
|
||||
page = '下载种子文件<form action="?"><input name="token" value="secret"></form>'.encode()
|
||||
redirect = _response(status)
|
||||
redirect.headers["Location"] = "https://other.example/file.torrent"
|
||||
redirect.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), redirect]
|
||||
|
||||
result = helper.download_torrent(URL, cookie="session=test")
|
||||
|
||||
assert result[1] is None
|
||||
assert result[4] == "首次下载确认重定向不能向其他来源提交表单"
|
||||
assert port.request.call_count == 2
|
||||
redirect.close.assert_called_once()
|
||||
cache.set.assert_not_called()
|
||||
helper.add_invalid.assert_not_called()
|
||||
sleep.assert_not_called()
|
||||
|
||||
|
||||
def test_confirmation_redirect_drops_credentials_permanently_after_cross_origin(boundary):
|
||||
"""
|
||||
确认后跨站改用 GET,再回原站也不恢复 Cookie、Referer 和表单字段。
|
||||
"""
|
||||
helper, port, _, _ = boundary
|
||||
page = '下载种子文件<form action="?"><input name="token" value="secret"></form>'.encode()
|
||||
redirects = [_response(302), _response(307)]
|
||||
redirects[0].headers["Location"] = "https://cdn.example/file.torrent"
|
||||
redirects[1].headers["Location"] = "https://tracker.example/final.torrent"
|
||||
for response in redirects:
|
||||
response.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), *redirects, _response()]
|
||||
|
||||
assert helper.download_torrent(URL, cookie="session=test", referer="https://tracker.example/private")[1] == CONTENT
|
||||
|
||||
calls = [call.kwargs for call in port.request.call_args_list]
|
||||
assert [call["method"] for call in calls] == ["GET", "POST", "GET", "GET"]
|
||||
assert [call["cookie"] for call in calls] == ["session=test", "session=test", None, None]
|
||||
assert [call["referer"] for call in calls] == ["https://tracker.example/private", URL, None, None]
|
||||
assert all(call.get("data") is None for call in calls[2:])
|
||||
for response in redirects:
|
||||
response.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("location, error", [
|
||||
(None, "种子下载重定向缺少目标地址"),
|
||||
("file:///tmp/private.torrent", "种子下载重定向目标无效"),
|
||||
("https://user:secret@other.example/file.torrent", "种子下载重定向目标无效"),
|
||||
])
|
||||
def test_confirmation_redirect_rejects_invalid_target(boundary, location, error):
|
||||
"""
|
||||
确认后缺失或非法跳转目标时返回明确错误,不能发起额外请求或缓存无效地址。
|
||||
"""
|
||||
helper, port, cache, _ = boundary
|
||||
page = '下载种子文件<form action="?"><input name="confirm" value="yes"></form>'.encode()
|
||||
redirect = _response(302)
|
||||
if location is not None:
|
||||
redirect.headers["Location"] = location
|
||||
redirect.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), redirect]
|
||||
|
||||
assert helper.download_torrent(URL)[4] == error
|
||||
assert port.request.call_count == 2
|
||||
redirect.close.assert_called_once()
|
||||
cache.set.assert_not_called()
|
||||
helper.add_invalid.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [302, 307])
|
||||
def test_confirmation_redirect_loop_is_bounded(boundary, status):
|
||||
"""
|
||||
按方法与地址识别确认跳转环路,允许 POST 转 GET,但不循环重发确认。
|
||||
"""
|
||||
helper, port, cache, _ = boundary
|
||||
page = '下载种子文件<form action="?"><input name="confirm" value="yes"></form>'.encode()
|
||||
redirect = _response(status)
|
||||
redirect.headers["Location"] = URL
|
||||
redirect.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), redirect, redirect]
|
||||
|
||||
assert helper.download_torrent(URL)[4] == "种子下载重定向出现环路"
|
||||
assert port.request.call_count == (3 if status == 302 else 2)
|
||||
assert [call.kwargs["method"] for call in port.request.call_args_list].count("POST") == 1
|
||||
cache.set.assert_not_called()
|
||||
helper.add_invalid.assert_not_called()
|
||||
|
||||
|
||||
def test_confirmation_redirect_hop_limit_is_shared_by_post_and_get(boundary):
|
||||
"""
|
||||
确认 POST 和后续 GET 共用有限跳转预算,不能无限追逐不同地址。
|
||||
"""
|
||||
helper, port, cache, _ = boundary
|
||||
page = '下载种子文件<form action="?"><input name="confirm" value="yes"></form>'.encode()
|
||||
redirects = [_response(302) for _ in range(9)]
|
||||
for index, response in enumerate(redirects):
|
||||
response.headers["Location"] = f"/file.torrent?hop={index}"
|
||||
response.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), *redirects]
|
||||
|
||||
assert helper.download_torrent(URL)[4] == "种子下载重定向次数超过上限"
|
||||
assert port.request.call_count == 10
|
||||
for response in redirects:
|
||||
response.close.assert_called_once()
|
||||
cache.set.assert_not_called()
|
||||
helper.add_invalid.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("in_body", [False, True])
|
||||
def test_confirmation_redirect_can_return_magnet(boundary, in_body):
|
||||
"""
|
||||
确认后跳转或落地正文给出磁力链接时直接返回,不缓存为损坏种子。
|
||||
"""
|
||||
helper, port, cache, _ = boundary
|
||||
page = '下载种子文件<form action="?"><input name="confirm" value="yes"></form>'.encode()
|
||||
magnet = "magnet:?xt=urn:btih:abc"
|
||||
redirect = _response(302)
|
||||
redirect.headers["Location"] = "/file.torrent" if in_body else magnet
|
||||
redirect.close = Mock()
|
||||
responses = [_response(content=page), redirect]
|
||||
if in_body:
|
||||
responses.append(_response(content=magnet.encode()))
|
||||
port.request.side_effect = responses
|
||||
|
||||
assert helper.download_torrent(URL)[1] == magnet
|
||||
assert port.request.call_count == (3 if in_body else 2)
|
||||
cache.set.assert_not_called()
|
||||
helper.add_invalid.assert_not_called()
|
||||
|
||||
|
||||
def test_confirmation_redirect_preserves_rate_limit(boundary):
|
||||
"""
|
||||
确认后的落地频控保留站点原因,不触发普通 GET 重试或失效缓存。
|
||||
"""
|
||||
helper, port, cache, sleep = boundary
|
||||
page = '下载种子文件<form action="?"><input name="confirm" value="yes"></form>'.encode()
|
||||
redirect = _response(302)
|
||||
redirect.headers["Location"] = "/file.torrent"
|
||||
redirect.close = Mock()
|
||||
message = "Request frequency limit, please try in 600 seconds."
|
||||
port.request.side_effect = [_response(content=page), redirect, _response(403, f"<html>{message}</html>".encode())]
|
||||
|
||||
assert helper.download_torrent(URL)[4] == f"站点限流:{message}"
|
||||
assert port.request.call_count == 3
|
||||
cache.set.assert_not_called()
|
||||
helper.add_invalid.assert_not_called()
|
||||
sleep.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("link_type, attempts", [("ordinary", 3), ("signed", 1), ("indirect", 1), ("signed-target", 1)])
|
||||
def test_confirmation_redirect_get_keeps_original_retry_policy(boundary, link_type, attempts):
|
||||
"""
|
||||
确认后的普通 GET 可退避,签名和换票链路仍只尝试一次且从不重放确认。
|
||||
"""
|
||||
helper, port, cache, sleep = boundary
|
||||
page = '下载种子文件<form action="?"><input name="confirm" value="yes"></form>'.encode()
|
||||
url = URL + "&t=1&sign=old" if link_type == "signed" else URL
|
||||
redirect = _response(302)
|
||||
redirect.headers["Location"] = "/file.torrent?t=1&sign=new" if link_type == "signed-target" else "/file.torrent"
|
||||
redirect.close = Mock()
|
||||
port.request.side_effect = [_response(content=page), redirect, *[_response(503) for _ in range(attempts)]]
|
||||
|
||||
assert helper.download_torrent(url, cache_invalid=link_type != "indirect")[4] == "下载种子出错,状态码:503"
|
||||
assert [call.kwargs["method"] for call in port.request.call_args_list] == ["GET", "POST", *["GET"] * attempts]
|
||||
assert sleep.call_count == attempts - 1
|
||||
cache.set.assert_not_called()
|
||||
helper.add_invalid.assert_not_called()
|
||||
|
||||
|
||||
def test_magnet_body_on_error_does_not_bypass_status_check(boundary):
|
||||
"""
|
||||
非成功响应即使正文看似磁力链接也必须保留 HTTP 错误。
|
||||
"""
|
||||
helper, port, cache, _ = boundary
|
||||
port.request.return_value = _response(401, b"magnet:?xt=urn:btih:abc")
|
||||
|
||||
assert helper.download_torrent(URL)[4] == "下载种子出错,状态码:401"
|
||||
port.request.assert_called_once()
|
||||
cache.set.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("form", [
|
||||
'<form action="?"><input value="yes" name="confirm"></form>',
|
||||
"<form method='POST' action='?'><input value='a&b' name='token'></form>",
|
||||
|
||||
Reference in New Issue
Block a user