Skip to content
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
18 changes: 6 additions & 12 deletions cterasdk/exceptions/base.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
from abc import abstractmethod


class CTERAException(Exception):
"""
Base Exception.

:param str message: Error message
:ivar str reason: Portal error message from :attr:`~BaseException.__cause__` when it is an HTTP transport error
:ivar str reason: Server error message
"""
def __init__(self, message=None, **kwargs):
super().__init__(message)
for k, v in kwargs.items():
setattr(self, k, v)

@property
@abstractmethod
def reason(self):
cause = self.__cause__
if cause is None:
return None
error_object = getattr(cause, 'error', None)
if error_object is None:
return None
response = getattr(error_object, 'response', None)
portal_error = getattr(response, 'error', None) if response else None
if portal_error is None or isinstance(portal_error, str):
return None
return getattr(portal_error, 'msg', None)
return self.__cause__.reason if self.__cause__ and hasattr(self.__cause__, 'reason') else None

def __repr__(self):
return str(self)
4 changes: 4 additions & 0 deletions cterasdk/exceptions/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def __init__(self, status, error):
self.name = status.name
self.error = error

@property
def reason(self):
return getattr(self.error.response.error, 'msg', None)


class BadRequest(HTTPError):

Expand Down
5 changes: 2 additions & 3 deletions docs/source/UserGuides/Miscellaneous/Changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ Changelog
Bug Fixes
^^^^^^^^^

* Expose portal validation details via :attr:`~cterasdk.exceptions.CTERAException.reason` (for
example, when ``admins.modify`` fails because the new password matches the current password).
* Add a ``reason`` property to :py:class:`~cterasdk.exceptions.base.CTERAException`.

Related issue: `SDK-369 <https://cteranet.atlassian.net/browse/SDK-369>`_
Related issues and pull requests on GitHub: `#374 <https://github.com/ctera/ctera-python-sdk/pull/374>`_

2.20.49
-------
Expand Down
3 changes: 0 additions & 3 deletions docs/source/UserGuides/Miscellaneous/Exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ Exceptions
:members:
:show-inheritance:

:attr:`~cterasdk.exceptions.CTERAException.reason` returns the portal ``msg`` from
:attr:`~BaseException.__cause__` when the cause is an HTTP transport error.

.. autoclass:: cterasdk.exceptions.ObjectNotFoundException
:noindex:
:members:
Expand Down
80 changes: 0 additions & 80 deletions tests/ut/core/admin/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,86 +319,6 @@ def test_modify_admin_user(self):
actual_param = self._global_admin.api.put.call_args[0][1]
self._assert_equal_objects(actual_param, new_user_object)

def test_modify_admin_user_same_password_exposes_portal_reason(self):
current_user_object = self._get_admin_object(
name=self._username,
email=self._email,
firstName=self._first_name,
lastName=self._last_name,
password=self._password,
role=self._role,
company=None,
comment=None
)
portal_msg = 'Object validation failed (field: password error: Old and new passwords cannot be the same)'
password_value = 'Str0ngP@ssword!1'
portal_error = Object()
portal_error.msg = portal_msg
field = Object()
field.name = 'password'
field.desc = 'Old and new passwords cannot be the same'
field.val = password_value
portal_error.fields = [field]
http_error = Object()
http_error.request = Object(url='/administrators/' + self._username)
http_error.response = Object(status=500, error=portal_error)
self._init_global_admin(get_response=current_user_object)
self._global_admin.api.put = mock.MagicMock(side_effect=exceptions.transport.InternalServerError(http_error))
with self.assertRaises(exceptions.CTERAException) as error:
admins.Administrators(self._global_admin).modify(self._username, password=self._password)
ref = f'/administrators/{self._username}'
self._global_admin.api.get.assert_called_once_with(ref)
self._global_admin.api.put.assert_called_once_with(ref, mock.ANY)
self.assertEqual(f'Could not modify user: {ref}', str(error.exception))
self.assertIn('Old and new passwords cannot be the same', error.exception.reason)
self.assertNotIn(password_value, error.exception.reason)

def test_modify_admin_user_http_error_without_portal_msg_uses_generic_message(self):
current_user_object = self._get_admin_object(
name=self._username,
email=self._email,
firstName=self._first_name,
lastName=self._last_name,
password=self._password,
role=self._role,
company=None,
comment=None
)
portal_error = Object()
http_error = Object()
http_error.request = Object(url='/administrators/' + self._username)
http_error.response = Object(status=500, error=portal_error)
self._init_global_admin(get_response=current_user_object)
self._global_admin.api.put = mock.MagicMock(side_effect=exceptions.transport.InternalServerError(http_error))
ref = f'/administrators/{self._username}'
expected_message = f'Could not modify user: {ref}'
with self.assertRaises(exceptions.CTERAException) as error:
admins.Administrators(self._global_admin).modify(self._username, password=self._password)
self.assertEqual(expected_message, str(error.exception))
self.assertIsNone(error.exception.reason)

def test_modify_admin_user_non_http_error_uses_generic_message(self):
current_user_object = self._get_admin_object(
name=self._username,
email=self._email,
firstName=self._first_name,
lastName=self._last_name,
password=self._password,
role=self._role,
company=None,
comment=None
)
self._init_global_admin(get_response=current_user_object)
self._global_admin.api.put = mock.MagicMock(
side_effect=exceptions.ObjectNotFoundException('Object not found')
)
ref = f'/administrators/{self._username}'
expected_message = f'Could not modify user: {ref}'
with self.assertRaises(exceptions.CTERAException) as error:
admins.Administrators(self._global_admin).modify(self._username, password=self._password)
self.assertEqual(expected_message, str(error.exception))
self.assertIsNone(error.exception.reason)

def test_delete_admin_user(self):
execute_response = 'Success'
self._init_global_admin(execute_response=execute_response)
Expand Down
65 changes: 19 additions & 46 deletions tests/ut/exceptions/test_base.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,22 @@
from cterasdk.common import Object
from cterasdk.exceptions import CTERAException
from cterasdk.exceptions.transport import InternalServerError
import munch
from unittest import mock
from cterasdk import exceptions
from cterasdk.core import admins
from tests.ut import base


class TestCTERAExceptionReason(base.BaseTest):

@staticmethod
def _internal_server_error(portal_error):
http_error = Object()
http_error.request = Object(url='https://example/admin/api/administrators/alice')
http_error.response = Object(status=500, error=portal_error)
return InternalServerError(http_error)

def test_reason_returns_portal_msg_from_http_cause(self):
portal_msg = 'Object validation failed (field: password error: Old and new passwords cannot be the same)'
portal_error = Object()
portal_error.msg = portal_msg
try:
raise CTERAException('Could not modify user: /administrators/alice') from self._internal_server_error(
portal_error
)
except CTERAException as error:
self.assertEqual(portal_msg, error.reason)

def test_reason_is_none_without_cause(self):
self.assertIsNone(CTERAException('Failed').reason)

def test_reason_is_none_when_cause_is_not_http_transport_error(self):
try:
raise CTERAException('Failed') from ValueError('not transport')
except CTERAException as error:
self.assertIsNone(error.reason)

def test_reason_is_none_when_portal_error_has_no_msg(self):
try:
raise CTERAException('Failed') from self._internal_server_error(Object())
except CTERAException as error:
self.assertIsNone(error.reason)

def test_reason_is_none_when_response_error_is_plain_string(self):
http_error = Object()
http_error.request = Object(url='https://example/admin')
http_error.response = Object(status=500, error='Internal Server Error')
try:
raise CTERAException('Failed') from InternalServerError(http_error)
except CTERAException as error:
self.assertIsNone(error.reason)
class TestExceptions(base.BaseTest):

def test_reason(self):
error_message = 'error message'
error = munch.Munch({
'response': munch.Munch({
'error': munch.Munch({
'msg': error_message
})
})
})
self._global_admin.api.get = mock.MagicMock(side_effect=exceptions.transport.InternalServerError(error))
with self.assertRaises(exceptions.CTERAException) as error:
admins.Administrators(self._global_admin).modify('user', password='password')
self.assertEqual(error_message, error.exception.reason)
Loading