JobData/tests/zhilian/test_zhilian_client.py
win 8c2c2d29d7 feat(03): migrate job51+zhilian to crawler_core (ARCH-04/05)
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)
2026-03-21 19:18:22 +08:00

199 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
智联招聘 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. SearchPositionsPOST 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. GetPositionDetailGET 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. GetCompanyExtDetailGET 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. GetCompanyDetailGET 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. SearchCompanyPositionsGET 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}"