Files
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

589 lines
23 KiB
Python

"""
Extended tests for security/url_builder.py
Covers additional edge cases, boundary conditions, and integration
scenarios beyond the base test suite for all five public functions
and the URLBuilderError exception.
"""
import pytest
from local_deep_research.security.url_builder import (
URLBuilderError,
normalize_bind_address,
build_base_url_from_settings,
build_full_url,
validate_constructed_url,
mask_sensitive_url,
)
# ---------------------------------------------------------------------------
# normalize_bind_address -- address normalization
# ---------------------------------------------------------------------------
class TestNormalizeBindAddressExtended:
"""Extended tests for normalize_bind_address."""
def test_ipv4_bind_all_to_localhost(self):
"""0.0.0.0 is converted to localhost."""
assert normalize_bind_address("0.0.0.0") == "localhost"
def test_ipv6_bind_all_to_localhost(self):
""":: is converted to localhost."""
assert normalize_bind_address("::") == "localhost"
def test_localhost_unchanged(self):
"""localhost passes through without modification."""
assert normalize_bind_address("localhost") == "localhost"
def test_specific_ipv4_unchanged(self):
"""A specific IPv4 address is returned as-is."""
assert normalize_bind_address("192.168.1.1") == "192.168.1.1"
def test_local_hostname_unchanged(self):
"""A .local hostname is returned as-is."""
assert normalize_bind_address("myhost.local") == "myhost.local"
def test_loopback_127_unchanged(self):
"""127.0.0.1 is not converted (only 0.0.0.0 and :: are)."""
assert normalize_bind_address("127.0.0.1") == "127.0.0.1"
def test_fqdn_unchanged(self):
"""A fully-qualified domain name passes through unchanged."""
assert normalize_bind_address("app.example.com") == "app.example.com"
def test_ipv6_loopback_unchanged(self):
"""::1 (IPv6 loopback) is not treated as bind-all."""
assert normalize_bind_address("::1") == "::1"
def test_empty_string_unchanged(self):
"""Empty string is returned as-is (caller is responsible for validation)."""
assert normalize_bind_address("") == ""
def test_10_network_address_unchanged(self):
"""Private 10.x.x.x address is returned unchanged."""
assert normalize_bind_address("10.0.0.1") == "10.0.0.1"
def test_172_network_address_unchanged(self):
"""Private 172.16.x.x address is returned unchanged."""
assert normalize_bind_address("172.16.0.1") == "172.16.0.1"
# ---------------------------------------------------------------------------
# build_base_url_from_settings -- URL construction from config
# ---------------------------------------------------------------------------
class TestBuildBaseUrlFromSettingsExtended:
"""Extended tests for build_base_url_from_settings."""
def test_external_url_priority_over_host_port(self):
"""external_url takes priority even when host and port are provided."""
result = build_base_url_from_settings(
external_url="https://public.example.com",
host="192.168.1.50",
port=8080,
)
assert result == "https://public.example.com"
def test_external_url_stripped_and_trailing_slash_removed(self):
"""external_url is stripped of whitespace and trailing slash."""
result = build_base_url_from_settings(
external_url=" https://app.example.com/ "
)
assert result == "https://app.example.com"
def test_host_port_constructs_http_url(self):
"""host + port constructs an http:// URL."""
result = build_base_url_from_settings(
external_url=None,
host="myserver.local",
port=3000,
)
assert result == "http://myserver.local:3000"
def test_bind_all_host_normalized_to_localhost(self):
"""0.0.0.0 host is normalized to localhost in the URL."""
result = build_base_url_from_settings(
external_url=None,
host="0.0.0.0",
port=5000,
)
assert result == "http://localhost:5000"
def test_fallback_used_when_no_external_or_host_port(self):
"""fallback_base is used when external_url and host/port are absent."""
result = build_base_url_from_settings(
external_url=None,
host=None,
port=None,
fallback_base="http://custom-fallback:9090",
)
assert result == "http://custom-fallback:9090"
def test_fallback_trailing_slash_stripped(self):
"""Trailing slash on fallback_base is removed."""
result = build_base_url_from_settings(
external_url=None,
host=None,
port=None,
fallback_base="http://fallback.local:9090/",
)
assert result == "http://fallback.local:9090"
def test_empty_external_url_treated_as_not_provided(self):
"""Empty string external_url falls through to host/port or fallback."""
result = build_base_url_from_settings(
external_url="",
host=None,
port=None,
)
assert result == "http://localhost:5000"
def test_whitespace_only_external_url_treated_as_not_provided(self):
"""Whitespace-only external_url falls through."""
result = build_base_url_from_settings(
external_url=" ",
host=None,
port=None,
)
assert result == "http://localhost:5000"
def test_port_as_string_is_accepted(self):
"""Port provided as string is converted to int."""
result = build_base_url_from_settings(
external_url=None,
host="localhost",
port="8080",
)
assert result == "http://localhost:8080"
def test_invalid_port_raises_url_builder_error(self):
"""Non-numeric port string raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="Failed to build base URL"):
build_base_url_from_settings(
external_url=None,
host="localhost",
port="abc",
)
def test_ipv6_bind_all_host_normalized(self):
""":: host is normalized to localhost in the URL."""
result = build_base_url_from_settings(
external_url=None,
host="::",
port=5000,
)
assert result == "http://localhost:5000"
def test_external_url_with_multiple_trailing_slashes(self):
"""Multiple trailing slashes are stripped from external_url."""
result = build_base_url_from_settings(
external_url="https://example.com///"
)
assert result == "https://example.com"
def test_external_url_with_path(self):
"""external_url that includes a path keeps the path (minus trailing /)."""
result = build_base_url_from_settings(
external_url="https://example.com/app/"
)
assert result == "https://example.com/app"
def test_default_fallback_value(self):
"""Default fallback_base is http://localhost:5000."""
result = build_base_url_from_settings()
assert result == "http://localhost:5000"
def test_host_without_port_uses_fallback(self):
"""If host is given but port is None, fallback is used."""
result = build_base_url_from_settings(
external_url=None,
host="somehost",
port=None,
fallback_base="http://default:7777",
)
assert result == "http://default:7777"
def test_port_without_host_uses_fallback(self):
"""If port is given but host is None, fallback is used."""
result = build_base_url_from_settings(
external_url=None,
host=None,
port=5000,
fallback_base="http://default:7777",
)
assert result == "http://default:7777"
# ---------------------------------------------------------------------------
# build_full_url -- base + path combination
# ---------------------------------------------------------------------------
class TestBuildFullUrlExtended:
"""Extended tests for build_full_url."""
def test_combines_base_and_path(self):
"""Base URL and path are joined correctly."""
result = build_full_url("https://example.com", "/api/v1", validate=True)
assert result == "https://example.com/api/v1"
def test_trailing_slash_removed_from_base(self):
"""Trailing slash on base_url is stripped before joining."""
result = build_full_url(
"https://example.com/", "/endpoint", validate=True
)
assert result == "https://example.com/endpoint"
def test_leading_slash_added_to_path(self):
"""Path without leading slash gets one prepended."""
result = build_full_url(
"https://example.com", "path/here", validate=True
)
assert result == "https://example.com/path/here"
def test_validate_true_raises_on_invalid_url(self):
"""validate=True raises URLBuilderError for a schemeless base."""
with pytest.raises(URLBuilderError):
build_full_url("not-a-url", "/path", validate=True)
def test_validate_false_skips_validation(self):
"""validate=False allows any string combination through."""
result = build_full_url("no-scheme", "/path", validate=False)
assert result == "no-scheme/path"
def test_allowed_schemes_rejects_wrong_scheme(self):
"""URL with disallowed scheme raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="not in allowed schemes"):
build_full_url(
"ftp://files.example.com",
"/data",
validate=True,
allowed_schemes=["http", "https"],
)
def test_allowed_schemes_accepts_correct_scheme(self):
"""URL with allowed scheme passes validation."""
result = build_full_url(
"https://example.com",
"/secure",
validate=True,
allowed_schemes=["https"],
)
assert result == "https://example.com/secure"
def test_path_already_has_leading_slash(self):
"""Path that already starts with / is not double-slashed."""
result = build_full_url(
"https://example.com", "/already/slashed", validate=True
)
assert "//already" not in result
assert result == "https://example.com/already/slashed"
def test_base_url_with_path_component(self):
"""Base URL that includes a path appends new path correctly."""
result = build_full_url(
"https://example.com/app", "/api/test", validate=False
)
assert result == "https://example.com/app/api/test"
def test_empty_path_gets_leading_slash(self):
"""Empty path results in trailing slash after base."""
result = build_full_url("https://example.com", "", validate=False)
assert result == "https://example.com/"
def test_path_with_query_string(self):
"""Path containing query string is appended as-is."""
result = build_full_url(
"https://example.com", "/search?q=test", validate=True
)
assert result == "https://example.com/search?q=test"
def test_path_with_fragment(self):
"""Path containing fragment is appended as-is."""
result = build_full_url(
"https://example.com", "/page#section", validate=True
)
assert result == "https://example.com/page#section"
# ---------------------------------------------------------------------------
# validate_constructed_url -- URL validation
# ---------------------------------------------------------------------------
class TestValidateConstructedUrlExtended:
"""Extended tests for validate_constructed_url."""
def test_empty_string_raises(self):
"""Empty string raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="non-empty string"):
validate_constructed_url("")
def test_none_raises(self):
"""None raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="non-empty string"):
validate_constructed_url(None)
def test_no_scheme_raises(self):
"""URL without scheme raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="scheme"):
validate_constructed_url("example.com/path")
def test_no_hostname_raises(self):
"""URL without hostname (empty netloc) raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="hostname"):
validate_constructed_url("http:///path/only")
def test_valid_http_url_returns_true(self):
"""Valid HTTP URL returns True."""
assert validate_constructed_url("http://example.com/api") is True
def test_valid_https_url_returns_true(self):
"""Valid HTTPS URL returns True."""
assert validate_constructed_url("https://secure.example.com") is True
def test_wrong_scheme_with_allowed_schemes_raises(self):
"""Scheme not in allowed_schemes list raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="not in allowed schemes"):
validate_constructed_url(
"http://example.com", allowed_schemes=["https"]
)
def test_correct_scheme_with_allowed_schemes_passes(self):
"""Scheme in allowed_schemes list passes."""
assert (
validate_constructed_url(
"https://example.com", allowed_schemes=["https"]
)
is True
)
def test_none_allowed_schemes_accepts_any(self):
"""None for allowed_schemes accepts any scheme."""
assert (
validate_constructed_url(
"custom://something.local", allowed_schemes=None
)
is True
)
def test_ftp_scheme_with_ftp_allowed(self):
"""ftp:// passes when ftp is in allowed_schemes."""
assert (
validate_constructed_url(
"ftp://files.example.com/data",
allowed_schemes=["ftp", "sftp"],
)
is True
)
def test_url_with_port(self):
"""URL with port validates successfully."""
assert validate_constructed_url("https://example.com:8443/api") is True
def test_url_with_auth_info(self):
"""URL containing user:pass@host validates (netloc is present)."""
assert (
validate_constructed_url("https://user:pass@example.com/path")
is True
)
def test_integer_input_raises(self):
"""Non-string input raises URLBuilderError."""
with pytest.raises(URLBuilderError, match="non-empty string"):
validate_constructed_url(42)
def test_url_with_query_and_fragment(self):
"""URL with query string and fragment validates."""
assert (
validate_constructed_url("https://example.com/path?key=val#section")
is True
)
# ---------------------------------------------------------------------------
# mask_sensitive_url -- sensitive data masking for logging
# ---------------------------------------------------------------------------
class TestMaskSensitiveUrlExtended:
"""Extended tests for mask_sensitive_url."""
def test_password_masked(self):
"""Password in user:pass@host URL is replaced with ***."""
result = mask_sensitive_url("https://admin:s3cret@example.com/api")
assert "s3cret" not in result
assert "admin" in result
assert "***" in result
assert "example.com" in result
def test_long_path_token_masked(self):
"""Path segments >= 20 alphanumeric chars are masked."""
token = "a" * 20 # exactly 20 chars
result = mask_sensitive_url(f"https://hooks.slack.com/services/{token}")
assert token not in result
assert "/***" in result
def test_query_string_masked(self):
"""Query string is replaced with ?***."""
result = mask_sensitive_url(
"https://api.example.com/data?api_key=supersecret&format=json"
)
assert "api_key" not in result
assert "supersecret" not in result
assert "?***" in result
def test_url_without_sensitive_parts_unchanged(self):
"""URL with no password, long tokens, or query is mostly preserved."""
url = "https://example.com/api/users"
result = mask_sensitive_url(url)
assert "example.com" in result
assert "/api/users" in result
assert "https://" in result
def test_short_path_segments_not_masked(self):
"""Path segments shorter than 20 chars are NOT masked."""
result = mask_sensitive_url("https://example.com/short/path/here")
assert "/short/path/here" in result
def test_invalid_url_returns_partial_mask(self):
"""Malformed URL without netloc does not crash and returns a string.
urlparse handles most strings without raising, so the function
reconstructs from whatever components it can extract. The except
branch (returning 'scheme://***') only fires for truly exceptional
inputs. Here we verify graceful handling rather than the exact
fallback format.
"""
result = mask_sensitive_url("http:not-valid-at-all")
assert result is not None
assert isinstance(result, str)
assert "http" in result
def test_webhook_url_with_long_token_masked(self):
"""Webhook URL with a long token in the path gets the token masked."""
token = "T0123456789abcdefghij" # 21 chars
result = mask_sensitive_url(
f"https://hooks.slack.com/services/{token}/more"
)
assert token not in result
assert "/***" in result
assert "hooks.slack.com" in result
def test_url_with_port_preserved(self):
"""Port number in URL is preserved after masking."""
result = mask_sensitive_url("https://example.com:8443/api/short")
assert ":8443" in result
def test_multiple_long_path_segments_masked(self):
"""Multiple long path segments are each masked independently."""
token1 = "abcdefghijklmnopqrstu" # 21 chars
token2 = "12345678901234567890XX" # 22 chars
result = mask_sensitive_url(
f"https://hooks.example.com/{token1}/{token2}"
)
assert token1 not in result
assert token2 not in result
def test_combined_password_token_and_query(self):
"""URL with password, long token, and query gets all masked."""
token = "aabbccddee1122334455ff" # 22 chars
result = mask_sensitive_url(
f"https://user:password@api.example.com/hook/{token}?secret=yes"
)
assert "password" not in result
assert token not in result
assert "secret=yes" not in result
assert "?***" in result
assert "user" in result
def test_scheme_preserved(self):
"""The URL scheme is always preserved."""
result = mask_sensitive_url("http://user:pass@example.com/path")
assert result.startswith("http://")
def test_path_segment_exactly_19_chars_not_masked(self):
"""A path segment of exactly 19 chars is NOT masked (threshold is 20)."""
segment = "a" * 19
result = mask_sensitive_url(f"https://example.com/{segment}")
assert segment in result
def test_path_segment_exactly_20_chars_masked(self):
"""A path segment of exactly 20 chars IS masked."""
segment = "b" * 20
result = mask_sensitive_url(f"https://example.com/{segment}")
assert segment not in result
assert "/***" in result
def test_url_with_only_query_no_password(self):
"""URL with query but no password masks only the query."""
result = mask_sensitive_url("https://example.com/api?token=abc123")
assert "token=abc123" not in result
assert "?***" in result
assert "example.com" in result
assert "/api" in result
def test_url_with_hyphen_underscore_token_masked(self):
"""Long tokens containing hyphens and underscores are masked."""
token = "abc-def_ghi-jkl_mno-pqrs" # 24 chars with hyphens/underscores
result = mask_sensitive_url(f"https://example.com/hook/{token}")
assert token not in result
def test_empty_path_no_crash(self):
"""URL with empty path does not crash."""
result = mask_sensitive_url("https://example.com")
assert "example.com" in result
# ---------------------------------------------------------------------------
# Integration -- combining multiple functions
# ---------------------------------------------------------------------------
class TestURLBuilderIntegrationExtended:
"""Integration scenarios combining multiple url_builder functions."""
def test_settings_to_full_url_to_mask_workflow(self):
"""Full workflow: settings -> base URL -> full URL -> masked for logging."""
base = build_base_url_from_settings(
external_url="https://app.example.com/"
)
full = build_full_url(base, "/api/v1/research/42")
assert validate_constructed_url(full) is True
masked = mask_sensitive_url(full)
assert "app.example.com" in masked
def test_host_port_to_full_url_normalized(self):
"""Build from 0.0.0.0 host, construct full URL, validate."""
base = build_base_url_from_settings(host="0.0.0.0", port=8080)
assert "localhost" in base
full = build_full_url(base, "health/check")
assert full == "http://localhost:8080/health/check"
assert validate_constructed_url(full) is True
def test_fallback_to_full_url(self):
"""Fallback base URL + path produces valid URL."""
base = build_base_url_from_settings(
external_url="",
host=None,
port=None,
fallback_base="http://fallback:7000/",
)
full = build_full_url(base, "/status")
assert full == "http://fallback:7000/status"
assert validate_constructed_url(full) is True
def test_mask_url_with_credentials_from_build(self):
"""Masking a URL built with embedded credentials hides the password."""
full = build_full_url(
"https://admin:topsecret@internal.host.com",
"/api/data",
validate=False,
)
masked = mask_sensitive_url(full)
assert "topsecret" not in masked
assert "admin" in masked
assert "***" in masked
def test_validate_rejects_then_build_without_validate(self):
"""Demonstrate that validate=False allows construction that would fail."""
# This should fail with validation
with pytest.raises(URLBuilderError):
build_full_url("noscheme", "/path", validate=True)
# But succeeds without validation
result = build_full_url("noscheme", "/path", validate=False)
assert result == "noscheme/path"