feat(08-03): create CrawlerConfig for proxy and rate limiting

- Add CrawlerConfig with ProxyConfig, delay, and retry settings
- Support HTTP_PROXY/HTTPS_PROXY environment variable parsing
- Add proxy_required validation with clear error message
- Add get_crawler_config singleton for configuration access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 21:57:09 +08:00
parent f41e75a1b2
commit 626fb73a58
2 changed files with 266 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
"""Tests for Crawler Proxy Configuration"""
import os
import pytest
from unittest.mock import patch
from kb_workflow.crawler.config import (
CrawlerConfig,
ProxyConfig,
get_crawler_config,
reset_crawler_config,
)
class TestProxyConfig:
"""Tests for ProxyConfig model."""
def test_parse_proxy_from_https_env(self):
"""Parse proxy from HTTPS_PROXY environment variable."""
reset_crawler_config()
with patch.dict(os.environ, {"HTTPS_PROXY": "http://127.0.0.1:7890"}, clear=False):
os.environ.pop("HTTP_PROXY", None)
config = CrawlerConfig.from_env()
assert config.proxy is not None
assert config.proxy.server == "http://127.0.0.1:7890"
def test_parse_proxy_from_http_env(self):
"""Parse proxy from HTTP_PROXY (HTTPS_PROXY takes priority)."""
reset_crawler_config()
with patch.dict(os.environ, {
"HTTP_PROXY": "http://127.0.0.1:8080",
"HTTPS_PROXY": "http://127.0.0.1:7890"
}, clear=False):
config = CrawlerConfig.from_env()
assert config.proxy.server == "http://127.0.0.1:7890"
def test_parse_proxy_with_auth(self):
"""Parse proxy with authentication credentials."""
reset_crawler_config()
with patch.dict(os.environ, {
"HTTPS_PROXY": "http://user:pass@proxy.example.com:8080"
}, clear=False):
os.environ.pop("HTTP_PROXY", None)
config = CrawlerConfig.from_env()
assert config.proxy.username == "user"
assert config.proxy.password == "pass"
assert config.proxy.server == "http://proxy.example.com:8080"
def test_no_proxy_configured(self):
"""Handle case when no proxy is configured."""
reset_crawler_config()
# Save current values
old_http = os.environ.get("HTTP_PROXY")
old_https = os.environ.get("HTTPS_PROXY")
try:
os.environ.pop("HTTP_PROXY", None)
os.environ.pop("HTTPS_PROXY", None)
config = CrawlerConfig.from_env()
assert config.proxy is None
finally:
# Restore
if old_http:
os.environ["HTTP_PROXY"] = old_http
if old_https:
os.environ["HTTPS_PROXY"] = old_https
def test_proxy_required_validation_fails(self):
"""Raise ValueError when proxy required but not configured."""
config = CrawlerConfig(proxy=None, proxy_required=True)
with pytest.raises(ValueError, match="Proxy configuration missing"):
config.validate_proxy()
def test_proxy_required_validation_passes(self):
"""Pass validation when proxy is configured and required."""
config = CrawlerConfig(
proxy=ProxyConfig(server="http://127.0.0.1:7890"),
proxy_required=True
)
# Should not raise exception
config.validate_proxy()
def test_proxy_not_required(self):
"""Pass validation when proxy is not required."""
config = CrawlerConfig(proxy=None, proxy_required=False)
# Should not raise exception
config.validate_proxy()
def test_to_playwright_format(self):
"""Convert to Playwright proxy format."""
proxy = ProxyConfig(
server="http://127.0.0.1:7890",
username="user",
password="pass"
)
result = proxy.to_playwright_format()
assert result == {
"server": "http://127.0.0.1:7890",
"username": "user",
"password": "pass"
}
def test_to_playwright_format_no_auth(self):
"""Convert to Playwright format without authentication."""
proxy = ProxyConfig(server="http://127.0.0.1:7890")
result = proxy.to_playwright_format()
assert result == {"server": "http://127.0.0.1:7890"}
def test_get_crawler_config_singleton(self):
"""Test that get_crawler_config returns cached instance."""
reset_crawler_config()
with patch.dict(os.environ, {"HTTPS_PROXY": "http://127.0.0.1:7890"}, clear=False):
os.environ.pop("HTTP_PROXY", None)
config1 = get_crawler_config()
config2 = get_crawler_config()
assert config1 is config2
def test_default_values(self):
"""Test default configuration values."""
config = CrawlerConfig(proxy=None, proxy_required=False)
assert config.min_delay == 1.0
assert config.max_delay == 3.0
assert config.max_retries == 3
assert config.retry_min_wait == 2.0
assert config.retry_max_wait == 10.0