From 1ca0af7d1468becad82e9b704e4a9e8e8d6522e9 Mon Sep 17 00:00:00 2001 From: Noor-ul-ain001 Date: Mon, 29 Jun 2026 19:12:38 +0500 Subject: [PATCH] fix: reject host-less catalog URLs in base and preset validators (#3209) `CatalogStackBase._validate_catalog_url` (inherited by `IntegrationCatalog`) and `PresetCatalog._validate_catalog_url` checked `parsed.netloc`, which is truthy for host-less URLs like `https://:8080` (port only) or `https://user@` (userinfo only). Such URLs slipped past validation despite the error message promising "a valid URL with a host", then failed later with a confusing fetch error. Switch both validators to `parsed.hostname` (None for those inputs), matching the workflow, step, and bundler catalog validators that already do this. Add regression tests covering port-only and userinfo-only URLs for both validators. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/specify_cli/catalogs.py | 6 +++++- src/specify_cli/presets/__init__.py | 6 +++++- tests/integrations/test_integration_catalog.py | 14 ++++++++++++++ tests/test_presets.py | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/catalogs.py b/src/specify_cli/catalogs.py index 8bd3b2dc06..0a64bf47cf 100644 --- a/src/specify_cli/catalogs.py +++ b/src/specify_cli/catalogs.py @@ -78,7 +78,11 @@ def _validate_catalog_url(cls, url: str) -> None: f"Catalog URL must use HTTPS (got {parsed.scheme}://). " "HTTP is only allowed for localhost." ) - if not parsed.netloc: + # Use .hostname (not .netloc) so host-less URLs like "https://:8080" + # (port only) or "https://user@" (userinfo only) are rejected: netloc + # is truthy for those even though there is no host. This matches the + # workflow/step/bundler catalog validators (issue #3209). + 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: diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index 8d5c044193..e1283990de 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -1861,7 +1861,11 @@ def _validate_catalog_url(self, url: str) -> None: f"Catalog URL must use HTTPS (got {parsed.scheme}://). " "HTTP is only allowed for localhost." ) - if not parsed.netloc: + # Use .hostname (not .netloc) so host-less URLs like "https://:8080" + # (port only) or "https://user@" (userinfo only) are rejected: netloc + # is truthy for those even though there is no host. This matches the + # workflow/step/bundler catalog validators (issue #3209). + if not parsed.hostname: raise PresetValidationError( "Catalog URL must be a valid URL with a host." ) diff --git a/tests/integrations/test_integration_catalog.py b/tests/integrations/test_integration_catalog.py index fae9e32d23..b90bc366a5 100644 --- a/tests/integrations/test_integration_catalog.py +++ b/tests/integrations/test_integration_catalog.py @@ -67,6 +67,20 @@ def test_missing_host_rejected(self): 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://:8080/catalog.json", + "https://user@", # userinfo only, no host + "https://user:pass@", + ], + ) + def test_hostless_url_rejected(self, url): + """netloc is truthy for these but there is no host (#3209).""" + with pytest.raises(IntegrationCatalogError, match="valid URL"): + IntegrationCatalog._validate_catalog_url(url) + # --------------------------------------------------------------------------- # IntegrationCatalog — active catalogs diff --git a/tests/test_presets.py b/tests/test_presets.py index 0632fe3a89..335e61879f 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -1424,6 +1424,21 @@ def test_validate_catalog_url_localhost_http_allowed(self, project_dir): 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://:8080/catalog.json", + "https://user@", # userinfo only, no host + "https://user:pass@", + ], + ) + def test_validate_catalog_url_hostless_rejected(self, project_dir, url): + """Host-less URLs (netloc truthy but no host) are rejected (#3209).""" + 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")