test(02-02): add Boss HTTP layer mock tests (QUAL-03)
- 22 tests covering SearchRecJobs, GetBrandDetail, SearchBrandJobs, GetJobDetail, BossClient - Uses MagicMock (requests_go not compatible with respx) - Covers success responses, HTTP errors, biz errors, Traceid injection - All 22 tests pass (0.08s)
This commit is contained in:
parent
919ed9f799
commit
5bd44774b9
1
tests/boss/__init__.py
Normal file
1
tests/boss/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# tests/boss/
|
||||
271
tests/boss/test_boss_client.py
Normal file
271
tests/boss/test_boss_client.py
Normal file
@ -0,0 +1,271 @@
|
||||
"""
|
||||
Boss 直聘 HTTP 层 mock 测试(QUAL-03)
|
||||
|
||||
使用 unittest.mock.MagicMock 替代真实 HTTP 客户端,
|
||||
覆盖正常响应和错误响应场景,无网络依赖。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from spiderJobs.platforms.boss.api import (
|
||||
GetBrandDetail,
|
||||
GetJobDetail,
|
||||
SearchBrandJobs,
|
||||
SearchRecJobs,
|
||||
_parse_boss_response,
|
||||
)
|
||||
from spiderJobs.platforms.boss.client import BossClient
|
||||
from crawler_core.base import Result
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 1. _parse_boss_response 纯函数测试
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestParseBossResponse:
|
||||
|
||||
def test_http_error_returns_failure(self):
|
||||
result = _parse_boss_response(500, {})
|
||||
assert result.success is False
|
||||
assert result.status_code == 500
|
||||
|
||||
def test_non_dict_raw_returns_failure(self):
|
||||
result = _parse_boss_response(200, "not a dict")
|
||||
assert result.success is False
|
||||
|
||||
def test_biz_error_code_35_returns_failure(self):
|
||||
result = _parse_boss_response(200, {"code": 35, "message": "IP地址存在异常"})
|
||||
assert result.success is False
|
||||
assert result.status_code == 35
|
||||
assert "IP" in result.error
|
||||
|
||||
def test_joblist_payload_parsed_correctly(self):
|
||||
raw = {
|
||||
"code": 0,
|
||||
"zpData": {
|
||||
"jobList": [{"title": "Python工程师"}],
|
||||
"hasMore": True,
|
||||
},
|
||||
}
|
||||
result = _parse_boss_response(200, raw)
|
||||
assert result.success is True
|
||||
assert len(result.list) == 1
|
||||
assert result.list[0]["title"] == "Python工程师"
|
||||
assert result.is_end_page is False # hasMore=True → is_end_page=False
|
||||
|
||||
def test_joblist_no_more_pages(self):
|
||||
raw = {
|
||||
"code": 0,
|
||||
"zpData": {"jobList": [{"title": "测试"}], "hasMore": False},
|
||||
}
|
||||
result = _parse_boss_response(200, raw)
|
||||
assert result.is_end_page is True
|
||||
|
||||
def test_detail_payload(self):
|
||||
raw = {"code": 0, "zpData": {"companyName": "测试公司"}}
|
||||
result = _parse_boss_response(200, raw)
|
||||
assert result.success is True
|
||||
assert result.data == {"companyName": "测试公司"}
|
||||
|
||||
def test_list_field_payload_parsed(self):
|
||||
raw = {
|
||||
"code": 0,
|
||||
"zpData": {"list": [{"jobName": "运营岗"}], "hasMore": True},
|
||||
}
|
||||
result = _parse_boss_response(200, raw)
|
||||
assert result.success is True
|
||||
assert len(result.list) == 1
|
||||
assert result.is_end_page is False
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 2. SearchRecJobs
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestSearchRecJobs:
|
||||
|
||||
def _make_mock_client(self, return_value):
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value = return_value
|
||||
return mock_client
|
||||
|
||||
def test_search_success(self):
|
||||
raw = {
|
||||
"code": 0,
|
||||
"zpData": {
|
||||
"jobList": [{"title": "测试职位1"}, {"title": "测试职位2"}],
|
||||
"hasMore": False,
|
||||
},
|
||||
}
|
||||
searcher = SearchRecJobs(city_code="101010100", client=self._make_mock_client((200, raw)))
|
||||
result = searcher.search(page_index=1)
|
||||
|
||||
assert result.success is True
|
||||
assert len(result.list) == 2
|
||||
assert result.is_end_page is True
|
||||
|
||||
def test_search_http_error(self):
|
||||
searcher = SearchRecJobs(client=self._make_mock_client((403, {})))
|
||||
result = searcher.search(page_index=1)
|
||||
assert result.success is False
|
||||
assert result.status_code == 403
|
||||
|
||||
def test_search_biz_error(self):
|
||||
raw = {"code": 35, "message": "IP地址存在异常"}
|
||||
searcher = SearchRecJobs(client=self._make_mock_client((200, raw)))
|
||||
result = searcher.search(page_index=1)
|
||||
assert result.success is False
|
||||
|
||||
def test_search_builds_city_code_param(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value = (200, {"code": 0, "zpData": {"jobList": [], "hasMore": False}})
|
||||
searcher = SearchRecJobs(city_code="101280600", page_size=10, client=mock_client)
|
||||
searcher.search(page_index=2)
|
||||
|
||||
# 验证 get 方法被调用
|
||||
assert mock_client.get.called
|
||||
call_args = mock_client.get.call_args
|
||||
# 第二个参数是 params dict
|
||||
params = call_args[0][1] if len(call_args[0]) > 1 else call_args[1].get("params")
|
||||
assert params["cityCode"] == "101280600"
|
||||
assert params["page"] == 2
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 3. GetBrandDetail
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestGetBrandDetail:
|
||||
|
||||
def test_fetch_success(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value = (200, {
|
||||
"code": 0,
|
||||
"zpData": {"brandName": "测试公司", "brandId": "abc123"},
|
||||
})
|
||||
fetcher = GetBrandDetail(brand_id="abc123", client=mock_client)
|
||||
result = fetcher.fetch()
|
||||
|
||||
assert result.success is True
|
||||
assert result.data["brandName"] == "测试公司"
|
||||
|
||||
def test_fetch_404(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value = (404, {})
|
||||
fetcher = GetBrandDetail(brand_id="notexist", client=mock_client)
|
||||
result = fetcher.fetch()
|
||||
|
||||
assert result.success is False
|
||||
assert result.status_code == 404
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 4. SearchBrandJobs
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestSearchBrandJobs:
|
||||
|
||||
def test_search_success_has_more(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value = (200, {
|
||||
"code": 0,
|
||||
"zpData": {"list": [{"jobName": "测试岗位"}], "hasMore": True},
|
||||
})
|
||||
searcher = SearchBrandJobs(brand_id="abc123", client=mock_client)
|
||||
result = searcher.search(page_index=1)
|
||||
|
||||
assert result.success is True
|
||||
assert len(result.list) == 1
|
||||
assert result.is_end_page is False
|
||||
|
||||
def test_search_success_no_more(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.get.return_value = (200, {
|
||||
"code": 0,
|
||||
"zpData": {"list": [], "hasMore": False},
|
||||
})
|
||||
searcher = SearchBrandJobs(brand_id="abc123", client=mock_client)
|
||||
result = searcher.search(page_index=1)
|
||||
|
||||
assert result.is_end_page is True
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 5. GetJobDetail(batch 接口路径)
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestGetJobDetail:
|
||||
|
||||
def test_fetch_success_merges_sub_requests(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.batch.return_value = (200, {
|
||||
"code": 0,
|
||||
"zpData": {
|
||||
"/wapi/zpgeek/miniapp/job/detail.json": {
|
||||
"zpData": {"jobName": "数据工程师"}
|
||||
},
|
||||
"/wapi/zpgeek/miniapp/jobdetail/improvement/query.json": {
|
||||
"zpData": {"tags": ["Python", "大数据"]}
|
||||
},
|
||||
},
|
||||
})
|
||||
fetcher = GetJobDetail(security_id="sid123", job_id="jid456", client=mock_client)
|
||||
result = fetcher.fetch()
|
||||
|
||||
assert result.success is True
|
||||
assert result.data["detail"]["jobName"] == "数据工程师"
|
||||
assert "Python" in result.data["improvement"]["tags"]
|
||||
|
||||
def test_fetch_biz_error(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.batch.return_value = (200, {"code": 35, "message": "IP地址存在异常"})
|
||||
fetcher = GetJobDetail(security_id="sid", job_id="jid", client=mock_client)
|
||||
result = fetcher.fetch()
|
||||
assert result.success is False
|
||||
|
||||
def test_fetch_exception_handled(self):
|
||||
mock_client = MagicMock()
|
||||
mock_client.batch.side_effect = ConnectionError("连接超时")
|
||||
fetcher = GetJobDetail(security_id="sid", job_id="jid", client=mock_client)
|
||||
result = fetcher.fetch()
|
||||
assert result.success is False
|
||||
assert "连接超时" in result.error
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 6. BossClient — Traceid/mpt/wt2 请求头注入
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestBossClientHeaders:
|
||||
|
||||
def test_get_injects_traceid(self):
|
||||
"""每次请求头包含 Traceid(以 M-W 开头)"""
|
||||
client = BossClient()
|
||||
headers = client._boss_headers()
|
||||
assert "Traceid" in headers
|
||||
assert headers["Traceid"].startswith("M-W")
|
||||
|
||||
def test_traceid_length_is_valid(self):
|
||||
"""Traceid 格式长度正确(前缀 + 22 字符)"""
|
||||
client = BossClient()
|
||||
traceid = client._boss_headers()["Traceid"]
|
||||
# 格式:M-W (3) + uuid_19 + checksum_3 = 25
|
||||
assert len(traceid) >= 20
|
||||
|
||||
def test_mpt_wt2_in_headers(self):
|
||||
"""signer 的 mpt/wt2 注入到请求头"""
|
||||
from crawler_core.boss.sign import BossSign
|
||||
signer = BossSign(mpt="test_mpt_value", wt2="test_wt2_value")
|
||||
client = BossClient(signer=signer)
|
||||
headers = client._boss_headers()
|
||||
assert headers["mpt"] == "test_mpt_value"
|
||||
assert headers["wt2"] == "test_wt2_value"
|
||||
|
||||
def test_default_signer_empty_mpt_wt2(self):
|
||||
"""默认 signer(无登录)mpt/wt2 为空字符串"""
|
||||
client = BossClient()
|
||||
headers = client._boss_headers()
|
||||
assert headers["mpt"] == ""
|
||||
assert headers["wt2"] == ""
|
||||
Loading…
x
Reference in New Issue
Block a user