diff --git a/tests/test_sync_http_client.py b/tests/test_sync_http_client.py index 2a0f571b..d86c5b99 100644 --- a/tests/test_sync_http_client.py +++ b/tests/test_sync_http_client.py @@ -10,6 +10,7 @@ BadRequestException, BaseRequestException, ConflictException, + EmailVerificationRequiredException, ServerException, ) from workos.utils.http_client import SyncHTTPClient @@ -263,6 +264,57 @@ def test_conflict_exception(self): assert str(ex) == "(message=No message, request_id=request-123)" assert ex.__class__ == ConflictException + def test_email_verification_required_exception(self): + request_id = "request-123" + email_verification_id = "email_verification_01J6K4PMSWQXVFGF5ZQJXC6VC8" + + self.http_client._client.request = MagicMock( + return_value=httpx.Response( + status_code=403, + json={ + "message": "Please verify your email to authenticate via password.", + "code": "email_verification_required", + "email_verification_id": email_verification_id, + }, + headers={"X-Request-ID": request_id}, + ), + ) + + try: + self.http_client.request("bad_place") + except EmailVerificationRequiredException as ex: + assert ( + ex.message == "Please verify your email to authenticate via password." + ) + assert ex.code == "email_verification_required" + assert ex.email_verification_id == email_verification_id + assert ex.request_id == request_id + assert ex.__class__ == EmailVerificationRequiredException + assert isinstance(ex, AuthorizationException) + + def test_regular_authorization_exception_still_raised(self): + request_id = "request-123" + + self.http_client._client.request = MagicMock( + return_value=httpx.Response( + status_code=403, + json={ + "message": "You do not have permission to access this resource.", + "code": "forbidden", + }, + headers={"X-Request-ID": request_id}, + ), + ) + + try: + self.http_client.request("bad_place") + except AuthorizationException as ex: + assert ex.message == "You do not have permission to access this resource." + assert ex.code == "forbidden" + assert ex.request_id == request_id + assert ex.__class__ == AuthorizationException + assert not isinstance(ex, EmailVerificationRequiredException) + def test_request_includes_base_headers(self, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) diff --git a/workos/exceptions.py b/workos/exceptions.py index 9aee53b2..a79e1159 100644 --- a/workos/exceptions.py +++ b/workos/exceptions.py @@ -45,6 +45,24 @@ class AuthorizationException(BaseRequestException): pass +class EmailVerificationRequiredException(AuthorizationException): + """Raised when email verification is required before authentication. + + This exception includes an email_verification_id field that can be used + to retrieve the email verification object or resend the verification email. + """ + + def __init__( + self, + response: httpx.Response, + response_json: Optional[Mapping[str, Any]], + ) -> None: + super().__init__(response, response_json) + self.email_verification_id = self.extract_from_json( + "email_verification_id", None + ) + + class AuthenticationException(BaseRequestException): pass diff --git a/workos/utils/_base_http_client.py b/workos/utils/_base_http_client.py index a9ab0c55..49dcbcf5 100644 --- a/workos/utils/_base_http_client.py +++ b/workos/utils/_base_http_client.py @@ -20,6 +20,7 @@ ServerException, AuthenticationException, AuthorizationException, + EmailVerificationRequiredException, NotFoundException, BadRequestException, ) @@ -99,6 +100,11 @@ def _maybe_raise_error_by_status_code( if status_code == 401: raise AuthenticationException(response, response_json) elif status_code == 403: + if ( + response_json is not None + and response_json.get("code") == "email_verification_required" + ): + raise EmailVerificationRequiredException(response, response_json) raise AuthorizationException(response, response_json) elif status_code == 404: raise NotFoundException(response, response_json)