Skip to content

Introducing UserNotFoundError type #309

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 4 commits into from
Jul 18, 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
12 changes: 11 additions & 1 deletion firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,18 @@ def __init__(self, message, cause=None, http_response=None):
exceptions.UnknownError.__init__(self, message, cause, http_response)


class UserNotFoundError(exceptions.NotFoundError):
"""No user record found for the specified identifier."""

default_message = 'No user record found for the given identifier'

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


_CODE_TO_EXC_TYPE = {
'INVALID_ID_TOKEN': InvalidIdTokenError,
'USER_NOT_FOUND': UserNotFoundError,
}


Expand Down Expand Up @@ -243,7 +253,7 @@ def _parse_error_body(response):
pass

# Auth error response format: {"error": {"message": "AUTH_ERROR_CODE: Optional text"}}
code = error_dict.get('message')
code = error_dict.get('message') if isinstance(error_dict, dict) else None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be wrong, but this doesn't seem very pythonic to me checking isinstance. Is it ever valid for error_dict to not be 'None' and also not support the get method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the server response format doesn't change we should be fine. I'm being a little too defensive here. In general, it's not clear what the Auth response format would be in the event of a major outage or similar issue.

custom_message = None
if code:
separator = code.find(':')
Expand Down
21 changes: 11 additions & 10 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
from firebase_admin import _user_import


INTERNAL_ERROR = 'INTERNAL_ERROR'
USER_NOT_FOUND_ERROR = 'USER_NOT_FOUND_ERROR'
USER_CREATE_ERROR = 'USER_CREATE_ERROR'
USER_UPDATE_ERROR = 'USER_UPDATE_ERROR'
USER_DELETE_ERROR = 'USER_DELETE_ERROR'
Expand Down Expand Up @@ -381,6 +379,7 @@ def photo_url(self):
def provider_id(self):
return self._data.get('providerId')


class ActionCodeSettings(object):
"""Contains required continue/state URL with optional Android and iOS settings.
Used when invoking the email action link generation APIs.
Expand All @@ -396,6 +395,7 @@ def __init__(self, url, handle_code_in_app=None, dynamic_link_domain=None, ios_b
self.android_install_app = android_install_app
self.android_minimum_version = android_minimum_version


def encode_action_code_settings(settings):
""" Validates the provided action code settings for email link generation and
populates the REST api parameters.
Expand Down Expand Up @@ -463,6 +463,7 @@ def encode_action_code_settings(settings):

return parameters


class UserManager(object):
"""Provides methods for interacting with the Google Identity Toolkit."""

Expand All @@ -484,16 +485,16 @@ def get_user(self, **kwargs):
raise TypeError('Unsupported keyword arguments: {0}.'.format(kwargs))

try:
response = self._client.body('post', '/accounts:lookup', json=payload)
body, http_resp = self._client.body_and_response(
'post', '/accounts:lookup', json=payload)
except requests.exceptions.RequestException as error:
msg = 'Failed to get user by {0}: {1}.'.format(key_type, key)
self._handle_http_error(INTERNAL_ERROR, msg, error)
raise _auth_utils.handle_auth_backend_error(error)
else:
if not response or not response.get('users'):
raise ApiCallError(
USER_NOT_FOUND_ERROR,
'No user record found for the provided {0}: {1}.'.format(key_type, key))
return response['users'][0]
if not body or not body.get('users'):
raise _auth_utils.UserNotFoundError(
'No user record found for the provided {0}: {1}.'.format(key_type, key),
http_response=http_resp)
return body['users'][0]

def list_users(self, page_token=None, max_results=MAX_LIST_USERS_RESULTS):
"""Retrieves a batch of users."""
Expand Down
35 changes: 14 additions & 21 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
'UserImportResult',
'UserInfo',
'UserMetadata',
'UserNotFoundError',
'UserProvider',
'UserRecord',

Expand Down Expand Up @@ -83,6 +84,7 @@
UserImportResult = _user_import.UserImportResult
UserInfo = _user_mgt.UserInfo
UserMetadata = _user_mgt.UserMetadata
UserNotFoundError = _auth_utils.UserNotFoundError
UserProvider = _user_import.UserProvider
UserRecord = _user_mgt.UserRecord

Expand Down Expand Up @@ -232,15 +234,12 @@ def get_user(uid, app=None):

Raises:
ValueError: If the user ID is None, empty or malformed.
AuthError: If an error occurs while retrieving the user or if the specified user ID
does not exist.
UserNotFoundError: If the specified user ID does not exist.
FirebaseError: If an error occurs while retrieving the user.
"""
user_manager = _get_auth_service(app).user_manager
try:
response = user_manager.get_user(uid=uid)
return UserRecord(response)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
response = user_manager.get_user(uid=uid)
return UserRecord(response)


def get_user_by_email(email, app=None):
Expand All @@ -255,15 +254,12 @@ def get_user_by_email(email, app=None):

Raises:
ValueError: If the email is None, empty or malformed.
AuthError: If an error occurs while retrieving the user or no user exists by the specified
email address.
UserNotFoundError: If no user exists by the specified email address.
FirebaseError: If an error occurs while retrieving the user.
"""
user_manager = _get_auth_service(app).user_manager
try:
response = user_manager.get_user(email=email)
return UserRecord(response)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
response = user_manager.get_user(email=email)
return UserRecord(response)


def get_user_by_phone_number(phone_number, app=None):
Expand All @@ -278,15 +274,12 @@ def get_user_by_phone_number(phone_number, app=None):

Raises:
ValueError: If the phone number is None, empty or malformed.
AuthError: If an error occurs while retrieving the user or no user exists by the specified
phone number.
UserNotFoundError: If no user exists by the specified phone number.
FirebaseError: If an error occurs while retrieving the user.
"""
user_manager = _get_auth_service(app).user_manager
try:
response = user_manager.get_user(phone_number=phone_number)
return UserRecord(response)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
response = user_manager.get_user(phone_number=phone_number)
return UserRecord(response)


def list_users(page_token=None, max_results=_user_mgt.MAX_LIST_USERS_RESULTS, app=None):
Expand Down
11 changes: 7 additions & 4 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import google.oauth2.credentials
from google.auth import transport


_verify_token_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken'
_verify_password_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword'
_password_reset_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword'
Expand Down Expand Up @@ -135,14 +136,16 @@ def test_session_cookie_error():
auth.create_session_cookie('not.a.token', expires_in=expires_in)

def test_get_non_existing_user():
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user('non.existing')
assert 'USER_NOT_FOUND_ERROR' in str(excinfo.value.code)
assert str(excinfo.value) == 'No user record found for the provided user ID: non.existing.'

def test_get_non_existing_user_by_email():
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user_by_email('non.existing@definitely.non.existing')
assert 'USER_NOT_FOUND_ERROR' in str(excinfo.value.code)
error_msg = ('No user record found for the provided email: '
'non.existing@definitely.non.existing.')
assert str(excinfo.value) == error_msg

def test_update_non_existing_user():
with pytest.raises(auth.AuthError) as excinfo:
Expand Down
81 changes: 67 additions & 14 deletions tests/test_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import firebase_admin
from firebase_admin import auth
from firebase_admin import exceptions
from firebase_admin import _auth_utils
from firebase_admin import _user_import
from firebase_admin import _user_mgt
Expand Down Expand Up @@ -211,30 +212,79 @@ def test_get_user_by_phone(self, user_mgt_app):

def test_get_user_non_existing(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"users":[]}')
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user('nonexistentuser', user_mgt_app)
assert excinfo.value.code == _user_mgt.USER_NOT_FOUND_ERROR
error_msg = 'No user record found for the provided user ID: nonexistentuser.'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None

def test_get_user_by_email_non_existing(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"users":[]}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user_by_email('nonexistent@user', user_mgt_app)
error_msg = 'No user record found for the provided email: nonexistent@user.'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None

def test_get_user_by_phone_non_existing(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"users":[]}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user_by_phone_number('+1234567890', user_mgt_app)
error_msg = 'No user record found for the provided phone number: +1234567890.'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None

def test_get_user_http_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": "USER_NOT_FOUND"}}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user('testuser', user_mgt_app)
assert excinfo.value.code == _user_mgt.INTERNAL_ERROR
assert '{"error":"test"}' in str(excinfo.value)
error_msg = 'No user record found for the given identifier (USER_NOT_FOUND).'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None

def test_get_user_http_error_unexpected_code(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "UNEXPECTED_CODE"}}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.get_user('testuser', 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

def test_get_user_http_error_malformed_response(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error": "UNEXPECTED_CODE"}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.get_user('testuser', user_mgt_app)
assert str(excinfo.value) == 'Unexpected error response: {"error": "UNEXPECTED_CODE"}'
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None

def test_get_user_by_email_http_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": "USER_NOT_FOUND"}}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user_by_email('non.existent.user@example.com', user_mgt_app)
assert excinfo.value.code == _user_mgt.INTERNAL_ERROR
assert '{"error":"test"}' in str(excinfo.value)
error_msg = 'No user record found for the given identifier (USER_NOT_FOUND).'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None

def test_get_user_by_phone_http_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": "USER_NOT_FOUND"}}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user_by_phone_number('+1234567890', user_mgt_app)
assert excinfo.value.code == _user_mgt.INTERNAL_ERROR
assert '{"error":"test"}' in str(excinfo.value)
error_msg = 'No user record found for the given identifier (USER_NOT_FOUND).'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None


class TestCreateUser(object):
Expand Down Expand Up @@ -718,6 +768,7 @@ def test_invalid_args(self, arg):
with pytest.raises(ValueError):
auth.UserMetadata(**arg)


class TestImportUserRecord(object):

_INVALID_USERS = (
Expand Down Expand Up @@ -1003,6 +1054,7 @@ def test_revoke_refresh_tokens(self, user_mgt_app):
assert int(request['validSince']) >= int(before_time)
assert int(request['validSince']) <= int(after_time)


class TestActionCodeSetting(object):

def test_valid_data(self):
Expand Down Expand Up @@ -1047,6 +1099,7 @@ def test_encode_action_code_bad_data(self):
with pytest.raises(AttributeError):
_user_mgt.encode_action_code_settings({"foo":"bar"})


class TestGenerateEmailActionLink(object):

def test_email_verification_no_settings(self, user_mgt_app):
Expand Down