Skip to content

Commit 8a0cf08

Browse files
authored
New error handling support in create/update/delete user APIs (#311)
* New error handling support in create/update/delete user APIs * Fixing some lint errors
1 parent 9fb0766 commit 8a0cf08

File tree

6 files changed

+104
-66
lines changed

6 files changed

+104
-66
lines changed

firebase_admin/_auth_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ def validate_action_type(action_type):
193193
return action_type
194194

195195

196+
class UidAlreadyExistsError(exceptions.AlreadyExistsError):
197+
"""The user with the provided uid already exists."""
198+
199+
default_message = 'The user with the provided uid already exists'
200+
201+
def __init__(self, message, cause, http_response=None):
202+
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)
203+
204+
196205
class InvalidIdTokenError(exceptions.InvalidArgumentError):
197206
"""The provided ID token is not a valid Firebase ID token."""
198207

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

220229

221230
_CODE_TO_EXC_TYPE = {
231+
'DUPLICATE_LOCAL_ID': UidAlreadyExistsError,
222232
'INVALID_ID_TOKEN': InvalidIdTokenError,
223233
'USER_NOT_FOUND': UserNotFoundError,
224234
}

firebase_admin/_user_mgt.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@
2424
from firebase_admin import _user_import
2525

2626

27-
USER_CREATE_ERROR = 'USER_CREATE_ERROR'
28-
USER_UPDATE_ERROR = 'USER_UPDATE_ERROR'
29-
USER_DELETE_ERROR = 'USER_DELETE_ERROR'
3027
USER_IMPORT_ERROR = 'USER_IMPORT_ERROR'
3128
USER_DOWNLOAD_ERROR = 'LIST_USERS_ERROR'
3229
GENERATE_EMAIL_ACTION_LINK_ERROR = 'GENERATE_EMAIL_ACTION_LINK_ERROR'
@@ -531,13 +528,14 @@ def create_user(self, uid=None, display_name=None, email=None, phone_number=None
531528
}
532529
payload = {k: v for k, v in payload.items() if v is not None}
533530
try:
534-
response = self._client.body('post', '/accounts', json=payload)
531+
body, http_resp = self._client.body_and_response('post', '/accounts', json=payload)
535532
except requests.exceptions.RequestException as error:
536-
self._handle_http_error(USER_CREATE_ERROR, 'Failed to create new user.', error)
533+
raise _auth_utils.handle_auth_backend_error(error)
537534
else:
538-
if not response or not response.get('localId'):
539-
raise ApiCallError(USER_CREATE_ERROR, 'Failed to create new user.')
540-
return response.get('localId')
535+
if not body or not body.get('localId'):
536+
raise _auth_utils.UnexpectedResponseError(
537+
'Failed to create new user.', http_response=http_resp)
538+
return body.get('localId')
541539

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

582580
payload = {k: v for k, v in payload.items() if v is not None}
583581
try:
584-
response = self._client.body('post', '/accounts:update', json=payload)
582+
body, http_resp = self._client.body_and_response(
583+
'post', '/accounts:update', json=payload)
585584
except requests.exceptions.RequestException as error:
586-
self._handle_http_error(
587-
USER_UPDATE_ERROR, 'Failed to update user: {0}.'.format(uid), error)
585+
raise _auth_utils.handle_auth_backend_error(error)
588586
else:
589-
if not response or not response.get('localId'):
590-
raise ApiCallError(USER_UPDATE_ERROR, 'Failed to update user: {0}.'.format(uid))
591-
return response.get('localId')
587+
if not body or not body.get('localId'):
588+
raise _auth_utils.UnexpectedResponseError(
589+
'Failed to update user: {0}.'.format(uid), http_response=http_resp)
590+
return body.get('localId')
592591

593592
def delete_user(self, uid):
594593
"""Deletes the user identified by the specified user ID."""
595594
_auth_utils.validate_uid(uid, required=True)
596595
try:
597-
response = self._client.body('post', '/accounts:delete', json={'localId' : uid})
596+
body, http_resp = self._client.body_and_response(
597+
'post', '/accounts:delete', json={'localId' : uid})
598598
except requests.exceptions.RequestException as error:
599-
self._handle_http_error(
600-
USER_DELETE_ERROR, 'Failed to delete user: {0}.'.format(uid), error)
599+
raise _auth_utils.handle_auth_backend_error(error)
601600
else:
602-
if not response or not response.get('kind'):
603-
raise ApiCallError(USER_DELETE_ERROR, 'Failed to delete user: {0}.'.format(uid))
601+
if not body or not body.get('kind'):
602+
raise _auth_utils.UnexpectedResponseError(
603+
'Failed to delete user: {0}.'.format(uid), http_response=http_resp)
604604

605605
def import_users(self, users, hash_alg=None):
606606
"""Imports the given list of users to Firebase Auth."""

firebase_admin/auth.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
'ImportUserRecord',
4545
'ListUsersPage',
4646
'TokenSignError',
47+
'UidAlreadyExistsError',
48+
'UnexpectedResponseError',
4749
'UserImportHash',
4850
'UserImportResult',
4951
'UserInfo',
@@ -80,6 +82,7 @@
8082
ImportUserRecord = _user_import.ImportUserRecord
8183
InvalidIdTokenError = _auth_utils.InvalidIdTokenError
8284
TokenSignError = _token_gen.TokenSignError
85+
UidAlreadyExistsError = _auth_utils.UidAlreadyExistsError
8386
UnexpectedResponseError = _auth_utils.UnexpectedResponseError
8487
UserImportResult = _user_import.UserImportResult
8588
UserInfo = _user_mgt.UserInfo
@@ -333,15 +336,12 @@ def create_user(**kwargs):
333336
334337
Raises:
335338
ValueError: If the specified user properties are invalid.
336-
AuthError: If an error occurs while creating the user account.
339+
FirebaseError: If an error occurs while creating the user account.
337340
"""
338341
app = kwargs.pop('app', None)
339342
user_manager = _get_auth_service(app).user_manager
340-
try:
341-
uid = user_manager.create_user(**kwargs)
342-
return UserRecord(user_manager.get_user(uid=uid))
343-
except _user_mgt.ApiCallError as error:
344-
raise AuthError(error.code, str(error), error.detail)
343+
uid = user_manager.create_user(**kwargs)
344+
return UserRecord(user_manager.get_user(uid=uid))
345345

346346

347347
def update_user(uid, **kwargs):
@@ -373,15 +373,12 @@ def update_user(uid, **kwargs):
373373
374374
Raises:
375375
ValueError: If the specified user ID or properties are invalid.
376-
AuthError: If an error occurs while updating the user account.
376+
FirebaseError: If an error occurs while updating the user account.
377377
"""
378378
app = kwargs.pop('app', None)
379379
user_manager = _get_auth_service(app).user_manager
380-
try:
381-
user_manager.update_user(uid, **kwargs)
382-
return UserRecord(user_manager.get_user(uid=uid))
383-
except _user_mgt.ApiCallError as error:
384-
raise AuthError(error.code, str(error), error.detail)
380+
user_manager.update_user(uid, **kwargs)
381+
return UserRecord(user_manager.get_user(uid=uid))
385382

386383

387384
def set_custom_user_claims(uid, custom_claims, app=None):
@@ -402,13 +399,10 @@ def set_custom_user_claims(uid, custom_claims, app=None):
402399
403400
Raises:
404401
ValueError: If the specified user ID or the custom claims are invalid.
405-
AuthError: If an error occurs while updating the user account.
402+
FirebaseError: If an error occurs while updating the user account.
406403
"""
407404
user_manager = _get_auth_service(app).user_manager
408-
try:
409-
user_manager.update_user(uid, custom_claims=custom_claims)
410-
except _user_mgt.ApiCallError as error:
411-
raise AuthError(error.code, str(error), error.detail)
405+
user_manager.update_user(uid, custom_claims=custom_claims)
412406

413407

414408
def delete_user(uid, app=None):
@@ -420,13 +414,10 @@ def delete_user(uid, app=None):
420414
421415
Raises:
422416
ValueError: If the user ID is None, empty or malformed.
423-
AuthError: If an error occurs while deleting the user account.
417+
FirebaseError: If an error occurs while deleting the user account.
424418
"""
425419
user_manager = _get_auth_service(app).user_manager
426-
try:
427-
user_manager.delete_user(uid)
428-
except _user_mgt.ApiCallError as error:
429-
raise AuthError(error.code, str(error), error.detail)
420+
user_manager.delete_user(uid)
430421

431422

432423
def import_users(users, hash_alg=None, app=None):

integration/test_auth.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,12 @@ def test_get_non_existing_user_by_email():
148148
assert str(excinfo.value) == error_msg
149149

150150
def test_update_non_existing_user():
151-
with pytest.raises(auth.AuthError) as excinfo:
151+
with pytest.raises(auth.UserNotFoundError):
152152
auth.update_user('non.existing')
153-
assert 'USER_UPDATE_ERROR' in str(excinfo.value.code)
154153

155154
def test_delete_non_existing_user():
156-
with pytest.raises(auth.AuthError) as excinfo:
155+
with pytest.raises(auth.UserNotFoundError):
157156
auth.delete_user('non.existing')
158-
assert 'USER_DELETE_ERROR' in str(excinfo.value.code)
159157

160158
@pytest.fixture
161159
def new_user():
@@ -258,9 +256,8 @@ def test_create_user(new_user):
258256
assert user.user_metadata.creation_timestamp > 0
259257
assert user.user_metadata.last_sign_in_timestamp is None
260258
assert len(user.provider_data) is 0
261-
with pytest.raises(auth.AuthError) as excinfo:
259+
with pytest.raises(auth.UidAlreadyExistsError):
262260
auth.create_user(uid=new_user.uid)
263-
assert excinfo.value.code == 'USER_CREATE_ERROR'
264261

265262
def test_update_user(new_user):
266263
_, email = _random_id()
@@ -329,9 +326,8 @@ def test_disable_user(new_user_with_params):
329326
def test_delete_user():
330327
user = auth.create_user()
331328
auth.delete_user(user.uid)
332-
with pytest.raises(auth.AuthError) as excinfo:
329+
with pytest.raises(auth.UserNotFoundError):
333330
auth.get_user(user.uid)
334-
assert excinfo.value.code == 'USER_NOT_FOUND_ERROR'
335331

336332
def test_revoke_refresh_tokens(new_user):
337333
user = auth.get_user(new_user.uid)

lint.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function lintAllFiles () {
2020
}
2121

2222
function lintChangedFiles () {
23-
files=`git status -s $1 | grep -v "^D" | awk '{print $NF}' | grep .py$`
23+
files=`git status -s $1 | (grep -v "^D") | awk '{print $NF}' | (grep .py$ || true)`
2424
for f in $files
2525
do
2626
echo "Running linter on $f"

tests/test_user_mgt.py

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,31 @@ def test_create_user_with_id(self, user_mgt_app):
351351
assert request == {'localId' : 'testuser'}
352352

353353
def test_create_user_error(self, user_mgt_app):
354-
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
355-
with pytest.raises(auth.AuthError) as excinfo:
354+
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
355+
with pytest.raises(exceptions.InternalError) as excinfo:
356356
auth.create_user(app=user_mgt_app)
357-
assert excinfo.value.code == _user_mgt.USER_CREATE_ERROR
358-
assert '{"error":"test"}' in str(excinfo.value)
357+
assert str(excinfo.value) == 'Error while calling Auth service (UNEXPECTED_CODE).'
358+
assert excinfo.value.http_response is not None
359+
assert excinfo.value.cause is not None
360+
361+
def test_uid_already_exists(self, user_mgt_app):
362+
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "DUPLICATE_LOCAL_ID"}}')
363+
with pytest.raises(auth.UidAlreadyExistsError) as excinfo:
364+
auth.create_user(app=user_mgt_app)
365+
assert isinstance(excinfo.value, exceptions.AlreadyExistsError)
366+
assert str(excinfo.value) == ('The user with the provided uid already exists '
367+
'(DUPLICATE_LOCAL_ID).')
368+
assert excinfo.value.http_response is not None
369+
assert excinfo.value.cause is not None
370+
371+
def test_create_user_unexpected_response(self, user_mgt_app):
372+
_instrument_user_manager(user_mgt_app, 200, '{"error": "test"}')
373+
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
374+
auth.create_user(app=user_mgt_app)
375+
assert str(excinfo.value) == 'Failed to create new user.'
376+
assert excinfo.value.http_response is not None
377+
assert excinfo.value.cause is None
378+
assert isinstance(excinfo.value, exceptions.UnknownError)
359379

360380

361381
class TestUpdateUser(object):
@@ -462,11 +482,21 @@ def test_update_user_delete_fields(self, user_mgt_app):
462482
}
463483

464484
def test_update_user_error(self, user_mgt_app):
465-
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
466-
with pytest.raises(auth.AuthError) as excinfo:
485+
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
486+
with pytest.raises(exceptions.InternalError) as excinfo:
467487
auth.update_user('user', app=user_mgt_app)
468-
assert excinfo.value.code == _user_mgt.USER_UPDATE_ERROR
469-
assert '{"error":"test"}' in str(excinfo.value)
488+
assert str(excinfo.value) == 'Error while calling Auth service (UNEXPECTED_CODE).'
489+
assert excinfo.value.http_response is not None
490+
assert excinfo.value.cause is not None
491+
492+
def test_update_user_unexpected_response(self, user_mgt_app):
493+
_instrument_user_manager(user_mgt_app, 200, '{"error": "test"}')
494+
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
495+
auth.update_user('user', app=user_mgt_app)
496+
assert str(excinfo.value) == 'Failed to update user: user.'
497+
assert excinfo.value.http_response is not None
498+
assert excinfo.value.cause is None
499+
assert isinstance(excinfo.value, exceptions.UnknownError)
470500

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

532562
def test_set_custom_user_claims_error(self, user_mgt_app):
533-
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
534-
with pytest.raises(auth.AuthError) as excinfo:
563+
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
564+
with pytest.raises(exceptions.InternalError) as excinfo:
535565
auth.set_custom_user_claims('user', {}, app=user_mgt_app)
536-
assert excinfo.value.code == _user_mgt.USER_UPDATE_ERROR
537-
assert '{"error":"test"}' in str(excinfo.value)
566+
assert str(excinfo.value) == 'Error while calling Auth service (UNEXPECTED_CODE).'
567+
assert excinfo.value.http_response is not None
568+
assert excinfo.value.cause is not None
538569

539570

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

552583
def test_delete_user_error(self, user_mgt_app):
553-
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
554-
with pytest.raises(auth.AuthError) as excinfo:
584+
_instrument_user_manager(user_mgt_app, 500, '{"error": {"message": "UNEXPECTED_CODE"}}')
585+
with pytest.raises(exceptions.InternalError) as excinfo:
555586
auth.delete_user('user', app=user_mgt_app)
556-
assert excinfo.value.code == _user_mgt.USER_DELETE_ERROR
557-
assert '{"error":"test"}' in str(excinfo.value)
587+
assert str(excinfo.value) == 'Error while calling Auth service (UNEXPECTED_CODE).'
588+
assert excinfo.value.http_response is not None
589+
assert excinfo.value.cause is not None
590+
591+
def test_delete_user_unexpected_response(self, user_mgt_app):
592+
_instrument_user_manager(user_mgt_app, 200, '{"error": "test"}')
593+
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
594+
auth.delete_user('user', app=user_mgt_app)
595+
assert str(excinfo.value) == 'Failed to delete user: user.'
596+
assert excinfo.value.http_response is not None
597+
assert excinfo.value.cause is None
598+
assert isinstance(excinfo.value, exceptions.UnknownError)
558599

559600

560601
class TestListUsers(object):

0 commit comments

Comments
 (0)