fix(catalogs): reject host-less catalog URLs in base and preset validators (#3210)

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.
This commit is contained in:
Quratulain-bilal
2026-06-29 20:29:14 +05:00
committed by GitHub
parent 9a40ed0b6e
commit 5bdcb4ad14
4 changed files with 44 additions and 2 deletions

View File

@@ -78,7 +78,10 @@ class CatalogStackBase:
f"Catalog 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 the host guarantee this error
# promises would not actually hold. hostname is None in those cases.
if not parsed.hostname:
raise cls._error("Catalog URL must be a valid URL with a host.")
def _load_catalog_config(self, config_path: Path) -> list[CatalogEntry] | None:

View File

@@ -1861,7 +1861,10 @@ class PresetCatalog:
f"Catalog 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 the host guarantee this error
# promises would not actually hold. hostname is None in those cases.
if not parsed.hostname:
raise PresetValidationError(
"Catalog URL must be a valid URL with a host."
)

View File

@@ -67,6 +67,22 @@ class TestCatalogURLValidation:
with pytest.raises(IntegrationCatalogError, match="valid URL"):
IntegrationCatalog._validate_catalog_url("https:///no-host")
@pytest.mark.parametrize(
"url",
[
"https://:8080", # port only, no host
"https://:0", # port only, no host
"https://user@", # userinfo only, no host
"https://user:pw@", # userinfo only, no host
],
)
def test_hostless_url_with_truthy_netloc_rejected(self, url):
# These have a truthy netloc (":8080", "user@") but no actual host,
# so a netloc-based check would wrongly accept them despite the
# "valid URL with a host" promise. hostname is None for all of them.
with pytest.raises(IntegrationCatalogError, match="valid URL"):
IntegrationCatalog._validate_catalog_url(url)
# ---------------------------------------------------------------------------
# IntegrationCatalog — active catalogs

View File

@@ -1424,6 +1424,26 @@ class TestPresetCatalog:
catalog._validate_catalog_url("http://localhost:8080/catalog.json")
catalog._validate_catalog_url("http://127.0.0.1:8080/catalog.json")
@pytest.mark.parametrize(
"url",
[
"https://:8080", # port only, no host
"https://:0", # port only, no host
"https://user@", # userinfo only, no host
"https://user:pw@", # userinfo only, no host
],
)
def test_validate_catalog_url_hostless_rejected(self, project_dir, url):
"""Reject host-less URLs whose netloc is truthy but hostname is None.
``urlparse('https://:8080').netloc`` is ``':8080'`` (truthy) but its
``hostname`` is ``None``, so a netloc-based check would accept a URL
with no actual host, contradicting the "valid URL with a host" error.
"""
catalog = PresetCatalog(project_dir)
with pytest.raises(PresetValidationError, match="valid URL with a host"):
catalog._validate_catalog_url(url)
def test_env_var_catalog_url(self, project_dir, monkeypatch):
"""Test catalog URL from environment variable."""
monkeypatch.setenv("SPECKIT_PRESET_CATALOG_URL", "https://custom.example.com/catalog.json")