"""Unit tests for crawler_core.zhilian.sign — ZhilianSign. All tests are pure function assertions: no HTTP, no network, no mocks. """ import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) import re import pytest from crawler_core.zhilian.sign import ZhilianSign EXPECTED_HEADER_KEYS = { "x-zp-at", "x-zp-rt", "x-zp-action-id", "x-zp-page-code", "x-zp-version", "x-zp-channel", "x-zp-platform", "x-zp-device-id", "x-zp-business-system", } EXPECTED_PARAM_KEYS = {"at", "rt", "channel", "platform", "version", "d"} class TestZhilianSignInit: def test_defaults(self): sign = ZhilianSign() assert sign.at == "" assert sign.rt == "" assert sign.version == "4.1.259" assert sign.channel == "wxxiaochengxu" assert sign.platform == "12" assert sign.device_id # auto-generated, not empty def test_custom_tokens(self): sign = ZhilianSign(at="at_token", rt="rt_token") assert sign.at == "at_token" assert sign.rt == "rt_token" def test_custom_device_id(self): sign = ZhilianSign(device_id="CUSTOM-DEVICE-ID") assert sign.device_id == "CUSTOM-DEVICE-ID" def test_auto_device_id_is_uuid4_format(self): sign = ZhilianSign() uuid_pattern = r'^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$' assert re.match(uuid_pattern, sign.device_id), \ f"device_id not UUID4 format: {sign.device_id}" class TestZhilianSignHeaders: def setup_method(self): self.sign = ZhilianSign(at="at123", rt="rt456") def test_keys_exactly_nine(self): headers = self.sign.sign_headers() assert set(headers.keys()) == EXPECTED_HEADER_KEYS, \ f"Header keys wrong: {set(headers.keys())}" def test_business_system_is_73(self): headers = self.sign.sign_headers() assert headers["x-zp-business-system"] == "73" def test_tokens_reflected(self): headers = self.sign.sign_headers() assert headers["x-zp-at"] == "at123" assert headers["x-zp-rt"] == "rt456" def test_action_id_is_uuid4_format(self): headers = self.sign.sign_headers() action_id = headers["x-zp-action-id"] uuid_pattern = r'^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$' assert re.match(uuid_pattern, action_id), \ f"action_id not UUID4 format: {action_id}" def test_action_id_unique_per_call(self): h1 = self.sign.sign_headers() h2 = self.sign.sign_headers() assert h1["x-zp-action-id"] != h2["x-zp-action-id"], \ "action_id must be freshly generated on each call" def test_device_id_in_headers(self): headers = self.sign.sign_headers() assert headers["x-zp-device-id"] == self.sign.device_id class TestZhilianSignParams: def setup_method(self): self.sign = ZhilianSign(at="at789", rt="rt012", device_id="DEV-ID") def test_keys_exactly_six(self): params = self.sign.sign_params() assert set(params.keys()) == EXPECTED_PARAM_KEYS, \ f"Param keys wrong: {set(params.keys())}" def test_device_id_as_d(self): params = self.sign.sign_params() assert params["d"] == "DEV-ID" def test_tokens_reflected(self): params = self.sign.sign_params() assert params["at"] == "at789" assert params["rt"] == "rt012" class TestZhilianGenerateUuid: def test_uuid4_format(self): uuid = ZhilianSign.generate_uuid() uuid_pattern = r'^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$' assert re.match(uuid_pattern, uuid), \ f"UUID not UUID4 format: {uuid}" def test_uuid_length(self): uuid = ZhilianSign.generate_uuid() assert len(uuid) == 36, f"Expected 36 chars, got {len(uuid)}" def test_uuid_version_4(self): uuid = ZhilianSign.generate_uuid() assert uuid[14] == "4", f"Version digit should be 4, got: {uuid[14]}"