diff --git a/firebase_admin/project_management.py b/firebase_admin/project_management.py index cc57471c5..075ee7a68 100644 --- a/firebase_admin/project_management.py +++ b/firebase_admin/project_management.py @@ -25,6 +25,7 @@ import six import firebase_admin +from firebase_admin import exceptions from firebase_admin import _http_client from firebase_admin import _utils @@ -139,21 +140,6 @@ def _check_not_none(obj, field_name): return obj -class ApiCallError(Exception): - """An error encountered while interacting with the Firebase Project Management Service.""" - - def __init__(self, message, error): - Exception.__init__(self, message) - self.detail = error - - -class _PollingError(Exception): - """An error encountered during the polling of an app's creation status.""" - - def __init__(self, message): - Exception.__init__(self, message) - - class AndroidApp(object): """A reference to an Android app within a Firebase project. @@ -185,7 +171,7 @@ def get_metadata(self): AndroidAppMetadata: An ``AndroidAppMetadata`` instance. Raises: - ApiCallError: If an error occurs while communicating with the Firebase Project + FirebaseError: If an error occurs while communicating with the Firebase Project Management Service. """ return self._service.get_android_app_metadata(self._app_id) @@ -200,7 +186,7 @@ def set_display_name(self, new_display_name): NoneType: None. Raises: - ApiCallError: If an error occurs while communicating with the Firebase Project + FirebaseError: If an error occurs while communicating with the Firebase Project Management Service. """ return self._service.set_android_app_display_name(self._app_id, new_display_name) @@ -216,7 +202,7 @@ def get_sha_certificates(self): list: A list of ``ShaCertificate`` instances. Raises: - ApiCallError: If an error occurs while communicating with the Firebase Project + FirebaseError: If an error occurs while communicating with the Firebase Project Management Service. """ return self._service.get_sha_certificates(self._app_id) @@ -231,7 +217,7 @@ def add_sha_certificate(self, certificate_to_add): NoneType: None. Raises: - ApiCallError: If an error occurs while communicating with the Firebase Project + FirebaseError: If an error occurs while communicating with the Firebase Project Management Service. (For example, if the certificate_to_add already exists.) """ return self._service.add_sha_certificate(self._app_id, certificate_to_add) @@ -246,7 +232,7 @@ def delete_sha_certificate(self, certificate_to_delete): NoneType: None. Raises: - ApiCallError: If an error occurs while communicating with the Firebase Project + FirebaseError: If an error occurs while communicating with the Firebase Project Management Service. (For example, if the certificate_to_delete is not found.) """ return self._service.delete_sha_certificate(certificate_to_delete) @@ -283,7 +269,7 @@ def get_metadata(self): IosAppMetadata: An ``IosAppMetadata`` instance. Raises: - ApiCallError: If an error occurs while communicating with the Firebase Project + FirebaseError: If an error occurs while communicating with the Firebase Project Management Service. """ return self._service.get_ios_app_metadata(self._app_id) @@ -298,7 +284,7 @@ def set_display_name(self, new_display_name): NoneType: None. Raises: - ApiCallError: If an error occurs while communicating with the Firebase Project + FirebaseError: If an error occurs while communicating with the Firebase Project Management Service. """ return self._service.set_ios_app_display_name(self._app_id, new_display_name) @@ -478,22 +464,11 @@ class _ProjectManagementService(object): MAXIMUM_POLLING_ATTEMPTS = 8 POLL_BASE_WAIT_TIME_SECONDS = 0.5 POLL_EXPONENTIAL_BACKOFF_FACTOR = 1.5 - ERROR_CODES = { - 401: 'Request not authorized.', - 403: 'Client does not have sufficient privileges.', - 404: 'Failed to find the resource.', - 409: 'The resource already exists.', - 429: 'Request throttled out by the backend server.', - 500: 'Internal server error.', - 503: 'Backend servers are over capacity. Try again later.' - } ANDROID_APPS_RESOURCE_NAME = 'androidApps' ANDROID_APP_IDENTIFIER_NAME = 'packageName' - ANDROID_APP_IDENTIFIER_LABEL = 'Package name' IOS_APPS_RESOURCE_NAME = 'iosApps' IOS_APP_IDENTIFIER_NAME = 'bundleId' - IOS_APP_IDENTIFIER_LABEL = 'Bundle ID' def __init__(self, app): project_id = app.project_id @@ -528,7 +503,7 @@ def _get_app_metadata(self, platform_resource_name, identifier_name, metadata_cl """Retrieves detailed information about an Android or iOS app.""" _check_is_nonempty_string(app_id, 'app_id') path = '/v1beta1/projects/-/{0}/{1}'.format(platform_resource_name, app_id) - response = self._make_request('get', path, app_id, 'App ID') + response = self._make_request('get', path) return metadata_class( response[identifier_name], name=response['name'], @@ -553,7 +528,7 @@ def _set_display_name(self, app_id, new_display_name, platform_resource_name): path = '/v1beta1/projects/-/{0}/{1}?updateMask=displayName'.format( platform_resource_name, app_id) request_body = {'displayName': new_display_name} - self._make_request('patch', path, app_id, 'App ID', json=request_body) + self._make_request('patch', path, json=request_body) def list_android_apps(self): return self._list_apps( @@ -571,7 +546,7 @@ def _list_apps(self, platform_resource_name, app_class): self._project_id, platform_resource_name, _ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE) - response = self._make_request('get', path, self._project_id, 'Project ID') + response = self._make_request('get', path) apps_list = [] while True: apps = response.get('apps') @@ -587,14 +562,13 @@ def _list_apps(self, platform_resource_name, app_class): platform_resource_name, next_page_token, _ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE) - response = self._make_request('get', path, self._project_id, 'Project ID') + response = self._make_request('get', path) return apps_list def create_android_app(self, package_name, display_name=None): return self._create_app( platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME, identifier_name=_ProjectManagementService.ANDROID_APP_IDENTIFIER_NAME, - identifier_label=_ProjectManagementService.ANDROID_APP_IDENTIFIER_LABEL, identifier=package_name, display_name=display_name, app_class=AndroidApp) @@ -603,7 +577,6 @@ def create_ios_app(self, bundle_id, display_name=None): return self._create_app( platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME, identifier_name=_ProjectManagementService.IOS_APP_IDENTIFIER_NAME, - identifier_label=_ProjectManagementService.IOS_APP_IDENTIFIER_LABEL, identifier=bundle_id, display_name=display_name, app_class=IosApp) @@ -612,7 +585,6 @@ def _create_app( self, platform_resource_name, identifier_name, - identifier_label, identifier, display_name, app_class): @@ -622,15 +594,10 @@ def _create_app( request_body = {identifier_name: identifier} if display_name: request_body['displayName'] = display_name - response = self._make_request('post', path, identifier, identifier_label, json=request_body) + response = self._make_request('post', path, json=request_body) operation_name = response['name'] - try: - poll_response = self._poll_app_creation(operation_name) - return app_class(app_id=poll_response['appId'], service=self) - except _PollingError as error: - raise ApiCallError( - _ProjectManagementService._extract_message(operation_name, 'Operation name', error), - error) + poll_response = self._poll_app_creation(operation_name) + return app_class(app_id=poll_response['appId'], service=self) def _poll_app_creation(self, operation_name): """Polls the Long-Running Operation repeatedly until it is done with exponential backoff.""" @@ -640,16 +607,17 @@ def _poll_app_creation(self, operation_name): wait_time_seconds = delay_factor * _ProjectManagementService.POLL_BASE_WAIT_TIME_SECONDS time.sleep(wait_time_seconds) path = '/v1/{0}'.format(operation_name) - poll_response = self._make_request('get', path, operation_name, 'Operation name') + poll_response, http_response = self._body_and_response('get', path) done = poll_response.get('done') if done: response = poll_response.get('response') if response: return response else: - raise _PollingError( - 'Polling finished, but the operation terminated in an error.') - raise _PollingError('Polling deadline exceeded.') + raise exceptions.UnknownError( + 'Polling finished, but the operation terminated in an error.', + http_response=http_response) + raise exceptions.DeadlineExceededError('Polling deadline exceeded.') def get_android_app_config(self, app_id): return self._get_app_config( @@ -662,14 +630,14 @@ def get_ios_app_config(self, app_id): def _get_app_config(self, platform_resource_name, app_id): path = '/v1beta1/projects/-/{0}/{1}/config'.format(platform_resource_name, app_id) - response = self._make_request('get', path, app_id, 'App ID') + response = self._make_request('get', path) # In Python 2.7, the base64 module works with strings, while in Python 3, it works with # bytes objects. This line works in both versions. return base64.standard_b64decode(response['configFileContents']).decode(encoding='utf-8') def get_sha_certificates(self, app_id): path = '/v1beta1/projects/-/androidApps/{0}/sha'.format(app_id) - response = self._make_request('get', path, app_id, 'App ID') + response = self._make_request('get', path) cert_list = response.get('certificates') or [] return [ShaCertificate(sha_hash=cert['shaHash'], name=cert['name']) for cert in cert_list] @@ -678,28 +646,20 @@ def add_sha_certificate(self, app_id, certificate_to_add): sha_hash = _check_not_none(certificate_to_add, 'certificate_to_add').sha_hash cert_type = certificate_to_add.cert_type request_body = {'shaHash': sha_hash, 'certType': cert_type} - self._make_request('post', path, app_id, 'App ID', json=request_body) + self._make_request('post', path, json=request_body) def delete_sha_certificate(self, certificate_to_delete): name = _check_not_none(certificate_to_delete, 'certificate_to_delete').name path = '/v1beta1/{0}'.format(name) - self._make_request('delete', path, name, 'SHA ID') + self._make_request('delete', path) + + def _make_request(self, method, url, json=None): + body, _ = self._body_and_response(method, url, json) + return body - def _make_request(self, method, url, resource_identifier, resource_identifier_label, json=None): + def _body_and_response(self, method, url, json=None): try: - return self._client.body(method=method, url=url, json=json, timeout=self._timeout) + return self._client.body_and_response( + method=method, url=url, json=json, timeout=self._timeout) except requests.exceptions.RequestException as error: - raise ApiCallError( - _ProjectManagementService._extract_message( - resource_identifier, resource_identifier_label, error), - error) - - @staticmethod - def _extract_message(identifier, identifier_label, error): - if not isinstance(error, requests.exceptions.RequestException) or error.response is None: - return '{0} "{1}": {2}'.format(identifier_label, identifier, str(error)) - status = error.response.status_code - message = _ProjectManagementService.ERROR_CODES.get(status) - if message: - return '{0} "{1}": {2}'.format(identifier_label, identifier, message) - return '{0} "{1}": Error {2}.'.format(identifier_label, identifier, status) + raise _utils.handle_platform_error_from_requests(error) diff --git a/integration/test_project_management.py b/integration/test_project_management.py index 7386a4837..7aa182a42 100644 --- a/integration/test_project_management.py +++ b/integration/test_project_management.py @@ -20,6 +20,7 @@ import pytest +from firebase_admin import exceptions from firebase_admin import project_management @@ -64,11 +65,12 @@ def ios_app(default_app): def test_create_android_app_already_exists(android_app): del android_app - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.AlreadyExistsError) as excinfo: project_management.create_android_app( package_name=TEST_APP_PACKAGE_NAME, display_name=TEST_APP_DISPLAY_NAME_PREFIX) - assert 'The resource already exists' in str(excinfo.value) - assert excinfo.value.detail is not None + assert 'Requested entity already exists' in str(excinfo.value) + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None def test_android_set_display_name_and_get_metadata(android_app, project_id): @@ -133,10 +135,11 @@ def test_android_sha_certificates(android_app): assert cert.name # Adding the same cert twice should cause an already-exists error. - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.AlreadyExistsError) as excinfo: android_app.add_sha_certificate(project_management.ShaCertificate(SHA_256_HASH_2)) - assert 'The resource already exists' in str(excinfo.value) - assert excinfo.value.detail is not None + assert 'Requested entity already exists' in str(excinfo.value) + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None # Delete all certs and assert that they have all been deleted successfully. for cert in cert_list: @@ -145,20 +148,22 @@ def test_android_sha_certificates(android_app): assert android_app.get_sha_certificates() == [] # Deleting a nonexistent cert should cause a not-found error. - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: android_app.delete_sha_certificate(cert_list[0]) - assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert 'Requested entity was not found' in str(excinfo.value) + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None def test_create_ios_app_already_exists(ios_app): del ios_app - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.AlreadyExistsError) as excinfo: project_management.create_ios_app( bundle_id=TEST_APP_BUNDLE_ID, display_name=TEST_APP_DISPLAY_NAME_PREFIX) - assert 'The resource already exists' in str(excinfo.value) - assert excinfo.value.detail is not None + assert 'Requested entity already exists' in str(excinfo.value) + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None def test_ios_set_display_name_and_get_metadata(ios_app, project_id): diff --git a/tests/test_project_management.py b/tests/test_project_management.py index 9de95f7fd..b139a73c5 100644 --- a/tests/test_project_management.py +++ b/tests/test_project_management.py @@ -20,6 +20,7 @@ import pytest import firebase_admin +from firebase_admin import exceptions from firebase_admin import project_management from tests import testutils @@ -196,6 +197,10 @@ display_name='My iOS App', project_id='test-project-id') +ALREADY_EXISTS_RESPONSE = ('{"error": {"status": "ALREADY_EXISTS", ' + '"message": "The resource already exists"}}') +NOT_FOUND_RESPONSE = '{"error": {"message": "Failed to find the resource"}}' +UNAVAILABLE_RESPONSE = '{"error": {"message": "Backend servers are over capacity"}}' class TestAndroidAppMetadata(object): @@ -578,15 +583,16 @@ def test_create_android_app(self): recorder[2], 'GET', 'https://firebase.googleapis.com/v1/operations/abcdefg') def test_create_android_app_already_exists(self): - recorder = self._instrument_service(statuses=[409], responses=['some error response']) + recorder = self._instrument_service(statuses=[409], responses=[ALREADY_EXISTS_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.AlreadyExistsError) as excinfo: project_management.create_android_app( package_name='com.hello.world.android', display_name='My Android App') assert 'The resource already exists' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_create_android_app_polling_rpc_error(self): @@ -595,16 +601,17 @@ def test_create_android_app_polling_rpc_error(self): responses=[ OPERATION_IN_PROGRESS_RESPONSE, # Request to create Android app asynchronously. OPERATION_IN_PROGRESS_RESPONSE, # Creation operation is still not done. - 'some error response', # Error 503. + UNAVAILABLE_RESPONSE, # Error 503. ]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnavailableError) as excinfo: project_management.create_android_app( package_name='com.hello.world.android', display_name='My Android App') assert 'Backend servers are over capacity' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 3 def test_create_android_app_polling_failure(self): @@ -616,13 +623,14 @@ def test_create_android_app_polling_failure(self): OPERATION_FAILED_RESPONSE, # Operation is finished, but terminated with an error. ]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnknownError) as excinfo: project_management.create_android_app( package_name='com.hello.world.android', display_name='My Android App') assert 'Polling finished, but the operation terminated in an error' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is None + assert excinfo.value.http_response is not None assert len(recorder) == 3 def test_create_android_app_polling_limit_exceeded(self): @@ -635,13 +643,13 @@ def test_create_android_app_polling_limit_exceeded(self): OPERATION_IN_PROGRESS_RESPONSE, # Creation Operation is still not done. ]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.DeadlineExceededError) as excinfo: project_management.create_android_app( package_name='com.hello.world.android', display_name='My Android App') assert 'Polling deadline exceeded' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is None assert len(recorder) == 3 @@ -695,15 +703,16 @@ def test_create_ios_app(self): recorder[2], 'GET', 'https://firebase.googleapis.com/v1/operations/abcdefg') def test_create_ios_app_already_exists(self): - recorder = self._instrument_service(statuses=[409], responses=['some error response']) + recorder = self._instrument_service(statuses=[409], responses=[ALREADY_EXISTS_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.AlreadyExistsError) as excinfo: project_management.create_ios_app( bundle_id='com.hello.world.ios', display_name='My iOS App') assert 'The resource already exists' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_create_ios_app_polling_rpc_error(self): @@ -712,16 +721,17 @@ def test_create_ios_app_polling_rpc_error(self): responses=[ OPERATION_IN_PROGRESS_RESPONSE, # Request to create iOS app asynchronously. OPERATION_IN_PROGRESS_RESPONSE, # Creation operation is still not done. - 'some error response', # Error 503. + UNAVAILABLE_RESPONSE, # Error 503. ]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnavailableError) as excinfo: project_management.create_ios_app( bundle_id='com.hello.world.ios', display_name='My iOS App') assert 'Backend servers are over capacity' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 3 def test_create_ios_app_polling_failure(self): @@ -733,13 +743,14 @@ def test_create_ios_app_polling_failure(self): OPERATION_FAILED_RESPONSE, # Operation is finished, but terminated with an error. ]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnknownError) as excinfo: project_management.create_ios_app( bundle_id='com.hello.world.ios', display_name='My iOS App') assert 'Polling finished, but the operation terminated in an error' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is None + assert excinfo.value.http_response is not None assert len(recorder) == 3 def test_create_ios_app_polling_limit_exceeded(self): @@ -752,13 +763,13 @@ def test_create_ios_app_polling_limit_exceeded(self): OPERATION_IN_PROGRESS_RESPONSE, # Creation Operation is still not done. ]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.DeadlineExceededError) as excinfo: project_management.create_ios_app( bundle_id='com.hello.world.ios', display_name='My iOS App') assert 'Polling deadline exceeded' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is None assert len(recorder) == 3 @@ -779,13 +790,14 @@ def test_list_android_apps(self): self._assert_request_is_correct(recorder[0], 'GET', TestListAndroidApps._LISTING_URL) def test_list_android_apps_rpc_error(self): - recorder = self._instrument_service(statuses=[503], responses=['some error response']) + recorder = self._instrument_service(statuses=[503], responses=[UNAVAILABLE_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnavailableError) as excinfo: project_management.list_android_apps() assert 'Backend servers are over capacity' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_list_android_apps_empty_list(self): @@ -813,13 +825,14 @@ def test_list_android_apps_multiple_pages(self): def test_list_android_apps_multiple_pages_rpc_error(self): recorder = self._instrument_service( statuses=[200, 503], - responses=[LIST_ANDROID_APPS_PAGE_1_RESPONSE, 'some error response']) + responses=[LIST_ANDROID_APPS_PAGE_1_RESPONSE, UNAVAILABLE_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnavailableError) as excinfo: project_management.list_android_apps() assert 'Backend servers are over capacity' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 2 @@ -840,13 +853,14 @@ def test_list_ios_apps(self): self._assert_request_is_correct(recorder[0], 'GET', TestListIosApps._LISTING_URL) def test_list_ios_apps_rpc_error(self): - recorder = self._instrument_service(statuses=[503], responses=['some error response']) + recorder = self._instrument_service(statuses=[503], responses=[UNAVAILABLE_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnavailableError) as excinfo: project_management.list_ios_apps() assert 'Backend servers are over capacity' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_list_ios_apps_empty_list(self): @@ -874,13 +888,14 @@ def test_list_ios_apps_multiple_pages(self): def test_list_ios_apps_multiple_pages_rpc_error(self): recorder = self._instrument_service( statuses=[200, 503], - responses=[LIST_IOS_APPS_PAGE_1_RESPONSE, 'some error response']) + responses=[LIST_IOS_APPS_PAGE_1_RESPONSE, UNAVAILABLE_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnavailableError) as excinfo: project_management.list_ios_apps() assert 'Backend servers are over capacity' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 2 @@ -936,21 +951,24 @@ def test_get_metadata_unknown_error(self, android_app): recorder = self._instrument_service( statuses=[428], responses=['precondition required error']) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnknownError) as excinfo: android_app.get_metadata() - assert 'Error 428' in str(excinfo.value) - assert excinfo.value.detail is not None + message = 'Unexpected HTTP response with status: 428; body: precondition required error' + assert str(excinfo.value) == message + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_get_metadata_not_found(self, android_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: android_app.get_metadata() assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_set_display_name(self, android_app): @@ -965,14 +983,15 @@ def test_set_display_name(self, android_app): recorder[0], 'PATCH', TestAndroidApp._SET_DISPLAY_NAME_URL, body) def test_set_display_name_not_found(self, android_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) new_display_name = 'A new display name!' - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: android_app.set_display_name(new_display_name) assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_get_config(self, android_app): @@ -985,13 +1004,14 @@ def test_get_config(self, android_app): self._assert_request_is_correct(recorder[0], 'GET', TestAndroidApp._GET_CONFIG_URL) def test_get_config_not_found(self, android_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: android_app.get_config() assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_get_sha_certificates(self, android_app): @@ -1005,13 +1025,14 @@ def test_get_sha_certificates(self, android_app): self._assert_request_is_correct(recorder[0], 'GET', TestAndroidApp._LIST_CERTS_URL) def test_get_sha_certificates_not_found(self, android_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: android_app.get_sha_certificates() assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_add_certificate_none_error(self, android_app): @@ -1042,14 +1063,15 @@ def test_add_sha_256_certificate(self, android_app): self._assert_request_is_correct(recorder[0], 'POST', TestAndroidApp._ADD_CERT_URL, body) def test_add_sha_certificates_already_exists(self, android_app): - recorder = self._instrument_service(statuses=[409], responses=['some error response']) + recorder = self._instrument_service(statuses=[409], responses=[ALREADY_EXISTS_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.AlreadyExistsError) as excinfo: android_app.add_sha_certificate( project_management.ShaCertificate('123456789a123456789a123456789a123456789a')) assert 'The resource already exists' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_delete_certificate_none_error(self, android_app): @@ -1075,13 +1097,14 @@ def test_delete_sha_256_certificate(self, android_app): recorder[0], 'DELETE', TestAndroidApp._DELETE_SHA_256_CERT_URL) def test_delete_sha_certificates_not_found(self, android_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: android_app.delete_sha_certificate(SHA_1_CERTIFICATE) assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_raises_if_app_has_no_project_id(self): @@ -1137,21 +1160,24 @@ def test_get_metadata_unknown_error(self, ios_app): recorder = self._instrument_service( statuses=[428], responses=['precondition required error']) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.UnknownError) as excinfo: ios_app.get_metadata() - assert 'Error 428' in str(excinfo.value) - assert excinfo.value.detail is not None + message = 'Unexpected HTTP response with status: 428; body: precondition required error' + assert str(excinfo.value) == message + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_get_metadata_not_found(self, ios_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: ios_app.get_metadata() assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_set_display_name(self, ios_app): @@ -1166,14 +1192,15 @@ def test_set_display_name(self, ios_app): recorder[0], 'PATCH', TestIosApp._SET_DISPLAY_NAME_URL, body) def test_set_display_name_not_found(self, ios_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) new_display_name = 'A new display name!' - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: ios_app.set_display_name(new_display_name) assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_get_config(self, ios_app): @@ -1186,13 +1213,14 @@ def test_get_config(self, ios_app): self._assert_request_is_correct(recorder[0], 'GET', TestIosApp._GET_CONFIG_URL) def test_get_config_not_found(self, ios_app): - recorder = self._instrument_service(statuses=[404], responses=['some error response']) + recorder = self._instrument_service(statuses=[404], responses=[NOT_FOUND_RESPONSE]) - with pytest.raises(project_management.ApiCallError) as excinfo: + with pytest.raises(exceptions.NotFoundError) as excinfo: ios_app.get_config() assert 'Failed to find the resource' in str(excinfo.value) - assert excinfo.value.detail is not None + assert excinfo.value.cause is not None + assert excinfo.value.http_response is not None assert len(recorder) == 1 def test_raises_if_app_has_no_project_id(self):