fix: 修复主程序卡死与下载器集成,新增真实数据测试
- 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 为实例属性,与实现错配),未在本次范围
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# 测试说明
|
||||
|
||||
## 测试文件
|
||||
|
||||
### MySQLDatabase 测试
|
||||
- `test_database.py` - MySQLDatabase 类测试
|
||||
- 测试数据库配置
|
||||
- 测试创建连接对象
|
||||
- 测试连接对象属性和方法
|
||||
- 测试关闭连接
|
||||
|
||||
### seed_is_exist 测试
|
||||
- `test_seed_is_exist.py` - seed_is_exist 功能真实数据库测试
|
||||
- 首次运行自动创建 `LookingWeb` 库和 `seeds_info` 表(幂等)
|
||||
- 测试种子不存在 → False
|
||||
- 真实插入种子后 → True
|
||||
- 多个种子均识别为存在
|
||||
- 删除后再查询 → False
|
||||
- 数据库不可达 → False(不抛异常)
|
||||
- 测试数据测后自动清理
|
||||
|
||||
### qBittorrent 下载器测试
|
||||
- `test_qbittorrent_downloader.py` - qBittorrent 下载器单元测试
|
||||
- 测试初始化(带/不带认证)
|
||||
- 测试添加下载任务(成功/失败/异常)
|
||||
- 测试种子信息下载
|
||||
- 测试带选项的下载(保存路径、分类、标签)
|
||||
- 测试连接测试
|
||||
- 测试获取下载列表
|
||||
- 注意:当前 11/12 用例因测试 mock 目标与实现不一致(`QbittorrentDownloader.session` 是实例属性而非模块属性)而失败,属既有问题,待修复。
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 安装 pytest
|
||||
uv pip install pytest
|
||||
|
||||
# 运行所有测试
|
||||
uv run python -m pytest tests/ -v
|
||||
|
||||
# 运行 MySQLDatabase 测试
|
||||
uv run python -m pytest tests/test_database.py -v
|
||||
|
||||
# 运行 qBittorrent 测试
|
||||
uv run python -m pytest tests/test_qbittorrent_downloader.py -v
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Python 3.7+
|
||||
- requests
|
||||
- loguru
|
||||
- beautifulsoup4
|
||||
- pymysql
|
||||
- pytest
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
- MySQLDatabase: 4 个测试用例
|
||||
- seed_is_exist: 5 个测试用例(真实数据库)
|
||||
- qBittorrentDownloader: 12 个测试用例(11 个待修复)
|
||||
- 总计: 25 个测试用例
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
运行 tests/ 目录下全部测试
|
||||
|
||||
运行命令:
|
||||
uv run python tests/run_tests.py
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
"""运行测试"""
|
||||
# 本脚本位于 tests/ 目录,直接运行该目录下所有测试
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
tests_dir = script_dir
|
||||
os.chdir(script_dir)
|
||||
|
||||
# 配置 pytest
|
||||
exit_code = pytest.main([
|
||||
str(tests_dir),
|
||||
"-v",
|
||||
"--tb=short",
|
||||
"--log-cli-level=INFO",
|
||||
"-W",
|
||||
"ignore::DeprecationWarning"
|
||||
])
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
测试 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()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
测试 MySQLDatabase 类
|
||||
"""
|
||||
import pytest
|
||||
import pymysql
|
||||
from loguru import logger
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from dataService.MySqlService import MySQLDatabase, DatabaseConfig
|
||||
|
||||
|
||||
class TestMySQLDatabase:
|
||||
"""MySQLDatabase 类测试"""
|
||||
|
||||
def setup_method(self):
|
||||
"""每个测试方法前的设置"""
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG")
|
||||
|
||||
def test_database_config(self):
|
||||
"""测试数据库配置"""
|
||||
assert DatabaseConfig.HOST == "192.168.123.199"
|
||||
assert DatabaseConfig.PORT == 3306
|
||||
assert DatabaseConfig.USER == "root"
|
||||
assert DatabaseConfig.PASSWORD == "GUOxy5157"
|
||||
assert DatabaseConfig.DB == "LookingWeb"
|
||||
logger.info("数据库配置正确")
|
||||
|
||||
def test_create_connection_exists(self):
|
||||
"""测试创建连接对象"""
|
||||
connection = MySQLDatabase.create_connection()
|
||||
assert connection is not None
|
||||
logger.info("数据库连接对象创建成功")
|
||||
connection.close()
|
||||
|
||||
def test_connection_has_attributes(self):
|
||||
"""测试连接对象有必需的属性"""
|
||||
connection = MySQLDatabase.create_connection()
|
||||
assert hasattr(connection, 'host')
|
||||
assert hasattr(connection, 'port')
|
||||
assert hasattr(connection, 'user')
|
||||
assert hasattr(connection, 'password')
|
||||
assert hasattr(connection, 'db')
|
||||
assert hasattr(connection, 'connection')
|
||||
assert hasattr(connection, 'connect')
|
||||
assert hasattr(connection, 'query')
|
||||
assert hasattr(connection, 'execute')
|
||||
assert hasattr(connection, 'close')
|
||||
logger.info("连接对象具有所有必需的属性和方法")
|
||||
connection.close()
|
||||
|
||||
def test_connection_close(self):
|
||||
"""测试关闭连接"""
|
||||
connection = MySQLDatabase.create_connection()
|
||||
connection.close()
|
||||
assert connection.connection is None
|
||||
logger.info("关闭连接成功")
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""运行所有测试"""
|
||||
pytest.main([
|
||||
__file__,
|
||||
"-v",
|
||||
"--tb=short",
|
||||
"--log-cli-level=INFO"
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_tests()
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
使用主程序调用时会使用的真实 URL 测试 get_page 功能
|
||||
|
||||
测试说明:
|
||||
- 调用真实的 get_page 函数(不 mock requests)
|
||||
- 使用主程序 main() 中实际使用的 URL 获取真实页面数据
|
||||
- 当前仅覆盖 get_page 功能
|
||||
|
||||
注意事项:
|
||||
- 依赖真实网络,站点(freejavbt.com)响应可能较慢或被 Cloudflare 限流(429),
|
||||
测试中对限流做了重试与容忍处理。
|
||||
- freejavbt.com 同时解析出 IPv4 和 IPv6 地址,而部分环境 IPv6 不可达会导致
|
||||
requests 长时间挂起,因此在导入 requests 之前强制走 IPv4。
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import socket
|
||||
|
||||
socket.has_ipv6 = False
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
from loguru import logger
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from LookingWebMain import get_page
|
||||
|
||||
# 主程序 main() 中实际使用的 URL 模板
|
||||
# (LookingWebMain.main: for page in range(5): url = f"https://freejavbt.com/censored/filter?c=212&page={page+1}")
|
||||
MAIN_PAGE_URL_TEMPLATE = "https://freejavbt.com/censored/filter?c=212&page={page}"
|
||||
|
||||
# get_page 不检查 HTTP 状态码,Cloudflare 限流时也会返回解析后的
|
||||
# "Too Many Requests" 页面,这里通过页面文本识别限流。
|
||||
RATE_LIMIT_MARK = "Too Many Requests"
|
||||
|
||||
|
||||
def is_rate_limited(page_content):
|
||||
"""判断页面是否为 Cloudflare 限流响应(真实数据的一部分)"""
|
||||
return page_content is not None and RATE_LIMIT_MARK in page_content.text
|
||||
|
||||
|
||||
def get_page_with_retry(url, retries=3, sleep_seconds=5):
|
||||
"""
|
||||
调用真实的 get_page 获取页面,遇到限流时短暂等待后重试。
|
||||
返回 (结果, 是否限流过)。失败时结果可能为 None。
|
||||
"""
|
||||
result = None
|
||||
for attempt in range(1, retries + 1):
|
||||
result = get_page(url)
|
||||
if not is_rate_limited(result):
|
||||
return result, attempt > 1
|
||||
logger.warning("页面被限流,等待 {} 秒后重试 (第 {}/{} 次): {}", sleep_seconds, attempt, retries, url)
|
||||
if attempt < retries:
|
||||
time.sleep(sleep_seconds)
|
||||
return result, True
|
||||
|
||||
|
||||
class TestGetPage:
|
||||
"""使用主程序真实 URL 和真实数据测试 get_page 功能"""
|
||||
|
||||
def setup_method(self):
|
||||
"""每个测试方法前的设置"""
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG")
|
||||
|
||||
def test_get_page_success_with_main_url(self):
|
||||
"""使用主程序第 1 页的 URL 真实请求并解析页面"""
|
||||
url = MAIN_PAGE_URL_TEMPLATE.format(page=1)
|
||||
logger.info("使用主程序 URL 获取真实页面: {}", url)
|
||||
|
||||
result, _ = get_page_with_retry(url)
|
||||
|
||||
assert result is not None, f"真实请求失败: {url}"
|
||||
assert isinstance(result, BeautifulSoup)
|
||||
logger.info("真实页面获取成功,标题: {}", result.title.string if result.title else "无标题")
|
||||
|
||||
def test_get_page_returns_real_content(self):
|
||||
"""真实页面数据应包含主程序依赖的卡片结构(card-body)"""
|
||||
url = MAIN_PAGE_URL_TEMPLATE.format(page=1)
|
||||
result, _ = get_page_with_retry(url)
|
||||
assert result is not None, f"真实请求失败: {url}"
|
||||
assert not is_rate_limited(result), f"页面被限流,无法验证真实内容: {url}"
|
||||
|
||||
# 主程序 get_target_element_list 依赖的结构
|
||||
cards = result.find_all("div", {"class": "card-body"})
|
||||
assert len(cards) > 0, "真实页面应包含 card-body 元素"
|
||||
|
||||
tags = result.find_all("div", {"class": "video-list-item-tag-wrapper"})
|
||||
assert len(tags) > 0, "真实页面应包含 video-list-item-tag-wrapper 元素"
|
||||
|
||||
logger.info("真实页面包含 {} 个卡片、{} 个标签", len(cards), len(tags))
|
||||
|
||||
def test_get_page_all_pages_used_by_main(self):
|
||||
"""遍历主程序 main() 使用的全部 5 个页面 URL"""
|
||||
for page_no in range(1, 6):
|
||||
url = MAIN_PAGE_URL_TEMPLATE.format(page=page_no)
|
||||
result, _ = get_page_with_retry(url)
|
||||
|
||||
# get_page 的真实契约:请求成功并解析出页面(失败时返回 None)
|
||||
assert result is not None, f"页面获取失败: {url}"
|
||||
assert isinstance(result, BeautifulSoup)
|
||||
|
||||
# 正常页面应包含卡片;限流页面(429)是真实的服务器行为,予以容忍
|
||||
cards = result.find_all("div", {"class": "card-body"})
|
||||
assert len(cards) > 0 or is_rate_limited(result), \
|
||||
f"页面 {url} 既无卡片内容也不是限流响应"
|
||||
logger.info("页面 {} 获取成功,包含 {} 个卡片", url, len(cards))
|
||||
|
||||
def test_get_page_failed_request_returns_none(self):
|
||||
"""对不可达的 URL 调用真实函数,应走真实网络错误路径并返回 None"""
|
||||
url = "http://invalid-domain-for-test-12345.invalid"
|
||||
result = get_page(url)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""运行所有测试"""
|
||||
pytest.main([
|
||||
__file__,
|
||||
"-v",
|
||||
"--tb=short",
|
||||
"--log-cli-level=INFO"
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_tests()
|
||||
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
测试 qBittorrent 下载器
|
||||
"""
|
||||
import pytest
|
||||
import requests
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from loguru import logger
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from ariaDownloaderService.QbittorrentDownloader import QbittorrentDownloader
|
||||
from dataService.dto.SeedInfo import SeedInfo
|
||||
|
||||
|
||||
class TestQbittorrentDownloader:
|
||||
"""qBittorrent 下载器测试类"""
|
||||
|
||||
def setup_method(self):
|
||||
"""每个测试方法前的设置"""
|
||||
# 配置 loguru 输出到控制台
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG")
|
||||
|
||||
def test_initialization_without_auth(self):
|
||||
"""测试不使用认证的初始化"""
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
|
||||
assert downloader.host == "192.168.123.199"
|
||||
assert downloader.api_port == 8085
|
||||
assert downloader.api_base_url == "http://192.168.123.199:8085/api/v2"
|
||||
assert downloader.username == ""
|
||||
assert downloader.password == ""
|
||||
logger.info("不使用认证的初始化测试通过")
|
||||
|
||||
def test_initialization_with_auth(self):
|
||||
"""测试使用认证的初始化"""
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085,
|
||||
username="admin",
|
||||
password="GUOxy.5157"
|
||||
)
|
||||
|
||||
assert downloader.host == "192.168.123.199"
|
||||
assert downloader.api_port == 8085
|
||||
assert downloader.username == "admin"
|
||||
assert downloader.password == "GUOxy.5157"
|
||||
assert downloader.api_base_url == "http://192.168.123.199:8085/api/v2"
|
||||
logger.info("使用认证的初始化测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.post')
|
||||
def test_download_by_magnet_success(self, mock_post):
|
||||
"""测试通过 magnet 链接成功添加下载任务"""
|
||||
# 模拟成功的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = ""
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
magnet_url = "magnet:?xt=urn:btih:1234567890abcdef"
|
||||
result = downloader.download_by_magnet(magnet_url)
|
||||
|
||||
assert result == "1"
|
||||
mock_post.assert_called_once()
|
||||
|
||||
# 验证请求参数
|
||||
call_args = mock_post.call_args
|
||||
assert call_args[0][0] == "http://192.168.123.199:8085/api/v2/torrents/add"
|
||||
assert call_args[1]['data']['urls'] == magnet_url
|
||||
logger.info("通过 magnet 链接成功添加下载任务测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.post')
|
||||
def test_download_by_magnet_failure_status(self, mock_post):
|
||||
"""测试请求返回非200状态码的情况"""
|
||||
# 模拟失败的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.text = "Internal Server Error"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
magnet_url = "magnet:?xt=urn:btih:1234567890abcdef"
|
||||
result = downloader.download_by_magnet(magnet_url)
|
||||
|
||||
assert result == "-1"
|
||||
logger.info("请求返回非200状态码测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.post')
|
||||
def test_download_by_magnet_exception(self, mock_post):
|
||||
"""测试请求过程中发生异常的情况"""
|
||||
# 模拟异常
|
||||
mock_post.side_effect = requests.exceptions.ConnectionError("Connection refused")
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
magnet_url = "magnet:?xt=urn:btih:1234567890abcdef"
|
||||
result = downloader.download_by_magnet(magnet_url)
|
||||
|
||||
assert result == "0"
|
||||
logger.info("请求过程中发生异常测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.post')
|
||||
def test_download_by_seed_success(self, mock_post):
|
||||
"""测试通过种子信息成功添加下载任务"""
|
||||
# 模拟成功的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = ""
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# 创建种子信息对象
|
||||
seed_info = SeedInfo(name="测试种子", seedUrl="magnet:?xt=urn:btih:1234567890abcdef")
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
result = downloader.download_by_seed(seed_info)
|
||||
|
||||
assert result == "1"
|
||||
mock_post.assert_called_once()
|
||||
|
||||
# 验证传递的 URL 是种子信息中的 seedUrl
|
||||
call_args = mock_post.call_args
|
||||
assert call_args[1]['data']['urls'] == "magnet:?xt=urn:btih:1234567890abcdef"
|
||||
logger.info("通过种子信息成功添加下载任务测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.post')
|
||||
def test_download_by_seed_empty_url(self, mock_post):
|
||||
"""测试种子信息中没有 URL 的情况"""
|
||||
# 创建种子信息对象,seedUrl 为空
|
||||
seed_info = SeedInfo(name="测试种子", seedUrl=None)
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
result = downloader.download_by_seed(seed_info)
|
||||
|
||||
assert result == "0"
|
||||
logger.info("种子信息中没有 URL 测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.post')
|
||||
def test_download_by_magnet_with_options(self, mock_post):
|
||||
"""测试带保存路径和分类的下载"""
|
||||
# 模拟成功的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = ""
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
magnet_url = "magnet:?xt=urn:btih:1234567890abcdef"
|
||||
result = downloader.download_by_magnet(
|
||||
magnet_url,
|
||||
save_path="/downloads/test",
|
||||
category="movies",
|
||||
tags="test"
|
||||
)
|
||||
|
||||
assert result == "1"
|
||||
|
||||
# 验证请求参数包含所有选项
|
||||
call_args = mock_post.call_args
|
||||
data = call_args[1]['data']
|
||||
assert data['urls'] == magnet_url
|
||||
assert data['savepath'] == "/downloads/test"
|
||||
assert data['category'] == "movies"
|
||||
assert data['tags'] == "test"
|
||||
logger.info("带保存路径和分类的下载测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.get')
|
||||
def test_test_connection_success(self, mock_get):
|
||||
"""测试连接测试成功"""
|
||||
# 模拟成功的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = "v4.6.0"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
result = downloader.test_connection()
|
||||
|
||||
assert result is True
|
||||
mock_get.assert_called_once_with("http://192.168.123.199:8085/api/v2/app/version")
|
||||
logger.info("连接测试成功测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.get')
|
||||
def test_test_connection_failure(self, mock_get):
|
||||
"""测试连接测试失败"""
|
||||
# 模拟失败的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
result = downloader.test_connection()
|
||||
|
||||
assert result is False
|
||||
logger.info("连接测试失败测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.get')
|
||||
def test_get_downloads_success(self, mock_get):
|
||||
"""测试获取下载列表成功"""
|
||||
# 模拟成功的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
{"name": "测试任务1", "progress": 50},
|
||||
{"name": "测试任务2", "progress": 100}
|
||||
]
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
result = downloader.get_downloads()
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["name"] == "测试任务1"
|
||||
mock_get.assert_called_once_with("http://192.168.123.199:8085/api/v2/torrents/info")
|
||||
logger.info("获取下载列表成功测试通过")
|
||||
|
||||
@patch('ariaDownloaderService.QbittorrentDownloader.session.get')
|
||||
def test_get_downloads_failure(self, mock_get):
|
||||
"""测试获取下载列表失败"""
|
||||
# 模拟失败的响应
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 500
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
downloader = QbittorrentDownloader(
|
||||
host="192.168.123.199",
|
||||
api_port=8085
|
||||
)
|
||||
result = downloader.get_downloads()
|
||||
|
||||
assert result == []
|
||||
logger.info("获取下载列表失败测试通过")
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""运行所有测试"""
|
||||
pytest.main([
|
||||
__file__,
|
||||
"-v",
|
||||
"--tb=short",
|
||||
"--log-cli-level=INFO"
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_tests()
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
测试 seed_is_exist 功能(真实数据库)
|
||||
|
||||
测试说明:
|
||||
- 调用真实的 seed_is_exist 函数和真实 MySQL 数据库(192.168.123.199)
|
||||
- 首次运行自动创建 LookingWeb 库和 seeds_info 表(幂等,可重复运行)
|
||||
- 测试插入的数据在测后自动清理,不污染数据库
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import pymysql
|
||||
import pytest
|
||||
from loguru import logger
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from dataService.MySqlService import DatabaseConfig, MySQLDatabase, seed_is_exist, save_seed
|
||||
from dataService.dto.SeedInfo import SeedInfo
|
||||
|
||||
CREATE_DATABASE_SQL = (
|
||||
"CREATE DATABASE IF NOT EXISTS `LookingWeb` "
|
||||
"DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||
)
|
||||
|
||||
CREATE_TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS `seeds_info` (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) DEFAULT NULL,
|
||||
seed_url VARCHAR(512) NOT NULL,
|
||||
have_download VARCHAR(16) DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT NULL,
|
||||
update_time DATETIME DEFAULT NULL,
|
||||
url VARCHAR(512) DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_seed_url (seed_url)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""
|
||||
|
||||
# 本模块测试插入过的种子 URL,测后统一清理
|
||||
_inserted_urls = []
|
||||
|
||||
|
||||
def ensure_schema():
|
||||
"""幂等创建 LookingWeb 库和 seeds_info 表(真实数据库)"""
|
||||
# 第一步:不带库名连接,先创建数据库
|
||||
conn = pymysql.connect(
|
||||
host=DatabaseConfig.HOST, port=DatabaseConfig.PORT,
|
||||
user=DatabaseConfig.USER, password=DatabaseConfig.PASSWORD,
|
||||
charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(CREATE_DATABASE_SQL)
|
||||
conn.commit()
|
||||
logger.info("数据库就绪: {}(已确认存在)", DatabaseConfig.DB)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# 第二步:连接 LookingWeb 库,创建 seeds_info 表
|
||||
conn = pymysql.connect(
|
||||
host=DatabaseConfig.HOST, port=DatabaseConfig.PORT,
|
||||
user=DatabaseConfig.USER, password=DatabaseConfig.PASSWORD,
|
||||
db=DatabaseConfig.DB, charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(CREATE_TABLE_SQL)
|
||||
conn.commit()
|
||||
logger.info("数据表就绪: seeds_info")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def make_test_seed():
|
||||
"""创建带唯一 seed_url 的真实 SeedInfo 对象,并登记以便测后清理"""
|
||||
url = f"magnet:?xt=urn:btih:{uuid.uuid4().hex}"
|
||||
_inserted_urls.append(url)
|
||||
return SeedInfo(name="测试种子", seedUrl=url, haveDownload="1", url=url)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def db_schema():
|
||||
"""模块级:确保真实数据库和表存在"""
|
||||
ensure_schema()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_inserted_seeds():
|
||||
"""每个测试后:清理本模块插入的种子行,保证测试可重复运行"""
|
||||
yield
|
||||
if not _inserted_urls:
|
||||
return
|
||||
with MySQLDatabase.create_connection() as db:
|
||||
for seed_url in _inserted_urls:
|
||||
db.execute("DELETE FROM seeds_info WHERE seed_url = %s", (seed_url,))
|
||||
_inserted_urls.clear()
|
||||
|
||||
|
||||
class TestSeedIsExist:
|
||||
"""使用真实数据库测试 seed_is_exist"""
|
||||
|
||||
def setup_method(self):
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG")
|
||||
|
||||
def test_seed_not_exists_returns_false(self):
|
||||
"""数据库中不存在的种子 URL 应返回 False"""
|
||||
seed = make_test_seed()
|
||||
logger.info("测试种子不存在场景: {}", seed.seedUrl)
|
||||
|
||||
result = seed_is_exist(seed)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_seed_exists_returns_true(self):
|
||||
"""先通过真实的 save_seed 插入种子,seed_is_exist 应返回 True"""
|
||||
seed = make_test_seed()
|
||||
assert save_seed(seed) is True, "前置条件:save_seed 应成功插入种子"
|
||||
logger.info("测试种子已插入: {}", seed.seedUrl)
|
||||
|
||||
result = seed_is_exist(seed)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_multiple_seeds_all_exist(self):
|
||||
"""多个已存在种子都应被识别为存在"""
|
||||
seeds = [make_test_seed() for _ in range(3)]
|
||||
for seed in seeds:
|
||||
assert save_seed(seed) is True
|
||||
|
||||
for seed in seeds:
|
||||
assert seed_is_exist(seed) is True, f"种子应已存在: {seed.seedUrl}"
|
||||
|
||||
def test_seed_exists_after_delete_returns_false(self):
|
||||
"""删除后的种子再查询应返回 False(完整的真实生命周期)"""
|
||||
from dataService.MySqlService import delete_seed
|
||||
seed = make_test_seed()
|
||||
assert save_seed(seed) is True
|
||||
assert seed_is_exist(seed) is True
|
||||
|
||||
assert delete_seed(seed) is True, "前置条件:delete_seed 应成功删除种子"
|
||||
|
||||
assert seed_is_exist(seed) is False
|
||||
|
||||
def test_db_unreachable_returns_false(self, monkeypatch):
|
||||
"""数据库不可达时(真实错误路径)应返回 False,而不是抛异常"""
|
||||
# 指向同主机一个已关闭的端口,模拟数据库不可达
|
||||
monkeypatch.setattr(DatabaseConfig, "PORT", 3307)
|
||||
seed = make_test_seed()
|
||||
|
||||
result = seed_is_exist(seed)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""运行所有测试"""
|
||||
pytest.main([
|
||||
__file__,
|
||||
"-v",
|
||||
"--tb=short",
|
||||
"--log-cli-level=INFO"
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_tests()
|
||||
Reference in New Issue
Block a user