e152b8ea05
- Add StealthConfig Pydantic model with all playwright-stealth options - Add apply_stealth_sync/apply_stealth_async functions for browser context - Add get_default_stealth_config convenience function - Add 9 unit tests covering config defaults, stealth application, and disabled mode - Update conftest.py with stealth_config and mock_browser_context fixtures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
138 lines
5.0 KiB
Python
138 lines
5.0 KiB
Python
"""Tests for stealth fingerprint masking module."""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch, call
|
|
|
|
|
|
class TestStealthConfig:
|
|
"""Tests for StealthConfig class."""
|
|
|
|
def test_stealth_config_defaults(self):
|
|
"""Test that default StealthConfig has all options enabled."""
|
|
from kb_workflow.crawler.stealth import StealthConfig
|
|
|
|
config = StealthConfig()
|
|
|
|
assert config.stealth_enabled is True
|
|
# STEALTH-01: navigator.webdriver hiding
|
|
assert config.navigator_webdriver is True
|
|
# STEALTH-04: User-Agent modification
|
|
assert config.navigator_user_agent is True
|
|
# STEALTH-03: Chrome properties
|
|
assert config.chrome_app is True
|
|
assert config.chrome_csi is True
|
|
assert config.chrome_load_times is True
|
|
# Other important options
|
|
assert config.navigator_plugins is True
|
|
assert config.webgl_vendor is True
|
|
|
|
def test_stealth_config_disabled(self):
|
|
"""Test that stealth can be disabled."""
|
|
from kb_workflow.crawler.stealth import StealthConfig
|
|
|
|
config = StealthConfig(stealth_enabled=False)
|
|
assert config.stealth_enabled is False
|
|
|
|
def test_create_stealth_returns_stealth_instance(self):
|
|
"""Test that create_stealth() returns configured Stealth."""
|
|
from kb_workflow.crawler.stealth import StealthConfig
|
|
from playwright_stealth import Stealth
|
|
|
|
config = StealthConfig()
|
|
stealth = config.create_stealth()
|
|
|
|
assert isinstance(stealth, Stealth)
|
|
|
|
def test_stealth_options_mapping(self):
|
|
"""Test that StealthConfig options map correctly to Stealth."""
|
|
from kb_workflow.crawler.stealth import StealthConfig
|
|
|
|
config = StealthConfig(
|
|
navigator_webdriver=True,
|
|
navigator_user_agent=False, # Disable one option
|
|
chrome_app=True,
|
|
)
|
|
|
|
stealth = config.create_stealth()
|
|
|
|
# Verify the Stealth object was created with correct options
|
|
# The Stealth class stores these as attributes
|
|
assert stealth.navigator_webdriver is True
|
|
assert stealth.navigator_user_agent is False
|
|
|
|
|
|
class TestApplyStealth:
|
|
"""Tests for apply_stealth functions."""
|
|
|
|
def test_apply_stealth_sync_calls_library(self, mock_browser_context):
|
|
"""Test that apply_stealth_sync calls playwright-stealth correctly."""
|
|
from kb_workflow.crawler.stealth import apply_stealth_sync, StealthConfig
|
|
|
|
config = StealthConfig()
|
|
|
|
with patch('kb_workflow.crawler.stealth.stealth_config.Stealth') as mock_stealth_class:
|
|
mock_stealth_instance = MagicMock()
|
|
mock_stealth_class.return_value = mock_stealth_instance
|
|
|
|
apply_stealth_sync(mock_browser_context, config)
|
|
|
|
# Verify Stealth was created with config options
|
|
mock_stealth_class.assert_called_once()
|
|
# Verify apply_stealth_sync was called on the Stealth instance
|
|
mock_stealth_instance.apply_stealth_sync.assert_called_once_with(
|
|
mock_browser_context
|
|
)
|
|
|
|
def test_apply_stealth_sync_disabled_skips_stealth(self, mock_browser_context):
|
|
"""Test that stealth is skipped when disabled."""
|
|
from kb_workflow.crawler.stealth import apply_stealth_sync, StealthConfig
|
|
|
|
config = StealthConfig(stealth_enabled=False)
|
|
|
|
with patch('kb_workflow.crawler.stealth.stealth_config.Stealth') as mock_stealth_class:
|
|
apply_stealth_sync(mock_browser_context, config)
|
|
|
|
# Stealth should not be created when disabled
|
|
mock_stealth_class.assert_not_called()
|
|
|
|
def test_apply_stealth_sync_default_config(self, mock_browser_context):
|
|
"""Test that apply_stealth_sync works with default config."""
|
|
from kb_workflow.crawler.stealth import apply_stealth_sync
|
|
|
|
with patch('kb_workflow.crawler.stealth.stealth_config.Stealth') as mock_stealth_class:
|
|
mock_stealth_instance = MagicMock()
|
|
mock_stealth_class.return_value = mock_stealth_instance
|
|
|
|
# Should work without explicit config
|
|
apply_stealth_sync(mock_browser_context)
|
|
|
|
mock_stealth_class.assert_called_once()
|
|
|
|
|
|
class TestGetDefaultStealthConfig:
|
|
"""Tests for get_default_stealth_config function."""
|
|
|
|
def test_get_default_stealth_config(self):
|
|
"""Test that get_default_stealth_config returns StealthConfig."""
|
|
from kb_workflow.crawler.stealth import get_default_stealth_config
|
|
|
|
config = get_default_stealth_config()
|
|
|
|
assert config.stealth_enabled is True
|
|
assert isinstance(config, type(config)) # Is a StealthConfig
|
|
|
|
def test_get_default_stealth_config_is_fresh_instance(self):
|
|
"""Test that each call returns a new instance."""
|
|
from kb_workflow.crawler.stealth import get_default_stealth_config
|
|
|
|
config1 = get_default_stealth_config()
|
|
config2 = get_default_stealth_config()
|
|
|
|
# Should be equal but not the same object
|
|
assert config1 == config2
|
|
assert config1 is not config2
|
|
|
|
|
|
# Wave 0 completion marker
|
|
WAVE_0_COMPLETE = True
|