Browse Source

feat: ConnectionManager handles trailing slash in bash_url

pull/707/head
Haoyu Yang 3 weeks ago
parent
commit
b535bb19d1
  1. 28
      src/keycloak/connection.py
  2. 4
      src/keycloak/keycloak_admin.py
  3. 25
      tests/test_connection.py

28
src/keycloak/connection.py

@ -24,11 +24,6 @@
from __future__ import annotations from __future__ import annotations
try:
from urllib.parse import urljoin
except ImportError: # pragma: no cover
from urlparse import urljoin # pyright: ignore[reportMissingImports]
from typing import Any from typing import Any
import httpx import httpx
@ -167,6 +162,13 @@ class ConnectionManager:
def base_url(self, value: str | None) -> None: def base_url(self, value: str | None) -> None:
self._base_url = value self._base_url = value
def _build_url(self, path: str) -> str:
"""Join the base_url and path, and handle trailing slashes."""
if not self.base_url or path.startswith(("http://", "https://")):
return path
return f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
@property @property
def timeout(self) -> int | None: def timeout(self) -> int | None:
""" """
@ -334,7 +336,7 @@ class ConnectionManager:
raise AttributeError(msg) raise AttributeError(msg)
try: try:
return self._s.get( return self._s.get(
urljoin(self.base_url, path),
self._build_url(path),
params=kwargs, params=kwargs,
headers=self.headers, headers=self.headers,
timeout=self.timeout, timeout=self.timeout,
@ -364,7 +366,7 @@ class ConnectionManager:
raise AttributeError(msg) raise AttributeError(msg)
try: try:
return self._s.post( return self._s.post(
urljoin(self.base_url, path),
self._build_url(path),
params=kwargs, params=kwargs,
data=data, data=data,
headers=self.headers, headers=self.headers,
@ -396,7 +398,7 @@ class ConnectionManager:
try: try:
return self._s.put( return self._s.put(
urljoin(self.base_url, path),
self._build_url(path),
params=kwargs, params=kwargs,
data=data, data=data,
headers=self.headers, headers=self.headers,
@ -428,7 +430,7 @@ class ConnectionManager:
try: try:
return self._s.delete( return self._s.delete(
urljoin(self.base_url, path),
self._build_url(path),
params=kwargs, params=kwargs,
data=data or {}, data=data or {},
headers=self.headers, headers=self.headers,
@ -458,7 +460,7 @@ class ConnectionManager:
try: try:
return await self.async_s.get( return await self.async_s.get(
urljoin(self.base_url, path),
self._build_url(path),
params=self._filter_query_params(kwargs), params=self._filter_query_params(kwargs),
headers=self.headers, headers=self.headers,
timeout=self.timeout, timeout=self.timeout,
@ -493,7 +495,7 @@ class ConnectionManager:
try: try:
return await self.async_s.request( return await self.async_s.request(
method="POST", method="POST",
url=urljoin(self.base_url, path),
url=self._build_url(path),
params=self._filter_query_params(kwargs), params=self._filter_query_params(kwargs),
**self._prepare_httpx_request_content(data), **self._prepare_httpx_request_content(data),
headers=self.headers, headers=self.headers,
@ -528,7 +530,7 @@ class ConnectionManager:
try: try:
return await self.async_s.put( return await self.async_s.put(
urljoin(self.base_url, path),
self._build_url(path),
params=self._filter_query_params(kwargs), params=self._filter_query_params(kwargs),
**self._prepare_httpx_request_content(data), **self._prepare_httpx_request_content(data),
headers=self.headers, headers=self.headers,
@ -564,7 +566,7 @@ class ConnectionManager:
try: try:
return await self.async_s.request( return await self.async_s.request(
method="DELETE", method="DELETE",
url=urljoin(self.base_url, path),
url=self._build_url(path),
**self._prepare_httpx_request_content(data or {}), **self._prepare_httpx_request_content(data or {}),
params=self._filter_query_params(kwargs), params=self._filter_query_params(kwargs),
headers=self.headers, headers=self.headers,

4
src/keycloak/keycloak_admin.py

@ -2386,7 +2386,7 @@ class KeycloakAdmin:
:return: Keycloak server response (GroupRepresentation) :return: Keycloak server response (GroupRepresentation)
:rtype: dict :rtype: dict
""" """
params_path = {"realm-name": self.connection.realm_name, "path": path}
params_path = {"realm-name": self.connection.realm_name, "path": path.lstrip("/")}
data_raw = self.connection.raw_get( data_raw = self.connection.raw_get(
urls_patterns.URL_ADMIN_GROUP_BY_PATH.format(**params_path), urls_patterns.URL_ADMIN_GROUP_BY_PATH.format(**params_path),
) )
@ -9276,7 +9276,7 @@ class KeycloakAdmin:
:return: Keycloak server response (GroupRepresentation) :return: Keycloak server response (GroupRepresentation)
:rtype: dict :rtype: dict
""" """
params_path = {"realm-name": self.connection.realm_name, "path": path}
params_path = {"realm-name": self.connection.realm_name, "path": path.lstrip("/")}
data_raw = await self.connection.a_raw_get( data_raw = await self.connection.a_raw_get(
urls_patterns.URL_ADMIN_GROUP_BY_PATH.format(**params_path), urls_patterns.URL_ADMIN_GROUP_BY_PATH.format(**params_path),
) )

25
tests/test_connection.py

@ -95,3 +95,28 @@ def test_counter_part() -> None:
continue continue
assert async_method[2:] in sync_methods assert async_method[2:] in sync_methods
def test_build_url() -> None:
"""Test URL building and sub-path preservation."""
# Scenario 1: Base URL WITHOUT a trailing slash
cm = ConnectionManager(base_url="http://test.test/auth")
assert cm._build_url("realms/master") == "http://test.test/auth/realms/master"
assert cm._build_url("/realms/master") == "http://test.test/auth/realms/master"
# Scenario 2: Base URL WITH a trailing slash
cm_slashed = ConnectionManager(base_url="http://test.test/auth/")
assert cm_slashed._build_url("realms/master") == "http://test.test/auth/realms/master"
assert cm_slashed._build_url("/realms/master") == "http://test.test/auth/realms/master"
# Scenario 3: Path is already an absolute URL
assert cm._build_url("http://absolute.test/realms") == "http://absolute.test/realms"
assert cm._build_url("https://absolute.test/realms") == "https://absolute.test/realms"
# Scenario 4: Empty base URL
cm_empty = ConnectionManager(base_url="")
assert cm_empty._build_url("realms/master") == "realms/master"
assert cm_empty._build_url("/realms/master") == "/realms/master"
Loading…
Cancel
Save