job51 (spiderJobs/platforms/job51/): - client.py: HTTPClient+Job51Sign from crawler_core - api.py: ApiResult→Result, self._http→self.http_client, _request() POST overrides - main.py: BaseFetcher/BaseSearcher from crawler_core - sign.py: backward-compatible stub re-exporting crawler_core.qcwy.sign.Job51Sign zhilian (spiderJobs/platforms/zhilian/): - client.py: HTTPClient+ZhilianSign from crawler_core - api.py: add _parse_zhilian_response (HTTP 200=success), add _parse()/_request() to all classes (GET fetchers + POST searcher overrides) - main.py: BaseFetcher/BaseSearcher from crawler_core - sign.py: backward-compatible stub re-exporting crawler_core.zhilian.sign.ZhilianSign tests: 34 new mock tests (17 job51 + 17 zhilian) Full regression: 98 passed (job51:17 + zhilian:17 + boss:22 + crawler_core:41 + 1)
199 lines
8.2 KiB
Python
199 lines
8.2 KiB
Python
"""
|
||
智联招聘 HTTP 层 mock 测试(ARCH-05 / QUAL-03)
|
||
|
||
使用 unittest.mock.MagicMock 替代真实 HTTP 客户端,无网络依赖。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from unittest.mock import MagicMock
|
||
|
||
from crawler_core.zhilian.sign import ZhilianSign
|
||
from spiderJobs.platforms.zhilian.api import (
|
||
GetCompanyDetail,
|
||
GetCompanyExtDetail,
|
||
GetPositionDetail,
|
||
SearchCompanyPositions,
|
||
SearchPositions,
|
||
)
|
||
from spiderJobs.platforms.zhilian.client import ZhilianClient
|
||
from crawler_core.base import Result
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 1. SearchPositions(POST cgate)
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestSearchPositions:
|
||
|
||
def _make_client(self, status_code=200, data=None):
|
||
mock_client = MagicMock()
|
||
mock_client.post.return_value = (status_code, data or {})
|
||
return mock_client
|
||
|
||
def test_search_success_returns_list(self):
|
||
data = {
|
||
"data": {
|
||
"list": [{"number": "CC123", "name": "Python 工程师"}],
|
||
"numFound": 1,
|
||
},
|
||
}
|
||
searcher = SearchPositions(
|
||
keyword="Python", city_code=538,
|
||
client=self._make_client(200, data),
|
||
)
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is True
|
||
|
||
def test_search_http_403(self):
|
||
searcher = SearchPositions(client=self._make_client(403, {}))
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is False
|
||
assert result.status_code == 403
|
||
|
||
def test_search_http_500(self):
|
||
searcher = SearchPositions(client=self._make_client(500, {}))
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is False
|
||
|
||
def test_search_builds_keyword_param(self):
|
||
mock_client = MagicMock()
|
||
mock_client.post.return_value = (200, {"data": {"list": []}})
|
||
searcher = SearchPositions(keyword="Java", city_code=530, client=mock_client)
|
||
searcher.search(page_index=1)
|
||
assert mock_client.post.called
|
||
call_kwargs = mock_client.post.call_args
|
||
body = call_kwargs[0][1] if len(call_kwargs[0]) > 1 else None
|
||
if body:
|
||
assert "Java" in str(body)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 2. GetPositionDetail(GET cgate)
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestGetPositionDetail:
|
||
|
||
def test_fetch_success(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (200, {
|
||
"data": {"number": "CC123", "jobName": "高级工程师"},
|
||
})
|
||
fetcher = GetPositionDetail(number="CC123", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is True
|
||
|
||
def test_fetch_404(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (404, {})
|
||
fetcher = GetPositionDetail(number="notexist", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is False
|
||
assert result.status_code == 404
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 3. GetCompanyExtDetail(GET cgate)
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestGetCompanyExtDetail:
|
||
|
||
def test_fetch_success(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (200, {
|
||
"data": {"companyName": "智联测试公司"},
|
||
})
|
||
fetcher = GetCompanyExtDetail(
|
||
company_name="智联测试公司",
|
||
company_number="CZ123",
|
||
client=mock_client,
|
||
)
|
||
result = fetcher.fetch()
|
||
assert result.success is True
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 4. GetCompanyDetail(GET cgate)
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestGetCompanyDetail:
|
||
|
||
def test_fetch_success(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (200, {
|
||
"data": {"companyNumber": "CZ123", "name": "智联公司"},
|
||
})
|
||
fetcher = GetCompanyDetail(number="CZ123", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is True
|
||
|
||
def test_fetch_http_error(self):
|
||
mock_client = MagicMock()
|
||
mock_client.get.return_value = (500, {})
|
||
fetcher = GetCompanyDetail(number="CZ123", client=mock_client)
|
||
result = fetcher.fetch()
|
||
assert result.success is False
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 5. SearchCompanyPositions(GET capi)— 验证 sign_params 被调用
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestSearchCompanyPositions:
|
||
|
||
def test_search_success_calls_sign_params(self):
|
||
mock_signer = MagicMock(spec=ZhilianSign)
|
||
mock_signer.sign_params.return_value = {"at": "", "rt": ""}
|
||
mock_client = MagicMock()
|
||
mock_client.signer = mock_signer
|
||
mock_client.get.return_value = (200, {
|
||
"data": {"list": [{"jobName": "测试岗位"}]},
|
||
"pageInfo": {},
|
||
})
|
||
searcher = SearchCompanyPositions(company_id="CZ123", client=mock_client)
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is True
|
||
assert mock_signer.sign_params.called # 确认 sign_params 被调用
|
||
|
||
def test_search_http_error(self):
|
||
mock_signer = MagicMock(spec=ZhilianSign)
|
||
mock_signer.sign_params.return_value = {}
|
||
mock_client = MagicMock()
|
||
mock_client.signer = mock_signer
|
||
mock_client.get.return_value = (403, {})
|
||
searcher = SearchCompanyPositions(company_id="CZ123", client=mock_client)
|
||
result = searcher.search(page_index=1)
|
||
assert result.success is False
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 6. ZhilianClient — 签名头注入
|
||
# ─────────────────────────────────────────────────────────
|
||
|
||
class TestZhilianClientHeaders:
|
||
|
||
def test_sign_headers_injects_at_rt(self):
|
||
signer = ZhilianSign(at="test_at", rt="test_rt")
|
||
client = ZhilianClient(signer=signer)
|
||
headers = client.signer.sign_headers()
|
||
assert headers["x-zp-at"] == "test_at"
|
||
assert headers["x-zp-rt"] == "test_rt"
|
||
|
||
def test_sign_headers_has_required_keys(self):
|
||
client = ZhilianClient()
|
||
headers = client.signer.sign_headers()
|
||
for key in ["x-zp-at", "x-zp-rt", "x-zp-action-id", "x-zp-device-id"]:
|
||
assert key in headers, f"缺少头信息: {key}"
|
||
|
||
def test_default_signer_empty_tokens(self):
|
||
client = ZhilianClient()
|
||
headers = client.signer.sign_headers()
|
||
assert headers["x-zp-at"] == ""
|
||
assert headers["x-zp-rt"] == ""
|
||
|
||
def test_sign_params_has_required_keys(self):
|
||
client = ZhilianClient()
|
||
params = client.signer.sign_params()
|
||
for key in ["at", "rt", "channel", "platform", "version", "d"]:
|
||
assert key in params, f"缺少签名参数: {key}"
|