diff --git a/src/specify_cli/bundler/services/adapters.py b/src/specify_cli/bundler/services/adapters.py index 17c9d3324c..91e3cf1cb4 100644 --- a/src/specify_cli/bundler/services/adapters.py +++ b/src/specify_cli/bundler/services/adapters.py @@ -75,7 +75,11 @@ def _validate_remote_url(source_id: str, url: str) -> None: f"Catalog '{source_id}' URL must use HTTPS (got {parsed.scheme}://). " "HTTP is only allowed for localhost." ) - if not parsed.netloc: + # Check hostname, not netloc: netloc is truthy for host-less URLs like + # "https://:8080" or "https://user@...", so requiring netloc would let + # those through even though they carry no host. hostname is None in those + # cases. Mirrors the fix in ``specify_cli.catalogs`` (#3210). + if not parsed.hostname: raise BundlerError( f"Catalog '{source_id}' URL must be a valid URL with a host: {url}" ) diff --git a/tests/unit/test_bundler_adapters.py b/tests/unit/test_bundler_adapters.py index 4a6b2cb808..a6a9b5c42d 100644 --- a/tests/unit/test_bundler_adapters.py +++ b/tests/unit/test_bundler_adapters.py @@ -69,3 +69,30 @@ def fake_open_url(url, timeout=10, extra_headers=None, redirect_validator=None): fetcher = adapters.make_catalog_fetcher(allow_network=True) with pytest.raises(BundlerError, match="must use HTTPS"): fetcher(_source("https://example.com/c.json")) + + +@pytest.mark.parametrize( + "url", + [ + "https://:8080", # port only, no host + "https://:0", + "https://user@", # userinfo only, no host + "https://user:pw@", + "https://:8080/catalog.json", + ], +) +def test_validate_remote_url_rejects_host_less_urls(url): + """A URL with a truthy netloc but no host (``https://:8080``, + ``https://user@``) must be rejected. + + ``urlparse`` gives these a non-empty ``netloc`` but ``hostname is None``, + so a ``netloc`` check would wrongly accept them. This mirrors the fix in + ``specify_cli.catalogs`` (#3210), which the docstring says this validator + mirrors.""" + with pytest.raises(BundlerError, match="valid URL with a host"): + adapters._validate_remote_url("team", url) + + +def test_validate_remote_url_accepts_normal_https_url(): + # Sanity: a real host with a port still passes. + adapters._validate_remote_url("team", "https://example.com:8080/c.json")