diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index 2dfa23e08..08b930ae6 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -198,10 +198,19 @@ class UidAlreadyExistsError(exceptions.AlreadyExistsError): default_message = 'The user with the provided uid already exists' - def __init__(self, message, cause, http_response=None): + def __init__(self, message, cause, http_response): exceptions.AlreadyExistsError.__init__(self, message, cause, http_response) +class InvalidDynamicLinkDomainError(exceptions.InvalidArgumentError): + """Dynamic link domain in ActionCodeSettings is not authorized.""" + + default_message = 'Dynamic link domain specified in ActionCodeSettings is not authorized' + + def __init__(self, message, cause, http_response): + exceptions.InvalidArgumentError.__init__(self, message, cause, http_response) + + class InvalidIdTokenError(exceptions.InvalidArgumentError): """The provided ID token is not a valid Firebase ID token.""" @@ -229,6 +238,7 @@ def __init__(self, message, cause=None, http_response=None): _CODE_TO_EXC_TYPE = { 'DUPLICATE_LOCAL_ID': UidAlreadyExistsError, + 'INVALID_DYNAMIC_LINK_DOMAIN': InvalidDynamicLinkDomainError, 'INVALID_ID_TOKEN': InvalidIdTokenError, 'USER_NOT_FOUND': UserNotFoundError, } diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index 61090da9b..3910f9690 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -26,7 +26,6 @@ USER_IMPORT_ERROR = 'USER_IMPORT_ERROR' USER_DOWNLOAD_ERROR = 'LIST_USERS_ERROR' -GENERATE_EMAIL_ACTION_LINK_ERROR = 'GENERATE_EMAIL_ACTION_LINK_ERROR' MAX_LIST_USERS_RESULTS = 1000 MAX_IMPORT_USERS_SIZE = 1000 @@ -654,14 +653,15 @@ def generate_email_action_link(self, action_type, email, action_code_settings=No payload.update(encode_action_code_settings(action_code_settings)) try: - response = self._client.body('post', '/accounts:sendOobCode', json=payload) + body, http_resp = self._client.body_and_response( + 'post', '/accounts:sendOobCode', json=payload) except requests.exceptions.RequestException as error: - self._handle_http_error(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.', - error) + raise _auth_utils.handle_auth_backend_error(error) else: - if not response or not response.get('oobLink'): - raise ApiCallError(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.') - return response.get('oobLink') + if not body or not body.get('oobLink'): + raise _auth_utils.UnexpectedResponseError( + 'Failed to generate email action link.', http_response=http_resp) + return body.get('oobLink') def _handle_http_error(self, code, msg, error): if error.response is not None: diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 19eaa54f5..61d71ad9f 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -42,6 +42,8 @@ 'ErrorInfo', 'ExportedUserRecord', 'ImportUserRecord', + 'InvalidDynamicLinkDomainError', + 'InvalidIdTokenError', 'ListUsersPage', 'TokenSignError', 'UidAlreadyExistsError', @@ -80,6 +82,7 @@ ListUsersPage = _user_mgt.ListUsersPage UserImportHash = _user_import.UserImportHash ImportUserRecord = _user_import.ImportUserRecord +InvalidDynamicLinkDomainError = _auth_utils.InvalidDynamicLinkDomainError InvalidIdTokenError = _auth_utils.InvalidIdTokenError TokenSignError = _token_gen.TokenSignError UidAlreadyExistsError = _auth_utils.UidAlreadyExistsError @@ -465,14 +468,11 @@ def generate_password_reset_link(email, action_code_settings=None, app=None): Raises: ValueError: If the provided arguments are invalid - AuthError: If an error occurs while generating the link + FirebaseError: If an error occurs while generating the link """ user_manager = _get_auth_service(app).user_manager - try: - return user_manager.generate_email_action_link('PASSWORD_RESET', email, - action_code_settings=action_code_settings) - except _user_mgt.ApiCallError as error: - raise AuthError(error.code, str(error), error.detail) + return user_manager.generate_email_action_link( + 'PASSWORD_RESET', email, action_code_settings=action_code_settings) def generate_email_verification_link(email, action_code_settings=None, app=None): @@ -490,14 +490,11 @@ def generate_email_verification_link(email, action_code_settings=None, app=None) Raises: ValueError: If the provided arguments are invalid - AuthError: If an error occurs while generating the link + FirebaseError: If an error occurs while generating the link """ user_manager = _get_auth_service(app).user_manager - try: - return user_manager.generate_email_action_link('VERIFY_EMAIL', email, - action_code_settings=action_code_settings) - except _user_mgt.ApiCallError as error: - raise AuthError(error.code, str(error), error.detail) + return user_manager.generate_email_action_link( + 'VERIFY_EMAIL', email, action_code_settings=action_code_settings) def generate_sign_in_with_email_link(email, action_code_settings, app=None): @@ -515,14 +512,11 @@ def generate_sign_in_with_email_link(email, action_code_settings, app=None): Raises: ValueError: If the provided arguments are invalid - AuthError: If an error occurs while generating the link + FirebaseError: If an error occurs while generating the link """ user_manager = _get_auth_service(app).user_manager - try: - return user_manager.generate_email_action_link('EMAIL_SIGNIN', email, - action_code_settings=action_code_settings) - except _user_mgt.ApiCallError as error: - raise AuthError(error.code, str(error), error.detail) + return user_manager.generate_email_action_link( + 'EMAIL_SIGNIN', email, action_code_settings=action_code_settings) def _check_jwt_revoked(verified_claims, error_code, label, app): diff --git a/tests/test_user_mgt.py b/tests/test_user_mgt.py index f2ee4b58c..594de2d4c 100644 --- a/tests/test_user_mgt.py +++ b/tests/test_user_mgt.py @@ -1200,9 +1200,29 @@ def test_password_reset_with_settings(self, user_mgt_app): auth.generate_password_reset_link, ]) def test_api_call_failure(self, user_mgt_app, func): - _instrument_user_manager(user_mgt_app, 500, '{"error":"dummy error"}') - with pytest.raises(auth.AuthError): + _instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "UNEXPECTED_CODE"}}') + with pytest.raises(exceptions.InternalError) as excinfo: + func('test@test.com', MOCK_ACTION_CODE_SETTINGS, app=user_mgt_app) + assert str(excinfo.value) == 'Error while calling Auth service (UNEXPECTED_CODE).' + assert excinfo.value.http_response is not None + assert excinfo.value.cause is not None + + @pytest.mark.parametrize('func', [ + auth.generate_sign_in_with_email_link, + auth.generate_email_verification_link, + auth.generate_password_reset_link, + ]) + def test_invalid_dynamic_link(self, user_mgt_app, func): + resp = '{"error":{"message": "INVALID_DYNAMIC_LINK_DOMAIN: Because of this reason."}}' + _instrument_user_manager(user_mgt_app, 500, resp) + with pytest.raises(auth.InvalidDynamicLinkDomainError) as excinfo: func('test@test.com', MOCK_ACTION_CODE_SETTINGS, app=user_mgt_app) + assert isinstance(excinfo.value, exceptions.InvalidArgumentError) + assert str(excinfo.value) == ('Dynamic link domain specified in ActionCodeSettings is ' + 'not authorized (INVALID_DYNAMIC_LINK_DOMAIN). Because ' + 'of this reason.') + assert excinfo.value.http_response is not None + assert excinfo.value.cause is not None @pytest.mark.parametrize('func', [ auth.generate_sign_in_with_email_link, @@ -1211,8 +1231,12 @@ def test_api_call_failure(self, user_mgt_app, func): ]) def test_api_call_no_link(self, user_mgt_app, func): _instrument_user_manager(user_mgt_app, 200, '{}') - with pytest.raises(auth.AuthError): + with pytest.raises(auth.UnexpectedResponseError) as excinfo: func('test@test.com', MOCK_ACTION_CODE_SETTINGS, app=user_mgt_app) + assert str(excinfo.value) == 'Failed to generate email action link.' + assert excinfo.value.http_response is not None + assert excinfo.value.cause is None + assert isinstance(excinfo.value, exceptions.UnknownError) @pytest.mark.parametrize('func', [ auth.generate_sign_in_with_email_link,