- Add Free_Proxy_Website-style fpw_* plugins and register them - Per-plugin crawl timeout (crawl_timeout_seconds=120); remove global crawl_timeout setting - Validator: fix connect vs total timeout on save; SOCKS session LRU cache; drop redundant semaphore - Validation handler uses single DB connection; batch upsert after crawl; WorkerPool put_nowait - Remove unused max_retries from settings API/UI; settings maintenance SQL + init_db cleanup of deprecated keys - WebSocket dashboard stats; ProxyList pool_filter and API alignment - POST /api/proxies/delete-one for IPv6-safe deletes; task poll stops on 404 - pytest uses PROXYPOOL_DB_PATH=db/proxies.test.sqlite so tests do not wipe production DB - .gitignore: explicit proxies.test.sqlite patterns; fix plugin_service ValidationException import Made-with: Cursor
191 lines
7.2 KiB
Python
191 lines
7.2 KiB
Python
"""设置 API 集成测试 - 测试 /api/settings 接口"""
|
||
import pytest
|
||
|
||
|
||
class TestSettingsAPI:
|
||
"""测试设置相关 API"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_settings(self, client):
|
||
"""测试 GET /api/settings"""
|
||
response = await client.get("/api/settings")
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["code"] == 200
|
||
assert "crawl_timeout" not in data["data"]
|
||
assert "validation_timeout" in data["data"]
|
||
assert "auto_validate" in data["data"]
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_settings_structure(self, client):
|
||
"""测试 GET /api/settings 返回结构"""
|
||
response = await client.get("/api/settings")
|
||
data = response.json()["data"]
|
||
|
||
expected_keys = [
|
||
"validation_timeout",
|
||
"default_concurrency",
|
||
"min_proxy_score",
|
||
"proxy_expiry_days",
|
||
"auto_validate",
|
||
"auto_validate_after_crawl",
|
||
"validate_interval_minutes",
|
||
"validation_targets",
|
||
]
|
||
for key in expected_keys:
|
||
assert key in data, f"缺少设置项: {key}"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_settings(self, client):
|
||
"""测试 POST /api/settings"""
|
||
settings = {
|
||
"validation_timeout": 15,
|
||
"default_concurrency": 100,
|
||
"min_proxy_score": 10,
|
||
"proxy_expiry_days": 14,
|
||
"auto_validate": True,
|
||
"auto_validate_after_crawl": False,
|
||
"validate_interval_minutes": 60,
|
||
"validation_targets": [
|
||
"http://httpbin.org/ip",
|
||
],
|
||
}
|
||
response = await client.post("/api/settings", json=settings)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["code"] == 200
|
||
for key, value in settings.items():
|
||
assert data["data"][key] == value
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_settings_partial(self, client):
|
||
"""测试 POST /api/settings - 部分更新(实际上会替换所有)"""
|
||
response = await client.get("/api/settings")
|
||
current_settings = response.json()["data"]
|
||
|
||
new_settings = current_settings.copy()
|
||
new_settings["validation_timeout"] = 25
|
||
new_settings["auto_validate"] = False
|
||
|
||
response = await client.post("/api/settings", json=new_settings)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["data"]["validation_timeout"] == 25
|
||
assert data["data"]["auto_validate"] is False
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_settings_validation_error(self, client):
|
||
"""测试 POST /api/settings - 验证错误"""
|
||
invalid_settings = {
|
||
"validation_timeout": 100,
|
||
"default_concurrency": 50,
|
||
"min_proxy_score": 0,
|
||
"proxy_expiry_days": 7,
|
||
"auto_validate": True,
|
||
"validate_interval_minutes": 30,
|
||
}
|
||
response = await client.post("/api/settings", json=invalid_settings)
|
||
assert response.status_code == 422
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_settings_invalid_type(self, client):
|
||
"""测试 POST /api/settings - 无效类型"""
|
||
invalid_settings = {
|
||
"validation_timeout": 10,
|
||
"default_concurrency": "invalid",
|
||
"min_proxy_score": 0,
|
||
"proxy_expiry_days": 7,
|
||
"auto_validate": True,
|
||
"validate_interval_minutes": 30,
|
||
}
|
||
response = await client.post("/api/settings", json=invalid_settings)
|
||
assert response.status_code == 422
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_settings_ignores_deprecated_crawl_timeout(self, client):
|
||
"""旧客户端若仍提交 crawl_timeout,应忽略且保存成功"""
|
||
response = await client.get("/api/settings")
|
||
base = response.json()["data"]
|
||
payload = {**base, "crawl_timeout": 999}
|
||
response = await client.post("/api/settings", json=payload)
|
||
assert response.status_code == 200
|
||
again = (await client.get("/api/settings")).json()["data"]
|
||
assert "crawl_timeout" not in again
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_settings_ignores_obsolete_max_retries(self, client):
|
||
"""已移除的 max_retries 键若仍被提交,应忽略。"""
|
||
response = await client.get("/api/settings")
|
||
base = response.json()["data"]
|
||
payload = {**base, "max_retries": 9}
|
||
response = await client.post("/api/settings", json=payload)
|
||
assert response.status_code == 200
|
||
again = (await client.get("/api/settings")).json()["data"]
|
||
assert "max_retries" not in again
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_settings_roundtrip(self, client):
|
||
"""测试设置读写一致性"""
|
||
import random
|
||
|
||
test_settings = {
|
||
"validation_timeout": random.randint(5, 30),
|
||
"default_concurrency": random.randint(20, 100),
|
||
"min_proxy_score": random.randint(0, 50),
|
||
"proxy_expiry_days": random.randint(1, 14),
|
||
"auto_validate": random.choice([True, False]),
|
||
"validate_interval_minutes": random.randint(10, 120),
|
||
}
|
||
|
||
response = await client.post("/api/settings", json=test_settings)
|
||
assert response.status_code == 200
|
||
|
||
response = await client.get("/api/settings")
|
||
saved_settings = response.json()["data"]
|
||
|
||
for key, value in test_settings.items():
|
||
assert saved_settings[key] == value, f"设置项 {key} 不一致"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_settings_roundtrip_with_validation_targets(self, client):
|
||
"""测试设置读写一致性 - 包含数组类型的 validation_targets"""
|
||
test_settings = {
|
||
"validation_timeout": 10,
|
||
"default_concurrency": 50,
|
||
"min_proxy_score": 0,
|
||
"proxy_expiry_days": 7,
|
||
"auto_validate": True,
|
||
"validate_interval_minutes": 30,
|
||
"validation_targets": [
|
||
"http://example.com/1",
|
||
"https://example.com/2",
|
||
],
|
||
}
|
||
|
||
response = await client.post("/api/settings", json=test_settings)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["data"]["validation_targets"] == test_settings["validation_targets"]
|
||
|
||
response = await client.get("/api/settings")
|
||
saved_settings = response.json()["data"]
|
||
assert saved_settings["validation_targets"] == test_settings["validation_targets"]
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_save_settings_empty_validation_targets(self, client):
|
||
"""测试保存空的 validation_targets"""
|
||
response = await client.get("/api/settings")
|
||
current_settings = response.json()["data"]
|
||
|
||
new_settings = current_settings.copy()
|
||
new_settings["validation_targets"] = []
|
||
|
||
response = await client.post("/api/settings", json=new_settings)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["data"]["validation_targets"] == []
|
||
|
||
response = await client.get("/api/settings")
|
||
saved_settings = response.json()["data"]
|
||
assert saved_settings["validation_targets"] == []
|