diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index c7b6c15f1..d90b494f5 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -202,6 +202,15 @@ def __init__(self, message, cause, http_response): exceptions.AlreadyExistsError.__init__(self, message, cause, http_response) +class EmailAlreadyExistsError(exceptions.AlreadyExistsError): + """The user with the provided email already exists.""" + + default_message = 'The user with the provided email already exists' + + 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.""" @@ -220,6 +229,15 @@ def __init__(self, message, cause=None, http_response=None): exceptions.InvalidArgumentError.__init__(self, message, cause, http_response) +class PhoneNumberAlreadyExistsError(exceptions.AlreadyExistsError): + """The user with the provided phone number already exists.""" + + default_message = 'The user with the provided phone number already exists' + + def __init__(self, message, cause, http_response): + exceptions.AlreadyExistsError.__init__(self, message, cause, http_response) + + class UnexpectedResponseError(exceptions.UnknownError): """Backend service responded with an unexpected or malformed response.""" @@ -237,9 +255,11 @@ def __init__(self, message, cause=None, http_response=None): _CODE_TO_EXC_TYPE = { + 'DUPLICATE_EMAIL': EmailAlreadyExistsError, 'DUPLICATE_LOCAL_ID': UidAlreadyExistsError, 'INVALID_DYNAMIC_LINK_DOMAIN': InvalidDynamicLinkDomainError, 'INVALID_ID_TOKEN': InvalidIdTokenError, + 'PHONE_NUMBER_EXISTS': PhoneNumberAlreadyExistsError, 'USER_NOT_FOUND': UserNotFoundError, } diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index bbc7c613a..cddc8ab0d 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -37,6 +37,7 @@ 'ActionCodeSettings', 'CertificateFetchError', 'DELETE_ATTRIBUTE', + 'EmailAlreadyExistsError', 'ErrorInfo', 'ExpiredIdTokenError', 'ExpiredSessionCookieError', @@ -46,6 +47,7 @@ 'InvalidIdTokenError', 'InvalidSessionCookieError', 'ListUsersPage', + 'PhoneNumberAlreadyExistsError', 'RevokedIdTokenError', 'RevokedSessionCookieError', 'TokenSignError', @@ -81,6 +83,7 @@ ActionCodeSettings = _user_mgt.ActionCodeSettings CertificateFetchError = _token_gen.CertificateFetchError DELETE_ATTRIBUTE = _user_mgt.DELETE_ATTRIBUTE +EmailAlreadyExistsError = _auth_utils.EmailAlreadyExistsError ErrorInfo = _user_import.ErrorInfo ExpiredIdTokenError = _token_gen.ExpiredIdTokenError ExpiredSessionCookieError = _token_gen.ExpiredSessionCookieError @@ -90,6 +93,7 @@ InvalidIdTokenError = _auth_utils.InvalidIdTokenError InvalidSessionCookieError = _token_gen.InvalidSessionCookieError ListUsersPage = _user_mgt.ListUsersPage +PhoneNumberAlreadyExistsError = _auth_utils.PhoneNumberAlreadyExistsError RevokedIdTokenError = _token_gen.RevokedIdTokenError RevokedSessionCookieError = _token_gen.RevokedSessionCookieError TokenSignError = _token_gen.TokenSignError diff --git a/firebase_admin/exceptions.py b/firebase_admin/exceptions.py index f1297dbb3..bfc3fff1f 100644 --- a/firebase_admin/exceptions.py +++ b/firebase_admin/exceptions.py @@ -38,7 +38,18 @@ class FirebaseError(Exception): - """Base class for all errors raised by the Admin SDK.""" + """Base class for all errors raised by the Admin SDK. + + Args: + code: A string error code that represents the type of the exception. Possible error + codes are defined in https://cloud.google.com/apis/design/errors#handling_errors. + message: A human-readable error message string. + cause: The exception that caused this error (optional). + http_response: If this error was caused by an HTTP error response, this property is + set to the ``requests.Response`` object that represents the HTTP response (optional). + See https://2.python-requests.org/en/master/api/#requests.Response for details of + this object. + """ def __init__(self, code, message, cause=None, http_response=None): Exception.__init__(self, message) diff --git a/tests/test_user_mgt.py b/tests/test_user_mgt.py index 34e2b019b..3847ff1ab 100644 --- a/tests/test_user_mgt.py +++ b/tests/test_user_mgt.py @@ -289,6 +289,12 @@ def test_get_user_by_phone_http_error(self, user_mgt_app): class TestCreateUser(object): + already_exists_errors = { + 'DUPLICATE_EMAIL': auth.EmailAlreadyExistsError, + 'DUPLICATE_LOCAL_ID': auth.UidAlreadyExistsError, + 'PHONE_NUMBER_EXISTS': auth.PhoneNumberAlreadyExistsError, + } + @pytest.mark.parametrize('arg', INVALID_STRINGS[1:] + ['a'*129]) def test_invalid_uid(self, user_mgt_app, arg): with pytest.raises(ValueError): @@ -358,13 +364,15 @@ def test_create_user_error(self, user_mgt_app): assert excinfo.value.http_response is not None assert excinfo.value.cause is not None - def test_uid_already_exists(self, user_mgt_app): - _instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "DUPLICATE_LOCAL_ID"}}') - with pytest.raises(auth.UidAlreadyExistsError) as excinfo: + @pytest.mark.parametrize('error_code', already_exists_errors.keys()) + def test_user_already_exists(self, user_mgt_app, error_code): + resp = {'error': {'message': error_code}} + _instrument_user_manager(user_mgt_app, 500, json.dumps(resp)) + exc_type = self.already_exists_errors[error_code] + with pytest.raises(exc_type) as excinfo: auth.create_user(app=user_mgt_app) assert isinstance(excinfo.value, exceptions.AlreadyExistsError) - assert str(excinfo.value) == ('The user with the provided uid already exists ' - '(DUPLICATE_LOCAL_ID).') + assert str(excinfo.value) == '{0} ({1}).'.format(exc_type.default_message, error_code) assert excinfo.value.http_response is not None assert excinfo.value.cause is not None