Skip to content

New error handling support in create/update/delete user APIs #311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,15 @@ def validate_action_type(action_type):
return action_type


class UidAlreadyExistsError(exceptions.AlreadyExistsError):
"""The user with the provided uid already exists."""

default_message = 'The user with the provided uid already exists'

def __init__(self, message, cause, http_response=None):
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)


class InvalidIdTokenError(exceptions.InvalidArgumentError):
"""The provided ID token is not a valid Firebase ID token."""

Expand All @@ -219,6 +228,7 @@ def __init__(self, message, cause=None, http_response=None):


_CODE_TO_EXC_TYPE = {
'DUPLICATE_LOCAL_ID': UidAlreadyExistsError,
'INVALID_ID_TOKEN': InvalidIdTokenError,
'USER_NOT_FOUND': UserNotFoundError,
}
Expand Down
38 changes: 19 additions & 19 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@
from firebase_admin import _user_import


USER_CREATE_ERROR = 'USER_CREATE_ERROR'
USER_UPDATE_ERROR = 'USER_UPDATE_ERROR'
USER_DELETE_ERROR = 'USER_DELETE_ERROR'
USER_IMPORT_ERROR = 'USER_IMPORT_ERROR'
USER_DOWNLOAD_ERROR = 'LIST_USERS_ERROR'
GENERATE_EMAIL_ACTION_LINK_ERROR = 'GENERATE_EMAIL_ACTION_LINK_ERROR'
Expand Down Expand Up @@ -531,13 +528,14 @@ def create_user(self, uid=None, display_name=None, email=None, phone_number=None
}
payload = {k: v for k, v in payload.items() if v is not None}
try:
response = self._client.body('post', '/accounts', json=payload)
body, http_resp = self._client.body_and_response('post', '/accounts', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(USER_CREATE_ERROR, 'Failed to create new user.', error)
raise _auth_utils.handle_auth_backend_error(error)
else:
if not response or not response.get('localId'):
raise ApiCallError(USER_CREATE_ERROR, 'Failed to create new user.')
return response.get('localId')
if not body or not body.get('localId'):
raise _auth_utils.UnexpectedResponseError(
'Failed to create new user.', http_response=http_resp)
return body.get('localId')

def update_user(self, uid, display_name=_UNSPECIFIED, email=None, phone_number=_UNSPECIFIED,
photo_url=_UNSPECIFIED, password=None, disabled=None, email_verified=None,
Expand Down Expand Up @@ -581,26 +579,28 @@ def update_user(self, uid, display_name=_UNSPECIFIED, email=None, phone_number=_

payload = {k: v for k, v in payload.items() if v is not None}
try:
response = self._client.body('post', '/accounts:update', json=payload)
body, http_resp = self._client.body_and_response(
'post', '/accounts:update', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(
USER_UPDATE_ERROR, 'Failed to update user: {0}.'.format(uid), error)
raise _auth_utils.handle_auth_backend_error(error)
else:
if not response or not response.get('localId'):
raise ApiCallError(USER_UPDATE_ERROR, 'Failed to update user: {0}.'.format(uid))
return response.get('localId')
if not body or not body.get('localId'):
raise _auth_utils.UnexpectedResponseError(
'Failed to update user: {0}.'.format(uid), http_response=http_resp)
return body.get('localId')

def delete_user(self, uid):
"""Deletes the user identified by the specified user ID."""
_auth_utils.validate_uid(uid, required=True)
try:
response = self._client.body('post', '/accounts:delete', json={'localId' : uid})
body, http_resp = self._client.body_and_response(
'post', '/accounts:delete', json={'localId' : uid})
except requests.exceptions.RequestException as error:
self._handle_http_error(
USER_DELETE_ERROR, 'Failed to delete user: {0}.'.format(uid), error)
raise _auth_utils.handle_auth_backend_error(error)
else:
if not response or not response.get('kind'):
raise ApiCallError(USER_DELETE_ERROR, 'Failed to delete user: {0}.'.format(uid))
if not body or not body.get('kind'):
raise _auth_utils.UnexpectedResponseError(
'Failed to delete user: {0}.'.format(uid), http_response=http_resp)

def import_users(self, users, hash_alg=None):
"""Imports the given list of users to Firebase Auth."""
Expand Down
35 changes: 13 additions & 22 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
'ImportUserRecord',
'ListUsersPage',
'TokenSignError',
'UidAlreadyExistsError',
'UnexpectedResponseError',
'UserImportHash',
'UserImportResult',
'UserInfo',
Expand Down Expand Up @@ -80,6 +82,7 @@
ImportUserRecord = _user_import.ImportUserRecord
InvalidIdTokenError = _auth_utils.InvalidIdTokenError
TokenSignError = _token_gen.TokenSignError
UidAlreadyExistsError = _auth_utils.UidAlreadyExistsError
UnexpectedResponseError = _auth_utils.UnexpectedResponseError
UserImportResult = _user_import.UserImportResult
UserInfo = _user_mgt.UserInfo
Expand Down Expand Up @@ -333,15 +336,12 @@ def create_user(**kwargs):

Raises:
ValueError: If the specified user properties are invalid.
AuthError: If an error occurs while creating the user account.
FirebaseError: If an error occurs while creating the user account.
"""
app = kwargs.pop('app', None)
user_manager = _get_auth_service(app).user_manager
try:
uid = user_manager.create_user(**kwargs)
return UserRecord(user_manager.get_user(uid=uid))
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
uid = user_manager.create_user(**kwargs)
return UserRecord(user_manager.get_user(uid=uid))


def update_user(uid, **kwargs):
Expand Down Expand Up @@ -373,15 +373,12 @@ def update_user(uid, **kwargs):

Raises:
ValueError: If the specified user ID or properties are invalid.
AuthError: If an error occurs while updating the user account.
FirebaseError: If an error occurs while updating the user account.
"""
app = kwargs.pop('app', None)
user_manager = _get_auth_service(app).user_manager
try:
user_manager.update_user(uid, **kwargs)
return UserRecord(user_manager.get_user(uid=uid))
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
user_manager.update_user(uid, **kwargs)
return UserRecord(user_manager.get_user(uid=uid))


def set_custom_user_claims(uid, custom_claims, app=None):
Expand All @@ -402,13 +399,10 @@ def set_custom_user_claims(uid, custom_claims, app=None):

Raises:
ValueError: If the specified user ID or the custom claims are invalid.
AuthError: If an error occurs while updating the user account.
FirebaseError: If an error occurs while updating the user account.
"""
user_manager = _get_auth_service(app).user_manager
try:
user_manager.update_user(uid, custom_claims=custom_claims)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
user_manager.update_user(uid, custom_claims=custom_claims)


def delete_user(uid, app=None):
Expand All @@ -420,13 +414,10 @@ def delete_user(uid, app=None):

Raises:
ValueError: If the user ID is None, empty or malformed.
AuthError: If an error occurs while deleting the user account.
FirebaseError: If an error occurs while deleting the user account.
"""
user_manager = _get_auth_service(app).user_manager
try:
user_manager.delete_user(uid)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
user_manager.delete_user(uid)


def import_users(users, hash_alg=None, app=None):
Expand Down
12 changes: 4 additions & 8 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,12 @@ def test_get_non_existing_user_by_email():
assert str(excinfo.value) == error_msg

def test_update_non_existing_user():
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UserNotFoundError):
auth.update_user('non.existing')
assert 'USER_UPDATE_ERROR' in str(excinfo.value.code)

def test_delete_non_existing_user():
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UserNotFoundError):
auth.delete_user('non.existing')
assert 'USER_DELETE_ERROR' in str(excinfo.value.code)

@pytest.fixture
def new_user():
Expand Down Expand Up @@ -258,9 +256,8 @@ def test_create_user(new_user):
assert user.user_metadata.creation_timestamp > 0
assert user.user_metadata.last_sign_in_timestamp is None
assert len(user.provider_data) is 0
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UidAlreadyExistsError):
auth.create_user(uid=new_user.uid)
assert excinfo.value.code == 'USER_CREATE_ERROR'

def test_update_user(new_user):
_, email = _random_id()
Expand Down Expand Up @@ -329,9 +326,8 @@ def test_disable_user(new_user_with_params):
def test_delete_user():
user = auth.create_user()
auth.delete_user(user.uid)
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UserNotFoundError):
auth.get_user(user.uid)
assert excinfo.value.code == 'USER_NOT_FOUND_ERROR'

def test_revoke_refresh_tokens(new_user):
user = auth.get_user(new_user.uid)
Expand Down
2 changes: 1 addition & 1 deletion lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function lintAllFiles () {
}

function lintChangedFiles () {
files=`git status -s $1 | grep -v "^D" | awk '{print $NF}' | grep .py$`
files=`git status -s $1 | (grep -v "^D") | awk '{print $NF}' | (grep .py$ || true)`
for f in $files
do
echo "Running linter on $f"
Expand Down
73 changes: 57 additions & 16 deletions tests/test_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,31 @@ def test_create_user_with_id(self, user_mgt_app):
assert request == {'localId' : 'testuser'}

def test_create_user_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
with pytest.raises(auth.AuthError) as excinfo:
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.create_user(app=user_mgt_app)
assert excinfo.value.code == _user_mgt.USER_CREATE_ERROR
assert '{"error":"test"}' in str(excinfo.value)
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

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:
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 excinfo.value.http_response is not None
assert excinfo.value.cause is not None

def test_create_user_unexpected_response(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"error": "test"}')
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
auth.create_user(app=user_mgt_app)
assert str(excinfo.value) == 'Failed to create new user.'
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None
assert isinstance(excinfo.value, exceptions.UnknownError)


class TestUpdateUser(object):
Expand Down Expand Up @@ -462,11 +482,21 @@ def test_update_user_delete_fields(self, user_mgt_app):
}

def test_update_user_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
with pytest.raises(auth.AuthError) as excinfo:
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.update_user('user', app=user_mgt_app)
assert excinfo.value.code == _user_mgt.USER_UPDATE_ERROR
assert '{"error":"test"}' in str(excinfo.value)
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

def test_update_user_unexpected_response(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"error": "test"}')
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
auth.update_user('user', app=user_mgt_app)
assert str(excinfo.value) == 'Failed to update user: user.'
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None
assert isinstance(excinfo.value, exceptions.UnknownError)

@pytest.mark.parametrize('arg', [1, 1.0])
def test_update_user_valid_since(self, user_mgt_app, arg):
Expand Down Expand Up @@ -530,11 +560,12 @@ def test_set_custom_user_claims_none(self, user_mgt_app):
assert request == {'localId' : 'testuser', 'customAttributes' : json.dumps({})}

def test_set_custom_user_claims_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
with pytest.raises(auth.AuthError) as excinfo:
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.set_custom_user_claims('user', {}, app=user_mgt_app)
assert excinfo.value.code == _user_mgt.USER_UPDATE_ERROR
assert '{"error":"test"}' in str(excinfo.value)
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


class TestDeleteUser(object):
Expand All @@ -550,11 +581,21 @@ def test_delete_user(self, user_mgt_app):
auth.delete_user('testuser', user_mgt_app)

def test_delete_user_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
with pytest.raises(auth.AuthError) as excinfo:
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.delete_user('user', app=user_mgt_app)
assert excinfo.value.code == _user_mgt.USER_DELETE_ERROR
assert '{"error":"test"}' in str(excinfo.value)
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

def test_delete_user_unexpected_response(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"error": "test"}')
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
auth.delete_user('user', app=user_mgt_app)
assert str(excinfo.value) == 'Failed to delete user: user.'
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None
assert isinstance(excinfo.value, exceptions.UnknownError)


class TestListUsers(object):
Expand Down