签入版本

This commit is contained in:
2026-09-16 12:00:58 +08:00
commit 20a2f846ff
842 changed files with 93035 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""业务模块单元测试。"""
+21
View File
@@ -0,0 +1,21 @@
"""认证模块测试。"""
import pytest
from modules.auth.schemas import LoginSchema
def test_login_schema_valid():
form = LoginSchema(username="admin", password="123456", code="123456", key="uuid-1")
assert form.username == "admin"
assert form.key == "uuid-1"
assert form.code == "123456"
def test_login_schema_missing_field():
with pytest.raises(Exception):
LoginSchema(username="admin")
def test_login_schema_extra_ignored():
form = LoginSchema(username="admin", password="123456", code="123456", key="x", extra_field=1)
assert not hasattr(form, "extra_field")
@@ -0,0 +1,66 @@
"""JobLogService 分页时间区间过滤测试。"""
from modules.job_log.service import JobLogService
from tests.fixtures.factories import FakeHandler
class TimeFilterQuery:
"""捕获 filter 条件的伪查询对象。"""
def __init__(self, items=None):
self.items = items or []
self.criteria = []
def filter(self, *criterion):
self.criteria.extend(criterion)
return self
def order_by(self, *cols):
return self
def all(self):
return self.items
class TimeFilterRepo:
"""伪仓储:仅返回捕获查询。"""
def __init__(self):
self.query = TimeFilterQuery()
def filter(self, *criterion):
return self.query
def close(self):
pass
def _service():
service = JobLogService(db=None)
service.repo = TimeFilterRepo() # 注入伪仓储
return service
def _range_names(query):
"""提取 filter 条件中的列名(用于断言区间过滤生效)。"""
return [c.left.name for c in query.criteria if hasattr(c, "left")]
def test_time_range_filter_applied():
service = _service()
handler = FakeHandler(query={
"startTime": "2026-08-01 00:00:00",
"endTime": "2026-08-15 23:59:59",
})
service._build_page_query(handler)
assert _range_names(service.repo.query).count("start_time") == 2
def test_time_range_filter_without_args_no_crash():
service = _service()
handler = FakeHandler(query={})
service._build_page_query(handler)
assert _range_names(service.repo.query) == []
+139
View File
@@ -0,0 +1,139 @@
"""任务调度器测试:执行日志记录、Cron 校验与执行策略防重叠。"""
import types
import pytest
from modules.job import scheduler
from modules.job_log import service as job_log_service
def make_job(**overrides):
"""构造一个伪任务快照(无 DB),与调度器要求的 JobSnapshot 对齐。
默认 URL 用公网 IP 字面量(8.8.8.8):无需 DNS 即能通过 getaddrinfo,
且 is_global=True,符合调度器防 SSRF 的 URL 校验(不再使用 127.0.0.1)。
"""
defaults = dict(
id=1, job_name="测试任务", job_group="系统任务", job_trigger="cron",
cron_expression="*/5 * * * *", url="http://8.8.8.8/api/v1/health",
execute_policy=1, status=1,
)
defaults.update(overrides)
return scheduler._snapshot_job(types.SimpleNamespace(**defaults))
class FakeResponse:
"""支持 with 语法的伪 HTTP 响应。"""
status = 200
def __enter__(self):
return self
def __exit__(self, *exc):
return False
@pytest.fixture(autouse=True)
def _clean_running():
"""每个用例前后清空运行中集合,避免相互污染。"""
scheduler._running.clear()
yield
scheduler._running.clear()
def _capture(monkeypatch):
"""打桩 record_job_log,返回记录列表。"""
records = []
monkeypatch.setattr(job_log_service, "record_job_log", lambda **kw: records.append(kw))
return records
def test_execute_job_success_records_log(monkeypatch):
records = _capture(monkeypatch)
# execute_job 经 _no_redirect_opener.open 发起请求(禁重定向,防 SSRF 302 跳内网)
monkeypatch.setattr(scheduler._no_redirect_opener, "open", lambda url, timeout=None: FakeResponse())
scheduler.execute_job(make_job())
assert len(records) == 1
assert records[0]["status"] == 0
assert records[0]["job_name"] == "测试任务"
assert records[0]["job_message"] == "任务触发成功"
assert records[0]["consume_time"] >= 0
def test_execute_job_failure_records_exception(monkeypatch):
records = _capture(monkeypatch)
def _boom(url, timeout=None):
raise ConnectionError("connect timeout")
monkeypatch.setattr(scheduler._no_redirect_opener, "open", _boom)
scheduler.execute_job(make_job())
assert len(records) == 1
assert records[0]["status"] == 1
assert "connect timeout" in records[0]["exception_info"]
assert records[0]["job_message"] == "任务触发失败"
def test_overlap_policy_skip_while_running(monkeypatch):
"""策略 3-放弃执行:任务执行中时跳过本次触发,不调用任务 URL。"""
records = _capture(monkeypatch)
calls = {"urlopen": 0}
def _urlopen(url, timeout=None):
calls["urlopen"] += 1
return FakeResponse()
monkeypatch.setattr(scheduler._no_redirect_opener, "open", _urlopen)
job = make_job(execute_policy=3)
scheduler._running.add(job.id)
scheduler.execute_job(job)
assert calls["urlopen"] == 0
assert len(records) == 1
assert records[0]["status"] == 0
assert "放弃执行" in records[0]["job_message"]
# 跳过不等于释放运行标记:原执行仍占用,任务应保留在运行集合
assert job.id in scheduler._running
def test_overlap_policy_immediate_runs_in_parallel(monkeypatch):
"""策略 1-立即执行:即使任务执行中仍再次执行(并行)。"""
records = _capture(monkeypatch)
monkeypatch.setattr(scheduler._no_redirect_opener, "open", lambda url, timeout=None: FakeResponse())
job = make_job(execute_policy=1)
scheduler._running.add(job.id)
scheduler.execute_job(job)
assert len(records) == 1
assert records[0]["status"] == 0
assert job.id not in scheduler._running # 执行结束从运行集合移除
def test_validate_cron_expression():
assert scheduler.validate_cron_expression("*/5 * * * *") is None
assert scheduler.validate_cron_expression("") is not None
assert scheduler.validate_cron_expression("not-a-cron") is not None
def test_validate_job_url_blocks_ssrf():
"""防 SSRF:拒绝非 http/https 协议与内网/回环/云元数据地址。"""
assert scheduler.validate_job_url("http://8.8.8.8/ping") is True
assert scheduler.validate_job_url("http://127.0.0.1:8041/x") is False
assert scheduler.validate_job_url("http://192.168.1.1/x") is False
assert scheduler.validate_job_url("http://169.254.169.254/latest/meta-data/") is False
assert scheduler.validate_job_url("file:///etc/passwd") is False
assert scheduler.validate_job_url("ftp://example.com/x") is False
def test_no_redirect_handler_blocks_redirect():
"""禁重定向:公网 URL 302 跳内网的 SSRF 通道被切断。"""
handler = scheduler._NoRedirectHandler()
# redirect_request 返回 None -> urlopen 抛 HTTPError,不会继续请求跳转目标
assert handler.redirect_request(None, None, 302, "", None, "http://169.254.169.254/") is None
+42
View File
@@ -0,0 +1,42 @@
"""用户校验模型测试。"""
import pytest
from modules.user.schemas import UserSchema, UserStatusSchema
def test_user_create_minimal():
form = UserSchema(
realname="测试", gender=1, avatar="default.png",
mobile="13800138000", email="test@example.com",
deptId=0, levelId=0, positionId=0,
address="测试地址", username="tester",
status=1, sort=0,
)
assert form.gender == 1
assert form.status == 1
assert form.city is None
def test_user_create_with_city_and_roles():
form = UserSchema(
realname="测试", gender=1, avatar="default.png",
mobile="13800138000", email="test@example.com",
deptId=0, levelId=0, positionId=0,
address="测试地址", username="tester",
status=1, sort=0,
city=[110000, 110100, 110101],
roles=[1, 2],
)
assert form.city == [110000, 110100, 110101]
assert form.roles == [1, 2]
def test_user_status_invalid():
with pytest.raises(Exception):
UserStatusSchema(id=0, status=1)
def test_user_status_valid():
form = UserStatusSchema(id=1, status=2)
assert form.id == 1
assert form.status == 2