diff --git a/src/keycloak/connection.py b/src/keycloak/connection.py index fb22a71..c4881f0 100644 --- a/src/keycloak/connection.py +++ b/src/keycloak/connection.py @@ -307,7 +307,7 @@ class ConnectionManager(object): try: return await self.async_s.get( urljoin(self.base_url, path), - params=kwargs, + params=self._filter_query_params(kwargs), headers=self.headers, timeout=self.timeout, ) @@ -331,7 +331,7 @@ class ConnectionManager(object): return await self.async_s.request( method="POST", url=urljoin(self.base_url, path), - params=kwargs, + params=self._filter_query_params(kwargs), data=data, headers=self.headers, timeout=self.timeout, @@ -355,7 +355,7 @@ class ConnectionManager(object): try: return await self.async_s.put( urljoin(self.base_url, path), - params=kwargs, + params=self._filter_query_params(kwargs), data=data, headers=self.headers, timeout=self.timeout, @@ -381,9 +381,23 @@ class ConnectionManager(object): method="DELETE", url=urljoin(self.base_url, path), data=data or dict(), - params=kwargs, + params=self._filter_query_params(kwargs), headers=self.headers, timeout=self.timeout, ) except Exception as e: raise KeycloakConnectionError("Can't connect to server (%s)" % e) + + @staticmethod + def _filter_query_params(query_params): + """Explicitly filter query params with None values for compatibility. + + Httpx and requests differ in the way they handle query params with the value None, + requests does not include params with the value None while httpx includes them as-is. + + :param query_params: the query params + :type query_params: dict + :returns: the filtered query params + :rtype: dict + """ + return {k: v for k, v in query_params.items() if v is not None} diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 15874ee..41f2e46 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -5073,7 +5073,7 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_put( urls_patterns.URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), data=json.dumps(payload), - kwargs=params_query, + **params_query, ) return raise_error_from_response(data_raw, KeycloakPutError) @@ -5097,7 +5097,7 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_put( urls_patterns.URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, - kwargs=params_query, + **params_query, ) return raise_error_from_response(data_raw, KeycloakPutError) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index cb88c2b..c6e6448 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -5,6 +5,7 @@ import os import uuid from inspect import iscoroutinefunction, signature from typing import Tuple +from unittest.mock import ANY, patch import freezegun import pytest @@ -12,7 +13,12 @@ from dateutil import parser as datetime_parser from packaging.version import Version import keycloak -from keycloak import KeycloakAdmin, KeycloakOpenID, KeycloakOpenIDConnection +from keycloak import ( + KeycloakAdmin, + KeycloakConnectionError, + KeycloakOpenID, + KeycloakOpenIDConnection, +) from keycloak.connection import ConnectionManager from keycloak.exceptions import ( KeycloakAuthenticationError, @@ -5170,6 +5176,55 @@ async def test_a_email(admin: KeycloakAdmin, user: str): assert err.match('500: b\'{"errorMessage":"Failed to send .*"}\'') +@pytest.mark.asyncio +async def test_a_email_query_param_handling(admin: KeycloakAdmin, user: str): + """Test that the optional parameters are correctly transformed into query params. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param user: Keycloak user + :type user: str + """ + with patch.object( + admin.connection.async_s, "put", side_effect=Exception("An expected error") + ) as mock_put, pytest.raises(KeycloakConnectionError): + await admin.a_send_update_account( + user_id=user, + payload=["UPDATE_PASSWORD"], + client_id="update-account-client-id", + redirect_uri="https://example.com", + ) + + mock_put.assert_awaited_once_with( + ANY, + data='["UPDATE_PASSWORD"]', + params={ + "client_id": "update-account-client-id", + "redirect_uri": "https://example.com", + }, + headers=ANY, + timeout=60, + ) + + with patch.object( + admin.connection.async_s, "put", side_effect=Exception("An expected error") + ) as mock_put, pytest.raises(KeycloakConnectionError): + await admin.a_send_verify_email( + user_id=user, client_id="verify-client-id", redirect_uri="https://example.com" + ) + + mock_put.assert_awaited_once_with( + ANY, + data=ANY, + params={ + "client_id": "verify-client-id", + "redirect_uri": "https://example.com", + }, + headers=ANY, + timeout=60, + ) + + @pytest.mark.asyncio async def test_a_get_sessions(admin: KeycloakAdmin): """Test get sessions.