Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion tests/api/test_proxy_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
__author__ = 'JHao'

import pytest
from unittest.mock import patch, MagicMock
from helper.proxy import Proxy
from api.proxyApi import JsonResponse


@pytest.fixture
Expand Down Expand Up @@ -151,4 +153,39 @@ def test_count_empty(self, client, mocks):
data = resp.get_json()
assert data["count"] == 0
assert data["http_type"] == {}
assert data["source"] == {}
assert data["source"] == {}


class TestRefresh:

def test_refresh_returns_success(self, client):
resp = client.get("/refresh/")
assert resp.status_code == 200
assert b"success" in resp.data


class TestJsonResponse:

def test_force_type_with_dict(self, app):
"""dict -> JSON Response"""
with app.app_context():
resp = JsonResponse.force_type({"key": "val"})
assert resp.content_type == "application/json"

def test_force_type_with_list(self, app):
"""list -> JSON Response"""
with app.app_context():
resp = JsonResponse.force_type([1, 2, 3])
assert resp.content_type == "application/json"


class TestRunFlask:

@patch("api.proxyApi.platform")
@patch("api.proxyApi.app")
def test_runflask_windows_path(self, mock_app, mock_platform):
"""Windows 下调用 app.run()"""
mock_platform.system.return_value = "Windows"
from api.proxyApi import runFlask
runFlask()
mock_app.run.assert_called_once()
276 changes: 276 additions & 0 deletions tests/unit/test_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
File Name: test_check.py
Description : helper/check.py 单元测试
Author : JHao
date: 2026/6/15
-------------------------------------------------
Change Activity:
2026/06/15:
-------------------------------------------------
"""
__author__ = 'JHao'

import pytest
from unittest.mock import patch, MagicMock, PropertyMock
from datetime import datetime

from helper.proxy import Proxy
from helper.check import DoValidator, _ThreadChecker


class TestDoValidator:
"""DoValidator.validator 测试"""

@patch("helper.check.ConfigHandler")
@patch("helper.check.ProxyValidator")
def test_validator_http_pass_https_pass(self, mock_pv_cls, mock_conf_cls):
"""HTTP 通过 + HTTPS 通过 -> https=True, fail_count 不变"""
mock_pv = MagicMock()
mock_pv.http_validator = [MagicMock(return_value=True)]
mock_pv.https_validator = [MagicMock(return_value=True)]
mock_pv_cls.http_validator = mock_pv.http_validator
mock_pv_cls.https_validator = mock_pv.https_validator

mock_conf = MagicMock()
mock_conf.proxyRegion = False
mock_conf_cls.return_value = mock_conf

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.fail_count = 0

# Patch DoValidator.conf at class level
with patch.object(DoValidator, "conf", mock_conf):
result = DoValidator.validator(proxy, "use")

assert result.https is True
assert result.last_status is True
assert result.check_count == 1
assert result.fail_count == 0

@patch("helper.check.ConfigHandler")
@patch("helper.check.ProxyValidator")
def test_validator_http_pass_https_fail(self, mock_pv_cls, mock_conf_cls):
"""HTTP 通过 + HTTPS 失败 -> https=False"""
mock_pv_cls.http_validator = [MagicMock(return_value=True)]
mock_pv_cls.https_validator = [MagicMock(return_value=False)]

mock_conf = MagicMock()
mock_conf.proxyRegion = False

proxy = Proxy("1.2.3.4:8080", source="test")

with patch.object(DoValidator, "conf", mock_conf):
result = DoValidator.validator(proxy, "use")

assert result.https is False
assert result.last_status is True

@patch("helper.check.ConfigHandler")
@patch("helper.check.ProxyValidator")
def test_validator_http_fail(self, mock_pv_cls, mock_conf_cls):
"""HTTP 失败 -> fail_count += 1, last_status=False"""
mock_pv_cls.http_validator = [MagicMock(return_value=False)]

mock_conf = MagicMock()
mock_conf.proxyRegion = False

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.fail_count = 0

with patch.object(DoValidator, "conf", mock_conf):
result = DoValidator.validator(proxy, "use")

assert result.last_status is False
assert result.fail_count == 1

@patch("helper.check.ConfigHandler")
@patch("helper.check.ProxyValidator")
def test_validator_fail_count_decrement(self, mock_pv_cls, mock_conf_cls):
"""HTTP 通过 + fail_count > 0 -> fail_count -= 1"""
mock_pv_cls.http_validator = [MagicMock(return_value=True)]
mock_pv_cls.https_validator = [MagicMock(return_value=True)]

mock_conf = MagicMock()
mock_conf.proxyRegion = False

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.fail_count = 3

with patch.object(DoValidator, "conf", mock_conf):
result = DoValidator.validator(proxy, "use")

assert result.fail_count == 2

@patch("helper.check.DoValidator.regionGetter", return_value="US")
@patch("helper.check.ConfigHandler")
@patch("helper.check.ProxyValidator")
def test_validator_raw_sets_region(self, mock_pv_cls, mock_conf_cls, mock_region):
"""work_type='raw' + proxyRegion=True -> regionGetter 被调用"""
mock_pv_cls.http_validator = [MagicMock(return_value=True)]
mock_pv_cls.https_validator = [MagicMock(return_value=True)]

mock_conf = MagicMock()
mock_conf.proxyRegion = True

proxy = Proxy("1.2.3.4:8080", source="test")

with patch.object(DoValidator, "conf", mock_conf):
result = DoValidator.validator(proxy, "raw")

assert result.region == "US"
mock_region.assert_called_once_with(proxy)

@patch("helper.check.DoValidator.regionGetter")
@patch("helper.check.ConfigHandler")
@patch("helper.check.ProxyValidator")
def test_validator_use_skips_region(self, mock_pv_cls, mock_conf_cls, mock_region):
"""work_type='use' -> 不调用 regionGetter"""
mock_pv_cls.http_validator = [MagicMock(return_value=True)]
mock_pv_cls.https_validator = [MagicMock(return_value=True)]

mock_conf = MagicMock()
mock_conf.proxyRegion = True

proxy = Proxy("1.2.3.4:8080", source="test")

with patch.object(DoValidator, "conf", mock_conf):
DoValidator.validator(proxy, "use")

mock_region.assert_not_called()


class TestRegionGetter:
"""DoValidator.regionGetter 测试"""

@patch("helper.check.WebRequest")
def test_success_returns_country_code(self, mock_wr_cls):
"""正常返回 -> country_code"""
mock_wr = MagicMock()
mock_wr.get.return_value.json = {"country_code": "CN"}
mock_wr_cls.return_value = mock_wr

proxy = Proxy("1.2.3.4:8080")
result = DoValidator.regionGetter(proxy)
assert result == "CN"

@patch("helper.check.WebRequest")
def test_exception_returns_error(self, mock_wr_cls):
"""异常 -> 'error'"""
mock_wr = MagicMock()
mock_wr.get.side_effect = Exception("timeout")
mock_wr_cls.return_value = mock_wr

proxy = Proxy("1.2.3.4:8080")
result = DoValidator.regionGetter(proxy)
assert result == "error"


def _make_checker(work_type, proxy_handler, conf=None):
"""构造手动注入依赖的 _ThreadChecker(绕过 Thread.__init__)"""
checker = _ThreadChecker.__new__(_ThreadChecker)
# 手动初始化 Thread 所需的状态
checker._initialized = True
checker._name = "test_thread"
checker._target = None
checker._args = ()
checker._kwargs = {}
checker._daemonic = False
checker._ident = None
checker._tstate_lock = None
checker._started = MagicMock()
checker._is_stopped = False
checker._block = MagicMock()
checker._waiters = []
checker._stderr = None
# 注入依赖
checker.proxy_handler = proxy_handler
checker.log = MagicMock()
checker.work_type = work_type
checker.conf = conf or MagicMock()
return checker


class TestThreadCheckerIfRaw:
"""_ThreadChecker.__ifRaw 测试"""

def test_ifraw_new_proxy_gets_put(self):
"""last_status=True, exists=False -> put"""
mock_ph = MagicMock()
mock_ph.exists.return_value = False

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.last_status = True

checker = _make_checker("raw", mock_ph)
checker._ThreadChecker__ifRaw(proxy)
mock_ph.put.assert_called_once_with(proxy)

def test_ifraw_existing_proxy_skipped(self):
"""last_status=True, exists=True -> 不 put"""
mock_ph = MagicMock()
mock_ph.exists.return_value = True

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.last_status = True

checker = _make_checker("raw", mock_ph)
checker._ThreadChecker__ifRaw(proxy)
mock_ph.put.assert_not_called()

def test_ifraw_failed_proxy_not_put(self):
"""last_status=False -> 不 put"""
mock_ph = MagicMock()

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.last_status = False

checker = _make_checker("raw", mock_ph)
checker._ThreadChecker__ifRaw(proxy)
mock_ph.put.assert_not_called()


class TestThreadCheckerIfUse:
"""_ThreadChecker.__ifUse 测试"""

def test_ifuse_pass_gets_put(self):
"""last_status=True -> put"""
mock_ph = MagicMock()

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.last_status = True

checker = _make_checker("use", mock_ph)
checker._ThreadChecker__ifUse(proxy)
mock_ph.put.assert_called_once_with(proxy)

def test_ifuse_fail_exceeds_max_deleted(self):
"""fail_count > maxFailCount -> delete"""
mock_ph = MagicMock()
mock_conf = MagicMock()
mock_conf.maxFailCount = 3

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.last_status = False
proxy.fail_count = 5

checker = _make_checker("use", mock_ph, mock_conf)
checker._ThreadChecker__ifUse(proxy)
mock_ph.delete.assert_called_once_with(proxy)
mock_ph.put.assert_not_called()

def test_ifuse_fail_below_max_kept(self):
"""fail_count <= maxFailCount -> put"""
mock_ph = MagicMock()
mock_conf = MagicMock()
mock_conf.maxFailCount = 3

proxy = Proxy("1.2.3.4:8080", source="test")
proxy.last_status = False
proxy.fail_count = 2

checker = _make_checker("use", mock_ph, mock_conf)
checker._ThreadChecker__ifUse(proxy)
mock_ph.put.assert_called_once_with(proxy)
mock_ph.delete.assert_not_called()
66 changes: 66 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
File Name: test_cli.py
Description : proxyPool CLI 单元测试
Author : JHao
date: 2026/6/15
-------------------------------------------------
Change Activity:
2026/06/15:
-------------------------------------------------
"""
__author__ = 'JHao'

import pytest
from unittest.mock import patch, MagicMock
from click.testing import CliRunner

from proxyPool import cli
from setting import VERSION


@pytest.fixture
def runner():
return CliRunner()


class TestCli:

def test_version_flag(self, runner):
"""--version 显示版本号"""
result = runner.invoke(cli, ["--version"])
assert result.exit_code == 0
assert VERSION in result.output

@patch("proxyPool.startScheduler")
def test_schedule_command(self, mock_scheduler, runner):
"""schedule 命令调用 startScheduler"""
result = runner.invoke(cli, ["schedule"])
assert result.exit_code == 0
mock_scheduler.assert_called_once()

@patch("proxyPool.startServer")
def test_server_command(self, mock_server, runner):
"""server 命令调用 startServer"""
result = runner.invoke(cli, ["server"])
assert result.exit_code == 0
mock_server.assert_called_once()

@patch("handler.configHandler.ConfigHandler")
@patch("helper.fetch._discover_fetchers")
def test_fetcher_command(self, mock_discover, mock_conf_cls, runner):
"""fetcher 命令输出启用的代理源列表"""
mock_cls1 = MagicMock()
mock_cls1.name = "freeProxy01"
mock_cls2 = MagicMock()
mock_cls2.name = "freeProxy02"
mock_discover.return_value = [mock_cls1, mock_cls2]
mock_conf = MagicMock()
mock_conf.fetcherExclude = []
mock_conf_cls.return_value = mock_conf

result = runner.invoke(cli, ["fetcher"])
assert result.exit_code == 0
assert "freeProxy01" in result.output
assert "freeProxy02" in result.output
Loading
Loading