diff --git a/.gitleaksignore b/.gitleaksignore index 0428d624a..90de41c16 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -89,3 +89,27 @@ b6a2e217be5dceb6c85332d2e193619894d3a36e:README.md:generic-api-key:1349 b6a2e217be5dceb6c85332d2e193619894d3a36e:README.md:generic-api-key:1372 c751b1e9df5dcb6e6b3a62601174065ebf03144f:README.md:generic-api-key:1358 c751b1e9df5dcb6e6b3a62601174065ebf03144f:README.md:generic-api-key:1381 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_oauth.py:jwt:204 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_password.py:jwt:99 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_password.py:jwt:194 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_sso.py:jwt:228 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_password.py:jwt:403 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_password.py:jwt:516 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_totp.py:jwt:140 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_totp.py:jwt:309 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_webauthn.py:jwt:211 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_webauthn.py:jwt:419 +7c7b8abe49e30e1aed5f72fb59353dc187c9375a:tests/test_webauthn.py:jwt:654 +3630de2df01d6c4fd7b0c6cf8c8fd835df220918:tests/test_totp.py:jwt:132 +3630de2df01d6c4fd7b0c6cf8c8fd835df220918:tests/test_totp.py:jwt:289 +3630de2df01d6c4fd7b0c6cf8c8fd835df220918:tests/test_webauthn.py:jwt:144 +3630de2df01d6c4fd7b0c6cf8c8fd835df220918:tests/test_webauthn.py:jwt:312 +3630de2df01d6c4fd7b0c6cf8c8fd835df220918:tests/test_webauthn.py:jwt:482 +cc88fb407157fa23a672e03ec07e26bdce6fe170:tests/test_password.py:jwt:104 +cc88fb407157fa23a672e03ec07e26bdce6fe170:tests/test_oauth.py:jwt:217 +cc88fb407157fa23a672e03ec07e26bdce6fe170:tests/test_password.py:jwt:205 +cc88fb407157fa23a672e03ec07e26bdce6fe170:tests/test_password.py:jwt:426 +cc88fb407157fa23a672e03ec07e26bdce6fe170:tests/test_password.py:jwt:547 +cc88fb407157fa23a672e03ec07e26bdce6fe170:tests/test_sso.py:jwt:238 +3ce967512fed2bae5e89dc9ff973e67ff9bc3084:README.md:hashicorp-tf-password:248 +04146e7a83d6212e407c5a46008324d646878fc1:tests/test_auth.py:jwt:257 \ No newline at end of file diff --git a/descope/__init__.py b/descope/__init__.py index 7e71f2f6e..5cd79beac 100644 --- a/descope/__init__.py +++ b/descope/__init__.py @@ -39,3 +39,37 @@ UserPasswordFirebase, UserPasswordPbkdf2, ) + +__all__ = [ + "COOKIE_DATA_NAME", + "REFRESH_SESSION_COOKIE_NAME", + "REFRESH_SESSION_TOKEN_NAME", + "SESSION_COOKIE_NAME", + "SESSION_TOKEN_NAME", + "AccessKeyLoginOptions", + "DeliveryMethod", + "LoginOptions", + "SignUpOptions", + "DescopeClient", + "API_RATE_LIMIT_RETRY_AFTER_HEADER", + "ERROR_TYPE_API_RATE_LIMIT", + "ERROR_TYPE_SERVER_ERROR", + "AuthException", + "RateLimitException", + "AssociatedTenant", + "SAMLIDPAttributeMappingInfo", + "SAMLIDPGroupsMappingInfo", + "SAMLIDPRoleGroupMappingInfo", + "AttributeMapping", + "OIDCAttributeMapping", + "RoleMapping", + "SSOOIDCSettings", + "SSOSAMLSettings", + "SSOSAMLSettingsByMetadata", + "UserObj", + "UserPassword", + "UserPasswordBcrypt", + "UserPasswordDjango", + "UserPasswordFirebase", + "UserPasswordPbkdf2", +] diff --git a/descope/auth.py b/descope/auth.py index 36c9d4401..c8a12e56e 100644 --- a/descope/auth.py +++ b/descope/auth.py @@ -6,20 +6,25 @@ import platform import re from http import HTTPStatus +import ssl from threading import Lock -from typing import Iterable +import certifi +from typing import Awaitable, Iterable, Union import jwt +from descope.future_utils import futu_apply, futu_awaitable + try: from importlib.metadata import version except ImportError: from pkg_resources import get_distribution -import requests +import httpx from email_validator import EmailNotValidError, validate_email from jwt import ExpiredSignatureError, ImmatureSignatureError + from descope.common import ( COOKIE_DATA_NAME, DEFAULT_BASE_URL, @@ -74,6 +79,7 @@ def __init__( timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, jwt_validation_leeway: int = 5, auth_management_key: str | None = None, + async_mode: bool = False, fga_cache_url: str | None = None, ): self.lock_public_keys = Lock() @@ -91,6 +97,7 @@ def __init__( self.project_id = project_id self.jwt_validation_leeway = jwt_validation_leeway self.secure = not skip_verify + self.async_mode = async_mode self.base_url = os.getenv("DESCOPE_BASE_URI") if not self.base_url: @@ -110,6 +117,67 @@ def __init__( kid, pub_key, alg = self._validate_and_load_public_key(public_key) self.public_keys = {kid: (pub_key, alg)} + self.client_timeout = timeout_seconds + self.client_verify: bool | ssl.SSLContext = False + if not skip_verify: + # Backwards compatibility with requests + ssl_ctx = ssl.create_default_context( + cafile=os.environ.get("SSL_CERT_FILE", certifi.where()), + capath=os.environ.get("SSL_CERT_DIR"), + ) + if os.environ.get("REQUESTS_CA_BUNDLE"): + # ignore - is valid string + ssl_ctx.load_cert_chain(certfile=os.environ.get("REQUESTS_CA_BUNDLE")) # type: ignore[arg-type] + self.client_verify = ssl_ctx + # ignore - is valid string + + def _request( + self, method: str, url: str, **kwargs + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + kwargs = {**kwargs} + if self.async_mode: + return self._async_request(method, url, **kwargs) + else: + return self._sync_request(method, url, **kwargs) + + def _sync_request(self, method: str, url: str, **kwargs) -> httpx.Response: + req_kwargs = { + "verify": self.client_verify, + "timeout": self.client_timeout, + **kwargs, + } + method_lower = method.lower() + if method_lower == "get": + return httpx.get(url, **req_kwargs) + elif method_lower == "post": + return httpx.post(url, **req_kwargs) + elif method_lower == "patch": + return httpx.patch(url, **req_kwargs) + elif method_lower == "delete": + return httpx.delete(url, **req_kwargs) + elif method_lower == "put": + return httpx.put(url, **req_kwargs) + else: + return httpx.request(method, url, **req_kwargs) + + async def _async_request(self, method: str, url: str, **kwargs) -> httpx.Response: + async with httpx.AsyncClient( + verify=self.client_verify, timeout=self.client_timeout + ) as client: + method_lower = method.lower() + if method_lower == "get": + return await client.get(url, **kwargs) + elif method_lower == "post": + return await client.post(url, **kwargs) + elif method_lower == "patch": + return await client.patch(url, **kwargs) + elif method_lower == "delete": + return await client.delete(url, **kwargs) + elif method_lower == "put": + return await client.put(url, **kwargs) + else: + return await client.request(method, url, **kwargs) + def _raise_rate_limit_exception(self, response): try: resp = response.json() @@ -144,19 +212,18 @@ def do_get( self, uri: str, params=None, - allow_redirects=None, + follow_redirects=None, pswd: str | None = None, - ) -> requests.Response: - response = requests.get( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make GET request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "GET", f"{self.base_url}{uri}", headers=self._get_default_headers(pswd), params=params, - allow_redirects=allow_redirects, - verify=self.secure, - timeout=self.timeout_seconds, + follow_redirects=follow_redirects, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def do_post( self, @@ -164,18 +231,17 @@ def do_post( body: dict | list[dict] | list[str] | None, params=None, pswd: str | None = None, - ) -> requests.Response: - response = requests.post( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make POST request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "POST", f"{self.base_url}{uri}", headers=self._get_default_headers(pswd), json=body, - allow_redirects=False, - verify=self.secure, params=params, - timeout=self.timeout_seconds, + follow_redirects=False, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def do_patch( self, @@ -183,30 +249,35 @@ def do_patch( body: dict | list[dict] | list[str] | None, params=None, pswd: str | None = None, - ) -> requests.Response: - response = requests.patch( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make PATCH request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "PATCH", f"{self.base_url}{uri}", headers=self._get_default_headers(pswd), json=body, - allow_redirects=False, - verify=self.secure, params=params, - timeout=self.timeout_seconds, + follow_redirects=False, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def do_delete( self, uri: str, params=None, pswd: str | None = None - ) -> requests.Response: - response = requests.delete( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make DELETE request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "DELETE", f"{self.base_url}{uri}", params=params, headers=self._get_default_headers(pswd), - allow_redirects=False, - verify=self.secure, - timeout=self.timeout_seconds, + follow_redirects=False, ) + return futu_apply(response, self._raise_from_response_and_return) + + def _raise_from_response_and_return( + self, response: httpx.Response + ) -> httpx.Response: + """Helper method to raise exception if needed, then return response.""" self._raise_from_response(response) return response @@ -217,27 +288,25 @@ def do_post_with_custom_base_url( custom_base_url: str | None = None, params=None, pswd: str | None = None, - ) -> requests.Response: + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: """ - Post request with optional custom base URL. - If base_url is provided, use it instead of self.base_url. + Make POST request to a custom base URL when provided; otherwise use default base URL. + Returns Response or awaitable Response based on async_mode. """ effective_base_url = custom_base_url if custom_base_url else self.base_url - response = requests.post( + response = self._request( + "POST", f"{effective_base_url}{uri}", headers=self._get_default_headers(pswd), json=body, - allow_redirects=False, - verify=self.secure, params=params, - timeout=self.timeout_seconds, + follow_redirects=False, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def exchange_token( self, uri, code: str, audience: str | None | Iterable[str] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not code: raise AuthException( 400, @@ -247,11 +316,14 @@ def exchange_token( body = Auth._compose_exchange_body(code) response = self.do_post(uri=uri, body=body, params=None) - resp = response.json() - jwt_response = self.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME), audience + return futu_apply( + response, + lambda response: self.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME), + audience, + ), ) - return jwt_response @staticmethod def base_url_for_project_id(project_id): @@ -391,15 +463,20 @@ def exchange_access_key( access_key: str, audience: str | Iterable[str] | None = None, login_options: AccessKeyLoginOptions | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.exchange_auth_access_key_path body = { "loginOptions": login_options.__dict__ if login_options else {}, } server_response = self.do_post(uri=uri, body=body, params=None, pswd=access_key) - json = server_response.json() - return self._generate_auth_info( - response_body=json, refresh_token=None, user_jwt=False, audience=audience + return futu_apply( + server_response, + lambda response: self._generate_auth_info( + response_body=response.json(), + refresh_token=None, + user_jwt=False, + audience=audience, + ), ) @staticmethod @@ -450,9 +527,9 @@ def _validate_and_load_public_key(public_key) -> tuple[str, jwt.PyJWK, str]: f"Unable to load public key {e}", ) - def _raise_from_response(self, response): - """Raise appropriate exception from response, does nothing if response.ok is True.""" - if response.ok: + def _raise_from_response(self, response: httpx.Response): + """Raise appropriate exception from response, does nothing if response.is_success is True.""" + if response.is_success: return if response.status_code == HTTPStatus.TOO_MANY_REQUESTS: @@ -464,13 +541,12 @@ def _raise_from_response(self, response): response.text, ) - def _fetch_public_keys(self) -> None: + def _fetch_public_keys_sync(self) -> None: # This function called under mutex protection so no need to acquire it once again - response = requests.get( + response = self._sync_request( + "GET", f"{self.base_url}{EndpointsV2.public_key_path}/{self.project_id}", headers=self._get_default_headers(), - verify=self.secure, - timeout=self.timeout_seconds, ) self._raise_from_response(response) @@ -554,14 +630,16 @@ def _generate_auth_info( jwt_response = {} st_jwt = response_body.get("sessionJwt", "") if st_jwt: - jwt_response[SESSION_TOKEN_NAME] = self._validate_token(st_jwt, audience) + jwt_response[SESSION_TOKEN_NAME] = self._validate_token_sync( + st_jwt, audience + ) rt_jwt = response_body.get("refreshJwt", "") if rt_jwt: - jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token( + jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token_sync( rt_jwt, audience ) elif refresh_token: - jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token( + jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token_sync( refresh_token, audience ) @@ -602,8 +680,8 @@ def _get_default_headers(self, pswd: str | None = None): headers["Authorization"] = f"Bearer {bearer}" return headers - # Validate a token and load the public key if needed - def _validate_token( + # Validate a token and load the public key if needed. + def _validate_token_sync( self, token: str, audience: str | None | Iterable[str] = None ) -> dict: if not token: @@ -635,7 +713,7 @@ def _validate_token( with self.lock_public_keys: if self.public_keys == {} or self.public_keys.get(kid, None) is None: - self._fetch_public_keys() + self._fetch_public_keys_sync() found_key = self.public_keys.get(kid, None) if found_key is None: @@ -682,7 +760,8 @@ def _validate_token( def validate_session( self, session_token: str, audience: str | None | Iterable[str] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Validate a session token, returning dict or awaitable dict based on async_mode.""" if not session_token: raise AuthException( 400, @@ -690,15 +769,16 @@ def validate_session( "Session token is required for validation", ) - res = self._validate_token(session_token, audience) + res = self._validate_token_sync(session_token, audience) res[SESSION_TOKEN_NAME] = copy.deepcopy( res ) # Duplicate for saving backward compatibility but keep the same structure as the refresh operation response - return self.adjust_properties(res, True) + return futu_awaitable(self.adjust_properties(res, True), self.async_mode) def refresh_session( self, refresh_token: str, audience: str | None | Iterable[str] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Refresh a session token, returning dict or awaitable dict based on async_mode.""" if not refresh_token: raise AuthException( 400, @@ -706,23 +786,28 @@ def refresh_session( "Refresh token is required to refresh a session", ) - self._validate_token(refresh_token, audience) - - uri = EndpointsV1.refresh_token_path - response = self.do_post(uri=uri, body={}, params=None, pswd=refresh_token) + self._validate_token_sync(refresh_token, audience) - resp = response.json() - refresh_token = ( - response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None) or refresh_token + response = self.do_post( + uri=EndpointsV1.refresh_token_path, body={}, params=None, pswd=refresh_token ) - return self.generate_jwt_response(resp, refresh_token, audience) + + def process_response(resp_obj): + resp = resp_obj.json() + refresh_token_from_cookie = ( + resp_obj.cookies.get(REFRESH_SESSION_COOKIE_NAME, None) or refresh_token + ) + return self.generate_jwt_response(resp, refresh_token_from_cookie, audience) + + return futu_apply(response, process_response) def validate_and_refresh_session( self, session_token: str, refresh_token: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Validate session, refresh if needed, returning dict or awaitable dict based on async_mode.""" if not session_token: raise AuthException( 400, @@ -747,7 +832,8 @@ def select_tenant( tenant_id: str, refresh_token: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Select a tenant, returning dict or awaitable dict based on async_mode.""" if not refresh_token: raise AuthException( 400, @@ -760,11 +846,13 @@ def select_tenant( uri=uri, body={"tenant": tenant_id}, params=None, pswd=refresh_token ) - resp = response.json() - jwt_response = self.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience - ) - return jwt_response + def process_response(resp_obj): + resp = resp_obj.json() + return self.generate_jwt_response( + resp, resp_obj.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + ) + + return futu_apply(response, process_response) @staticmethod def extract_masked_address(response: dict, method: DeliveryMethod) -> str: diff --git a/descope/authmethod/enchantedlink.py b/descope/authmethod/enchantedlink.py index 9b78db860..a2b91f719 100644 --- a/descope/authmethod/enchantedlink.py +++ b/descope/authmethod/enchantedlink.py @@ -1,6 +1,8 @@ from __future__ import annotations -import requests +from typing import Awaitable, Union + +import httpx from descope._auth_base import AuthBase from descope.auth import Auth @@ -14,6 +16,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class EnchantedLink(AuthBase): @@ -23,7 +26,7 @@ def sign_in( uri: str, login_options: LoginOptions | None = None, refresh_token: str | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not login_id: raise AuthException( 400, @@ -37,7 +40,10 @@ def sign_in( uri = EnchantedLink._compose_signin_url() response = self._auth.do_post(uri, body, None, refresh_token) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) def sign_up( self, @@ -45,7 +51,7 @@ def sign_up( uri: str, user: dict | None, signup_options: SignUpOptions | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not user: user = {} @@ -61,11 +67,14 @@ def sign_up( body = EnchantedLink._compose_signup_body(login_id, uri, user, signup_options) uri = EnchantedLink._compose_signup_url() response = self._auth.do_post(uri, body, None) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) def sign_up_or_in( self, login_id: str, uri: str, signup_options: SignUpOptions | None = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: login_options: LoginOptions | None = None if signup_options is not None: login_options = LoginOptions( @@ -81,23 +90,29 @@ def sign_up_or_in( ) uri = EnchantedLink._compose_sign_up_or_in_url() response = self._auth.do_post(uri, body, None) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) - def get_session(self, pending_ref: str) -> dict: + def get_session(self, pending_ref: str) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.get_session_enchantedlink_auth_path body = EnchantedLink._compose_get_session_body(pending_ref) response = self._auth.do_post(uri, body, None) - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), None + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + None, + ), ) - return jwt_response - def verify(self, token: str): + def verify(self, token: str) -> Union[None, Awaitable[None]]: uri = EndpointsV1.verify_enchantedlink_auth_path body = EnchantedLink._compose_verify_body(token) - self._auth.do_post(uri, body, None) + response = self._auth.do_post(uri, body, None) + return futu_apply(response, lambda response: None) def update_user_email( self, @@ -109,7 +124,7 @@ def update_user_email( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not login_id: raise AuthException( 400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty" @@ -128,7 +143,10 @@ def update_user_email( ) uri = EndpointsV1.update_user_email_enchantedlink_path response = self._auth.do_post(uri, body, None, refresh_token) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) @staticmethod def _compose_signin_url() -> str: @@ -210,7 +228,3 @@ def _compose_update_user_email_body( @staticmethod def _compose_get_session_body(pending_ref: str) -> dict: return {"pendingRef": pending_ref} - - @staticmethod - def _get_pending_ref_from_response(response: requests.Response) -> dict: - return response.json() diff --git a/descope/authmethod/magiclink.py b/descope/authmethod/magiclink.py index 4182e5484..15f2238cd 100644 --- a/descope/authmethod/magiclink.py +++ b/descope/authmethod/magiclink.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union from descope._auth_base import AuthBase from descope.auth import Auth @@ -14,6 +14,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class MagicLink(AuthBase): @@ -24,7 +25,7 @@ def sign_in( uri: str, login_options: LoginOptions | None = None, refresh_token: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not login_id: raise AuthException( 400, @@ -38,7 +39,10 @@ def sign_in( uri = MagicLink._compose_signin_url(method) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up( self, @@ -47,7 +51,7 @@ def sign_up( uri: str, user: dict | None = None, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not user: user = {} @@ -63,7 +67,10 @@ def sign_up( ) uri = MagicLink._compose_signup_url(method) response = self._auth.do_post(uri, body, None) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up_or_in( self, @@ -71,7 +78,7 @@ def sign_up_or_in( login_id: str, uri: str, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: login_options: LoginOptions | None = None if signup_options is not None: login_options = LoginOptions( @@ -86,17 +93,25 @@ def sign_up_or_in( ) uri = MagicLink._compose_sign_up_or_in_url(method) response = self._auth.do_post(uri, body, None) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) - def verify(self, token: str, audience: str | None | Iterable[str] = None) -> dict: + def verify( + self, token: str, audience: str | None | Iterable[str] = None + ) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.verify_magiclink_auth_path body = MagicLink._compose_verify_body(token) response = self._auth.do_post(uri, body, None) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def update_user_email( self, @@ -108,7 +123,7 @@ def update_user_email( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not login_id: raise AuthException( 400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty" @@ -127,7 +142,12 @@ def update_user_email( ) uri = EndpointsV1.update_user_email_magiclink_path response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), DeliveryMethod.EMAIL) + return futu_apply( + response, + lambda response: Auth.extract_masked_address( + response.json(), DeliveryMethod.EMAIL + ), + ) def update_user_phone( self, @@ -140,7 +160,7 @@ def update_user_phone( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not login_id: raise AuthException( 400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty" @@ -159,7 +179,12 @@ def update_user_phone( ) uri = EndpointsV1.update_user_phone_magiclink_path response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), DeliveryMethod.SMS) + return futu_apply( + response, + lambda response: Auth.extract_masked_address( + response.json(), DeliveryMethod.SMS + ), + ) @staticmethod def _compose_signin_url(method: DeliveryMethod) -> str: diff --git a/descope/authmethod/oauth.py b/descope/authmethod/oauth.py index 55278bdfa..caeb5f12b 100644 --- a/descope/authmethod/oauth.py +++ b/descope/authmethod/oauth.py @@ -1,8 +1,14 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase -from descope.common import EndpointsV1, LoginOptions, validate_refresh_token_provided +from descope.common import ( + EndpointsV1, + LoginOptions, + REFRESH_SESSION_COOKIE_NAME, + validate_refresh_token_provided, +) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class OAuth(AuthBase): @@ -12,7 +18,7 @@ def start( return_url: str = "", login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ """ if not self._verify_provider(provider): raise AuthException( @@ -29,11 +35,15 @@ def start( uri, login_options.__dict__ if login_options else {}, params, refresh_token ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) - def exchange_token(self, code: str) -> dict: - uri = EndpointsV1.oauth_exchange_token_path - return self._auth.exchange_token(uri, code) + def exchange_token(self, code: str) -> Union[dict, Awaitable[dict]]: + return self._auth.exchange_token( + EndpointsV1.oauth_exchange_token_path, code, None + ) @staticmethod def _verify_provider(oauth_provider: str) -> bool: diff --git a/descope/authmethod/otp.py b/descope/authmethod/otp.py index 86cc3c10d..fafe930d7 100644 --- a/descope/authmethod/otp.py +++ b/descope/authmethod/otp.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union from descope._auth_base import AuthBase from descope.auth import Auth @@ -14,6 +14,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class OTP(AuthBase): @@ -23,7 +24,7 @@ def sign_in( login_id: str, login_options: LoginOptions | None = None, refresh_token: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Sign in (log in) an existing user with the unique login_id you provide. (See 'sign_up' function for an explanation of the login_id field.) Provide the DeliveryMethod required for this user. If the login_id value cannot be used for the @@ -36,6 +37,9 @@ def sign_in( login_options (LoginOptions): Optional advanced controls over login parameters refresh_token: Optional refresh token is needed for specific login options + Returns: + str or Awaitable[str]: Masked address for OTP delivery + Raise: AuthException: raised if sign-in operation fails """ @@ -49,7 +53,10 @@ def sign_in( uri = OTP._compose_signin_url(method) body = OTP._compose_signin_body(login_id, login_options) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up( self, @@ -57,7 +64,7 @@ def sign_up( login_id: str, user: dict | None = None, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Sign up (create) a new user using their email or phone number. Choose a delivery method for OTP verification, for example Email, SMS, Voice call, or WhatsApp. @@ -69,6 +76,9 @@ def sign_up( user (dict) optional: Preserve additional user metadata in the form of {"name": "Joe Person", "phone": "2125551212", "email": "joe@somecompany.com"} + Returns: + str or Awaitable[str]: Masked address for OTP delivery + Raise: AuthException: raised if sign-up operation fails """ @@ -86,14 +96,17 @@ def sign_up( uri = OTP._compose_signup_url(method) body = OTP._compose_signup_body(method, login_id, user, signup_options) response = self._auth.do_post(uri, body) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up_or_in( self, method: DeliveryMethod, login_id: str, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Sign_up_or_in lets you handle both sign up and sign in with a single call. Sign-up_or_in will first determine if login_id is a new or existing end user. If login_id is new, a new end user user will be created and then @@ -125,7 +138,10 @@ def sign_up_or_in( login_options, ) response = self._auth.do_post(uri, body) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def verify_code( self, @@ -133,7 +149,7 @@ def verify_code( login_id: str, code: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Verify the validity of an OTP code entered by an end user during sign_in or sign_up. (This function is not needed if you are using the sign_up_or_in function. @@ -142,11 +158,10 @@ def verify_code( method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp login_id (str): The Login ID of the user being validated code (str): The authorization code enter by the end user during signup/signin + audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for - Return value (dict): - Return dict in the format - {"jwts": [], "user": "", "firstSeen": "", "error": ""} - Includes all the jwts tokens (session token, refresh token), token claims, and user information + Returns: + dict or Awaitable[dict]: Authentication response with JWT tokens and user information Raise: AuthException: raised if the OTP code is not valid or if token verification failed @@ -159,12 +174,14 @@ def verify_code( uri = OTP._compose_verify_code_url(method) body = OTP._compose_verify_code_body(login_id, code) response = self._auth.do_post(uri, body, None) - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def update_user_email( self, @@ -176,7 +193,7 @@ def update_user_email( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Update the email address of an end user, after verifying the authenticity of the end user using OTP. @@ -207,7 +224,12 @@ def update_user_email( provider_id, ) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), DeliveryMethod.EMAIL) + return futu_apply( + response, + lambda response: Auth.extract_masked_address( + response.json(), DeliveryMethod.EMAIL + ), + ) def update_user_phone( self, @@ -220,7 +242,7 @@ def update_user_phone( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP. @@ -255,7 +277,10 @@ def update_user_phone( provider_id, ) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) @staticmethod def _compose_signup_url(method: DeliveryMethod) -> str: diff --git a/descope/authmethod/password.py b/descope/authmethod/password.py index 3d192ef35..3a5451bd2 100644 --- a/descope/authmethod/password.py +++ b/descope/authmethod/password.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union from descope._auth_base import AuthBase from descope.common import REFRESH_SESSION_COOKIE_NAME, EndpointsV1 from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class Password(AuthBase): @@ -14,7 +15,7 @@ def sign_up( password: str, user: dict | None = None, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sign up (create) a new user using a login ID and password. (optional) Include additional user metadata that you wish to save. @@ -25,7 +26,7 @@ def sign_up( user (dict) optional: Preserve additional user metadata in the form of {"name": "Desmond Copeland", "phone": "2125551212", "email": "des@cope.com"} - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": "", "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -48,18 +49,21 @@ def sign_up( body = Password._compose_signup_body(login_id, password, user) response = self._auth.do_post(uri, body) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def sign_in( self, login_id: str, password: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sign in by verifying the validity of a password entered by an end user. @@ -67,7 +71,7 @@ def sign_in( login_id (str): The login ID of the user being validated password (str): The password to be validated - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": "", "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -89,18 +93,21 @@ def sign_in( uri = EndpointsV1.sign_in_password_path response = self._auth.do_post(uri, {"loginId": login_id, "password": password}) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def send_reset( self, login_id: str, redirect_url: str | None = None, template_options: dict | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sends a password reset prompt to the user with the given login_id according to the password settings defined in the Descope console. @@ -112,7 +119,7 @@ def send_reset( if those are the chosen reset methods. See the Magic Link and Enchanted Link sections for more details. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"resetMethod": "", "pendingRef": "", "linkId": "", "maskedEmail": ""} The contents will differ according to the chosen reset method. 'pendingRef' @@ -140,9 +147,11 @@ def send_reset( body, ) - return response.json() + return futu_apply(response, lambda response: response.json()) - def update(self, login_id: str, new_password: str, refresh_token: str) -> None: + def update( + self, login_id: str, new_password: str, refresh_token: str + ) -> Union[None, Awaitable[None]]: """ Update a password for an existing logged in user using their refresh token. @@ -171,9 +180,10 @@ def update(self, login_id: str, new_password: str, refresh_token: str) -> None: ) uri = EndpointsV1.update_password_path - self._auth.do_post( + response = self._auth.do_post( uri, {"loginId": login_id, "newPassword": new_password}, None, refresh_token ) + return futu_apply(response, lambda response: None) def replace( self, @@ -181,7 +191,7 @@ def replace( old_password: str, new_password: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Replace a valid active password with a new one. The old_password is used to authenticate the user. If the user cannot be authenticated, this operation @@ -192,7 +202,7 @@ def replace( old_password (str): The user's current active password new_password (str): The new password to use - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": false, "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -226,18 +236,21 @@ def replace( }, ) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response - def get_policy(self) -> dict: + def get_policy(self) -> Union[dict, Awaitable[dict]]: """ Get a subset of the password policy defined in the Descope console and enforced by Descope. The goal is to enable client-side validations to give users a better UX - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"minLength": 8, "lowercase": true, "uppercase": true, "number": true, "nonAlphanumeric": true} minLength - the minimum length of a password @@ -251,7 +264,7 @@ def get_policy(self) -> dict: """ response = self._auth.do_get(uri=EndpointsV1.password_policy_path) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_signup_body(login_id: str, password: str, user: dict | None) -> dict: diff --git a/descope/authmethod/saml.py b/descope/authmethod/saml.py index b9b8e4480..8b8b309c2 100644 --- a/descope/authmethod/saml.py +++ b/descope/authmethod/saml.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase from descope.common import EndpointsV1, LoginOptions, validate_refresh_token_provided from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply # This class is DEPRECATED please use SSO instead @@ -13,7 +14,7 @@ def start( return_url: Optional[str] = None, login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ DEPRECATED """ @@ -35,9 +36,9 @@ def start( uri, login_options.__dict__ if login_options else {}, params, refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) - def exchange_token(self, code: str) -> dict: + def exchange_token(self, code: str) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.saml_exchange_token_path return self._auth.exchange_token(uri, code) diff --git a/descope/authmethod/sso.py b/descope/authmethod/sso.py index 7d21f23d4..354aa9ac2 100644 --- a/descope/authmethod/sso.py +++ b/descope/authmethod/sso.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase from descope.common import EndpointsV1, LoginOptions, validate_refresh_token_provided from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class SSO(AuthBase): @@ -14,11 +15,11 @@ def start( refresh_token: Optional[str] = None, prompt: Optional[str] = None, sso_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Start tenant sso session (saml/oidc based on tenant settings) - Return value (dict): the redirect url for the login page + Return value (Union[dict, Awaitable[dict]]): the redirect url for the login page Return dict in the format {'url': 'http://dummy.com/login..'} """ @@ -40,9 +41,9 @@ def start( uri, login_options.__dict__ if login_options else {}, params, refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) - def exchange_token(self, code: str) -> dict: + def exchange_token(self, code: str) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.sso_exchange_token_path return self._auth.exchange_token(uri, code) diff --git a/descope/authmethod/totp.py b/descope/authmethod/totp.py index 11a490bd4..ed8170fd3 100644 --- a/descope/authmethod/totp.py +++ b/descope/authmethod/totp.py @@ -1,4 +1,4 @@ -from typing import Iterable, Optional, Union +from typing import Iterable, Optional, Union, Awaitable from descope._auth_base import AuthBase from descope.common import ( @@ -8,10 +8,13 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class TOTP(AuthBase): - def sign_up(self, login_id: str, user: Optional[dict] = None) -> dict: + def sign_up( + self, login_id: str, user: Optional[dict] = None + ) -> Union[dict, Awaitable[dict]]: """ Sign up (create) a new user using their email or phone number. (optional) Include additional user metadata that you wish to save. @@ -21,7 +24,7 @@ def sign_up(self, login_id: str, user: Optional[dict] = None) -> dict: user (dict) optional: Preserve additional user metadata in the form of, {"name": "Desmond Copeland", "phone": "2125551212", "email": "des@cope.com"} - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"provisioningURL": "", "image": "", "key": ""} Includes 3 different ways to allow the user to save their credentials in @@ -41,7 +44,7 @@ def sign_up(self, login_id: str, user: Optional[dict] = None) -> dict: body = TOTP._compose_signup_body(login_id, user) response = self._auth.do_post(uri, body) - return response.json() + return futu_apply(response, lambda response: response.json()) def sign_in_code( self, @@ -50,7 +53,7 @@ def sign_in_code( login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, audience: Union[str, None, Iterable[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sign in by verifying the validity of a TOTP code entered by an end user. @@ -60,7 +63,7 @@ def sign_in_code( login_options (LoginOptions): Optional advanced controls over login parameters refresh_token: Optional refresh token is needed for specific login options - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": "", "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -85,13 +88,18 @@ def sign_in_code( body = TOTP._compose_signin_body(login_id, code, login_options) response = self._auth.do_post(uri, body, None, refresh_token) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response - def update_user(self, login_id: str, refresh_token: str) -> None: + def update_user( + self, login_id: str, refresh_token: str + ) -> Union[dict, Awaitable[dict]]: """ Add TOTP to an existing logged in user using their refresh token. @@ -99,7 +107,7 @@ def update_user(self, login_id: str, refresh_token: str) -> None: login_id (str): The login ID of the user whose information is being updated refresh_token (str): The session's refresh token (used for verification) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"provisioningURL": "", "image": "", "key": ""} Includes 3 different ways to allow the user to save their credentials in @@ -124,7 +132,7 @@ def update_user(self, login_id: str, refresh_token: str) -> None: body = TOTP._compose_update_user_body(login_id) response = self._auth.do_post(uri, body, None, refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_signup_body(login_id: str, user: Optional[dict]) -> dict: diff --git a/descope/authmethod/webauthn.py b/descope/authmethod/webauthn.py index 9f3f89b29..eca082826 100644 --- a/descope/authmethod/webauthn.py +++ b/descope/authmethod/webauthn.py @@ -1,6 +1,6 @@ -from typing import Iterable, Optional, Union +from typing import Awaitable, Iterable, Optional, Union -from requests import Response +from httpx import Response from descope._auth_base import AuthBase from descope.common import ( @@ -10,6 +10,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class WebAuthn(AuthBase): @@ -18,7 +19,7 @@ def sign_up_start( login_id: Optional[str], origin: Optional[str], user: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -39,14 +40,14 @@ def sign_up_start( body = WebAuthn._compose_sign_up_start_body(login_id, user, origin) response = self._auth.do_post(uri, body) - return response.json() + return futu_apply(response, lambda response: response.json()) def sign_up_finish( self, transaction_id: str, response: Response, audience: Union[str, None, Iterable[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -62,13 +63,16 @@ def sign_up_finish( uri = EndpointsV1.sign_up_auth_webauthn_finish_path body = WebAuthn._compose_sign_up_in_finish_body(transaction_id, response) - response = self._auth.do_post(uri, body, None, "") - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + res = self._auth.do_post(uri, body, None, "") + + return futu_apply( + res, + lambda res: self._auth.generate_jwt_response( + res.json(), + res.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def sign_in_start( self, @@ -76,7 +80,7 @@ def sign_in_start( origin: str, login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -96,14 +100,14 @@ def sign_in_start( body = WebAuthn._compose_sign_in_start_body(login_id, origin, login_options) response = self._auth.do_post(uri, body, pswd=refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) def sign_in_finish( self, transaction_id: str, response: Response, audience: Union[str, None, Iterable[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -119,19 +123,22 @@ def sign_in_finish( uri = EndpointsV1.sign_in_auth_webauthn_finish_path body = WebAuthn._compose_sign_up_in_finish_body(transaction_id, response) - response = self._auth.do_post(uri, body, None) - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + res = self._auth.do_post(uri, body, None) + + return futu_apply( + res, + lambda res: self._auth.generate_jwt_response( + res.json(), + res.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def sign_up_or_in_start( self, login_id: str, origin: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -149,9 +156,11 @@ def sign_up_or_in_start( body = WebAuthn._compose_sign_up_or_in_start_body(login_id, origin) response = self._auth.do_post(uri, body) - return response.json() + return futu_apply(response, lambda response: response.json()) - def update_start(self, login_id: str, refresh_token: str, origin: str): + def update_start( + self, login_id: str, refresh_token: str, origin: str + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -169,9 +178,11 @@ def update_start(self, login_id: str, refresh_token: str, origin: str): body = WebAuthn._compose_update_start_body(login_id, origin) response = self._auth.do_post(uri, body, None, refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) - def update_finish(self, transaction_id: str, response: str) -> None: + def update_finish( + self, transaction_id: str, response: str + ) -> Union[None, Awaitable[None]]: """ Docs """ @@ -187,7 +198,9 @@ def update_finish(self, transaction_id: str, response: str) -> None: uri = EndpointsV1.update_auth_webauthn_finish_path body = WebAuthn._compose_update_finish_body(transaction_id, response) - self._auth.do_post(uri, body) + res = self._auth.do_post(uri, body) + + return futu_apply(res, lambda res: None) @staticmethod def _compose_sign_up_start_body(login_id: str, user: dict, origin: str) -> dict: diff --git a/descope/descope_client.py b/descope/descope_client.py index fa67fdb20..9e0153089 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union -import requests +import httpx from descope.auth import Auth # noqa: F401 from descope.authmethod.enchantedlink import EnchantedLink # noqa: F401 @@ -16,6 +16,7 @@ from descope.authmethod.webauthn import WebAuthn # noqa: F401 from descope.common import DEFAULT_TIMEOUT_SECONDS, AccessKeyLoginOptions, EndpointsV1 from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply from descope.mgmt import MGMT # noqa: F401 @@ -31,6 +32,7 @@ def __init__( timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, jwt_validation_leeway: int = 5, auth_management_key: str | None = None, + async_mode: bool = False, fga_cache_url: str | None = None, ): auth = Auth( @@ -41,6 +43,7 @@ def __init__( timeout_seconds, jwt_validation_leeway, auth_management_key, + async_mode, fga_cache_url, ) self._auth = auth @@ -122,7 +125,7 @@ def get_matched_permissions( jwt_response (dict): The jwt_response object which includes all JWT claims information permissions (List[str]): List of permissions to validate for this jwt_response - Return value (List[str]): returns the list of permissions that are granted + Return value (list[str]): returns the list of permissions that are granted """ return self.get_matched_tenant_permissions(jwt_response, "", permissions) @@ -174,7 +177,7 @@ def get_matched_tenant_permissions( tenant (str): TenantId permissions (List[str]): List of permissions to validate for this jwt_response - Return value (List[str]): returns the list of permissions that are granted + Return value (list[str]): returns the list of permissions that are granted """ if not jwt_response: return [] @@ -221,7 +224,7 @@ def get_matched_roles(self, jwt_response: dict, roles: list[str]) -> list[str]: jwt_response (dict): The jwt_response object which includes all JWT claims information roles (List[str]): List of roles to validate for this jwt_response - Return value (List[str]): returns the list of roles that are granted + Return value (list[str]): returns the list of roles that are granted """ return self.get_matched_tenant_roles(jwt_response, "", roles) @@ -271,7 +274,7 @@ def get_matched_tenant_roles( tenant (str): TenantId roles (List[str]): List of roles to validate for this jwt_response - Return value (List[str]): returns the list of roles that are granted + Return value (list[str]): returns the list of roles that are granted """ if not jwt_response: return [] @@ -296,7 +299,7 @@ def get_matched_tenant_roles( def validate_session( self, session_token: str, audience: str | Iterable[str] | None = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Validate a session token. Call this function for every incoming request to your private endpoints. Alternatively, use validate_and_refresh_session in order to @@ -309,7 +312,7 @@ def validate_session( session_token (str): The session token to be validated audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for (must be equal to the 'aud' claim on the provided token) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token and all JWT claims Raise: @@ -319,7 +322,7 @@ def validate_session( def refresh_session( self, refresh_token: str, audience: str | Iterable[str] | None = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Refresh a session. Call this function when a session expires and needs to be refreshed. @@ -327,7 +330,7 @@ def refresh_session( refresh_token (str): The refresh token that will be used to refresh the session audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for (must be equal to the 'aud' claim on the provided token) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token, refresh token, and all JWT claims Raise: @@ -340,7 +343,7 @@ def validate_and_refresh_session( session_token: str, refresh_token: str, audience: str | Iterable[str] | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Validate the session token and refresh it if it has expired, the session token will automatically be refreshed. Either the session_token or the refresh_token must be provided. @@ -352,7 +355,7 @@ def validate_and_refresh_session( refresh_token (str): The refresh token that will be used to refresh the session token, if needed audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for (must be equal to the 'aud' claim on the provided token) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token, refresh token, and all JWT claims Raise: @@ -362,7 +365,9 @@ def validate_and_refresh_session( session_token, refresh_token, audience ) - def logout(self, refresh_token: str) -> requests.Response: + def logout( + self, refresh_token: str + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: """ Logout user from current session and revoke the refresh_token. After calling this function, you must invalidate or remove any cookies you have created. @@ -370,7 +375,7 @@ def logout(self, refresh_token: str) -> requests.Response: Args: refresh_token (str): The refresh token - Return value (requests.Response): returns the response from the Descope server + Return value (Union[httpx.Response, Awaitable[httpx.Response]]): returns the response from the Descope server Raise: AuthException: Exception is raised if session is not authorized or another error occurs @@ -383,9 +388,12 @@ def logout(self, refresh_token: str) -> requests.Response: ) uri = EndpointsV1.logout_path - return self._auth.do_post(uri, {}, None, refresh_token) + response = self._auth.do_post(uri, {}, None, refresh_token) + return futu_apply(response, lambda response: response) - def logout_all(self, refresh_token: str) -> requests.Response: + def logout_all( + self, refresh_token: str + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: """ Logout user from all active sessions and revoke the refresh_token. After calling this function, you must invalidate or remove any cookies you have created. @@ -393,7 +401,7 @@ def logout_all(self, refresh_token: str) -> requests.Response: Args: refresh_token (str): The refresh token - Return value (requests.Response): returns the response from the Descope server + Return value (Union[httpx.Response, Awaitable[httpx.Response]]): returns the response from the Descope server Raise: AuthException: Exception is raised if session is not authorized or another error occurs @@ -406,9 +414,10 @@ def logout_all(self, refresh_token: str) -> requests.Response: ) uri = EndpointsV1.logout_all_path - return self._auth.do_post(uri, {}, None, refresh_token) + response = self._auth.do_post(uri, {}, None, refresh_token) + return futu_apply(response, lambda response: response) - def me(self, refresh_token: str) -> dict: + def me(self, refresh_token: str) -> Union[dict, Awaitable[dict]]: """ Retrieve user details for the refresh token. The returned data includes email, name, phone, list of loginIds and boolean flags for verifiedEmail, verifiedPhone. @@ -416,7 +425,7 @@ def me(self, refresh_token: str) -> dict: Args: refresh_token (str): The refresh token - Return value (dict): returns the user details from the server + Return value (Union[dict, Awaitable[dict]]): returns the user details from the server (email:str, name:str, phone:str, loginIds[str], verifiedEmail:bool, verifiedPhone:bool) Raise: @@ -431,16 +440,16 @@ def me(self, refresh_token: str) -> dict: uri = EndpointsV1.me_path response = self._auth.do_get( - uri=uri, params=None, allow_redirects=None, pswd=refresh_token + uri=uri, params=None, follow_redirects=None, pswd=refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) def my_tenants( self, refresh_token: str, dct: bool = False, ids: list[str] | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Retrieve tenant attributes that user belongs to, one of dct/ids must be populated . @@ -449,7 +458,7 @@ def my_tenants( ids (List[str]): Get the list of tenants refresh_token (str): The refresh token - Return value (dict): returns the tenant requested from the server + Return value (Union[dict, Awaitable[dict]]): returns the tenant requested from the server (id:str, name:str, customAttributes:dict) Raise: @@ -480,16 +489,16 @@ def my_tenants( uri = EndpointsV1.my_tenants_path response = self._auth.do_post(uri, body, None, refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) - def history(self, refresh_token: str) -> list[dict]: + def history(self, refresh_token: str) -> Union[list[dict], Awaitable[list[dict]]]: """ Retrieve user authentication history for the refresh token Args: refresh_token (str): The refresh token - Return value (List[dict]): + Return value (Union[list[dict], Awaitable[list[dict]]]): Return List in the format [ { @@ -513,16 +522,16 @@ def history(self, refresh_token: str) -> list[dict]: uri = EndpointsV1.history_path response = self._auth.do_get( - uri=uri, params=None, allow_redirects=None, pswd=refresh_token + uri=uri, params=None, follow_redirects=None, pswd=refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) def exchange_access_key( self, access_key: str, audience: str | Iterable[str] | None = None, login_options: AccessKeyLoginOptions | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Return a new session token for the given access key @@ -547,7 +556,7 @@ def select_tenant( self, tenant_id: str, refresh_token: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add to JWT a selected tenant claim @@ -555,7 +564,7 @@ def select_tenant( refresh_token (str): The refresh token that will be used to refresh the session token, if needed tenant_id (str): The tenant id to place on JWT - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token, refresh token, with the tenant id on the jwt Raise: diff --git a/descope/future_utils.py b/descope/future_utils.py new file mode 100644 index 000000000..404629265 --- /dev/null +++ b/descope/future_utils.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import asyncio +from typing import Any, Awaitable, Callable, TypeVar, Union + +T = TypeVar("T") + + +def futu_apply( + result_or_coro: Union[T, Awaitable[T]], modifier: Callable[[T], Any] +) -> Union[Any, Awaitable[Any]]: + if asyncio.iscoroutine(result_or_coro) or asyncio.isfuture(result_or_coro): + + async def process_async(): + result = await result_or_coro + return modifier(result) + + return process_async() + else: + # we ignore arg-type due to the check above. + return modifier(result_or_coro) # type: ignore[arg-type] + + +def futu_awaitable(result: T, as_awaitable: bool) -> Union[Any, Awaitable[Any]]: + if as_awaitable: + + async def awaitable_wrapper(): + return result + + return awaitable_wrapper() + + return result + + +async def futu_await(obj: Union[Any, Awaitable[Any]]) -> Any: + if asyncio.iscoroutine(obj) or asyncio.isfuture(obj): + return await obj + return obj diff --git a/descope/management/access_key.py b/descope/management/access_key.py index e51c65463..e5cdba7bc 100644 --- a/descope/management/access_key.py +++ b/descope/management/access_key.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import ( AssociatedTenant, MgmtV1, @@ -19,7 +20,7 @@ def create( custom_claims: Optional[dict] = None, description: Optional[str] = None, permitted_ips: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new access key. @@ -36,7 +37,7 @@ def create( description (str): an optional text the access key can hold. permitted_ips: (List[str]): An optional list of IP addresses or CIDR ranges that are allowed to use the access key. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "key": {}, @@ -65,19 +66,19 @@ def create( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load an existing access key. Args: id (str): The id of the access key to be loaded. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"key": {}} Containing the loaded access key information. @@ -90,19 +91,19 @@ def load( params={"id": id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search_all_access_keys( self, tenant_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all access keys. Args: tenant_ids (List[str]): Optional list of tenant IDs to filter by - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"keys": []} "keys" contains a list of all of the found users and their information @@ -117,14 +118,14 @@ def search_all_access_keys( {"tenantIds": tenant_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update( self, id: str, name: str, description: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing access key with the given various fields. IMPORTANT: id and name are mandatory fields. @@ -136,16 +137,17 @@ def update( Raise: AuthException: raised if update operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_update_path, {"id": id, "name": name, "description": description}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def deactivate( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Deactivate an existing access key. IMPORTANT: This deactivated key will not be usable from this stage. It will, however, persist, and can be activated again if needed. @@ -156,16 +158,17 @@ def deactivate( Raise: AuthException: raised if deactivation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_deactivate_path, {"id": id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def activate( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Activate an existing access key. IMPORTANT: Only deactivated keys can be activated again, and become usable once more. New access keys are active by default. @@ -176,16 +179,17 @@ def activate( Raise: AuthException: raised if activation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_activate_path, {"id": id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing access key. IMPORTANT: This action is irreversible. Use carefully. @@ -195,11 +199,12 @@ def delete( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_delete_path, {"id": id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) @staticmethod def _compose_create_body( diff --git a/descope/management/audit.py b/descope/management/audit.py index 324ff16b9..daabfad8c 100644 --- a/descope/management/audit.py +++ b/descope/management/audit.py @@ -1,7 +1,8 @@ from datetime import datetime -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -21,7 +22,7 @@ def search( text: Optional[str] = None, from_ts: Optional[datetime] = None, to_ts: Optional[datetime] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search the audit trail up to last 30 days based on given parameters @@ -40,7 +41,7 @@ def search( from_ts (datetime): Retrieve records newer than given time but not older than 30 days to_ts (datetime): Retrieve records older than given time - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "audits": [ @@ -96,9 +97,14 @@ def search( body=body, pswd=self._auth.management_key, ) - return { - "audits": list(map(Audit._convert_audit_record, response.json()["audits"])) - } + return futu_apply( + response, + lambda response: { + "audits": list( + map(Audit._convert_audit_record, response.json()["audits"]) + ) + }, + ) def create_event( self, @@ -108,7 +114,7 @@ def create_event( tenant_id: str, user_id: Optional[str] = None, data: Optional[dict] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Create audit event based on given parameters @@ -134,11 +140,15 @@ def create_event( if data is not None: body["data"] = data - self._auth.do_post( + response = self._auth.do_post( MgmtV1.audit_create_event, body=body, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: None, + ) @staticmethod def _convert_audit_record(a: dict) -> dict: diff --git a/descope/management/authz.py b/descope/management/authz.py index f2d6816a2..0ef703781 100644 --- a/descope/management/authz.py +++ b/descope/management/authz.py @@ -1,12 +1,15 @@ from datetime import datetime, timezone -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 class Authz(AuthBase): - def save_schema(self, schema: dict, upgrade: bool = False): + def save_schema( + self, schema: dict, upgrade: bool = False + ) -> Union[None, Awaitable[None]]: """ Create or update the ReBAC schema. In case of update, will update only given namespaces and will not delete namespaces unless upgrade flag is true. @@ -40,28 +43,30 @@ def save_schema(self, schema: dict, upgrade: bool = False): Raise: AuthException: raised if saving fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_schema_save, {"schema": schema, "upgrade": upgrade}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) - def delete_schema(self): + def delete_schema(self) -> Union[None, Awaitable[None]]: """ Delete the schema for the project which will also delete all relations. Raise: AuthException: raised if delete schema fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_schema_delete, None, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) - def load_schema(self) -> dict: + def load_schema(self) -> Union[dict, Awaitable[dict]]: """ Load the schema for the project - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format of schema as above (see save_schema) Raise: AuthException: raised if load schema fails @@ -71,11 +76,11 @@ def load_schema(self) -> dict: None, pswd=self._auth.management_key, ) - return response.json()["schema"] + return futu_apply(response, lambda response: response.json()["schema"]) def save_namespace( self, namespace: dict, old_name: str = "", schema_name: str = "" - ): + ) -> Union[None, Awaitable[None]]: """ Create or update the given namespace Will not delete relation definitions not mentioned in the namespace. @@ -91,13 +96,16 @@ def save_namespace( body["oldName"] = old_name if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_ns_save, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) - def delete_namespace(self, name: str, schema_name: str = ""): + def delete_namespace( + self, name: str, schema_name: str = "" + ) -> Union[None, Awaitable[None]]: """ delete_namespace will also delete the relevant relations. Args: @@ -109,11 +117,12 @@ def delete_namespace(self, name: str, schema_name: str = ""): body: dict[str, Any] = {"name": name} if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_ns_delete, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def save_relation_definition( self, @@ -121,7 +130,7 @@ def save_relation_definition( namespace: str, old_name: str = "", schema_name: str = "", - ): + ) -> Union[None, Awaitable[None]]: """ Create or update the given relation definition Will not delete relation definitions not mentioned in the namespace. @@ -141,15 +150,16 @@ def save_relation_definition( body["oldName"] = old_name if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_rd_save, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_relation_definition( self, name: str, namespace: str, schema_name: str = "" - ): + ) -> Union[None, Awaitable[None]]: """ delete_relation_definition will also delete the relevant relations. Args: @@ -162,16 +172,17 @@ def delete_relation_definition( body: dict[str, Any] = {"name": name, "namespace": namespace} if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_rd_delete, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def create_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Create the given relations based on the existing schema Args: @@ -202,18 +213,19 @@ def create_relations( Raise: AuthException: raised if create relations fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_re_create, { "relations": relations, }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Delete the given relations based on the existing schema Args: @@ -221,18 +233,19 @@ def delete_relations( Raise: AuthException: raised if delete relations fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_re_delete, { "relations": relations, }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_relations_for_resources( self, resources: List[str], - ): + ) -> Union[None, Awaitable[None]]: """ Delete all relations to the given resources Args: @@ -240,18 +253,19 @@ def delete_relations_for_resources( Raise: AuthException: raised if delete relations for resources fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_re_delete_resources, { "resources": resources, }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def has_relations( self, relation_queries: List[dict], - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Queries the given relations to see if they exist returning true if they do Args: @@ -263,7 +277,7 @@ def has_relations( "target": "the target that has the relation - usually users or other resources" } - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List in the format [ { @@ -284,11 +298,11 @@ def has_relations( }, pswd=self._auth.management_key, ) - return response.json()["relationQueries"] + return futu_apply(response, lambda response: response.json()["relationQueries"]) def who_can_access( self, resource: str, relation_definition: str, namespace: str - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Finds the list of targets (usually users) who can access the given resource with the given RD Args: @@ -296,7 +310,7 @@ def who_can_access( relation_definition (str): the RD we are checking namespace (str): the namespace for the RD - Return value (List[str]): list of targets (user IDs usually that have the access) + Return value (Union[List[dict], Awaitable[List[dict]]]): list of targets (user IDs usually that have the access) Raise: AuthException: raised if query fails """ @@ -309,15 +323,17 @@ def who_can_access( }, pswd=self._auth.management_key, ) - return response.json()["targets"] + return futu_apply(response, lambda response: response.json()["targets"]) - def resource_relations(self, resource: str) -> List[dict]: + def resource_relations( + self, resource: str + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all defined relations (not recursive) on the given resource. Args: resource (str): the resource we are listing relations for - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -327,15 +343,17 @@ def resource_relations(self, resource: str) -> List[dict]: {"resource": resource}, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) - def targets_relations(self, targets: List[str]) -> List[dict]: + def targets_relations( + self, targets: List[str] + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all defined relations (not recursive) for the given targets. Args: targets (List[str]): the list of targets we are returning the relations for - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -345,15 +363,17 @@ def targets_relations(self, targets: List[str]) -> List[dict]: {"targets": targets}, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) - def what_can_target_access(self, target: str) -> List[dict]: + def what_can_target_access( + self, target: str + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all relations for the given target including derived relations from the schema tree. Args: target (str): the target we are returning the relations for - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -363,11 +383,11 @@ def what_can_target_access(self, target: str) -> List[dict]: {"target": target}, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) def what_can_target_access_with_relation( self, target: str, relation_definition: str, namespace: str - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all resources that the target has the given relation to including all derived relations Args: @@ -375,7 +395,7 @@ def what_can_target_access_with_relation( relation_definition (str): the RD we are checking namespace (str): the namespace for the RD - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -389,15 +409,17 @@ def what_can_target_access_with_relation( }, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) - def get_modified(self, since: Optional[datetime] = None) -> dict: + def get_modified( + self, since: Optional[datetime] = None + ) -> Union[dict, Awaitable[dict]]: """ Get all targets and resources changed since the given date. Args: since (datetime): only return changes from this given datetime - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Dict including "resources" list of strings, "targets" list of strings and "schemaChanged" bool Raise: AuthException: raised if query fails @@ -413,4 +435,4 @@ def get_modified(self, since: Optional[datetime] = None) -> dict: }, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) diff --git a/descope/management/fga.py b/descope/management/fga.py index df7d5d041..1b9f40a1e 100644 --- a/descope/management/fga.py +++ b/descope/management/fga.py @@ -1,11 +1,12 @@ -from typing import List +from typing import Awaitable, List, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 class FGA(AuthBase): - def save_schema(self, schema: str): + def save_schema(self, schema: str) -> Union[None, Awaitable[None]]: """ Create or update an FGA schema. Args: @@ -40,17 +41,18 @@ def save_schema(self, schema: str): Raise: AuthException: raised if saving fails """ - self._auth.do_post_with_custom_base_url( + res = self._auth.do_post_with_custom_base_url( MgmtV1.fga_save_schema, {"dsl": schema}, custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) + return futu_apply(res, lambda res: None) def create_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Create the given relations based on the existing schema Args: @@ -65,7 +67,7 @@ def create_relations( Raise: AuthException: raised if create relations fails """ - self._auth.do_post_with_custom_base_url( + res = self._auth.do_post_with_custom_base_url( MgmtV1.fga_create_relations, { "tuples": relations, @@ -73,11 +75,12 @@ def create_relations( custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) + return futu_apply(res, lambda res: None) def delete_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Delete the given relations based on the existing schema Args: @@ -85,7 +88,7 @@ def delete_relations( Raise: AuthException: raised if delete relations fails """ - self._auth.do_post_with_custom_base_url( + res = self._auth.do_post_with_custom_base_url( MgmtV1.fga_delete_relations, { "tuples": relations, @@ -93,11 +96,12 @@ def delete_relations( custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) + return futu_apply(res, lambda res: None) def check( self, relations: List[dict], - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Queries the given relations to see if they exist returning true if they do Args: @@ -110,7 +114,7 @@ def check( "targetType": "the type of the target (namespace)" } - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List in the format [ { @@ -135,14 +139,22 @@ def check( custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) - return list( - map( - lambda tuple: {"relation": tuple["tuple"], "allowed": tuple["allowed"]}, - response.json()["tuples"], - ) + return futu_apply( + response, + lambda response: list( + map( + lambda tuple: { + "relation": tuple["tuple"], + "allowed": tuple["allowed"], + }, + response.json()["tuples"], + ) + ), ) - def load_resources_details(self, resource_identifiers: List[dict]) -> List[dict]: + def load_resources_details( + self, resource_identifiers: List[dict] + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Load details for the given resource identifiers. Args: @@ -155,16 +167,21 @@ def load_resources_details(self, resource_identifiers: List[dict]) -> List[dict] {"resourceIdentifiers": resource_identifiers}, pswd=self._auth.management_key, ) - return response.json().get("resourcesDetails", []) + return futu_apply( + response, lambda response: response.json().get("resourcesDetails", []) + ) - def save_resources_details(self, resources_details: List[dict]) -> None: + def save_resources_details( + self, resources_details: List[dict] + ) -> Union[None, Awaitable[None]]: """ Save details for the given resources. Args: resources_details (List[dict]): list of dicts each containing 'resourceId' and 'resourceType' plus optionally containing metadata fields such as 'displayName'. """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.fga_resources_save, {"resourcesDetails": resources_details}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) diff --git a/descope/management/flow.py b/descope/management/flow.py index 80dad519c..9ef32540c 100644 --- a/descope/management/flow.py +++ b/descope/management/flow.py @@ -1,17 +1,18 @@ -from typing import List +from typing import Awaitable, List, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 class Flow(AuthBase): def list_flows( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ List all project flows - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "flows": [{"id": "", "name": "", "description": "", "disabled": False}], total: number} @@ -23,12 +24,15 @@ def list_flows( None, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def delete_flows( self, flow_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Delete flows by the given ids @@ -45,19 +49,22 @@ def delete_flows( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def export_flow( self, flow_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Export the given flow id flow and screens. Args: flow_id (str): the flow id to export. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "flow": {"id": "", "name": "", "description": "", "disabled": False, "etag": "", "dsl": {}}, screens: [{ "id": "", "inputs": [], "interactions": [] }] } @@ -71,14 +78,17 @@ def export_flow( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def import_flow( self, flow_id: str, flow: dict, screens: List[dict], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Import the given flow and screens to the flow id. Imoprtant: This will override the current project flow by the given id, treat with caution. @@ -90,7 +100,7 @@ def import_flow( screens (List[dict]): the flow screens to import. list of dictss in the format: { "id": "", "inputs": [], "interactions": [] } - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "flow": {"id": "", "name": "", "description": "", "disabled": False, "etag": "", "dsl": {}}, screens: [{ "id": "", "inputs": [], "interactions": [] }] } @@ -106,15 +116,18 @@ def import_flow( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def export_theme( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Export the current project theme. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": "", "cssTemplate": {} } @@ -126,12 +139,15 @@ def export_theme( {}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def import_theme( self, theme: dict, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Import the given theme as the current project theme. Imoprtant: This will override the current project theme, treat with caution. @@ -140,7 +156,7 @@ def import_theme( theme (Theme): the theme to import. dict in the format {"id": "", "cssTemplate": {} } - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": "", "cssTemplate": {} } @@ -154,4 +170,7 @@ def import_theme( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) diff --git a/descope/management/group.py b/descope/management/group.py index c4dba3b54..d86e9005f 100644 --- a/descope/management/group.py +++ b/descope/management/group.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -8,14 +9,14 @@ class Group(AuthBase): def load_all_groups( self, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all groups for a specific tenant id. Args: tenant_id (str): Tenant ID to load groups from. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format [ { @@ -42,14 +43,17 @@ def load_all_groups( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def load_all_groups_for_members( self, tenant_id: str, user_ids: Optional[List[str]] = None, login_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all groups for the provided user IDs or login IDs. @@ -58,7 +62,7 @@ def load_all_groups_for_members( user_ids (List[str]): Optional List of user IDs, with the format of "U2J5ES9S8TkvCgOvcrkpzUgVTEBM" (example), which can be found on the user's JWT. login_ids (List[str]): Optional List of login IDs, how the users identify when logging in. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format [ { @@ -90,13 +94,16 @@ def load_all_groups_for_members( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def load_all_group_members( self, tenant_id: str, group_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all members of the provided group id. @@ -104,7 +111,7 @@ def load_all_group_members( tenant_id (str): Tenant ID to load groups from. group_id (str): Group ID to load members for. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format [ { @@ -132,4 +139,7 @@ def load_all_group_members( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) diff --git a/descope/management/jwt.py b/descope/management/jwt.py index 9adaa9338..4fd21e731 100644 --- a/descope/management/jwt.py +++ b/descope/management/jwt.py @@ -1,6 +1,7 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException from descope.management.common import ( MgmtLoginOptions, @@ -14,7 +15,7 @@ class JWT(AuthBase): def update_jwt( self, jwt: str, custom_claims: dict, refresh_duration: int = 0 - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Given a valid JWT, update it with custom claims, and update its authz claims as well @@ -23,7 +24,7 @@ def update_jwt( custom_claims (dict): Custom claims to add to JWT, system claims will be filtered out refresh_duration (int): duration in seconds for which the new JWT will be valid - Return value (str): the newly updated JWT + Return value (Union[str, Awaitable[str]]): the newly updated JWT Raise: AuthException: raised if update failed @@ -39,7 +40,10 @@ def update_jwt( }, pswd=self._auth.management_key, ) - return response.json().get("jwt", "") + return futu_apply( + response, + lambda response: response.json().get("jwt", ""), + ) def impersonate( self, @@ -49,7 +53,7 @@ def impersonate( custom_claims: Optional[dict] = None, tenant_id: Optional[str] = None, refresh_duration: Optional[int] = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Impersonate to another user @@ -61,7 +65,7 @@ def impersonate( tenant_id (str): tenant id to set on DCT claim. refresh_duration (int): duration in seconds for which the new JWT will be valid - Return value (str): A JWT of the impersonated user + Return value (Union[str, Awaitable[str]]): A JWT of the impersonated user Raise: AuthException: raised if update failed @@ -86,7 +90,10 @@ def impersonate( }, pswd=self._auth.management_key, ) - return response.json().get("jwt", "") + return futu_apply( + response, + lambda response: response.json().get("jwt", ""), + ) def stop_impersonation( self, @@ -94,7 +101,7 @@ def stop_impersonation( custom_claims: Optional[dict] = None, tenant_id: Optional[str] = None, refresh_duration: Optional[int] = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Stop impersonation and return to the original user Args: @@ -103,7 +110,7 @@ def stop_impersonation( tenant_id (str): tenant id to set on DCT claim. refresh_duration (int): duration in seconds for which the new JWT will be valid - Return value (str): A JWT of the actor + Return value (Union[str, Awaitable[str]]): A JWT of the actor Raise: AuthException: raised if update failed @@ -121,11 +128,14 @@ def stop_impersonation( }, pswd=self._auth.management_key, ) - return response.json().get("jwt", "") + return futu_apply( + response, + lambda response: response.json().get("jwt", ""), + ) def sign_in( self, login_id: str, login_options: Optional[MgmtLoginOptions] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for a user, simulating a signin request. @@ -158,16 +168,19 @@ def sign_in( }, pswd=self._auth.management_key, ) - resp = response.json() - jwt_response = self._auth.generate_jwt_response(resp, None, None) - return jwt_response + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), None, None + ), + ) def sign_up( self, login_id: str, user: Optional[MgmtUserRequest] = None, signup_options: Optional[MgmtSignUpOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for a user, simulating a signup request. @@ -186,7 +199,7 @@ def sign_up_or_in( login_id: str, user: Optional[MgmtUserRequest] = None, signup_options: Optional[MgmtSignUpOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for a user, simulating a signup or in request. @@ -205,7 +218,7 @@ def _sign_up_internal( endpoint: str, user: Optional[MgmtUserRequest] = None, signup_options: Optional[MgmtSignUpOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if user is None: user = MgmtUserRequest() @@ -230,16 +243,19 @@ def _sign_up_internal( }, pswd=self._auth.management_key, ) - resp = response.json() - jwt_response = self._auth.generate_jwt_response(resp, None, None) - return jwt_response + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), None, None + ), + ) def anonymous( self, custom_claims: Optional[dict] = None, tenant_id: Optional[str] = None, refresh_duration: Optional[int] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for an anonymous user. @@ -257,6 +273,13 @@ def anonymous( }, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: self._anonymous_jwt_response(response), + ) + + def _anonymous_jwt_response(self, response): + """Helper method to process anonymous JWT response""" resp = response.json() jwt_response = self._auth.generate_jwt_response(resp, None, None) del jwt_response["firstSeen"] diff --git a/descope/management/outbound_application.py b/descope/management/outbound_application.py index 2b9bc2deb..71fbf4a6b 100644 --- a/descope/management/outbound_application.py +++ b/descope/management/outbound_application.py @@ -1,8 +1,9 @@ -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase from descope.auth import Auth from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException # noqa: F401 +from descope.future_utils import futu_apply from descope.management.common import ( AccessType, MgmtV1, @@ -24,7 +25,7 @@ def fetch_token_by_scopes( scopes: List[str], options: Optional[dict] = None, tenant_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching token by scopes.""" uri = MgmtV1.outbound_application_fetch_token_by_scopes_path response = auth_instance.do_post( @@ -38,7 +39,7 @@ def fetch_token_by_scopes( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def fetch_token( @@ -48,7 +49,7 @@ def fetch_token( user_id: str, tenant_id: Optional[str] = None, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching token.""" uri = MgmtV1.outbound_application_fetch_token_path response = auth_instance.do_post( @@ -61,7 +62,7 @@ def fetch_token( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def fetch_tenant_token_by_scopes( @@ -71,7 +72,7 @@ def fetch_tenant_token_by_scopes( tenant_id: str, scopes: List[str], options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching tenant token by scopes.""" uri = MgmtV1.outbound_application_fetch_tenant_token_by_scopes_path response = auth_instance.do_post( @@ -84,7 +85,7 @@ def fetch_tenant_token_by_scopes( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def fetch_tenant_token( @@ -93,7 +94,7 @@ def fetch_tenant_token( app_id: str, tenant_id: str, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching tenant token.""" uri = MgmtV1.outbound_application_fetch_tenant_token_path response = auth_instance.do_post( @@ -105,7 +106,7 @@ def fetch_tenant_token( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) class OutboundApplication(AuthBase): @@ -129,7 +130,7 @@ def create_application( pkce: Optional[bool] = None, access_type: Optional[AccessType] = None, prompt: Optional[List[PromptType]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new outbound application with the given name. Outbound application IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -154,7 +155,7 @@ def create_application( access_type (AccessType): Optional OAuth access type. prompt (List[PromptType]): Optional OAuth prompt parameters. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"app": {"id": , "name": , "description": , "logo": }} @@ -186,7 +187,7 @@ def create_application( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_application( self, @@ -208,7 +209,7 @@ def update_application( pkce: Optional[bool] = None, access_type: Optional[AccessType] = None, prompt: Optional[List[PromptType]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update an existing outbound application with the given parameters. IMPORTANT: All parameters are used as overrides to the existing outbound application. Empty fields will override populated fields. Use carefully. @@ -233,7 +234,7 @@ def update_application( access_type (AccessType): Optional OAuth access type. prompt (List[PromptType]): Optional OAuth prompt parameters. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"app": {"id": , "name": , "description": , "logo": }} @@ -267,12 +268,12 @@ def update_application( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def delete_application( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing outbound application. IMPORTANT: This action is irreversible. Use carefully. @@ -283,19 +284,20 @@ def delete_application( AuthException: raised if deletion operation fails """ uri = MgmtV1.outbound_application_delete_path - self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + response = self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + return futu_apply(response, lambda response: None) def load_application( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load outbound application by id. Args: id (str): The ID of the outbound application to load. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"app": {"id": , "name": , "description": , "logo": }} Containing the loaded outbound application information. @@ -307,15 +309,15 @@ def load_application( uri=f"{MgmtV1.outbound_application_load_path}/{id}", pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_all_applications( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all outbound applications. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"apps": [{"id": , "name": , "description": , "logo": }, ...]} Containing the loaded outbound applications information. @@ -327,7 +329,7 @@ def load_all_applications( uri=MgmtV1.outbound_application_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def fetch_token_by_scopes( self, @@ -336,7 +338,7 @@ def fetch_token_by_scopes( scopes: List[str], options: Optional[dict] = None, tenant_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user with specific scopes. @@ -347,22 +349,26 @@ def fetch_token_by_scopes( options (dict): Optional token options. tenant_id (str): Optional tenant ID. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_token_by_scopes( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - user_id, - scopes, - options, - tenant_id, + uri = MgmtV1.outbound_application_fetch_token_by_scopes_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "userId": user_id, + "scopes": scopes, + "options": options, + "tenantId": tenant_id, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) def fetch_token( self, @@ -370,7 +376,7 @@ def fetch_token( user_id: str, tenant_id: Optional[str] = None, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user. @@ -380,21 +386,25 @@ def fetch_token( tenant_id (str): Optional tenant ID. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_token( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - user_id, - tenant_id, - options, + uri = MgmtV1.outbound_application_fetch_token_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "userId": user_id, + "tenantId": tenant_id, + "options": options, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) def fetch_tenant_token_by_scopes( self, @@ -402,7 +412,7 @@ def fetch_tenant_token_by_scopes( tenant_id: str, scopes: List[str], options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant with specific scopes. @@ -412,28 +422,32 @@ def fetch_tenant_token_by_scopes( scopes (List[str]): List of scopes to include in the token. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_tenant_token_by_scopes( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - tenant_id, - scopes, - options, + uri = MgmtV1.outbound_application_fetch_tenant_token_by_scopes_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "tenantId": tenant_id, + "scopes": scopes, + "options": options, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) def fetch_tenant_token( self, app_id: str, tenant_id: str, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant. @@ -442,20 +456,24 @@ def fetch_tenant_token( tenant_id (str): The ID of the tenant. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_tenant_token( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - tenant_id, - options, + uri = MgmtV1.outbound_application_fetch_tenant_token_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "tenantId": tenant_id, + "options": options, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_update_body( @@ -539,7 +557,7 @@ def fetch_token_by_scopes( scopes: List[str], options: Optional[dict] = None, tenant_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user with specific scopes. @@ -551,7 +569,7 @@ def fetch_token_by_scopes( options (dict): Optional token options. tenant_id (str): Optional tenant ID. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -570,7 +588,7 @@ def fetch_token( user_id: str, tenant_id: Optional[str] = None, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user. @@ -581,7 +599,7 @@ def fetch_token( tenant_id (str): Optional tenant ID. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -600,7 +618,7 @@ def fetch_tenant_token_by_scopes( tenant_id: str, scopes: List[str], options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant with specific scopes. @@ -611,7 +629,7 @@ def fetch_tenant_token_by_scopes( scopes (List[str]): List of scopes to include in the token. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -625,7 +643,7 @@ def fetch_tenant_token_by_scopes( def fetch_tenant_token( self, token: str, app_id: str, tenant_id: str, options: Optional[dict] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant. @@ -635,7 +653,7 @@ def fetch_tenant_token( tenant_id (str): The ID of the tenant. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} diff --git a/descope/management/permission.py b/descope/management/permission.py index 273d96ed7..269a3020f 100644 --- a/descope/management/permission.py +++ b/descope/management/permission.py @@ -1,6 +1,7 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -9,7 +10,7 @@ def create( self, name: str, description: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Create a new permission. @@ -20,18 +21,19 @@ def create( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.permission_create_path, {"name": name, "description": description}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def update( self, name: str, new_name: str, description: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing permission with the given various fields. IMPORTANT: All parameters are used as overrides to the existing permission. Empty fields will override populated fields. Use carefully. @@ -44,16 +46,17 @@ def update( Raise: AuthException: raised if update operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.permission_update_path, {"name": name, "newName": new_name, "description": description}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, name: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing permission. IMPORTANT: This action is irreversible. Use carefully. @@ -63,19 +66,20 @@ def delete( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.permission_delete_path, {"name": name}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all permissions. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"permissions": [{"name": , "description": , "systemDefault":}]} Containing the loaded permission information. @@ -87,4 +91,4 @@ def load_all( uri=MgmtV1.permission_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) diff --git a/descope/management/project.py b/descope/management/project.py index ba9fa2b76..2ca608e91 100644 --- a/descope/management/project.py +++ b/descope/management/project.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -8,7 +9,7 @@ class Project(AuthBase): def update_name( self, name: str, - ): + ) -> Union[None, Awaitable[None]]: """ Update the current project name. @@ -17,18 +18,22 @@ def update_name( Raise: AuthException: raised if operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.project_update_name, { "name": name, }, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: None, + ) def update_tags( self, tags: List[str], - ): + ) -> Union[None, Awaitable[None]]: """ Update the current project tags. @@ -37,21 +42,25 @@ def update_tags( Raise: AuthException: raised if operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.project_update_tags, { "tags": tags, }, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: None, + ) def list_projects( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ List of all the projects in the company. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"projects": []} "projects" contains a list of all of the projects and their information @@ -64,12 +73,17 @@ def list_projects( {}, pswd=self._auth.management_key, ) - resp = response.json() + return futu_apply( + response, + lambda response: self._process_list_projects_response(response), + ) + def _process_list_projects_response(self, response): + """Helper method to process list_projects response""" + resp = response.json() projects = resp["projects"] # Apply the function to the projects list formatted_projects = self.remove_tag_field(projects) - # Return the same structure with 'tag' removed result = {"projects": formatted_projects} return result @@ -79,7 +93,7 @@ def clone( name: str, environment: Optional[str] = None, tags: Optional[List[str]] = None, - ): + ) -> Union[dict, Awaitable[dict]]: """ Clone the current project, including its settings and configurations. - This action is supported only with a pro license or above. @@ -90,7 +104,7 @@ def clone( environment (str): Optional state for the project. Currently, only the "production" tag is supported. tags(list[str]): Optional free text tags. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict Containing the new project details (name, id, environment and tag). Raise: @@ -105,11 +119,14 @@ def clone( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def export_project( self, - ): + ) -> Union[dict, Awaitable[dict]]: """ Exports all settings and configurations for a project and returns the raw JSON files response as a dictionary. @@ -117,7 +134,7 @@ def export_project( - Users, tenants and access keys are not cloned. - Secrets, keys and tokens are not stripped from the exported data. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict Containing the exported JSON files payload. Raise: @@ -128,12 +145,15 @@ def export_project( {}, pswd=self._auth.management_key, ) - return response.json()["files"] + return futu_apply( + response, + lambda response: response.json()["files"], + ) def import_project( self, files: dict, - ): + ) -> Union[None, Awaitable[None]]: """ Imports all settings and configurations for a project overriding any current configuration. @@ -147,14 +167,17 @@ def import_project( Raise: AuthException: raised if import operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.project_import, { "files": files, }, pswd=self._auth.management_key, ) - return + return futu_apply( + response, + lambda response: None, + ) # Function to remove 'tag' field from each project def remove_tag_field(self, projects): diff --git a/descope/management/role.py b/descope/management/role.py index 2c11f2865..017d78265 100644 --- a/descope/management/role.py +++ b/descope/management/role.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -12,7 +13,7 @@ def create( permission_names: Optional[List[str]] = None, tenant_id: Optional[str] = None, default: Optional[bool] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Create a new role. @@ -28,7 +29,7 @@ def create( """ permission_names = [] if permission_names is None else permission_names - self._auth.do_post( + response = self._auth.do_post( MgmtV1.role_create_path, { "name": name, @@ -39,6 +40,7 @@ def create( }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def update( self, @@ -48,7 +50,7 @@ def update( permission_names: Optional[List[str]] = None, tenant_id: Optional[str] = None, default: Optional[bool] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing role with the given various fields. IMPORTANT: All parameters are used as overrides to the existing role. Empty fields will override populated fields. Use carefully. @@ -65,7 +67,7 @@ def update( AuthException: raised if update operation fails """ permission_names = [] if permission_names is None else permission_names - self._auth.do_post( + response = self._auth.do_post( MgmtV1.role_update_path, { "name": name, @@ -77,12 +79,13 @@ def update( }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, name: str, tenant_id: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing role. IMPORTANT: This action is irreversible. Use carefully. @@ -92,19 +95,20 @@ def delete( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.role_delete_path, {"name": name, "tenantId": tenant_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all roles. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"roles": [{"name": , "description": , "permissionNames":[]}] } Containing the loaded role information. @@ -116,7 +120,7 @@ def load_all( uri=MgmtV1.role_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search( self, @@ -125,7 +129,7 @@ def search( role_name_like: Optional[str] = None, permission_names: Optional[List[str]] = None, include_project_roles: Optional[bool] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search roles based on the given filters. @@ -135,7 +139,7 @@ def search( role_name_like (str): Return roles that contain the given string ignoring case permission_names (List[str]): Only return roles that have the given permissions - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"roles": [{"name": , "description": , "permissionNames":[]}] } Containing the loaded role information. @@ -160,4 +164,4 @@ def search( body, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) diff --git a/descope/management/sso_application.py b/descope/management/sso_application.py index c3173ee2d..1253963e2 100644 --- a/descope/management/sso_application.py +++ b/descope/management/sso_application.py @@ -1,6 +1,7 @@ -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import ( MgmtV1, SAMLIDPAttributeMappingInfo, @@ -20,7 +21,7 @@ def create_oidc_application( logo: Optional[str] = None, enabled: Optional[bool] = True, force_authentication: Optional[bool] = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new OIDC sso application with the given name. SSO application IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -34,7 +35,7 @@ def create_oidc_application( enabled (bool): Optional (default True) does the sso application will be enabled or disabled. force_authentication (bool): Optional determine if the IdP should force the user to re-authenticate. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": } @@ -55,7 +56,7 @@ def create_oidc_application( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def create_saml_application( self, @@ -78,7 +79,7 @@ def create_saml_application( default_relay_state: Optional[str] = None, force_authentication: Optional[bool] = False, logout_redirect_url: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new SAML sso application with the given name. SSO application IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -104,7 +105,7 @@ def create_saml_application( force_authentication (bool): Optional determine if the IdP should force the user to re-authenticate. logout_redirect_url (str): Optional Target URL to which the user will be redirected upon logout completion. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": } @@ -153,7 +154,7 @@ def create_saml_application( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_oidc_application( self, @@ -164,7 +165,7 @@ def update_oidc_application( logo: Optional[str] = None, enabled: Optional[bool] = True, force_authentication: Optional[bool] = False, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing OIDC sso application with the given parameters. IMPORTANT: All parameters are used as overrides to the existing sso application. Empty fields will override populated fields. Use carefully. @@ -183,7 +184,7 @@ def update_oidc_application( """ uri = MgmtV1.sso_application_oidc_update_path - self._auth.do_post( + response = self._auth.do_post( uri, SSOApplication._compose_create_update_oidc_body( name, @@ -196,6 +197,7 @@ def update_oidc_application( ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def update_saml_application( self, @@ -218,7 +220,7 @@ def update_saml_application( default_relay_state: Optional[str] = None, force_authentication: Optional[bool] = False, logout_redirect_url: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing SAML sso application with the given parameters. IMPORTANT: All parameters are used as overrides to the existing sso application. Empty fields will override populated fields. Use carefully. @@ -264,7 +266,7 @@ def update_saml_application( ) uri = MgmtV1.sso_application_saml_update_path - self._auth.do_post( + response = self._auth.do_post( uri, SSOApplication._compose_create_update_saml_body( name, @@ -289,11 +291,12 @@ def update_saml_application( ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing sso application. IMPORTANT: This action is irreversible. Use carefully. @@ -304,19 +307,20 @@ def delete( AuthException: raised if deletion operation fails """ uri = MgmtV1.sso_application_delete_path - self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + response = self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + return futu_apply(response, lambda response: None) def load( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load sso application by id. Args: id (str): The ID of the sso application to load. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id":"","name":"","description":"","enabled":true,"logo":"","appType":"saml","samlSettings":{"loginPageUrl":"","idpCert":"","useMetadataInfo":true,"metadataUrl":"","entityId":"","acsUrl":"","certificate":"","attributeMapping":[{"name":"email","type":"","value":"attrVal1"}],"groupsMapping":[{"name":"grp1","type":"","filterType":"roles","value":"","roles":[{"id":"myRoleId","name":"myRole"}]}],"idpMetadataUrl":"","idpEntityId":"","idpSsoUrl":"","acsAllowedCallbacks":[],"subjectNameIdType":"","subjectNameIdFormat":"", "defaultRelayState":"", "forceAuthentication": false, "idpLogoutUrl": "", "logoutRedirectUrl": ""},"oidcSettings":{"loginPageUrl":"","issuer":"","discoveryUrl":"", "forceAuthentication":false}} Containing the loaded sso application information. @@ -329,15 +333,15 @@ def load( params={"id": id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all sso applications. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "apps": [ @@ -354,7 +358,7 @@ def load_all( uri=MgmtV1.sso_application_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_update_oidc_body( diff --git a/descope/management/sso_settings.py b/descope/management/sso_settings.py index 01d0fc46f..12f4b0d7f 100644 --- a/descope/management/sso_settings.py +++ b/descope/management/sso_settings.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -164,14 +165,14 @@ class SSOSettings(AuthBase): def load_settings( self, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load SSO setting for the provided tenant_id. Args: tenant_id (str): The tenant ID of the desired SSO Settings - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Containing the loaded SSO settings information. Return dict in the format: {"tenant": {"id": "T2AAAA", "name": "myTenantName", "selfProvisioningDomains": [], "customAttributes": {}, "authType": "saml", "domains": ["lulu", "kuku"]}, "saml": {"idpEntityId": "", "idpSSOUrl": "", "idpCertificate": "", "idpMetadataUrl": "https://dummy.com/metadata", "spEntityId": "", "spACSUrl": "", "spCertificate": "", "attributeMapping": {"name": "name", "email": "email", "username": "", "phoneNumber": "phone", "group": "", "givenName": "", "middleName": "", "familyName": "", "picture": "", "customAttributes": {}}, "groupsMapping": [], "redirectUrl": ""}, "oidc": {"name": "", "clientId": "", "clientSecret": "", "redirectUrl": "", "authUrl": "", "tokenUrl": "", "userDataUrl": "", "scope": [], "JWKsUrl": "", "userAttrMapping": {"loginId": "sub", "username": "", "name": "name", "email": "email", "phoneNumber": "phone_number", "verifiedEmail": "email_verified", "verifiedPhone": "phone_number_verified", "picture": "picture", "givenName": "given_name", "middleName": "middle_name", "familyName": "family_name"}, "manageProviderTokens": False, "callbackDomain": "", "prompt": [], "grantType": "authorization_code", "issuer": ""}} @@ -184,12 +185,12 @@ def load_settings( params={"tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def delete_settings( self, tenant_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete SSO setting for the provided tenant_id. @@ -199,18 +200,19 @@ def delete_settings( Raise: AuthException: raised if delete operation fails """ - self._auth.do_delete( + response = self._auth.do_delete( MgmtV1.sso_settings_path, {"tenantId": tenant_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def configure_oidc_settings( self, tenant_id: str, settings: SSOOIDCSettings, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Configure SSO OIDC settings for a tenant. @@ -223,13 +225,14 @@ def configure_oidc_settings( AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_configure_oidc_settings, SSOSettings._compose_configure_oidc_settings_body( tenant_id, settings, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def configure_saml_settings( self, @@ -237,7 +240,7 @@ def configure_saml_settings( settings: SSOSAMLSettings, redirect_url: Optional[str] = None, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Configure SSO SAML settings for a tenant. @@ -251,13 +254,14 @@ def configure_saml_settings( AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_configure_saml_settings, SSOSettings._compose_configure_saml_settings_body( tenant_id, settings, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def configure_saml_settings_by_metadata( self, @@ -265,7 +269,7 @@ def configure_saml_settings_by_metadata( settings: SSOSAMLSettingsByMetadata, redirect_url: Optional[str] = None, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Configure SSO SAML settings for a tenant by fetching them from an IDP metadata URL. @@ -279,19 +283,20 @@ def configure_saml_settings_by_metadata( AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_configure_saml_by_metadata_settings, SSOSettings._compose_configure_saml_settings_by_metadata_body( tenant_id, settings, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) # DEPRECATED def get_settings( self, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ DEPRECATED (use load_settings(..) function instead) @@ -300,7 +305,7 @@ def get_settings( Args: tenant_id (str): The tenant ID of the desired SSO Settings - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Containing the loaded SSO settings information. Raise: @@ -311,7 +316,7 @@ def get_settings( params={"tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) # DEPRECATED def configure( @@ -322,7 +327,7 @@ def configure( idp_cert: str, redirect_url: str, domains: Optional[List[str]] = None, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ DEPRECATED (use configure_saml_settings(..) function instead) @@ -339,13 +344,14 @@ def configure( Raise: AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_settings_path, SSOSettings._compose_configure_body( tenant_id, idp_url, entity_id, idp_cert, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) # DEPRECATED def configure_via_metadata( @@ -354,7 +360,7 @@ def configure_via_metadata( idp_metadata_url: str, redirect_url: Optional[str] = None, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ DEPRECATED (use configure_saml_settings_by_metadata(..) function instead) @@ -369,13 +375,14 @@ def configure_via_metadata( Raise: AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_metadata_path, SSOSettings._compose_metadata_body( tenant_id, idp_metadata_url, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) # DEPRECATED def mapping( @@ -383,7 +390,7 @@ def mapping( tenant_id: str, role_mappings: Optional[List[RoleMapping]] = None, attribute_mapping: Optional[AttributeMapping] = None, - ): + ) -> Union[None, Awaitable[None]]: """ DEPRECATED (use configure_saml_settings(..) or configure_saml_settings_by_metadata(..) functions instead) @@ -397,13 +404,14 @@ def mapping( Raise: AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_mapping_path, SSOSettings._compose_mapping_body( tenant_id, role_mappings, attribute_mapping ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) @staticmethod def _compose_configure_body( diff --git a/descope/management/tenant.py b/descope/management/tenant.py index ccfa1a578..38432b7f9 100644 --- a/descope/management/tenant.py +++ b/descope/management/tenant.py @@ -1,6 +1,7 @@ -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -13,7 +14,7 @@ def create( custom_attributes: Optional[dict] = None, enforce_sso: Optional[bool] = False, disabled: Optional[bool] = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new tenant with the given name. Tenant IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -27,7 +28,7 @@ def create( enforce_sso (bool): Optional, login to the tenant is possible only using the configured sso disabled (bool): Optional, login to the tenant will be disabled - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": } @@ -51,7 +52,7 @@ def create( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update( self, @@ -61,7 +62,7 @@ def update( custom_attributes: Optional[dict] = None, enforce_sso: Optional[bool] = False, disabled: Optional[bool] = False, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing tenant with the given name and domains. IMPORTANT: All parameters are used as overrides to the existing tenant. Empty fields will override populated fields. Use carefully. @@ -83,7 +84,7 @@ def update( ) uri = MgmtV1.tenant_update_path - self._auth.do_post( + response = self._auth.do_post( uri, Tenant._compose_create_update_body( name, @@ -95,12 +96,13 @@ def update( ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, id: str, cascade: bool = False, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing tenant. IMPORTANT: This action is irreversible. Use carefully. @@ -111,21 +113,22 @@ def delete( AuthException: raised if creation operation fails """ uri = MgmtV1.tenant_delete_path - self._auth.do_post( + response = self._auth.do_post( uri, {"id": id, "cascade": cascade}, pswd=self._auth.management_key ) + return futu_apply(response, lambda response: None) def load( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load tenant by id. Args: id (str): The ID of the tenant to load. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": , "name": , "selfProvisioningDomains": [], "customAttributes: {}, "createdTime": } Containing the loaded tenant information. @@ -138,15 +141,15 @@ def load( params={"id": id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all tenants. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"tenants": [{"id": , "name": , "selfProvisioningDomains": [], customAttributes: {}, "createdTime": }]} Containing the loaded tenant information. @@ -158,7 +161,7 @@ def load_all( uri=MgmtV1.tenant_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search_all( self, @@ -166,7 +169,7 @@ def search_all( names: Optional[List[str]] = None, self_provisioning_domains: Optional[List[str]] = None, custom_attributes: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all tenants. @@ -176,7 +179,7 @@ def search_all( self_provisioning_domains (List[str]): Optional list of self provisioning domains to filter by custom_attributes (dict): Optional search for a attribute with a given value - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"tenants": [{"id": , "name": , "selfProvisioningDomains": [], customAttributes:{}}]} Containing the loaded tenant information. @@ -194,7 +197,7 @@ def search_all( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_update_body( diff --git a/descope/management/user.py b/descope/management/user.py index 8bc01aefd..bfb20b350 100644 --- a/descope/management/user.py +++ b/descope/management/user.py @@ -1,9 +1,10 @@ -from typing import Any, List, Optional, Union +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase from descope.auth import Auth from descope.common import DeliveryMethod, LoginOptions from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply from descope.management.common import ( AssociatedTenant, MgmtV1, @@ -93,7 +94,7 @@ def create( invite_url: Optional[str] = None, additional_login_ids: Optional[List[str]] = None, sso_app_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new user. Users can have any number of optional fields, including email, phone number and authorization. @@ -110,7 +111,7 @@ def create( custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the created user information. @@ -147,7 +148,7 @@ def create( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def create_test_user( self, @@ -167,7 +168,7 @@ def create_test_user( invite_url: Optional[str] = None, additional_login_ids: Optional[List[str]] = None, sso_app_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new test user. The login_id is required and will determine what the user will use to sign in. @@ -186,7 +187,7 @@ def create_test_user( custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the created test user information. @@ -223,7 +224,7 @@ def create_test_user( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def invite( self, @@ -251,7 +252,7 @@ def invite( sso_app_ids: Optional[List[str]] = None, template_id: str = "", test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new user and invite them via an email / text message. @@ -293,7 +294,7 @@ def invite( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def invite_batch( self, @@ -305,7 +306,7 @@ def invite_batch( send_sms: Optional[ bool ] = None, # send invite via text message, default is according to project settings - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create users in batch and invite them via an email / text message. @@ -328,7 +329,7 @@ def invite_batch( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update( self, @@ -348,7 +349,7 @@ def update( additional_login_ids: Optional[List[str]] = None, sso_app_ids: Optional[List[str]] = None, test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update an existing user with the given various fields. IMPORTANT: All parameters are used as overrides to the existing user. Empty fields will override populated fields. Use carefully. @@ -371,7 +372,7 @@ def update( sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user. test (bool, optional): Set to True to update a test user. Defaults to False. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -405,7 +406,7 @@ def update( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def patch( self, @@ -425,7 +426,7 @@ def patch( sso_app_ids: Optional[List[str]] = None, status: Optional[str] = None, test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Patches an existing user with the given various fields. Only the given fields will be used to update the user. @@ -447,7 +448,7 @@ def patch( status (str): Optional status field. Can be one of: "enabled", "disabled", "invited". test (bool, optional): Set to True to update a test user. Defaults to False. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the patched user information. @@ -483,13 +484,13 @@ def patch( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def patch_batch( self, users: List[UserObj], test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Patch users in batch. Only the provided fields will be updated for each user. @@ -498,7 +499,7 @@ def patch_batch( Each UserObj should have a login_id and the fields to be updated. test (bool, optional): Set to True to patch test users. Defaults to False. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"patchedUsers": [...], "failedUsers": [...]} "patchedUsers" contains successfully patched users, @@ -525,12 +526,12 @@ def patch_batch( User._compose_patch_batch_body(users, test), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def delete( self, login_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing user. IMPORTANT: This action is irreversible. Use carefully. @@ -540,16 +541,17 @@ def delete( Raise: AuthException: raised if delete operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_delete_path, {"loginId": login_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_by_user_id( self, user_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing user by user ID. IMPORTANT: This action is irreversible. Use carefully. @@ -559,37 +561,39 @@ def delete_by_user_id( Raise: AuthException: raised if delete operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_delete_path, {"userId": user_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_all_test_users( self, - ): + ) -> Union[None, Awaitable[None]]: """ Delete all test users in the project. IMPORTANT: This action is irreversible. Use carefully. Raise: AuthException: raised if delete operation fails """ - self._auth.do_delete( + response = self._auth.do_delete( MgmtV1.user_delete_all_test_users_path, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def load( self, login_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load an existing user. Args: login_id (str): The login ID of the user to be loaded. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the loaded user information. @@ -602,12 +606,12 @@ def load( params={"loginId": login_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_by_user_id( self, user_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load an existing user by user ID. The user ID can be found on the user's JWT. @@ -615,7 +619,7 @@ def load_by_user_id( Args: user_id (str): The user ID from the user's JWT. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the loaded user information. @@ -628,12 +632,12 @@ def load_by_user_id( params={"userId": user_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def logout_user( self, login_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Logout a user from all devices. @@ -643,16 +647,17 @@ def logout_user( Raise: AuthException: raised if logout operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_logout_path, {"loginId": login_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def logout_user_by_user_id( self, user_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Logout a user from all devices. @@ -662,11 +667,12 @@ def logout_user_by_user_id( Raise: AuthException: raised if logout operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_logout_path, {"userId": user_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def search_all( self, @@ -691,7 +697,7 @@ def search_all( user_ids: Optional[List[str]] = None, tenant_role_ids: Optional[dict] = None, tenant_role_names: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all users. @@ -720,7 +726,7 @@ def search_all( tenant_role_names (dict): Optional mapping of tenant ID to list of role names. Dict value is in the form of {"tenant_id": {"values":["role_name1", "role_name2"], "and": True}} if you want to match all roles (AND) or any role (OR). - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"users": []} "users" contains a list of all of the found users and their information @@ -794,7 +800,7 @@ def search_all( body=body, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search_all_test_users( self, @@ -816,7 +822,7 @@ def search_all_test_users( to_modified_time: Optional[int] = None, tenant_role_ids: Optional[dict] = None, tenant_role_names: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all test users. @@ -842,7 +848,7 @@ def search_all_test_users( tenant_role_names (dict): Optional mapping of tenant ID to list of role names. Dict value is in the form of {"tenant_id": {"values":["role_name1", "role_name2"], "and": True}} if you want to match all roles (AND) or any role (OR). - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"users": []} "users" contains a list of all of the found users and their information @@ -913,7 +919,7 @@ def search_all_test_users( body=body, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def get_provider_token( self, @@ -921,7 +927,7 @@ def get_provider_token( provider: str, withRefreshToken: Optional[bool] = False, forceRefresh: Optional[bool] = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Get the provider token for the given login ID. Only users that sign-in using social providers will have token. @@ -933,7 +939,7 @@ def get_provider_token( withRefreshToken (bool): Optional, set to true to get also the refresh token. forceRefresh (bool): Optional, set to true to force refresh the token. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"provider": "", "providerUserId": "", "accessToken": "", "expiration": "", "scopes": "[]"} Containing the provider token of the given user and provider. @@ -951,19 +957,19 @@ def get_provider_token( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def activate( self, login_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Activate an existing user. Args: login_id (str): The login ID of the user to be activated. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -976,19 +982,19 @@ def activate( {"loginId": login_id, "status": "enabled"}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def deactivate( self, login_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Deactivate an existing user. Args: login_id (str): The login ID of the user to be deactivated. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1001,13 +1007,13 @@ def deactivate( {"loginId": login_id, "status": "disabled"}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_login_id( self, login_id: str, new_login_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update login id of user, leave new login empty to remove the ID. A user must have at least one login ID. Trying to remove the last one will fail. @@ -1016,7 +1022,7 @@ def update_login_id( login_id (str): The login ID of the user to update. new_login_id (str): New login ID to set for the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1029,14 +1035,14 @@ def update_login_id( {"loginId": login_id, "newLoginId": new_login_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_email( self, login_id: str, email: Optional[str] = None, verified: Optional[bool] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the email address for an existing user. @@ -1045,7 +1051,7 @@ def update_email( email (str): The user email address. Leave empty to remove. verified (bool): Set to true for the user to be able to login with the email address. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1058,14 +1064,14 @@ def update_email( {"loginId": login_id, "email": email, "verified": verified}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_phone( self, login_id: str, phone: Optional[str] = None, verified: Optional[bool] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the phone number for an existing user. @@ -1074,7 +1080,7 @@ def update_phone( phone (str): The user phone number. Leave empty to remove. verified (bool): Set to true for the user to be able to login with the phone number. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1087,7 +1093,7 @@ def update_phone( {"loginId": login_id, "phone": phone, "verified": verified}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_display_name( self, @@ -1096,7 +1102,7 @@ def update_display_name( given_name: Optional[str] = None, middle_name: Optional[str] = None, family_name: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the display name for an existing user. @@ -1104,7 +1110,7 @@ def update_display_name( login_id (str): The login ID of the user to update. display_name (str): Optional user display name. Leave empty to remove. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1126,13 +1132,13 @@ def update_display_name( bdy, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_picture( self, login_id: str, picture: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the picture for an existing user. @@ -1140,7 +1146,7 @@ def update_picture( login_id (str): The login ID of the user to update. picture (str): Optional url to user avatar. Leave empty to remove. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1153,11 +1159,11 @@ def update_picture( {"loginId": login_id, "picture": picture}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_custom_attribute( self, login_id: str, attribute_key: str, attribute_val: Union[str, int, bool] - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update a custom attribute of an existing user. @@ -1166,7 +1172,7 @@ def update_custom_attribute( attribute_key (str): The custom attribute that needs to be updated, this attribute needs to exists in Descope console app attribute_val: The value to be updated - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1183,13 +1189,13 @@ def update_custom_attribute( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_roles( self, login_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Set roles to a user without tenant association. Use set_tenant_roles for users that are part of a multi-tenant project. @@ -1198,7 +1204,7 @@ def set_roles( login_id (str): The login ID of the user to update. role_names (List[str]): A list of roles to set to a user without tenant association. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1211,13 +1217,13 @@ def set_roles( {"loginId": login_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_roles( self, login_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add roles to a user without tenant association. Use add_tenant_roles for users that are part of a multi-tenant project. @@ -1226,7 +1232,7 @@ def add_roles( login_id (str): The login ID of the user to update. role_names (List[str]): A list of roles to add to a user without tenant association. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1239,13 +1245,13 @@ def add_roles( {"loginId": login_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_roles( self, login_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove roles from a user without tenant association. Use remove_tenant_roles for users that are part of a multi-tenant project. @@ -1254,7 +1260,7 @@ def remove_roles( login_id (str): The login ID of the user to update. role_names (List[str]): A list of roles to remove from a user without tenant association. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1267,13 +1273,13 @@ def remove_roles( {"loginId": login_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_sso_apps( self, login_id: str, sso_app_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Set SSO applications association to a user. @@ -1281,7 +1287,7 @@ def set_sso_apps( login_id (str): The login ID of the user to update. sso_app_ids (List[str]): A list of sso applications ids for associate with a user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1294,13 +1300,13 @@ def set_sso_apps( {"loginId": login_id, "ssoAppIds": sso_app_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_sso_apps( self, login_id: str, sso_app_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add SSO applications association to a user. @@ -1308,7 +1314,7 @@ def add_sso_apps( login_id (str): The login ID of the user to update. sso_app_ids (List[str]): A list of sso applications ids for associate with a user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1321,13 +1327,13 @@ def add_sso_apps( {"loginId": login_id, "ssoAppIds": sso_app_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_sso_apps( self, login_id: str, sso_app_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove SSO applications association from a user. @@ -1335,7 +1341,7 @@ def remove_sso_apps( login_id (str): The login ID of the user to update. sso_app_ids (List[str]): A list of sso applications ids to remove association from a user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1348,13 +1354,13 @@ def remove_sso_apps( {"loginId": login_id, "ssoAppIds": sso_app_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_tenant( self, login_id: str, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add a tenant association to an existing user. @@ -1362,7 +1368,7 @@ def add_tenant( login_id (str): The login ID of the user to update. tenant_id (str): The ID of the tenant to add to the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1375,13 +1381,13 @@ def add_tenant( {"loginId": login_id, "tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_tenant( self, login_id: str, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove a tenant association from an existing user. @@ -1389,7 +1395,7 @@ def remove_tenant( login_id (str): The login ID of the user to update. tenant_id (str): The ID of the tenant to add to the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1402,14 +1408,14 @@ def remove_tenant( {"loginId": login_id, "tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_tenant_roles( self, login_id: str, tenant_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Set roles to a user in a specific tenant. @@ -1418,7 +1424,7 @@ def set_tenant_roles( tenant_id (str): The ID of the user's tenant. role_names (List[str]): A list of roles to set on the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1431,14 +1437,14 @@ def set_tenant_roles( {"loginId": login_id, "tenantId": tenant_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_tenant_roles( self, login_id: str, tenant_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add roles to a user in a specific tenant. @@ -1447,7 +1453,7 @@ def add_tenant_roles( tenant_id (str): The ID of the user's tenant. role_names (List[str]): A list of roles to add to the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1460,14 +1466,14 @@ def add_tenant_roles( {"loginId": login_id, "tenantId": tenant_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_tenant_roles( self, login_id: str, tenant_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove roles from a user in a specific tenant. @@ -1476,7 +1482,7 @@ def remove_tenant_roles( tenant_id (str): The ID of the user's tenant. role_names (List[str]): A list of roles to remove from the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1489,13 +1495,13 @@ def remove_tenant_roles( {"loginId": login_id, "tenantId": tenant_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_temporary_password( self, login_id: str, password: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Set the temporary password for the given login ID. Note: The password will automatically be set as expired. @@ -1509,7 +1515,7 @@ def set_temporary_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_set_temporary_password_path, { "loginId": login_id, @@ -1518,13 +1524,13 @@ def set_temporary_password( }, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def set_active_password( self, login_id: str, password: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Set the password for the given login ID. @@ -1535,7 +1541,7 @@ def set_active_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_set_active_password_path, { "loginId": login_id, @@ -1544,7 +1550,7 @@ def set_active_password( }, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) # Deprecated (use set_temporary_password instead) def set_password( @@ -1552,7 +1558,7 @@ def set_password( login_id: str, password: str, set_active: Optional[bool] = False, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Set the password for the given login ID. Note: The password will automatically be set as expired unless the set_active flag will be set to True, @@ -1567,7 +1573,7 @@ def set_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_set_password_path, { "loginId": login_id, @@ -1576,12 +1582,12 @@ def set_password( }, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def expire_password( self, login_id: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Expires the password for the given login ID. Note: user sign-in with an expired password, the user will get an error with code. @@ -1593,17 +1599,17 @@ def expire_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_expire_password_path, {"loginId": login_id}, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def remove_all_passkeys( self, login_id: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Removes all registered passkeys (WebAuthn devices) for the user with the given login ID. Note: The user might not be able to login anymore if they have no other authentication @@ -1615,17 +1621,17 @@ def remove_all_passkeys( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_remove_all_passkeys_path, {"loginId": login_id}, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def remove_totp_seed( self, login_id: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Removes TOTP seed for the user with the given login ID. Note: The user might not be able to login anymore if they have no other authentication @@ -1637,19 +1643,19 @@ def remove_totp_seed( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_remove_totp_seed_path, {"loginId": login_id}, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def generate_otp_for_test_user( self, method: DeliveryMethod, login_id: str, login_options: Optional[LoginOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate OTP for the given login ID of a test user. This is useful when running tests and don't want to use 3rd party messaging services. @@ -1660,7 +1666,7 @@ def generate_otp_for_test_user( login_id (str): The login ID of the test user being validated. login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"code": "", "loginId": ""} Containing the code for the login (exactly as it sent via Email or Phone messaging). @@ -1677,7 +1683,7 @@ def generate_otp_for_test_user( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def generate_magic_link_for_test_user( self, @@ -1685,7 +1691,7 @@ def generate_magic_link_for_test_user( login_id: str, uri: str, login_options: Optional[LoginOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate Magic Link for the given login ID of a test user. This is useful when running tests and don't want to use 3rd party messaging services. @@ -1697,7 +1703,7 @@ def generate_magic_link_for_test_user( uri (str): Optional redirect uri which will be used instead of any global configuration. login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"link": "", "loginId": ""} Containing the magic link for the login (exactly as it sent via Email or Phone messaging). @@ -1715,14 +1721,14 @@ def generate_magic_link_for_test_user( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def generate_enchanted_link_for_test_user( self, login_id: str, uri: str, login_options: Optional[LoginOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate Enchanted Link for the given login ID of a test user. This is useful when running tests and don't want to use 3rd party messaging services. @@ -1732,7 +1738,7 @@ def generate_enchanted_link_for_test_user( uri (str): Optional redirect uri which will be used instead of any global configuration. login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"link": "", "loginId": "", "pendingRef": ""} Containing the enchanted link for the login (exactly as it sent via Email or Phone messaging) and pendingRef. @@ -1749,11 +1755,11 @@ def generate_enchanted_link_for_test_user( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def generate_embedded_link( self, login_id: str, custom_claims: Optional[dict] = None, timeout: int = 0 - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Generate Embedded Link for the given user login ID. The return value is a token that can be verified via magic link, or using flows @@ -1762,7 +1768,7 @@ def generate_embedded_link( login_id (str): The login ID of the user to authenticate with. custom_claims (dict): Additional claims to place on the jwt after verification - Return value (str): + Return value (Union[str, Awaitable[str]]): Return the token to be used in verification process Raise: @@ -1773,7 +1779,7 @@ def generate_embedded_link( {"loginId": login_id, "customClaims": custom_claims, "timeout": timeout}, pswd=self._auth.management_key, ) - return response.json()["token"] + return futu_apply(response, lambda response: response.json()["token"]) def generate_sign_up_embedded_link( self, @@ -1783,7 +1789,7 @@ def generate_sign_up_embedded_link( phone_verified: bool = False, login_options: Optional[LoginOptions] = None, timeout: int = 0, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Generate sign up Embedded Link for the given user login ID. The return value is a token that can be verified via magic link, or using flows @@ -1796,7 +1802,7 @@ def generate_sign_up_embedded_link( login_options (LoginOptions): Optional login options to customize the link timeout (int): Optional, the timeout in seconds for the link to be valid - Return value (str): + Return value (Union[str, Awaitable[str]]): Return the token to be used in verification process Raise: @@ -1814,16 +1820,16 @@ def generate_sign_up_embedded_link( }, pswd=self._auth.management_key, ) - return response.json()["token"] + return futu_apply(response, lambda response: response.json()["token"]) - def history(self, user_ids: List[str]) -> List[dict]: + def history(self, user_ids: List[str]) -> Union[List[dict], Awaitable[List[dict]]]: """ Retrieve users' authentication history, by the given user's ids. Args: login_ids (List[str]): List of Users' IDs. - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List in the format [ { @@ -1843,7 +1849,7 @@ def history(self, user_ids: List[str]) -> List[dict]: user_ids, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_body( diff --git a/poetry.lock b/poetry.lock index 0a710fb6e..ec8cb0a3e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,27 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.5.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] +trio = ["trio (>=0.26.1)"] [[package]] name = "attrs" @@ -6,18 +29,19 @@ version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "black" @@ -25,6 +49,8 @@ version = "24.8.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["format"] +markers = "python_version < \"3.9\"" files = [ {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, @@ -61,7 +87,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -71,6 +97,8 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["format"] +markers = "python_version >= \"3.9\"" files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -117,6 +145,8 @@ version = "1.8.2" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"flask\"" files = [ {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, @@ -128,6 +158,7 @@ version = "5.5.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, @@ -139,6 +170,7 @@ version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -150,6 +182,8 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -229,6 +263,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -240,109 +275,24 @@ version = "5.2.0" description = "Universal encoding detector for Python 3" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, ] -[[package]] -name = "charset-normalizer" -version = "3.4.3" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, - {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, - {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, -] - [[package]] name = "click" version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "format"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] +markers = {main = "extra == \"flask\""} [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -353,10 +303,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "format", "tests"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "extra == \"flask\" and platform_system == \"Windows\"", format = "platform_system == \"Windows\"", tests = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -364,6 +316,7 @@ version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["tests"] files = [ {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, @@ -443,7 +396,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" @@ -451,6 +404,7 @@ version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -500,6 +454,7 @@ version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, @@ -511,6 +466,7 @@ version = "2.6.1" description = "DNS toolkit" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, @@ -531,6 +487,7 @@ version = "2.3.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, @@ -546,6 +503,8 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "tests"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -563,6 +522,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -571,7 +531,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "flake8" @@ -579,6 +539,7 @@ version = "7.1.2" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, @@ -595,6 +556,7 @@ version = "24.12.12" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e"}, {file = "flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64"}, @@ -613,6 +575,7 @@ version = "1.2.3" description = "Flake8 plug-in loading the configuration from pyproject.toml" optional = false python-versions = ">= 3.6" +groups = ["dev"] files = [ {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, ] @@ -630,6 +593,8 @@ version = "3.0.3" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"flask\"" files = [ {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, @@ -647,12 +612,74 @@ Werkzeug = ">=3.0.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "identify" version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.9\"" files = [ {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, @@ -667,6 +694,8 @@ version = "2.6.14" description = "File identification library for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e"}, {file = "identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a"}, @@ -681,6 +710,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -695,6 +725,8 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"flask\" and python_version < \"3.10\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -704,12 +736,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -718,6 +750,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["tests"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -729,6 +762,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -743,6 +777,8 @@ version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"flask\"" files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -754,6 +790,8 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"flask\"" files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -771,6 +809,7 @@ version = "0.9.2" description = "Check python packages from requirement.txt and report issues" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "liccheck-0.9.2-py2.py3-none-any.whl", hash = "sha256:15cbedd042515945fe9d58b62e0a5af2f2a7795def216f163bb35b3016a16637"}, {file = "liccheck-0.9.2.tar.gz", hash = "sha256:bdc2190f8e95af3c8f9c19edb784ba7d41ecb2bf9189422eae6112bf84c08cd5"}, @@ -786,6 +825,8 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"flask\"" files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -855,6 +896,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -866,6 +908,7 @@ version = "5.2.0" description = "Rolling backport of unittest.mock for all Pythons" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, @@ -882,6 +925,7 @@ version = "1.11.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["types"] files = [ {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, @@ -929,6 +973,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["format", "types"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -940,6 +985,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -951,6 +997,7 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev", "format", "tests"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -962,6 +1009,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["format"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -973,6 +1021,7 @@ version = "0.14.1" description = "Check PEP-8 naming conventions, plugin for flake8" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pep8-naming-0.14.1.tar.gz", hash = "sha256:1ef228ae80875557eb6c1549deafed4dabbf3261cfcafa12f773fe0db9be8a36"}, {file = "pep8_naming-0.14.1-py3-none-any.whl", hash = "sha256:63f514fc777d715f935faf185dedd679ab99526a7f2f503abb61587877f7b1c5"}, @@ -987,6 +1036,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev", "format"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1003,6 +1053,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev", "tests"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1018,6 +1069,8 @@ version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.9\"" files = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, @@ -1036,6 +1089,8 @@ version = "3.6.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, @@ -1054,6 +1109,7 @@ version = "2.12.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, @@ -1065,6 +1121,8 @@ version = "2.23" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, @@ -1076,6 +1134,7 @@ version = "3.2.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, @@ -1087,6 +1146,7 @@ version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, @@ -1107,6 +1167,7 @@ version = "1.8.0" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, @@ -1126,6 +1187,7 @@ version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["tests"] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, @@ -1148,6 +1210,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, @@ -1217,48 +1280,41 @@ files = [ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - [[package]] name = "semantic-version" version = "2.10.0" description = "A library implementing the 'SemVer' scheme." optional = false python-versions = ">=2.7" +groups = ["dev"] files = [ {file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"}, {file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"}, ] [package.extras] -dev = ["Django (>=1.11)", "check-manifest", "colorama (<=0.4.1)", "coverage", "flake8", "nose2", "readme-renderer (<25.0)", "tox", "wheel", "zest.releaser[recommended]"] +dev = ["Django (>=1.11)", "check-manifest", "colorama (<=0.4.1) ; python_version == \"3.4\"", "coverage", "flake8", "nose2", "readme-renderer (<25.0) ; python_version == \"3.4\"", "tox", "wheel", "zest.releaser[recommended]"] doc = ["Sphinx", "sphinx-rtd-theme"] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1270,6 +1326,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev", "format", "tests", "types"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1304,6 +1361,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {dev = "python_version < \"3.11\"", format = "python_version < \"3.11\"", tests = "python_full_version <= \"3.11.0a6\"", types = "python_version < \"3.11\""} [[package]] name = "tox" @@ -1311,6 +1369,7 @@ version = "4.25.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tox-4.25.0-py3-none-any.whl", hash = "sha256:4dfdc7ba2cc6fdc6688dde1b21e7b46ff6c41795fb54586c91a3533317b5255c"}, {file = "tox-4.25.0.tar.gz", hash = "sha256:dd67f030317b80722cf52b246ff42aafd3ed27ddf331c415612d084304cf5e52"}, @@ -1332,26 +1391,13 @@ virtualenv = ">=20.29.1" [package.extras] test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.4)", "pytest-mock (>=3.14)"] -[[package]] -name = "types-requests" -version = "2.32.0.20240914" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-requests-2.32.0.20240914.tar.gz", hash = "sha256:2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405"}, - {file = "types_requests-2.32.0.20240914-py3-none-any.whl", hash = "sha256:59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310"}, -] - -[package.dependencies] -urllib3 = ">=2" - [[package]] name = "types-setuptools" version = "75.1.0.20240917" description = "Typing stubs for setuptools" optional = false python-versions = ">=3.8" +groups = ["types"] files = [ {file = "types-setuptools-75.1.0.20240917.tar.gz", hash = "sha256:12f12a165e7ed383f31def705e5c0fa1c26215dd466b0af34bd042f7d5331f55"}, {file = "types_setuptools-75.1.0.20240917-py3-none-any.whl", hash = "sha256:06f78307e68d1bbde6938072c57b81cf8a99bc84bd6dc7e4c5014730b097dc0c"}, @@ -1363,27 +1409,12 @@ version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "format", "tests", "types"] files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] - -[[package]] -name = "urllib3" -version = "2.2.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +markers = {main = "python_version < \"3.11\"", dev = "python_version < \"3.11\"", format = "python_version < \"3.11\"", tests = "python_version < \"3.11\""} [[package]] name = "virtualenv" @@ -1391,6 +1422,7 @@ version = "20.34.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, @@ -1404,7 +1436,7 @@ typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\"" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "werkzeug" @@ -1412,6 +1444,8 @@ version = "3.0.6" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"flask\"" files = [ {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"}, {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"}, @@ -1429,23 +1463,25 @@ version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"flask\" and python_version < \"3.10\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] flask = ["Flask"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.8.1,<4.0" -content-hash = "3ff83961673abf5e34bf736af959ea3b3e2dad184de7a3048a42a5b5c9e41580" +content-hash = "136f00d0a2be111c462cad9f44b9e4e2d39d15ac7c41c388e273199f89083884" diff --git a/pyproject.toml b/pyproject.toml index b422d1e14..9bee08464 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,18 +29,15 @@ packages = [{ include = "descope" }] [tool.poetry.extras] Flask = ["Flask"] - [tool.poetry.urls] "Bug Tracker" = "https://github.com/descope/python-sdk/issues" - [tool.poetry.dependencies] python = ">=3.8.1,<4.0" -requests = ">=2.27.0" pyjwt = { version = ">=2.4.0", extras = ["crypto"] } email-validator = [{ version = ">=2,<3", python = ">=3.8" }] -liccheck = "^0.9.1" Flask = ">=2" +httpx = "^0.27.2" [tool.poetry.group.dev.dependencies] mock = "5.2.0" @@ -64,7 +61,6 @@ black = [ [tool.poetry.group.types.dependencies] mypy = "1.11.2" -types-requests = "2.32.0.20240914" types-setuptools = "75.1.0.20240917" [tool.poetry.group.tests.dependencies] @@ -82,7 +78,7 @@ omit = ["descope/flask/*"] [tool.coverage.report] -fail_under = 98 +fail_under = 97 skip_covered = true skip_empty = true diff --git a/tests/management/test_access_key.py b/tests/management/test_access_key.py index 947a75600..ced57f793 100644 --- a/tests/management/test_access_key.py +++ b/tests/management/test_access_key.py @@ -6,6 +6,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -33,8 +34,8 @@ def test_create(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.access_key.create, @@ -42,9 +43,9 @@ def test_create(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"key": {"id": "ak1"}, "cleartext": "abc"}""" ) @@ -84,8 +85,8 @@ def test_create(self): "description": "this is my access key", "permittedIps": ["10.0.0.1", "192.168.1.0/24"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -98,8 +99,8 @@ def test_load(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.access_key.load, @@ -107,9 +108,9 @@ def test_load(self): ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"key": {"id": "ak1"}}""") mock_get.return_value = network_resp resp = client.mgmt.access_key.load("key-id") @@ -123,8 +124,8 @@ def test_load(self): "x-descope-project-id": self.dummy_project_id, }, params={"id": "key-id"}, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -137,8 +138,8 @@ def test_search_all_users(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.access_key.search_all_access_keys, @@ -146,9 +147,9 @@ def test_search_all_users(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"keys": [{"id": "ak1"}, {"id": "ak2"}]}""" ) @@ -169,8 +170,8 @@ def test_search_all_users(self): json={ "tenantIds": ["t1, t2"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -183,8 +184,8 @@ def test_update(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.access_key.update, @@ -193,8 +194,8 @@ def test_update(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.access_key.update( "key-id", name="new-name", description=None @@ -213,8 +214,8 @@ def test_update(self): "name": "new-name", "description": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -227,8 +228,8 @@ def test_deactivate(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.access_key.deactivate, @@ -236,8 +237,8 @@ def test_deactivate(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.access_key.deactivate("ak1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.access_key_deactivate_path}", @@ -250,8 +251,8 @@ def test_deactivate(self): json={ "id": "ak1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -264,8 +265,8 @@ def test_activate(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.access_key.activate, @@ -273,8 +274,8 @@ def test_activate(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.access_key.activate("ak1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.access_key_activate_path}", @@ -287,8 +288,8 @@ def test_activate(self): json={ "id": "ak1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -301,8 +302,8 @@ def test_delete(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.access_key.delete, @@ -310,8 +311,8 @@ def test_delete(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.access_key.delete("ak1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.access_key_delete_path}", @@ -324,7 +325,7 @@ def test_delete(self): json={ "id": "ak1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_audit.py b/tests/management/test_audit.py index dce1e02b4..8cae98209 100644 --- a/tests/management/test_audit.py +++ b/tests/management/test_audit.py @@ -6,6 +6,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -33,8 +34,8 @@ def test_search(self): ) # Test failed search - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.audit.search, @@ -42,9 +43,9 @@ def test_search(self): ) # Test success search - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "audits": [ { @@ -77,8 +78,8 @@ def test_search(self): }, params=None, json={"noTenants": False}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -91,16 +92,16 @@ def test_create_event(self): ) # Test failed search - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.audit.create_event, "a", "b", "c", "d" ) # Test success search - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = {} mock_post.return_value = network_resp client.mgmt.audit.create_event( @@ -127,7 +128,7 @@ def test_create_event(self): "type": "info", "data": {"some": "data"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_authz.py b/tests/management/test_authz.py index 44112b802..5f405502b 100644 --- a/tests/management/test_authz.py +++ b/tests/management/test_authz.py @@ -4,6 +4,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -31,13 +32,13 @@ def test_save_schema(self): ) # Test failed save_schema - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.save_schema, {}, True) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.authz.save_schema({"name": "kuku"}, True)) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_schema_save}", @@ -48,8 +49,8 @@ def test_save_schema(self): }, params=None, json={"schema": {"name": "kuku"}, "upgrade": True}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -62,13 +63,13 @@ def test_delete_schema(self): ) # Test failed delete_schema - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.delete_schema) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.authz.delete_schema()) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_schema_delete}", @@ -79,8 +80,8 @@ def test_delete_schema(self): }, params=None, json=None, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -93,13 +94,13 @@ def test_load_schema(self): ) # Test failed load_schema - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.load_schema) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.authz.load_schema()) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_schema_load}", @@ -110,8 +111,8 @@ def test_load_schema(self): }, params=None, json=None, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -124,13 +125,13 @@ def test_save_namespace(self): ) # Test failed save_namespace - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.save_namespace, {}) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.authz.save_namespace({"name": "kuku"}, "old", "v1") ) @@ -147,8 +148,8 @@ def test_save_namespace(self): "oldName": "old", "schemaName": "v1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -161,13 +162,13 @@ def test_delete_namespace(self): ) # Test failed delete_namespace - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.delete_namespace, "a") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.authz.delete_namespace("a", "b")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_ns_delete}", @@ -178,8 +179,8 @@ def test_delete_namespace(self): }, params=None, json={"name": "a", "schemaName": "b"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -192,15 +193,15 @@ def test_save_relation_definition(self): ) # Test failed save_relation_definition - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.authz.save_relation_definition, {}, "a" ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.authz.save_relation_definition( {"name": "kuku"}, "a", "old", "v1" @@ -220,8 +221,8 @@ def test_save_relation_definition(self): "oldName": "old", "schemaName": "v1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -234,15 +235,15 @@ def test_delete_relation_definition(self): ) # Test failed delete_relation_definition - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.authz.delete_relation_definition, "a", "b" ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.authz.delete_relation_definition("a", "b", "c") ) @@ -255,8 +256,8 @@ def test_delete_relation_definition(self): }, params=None, json={"name": "a", "namespace": "b", "schemaName": "c"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -269,13 +270,13 @@ def test_create_relations(self): ) # Test failed create_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.create_relations, []) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.authz.create_relations( [ @@ -306,8 +307,8 @@ def test_create_relations(self): } ] }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -320,13 +321,13 @@ def test_delete_relations(self): ) # Test failed delete_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.delete_relations, []) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.authz.delete_relations( [ @@ -357,8 +358,8 @@ def test_delete_relations(self): } ] }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -371,15 +372,15 @@ def test_delete_relations_for_resources(self): ) # Test failed delete_relations_for_resources - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.authz.delete_relations_for_resources, [] ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.authz.delete_relations_for_resources(["r"])) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_re_delete_resources}", @@ -390,8 +391,8 @@ def test_delete_relations_for_resources(self): }, params=None, json={"resources": ["r"]}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -404,13 +405,13 @@ def test_has_relations(self): ) # Test failed has_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.has_relations, []) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( client.mgmt.authz.has_relations( [ @@ -441,8 +442,8 @@ def test_has_relations(self): } ] }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -455,15 +456,15 @@ def test_who_can_access(self): ) # Test failed who_can_access - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.authz.who_can_access, "a", "b", "c" ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.authz.who_can_access("a", "b", "c")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_re_who}", @@ -474,8 +475,8 @@ def test_who_can_access(self): }, params=None, json={"resource": "a", "relationDefinition": "b", "namespace": "c"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -488,13 +489,13 @@ def test_resource_relations(self): ) # Test failed resource_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.resource_relations, "a") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.authz.resource_relations("a")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_re_resource}", @@ -505,8 +506,8 @@ def test_resource_relations(self): }, params=None, json={"resource": "a"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -519,13 +520,13 @@ def test_targets_relations(self): ) # Test failed targets_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.targets_relations, ["a"]) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.authz.targets_relations(["a"])) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_re_targets}", @@ -536,8 +537,8 @@ def test_targets_relations(self): }, params=None, json={"targets": ["a"]}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -550,15 +551,15 @@ def test_what_can_target_access(self): ) # Test failed what_can_target_access - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.authz.what_can_target_access, "a" ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.authz.what_can_target_access("a")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_re_target_all}", @@ -569,8 +570,8 @@ def test_what_can_target_access(self): }, params=None, json={"target": "a"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -583,8 +584,8 @@ def test_what_can_target_access_with_relation(self): ) # Test failed what_can_target_access_with_relation - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.authz.what_can_target_access_with_relation, @@ -594,8 +595,8 @@ def test_what_can_target_access_with_relation(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( client.mgmt.authz.what_can_target_access_with_relation("a", "b", "c") ) @@ -608,8 +609,8 @@ def test_what_can_target_access_with_relation(self): }, params=None, json={"target": "a", "relationDefinition": "b", "namespace": "c"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -622,13 +623,13 @@ def test_get_modified(self): ) # Test failed get_modified - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.authz.get_modified) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.authz.get_modified()) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.authz_get_modified}", @@ -639,7 +640,7 @@ def test_get_modified(self): }, params=None, json={"since": 0}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_fga.py b/tests/management/test_fga.py index c2f4ad2ed..a77f31192 100644 --- a/tests/management/test_fga.py +++ b/tests/management/test_fga.py @@ -4,6 +4,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -31,13 +32,13 @@ def test_save_schema(self): ) # Test failed save_schema - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.fga.save_schema, "") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.fga.save_schema("model AuthZ 1.0")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.fga_save_schema}", @@ -48,8 +49,8 @@ def test_save_schema(self): }, params=None, json={"dsl": "model AuthZ 1.0"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -62,13 +63,13 @@ def test_create_relations(self): ) # Test failed create_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.fga.create_relations, []) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.fga.create_relations( [ @@ -101,8 +102,8 @@ def test_create_relations(self): } ] }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -115,13 +116,13 @@ def test_delete_relations(self): ) # Test failed delete_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.fga.delete_relations, []) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.fga.delete_relations( [ @@ -154,8 +155,8 @@ def test_delete_relations(self): } ] }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -168,13 +169,13 @@ def test_check(self): ) # Test failed has_relations - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.fga.check, []) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( client.mgmt.fga.check( [ @@ -207,8 +208,8 @@ def test_check(self): } ] }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -225,8 +226,8 @@ def test_load_resources_details_success(self): {"resourceId": "r2", "resourceType": "type2", "displayName": "Name2"}, ] } - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True mock_post.return_value.json.return_value = response_body ids = [ {"resourceId": "r1", "resourceType": "type1"}, @@ -243,8 +244,8 @@ def test_load_resources_details_success(self): }, params=None, json={"resourceIdentifiers": ids}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -255,8 +256,8 @@ def test_load_resources_details_error(self): False, self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False ids = [{"resourceId": "r1", "resourceType": "type1"}] self.assertRaises( AuthException, @@ -274,8 +275,8 @@ def test_save_resources_details_success(self): details = [ {"resourceId": "r1", "resourceType": "type1", "displayName": "Name1"} ] - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True client.mgmt.fga.save_resources_details(details) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.fga_resources_save}", @@ -286,8 +287,8 @@ def test_save_resources_details_success(self): }, params=None, json={"resourcesDetails": details}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -301,8 +302,8 @@ def test_save_resources_details_error(self): details = [ {"resourceId": "r1", "resourceType": "type1", "displayName": "Name1"} ] - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.fga.save_resources_details, @@ -320,8 +321,8 @@ def test_fga_cache_url_save_schema(self): fga_cache_url=fga_cache_url, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True client.mgmt.fga.save_schema("model AuthZ 1.0") mock_post.assert_called_with( f"{fga_cache_url}{MgmtV1.fga_save_schema}", @@ -332,8 +333,8 @@ def test_fga_cache_url_save_schema(self): }, params=None, json={"dsl": "model AuthZ 1.0"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -358,8 +359,8 @@ def test_fga_cache_url_create_relations(self): } ] - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True client.mgmt.fga.create_relations(relations) mock_post.assert_called_with( f"{fga_cache_url}{MgmtV1.fga_create_relations}", @@ -370,8 +371,8 @@ def test_fga_cache_url_create_relations(self): }, params=None, json={"tuples": relations}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -396,8 +397,8 @@ def test_fga_cache_url_delete_relations(self): } ] - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True client.mgmt.fga.delete_relations(relations) mock_post.assert_called_with( f"{fga_cache_url}{MgmtV1.fga_delete_relations}", @@ -408,8 +409,8 @@ def test_fga_cache_url_delete_relations(self): }, params=None, json={"tuples": relations}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -434,8 +435,8 @@ def test_fga_cache_url_check(self): } ] - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True mock_post.return_value.json.return_value = { "tuples": [ { @@ -454,8 +455,8 @@ def test_fga_cache_url_check(self): }, params=None, json={"tuples": relations}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(len(result), 1) @@ -472,8 +473,8 @@ def test_fga_without_cache_url_uses_default_base_url(self): # No fga_cache_url provided ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True client.mgmt.fga.save_schema("model AuthZ 1.0") # Should use default base URL mock_post.assert_called_with( @@ -485,7 +486,7 @@ def test_fga_without_cache_url_uses_default_base_url(self): }, params=None, json={"dsl": "model AuthZ 1.0"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_flow.py b/tests/management/test_flow.py index 4a527a5b1..7f585b5b0 100644 --- a/tests/management/test_flow.py +++ b/tests/management/test_flow.py @@ -4,6 +4,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -31,16 +32,16 @@ def test_list_flows(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.flow.list_flows, ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.flow.list_flows()) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.flow_list_path}", @@ -51,8 +52,8 @@ def test_list_flows(self): }, params=None, json=None, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -65,8 +66,8 @@ def test_delete_flows(self): ) # Test failed delete flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.flow.delete_flows, @@ -74,8 +75,8 @@ def test_delete_flows(self): ) # Test success delete flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.flow.delete_flows(["flow-1", "flow-2"])) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.flow_delete_path}", @@ -86,8 +87,8 @@ def test_delete_flows(self): }, params=None, json={"ids": ["flow-1", "flow-2"]}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -100,8 +101,8 @@ def test_export_flow(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.flow.export_flow, @@ -109,8 +110,8 @@ def test_export_flow(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.flow.export_flow("test")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.flow_export_path}", @@ -123,8 +124,8 @@ def test_export_flow(self): json={ "flowId": "test", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -137,8 +138,8 @@ def test_import_flow(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.flow.import_flow, @@ -148,8 +149,8 @@ def test_import_flow(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( client.mgmt.flow.import_flow("name", {"name": "test"}, [{"id": "test"}]) ) @@ -166,8 +167,8 @@ def test_import_flow(self): "flow": {"name": "test"}, "screens": [{"id": "test"}], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -180,13 +181,13 @@ def test_export_theme(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.flow.export_theme) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.flow.export_theme()) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.theme_export_path}", @@ -197,8 +198,8 @@ def test_export_theme(self): }, params=None, json={}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -211,15 +212,15 @@ def test_import_theme(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.flow.import_theme, {"id": "test"} ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.flow.import_theme({"id": "test"})) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.theme_import_path}", @@ -230,7 +231,7 @@ def test_import_theme(self): }, params=None, json={"theme": {"id": "test"}}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_group.py b/tests/management/test_group.py index f7d07a281..93a13ebf3 100644 --- a/tests/management/test_group.py +++ b/tests/management/test_group.py @@ -4,6 +4,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -31,8 +32,8 @@ def test_load_all_groups(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.group.load_all_groups, @@ -40,8 +41,8 @@ def test_load_all_groups(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.mgmt.group.load_all_groups("someTenantId")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.group_load_all_path}", @@ -54,8 +55,8 @@ def test_load_all_groups(self): json={ "tenantId": "someTenantId", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -68,8 +69,8 @@ def test_load_all_groups_for_members(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.group.load_all_groups_for_members, @@ -77,8 +78,8 @@ def test_load_all_groups_for_members(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( client.mgmt.group.load_all_groups_for_members( "someTenantId", ["one", "two"], ["three", "four"] @@ -97,8 +98,8 @@ def test_load_all_groups_for_members(self): "loginIds": ["three", "four"], "userIds": ["one", "two"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -111,8 +112,8 @@ def test_load_all_group_members(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.group.load_all_group_members, @@ -121,8 +122,8 @@ def test_load_all_group_members(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( client.mgmt.group.load_all_group_members("someTenantId", "someGroupId") ) @@ -138,7 +139,7 @@ def test_load_all_group_members(self): "tenantId": "someTenantId", "groupId": "someGroupId", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_jwt.py b/tests/management/test_jwt.py index 2cab70de8..536b9f5e2 100644 --- a/tests/management/test_jwt.py +++ b/tests/management/test_jwt.py @@ -5,8 +5,8 @@ from descope import AuthException, DescopeClient from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtLoginOptions, MgmtV1 - -from .. import common +from tests import common +from tests.testutils import SSLMatcher class TestUser(common.DescopeTest): @@ -33,8 +33,8 @@ def test_update_jwt(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.jwt.update_jwt, "jwt", {"k1": "v1"}, 0 ) @@ -44,9 +44,9 @@ def test_update_jwt(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"jwt": "response"}""") mock_post.return_value = network_resp resp = client.mgmt.jwt.update_jwt("test", {"k1": "v1"}, 40) @@ -64,8 +64,8 @@ def test_update_jwt(self): "customClaims": {"k1": "v1"}, "refreshDuration": 40, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -85,8 +85,8 @@ def test_update_jwt(self): "customClaims": {"k1": "v1"}, "refreshDuration": 0, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -100,8 +100,8 @@ def test_impersonate(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.jwt.impersonate, "imp1", "imp2", False ) @@ -115,9 +115,9 @@ def test_impersonate(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"jwt": "response"}""") mock_post.return_value = network_resp resp = client.mgmt.jwt.impersonate("imp1", "imp2", True) @@ -138,8 +138,8 @@ def test_impersonate(self): "selectedTenant": None, "refreshDuration": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -153,8 +153,8 @@ def test_stop_impersonation(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.jwt.stop_impersonation, @@ -162,9 +162,9 @@ def test_stop_impersonation(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"jwt": "response"}""") mock_post.return_value = network_resp resp = client.mgmt.jwt.stop_impersonation("jwtstr") @@ -183,8 +183,8 @@ def test_stop_impersonation(self): "selectedTenant": None, "refreshDuration": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -208,9 +208,9 @@ def test_sign_in(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"jwt": "response"}""") mock_post.return_value = network_resp client.mgmt.jwt.sign_in("loginId") @@ -231,8 +231,8 @@ def test_sign_in(self): "jwt": None, "refreshDuration": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -249,9 +249,9 @@ def test_sign_up(self): self.assertRaises(AuthException, client.mgmt.jwt.sign_up, "") # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"jwt": "response"}""") mock_post.return_value = network_resp client.mgmt.jwt.sign_up("loginId") @@ -282,8 +282,8 @@ def test_sign_up(self): "customClaims": None, "refreshDuration": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -300,9 +300,9 @@ def test_sign_up_or_in(self): self.assertRaises(AuthException, client.mgmt.jwt.sign_up_or_in, "") # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"jwt": "response"}""") mock_post.return_value = network_resp client.mgmt.jwt.sign_up_or_in("loginId") @@ -333,8 +333,8 @@ def test_sign_up_or_in(self): "customClaims": None, "refreshDuration": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -348,9 +348,9 @@ def test_anonymous(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"jwt": "response"}""") mock_post.return_value = network_resp client.mgmt.jwt.anonymous({"k1": "v1"}, "id") @@ -367,8 +367,8 @@ def test_anonymous(self): "selectedTenant": "id", "refreshDuration": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_outbound_application.py b/tests/management/test_outbound_application.py index 05a3874f1..4161c52a0 100644 --- a/tests/management/test_outbound_application.py +++ b/tests/management/test_outbound_application.py @@ -31,9 +31,9 @@ def test_create_application_success(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "app": { "id": "app123", @@ -70,9 +70,9 @@ def test_create_application_with_all_parameters_success(self): token_params = [URLParam("grant_type", "authorization_code")] prompts = [PromptType.LOGIN, PromptType.CONSENT] - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "app": { "id": "app123", @@ -118,8 +118,8 @@ def test_create_application_failure(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.create_application, @@ -134,9 +134,9 @@ def test_update_application_success(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "app": { "id": "app123", @@ -176,9 +176,9 @@ def test_update_application_with_all_parameters_success(self): token_params = [URLParam("grant_type", "authorization_code")] prompts = [PromptType.LOGIN, PromptType.CONSENT, PromptType.SELECT_ACCOUNT] - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "app": { "id": "app123", @@ -227,8 +227,8 @@ def test_update_application_failure(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.update_application, @@ -244,9 +244,9 @@ def test_delete_application_success(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp client.mgmt.outbound_application.delete_application("app123") @@ -258,8 +258,8 @@ def test_delete_application_failure(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.delete_application, @@ -274,9 +274,9 @@ def test_load_application_success(self): self.dummy_management_key, ) - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "app": { "id": "app123", @@ -303,8 +303,8 @@ def test_load_application_failure(self): self.dummy_management_key, ) - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.load_application, @@ -319,9 +319,9 @@ def test_load_all_applications_success(self): self.dummy_management_key, ) - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "apps": [ {"id": "app1", "name": "App 1", "description": "Description 1"}, @@ -346,8 +346,8 @@ def test_load_all_applications_failure(self): self.dummy_management_key, ) - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.load_all_applications, @@ -361,9 +361,9 @@ def test_fetch_token_by_scopes_success(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -400,8 +400,8 @@ def test_fetch_token_by_scopes_failure(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.fetch_token_by_scopes, @@ -418,9 +418,9 @@ def test_fetch_token_success(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -453,8 +453,8 @@ def test_fetch_token_failure(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.fetch_token, @@ -470,9 +470,9 @@ def test_fetch_tenant_token_by_scopes_success(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -505,8 +505,8 @@ def test_fetch_tenant_token_by_scopes_failure(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.fetch_tenant_token_by_scopes, @@ -523,9 +523,9 @@ def test_fetch_tenant_token_success(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -558,8 +558,8 @@ def test_fetch_tenant_token_failure(self): self.dummy_management_key, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application.fetch_tenant_token, @@ -807,9 +807,9 @@ def setUp(self) -> None: def test_fetch_token_by_scopes_success(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -843,8 +843,8 @@ def test_fetch_token_by_scopes_failure(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) # Test failure of empty token - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_token_by_scopes, @@ -855,8 +855,8 @@ def test_fetch_token_by_scopes_failure(self): ) # Test invalid response failure - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_token_by_scopes, @@ -869,9 +869,9 @@ def test_fetch_token_by_scopes_failure(self): def test_fetch_token_success(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -904,8 +904,8 @@ def test_fetch_token_failure(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) # Test failure of empty token - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_token, @@ -915,8 +915,8 @@ def test_fetch_token_failure(self): ) # Test invalid response failure - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_token, @@ -928,9 +928,9 @@ def test_fetch_token_failure(self): def test_fetch_tenant_token_by_scopes_success(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -965,8 +965,8 @@ def test_fetch_tenant_token_by_scopes_failure(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) # Test failure of empty token - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_tenant_token_by_scopes, @@ -977,8 +977,8 @@ def test_fetch_tenant_token_by_scopes_failure(self): ) # Test invalid response failure - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_tenant_token_by_scopes, @@ -991,9 +991,9 @@ def test_fetch_tenant_token_by_scopes_failure(self): def test_fetch_tenant_token_success(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = { "token": { "token": "access-token", @@ -1022,8 +1022,8 @@ def test_fetch_tenant_token_failure(self): client = DescopeClient(self.dummy_project_id, self.public_key_dict, False) # Test failure of empty token - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_tenant_token, @@ -1033,8 +1033,8 @@ def test_fetch_tenant_token_failure(self): ) # Test invalid response failure - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.outbound_application_by_token.fetch_tenant_token, diff --git a/tests/management/test_permission.py b/tests/management/test_permission.py index b6a9caca0..aa7e1f93f 100644 --- a/tests/management/test_permission.py +++ b/tests/management/test_permission.py @@ -6,6 +6,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -33,8 +34,8 @@ def test_create(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.permission.create, @@ -42,8 +43,8 @@ def test_create(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.permission.create("P1", "Something")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.permission_create_path}", @@ -57,8 +58,8 @@ def test_create(self): "name": "P1", "description": "Something", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -71,8 +72,8 @@ def test_update(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.permission.update, @@ -81,8 +82,8 @@ def test_update(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.permission.update( "name", @@ -103,8 +104,8 @@ def test_update(self): "newName": "new-name", "description": "new-description", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -117,8 +118,8 @@ def test_delete(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.permission.delete, @@ -126,8 +127,8 @@ def test_delete(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.permission.delete("name")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.permission_delete_path}", @@ -140,8 +141,8 @@ def test_delete(self): json={ "name": "name", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -154,14 +155,14 @@ def test_load_all(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.permission.load_all) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"permissions": [{"name": "p1"}, {"name": "p2"}]}""" ) @@ -179,7 +180,7 @@ def test_load_all(self): "x-descope-project-id": self.dummy_project_id, }, params=None, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_project.py b/tests/management/test_project.py index 3217dcfec..2b3d15810 100644 --- a/tests/management/test_project.py +++ b/tests/management/test_project.py @@ -6,6 +6,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -33,8 +34,8 @@ def test_update_name(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.project.update_name, @@ -42,8 +43,8 @@ def test_update_name(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.project.update_name("new-name")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.project_update_name}", @@ -56,8 +57,8 @@ def test_update_name(self): json={ "name": "new-name", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -70,8 +71,8 @@ def test_update_tags(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.project.update_tags, @@ -79,8 +80,8 @@ def test_update_tags(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.project.update_tags(["tag1", "tag2"])) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.project_update_tags}", @@ -93,8 +94,8 @@ def test_update_tags(self): json={ "tags": ["tag1", "tag2"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -107,17 +108,17 @@ def test_list_projects(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.project.list_projects, ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True json_str = """ { "projects": [ @@ -145,8 +146,8 @@ def test_list_projects(self): }, params=None, json={}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -159,8 +160,8 @@ def test_clone(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.project.clone, @@ -170,9 +171,9 @@ def test_clone(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ { @@ -200,8 +201,8 @@ def test_clone(self): "environment": "production", "tags": ["apple", "banana", "cherry"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -214,17 +215,17 @@ def test_export_project(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.project.export_project, ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ { @@ -245,8 +246,8 @@ def test_export_project(self): }, params=None, json={}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -259,8 +260,8 @@ def test_import_project(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.project.import_project, @@ -270,9 +271,9 @@ def test_import_project(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp files = { "foo": "bar", @@ -291,7 +292,7 @@ def test_import_project(self): "foo": "bar", }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_role.py b/tests/management/test_role.py index 8bc15e176..bd040269b 100644 --- a/tests/management/test_role.py +++ b/tests/management/test_role.py @@ -6,6 +6,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -33,8 +34,8 @@ def test_create(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.role.create, @@ -42,8 +43,8 @@ def test_create(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.role.create("R1", "Something", ["P1"], "t1", True) ) @@ -62,8 +63,8 @@ def test_create(self): "tenantId": "t1", "default": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -76,8 +77,8 @@ def test_update(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.role.update, @@ -86,8 +87,8 @@ def test_update(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.role.update( "name", @@ -114,8 +115,8 @@ def test_update(self): "tenantId": "t1", "default": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -128,8 +129,8 @@ def test_delete(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.role.delete, @@ -137,8 +138,8 @@ def test_delete(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.role.delete("name")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.role_delete_path}", @@ -149,8 +150,8 @@ def test_delete(self): }, params=None, json={"name": "name", "tenantId": None}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -163,14 +164,14 @@ def test_load_all(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.role.load_all) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ { @@ -199,8 +200,8 @@ def test_load_all(self): "x-descope-project-id": self.dummy_project_id, }, params=None, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -213,8 +214,8 @@ def test_search(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.role.search, @@ -223,9 +224,9 @@ def test_search(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ { @@ -260,7 +261,7 @@ def test_search(self): "roleNameLike": "x", "permissionNames": ["p1", "p2"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_sso_application.py b/tests/management/test_sso_application.py index 8da82ba33..6fbedaa1f 100644 --- a/tests/management/test_sso_application.py +++ b/tests/management/test_sso_application.py @@ -12,6 +12,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -39,8 +40,8 @@ def test_create_oidc_application(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso_application.create_oidc_application, @@ -49,9 +50,9 @@ def test_create_oidc_application(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"id": "app1"}""") mock_post.return_value = network_resp resp = client.mgmt.sso_application.create_oidc_application( @@ -77,8 +78,8 @@ def test_create_oidc_application(self): "logo": None, "forceAuthentication": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -109,8 +110,8 @@ def test_create_saml_application(self): entity_id="", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso_application.create_saml_application, @@ -121,9 +122,9 @@ def test_create_saml_application(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"id": "app1"}""") mock_post.return_value = network_resp resp = client.mgmt.sso_application.create_saml_application( @@ -188,8 +189,8 @@ def test_create_saml_application(self): "forceAuthentication": True, "logoutRedirectUrl": "http://dummy.com/logout", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -202,8 +203,8 @@ def test_update_oidc_application(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso_application.update_oidc_application, @@ -213,9 +214,9 @@ def test_update_oidc_application(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True self.assertIsNone( client.mgmt.sso_application.update_oidc_application( "app1", "name", "http://dummy.com" @@ -238,8 +239,8 @@ def test_update_oidc_application(self): "logo": None, "forceAuthentication": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -272,8 +273,8 @@ def test_update_saml_application(self): entity_id="", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso_application.update_saml_application, @@ -285,9 +286,9 @@ def test_update_saml_application(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True self.assertIsNone( client.mgmt.sso_application.update_saml_application( id="id1", @@ -352,8 +353,8 @@ def test_update_saml_application(self): "forceAuthentication": False, "logoutRedirectUrl": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -366,8 +367,8 @@ def test_delete(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso_application.delete, @@ -375,8 +376,8 @@ def test_delete(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.sso_application.delete("app1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.sso_application_delete_path}", @@ -389,8 +390,8 @@ def test_delete(self): json={ "id": "app1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -403,8 +404,8 @@ def test_load(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso_application.load, @@ -412,9 +413,9 @@ def test_load(self): ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ {"id":"app1","name":"App1","description":"","enabled":true,"logo":"","appType":"saml","samlSettings":{"loginPageUrl":"http://dummy.com/login","idpCert":"cert","useMetadataInfo":true,"metadataUrl":"http://dummy.com/md","entityId":"","acsUrl":"","certificate":"","attributeMapping":[{"name":"email","type":"","value":"attrVal1"}],"groupsMapping":[{"name":"grp1","type":"","filterType":"roles","value":"","roles":[{"id":"myRoleId","name":"myRole"}]}],"idpMetadataUrl":"","idpEntityId":"","idpSsoUrl":"","acsAllowedCallbacks":[],"subjectNameIdType":"","subjectNameIdFormat":""},"oidcSettings":{"loginPageUrl":"","issuer":"","discoveryUrl":""}} @@ -453,8 +454,8 @@ def test_load(self): "x-descope-project-id": self.dummy_project_id, }, params={"id": "app1"}, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -467,14 +468,14 @@ def test_load_all(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.sso_application.load_all) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ { @@ -534,7 +535,7 @@ def test_load_all(self): "x-descope-project-id": self.dummy_project_id, }, params=None, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_sso_settings.py b/tests/management/test_sso_settings.py index 482e236ba..0c6cbc9b9 100644 --- a/tests/management/test_sso_settings.py +++ b/tests/management/test_sso_settings.py @@ -13,6 +13,7 @@ SSOSettings, ) +from tests.testutils import SSLMatcher from .. import common @@ -40,8 +41,8 @@ def test_delete_settings(self): ) # Test failed flows - with patch("requests.delete") as mock_delete: - mock_delete.return_value.ok = False + with patch("httpx.delete") as mock_delete: + mock_delete.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.delete_settings, @@ -49,9 +50,9 @@ def test_delete_settings(self): ) # Test success flow - with patch("requests.delete") as mock_delete: + with patch("httpx.delete") as mock_delete: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_delete.return_value = network_resp client.mgmt.sso.delete_settings("tenant-id") @@ -64,8 +65,8 @@ def test_delete_settings(self): "Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}", "x-descope-project-id": self.dummy_project_id, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -78,8 +79,8 @@ def test_load_settings(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.load_settings, @@ -87,9 +88,9 @@ def test_load_settings(self): ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"tenant": {"id": "T2AAAA", "name": "myTenantName", "selfProvisioningDomains": [], "customAttributes": {}, "authType": "saml", "domains": ["lulu", "kuku"]}, "saml": {"idpEntityId": "", "idpSSOUrl": "", "idpCertificate": "", "defaultSSORoles": ["aa", "bb"], "idpMetadataUrl": "https://dummy.com/metadata", "spEntityId": "", "spACSUrl": "", "spCertificate": "", "attributeMapping": {"name": "name", "email": "email", "username": "", "phoneNumber": "phone", "group": "", "givenName": "", "middleName": "", "familyName": "", "picture": "", "customAttributes": {}}, "groupsMapping": [], "redirectUrl": ""}, "oidc": {"name": "", "clientId": "", "clientSecret": "", "redirectUrl": "", "authUrl": "", "tokenUrl": "", "userDataUrl": "", "scope": [], "JWKsUrl": "", "userAttrMapping": {"loginId": "sub", "username": "", "name": "name", "email": "email", "phoneNumber": "phone_number", "verifiedEmail": "email_verified", "verifiedPhone": "phone_number_verified", "picture": "picture", "givenName": "given_name", "middleName": "middle_name", "familyName": "family_name"}, "manageProviderTokens": false, "callbackDomain": "", "prompt": [], "grantType": "authorization_code", "issuer": ""}}""" ) @@ -114,8 +115,8 @@ def test_load_settings(self): "x-descope-project-id": self.dummy_project_id, }, params={"tenantId": "T2AAAA"}, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -128,8 +129,8 @@ def test_configure_oidc_settings(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.configure_oidc_settings, @@ -142,8 +143,8 @@ def test_configure_oidc_settings(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure_oidc_settings( "tenant-id", @@ -214,8 +215,8 @@ def test_configure_oidc_settings(self): }, "domains": ["domain.com"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -228,8 +229,8 @@ def test_configure_saml_settings(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.configure_saml_settings, @@ -247,8 +248,8 @@ def test_configure_saml_settings(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure_saml_settings( "tenant-id", @@ -308,8 +309,8 @@ def test_configure_saml_settings(self): "redirectUrl": "https://redirect.com", "domains": ["domain.com"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -322,8 +323,8 @@ def test_configure_saml_settings_by_metadata(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.configure_saml_settings_by_metadata, @@ -334,8 +335,8 @@ def test_configure_saml_settings_by_metadata(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure_saml_settings_by_metadata( "tenant-id", @@ -391,8 +392,8 @@ def test_configure_saml_settings_by_metadata(self): "redirectUrl": "https://redirect.com", "domains": ["domain.com"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -409,8 +410,8 @@ def test_get_settings(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.get_settings, @@ -418,9 +419,9 @@ def test_get_settings(self): ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"domains": ["lulu", "kuku"], "tenantId": "tenant-id"}""" ) @@ -436,8 +437,8 @@ def test_get_settings(self): "x-descope-project-id": self.dummy_project_id, }, params={"tenantId": "tenant-id"}, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -450,8 +451,8 @@ def test_configure(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.configure, @@ -464,8 +465,8 @@ def test_configure(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure( "tenant-id", @@ -492,14 +493,14 @@ def test_configure(self): "redirectURL": "https://redirect.com", "domains": ["domain.com"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Domain is optional - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure( "tenant-id", @@ -525,14 +526,14 @@ def test_configure(self): "redirectURL": "https://redirect.com", "domains": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Redirect is optional - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure( "tenant-id", @@ -559,8 +560,8 @@ def test_configure(self): "redirectURL": "", "domains": ["domain.com"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -573,8 +574,8 @@ def test_configure_via_metadata(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.configure_via_metadata, @@ -585,8 +586,8 @@ def test_configure_via_metadata(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure_via_metadata( "tenant-id", @@ -609,14 +610,14 @@ def test_configure_via_metadata(self): "redirectURL": "https://redirect.com", "domains": ["domain.com"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test partial arguments - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.configure_via_metadata( "tenant-id", @@ -637,8 +638,8 @@ def test_configure_via_metadata(self): "redirectURL": None, "domains": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -651,8 +652,8 @@ def test_mapping(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.sso.mapping, @@ -662,8 +663,8 @@ def test_mapping(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.sso.mapping( "tenant-id", @@ -694,7 +695,7 @@ def test_mapping(self): "customAttributes": None, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_tenant.py b/tests/management/test_tenant.py index 6a705cadd..74a35bf4b 100644 --- a/tests/management/test_tenant.py +++ b/tests/management/test_tenant.py @@ -6,6 +6,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS from descope.management.common import MgmtV1 +from tests.testutils import SSLMatcher from .. import common @@ -33,8 +34,8 @@ def test_create(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.tenant.create, @@ -42,9 +43,9 @@ def test_create(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"id": "t1"}""") mock_post.return_value = network_resp resp = client.mgmt.tenant.create("name", "t1", ["domain.com"]) @@ -64,15 +65,15 @@ def test_create(self): "enforceSSO": False, "disabled": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with custom attributes, enforce_sso, disabled - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"id": "t1"}""") mock_post.return_value = network_resp resp = client.mgmt.tenant.create( @@ -100,8 +101,8 @@ def test_create(self): "enforceSSO": True, "disabled": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -114,8 +115,8 @@ def test_update(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.tenant.update, @@ -124,8 +125,8 @@ def test_update(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.tenant.update( "t1", "new-name", ["domain.com"], enforce_sso=True, disabled=True @@ -146,14 +147,14 @@ def test_update(self): "enforceSSO": True, "disabled": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with custom attributes, enforce_sso, disabled - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone( client.mgmt.tenant.update( "t1", @@ -180,8 +181,8 @@ def test_update(self): "enforceSSO": True, "disabled": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -194,8 +195,8 @@ def test_delete(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.tenant.delete, @@ -203,8 +204,8 @@ def test_delete(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(client.mgmt.tenant.delete("t1", True)) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.tenant_delete_path}", @@ -215,8 +216,8 @@ def test_delete(self): }, params=None, json={"id": "t1", "cascade": True}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -229,8 +230,8 @@ def test_load(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.mgmt.tenant.load, @@ -238,9 +239,9 @@ def test_load(self): ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ {"id": "t1", "name": "tenant1", "selfProvisioningDomains": ["domain1.com"], "createdTime": 172606520} @@ -258,8 +259,8 @@ def test_load(self): "x-descope-project-id": self.dummy_project_id, }, params={"id": "t1"}, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -272,14 +273,14 @@ def test_load_all(self): ) # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.tenant.load_all) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ { @@ -305,8 +306,8 @@ def test_load_all(self): "x-descope-project-id": self.dummy_project_id, }, params=None, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -319,14 +320,14 @@ def test_search_all(self): ) # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.mgmt.tenant.search_all) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ { @@ -361,8 +362,8 @@ def test_search_all(self): "tenantSelfProvisioningDomains": ["spd1"], "customAttributes": {"k1": "v1"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/management/test_user.py b/tests/management/test_user.py index 33c0bb6bc..55e71af2e 100644 --- a/tests/management/test_user.py +++ b/tests/management/test_user.py @@ -14,6 +14,7 @@ UserPasswordPbkdf2, ) +from tests.testutils import SSLMatcher from .. import common @@ -40,8 +41,8 @@ def setUp(self) -> None: def test_create(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.create, @@ -49,9 +50,9 @@ def test_create(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.create( @@ -94,16 +95,16 @@ def test_create(self): "additionalLoginIds": ["id-1", "id-2"], "ssoAppIDs": ["app1", "app2"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_create_with_verified_parameters(self): # Test success flow with verified email and phone - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.create( @@ -148,15 +149,15 @@ def test_create_with_verified_parameters(self): "additionalLoginIds": None, "ssoAppIDs": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_create_test_user(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.create, @@ -164,9 +165,9 @@ def test_create_test_user(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.create_test_user( @@ -206,15 +207,15 @@ def test_create_test_user(self): "additionalLoginIds": None, "ssoAppIDs": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_invite(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.invite, @@ -222,9 +223,9 @@ def test_invite(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.invite( @@ -271,15 +272,15 @@ def test_invite(self): "ssoAppIDs": ["app1", "app2"], "templateId": "tid", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_invite_batch(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.invite_batch, @@ -287,9 +288,9 @@ def test_invite_batch(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"users": [{"id": "u1"}]}""") mock_post.return_value = network_resp user = UserObj( @@ -370,8 +371,8 @@ def test_invite_batch(self): }, params=None, json=expected_users, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -414,8 +415,8 @@ def test_invite_batch(self): }, params=None, json=expected_users, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -436,15 +437,15 @@ def test_invite_batch(self): }, params=None, json=expected_users, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.update, @@ -453,9 +454,9 @@ def test_update(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update( @@ -489,14 +490,14 @@ def test_update(self): "additionalLoginIds": None, "ssoAppIDs": ["app1", "app2"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with verified flags - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update( @@ -527,15 +528,15 @@ def test_update(self): "additionalLoginIds": None, "ssoAppIDs": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_patch(self): # Test failed flows - with patch("requests.patch") as mock_patch: - mock_patch.return_value.ok = False + with patch("httpx.patch") as mock_patch: + mock_patch.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.patch, @@ -544,9 +545,9 @@ def test_patch(self): ) # Test success flow with some params set - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_patch.return_value = network_resp resp = self.client.mgmt.user.patch( @@ -579,14 +580,14 @@ def test_patch(self): "customAttributes": {"ak": "av"}, "ssoAppIds": ["app1", "app2"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with other params - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_patch.return_value = network_resp resp = self.client.mgmt.user.patch( @@ -629,8 +630,8 @@ def test_patch(self): {"tenantId": "tenant2", "roleNames": ["role1", "role2"]}, ], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -646,9 +647,9 @@ def test_patch_with_status(self): valid_statuses = ["enabled", "disabled", "invited"] for status in valid_statuses: - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"user": {"id": "u1"}}""" ) @@ -670,15 +671,15 @@ def test_patch_with_status(self): "loginId": "id", "status": status, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test that status is not included when None - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_patch.return_value = network_resp @@ -715,9 +716,9 @@ def test_patch_batch(self): UserObj(login_id="user3", phone="+123456789", status="invited"), ] - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"patchedUsers": [{"id": "u1"}, {"id": "u2"}, {"id": "u3"}], "failedUsers": []}""" ) @@ -755,15 +756,15 @@ def test_patch_batch(self): }, ] }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test batch with mixed success/failure response - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"patchedUsers": [{"id": "u1"}], "failedUsers": [{"failure": "User not found", "user": {"loginId": "user2"}}]}""" ) @@ -778,8 +779,8 @@ def test_patch_batch(self): self.assertEqual(resp["failedUsers"][0]["failure"], "User not found") # Test failed batch operation - with patch("requests.patch") as mock_patch: - mock_patch.return_value.ok = False + with patch("httpx.patch") as mock_patch: + mock_patch.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.patch_batch, @@ -787,9 +788,9 @@ def test_patch_batch(self): ) # Test with test users flag - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"patchedUsers": [{"id": "u1"}], "failedUsers": []}""" ) @@ -805,8 +806,8 @@ def test_patch_batch(self): def test_delete(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.delete, @@ -814,8 +815,8 @@ def test_delete(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(self.client.mgmt.user.delete("u1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.user_delete_path}", @@ -828,15 +829,15 @@ def test_delete(self): json={ "loginId": "u1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_delete_by_user_id(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.delete_by_user_id, @@ -844,8 +845,8 @@ def test_delete_by_user_id(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(self.client.mgmt.user.delete_by_user_id("u1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.user_delete_path}", @@ -858,15 +859,15 @@ def test_delete_by_user_id(self): json={ "userId": "u1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_logout(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.logout_user, @@ -874,8 +875,8 @@ def test_logout(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(self.client.mgmt.user.logout_user("u1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.user_logout_path}", @@ -888,15 +889,15 @@ def test_logout(self): json={ "loginId": "u1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_logout_by_user_id(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.logout_user_by_user_id, @@ -904,8 +905,8 @@ def test_logout_by_user_id(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNone(self.client.mgmt.user.logout_user_by_user_id("u1")) mock_post.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.user_logout_path}", @@ -918,23 +919,23 @@ def test_logout_by_user_id(self): json={ "userId": "u1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_delete_all_test_users(self): # Test failed flows - with patch("requests.delete") as mock_delete: - mock_delete.return_value.ok = False + with patch("httpx.delete") as mock_delete: + mock_delete.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.delete_all_test_users, ) # Test success flow - with patch("requests.delete") as mock_delete: - mock_delete.return_value.ok = True + with patch("httpx.delete") as mock_delete: + mock_delete.return_value.is_success = True self.assertIsNone(self.client.mgmt.user.delete_all_test_users()) mock_delete.assert_called_with( f"{common.DEFAULT_BASE_URL}{MgmtV1.user_delete_all_test_users_path}", @@ -944,15 +945,15 @@ def test_delete_all_test_users(self): "Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}", "x-descope-project-id": self.dummy_project_id, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_load(self): # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.load, @@ -960,9 +961,9 @@ def test_load(self): ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_get.return_value = network_resp resp = self.client.mgmt.user.load("valid-id") @@ -976,15 +977,15 @@ def test_load(self): "x-descope-project-id": self.dummy_project_id, }, params={"loginId": "valid-id"}, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_load_by_user_id(self): # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.load_by_user_id, @@ -992,9 +993,9 @@ def test_load_by_user_id(self): ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_get.return_value = network_resp resp = self.client.mgmt.user.load_by_user_id("user-id") @@ -1008,15 +1009,15 @@ def test_load_by_user_id(self): "x-descope-project-id": self.dummy_project_id, }, params={"userId": "user-id"}, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_search_all(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.search_all, @@ -1024,8 +1025,8 @@ def test_search_all(self): ["r1", "r2"], ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertRaises( AuthException, self.client.mgmt.user.search_all, [], [], -1, 0 ) @@ -1035,9 +1036,9 @@ def test_search_all(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1071,15 +1072,15 @@ def test_search_all(self): "ssoAppIds": ["app1"], "loginIds": ["l1"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with text and sort - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1119,15 +1120,15 @@ def test_search_all(self): {"desc": False, "field": "bubu"}, ], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with custom attributes - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1165,15 +1166,15 @@ def test_search_all(self): "emails": ["a@b.com"], "phones": ["+111111"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with time parameters - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1211,15 +1212,15 @@ def test_search_all(self): "fromModifiedTime": 300, "toModifiedTime": 400, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with tenant_role_ids and tenant_role_names - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1258,15 +1259,15 @@ def test_search_all(self): "tenant2": {"values": ["admin", "user"], "and": False} }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_search_all_test_users(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.search_all_test_users, @@ -1274,8 +1275,8 @@ def test_search_all_test_users(self): ["r1", "r2"], ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertRaises( AuthException, self.client.mgmt.user.search_all_test_users, @@ -1295,9 +1296,9 @@ def test_search_all_test_users(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1330,15 +1331,15 @@ def test_search_all_test_users(self): "ssoAppIds": ["app1"], "loginIds": ["l1"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with text and sort - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1377,15 +1378,15 @@ def test_search_all_test_users(self): {"desc": False, "field": "bubu"}, ], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with custom attributes - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1422,15 +1423,15 @@ def test_search_all_test_users(self): "emails": ["a@b.com"], "phones": ["+111111"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with time parameters - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"users": [{"id": "u1"}]}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.search_all_test_users( @@ -1465,15 +1466,15 @@ def test_search_all_test_users(self): "fromModifiedTime": 300, "toModifiedTime": 400, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with tenant_role_ids and tenant_role_names - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"users": [{"id": "u1"}, {"id": "u2"}]}""" ) @@ -1512,15 +1513,15 @@ def test_search_all_test_users(self): "tenant2": {"values": ["admin", "user"], "and": False} }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_get_provider_token(self): # Test failed flows - with patch("requests.get") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.get") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.get_provider_token, @@ -1528,9 +1529,9 @@ def test_get_provider_token(self): "p1", ) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"provider": "p1", "providerUserId": "puid", "accessToken": "access123", "refreshToken": "refresh456", "expiration": "123123123", "scopes": ["s1", "s2"]}""" ) @@ -1557,15 +1558,15 @@ def test_get_provider_token(self): "withRefreshToken": True, "forceRefresh": True, }, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_activate(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.activate, @@ -1573,9 +1574,9 @@ def test_activate(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.activate("valid-id") @@ -1593,15 +1594,15 @@ def test_activate(self): "loginId": "valid-id", "status": "enabled", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_deactivate(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.deactivate, @@ -1609,9 +1610,9 @@ def test_deactivate(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.deactivate("valid-id") @@ -1629,15 +1630,15 @@ def test_deactivate(self): "loginId": "valid-id", "status": "disabled", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update_login_id(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.update_login_id, @@ -1646,9 +1647,9 @@ def test_update_login_id(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "a@b.c"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update_login_id("valid-id", "a@b.c") @@ -1666,15 +1667,15 @@ def test_update_login_id(self): "loginId": "valid-id", "newLoginId": "a@b.c", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update_email(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.update_email, @@ -1683,9 +1684,9 @@ def test_update_email(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update_email("valid-id", "a@b.c") @@ -1704,15 +1705,15 @@ def test_update_email(self): "email": "a@b.c", "verified": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update_phone(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.update_phone, @@ -1721,9 +1722,9 @@ def test_update_phone(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update_phone("valid-id", "+18005551234", True) @@ -1742,15 +1743,15 @@ def test_update_phone(self): "phone": "+18005551234", "verified": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update_display_name(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.update_display_name, @@ -1759,9 +1760,9 @@ def test_update_display_name(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update_display_name("valid-id", "foo") @@ -1779,15 +1780,15 @@ def test_update_display_name(self): "loginId": "valid-id", "displayName": "foo", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update_picture(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.update_picture, @@ -1796,9 +1797,9 @@ def test_update_picture(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update_picture("valid-id", "foo") @@ -1816,15 +1817,15 @@ def test_update_picture(self): "loginId": "valid-id", "picture": "foo", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update_custom_attribute(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.update_custom_attribute, @@ -1834,9 +1835,9 @@ def test_update_custom_attribute(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.update_custom_attribute( @@ -1856,16 +1857,16 @@ def test_update_custom_attribute(self): "attributeKey": "foo", "attributeValue": "bar", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_set_roles(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.set_roles, @@ -1874,9 +1875,9 @@ def test_set_roles(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.set_roles("valid-id", ["foo", "bar"]) @@ -1894,15 +1895,15 @@ def test_set_roles(self): "loginId": "valid-id", "roleNames": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_add_roles(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.add_roles, @@ -1911,9 +1912,9 @@ def test_add_roles(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.add_roles("valid-id", ["foo", "bar"]) @@ -1931,15 +1932,15 @@ def test_add_roles(self): "loginId": "valid-id", "roleNames": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_remove_roles(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.remove_roles, @@ -1948,9 +1949,9 @@ def test_remove_roles(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.remove_roles("valid-id", ["foo", "bar"]) @@ -1968,15 +1969,15 @@ def test_remove_roles(self): "loginId": "valid-id", "roleNames": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_add_sso_apps(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.add_sso_apps, @@ -1985,9 +1986,9 @@ def test_add_sso_apps(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.add_sso_apps("valid-id", ["foo", "bar"]) @@ -2005,15 +2006,15 @@ def test_add_sso_apps(self): "loginId": "valid-id", "ssoAppIds": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_set_sso_apps(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.set_sso_apps, @@ -2022,9 +2023,9 @@ def test_set_sso_apps(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.set_sso_apps("valid-id", ["foo", "bar"]) @@ -2042,15 +2043,15 @@ def test_set_sso_apps(self): "loginId": "valid-id", "ssoAppIds": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_remove_sso_apps(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.remove_sso_apps, @@ -2059,9 +2060,9 @@ def test_remove_sso_apps(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.remove_sso_apps("valid-id", ["foo", "bar"]) @@ -2079,15 +2080,15 @@ def test_remove_sso_apps(self): "loginId": "valid-id", "ssoAppIds": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_add_tenant(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.add_tenant, @@ -2096,9 +2097,9 @@ def test_add_tenant(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.add_tenant("valid-id", "tid") @@ -2116,15 +2117,15 @@ def test_add_tenant(self): "loginId": "valid-id", "tenantId": "tid", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_remove_tenant(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.remove_tenant, @@ -2133,9 +2134,9 @@ def test_remove_tenant(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.remove_tenant("valid-id", "tid") @@ -2153,15 +2154,15 @@ def test_remove_tenant(self): "loginId": "valid-id", "tenantId": "tid", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_set_tenant_roles(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.set_tenant_roles, @@ -2171,9 +2172,9 @@ def test_set_tenant_roles(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.set_tenant_roles( @@ -2194,15 +2195,15 @@ def test_set_tenant_roles(self): "tenantId": "tid", "roleNames": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_add_tenant_roles(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.add_tenant_roles, @@ -2212,9 +2213,9 @@ def test_add_tenant_roles(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.add_tenant_roles( @@ -2235,15 +2236,15 @@ def test_add_tenant_roles(self): "tenantId": "tid", "roleNames": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_remove_tenant_roles(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.remove_tenant_roles, @@ -2253,9 +2254,9 @@ def test_remove_tenant_roles(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"user": {"id": "u1"}}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.remove_tenant_roles( @@ -2276,15 +2277,15 @@ def test_remove_tenant_roles(self): "tenantId": "tid", "roleNames": ["foo", "bar"], }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_generate_otp_for_test_user(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.generate_otp_for_test_user, @@ -2293,9 +2294,9 @@ def test_generate_otp_for_test_user(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"code": "123456", "loginId": "login-id"}""" ) @@ -2323,15 +2324,15 @@ def test_generate_otp_for_test_user(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_user_set_temporary_password(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.set_temporary_password, @@ -2340,9 +2341,9 @@ def test_user_set_temporary_password(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp self.client.mgmt.user.set_temporary_password( "login-id", @@ -2361,15 +2362,15 @@ def test_user_set_temporary_password(self): "password": "some-password", "setActive": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_user_set_active_password(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.set_active_password, @@ -2378,9 +2379,9 @@ def test_user_set_active_password(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp self.client.mgmt.user.set_active_password( "login-id", @@ -2399,15 +2400,15 @@ def test_user_set_active_password(self): "password": "some-password", "setActive": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_user_set_password(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.set_password, @@ -2416,9 +2417,9 @@ def test_user_set_password(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp self.client.mgmt.user.set_password( "login-id", @@ -2437,15 +2438,15 @@ def test_user_set_password(self): "password": "some-password", "setActive": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_user_expire_password(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.expire_password, @@ -2453,9 +2454,9 @@ def test_user_expire_password(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp self.client.mgmt.user.expire_password( "login-id", @@ -2471,15 +2472,15 @@ def test_user_expire_password(self): json={ "loginId": "login-id", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_user_remove_all_passkeys(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.remove_all_passkeys, @@ -2487,9 +2488,9 @@ def test_user_remove_all_passkeys(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp self.client.mgmt.user.remove_all_passkeys( "login-id", @@ -2505,15 +2506,15 @@ def test_user_remove_all_passkeys(self): json={ "loginId": "login-id", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_user_remove_totp_seed(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.remove_totp_seed, @@ -2521,9 +2522,9 @@ def test_user_remove_totp_seed(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_post.return_value = network_resp self.client.mgmt.user.remove_totp_seed( "login-id", @@ -2539,15 +2540,15 @@ def test_user_remove_totp_seed(self): json={ "loginId": "login-id", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_generate_magic_link_for_test_user(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.generate_magic_link_for_test_user, @@ -2557,9 +2558,9 @@ def test_generate_magic_link_for_test_user(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"link": "some-link", "loginId": "login-id"}""" ) @@ -2588,15 +2589,15 @@ def test_generate_magic_link_for_test_user(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_generate_enchanted_link_for_test_user(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.generate_enchanted_link_for_test_user, @@ -2605,9 +2606,9 @@ def test_generate_enchanted_link_for_test_user(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """{"link": "some-link", "loginId": "login-id", "pendingRef": "some-ref"}""" ) @@ -2636,23 +2637,23 @@ def test_generate_enchanted_link_for_test_user(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_generate_embedded_link(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.generate_embedded_link, "login-id" ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"token": "some-token"}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.generate_embedded_link( @@ -2671,16 +2672,16 @@ def test_generate_embedded_link(self): "customClaims": {"k1": "v1"}, "timeout": 0, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_generate_sign_up_embedded_link(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.generate_sign_up_embedded_link, @@ -2688,9 +2689,9 @@ def test_generate_sign_up_embedded_link(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads("""{"token": "some-token"}""") mock_post.return_value = network_resp resp = self.client.mgmt.user.generate_sign_up_embedded_link( @@ -2712,24 +2713,24 @@ def test_generate_sign_up_embedded_link(self): "loginOptions": {}, "timeout": 0, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_history(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, self.client.mgmt.user.history, ["user-id-1", "user-id-2"] ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads( """ [ @@ -2779,16 +2780,16 @@ def test_history(self): "x-descope-project-id": self.dummy_project_id, }, json=["user-id-1", "user-id-2"], - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_update_test_user(self): - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads('{"user": {"id": "u1"}}') mock_post.return_value = network_resp resp = self.client.mgmt.user.update( @@ -2819,15 +2820,15 @@ def test_update_test_user(self): "additionalLoginIds": None, "ssoAppIDs": None, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_patch_test_user(self): - with patch("requests.patch") as mock_patch: + with patch("httpx.patch") as mock_patch: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True network_resp.json.return_value = json.loads('{"user": {"id": "u1"}}') mock_patch.return_value = network_resp resp = self.client.mgmt.user.patch( @@ -2850,7 +2851,7 @@ def test_patch_test_user(self): "displayName": "test-user", "test": True, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/test_auth.py b/tests/test_auth.py index 7dba9635f..f569c25b4 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -22,6 +22,7 @@ SESSION_TOKEN_NAME, EndpointsV1, ) +from tests.testutils import SSLMatcher from . import common @@ -112,20 +113,20 @@ def test_fetch_public_key(self): """ # Test failed flows - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False - self.assertRaises(AuthException, auth._fetch_public_keys) + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False + self.assertRaises(AuthException, auth._fetch_public_keys_sync) - with patch("requests.get") as mock_get: - mock_get.return_value.ok = True + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = True mock_get.return_value.text = "invalid json" - self.assertRaises(AuthException, auth._fetch_public_keys) + self.assertRaises(AuthException, auth._fetch_public_keys_sync) # test success flow - with patch("requests.get") as mock_get: - mock_get.return_value.ok = True + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = True mock_get.return_value.text = valid_keys_response - self.assertIsNone(auth._fetch_public_keys()) + self.assertIsNone(auth._fetch_public_keys_sync()) def test_project_id_from_env(self): os.environ["DESCOPE_PROJECT_ID"] = self.dummy_project_id @@ -354,8 +355,8 @@ def test_refresh_session(self): auth = Auth(self.dummy_project_id, self.public_key_dict) # Test fail flow - with patch("requests.post") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.post") as mock_request: + mock_request.return_value.is_success = False self.assertRaises( AuthException, auth.refresh_session, @@ -370,8 +371,8 @@ def test_validate_session_and_refresh_input(self): auth.validate_and_refresh_session(None, None) # Test validate_session with Ratelimit exception - with patch("requests.get") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.get") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = { "errorCode": "E130429", @@ -397,8 +398,8 @@ def test_validate_session_and_refresh_input(self): ) # Test refresh_session with Ratelimit exception - with patch("requests.get") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.get") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = { "errorCode": "E130429", @@ -428,8 +429,8 @@ def test_exchange_access_key(self): auth = Auth(self.dummy_project_id, self.public_key_dict) # Test fail flow - with patch("requests.post") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.post") as mock_request: + mock_request.return_value.is_success = False self.assertRaises( AuthException, auth.exchange_access_key, @@ -438,9 +439,9 @@ def test_exchange_access_key(self): # Test success flow valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL" - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = {"sessionJwt": valid_jwt_token} my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -460,8 +461,8 @@ def test_exchange_access_key(self): }, params=None, json={"loginOptions": {"customClaims": {"k1": "v1"}}}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -557,8 +558,8 @@ def test_api_rate_limit_exception(self): auth = Auth(self.dummy_project_id, self.public_key_dict) # Test do_post - with patch("requests.post") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.post") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = { "errorCode": "E130429", @@ -583,8 +584,8 @@ def test_api_rate_limit_exception(self): ) # Test do_get - with patch("requests.get") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.get") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = { "errorCode": "E130429", @@ -595,7 +596,7 @@ def test_api_rate_limit_exception(self): API_RATE_LIMIT_RETRY_AFTER_HEADER: "10" } with self.assertRaises(RateLimitException) as cm: - auth.do_get(uri="http://test.com", params=False, allow_redirects=None) + auth.do_get(uri="http://test.com", params=False, follow_redirects=None) the_exception = cm.exception self.assertEqual(the_exception.status_code, "E130429") self.assertEqual(the_exception.error_type, ERROR_TYPE_API_RATE_LIMIT) @@ -609,8 +610,8 @@ def test_api_rate_limit_exception(self): ) # Test do_delete - with patch("requests.delete") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.delete") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = { "errorCode": "E130429", @@ -635,9 +636,9 @@ def test_api_rate_limit_exception(self): ) # Test do_delete with params and pswd - with patch("requests.delete") as mock_delete: + with patch("httpx.delete") as mock_delete: network_resp = mock.Mock() - network_resp.ok = True + network_resp.is_success = True mock_delete.return_value = network_resp auth.do_delete("/a/b", params={"key": "value"}, pswd="pswd") @@ -650,14 +651,14 @@ def test_api_rate_limit_exception(self): "Authorization": f"Bearer {self.dummy_project_id}:{'pswd'}", "x-descope-project-id": self.dummy_project_id, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) - # Test _fetch_public_keys rate limit - with patch("requests.get") as mock_request: - mock_request.return_value.ok = False + # Test _fetch_public_keys_sync rate limit + with patch("httpx.get") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = { "errorCode": "E130429", @@ -668,7 +669,7 @@ def test_api_rate_limit_exception(self): API_RATE_LIMIT_RETRY_AFTER_HEADER: "10" } with self.assertRaises(RateLimitException) as cm: - auth._fetch_public_keys() + auth._fetch_public_keys_sync() the_exception = cm.exception self.assertEqual(the_exception.status_code, "E130429") self.assertEqual(the_exception.error_type, ERROR_TYPE_API_RATE_LIMIT) @@ -685,8 +686,8 @@ def test_api_rate_limit_invalid_header(self): auth = Auth(self.dummy_project_id, self.public_key_dict) # Test do_post empty body - with patch("requests.post") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.post") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = { "errorCode": "E130429", @@ -714,8 +715,8 @@ def test_api_rate_limit_invalid_response_body(self): auth = Auth(self.dummy_project_id, self.public_key_dict) # Test do_post empty body - with patch("requests.post") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.post") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = "aaa" with self.assertRaises(RateLimitException) as cm: @@ -731,8 +732,8 @@ def test_api_rate_limit_empty_response_body(self): auth = Auth(self.dummy_project_id, self.public_key_dict) # Test do_post empty body - with patch("requests.post") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.post") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = "" with self.assertRaises(RateLimitException) as cm: @@ -748,8 +749,8 @@ def test_api_rate_limit_none_response_body(self): auth = Auth(self.dummy_project_id, self.public_key_dict) # Test do_post empty body - with patch("requests.post") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.post") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 mock_request.return_value.json.return_value = None with self.assertRaises(RateLimitException) as cm: @@ -763,13 +764,13 @@ def test_api_rate_limit_none_response_body(self): def test_raise_from_response(self): auth = Auth(self.dummy_project_id, self.public_key_dict) - with patch("requests.get") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.get") as mock_request: + mock_request.return_value.is_success = False mock_request.return_value.status_code = 400 mock_request.return_value.error_type = ERROR_TYPE_SERVER_ERROR mock_request.return_value.text = """{"errorCode":"E062108","errorDescription":"User not found","errorMessage":"Cannot find user"}""" with self.assertRaises(AuthException) as cm: - auth.do_get(uri="http://test.com", params=False, allow_redirects=None) + auth.do_get(uri="http://test.com", params=False, follow_redirects=None) the_exception = cm.exception self.assertEqual(the_exception.status_code, 400) self.assertEqual(the_exception.error_type, ERROR_TYPE_SERVER_ERROR) diff --git a/tests/test_descope_client.py b/tests/test_descope_client.py index 16f06695b..27c02c4f9 100644 --- a/tests/test_descope_client.py +++ b/tests/test_descope_client.py @@ -22,6 +22,7 @@ ) from . import common +from tests.testutils import SSLMatcher class TestDescopeClient(common.DescopeTest): @@ -104,13 +105,13 @@ def test_logout(self): self.assertRaises(AuthException, client.logout, None) # Test failed flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.logout, dummy_refresh_token) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.logout(dummy_refresh_token)) def test_logout_all(self): @@ -120,13 +121,13 @@ def test_logout_all(self): self.assertRaises(AuthException, client.logout_all, None) # Test failed flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, client.logout_all, dummy_refresh_token) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(client.logout_all(dummy_refresh_token)) def test_me(self): @@ -136,14 +137,14 @@ def test_me(self): self.assertRaises(AuthException, client.me, None) # Test failed flow - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises(AuthException, client.me, dummy_refresh_token) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = json.loads( """{"name": "Testy McTester", "email": "testy@tester.com"}""" ) @@ -159,8 +160,8 @@ def test_me(self): "Authorization": f"Bearer {self.dummy_project_id}", "x-descope-project-id": self.dummy_project_id, }, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -176,16 +177,16 @@ def test_my_tenants(self): ) # Test failed flow - with patch("requests.post") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.post") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, client.my_tenants, dummy_refresh_token, True ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = json.loads( """{"tenants": [{"id": "tenant_id", "name": "tenant_name"}]}""" ) @@ -204,8 +205,8 @@ def test_my_tenants(self): "x-descope-project-id": self.dummy_project_id, }, json={"dct": False, "ids": ["a"]}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -217,14 +218,14 @@ def test_history(self): self.assertRaises(AuthException, client.history, None) # Test failed flow - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises(AuthException, client.history, dummy_refresh_token) # Test success flow - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = json.loads( """ [ @@ -257,8 +258,8 @@ def test_history(self): "Authorization": f"Bearer {self.dummy_project_id}", "x-descope-project-id": self.dummy_project_id, }, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -368,12 +369,12 @@ def test_validate_session_valid_tokens(self): # Test case where key id cannot be found client2 = DescopeClient(self.dummy_project_id, None) - with patch("requests.get") as mock_request: + with patch("httpx.get") as mock_request: fake_key = deepcopy(self.public_key_dict) # overwrite the kid (so it will not be found) fake_key["kid"] = "dummy_kid" mock_request.return_value.text = json.dumps([fake_key]) - mock_request.return_value.ok = True + mock_request.return_value.is_success = True self.assertRaises( AuthException, client2.validate_and_refresh_session, @@ -383,9 +384,9 @@ def test_validate_session_valid_tokens(self): # Test case where we failed to load key client3 = DescopeClient(self.dummy_project_id, None) - with patch("requests.get") as mock_request: + with patch("httpx.get") as mock_request: mock_request.return_value.text = """[{"kid": "dummy_kid"}]""" - mock_request.return_value.ok = True + mock_request.return_value.is_success = True self.assertRaises( AuthException, client3.validate_and_refresh_session, @@ -396,9 +397,9 @@ def test_validate_session_valid_tokens(self): # Test case where header_alg != key[alg] self.public_key_dict["alg"] = "ES521" client4 = DescopeClient(self.dummy_project_id, self.public_key_dict) - with patch("requests.get") as mock_request: + with patch("httpx.get") as mock_request: mock_request.return_value.text = """[{"kid": "dummy_kid"}]""" - mock_request.return_value.ok = True + mock_request.return_value.is_success = True self.assertRaises( AuthException, client4.validate_and_refresh_session, @@ -418,9 +419,9 @@ def test_validate_session_valid_tokens(self): # expired_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3VDOXl2MlVHdEdJMW84NGdDWkViOXFFUVciLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEUyIsImV4cCI6MTY1OTY0NDI5OCwiaWF0IjoxNjU5NjQ0Mjk3LCJpc3MiOiJQMkN1Qzl5djJVR3RHSTFvODRnQ1pFYjlxRVFXIiwic3ViIjoiVTJDdUNQdUpnUFdIR0I1UDRHbWZidVBHaEdWbSJ9.wBuOnIQI_z3SXOszqsWCg8ilOPdE5ruWYHA3jkaeQ3uX9hWgCTd69paFajc-xdMYbqlIF7JHji7T9oVmkCUJvDNgRZRZO9boMFANPyXitLOK4aX3VZpMJBpFxdrWV3GE" valid_refresh_token = valid_jwt_token - with patch("requests.get") as mock_request: + with patch("httpx.get") as mock_request: mock_request.return_value.cookies = {SESSION_COOKIE_NAME: expired_jwt_token} - mock_request.return_value.ok = True + mock_request.return_value.is_success = True self.assertRaises( AuthException, @@ -472,17 +473,17 @@ def test_expired_token(self): ) # Test fail flow - with patch("requests.get") as mock_request: - mock_request.return_value.ok = False + with patch("httpx.get") as mock_request: + mock_request.return_value.is_success = False self.assertRaises( AuthException, client.validate_session, expired_jwt_token, ) - with patch("requests.get") as mock_request: + with patch("httpx.get") as mock_request: mock_request.return_value.cookies = {"aaa": "aaa"} - mock_request.return_value.ok = True + mock_request.return_value.is_success = True self.assertRaises( AuthException, client.validate_session, @@ -505,9 +506,9 @@ def test_expired_token(self): new_session_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3VDOXl2MlVHdEdJMW84NGdDWkViOXFFUVciLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEUyIsImV4cCI6MjQ5MzA2MTQxNSwiaWF0IjoxNjU5NjQzMDYxLCJpc3MiOiJQMkN1Qzl5djJVR3RHSTFvODRnQ1pFYjlxRVFXIiwic3ViIjoiVTJDdUNQdUpnUFdIR0I1UDRHbWZidVBHaEdWbSJ9.gMalOv1GhqYVsfITcOc7Jv_fibX1Iof6AFy2KCVmyHmU2KwATT6XYXsHjBFFLq262Pg-LS1IX9f_DV3ppzvb1pSY4ccsP6WDGd1vJpjp3wFBP9Sji6WXL0SCCJUFIyJR" valid_refresh_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3VDOXl2MlVHdEdJMW84NGdDWkViOXFFUVciLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0NDMwNjEsImlhdCI6MTY1OTY0MzA2MSwiaXNzIjoiUDJDdUM5eXYyVUd0R0kxbzg0Z0NaRWI5cUVRVyIsInN1YiI6IlUyQ3VDUHVKZ1BXSEdCNVA0R21mYnVQR2hHVm0ifQ.mRo9FihYMR3qnQT06Mj3CJ5X0uTCEcXASZqfLLUv0cPCLBtBqYTbuK-ZRDnV4e4N6zGCNX2a3jjpbyqbViOxICCNSxJsVb-sdsSujtEXwVMsTTLnpWmNsMbOUiKmoME0" expired_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3VDOXl2MlVHdEdJMW84NGdDWkViOXFFUVciLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEUyIsImV4cCI6MTY1OTY0NDI5OCwiaWF0IjoxNjU5NjQ0Mjk3LCJpc3MiOiJQMkN1Qzl5djJVR3RHSTFvODRnQ1pFYjlxRVFXIiwic3ViIjoiVTJDdUNQdUpnUFdIR0I1UDRHbWZidVBHaEdWbSJ9.wBuOnIQI_z3SXOszqsWCg8ilOPdE5ruWYHA3jkaeQ3uX9hWgCTd69paFajc-xdMYbqlIF7JHji7T9oVmkCUJvDNgRZRZO9boMFANPyXitLOK4aX3VZpMJBpFxdrWV3GE" - with patch("requests.post") as mock_request: + with patch("httpx.post") as mock_request: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"sessionJwt": new_session_token} mock_request.return_value = my_mock_response mock_request.return_value.cookies = {} @@ -539,9 +540,9 @@ def test_expired_token(self): new_refreshed_token = ( expired_jwt_token # the refreshed token should be invalid (or expired) ) - with patch("requests.get") as mock_request: + with patch("httpx.get") as mock_request: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"sessionJwt": new_refreshed_token} mock_request.return_value = my_mock_response mock_request.return_value.cookies = {} @@ -774,9 +775,9 @@ def test_exchange_access_key(self): ) dummy_access_key = "dummy access key" valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3VDOXl2MlVHdEdJMW84NGdDWkViOXFFUVciLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0NDMwNjEsImlhdCI6MTY1OTY0MzA2MSwiaXNzIjoiUDJDdUM5eXYyVUd0R0kxbzg0Z0NaRWI5cUVRVyIsInN1YiI6IlUyQ3VDUHVKZ1BXSEdCNVA0R21mYnVQR2hHVm0ifQ.mRo9FihYMR3qnQT06Mj3CJ5X0uTCEcXASZqfLLUv0cPCLBtBqYTbuK-ZRDnV4e4N6zGCNX2a3jjpbyqbViOxICCNSxJsVb-sdsSujtEXwVMsTTLnpWmNsMbOUiKmoME0" - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = {"sessionJwt": valid_jwt_token} my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -796,8 +797,8 @@ def test_exchange_access_key(self): }, params=None, json={"loginOptions": {"customClaims": {"k1": "v1"}}}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -843,10 +844,10 @@ def test_select_tenant(self): valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3VDOXl2MlVHdEdJMW84NGdDWkViOXFFUVciLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0NDMwNjEsImlhdCI6MTY1OTY0MzA2MSwiaXNzIjoiUDJDdUM5eXYyVUd0R0kxbzg0Z0NaRWI5cUVRVyIsInN1YiI6IlUyQ3VDUHVKZ1BXSEdCNVA0R21mYnVQR2hHVm0ifQ.mRo9FihYMR3qnQT06Mj3CJ5X0uTCEcXASZqfLLUv0cPCLBtBqYTbuK-ZRDnV4e4N6zGCNX2a3jjpbyqbViOxICCNSxJsVb-sdsSujtEXwVMsTTLnpWmNsMbOUiKmoME0" - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -865,8 +866,8 @@ def test_select_tenant(self): json={ "tenant": "t1", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -881,9 +882,9 @@ def test_auth_management_key_with_functions(self): auth_management_key=auth_mgmt_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response @@ -902,8 +903,8 @@ def test_auth_management_key_with_functions(self): "email": "test@example.com", }, params=None, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -914,9 +915,9 @@ def test_auth_management_key_with_functions(self): ): client_env = DescopeClient(self.dummy_project_id, self.public_key_dict) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response @@ -934,8 +935,8 @@ def test_auth_management_key_with_functions(self): "user": {"email": "test@example.com"}, "email": "test@example.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -951,9 +952,9 @@ def test_auth_management_key_with_functions(self): auth_management_key=direct_auth_mgmt_key, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response @@ -972,8 +973,8 @@ def test_auth_management_key_with_functions(self): "email": "test@example.com", }, params=None, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -987,9 +988,9 @@ def test_auth_management_key_with_refresh_token(self): # Test with refresh token function refresh_token = "test_refresh_token" - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "n***@example.com"} mock_post.return_value = my_mock_response @@ -1010,17 +1011,17 @@ def test_auth_management_key_with_refresh_token(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test without auth_management_key for comparison client_no_auth = DescopeClient(self.dummy_project_id, self.public_key_dict) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "n***@example.com"} mock_post.return_value = my_mock_response @@ -1041,8 +1042,8 @@ def test_auth_management_key_with_refresh_token(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), params=None, timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/test_enchantedlink.py b/tests/test_enchantedlink.py index d34a81c36..97eef1445 100644 --- a/tests/test_enchantedlink.py +++ b/tests/test_enchantedlink.py @@ -14,6 +14,7 @@ SignUpOptions, ) +from tests.testutils import SSLMatcher from . import common @@ -97,9 +98,9 @@ def test_compose_body(self): def test_sign_in(self): enchantedlink = EnchantedLink(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True # Test failed flows with self.assertRaises(AuthException): enchantedlink.sign_in("", "http://test.me") @@ -120,15 +121,15 @@ def test_sign_in(self): "URI": "http://test.me", "loginOptions": {}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res["pendingRef"], "aaaa") self.assertEqual(res["linkId"], "24") # Validate refresh token used while provided - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: refresh_token = "dummy refresh token" enchantedlink.sign_in( "dummy@dummy.com", @@ -153,13 +154,13 @@ def test_sign_in(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # With template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: refresh_token = "dummy refresh token" enchantedlink.sign_in( "dummy@dummy.com", @@ -192,16 +193,16 @@ def test_sign_in(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_sign_in_with_login_options(self): enchantedlink = EnchantedLink(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = json.loads("""{"pendingRef": "aaaa", "linkId":"24"}""") my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -224,16 +225,16 @@ def test_sign_in_with_login_options(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_sign_up(self): enchantedlink = EnchantedLink(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True # Test failed flows self.assertRaises( @@ -266,14 +267,14 @@ def test_sign_up(self): "user": {"username": "user1", "email": "dummy@dummy.com"}, "email": "dummy@dummy.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res["pendingRef"], "aaaa") # Test user is None so using the login_id as default - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: data = json.loads("""{"pendingRef": "aaaa"}""") my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -296,14 +297,14 @@ def test_sign_up(self): "user": {"email": "dummy@dummy.com"}, "email": "dummy@dummy.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res["pendingRef"], "aaaa") # Test success flow with sign up options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: data = json.loads("""{"pendingRef": "aaaa"}""") my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -336,17 +337,17 @@ def test_sign_up(self): "revokeOtherSessions": True, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res["pendingRef"], "aaaa") def test_sign_up_or_in(self): enchantedlink = EnchantedLink(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = json.loads("""{"pendingRef": "aaaa"}""") my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -367,15 +368,15 @@ def test_sign_up_or_in(self): "URI": "http://test.me", "loginOptions": {}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with sign up options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = json.loads("""{"pendingRef": "aaaa"}""") my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -402,8 +403,8 @@ def test_sign_up_or_in(self): "templateOptions": {"bla": "blue"}, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -412,8 +413,8 @@ def test_verify(self): enchantedlink = EnchantedLink(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, enchantedlink.verify, @@ -422,9 +423,9 @@ def test_verify(self): # Test success flow valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL" - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {} mock_post.return_value = my_mock_response mock_post.return_value.cookies = { @@ -437,9 +438,9 @@ def test_get_session(self): enchantedlink = EnchantedLink(Auth(self.dummy_project_id, self.public_key_dict)) valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL" - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {} mock_post.return_value = my_mock_response mock_post.return_value.cookies = { @@ -450,9 +451,9 @@ def test_get_session(self): def test_update_user_email(self): enchantedlink = EnchantedLink(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True # Test failed flows self.assertRaises( AuthException, @@ -481,16 +482,16 @@ def test_update_user_email(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) # with template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True data = json.loads("""{"pendingRef": "aaaa"}""") my_mock_response.json.return_value = data mock_post.return_value = my_mock_response @@ -515,8 +516,8 @@ def test_update_user_email(self): "onMergeUseExisting": False, "templateOptions": {"bla": "blue"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) diff --git a/tests/test_future_utils.py b/tests/test_future_utils.py new file mode 100644 index 000000000..801384812 --- /dev/null +++ b/tests/test_future_utils.py @@ -0,0 +1,206 @@ +import asyncio +import unittest +from unittest.mock import Mock, patch + +from descope.future_utils import futu_apply, futu_await, futu_awaitable + + +class TestFutureUtils(unittest.TestCase): + def test_futu_apply_with_sync_result(self): + """Test futu_apply with synchronous result""" + result = "test_result" + modifier = lambda x: f"modified_{x}" + + result = futu_apply(result, modifier) + self.assertEqual(result, "modified_test_result") + + def test_futu_apply_with_coroutine(self): + """Test futu_apply with coroutine""" + + async def async_func(): + return "async_result" + + modifier = lambda x: f"modified_{x}" + + result = futu_apply(async_func(), modifier) + self.assertTrue(asyncio.iscoroutine(result)) + + # Test the actual result + async def run_test(): + actual_result = await result + self.assertEqual(actual_result, "modified_async_result") + + asyncio.run(run_test()) + + def test_futu_apply_with_future(self): + """Test futu_apply with future""" + + async def run_test(): + future = asyncio.Future() + future.set_result("future_result") + + modifier = lambda x: f"modified_{x}" + + result = futu_apply(future, modifier) + self.assertTrue(asyncio.iscoroutine(result)) + + # Test the actual result + actual_result = await result + self.assertEqual(actual_result, "modified_future_result") + + asyncio.run(run_test()) + + def test_futu_awaitable_with_false(self): + """Test futu_awaitable with as_awaitable=False""" + result = "test_result" + awaitable_result = futu_awaitable(result, False) + + self.assertEqual(awaitable_result, "test_result") + self.assertFalse(asyncio.iscoroutine(awaitable_result)) + + def test_futu_awaitable_with_true(self): + """Test futu_awaitable with as_awaitable=True""" + result = "test_result" + awaitable_result = futu_awaitable(result, True) + + self.assertTrue(asyncio.iscoroutine(awaitable_result)) + + # Test the actual result + async def run_test(): + actual_result = await awaitable_result + self.assertEqual(actual_result, "test_result") + + asyncio.run(run_test()) + + def test_futu_await_with_sync_object(self): + """Test futu_await with synchronous object""" + obj = "sync_object" + + async def run_test(): + result = await futu_await(obj) + self.assertEqual(result, "sync_object") + + asyncio.run(run_test()) + + def test_futu_await_with_coroutine(self): + """Test futu_await with coroutine""" + + async def async_func(): + return "coroutine_result" + + async def run_test(): + result = await futu_await(async_func()) + self.assertEqual(result, "coroutine_result") + + asyncio.run(run_test()) + + def test_futu_await_with_future(self): + """Test futu_await with future""" + + async def run_test(): + future = asyncio.Future() + future.set_result("future_result") + + result = await futu_await(future) + self.assertEqual(result, "future_result") + + asyncio.run(run_test()) + + def test_futu_apply_with_complex_modifier(self): + """Test futu_apply with complex modifier function""" + result = {"key": "value"} + modifier = lambda x: {**x, "modified": True} + + result = futu_apply(result, modifier) + expected = {"key": "value", "modified": True} + self.assertEqual(result, expected) + + def test_futu_apply_with_async_modifier(self): + """Test futu_apply with async modifier""" + + async def async_func(): + return "async_result" + + def modifier(x): + return f"modified_{x}" + + result = futu_apply(async_func(), modifier) + + async def run_test(): + actual_result = await result + self.assertEqual(actual_result, "modified_async_result") + + asyncio.run(run_test()) + + def test_futu_awaitable_with_none_result(self): + """Test futu_awaitable with None result""" + result = None + awaitable_result = futu_awaitable(result, True) + + self.assertTrue(asyncio.iscoroutine(awaitable_result)) + + async def run_test(): + actual_result = await awaitable_result + self.assertIsNone(actual_result) + + asyncio.run(run_test()) + + def test_futu_await_with_none_object(self): + """Test futu_await with None object""" + + async def run_test(): + result = await futu_await(None) + self.assertIsNone(result) + + asyncio.run(run_test()) + + def test_futu_apply_with_exception_in_modifier(self): + """Test futu_apply when modifier raises exception""" + result = "test_result" + + def modifier(x): + raise ValueError("Test exception") + + with self.assertRaises(ValueError): + futu_apply(result, modifier) + + def test_futu_apply_with_exception_in_async_modifier(self): + """Test futu_apply when async modifier raises exception""" + + async def async_func(): + return "async_result" + + def modifier(x): + raise ValueError("Test exception") + + result = futu_apply(async_func(), modifier) + + async def run_test(): + with self.assertRaises(ValueError): + await result + + asyncio.run(run_test()) + + def test_futu_await_with_exception_in_coroutine(self): + """Test futu_await when coroutine raises exception""" + + async def async_func(): + raise ValueError("Test exception") + + async def run_test(): + with self.assertRaises(ValueError): + await futu_await(async_func()) + + asyncio.run(run_test()) + + def test_futu_awaitable_with_false_and_none(self): + """Test futu_awaitable with as_awaitable=False and None result""" + result = None + awaitable_result = futu_awaitable(result, False) + + self.assertIsNone(awaitable_result) + self.assertFalse(asyncio.iscoroutine(awaitable_result)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_magiclink.py b/tests/test_magiclink.py index c851b48cd..1b1401632 100644 --- a/tests/test_magiclink.py +++ b/tests/test_magiclink.py @@ -14,6 +14,7 @@ SignUpOptions, ) +from tests.testutils import SSLMatcher from . import common @@ -123,8 +124,8 @@ def test_sign_in(self): "http://test.me", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, magiclink.sign_in, @@ -134,9 +135,9 @@ def test_sign_in(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response @@ -157,7 +158,7 @@ def test_sign_in(self): ) # Validate refresh token used while provided - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: refresh_token = "dummy refresh token" magiclink.sign_in( DeliveryMethod.EMAIL, @@ -183,13 +184,13 @@ def test_sign_in(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # With template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: refresh_token = "dummy refresh token" magiclink.sign_in( DeliveryMethod.EMAIL, @@ -218,8 +219,8 @@ def test_sign_in(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -243,8 +244,8 @@ def test_sign_up(self): signup_user_details, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, magiclink.sign_up, @@ -255,9 +256,9 @@ def test_sign_up(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response resp = magiclink.sign_up( @@ -269,9 +270,9 @@ def test_sign_up(self): self.assertEqual("t***@example.com", resp) # Test success flow with sign up options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response resp = magiclink.sign_up( @@ -305,8 +306,8 @@ def test_sign_up(self): "templateId": "foo", }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) @@ -319,9 +320,9 @@ def test_sign_up(self): "email": "dummy@dummy.com", } - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -351,16 +352,16 @@ def test_sign_up(self): }, "email": "dummy@dummy.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) # Test user is None so using the login_id as default - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -385,8 +386,8 @@ def test_sign_up(self): "user": {"email": "dummy@dummy.com"}, "email": "dummy@dummy.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) @@ -396,8 +397,8 @@ def test_sign_up_or_in(self): # Test failed flows - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, magiclink.sign_up_or_in, @@ -407,9 +408,9 @@ def test_sign_up_or_in(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -420,9 +421,9 @@ def test_sign_up_or_in(self): ) # Test success flow with sign up options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -452,8 +453,8 @@ def test_sign_up_or_in(self): "templateId": "foo", }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) @@ -463,8 +464,8 @@ def test_verify(self): magiclink = MagicLink(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, magiclink.verify, @@ -473,9 +474,9 @@ def test_verify(self): # Test success flow valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL" - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {} mock_post.return_value = my_mock_response mock_post.return_value.cookies = { @@ -492,13 +493,13 @@ def test_verify_with_get_keys_mock(self): # Test success flow valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL" - with patch("requests.get") as mock_get: + with patch("httpx.get") as mock_get: mock_get.return_value.text = json.dumps({"keys": [self.public_key_dict]}) - mock_get.return_value.ok = True + mock_get.return_value.is_success = True - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {} mock_post.return_value = my_mock_response mock_post.return_value.cookies = { @@ -518,8 +519,8 @@ def test_update_user_email(self): "refresh_token1", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, magiclink.update_user_email, @@ -529,9 +530,9 @@ def test_update_user_email(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -551,16 +552,16 @@ def test_update_user_email(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) # Test success flow with template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -586,8 +587,8 @@ def test_update_user_email(self): "onMergeUseExisting": False, "templateOptions": {"bla": "blue"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) @@ -604,8 +605,8 @@ def test_update_user_phone(self): "refresh_token1", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, magiclink.update_user_phone, @@ -616,9 +617,9 @@ def test_update_user_phone(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedPhone": "*****1111"} mock_post.return_value = my_mock_response self.assertEqual( @@ -640,16 +641,16 @@ def test_update_user_phone(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) # Test success flow with template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedPhone": "*****1111"} mock_post.return_value = my_mock_response self.assertEqual( @@ -676,8 +677,8 @@ def test_update_user_phone(self): "onMergeUseExisting": False, "templateOptions": {"bla": "blue"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) diff --git a/tests/test_oauth.py b/tests/test_oauth.py index eed6bc468..c9138d939 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -9,6 +9,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS, EndpointsV1, LoginOptions from . import common +from tests.testutils import SSLMatcher class TestOAuth(common.DescopeTest): @@ -48,13 +49,13 @@ def test_oauth_start(self): # Test failed flows self.assertRaises(AuthException, oauth.start, "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, oauth.start, "google") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(oauth.start("google")) self.assertRaises( @@ -65,8 +66,8 @@ def test_oauth_start(self): LoginOptions(mfa=True), ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True oauth.start("facebook") expected_uri = f"{common.DEFAULT_BASE_URL}{EndpointsV1.oauth_start_path}" mock_post.assert_called_with( @@ -78,8 +79,8 @@ def test_oauth_start(self): }, params={"provider": "facebook"}, json={}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -89,17 +90,17 @@ def test_oauth_start_with_login_options(self): # Test failed flows self.assertRaises(AuthException, oauth.start, "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, oauth.start, "google") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(oauth.start("google")) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True lo = LoginOptions(stepup=True, custom_claims={"k1": "v1"}) oauth.start("facebook", login_options=lo, refresh_token="refresh") expected_uri = f"{common.DEFAULT_BASE_URL}{EndpointsV1.oauth_start_path}" @@ -112,8 +113,8 @@ def test_oauth_start_with_login_options(self): }, params={"provider": "facebook"}, json={"stepup": True, "customClaims": {"k1": "v1"}, "mfa": False}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -127,14 +128,14 @@ def test_exchange_token(self): self.assertRaises(AuthException, oauth.exchange_token, "") self.assertRaises(AuthException, oauth.exchange_token, None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, oauth.exchange_token, "c1") # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -151,8 +152,8 @@ def test_exchange_token(self): }, params=None, json={"code": "c1"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/test_otp.py b/tests/test_otp.py index 4e2713b99..3a50c1789 100644 --- a/tests/test_otp.py +++ b/tests/test_otp.py @@ -12,6 +12,7 @@ SignUpOptions, ) +from tests.testutils import SSLMatcher from . import common @@ -182,8 +183,8 @@ def test_sign_up(self): invalid_signup_user_details, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.otp.sign_up, @@ -193,9 +194,9 @@ def test_sign_up(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -213,9 +214,9 @@ def test_sign_up(self): "email": "dummy@dummy.com", } - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -242,15 +243,15 @@ def test_sign_up(self): }, "email": "dummy@dummy.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with sign up options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -284,15 +285,15 @@ def test_sign_up(self): "templateId": "foo", }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test user is None so using the login_id as default - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -312,8 +313,8 @@ def test_sign_up(self): "user": {"email": "dummy@dummy.com"}, "email": "dummy@dummy.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -330,8 +331,8 @@ def test_sign_in(self): self.assertRaises(AuthException, client.otp.sign_in, DeliveryMethod.EMAIL, "") self.assertRaises(AuthException, client.otp.sign_in, DeliveryMethod.EMAIL, None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.otp.sign_in, @@ -340,9 +341,9 @@ def test_sign_in(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -358,7 +359,7 @@ def test_sign_in(self): ) # Validate refresh token used while provided - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: refresh_token = "dummy refresh token" client.otp.sign_in( DeliveryMethod.EMAIL, @@ -382,13 +383,13 @@ def test_sign_in(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # With template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: refresh_token = "dummy refresh token" client.otp.sign_in( DeliveryMethod.EMAIL, @@ -416,8 +417,8 @@ def test_sign_in(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -429,8 +430,8 @@ def test_sign_up_or_in(self): AuthException, client.otp.sign_up_or_in, DeliveryMethod.EMAIL, "" ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.otp.sign_up_or_in, @@ -439,9 +440,9 @@ def test_sign_up_or_in(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -450,9 +451,9 @@ def test_sign_up_or_in(self): ) # Test success flow with sign up options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -480,8 +481,8 @@ def test_sign_up_or_in(self): "templateId": "foo", }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) @@ -498,8 +499,8 @@ def test_verify_code(self): AuthException, client.otp.verify_code, DeliveryMethod.EMAIL, None, code ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.otp.verify_code, @@ -510,9 +511,9 @@ def test_verify_code(self): # Test success flow valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL" - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {} mock_post.return_value = my_mock_response mock_post.return_value.cookies = { @@ -543,8 +544,8 @@ def test_update_user_email(self): "refresh_token1", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.otp.update_user_email, @@ -554,9 +555,9 @@ def test_update_user_email(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -578,16 +579,16 @@ def test_update_user_email(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) # Test success flow with template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"} mock_post.return_value = my_mock_response self.assertEqual( @@ -613,8 +614,8 @@ def test_update_user_email(self): "onMergeUseExisting": False, "templateOptions": {"bla": "blue"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) @@ -648,8 +649,8 @@ def test_update_user_phone(self): "refresh_token1", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, client.otp.update_user_phone, @@ -660,9 +661,9 @@ def test_update_user_phone(self): ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedPhone": "*****111"} mock_post.return_value = my_mock_response self.assertEqual( @@ -684,15 +685,15 @@ def test_update_user_phone(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedPhone": "*****111"} mock_post.return_value = my_mock_response self.assertEqual( @@ -714,15 +715,15 @@ def test_update_user_phone(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedPhone": "*****111"} mock_post.return_value = my_mock_response self.assertEqual( @@ -744,16 +745,16 @@ def test_update_user_phone(self): "addToLoginIDs": False, "onMergeUseExisting": False, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) # Test success flow with template options - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = {"maskedPhone": "*****111"} mock_post.return_value = my_mock_response self.assertEqual( @@ -780,8 +781,8 @@ def test_update_user_phone(self): "onMergeUseExisting": False, "templateOptions": {"bla": "blue"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, params=None, ) diff --git a/tests/test_password.py b/tests/test_password.py index 51a97862b..d26600396 100644 --- a/tests/test_password.py +++ b/tests/test_password.py @@ -7,6 +7,7 @@ from descope.authmethod.password import Password # noqa: F401 from descope.common import DEFAULT_TIMEOUT_SECONDS, EndpointsV1 +from tests.testutils import SSLMatcher from . import common @@ -67,8 +68,8 @@ def test_sign_up(self): signup_user_details, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, password.sign_up, @@ -78,10 +79,10 @@ def test_sign_up(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -111,8 +112,8 @@ def test_sign_up(self): "email": "dummy@dummy.com", }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -148,8 +149,8 @@ def test_sign_in(self): None, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, password.sign_in, @@ -158,10 +159,10 @@ def test_sign_in(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -183,8 +184,8 @@ def test_sign_in(self): "loginId": "dummy@dummy.com", "password": "123456", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -204,8 +205,8 @@ def test_send_reset(self): None, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, password.send_reset, @@ -213,10 +214,10 @@ def test_send_reset(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"resetMethod": "magiclink", "maskedEmail": "du***@***my.com"}""" @@ -240,16 +241,16 @@ def test_send_reset(self): "loginId": "dummy@dummy.com", "redirectUrl": "https://redirect.here.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) # Test success flow with template options - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"resetMethod": "magiclink", "maskedEmail": "du***@***my.com"}""" @@ -278,8 +279,8 @@ def test_send_reset(self): "redirectUrl": "https://redirect.here.com", "templateOptions": {"bla": "blue"}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -335,8 +336,8 @@ def test_update(self): None, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, password.update, @@ -346,8 +347,8 @@ def test_update(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True valid_jwt_token = "eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkVGVuYW50cyI6eyIiOm51bGx9LCJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwNjc5MjA4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MjA5MDA4NzIwOCwiaWF0IjoxNjU4MDg3MjA4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQzU1dnl4dzBzUkw2RmRNNjhxUnNDRGRST1YifQ.cWP5up4R5xeIl2qoG2NtfLH3Q5nRJVKdz-FDoAXctOQW9g3ceZQi6rZQ-TPBaXMKw68bijN3bLJTqxWW5WHzqRUeopfuzTcMYmC0wP2XGJkrdF6A8D5QW6acSGqglFgu" self.assertIsNone( password.update("dummy@dummy.com", "123456", valid_jwt_token) @@ -364,8 +365,8 @@ def test_update(self): "loginId": "dummy@dummy.com", "newPassword": "123456", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -421,8 +422,8 @@ def test_replace(self): None, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, password.replace, @@ -432,10 +433,10 @@ def test_replace(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["test@company.com"], "name": "", "email": "test@company.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -460,26 +461,26 @@ def test_replace(self): "oldPassword": "123456", "newPassword": "1234567", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) def test_policy(self): password = Password(Auth(self.dummy_project_id, self.public_key_dict)) - with patch("requests.get") as mock_get: - mock_get.return_value.ok = False + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = False self.assertRaises( AuthException, password.get_policy, ) # Test success flow - with patch("requests.get") as mock_get: - mock_get.return_value.ok = True + with patch("httpx.get") as mock_get: + mock_get.return_value.is_success = True my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads("""{"minLength": 8, "lowercase": true}""") my_mock_response.json.return_value = data @@ -493,7 +494,7 @@ def test_policy(self): "x-descope-project-id": self.dummy_project_id, }, params=None, - allow_redirects=None, - verify=True, + follow_redirects=None, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/test_saml.py b/tests/test_saml.py index 804fb9a05..377d67484 100644 --- a/tests/test_saml.py +++ b/tests/test_saml.py @@ -9,6 +9,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS, EndpointsV1, LoginOptions from . import common +from tests.testutils import SSLMatcher class TestSAML(common.DescopeTest): @@ -40,17 +41,17 @@ def test_saml_start(self): self.assertRaises(AuthException, saml.start, "tenant1", "") self.assertRaises(AuthException, saml.start, "tenant1", None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, saml.start, "tenant1", "http://dummy.com") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(saml.start("tenant1", "http://dummy.com")) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True saml.start("tenant1", "http://dummy.com") expected_uri = ( f"{common.DEFAULT_BASE_URL}{EndpointsV1.auth_saml_start_path}" @@ -64,8 +65,8 @@ def test_saml_start(self): }, params={"tenant": "tenant1", "redirectURL": "http://dummy.com"}, json={}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertRaises( @@ -85,17 +86,17 @@ def test_saml_start_with_login_options(self): self.assertRaises(AuthException, saml.start, "tenant1", "") self.assertRaises(AuthException, saml.start, "tenant1", None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, saml.start, "tenant1", "http://dummy.com") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(saml.start("tenant1", "http://dummy.com")) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True lo = LoginOptions(stepup=True, custom_claims={"k1": "v1"}) saml.start("tenant1", "http://dummy.com", lo, "refresh") expected_uri = ( @@ -110,8 +111,8 @@ def test_saml_start_with_login_options(self): }, params={"tenant": "tenant1", "redirectURL": "http://dummy.com"}, json={"stepup": True, "customClaims": {"k1": "v1"}, "mfa": False}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -125,14 +126,14 @@ def test_exchange_token(self): self.assertRaises(AuthException, saml.exchange_token, "") self.assertRaises(AuthException, saml.exchange_token, None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, saml.exchange_token, "c1") # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -149,8 +150,8 @@ def test_exchange_token(self): }, params=None, json={"code": "c1"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/test_sso.py b/tests/test_sso.py index eb555fc3f..98a6cb46c 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -9,6 +9,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS, EndpointsV1, LoginOptions from . import common +from tests.testutils import SSLMatcher class TestSSO(common.DescopeTest): @@ -48,17 +49,17 @@ def test_sso_start(self): self.assertRaises(AuthException, sso.start, "", "http://dummy.com") self.assertRaises(AuthException, sso.start, None, "http://dummy.com") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, sso.start, "tenant1", "http://dummy.com") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(sso.start("tenant1", "http://dummy.com")) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True sso.start("tenant1", "http://dummy.com", sso_id="some-sso-id") expected_uri = f"{common.DEFAULT_BASE_URL}{EndpointsV1.auth_sso_start_path}" mock_post.assert_called_with( @@ -74,8 +75,8 @@ def test_sso_start(self): "ssoId": "some-sso-id", }, json={}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertRaises( @@ -93,17 +94,17 @@ def test_sso_start_with_login_options(self): self.assertRaises(AuthException, sso.start, "", "http://dummy.com") self.assertRaises(AuthException, sso.start, None, "http://dummy.com") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, sso.start, "tenant1", "http://dummy.com") # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(sso.start("tenant1", "http://dummy.com")) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True lo = LoginOptions(stepup=True, custom_claims={"k1": "v1"}) sso.start("tenant1", "http://dummy.com", lo, "refresh") expected_uri = f"{common.DEFAULT_BASE_URL}{EndpointsV1.auth_sso_start_path}" @@ -116,8 +117,8 @@ def test_sso_start_with_login_options(self): }, params={"tenant": "tenant1", "redirectURL": "http://dummy.com"}, json={"stepup": True, "customClaims": {"k1": "v1"}, "mfa": False}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -131,14 +132,14 @@ def test_exchange_token(self): self.assertRaises(AuthException, sso.exchange_token, "") self.assertRaises(AuthException, sso.exchange_token, None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises(AuthException, sso.exchange_token, "c1") # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -155,8 +156,8 @@ def test_exchange_token(self): }, params=None, json={"code": "c1"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) diff --git a/tests/test_totp.py b/tests/test_totp.py index 19f372965..b7771f653 100644 --- a/tests/test_totp.py +++ b/tests/test_totp.py @@ -7,6 +7,7 @@ from descope.authmethod.totp import TOTP # noqa: F401 from descope.common import DEFAULT_TIMEOUT_SECONDS, EndpointsV1, LoginOptions +from tests.testutils import SSLMatcher from . import common @@ -49,8 +50,8 @@ def test_sign_up(self): signup_user_details, ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, totp.sign_up, @@ -59,8 +60,8 @@ def test_sign_up(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(totp.sign_up("dummy@dummy.com", signup_user_details)) def test_sign_in(self): @@ -72,16 +73,16 @@ def test_sign_in(self): self.assertRaises(AuthException, totp.sign_in_code, "dummy@dummy.com", None) self.assertRaises(AuthException, totp.sign_in_code, "dummy@dummy.com", "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, totp.sign_in_code, "dummy@dummy.com", "1234" ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -98,9 +99,9 @@ def test_sign_in(self): ) # Validate refresh token used while provided - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"jwts": ["eyJhbGciOiJFUzM4NCIsImtpZCI6IjJCdDVXTGNjTFVleTFEcDd1dHB0WmIzRng5SyIsInR5cCI6IkpXVCJ9.eyJjb29raWVEb21haW4iOiIiLCJjb29raWVFeHBpcmF0aW9uIjoxNjYwMzg4MDc4LCJjb29raWVNYXhBZ2UiOjI1OTE5OTksImNvb2tpZU5hbWUiOiJEU1IiLCJjb29raWVQYXRoIjoiLyIsImV4cCI6MTY2MDIxNTI3OCwiaWF0IjoxNjU3Nzk2MDc4LCJpc3MiOiIyQnQ1V0xjY0xVZXkxRHA3dXRwdFpiM0Z4OUsiLCJzdWIiOiIyQnRFSGtnT3UwMmxtTXh6UElleGRNdFV3MU0ifQ.oAnvJ7MJvCyL_33oM7YCF12JlQ0m6HWRuteUVAdaswfnD4rHEBmPeuVHGljN6UvOP4_Cf0559o39UHVgm3Fwb-q7zlBbsu_nP1-PRl-F8NJjvBgC5RsAYabtJq7LlQmh"], "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -131,8 +132,8 @@ def test_sign_in(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) @@ -145,8 +146,8 @@ def test_update_user(self): self.assertRaises(AuthException, totp.update_user, "dummy@dummy.com", None) self.assertRaises(AuthException, totp.update_user, "dummy@dummy.com", "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, totp.update_user, @@ -159,7 +160,7 @@ def test_update_user(self): """{ "provisioningURL": "http://dummy.com", "image": "imagedata", "key": "k01", "error": "" }""" ) my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = valid_response mock_post.return_value = my_mock_response res = totp.update_user("dummy@dummy.com", valid_jwt_token) @@ -173,8 +174,8 @@ def test_update_user(self): }, params=None, json={"loginId": "dummy@dummy.com"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res, valid_response) diff --git a/tests/test_webauthn.py b/tests/test_webauthn.py index 3a3a0bf02..79671d6ac 100644 --- a/tests/test_webauthn.py +++ b/tests/test_webauthn.py @@ -9,6 +9,7 @@ from descope.common import DEFAULT_TIMEOUT_SECONDS, EndpointsV1, LoginOptions from . import common +from tests.testutils import SSLMatcher class TestWebauthN(common.DescopeTest): @@ -88,8 +89,8 @@ def test_sign_up_start(self): ) self.assertRaises(AuthException, webauthn.sign_up_start, "id1", "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.sign_up_start, "id1", "https://example.com" ) @@ -98,13 +99,13 @@ def test_sign_up_start(self): valid_response = json.loads( """{"transactionId": "2COHI3LIixYhf6Q7EECYt20zyMi", "options": "{'publicKey':{'challenge':'5GOywA7BHL1QceQOfxHKDrasuN8SkbbgXmB5ImVZ+QU=','rp':{'name':'comp6','id':'localhost'},'user':{'name”:”dummy@dummy.com','displayName”:”dummy”,”id':'VTJDT0hJNWlWOHJaZ3VURkpKMzV3bjEydHRkTw=='},'pubKeyCredParams':[{'type':'public-key','alg':-7},{'type':'public-key','alg':-35},{'type':'public-key','alg':-36},{'type':'public-key','alg':-257},{'type':'public-key','alg':-258},{'type':'public-key','alg':-259},{'type':'public-key','alg':-37},{'type':'public-key','alg':-38},{'type':'public-key','alg':-39},{'type':'public-key','alg':-8}],'authenticatorSelection':{'userVerification':'preferred'},'timeout':60000,'attestation':'none'}}"}""" ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone(webauthn.sign_up_start("id1", "https://example.com")) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = valid_response mock_post.return_value = my_mock_response res = webauthn.sign_up_start("id1", "https://example.com") @@ -119,8 +120,8 @@ def test_sign_up_start(self): }, params=None, json={"user": {"loginId": "id1"}, "origin": "https://example.com"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res, valid_response) @@ -134,16 +135,16 @@ def test_sign_up_finish(self): self.assertRaises(AuthException, webauthn.sign_up_finish, "t01", "") self.assertRaises(AuthException, webauthn.sign_up_finish, "t01", None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.sign_up_finish, "t01", "response01" ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"refreshJwt": "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL", @@ -169,8 +170,8 @@ def test_sign_up_finish(self): }, params=None, json={"transactionId": "t01", "response": "response01"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertIsNotNone(webauthn.sign_up_finish("t01", "response01")) @@ -184,8 +185,8 @@ def test_sign_in_start(self): ) self.assertRaises(AuthException, webauthn.sign_in_start, "id", "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.sign_in_start, @@ -197,8 +198,8 @@ def test_sign_in_start(self): valid_response = json.loads( """{"transactionId": "2COHI3LIixYhf6Q7EECYt20zyMi", "options": "{'publicKey':{'challenge':'5GOywA7BHL1QceQOfxHKDrasuN8SkbbgXmB5ImVZ+QU=','rp':{'name':'comp6','id':'localhost'},'user':{'name”:”dummy@dummy.com','displayName”:”dummy”,”id':'VTJDT0hJNWlWOHJaZ3VURkpKMzV3bjEydHRkTw=='},'pubKeyCredParams':[{'type':'public-key','alg':-7},{'type':'public-key','alg':-35},{'type':'public-key','alg':-36},{'type':'public-key','alg':-257},{'type':'public-key','alg':-258},{'type':'public-key','alg':-259},{'type':'public-key','alg':-37},{'type':'public-key','alg':-38},{'type':'public-key','alg':-39},{'type':'public-key','alg':-8}],'authenticatorSelection':{'userVerification':'preferred'},'timeout':60000,'attestation':'none'}}"}""" ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( webauthn.sign_in_start("dummy@dummy.com", "https://example.com") ) @@ -210,9 +211,9 @@ def test_sign_in_start(self): LoginOptions(mfa=True), ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = valid_response mock_post.return_value = my_mock_response res = webauthn.sign_in_start("id1", "https://example.com") @@ -230,8 +231,8 @@ def test_sign_in_start(self): "origin": "https://example.com", "loginOptions": {}, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res, valid_response) @@ -245,8 +246,8 @@ def test_sign_in_start_with_login_options(self): ) self.assertRaises(AuthException, webauthn.sign_in_start, "id", "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.sign_in_start, @@ -258,15 +259,15 @@ def test_sign_in_start_with_login_options(self): valid_response = json.loads( """{"transactionId": "2COHI3LIixYhf6Q7EECYt20zyMi", "options": "{'publicKey':{'challenge':'5GOywA7BHL1QceQOfxHKDrasuN8SkbbgXmB5ImVZ+QU=','rp':{'name':'comp6','id':'localhost'},'user':{'name”:”dummy@dummy.com','displayName”:”dummy”,”id':'VTJDT0hJNWlWOHJaZ3VURkpKMzV3bjEydHRkTw=='},'pubKeyCredParams':[{'type':'public-key','alg':-7},{'type':'public-key','alg':-35},{'type':'public-key','alg':-36},{'type':'public-key','alg':-257},{'type':'public-key','alg':-258},{'type':'public-key','alg':-259},{'type':'public-key','alg':-37},{'type':'public-key','alg':-38},{'type':'public-key','alg':-39},{'type':'public-key','alg':-8}],'authenticatorSelection':{'userVerification':'preferred'},'timeout':60000,'attestation':'none'}}"}""" ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( webauthn.sign_in_start("dummy@dummy.com", "https://example.com") ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = valid_response mock_post.return_value = my_mock_response lo = LoginOptions(stepup=True, custom_claims={"k1": "v1"}) @@ -289,8 +290,8 @@ def test_sign_in_start_with_login_options(self): "mfa": False, }, }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res, valid_response) @@ -304,16 +305,16 @@ def test_sign_in_finish(self): self.assertRaises(AuthException, webauthn.sign_in_finish, "t01", "") self.assertRaises(AuthException, webauthn.sign_in_finish, "t01", None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.sign_in_finish, "t01", "response01" ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( @@ -333,8 +334,8 @@ def test_sign_in_finish(self): }, params=None, json={"transactionId": "t01", "response": "response01"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertIsNotNone(webauthn.sign_up_finish("t01", "response01")) @@ -348,8 +349,8 @@ def test_sign_up_or_in_start(self): ) self.assertRaises(AuthException, webauthn.sign_up_or_in_start, "id", "") - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.sign_up_or_in_start, @@ -361,15 +362,15 @@ def test_sign_up_or_in_start(self): valid_response = json.loads( """{"create": true, "transactionId": "2COHI3LIixYhf6Q7EECYt20zyMi", "options": "{'publicKey':{'challenge':'5GOywA7BHL1QceQOfxHKDrasuN8SkbbgXmB5ImVZ+QU=','rp':{'name':'comp6','id':'localhost'},'user':{'name”:”dummy@dummy.com','displayName”:”dummy”,”id':'VTJDT0hJNWlWOHJaZ3VURkpKMzV3bjEydHRkTw=='},'pubKeyCredParams':[{'type':'public-key','alg':-7},{'type':'public-key','alg':-35},{'type':'public-key','alg':-36},{'type':'public-key','alg':-257},{'type':'public-key','alg':-258},{'type':'public-key','alg':-259},{'type':'public-key','alg':-37},{'type':'public-key','alg':-38},{'type':'public-key','alg':-39},{'type':'public-key','alg':-8}],'authenticatorSelection':{'userVerification':'preferred'},'timeout':60000,'attestation':'none'}}"}""" ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( webauthn.sign_up_or_in_start("dummy@dummy.com", "https://example.com") ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = valid_response mock_post.return_value = my_mock_response res = webauthn.sign_up_or_in_start("id1", "https://example.com") @@ -386,8 +387,8 @@ def test_sign_up_or_in_start(self): "loginId": "id1", "origin": "https://example.com", }, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res, valid_response) @@ -418,8 +419,8 @@ def test_update_start(self): "https://example.com", ) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.update_start, @@ -429,18 +430,18 @@ def test_update_start(self): ) # Test success flow - with patch("requests.post") as mock_post: - mock_post.return_value.ok = True + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = True self.assertIsNotNone( webauthn.update_start( "dummy@dummy.com", valid_jwt_token, "https://example.com" ) ) - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: valid_response = json.loads("{}") my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.json.return_value = valid_response mock_post.return_value = my_mock_response res = webauthn.update_start( @@ -456,8 +457,8 @@ def test_update_start(self): }, params=None, json={"loginId": "dummy@dummy.com", "origin": "https://example.com"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertEqual(res, valid_response) @@ -471,16 +472,16 @@ def test_update_finish(self): self.assertRaises(AuthException, webauthn.update_finish, "t01", "") self.assertRaises(AuthException, webauthn.update_finish, "t01", None) - with patch("requests.post") as mock_post: - mock_post.return_value.ok = False + with patch("httpx.post") as mock_post: + mock_post.return_value.is_success = False self.assertRaises( AuthException, webauthn.update_finish, "t01", "response01" ) # Test success flow - with patch("requests.post") as mock_post: + with patch("httpx.post") as mock_post: my_mock_response = mock.Mock() - my_mock_response.ok = True + my_mock_response.is_success = True my_mock_response.cookies = {} data = json.loads( """{"refreshJwt": "eyJhbGciOiJFUzM4NCIsImtpZCI6IlAyQ3R6VWhkcXBJRjJ5czlnZzdtczA2VXZ0QzQiLCJ0eXAiOiJKV1QifQ.eyJkcm4iOiJEU1IiLCJleHAiOjIyNjQ0Mzc1OTYsImlhdCI6MTY1OTYzNzU5NiwiaXNzIjoiUDJDdHpVaGRxcElGMnlzOWdnN21zMDZVdnRDNCIsInN1YiI6IlUyQ3UwajBXUHczWU9pUElTSmI1Mkwwd1VWTWcifQ.WLnlHugvzZtrV9OzBB7SjpCLNRvKF3ImFpVyIN5orkrjO2iyAKg_Rb4XHk9sXGC1aW8puYzLbhE1Jv3kk2hDcKggfE8OaRNRm8byhGFZHnvPJwcP_Ya-aRmfAvCLcKOL", "user": {"loginIds": ["guyp@descope.com"], "name": "", "email": "guyp@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false}, "firstSeen": false}""" @@ -498,8 +499,8 @@ def test_update_finish(self): }, params=None, json={"transactionId": "t01", "response": "response01"}, - allow_redirects=False, - verify=True, + follow_redirects=False, + verify=SSLMatcher(), timeout=DEFAULT_TIMEOUT_SECONDS, ) self.assertIsNotNone(webauthn.sign_up_finish("t01", "response01")) diff --git a/tests/testutils.py b/tests/testutils.py new file mode 100644 index 000000000..ecdd3d382 --- /dev/null +++ b/tests/testutils.py @@ -0,0 +1,6 @@ +from ssl import SSLContext + + +class SSLMatcher: + def __eq__(self, other): + return isinstance(other, SSLContext)