Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/specify_cli/catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ def _validate_catalog_url(cls, url: str) -> None:
raise cls._error(f"Catalog URL is malformed: {url}") from None
is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
# A scheme-less value (``example.com/catalog.json``) would otherwise read
# "got ://". The value itself is not echoed: callers render errors as Rich markup.
got = f"{parsed.scheme}://" if parsed.scheme else "a URL with no scheme"
raise cls._error(
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
f"Catalog URL must use HTTPS (got {got}). "
"HTTP is only allowed for localhost."
)
# Check hostname, not netloc: netloc is truthy for host-less URLs like
Expand Down
5 changes: 4 additions & 1 deletion src/specify_cli/presets/_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,11 @@ def _validate_catalog_url(self, url: str) -> None:
if parsed.scheme != "https" and not (
parsed.scheme == "http" and is_localhost
):
# A scheme-less value (``example.com/catalog.json``) would otherwise read
# "got ://". The value itself is not echoed: callers render errors as Rich markup.
got = f"{parsed.scheme}://" if parsed.scheme else "a URL with no scheme"
raise PresetValidationError(
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
f"Catalog URL must use HTTPS (got {got}). "
"HTTP is only allowed for localhost."
)
# Check hostname, not netloc: netloc is truthy for host-less URLs like
Expand Down
5 changes: 4 additions & 1 deletion src/specify_cli/workflows/catalog/_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,11 @@ def _validate_catalog_url(self, url: str) -> None:
if parsed.scheme != "https" and not (
parsed.scheme == "http" and is_localhost
):
# A scheme-less value (``example.com/catalog.json``) would otherwise read
# "got ://". The value itself is not echoed: callers render errors as Rich markup.
got = f"{parsed.scheme}://" if parsed.scheme else "a URL with no scheme"
raise WorkflowValidationError(
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
f"Catalog URL must use HTTPS (got {got}). "
"HTTP is only allowed for localhost."
)
if not hostname:
Expand Down
5 changes: 4 additions & 1 deletion src/specify_cli/workflows/step/catalog/_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,11 @@ def _validate_catalog_url(self, url: str) -> None:
if parsed.scheme != "https" and not (
parsed.scheme == "http" and is_localhost
):
# A scheme-less value (``example.com/catalog.json``) would otherwise read
# "got ://". The value itself is not echoed: callers render errors as Rich markup.
got = f"{parsed.scheme}://" if parsed.scheme else "a URL with no scheme"
raise StepValidationError(
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
f"Catalog URL must use HTTPS (got {got}). "
"HTTP is only allowed for localhost."
)
if not hostname:
Expand Down
6 changes: 6 additions & 0 deletions tests/specify_cli/integrations/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ def test_http_rejected(self):
with pytest.raises(IntegrationCatalogError, match="HTTPS"):
IntegrationCatalog._validate_catalog_url("http://example.com/catalog.json")

def test_schemeless_url_names_the_value(self):
with pytest.raises(IntegrationCatalogError) as excinfo:
IntegrationCatalog._validate_catalog_url("example.com/catalog.json")
assert "(got a URL with no scheme)" in str(excinfo.value)
assert "got ://" not in str(excinfo.value)

def test_http_localhost_allowed(self):
IntegrationCatalog._validate_catalog_url("http://localhost:8080/catalog.json")
IntegrationCatalog._validate_catalog_url("http://127.0.0.1/catalog.json")
Expand Down
6 changes: 6 additions & 0 deletions tests/specify_cli/presets/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ def test_validate_catalog_url_http_rejected(self, project_dir):
with pytest.raises(PresetValidationError, match="must use HTTPS"):
catalog._validate_catalog_url("http://example.com/catalog.json")

def test_validate_catalog_url_schemeless_names_the_value(self, project_dir):
"""A URL without a scheme is named instead of reported as "got ://"."""
catalog = PresetCatalog(project_dir)
with pytest.raises(PresetValidationError, match=r"\(got a URL with no scheme\)"):
catalog._validate_catalog_url("example.com/catalog.json")

def test_validate_catalog_url_localhost_http_allowed(self, project_dir):
"""Test that HTTP is allowed for localhost."""
catalog = PresetCatalog(project_dir)
Expand Down
11 changes: 11 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -10349,6 +10349,17 @@ def test_validate_url_http_rejected(self, project_dir):
with pytest.raises(WorkflowValidationError, match="HTTPS"):
catalog._validate_catalog_url("http://evil.com/catalog.json")

def test_validate_url_schemeless_names_the_value(self, project_dir):
from specify_cli.workflows.catalog import WorkflowCatalog, WorkflowValidationError
from specify_cli.workflows.step.catalog import StepCatalog, StepValidationError

for catalog, error in (
(WorkflowCatalog(project_dir), WorkflowValidationError),
(StepCatalog(project_dir), StepValidationError),
):
with pytest.raises(error, match=r"\(got a URL with no scheme\)"):
catalog._validate_catalog_url("example.com/catalog.json")

def test_validate_url_localhost_http_allowed(self, project_dir):
from specify_cli.workflows.catalog import WorkflowCatalog

Expand Down
Loading