This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user