Skip to content

Commit 231cd2f

Browse files
authored
MPT-17614 validate, clean base url (#209)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> Closes [MPT-17614](https://softwareone.atlassian.net/browse/MPT-17614) - Add validate_base_url() in mpt_api_client/http/client_utils.py to centralize base URL validation and normalization. - validate_base_url: - Requires a non-empty string, defaults scheme to https, requires a hostname. - Strips userinfo, handles ports (including IPv6 bracket-wrapping), and removes specific path prefixes ("/", "/public/", "/public/v1/"). - Raises ValueError with a clear message for invalid inputs. - Replace manual base URL retrieval/validation in HTTPClient and AsyncHTTPClient with validate_base_url(base_url or os.getenv("MPT_URL")) (removes prior in-place missing-URL ValueError). - Add unit tests tests/unit/http/test_client_utils.py covering IPv6, userinfo stripping, path normalization, scheme defaults, and invalid inputs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> [MPT-17614]: https://softwareone.atlassian.net/browse/MPT-17614?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2 parents 1587b9e + 2de5b67 commit 231cd2f

File tree

4 files changed

+93
-14
lines changed

4 files changed

+93
-14
lines changed

mpt_api_client/http/async_client.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from mpt_api_client.constants import APPLICATION_JSON
1212
from mpt_api_client.exceptions import MPTError, transform_http_status_exception
1313
from mpt_api_client.http.client import json_to_file_payload
14+
from mpt_api_client.http.client_utils import validate_base_url
1415
from mpt_api_client.http.types import (
1516
HeaderTypes,
1617
QueryParam,
@@ -38,13 +39,7 @@ def __init__(
3839
"argument to MPTClient."
3940
)
4041

41-
base_url = base_url or os.getenv("MPT_URL")
42-
if not base_url:
43-
raise ValueError(
44-
"Base URL is required. "
45-
"Set it up as env variable MPT_URL or pass it as `base_url` "
46-
"argument to MPTClient."
47-
)
42+
base_url = validate_base_url(base_url or os.getenv("MPT_URL"))
4843
base_headers = {
4944
"User-Agent": "swo-marketplace-client/1.0",
5045
"Authorization": f"Bearer {api_token}",

mpt_api_client/http/client.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
MPTError,
1515
transform_http_status_exception,
1616
)
17+
from mpt_api_client.http.client_utils import validate_base_url
1718
from mpt_api_client.http.types import (
1819
HeaderTypes,
1920
QueryParam,
@@ -51,13 +52,7 @@ def __init__(
5152
"argument to MPTClient."
5253
)
5354

54-
base_url = base_url or os.getenv("MPT_URL")
55-
if not base_url:
56-
raise ValueError(
57-
"Base URL is required. "
58-
"Set it up as env variable MPT_URL or pass it as `base_url` "
59-
"argument to MPTClient."
60-
)
55+
base_url = validate_base_url(base_url or os.getenv("MPT_URL"))
6156
base_headers = {
6257
"User-Agent": "swo-marketplace-client/1.0",
6358
"Authorization": f"Bearer {api_token}",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import re
2+
from urllib.parse import SplitResult, urlsplit, urlunparse
3+
4+
INVALID_ENV_URL_MESSAGE = (
5+
"Base URL is required. "
6+
"Set it up as env variable MPT_URL or pass it as `base_url` "
7+
"argument to MPTClient. Expected format scheme://host[:port]"
8+
)
9+
PATHS_TO_REMOVE_RE = re.compile(r"^/$|^/public/?$|^/public/v1/?$")
10+
11+
12+
def _format_host(hostname: str | None) -> str:
13+
if not hostname or not isinstance(hostname, str):
14+
raise ValueError(INVALID_ENV_URL_MESSAGE)
15+
16+
return f"[{hostname}]" if ":" in hostname else hostname
17+
18+
19+
def _format_port(split_result: SplitResult) -> str:
20+
try:
21+
parsed_port = split_result.port
22+
except ValueError as exc:
23+
raise ValueError(INVALID_ENV_URL_MESSAGE) from exc
24+
return f":{parsed_port}" if parsed_port else ""
25+
26+
27+
def _sanitize_path(path: str) -> str:
28+
return PATHS_TO_REMOVE_RE.sub("", path)
29+
30+
31+
def _build_sanitized_base_url(split_result: SplitResult) -> str:
32+
host = _format_host(split_result.hostname)
33+
port = _format_port(split_result)
34+
path = _sanitize_path(split_result.path)
35+
return str(urlunparse((split_result.scheme, f"{host}{port}", path, "", "", "")))
36+
37+
38+
def validate_base_url(base_url: str | None) -> str:
39+
"""Validate base url."""
40+
if not base_url or not isinstance(base_url, str):
41+
raise ValueError(INVALID_ENV_URL_MESSAGE)
42+
43+
split_result = urlsplit(base_url, scheme="https")
44+
if not split_result.scheme or not split_result.hostname:
45+
raise ValueError(INVALID_ENV_URL_MESSAGE)
46+
47+
return _build_sanitized_base_url(split_result)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
3+
from mpt_api_client.http.client_utils import validate_base_url
4+
5+
6+
@pytest.mark.parametrize(
7+
("input_url", "expected"),
8+
[
9+
("//[2001:db8:85a3::8a2e:370:7334]:80/a", "https://[2001:db8:85a3::8a2e:370:7334]:80/a"),
10+
("//example.com", "https://example.com"),
11+
("http://example.com", "http://example.com"),
12+
("http://example.com:88/something/else", "http://example.com:88/something/else"),
13+
("http://user@example.com:88/", "http://example.com:88"),
14+
("http://user:pass@example.com:88/", "http://example.com:88"),
15+
("http://example.com/public", "http://example.com"),
16+
("http://example.com/public/", "http://example.com"),
17+
("http://example.com/public/else", "http://example.com/public/else"),
18+
("http://example.com/public/v1", "http://example.com"),
19+
("http://example.com/public/v1/", "http://example.com"),
20+
("http://example.com/else/public", "http://example.com/else/public"),
21+
("http://example.com/elsepublic", "http://example.com/elsepublic"),
22+
],
23+
)
24+
def test_protocol_and_host(input_url, expected):
25+
result = validate_base_url(input_url)
26+
27+
assert result == expected
28+
29+
30+
@pytest.mark.parametrize(
31+
"input_url",
32+
[
33+
"",
34+
"http//example.com",
35+
"://example.com",
36+
"http:example.com",
37+
"http:/example.com",
38+
],
39+
)
40+
def test_protocol_and_host_error(input_url):
41+
with pytest.raises(ValueError):
42+
validate_base_url(input_url)

0 commit comments

Comments
 (0)