Skip to content

Commit 2d6905c

Browse files
fix(catalogs): reject host-less catalog URLs in base and preset validators
the shared CatalogStackBase validator and PresetCatalog validator checked parsed.netloc to enforce 'a valid URL with a host'. but netloc is truthy for host-less URLs like https://:8080 or https://user@, so those slipped through even though they have no host - contradicting the error message. the workflow, step, and bundler validators already check parsed.hostname (which is None in those cases); this aligns the two stragglers with that. add regression tests covering port-only and userinfo-only URLs.
1 parent b7e67f5 commit 2d6905c

4 files changed

Lines changed: 44 additions & 2 deletions

File tree

src/specify_cli/catalogs.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ def _validate_catalog_url(cls, url: str) -> None:
7878
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
7979
"HTTP is only allowed for localhost."
8080
)
81-
if not parsed.netloc:
81+
# Check hostname, not netloc: netloc is truthy for host-less URLs like
82+
# "https://:8080" or "https://user@", so the host guarantee this error
83+
# promises would not actually hold. hostname is None in those cases.
84+
if not parsed.hostname:
8285
raise cls._error("Catalog URL must be a valid URL with a host.")
8386

8487
def _load_catalog_config(self, config_path: Path) -> list[CatalogEntry] | None:

src/specify_cli/presets/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1861,7 +1861,10 @@ def _validate_catalog_url(self, url: str) -> None:
18611861
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
18621862
"HTTP is only allowed for localhost."
18631863
)
1864-
if not parsed.netloc:
1864+
# Check hostname, not netloc: netloc is truthy for host-less URLs like
1865+
# "https://:8080" or "https://user@", so the host guarantee this error
1866+
# promises would not actually hold. hostname is None in those cases.
1867+
if not parsed.hostname:
18651868
raise PresetValidationError(
18661869
"Catalog URL must be a valid URL with a host."
18671870
)

tests/integrations/test_integration_catalog.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ def test_missing_host_rejected(self):
6767
with pytest.raises(IntegrationCatalogError, match="valid URL"):
6868
IntegrationCatalog._validate_catalog_url("https:///no-host")
6969

70+
@pytest.mark.parametrize(
71+
"url",
72+
[
73+
"https://:8080", # port only, no host
74+
"https://:0", # port only, no host
75+
"https://user@", # userinfo only, no host
76+
"https://user:pw@", # userinfo only, no host
77+
],
78+
)
79+
def test_hostless_url_with_truthy_netloc_rejected(self, url):
80+
# These have a truthy netloc (":8080", "user@") but no actual host,
81+
# so a netloc-based check would wrongly accept them despite the
82+
# "valid URL with a host" promise. hostname is None for all of them.
83+
with pytest.raises(IntegrationCatalogError, match="valid URL"):
84+
IntegrationCatalog._validate_catalog_url(url)
85+
7086

7187
# ---------------------------------------------------------------------------
7288
# IntegrationCatalog — active catalogs

tests/test_presets.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,26 @@ def test_validate_catalog_url_localhost_http_allowed(self, project_dir):
14241424
catalog._validate_catalog_url("http://localhost:8080/catalog.json")
14251425
catalog._validate_catalog_url("http://127.0.0.1:8080/catalog.json")
14261426

1427+
@pytest.mark.parametrize(
1428+
"url",
1429+
[
1430+
"https://:8080", # port only, no host
1431+
"https://:0", # port only, no host
1432+
"https://user@", # userinfo only, no host
1433+
"https://user:pw@", # userinfo only, no host
1434+
],
1435+
)
1436+
def test_validate_catalog_url_hostless_rejected(self, project_dir, url):
1437+
"""Reject host-less URLs whose netloc is truthy but hostname is None.
1438+
1439+
``urlparse('https://:8080').netloc`` is ``':8080'`` (truthy) but its
1440+
``hostname`` is ``None``, so a netloc-based check would accept a URL
1441+
with no actual host, contradicting the "valid URL with a host" error.
1442+
"""
1443+
catalog = PresetCatalog(project_dir)
1444+
with pytest.raises(PresetValidationError, match="valid URL with a host"):
1445+
catalog._validate_catalog_url(url)
1446+
14271447
def test_env_var_catalog_url(self, project_dir, monkeypatch):
14281448
"""Test catalog URL from environment variable."""
14291449
monkeypatch.setenv("SPECKIT_PRESET_CATALOG_URL", "https://custom.example.com/catalog.json")

0 commit comments

Comments
 (0)