diff --git a/tests/test_crawler/test_zhihu_crawler_stealth.py b/tests/test_crawler/test_zhihu_crawler_stealth.py new file mode 100644 index 0000000..98b985d --- /dev/null +++ b/tests/test_crawler/test_zhihu_crawler_stealth.py @@ -0,0 +1,165 @@ +"""Integration tests for ZhihuCrawler stealth functionality. + +Tests that stealth fingerprint masking is properly integrated +into ZhihuCrawler's browser initialization. +""" + +import pytest +from unittest.mock import MagicMock, patch, call + + +class TestZhihuCrawlerStealthIntegration: + """Tests for stealth integration in ZhihuCrawler.""" + + def test_crawler_has_stealth_config_param(self): + """Test that ZhihuCrawler accepts stealth_config parameter.""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + from kb_workflow.crawler.stealth import StealthConfig + + config = StealthConfig(stealth_enabled=False) + crawler = ZhihuCrawler(stealth_config=config) + + assert crawler._stealth_config == config + + def test_crawler_default_stealth_enabled(self): + """Test that stealth is enabled by default (None = use defaults).""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + + crawler = ZhihuCrawler() + + # None means use default stealth config (enabled) + assert crawler._stealth_config is None + + def test_crawler_can_disable_stealth(self): + """Test that stealth can be explicitly disabled.""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + from kb_workflow.crawler.stealth import StealthConfig + + disabled_config = StealthConfig(stealth_enabled=False) + crawler = ZhihuCrawler(stealth_config=disabled_config) + + assert crawler._stealth_config.stealth_enabled is False + + @patch('kb_workflow.crawler.zhihu_crawler.sync_playwright') + @patch('kb_workflow.crawler.zhihu_crawler.apply_stealth_sync') + def test_start_browser_applies_stealth( + self, + mock_apply_stealth, + mock_sync_playwright, + ): + """Test that _start_browser_with_proxy calls apply_stealth_sync.""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + + # Setup mocks + mock_playwright = MagicMock() + mock_browser = MagicMock() + mock_context = MagicMock() + mock_sync_playwright.return_value.start.return_value = mock_playwright + mock_playwright.chromium.launch.return_value = mock_browser + mock_browser.new_context.return_value = mock_context + + crawler = ZhihuCrawler() + crawler._start_browser_with_proxy(cookie="test_cookie") + + # Verify apply_stealth_sync was called + mock_apply_stealth.assert_called_once() + call_args = mock_apply_stealth.call_args + assert call_args[0][0] == mock_context # First arg is context + + @patch('kb_workflow.crawler.zhihu_crawler.sync_playwright') + @patch('kb_workflow.crawler.zhihu_crawler.apply_stealth_sync') + def test_start_browser_launch_args_include_disable_blink_features( + self, + mock_apply_stealth, + mock_sync_playwright, + ): + """Test that launch includes --disable-blink-features=AutomationControlled.""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + + # Setup mocks + mock_playwright = MagicMock() + mock_browser = MagicMock() + mock_context = MagicMock() + mock_sync_playwright.return_value.start.return_value = mock_playwright + mock_playwright.chromium.launch.return_value = mock_browser + mock_browser.new_context.return_value = mock_context + + crawler = ZhihuCrawler() + crawler._start_browser_with_proxy(cookie="test_cookie") + + # Verify launch was called with correct args + launch_call = mock_playwright.chromium.launch + launch_call.assert_called_once() + call_kwargs = launch_call.call_args[1] + + assert 'args' in call_kwargs + assert '--disable-blink-features=AutomationControlled' in call_kwargs['args'] + + @patch('kb_workflow.crawler.zhihu_crawler.sync_playwright') + @patch('kb_workflow.crawler.zhihu_crawler.apply_stealth_sync') + def test_start_browser_stealth_disabled_no_blink_features_arg( + self, + mock_apply_stealth, + mock_sync_playwright, + ): + """Test that --disable-blink-features is not added when stealth disabled.""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + from kb_workflow.crawler.stealth import StealthConfig + + # Setup mocks + mock_playwright = MagicMock() + mock_browser = MagicMock() + mock_context = MagicMock() + mock_sync_playwright.return_value.start.return_value = mock_playwright + mock_playwright.chromium.launch.return_value = mock_browser + mock_browser.new_context.return_value = mock_context + + disabled_config = StealthConfig(stealth_enabled=False) + crawler = ZhihuCrawler(stealth_config=disabled_config) + crawler._start_browser_with_proxy(cookie="test_cookie") + + # Verify launch was called without args or with empty/None args + launch_call = mock_playwright.chromium.launch + call_kwargs = launch_call.call_args[1] + + # When stealth disabled, args should be None or not contain the flag + if 'args' in call_kwargs and call_kwargs['args']: + assert '--disable-blink-features=AutomationControlled' not in call_kwargs['args'] + + # apply_stealth_sync should still be called but with disabled config + mock_apply_stealth.assert_called_once() + + +class TestZhihuCrawlerBackwardCompatibility: + """Tests for backward compatibility after stealth integration.""" + + def test_crawler_works_without_stealth_config(self): + """Test that ZhihuCrawler works without explicit stealth_config.""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + + # Should not raise any errors + crawler = ZhihuCrawler( + headless=True, + timeout=30 + ) + assert crawler is not None + + def test_crawler_accepts_all_existing_params(self): + """Test that all existing parameters still work.""" + from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler + from kb_workflow.crawler.config import CrawlerConfig + + config = CrawlerConfig() + crawler = ZhihuCrawler( + headless=False, + timeout=60, + config=config + ) + + assert crawler.headless is False + assert crawler.timeout == 60 + assert crawler.config == config + + +# Wave 0 completion marker +WAVE_0_COMPLETE = True