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
17 changes: 16 additions & 1 deletion cterasdk/exceptions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,27 @@ class CTERAException(Exception):
"""
Base Exception.

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

@property
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)

def __repr__(self):
return str(self)
11 changes: 11 additions & 0 deletions docs/source/UserGuides/Miscellaneous/Changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

2.20.50
-------

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).

Related issue: `SDK-369 <https://cteranet.atlassian.net/browse/SDK-369>`_

2.20.49
-------

Expand Down
3 changes: 3 additions & 0 deletions docs/source/UserGuides/Miscellaneous/Exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ 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: 80 additions & 0 deletions tests/ut/core/admin/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,86 @@ 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
49 changes: 49 additions & 0 deletions tests/ut/exceptions/test_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from cterasdk.common import Object
from cterasdk.exceptions import CTERAException
from cterasdk.exceptions.transport import InternalServerError
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)