签入版本
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""业务模块单元测试。"""
|
||||
@@ -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) == []
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user