项目文件夹

文件
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

598 行
20 KiB
Python

"""
Comprehensive coverage tests for url_utils.normalize_url.
Focuses on exhaustive path coverage including:
- http/https scheme passthrough
- Malformed scheme correction (http: / https: without //)
- Double-slash prefix stripping
- Hostname extraction for private vs public classification
- IPv6 bracket handling
- Edge cases: empty, whitespace, None, special characters
"""
import pytest
from unittest.mock import patch
# ---------------------------------------------------------------------------
# Helper – import once per test to keep tests independent of import order
# ---------------------------------------------------------------------------
def _normalize(url):
from local_deep_research.utilities.url_utils import normalize_url
return normalize_url(url)
def _is_private(hostname):
from local_deep_research.security.network_utils import is_private_ip
return is_private_ip(hostname)
# ===================================================================
# 1. Proper scheme passthrough (early return on line 34)
# ===================================================================
class TestSchemePassthrough:
"""URLs that already have http:// or https:// are returned verbatim."""
@pytest.mark.parametrize(
"url",
[
"http://localhost",
"http://localhost:11434",
"http://127.0.0.1:8080",
"http://192.168.1.1/api/v1",
"http://[::1]:11434",
"http://example.com?q=1&r=2#frag",
"http://user:pass@host:9200/path",
],
)
def test_http_urls_returned_unchanged(self, url):
assert _normalize(url) == url
@pytest.mark.parametrize(
"url",
[
"https://example.com",
"https://api.openai.com/v1/chat",
"https://example.com:8443",
"https://example.com/search?q=hello",
"https://[2001:db8::1]:443/path",
],
)
def test_https_urls_returned_unchanged(self, url):
assert _normalize(url) == url
def test_http_triple_slash_passthrough(self):
"""http:/// still starts with http:// so the early return fires."""
assert _normalize("http:///some/path") == "http:///some/path"
def test_https_triple_slash_passthrough(self):
assert _normalize("https:///some/path") == "https:///some/path"
# ===================================================================
# 2. Malformed scheme correction (lines 38-43)
# ===================================================================
class TestMalformedScheme:
"""URLs like http:host or https:host get slashes inserted."""
def test_http_colon_hostname(self):
assert _normalize("http:example.com") == "http://example.com"
def test_https_colon_hostname(self):
assert _normalize("https:example.com") == "https://example.com"
def test_http_colon_hostname_with_port(self):
assert _normalize("http:example.com:8080") == "http://example.com:8080"
def test_https_colon_hostname_with_path(self):
assert (
_normalize("https:example.com/api/v1")
== "https://example.com/api/v1"
)
def test_http_colon_localhost(self):
assert _normalize("http:localhost:11434") == "http://localhost:11434"
def test_https_colon_ip(self):
assert _normalize("https:8.8.8.8") == "https://8.8.8.8"
def test_http_colon_private_ip(self):
assert _normalize("http:192.168.1.1:3000") == "http://192.168.1.1:3000"
def test_malformed_preserves_query(self):
assert (
_normalize("http:example.com/p?a=1") == "http://example.com/p?a=1"
)
# ===================================================================
# 3. Double-slash prefix (lines 46-48)
# ===================================================================
class TestDoubleSlashPrefix:
"""Protocol-relative URLs starting with // are stripped then re-schemed."""
def test_double_slash_public_host(self):
result = _normalize("//example.com")
assert result == "https://example.com"
def test_double_slash_public_with_port(self):
result = _normalize("//example.com:443")
assert result == "https://example.com:443"
def test_double_slash_localhost(self):
result = _normalize("//localhost:8080")
assert result == "http://localhost:8080"
def test_double_slash_private_ip(self):
result = _normalize("//192.168.1.1:9200")
assert result == "http://192.168.1.1:9200"
def test_double_slash_with_path(self):
result = _normalize("//example.com/api/v1")
assert result == "https://example.com/api/v1"
def test_double_slash_127(self):
result = _normalize("//127.0.0.1:11434")
assert result == "http://127.0.0.1:11434"
# ===================================================================
# 4. Localhost / loopback addresses
# ===================================================================
class TestLocalhost:
"""localhost and 127.0.0.1 should always get http://."""
def test_localhost_bare(self):
assert _normalize("localhost") == "http://localhost"
def test_localhost_with_port(self):
assert _normalize("localhost:11434") == "http://localhost:11434"
def test_localhost_with_path(self):
assert _normalize("localhost:8080/api") == "http://localhost:8080/api"
def test_127_0_0_1_bare(self):
assert _normalize("127.0.0.1") == "http://127.0.0.1"
def test_127_0_0_1_with_port(self):
assert _normalize("127.0.0.1:11434") == "http://127.0.0.1:11434"
def test_0_0_0_0(self):
assert _normalize("0.0.0.0") == "http://0.0.0.0"
def test_0_0_0_0_with_port(self):
assert _normalize("0.0.0.0:5000") == "http://0.0.0.0:5000"
# ===================================================================
# 5. RFC 1918 private IP ranges
# ===================================================================
class TestPrivateIPs:
"""Private IPs (10.x, 172.16-31.x, 192.168.x) should get http://."""
@pytest.mark.parametrize(
"ip",
[
"10.0.0.1",
"10.255.255.255",
"172.16.0.1",
"172.31.255.255",
"192.168.0.1",
"192.168.255.254",
],
)
def test_private_ip_gets_http(self, ip):
result = _normalize(ip)
assert result == f"http://{ip}"
@pytest.mark.parametrize(
"ip",
[
"10.0.0.1:8080",
"172.16.0.50:3000",
"192.168.1.100:8888",
],
)
def test_private_ip_with_port_gets_http(self, ip):
result = _normalize(ip)
assert result == f"http://{ip}"
# ===================================================================
# 6. Public IPs and hostnames -> https
# ===================================================================
class TestPublicHosts:
"""Public IPs and hostnames should get https://."""
@pytest.mark.parametrize(
"host",
[
"8.8.8.8",
"1.1.1.1",
"example.com",
"api.openai.com",
"sub.domain.example.org",
],
)
def test_public_host_gets_https(self, host):
result = _normalize(host)
assert result == f"https://{host}"
def test_public_ip_with_port(self):
assert _normalize("8.8.8.8:53") == "https://8.8.8.8:53"
def test_public_hostname_with_port(self):
assert _normalize("example.com:443") == "https://example.com:443"
# ===================================================================
# 7. IPv6 addresses
# ===================================================================
class TestIPv6:
"""IPv6 addresses in brackets."""
def test_ipv6_loopback_gets_http(self):
assert _normalize("[::1]") == "http://[::1]"
def test_ipv6_loopback_with_port(self):
assert _normalize("[::1]:8080") == "http://[::1]:8080"
def test_ipv6_link_local_gets_http(self):
result = _normalize("[fe80::1]:8080")
assert result.startswith("http://")
def test_ipv6_private_fc00_gets_http(self):
result = _normalize("[fc00::1]:9200")
assert result.startswith("http://")
def test_ipv6_public_gets_https(self):
result = _normalize("[2001:4860:4860::8888]")
assert result.startswith("https://")
def test_ipv6_public_with_port_gets_https(self):
result = _normalize("[2607:f8b0::1]:443")
assert result.startswith("https://")
assert "[2607:f8b0::1]" in result
def test_ipv6_with_existing_http_scheme(self):
assert _normalize("http://[::1]:11434") == "http://[::1]:11434"
def test_ipv6_with_existing_https_scheme(self):
url = "https://[2001:db8::1]:443"
assert _normalize(url) == url
# ===================================================================
# 8. .local mDNS domains
# ===================================================================
class TestLocalDomains:
"""mDNS .local domains should be treated as private."""
def test_dot_local_gets_http(self):
assert _normalize("myserver.local") == "http://myserver.local"
def test_dot_local_with_port(self):
assert _normalize("myserver.local:8080") == "http://myserver.local:8080"
def test_dot_local_with_path(self):
assert (
_normalize("printer.local/status") == "http://printer.local/status"
)
def test_not_local_suffix(self):
"""localnet.com is NOT a .local domain."""
assert _normalize("localnet.com") == "https://localnet.com"
# ===================================================================
# 9. Link-local addresses (169.254.x.x)
# ===================================================================
class TestLinkLocal:
def test_link_local_ipv4(self):
assert _normalize("169.254.0.1") == "http://169.254.0.1"
def test_link_local_ipv4_with_port(self):
assert _normalize("169.254.1.100:3000") == "http://169.254.1.100:3000"
# ===================================================================
# 10. Private/public boundary cases
# ===================================================================
class TestBoundary:
"""Boundary values at the edges of private ranges."""
def test_172_15_is_public(self):
"""172.15.x.x is NOT in 172.16.0.0/12 private range."""
assert _normalize("172.15.255.255") == "https://172.15.255.255"
def test_172_16_is_private(self):
assert _normalize("172.16.0.0") == "http://172.16.0.0"
def test_172_31_is_private(self):
assert _normalize("172.31.255.255") == "http://172.31.255.255"
def test_172_32_is_public(self):
"""172.32.x.x is beyond the 172.16.0.0/12 range."""
assert _normalize("172.32.0.0") == "https://172.32.0.0"
def test_11_0_0_1_is_public(self):
"""11.0.0.1 is outside the 10.0.0.0/8 range."""
assert _normalize("11.0.0.1") == "https://11.0.0.1"
def test_192_167_is_public(self):
assert _normalize("192.167.1.1") == "https://192.167.1.1"
# ===================================================================
# 11. Whitespace handling
# ===================================================================
class TestWhitespace:
def test_leading_whitespace(self):
assert _normalize(" https://example.com") == "https://example.com"
def test_trailing_whitespace(self):
assert _normalize("https://example.com ") == "https://example.com"
def test_both_whitespace(self):
assert _normalize(" https://example.com ") == "https://example.com"
def test_whitespace_around_bare_host(self):
assert _normalize(" localhost:8080 ") == "http://localhost:8080"
def test_tab_whitespace(self):
assert _normalize("\thttps://example.com\t") == "https://example.com"
def test_whitespace_only_returns_bare_scheme(self):
"""Whitespace-only passes initial truthy check, strips to empty."""
# After strip, raw_url is "" which is falsy but the check is before strip
# Actually: " " is truthy, strip makes it "", then no early return,
# hostname becomes "" which is_private_ip returns False -> https://
result = _normalize(" ")
assert result == "https://"
# ===================================================================
# 12. Empty / None / invalid input
# ===================================================================
class TestInvalidInput:
def test_empty_string_raises(self):
with pytest.raises(ValueError, match="(?i)empty"):
_normalize("")
def test_none_raises(self):
with pytest.raises((ValueError, TypeError, AttributeError)):
_normalize(None)
# ===================================================================
# 13. URL preservation (paths, query strings, fragments, userinfo)
# ===================================================================
class TestUrlPreservation:
"""Everything after the host should be preserved intact."""
def test_path_preserved(self):
assert (
_normalize("http://example.com/a/b/c") == "http://example.com/a/b/c"
)
def test_query_preserved(self):
assert (
_normalize("http://example.com?a=1&b=2")
== "http://example.com?a=1&b=2"
)
def test_fragment_preserved(self):
assert (
_normalize("http://example.com/p#sec") == "http://example.com/p#sec"
)
def test_encoded_chars_preserved(self):
url = "http://example.com/path%20with%20spaces"
assert _normalize(url) == url
def test_userinfo_preserved(self):
url = "http://user:pass@example.com:9200/path"
assert _normalize(url) == url
def test_complex_query_preserved(self):
url = "https://example.com/search?q=hello+world&lang=en&page=2#results"
assert _normalize(url) == url
def test_path_added_to_bare_host(self):
result = _normalize("example.com/api/v1/data")
assert result == "https://example.com/api/v1/data"
# ===================================================================
# 14. is_private_ip direct tests (re-exported from url_utils)
# ===================================================================
class TestIsPrivateIpReExport:
"""Verify is_private_ip is accessible from url_utils and works."""
def test_reexport_available(self):
from local_deep_research.utilities.url_utils import is_private_ip
assert callable(is_private_ip)
def test_reexport_matches_original(self):
from local_deep_research.utilities.url_utils import (
is_private_ip as via_utils,
)
from local_deep_research.security.network_utils import (
is_private_ip as via_security,
)
for host in [
"localhost",
"127.0.0.1",
"10.0.0.1",
"8.8.8.8",
"example.com",
]:
assert via_utils(host) == via_security(host), f"Mismatch for {host}"
# ===================================================================
# 15. is_private_ip detailed tests
# ===================================================================
class TestIsPrivateIp:
"""Directly test the is_private_ip function."""
@pytest.mark.parametrize(
"hostname",
["localhost", "127.0.0.1", "[::1]", "0.0.0.0"],
)
def test_known_localhost_values(self, hostname):
assert _is_private(hostname) is True
@pytest.mark.parametrize(
"hostname",
[
"10.0.0.1",
"10.255.255.255",
"172.16.0.1",
"172.31.255.255",
"192.168.0.1",
"192.168.255.254",
],
)
def test_rfc1918_private(self, hostname):
assert _is_private(hostname) is True
@pytest.mark.parametrize(
"hostname",
["8.8.8.8", "1.1.1.1", "172.15.0.1", "172.32.0.1"],
)
def test_public_ips(self, hostname):
assert _is_private(hostname) is False
def test_ipv6_loopback_bracketed(self):
assert _is_private("[::1]") is True
def test_ipv6_link_local(self):
assert _is_private("fe80::1") is True
def test_ipv6_private_fc00(self):
assert _is_private("fc00::1") is True
def test_ipv6_public(self):
assert _is_private("2001:4860:4860::8888") is False
def test_dot_local_domain(self):
assert _is_private("printer.local") is True
def test_non_local_domain(self):
assert _is_private("example.com") is False
def test_empty_string(self):
assert _is_private("") is False
def test_bracketed_ipv6_private(self):
"""Bracketed fc00:: should be private."""
assert _is_private("[fc00::1]") is True
def test_169_254_link_local(self):
assert _is_private("169.254.1.1") is True
# ===================================================================
# 16. Real-world usage patterns
# ===================================================================
class TestRealWorldPatterns:
"""URLs commonly seen in local-deep-research configuration."""
def test_ollama_default(self):
assert _normalize("localhost:11434") == "http://localhost:11434"
def test_ollama_with_path(self):
assert (
_normalize("localhost:11434/api/generate")
== "http://localhost:11434/api/generate"
)
def test_searxng_local(self):
assert _normalize("192.168.1.100:8888") == "http://192.168.1.100:8888"
def test_docker_internal(self):
assert _normalize("172.17.0.2:5000") == "http://172.17.0.2:5000"
def test_openai_api(self):
assert _normalize("api.openai.com") == "https://api.openai.com"
def test_already_correct_ollama(self):
assert _normalize("http://localhost:11434") == "http://localhost:11434"
def test_already_correct_openai(self):
url = "https://api.openai.com/v1/chat/completions"
assert _normalize(url) == url
def test_elasticsearch_local(self):
assert _normalize("localhost:9200") == "http://localhost:9200"
def test_vllm_local(self):
assert _normalize("0.0.0.0:8000") == "http://0.0.0.0:8000"
# ===================================================================
# 17. Mock-based tests for explicit branch coverage
# ===================================================================
class TestWithMockedPrivateIp:
"""Use mocks to guarantee we hit both branches of the scheme decision."""
def test_private_branch_returns_http(self):
with patch(
"local_deep_research.utilities.url_utils.is_private_ip",
return_value=True,
):
assert _normalize("anyhost:1234") == "http://anyhost:1234"
def test_public_branch_returns_https(self):
with patch(
"local_deep_research.utilities.url_utils.is_private_ip",
return_value=False,
):
assert _normalize("anyhost:1234") == "https://anyhost:1234"
def test_double_slash_private_branch(self):
with patch(
"local_deep_research.utilities.url_utils.is_private_ip",
return_value=True,
):
assert _normalize("//somehost") == "http://somehost"
def test_double_slash_public_branch(self):
with patch(
"local_deep_research.utilities.url_utils.is_private_ip",
return_value=False,
):
assert _normalize("//somehost") == "https://somehost"
def test_ipv6_bracket_extraction_calls_is_private_with_brackets(self):
"""When a bracketed IPv6 is found, the hostname includes brackets."""
with patch(
"local_deep_research.utilities.url_utils.is_private_ip",
return_value=True,
) as mock_ip:
_normalize("[::1]:8080")
mock_ip.assert_called_once_with("[::1]")
def test_hostname_extraction_splits_on_colon(self):
"""Hostname is extracted before : for non-IPv6."""
with patch(
"local_deep_research.utilities.url_utils.is_private_ip",
return_value=False,
) as mock_ip:
_normalize("myhost:9999")
mock_ip.assert_called_once_with("myhost")
def test_hostname_extraction_splits_on_slash(self):
"""Hostname is extracted before / when no port."""
with patch(
"local_deep_research.utilities.url_utils.is_private_ip",
return_value=False,
) as mock_ip:
_normalize("myhost/some/path")
mock_ip.assert_called_once_with("myhost")