def run(self, request): try: # noinspection PyUnresolvedReferences return { 'random': random.randint(request.body['min'], request.body['max']), 'response': function_which_shall_be_mocked(request.body['max'], request.body['min'], **request.body['kwargs']), 'extra': function_which_shall_be_mocked.extra.value(). for_me, # type: ignore } except AttributeError: raise ActionError(errors=[ Error('ATTRIBUTE_ERROR', 'An attribute error was raised') ]) except BytesWarning: raise ActionError( errors=[Error('BYTES_WARNING', 'A bytes warning was raised')]) except ExpectedException: raise ActionError(errors=[ Error('EXPECTED_EXCEPTION', 'An expected exception was raised') ])
def run(self, request): if request.body.get('errors') == 1: raise ActionError([Error('FOO', 'Foo error')]) if request.body.get('errors') == 2: raise ActionError( [Error('BAZ', 'Baz error'), Error('QUX', 'Qux error')]) return {'salutation': 'Hello, {}'.format(request.body['name'])}
def test_two_stubs_with_parallel_calls_and_action_errors_raised( self, stub_test_action_1, stub_test_action_2): stub_test_action_1.side_effect = ActionError( errors=[Error(code='BAD_ACTION', message='You are a bad actor')]) with self.assertRaises(self.client.CallActionError) as error_context: self.client.call_jobs_parallel([ { 'service_name': 'test_service', 'actions': [{ 'action': 'test_action_1', 'body': { 'a': 'b' } }] }, { 'service_name': 'test_service', 'actions': [{ 'action': 'test_action_2', 'body': { 'c': 'd' } }] }, ], ) self.assertEqual( [Error(code='BAD_ACTION', message='You are a bad actor')], error_context.exception.actions[0].errors, ) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'})
def run(self, request): try: return { 'forwarded_response': request.client.call_action('examiner', 'magnify', body=request.body).body } except request.client.CallActionError as e: raise ActionError(errors=e.actions[0].errors)
def run(self, request): try: return { 'one': request.client.call_action('examiner', 'roll', body=request.body['one']).body, 'two': request.client.call_action('player', 'pitch', body=request.body['two']).body, } except request.client.CallActionError as e: raise ActionError(errors=e.actions[0].errors)
def run(self, request): if self.errors: raise ActionError(errors=[ Error( code=e['code'], message=e['message'], field=e.get('field'), ) if not isinstance(e, Error) else e for e in self.errors ]) else: return self.body
def validate(self, request): super().validate(request) try: Category.objects.get(name=request.get('category')) except Category.DoesNotExist: raise ActionError([ Error( message='category not found', field='category', ), ], )
class ProcessJobServer(Server): """ Stub server to test against. """ service_name = 'test_service' action_class_map = { 'respond_actionerror': factories.ActionFactory(exception=ActionError(errors=[Error( code=ERROR_CODE_INVALID, message='This field is invalid.', field='body.field', )])), 'respond_empty': factories.ActionFactory(), }
def test_mock_action_with_error_side_effect_raises_exception(self, stub_test_action_2): stub_test_action_2.side_effect = ActionError(errors=[Error(code='BAR_BAD', field='bar', message='Uh-uh')]) with self.assertRaises(Client.CallActionError) as e: self.client.call_action('test_service', 'test_action_2', {'a': 'body'}) self.assertEqual('BAR_BAD', e.exception.actions[0].errors[0].code) self.assertEqual('bar', e.exception.actions[0].errors[0].field) self.assertEqual('Uh-uh', e.exception.actions[0].errors[0].message) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'a': 'body'}, stub_test_action_2.call_body) stub_test_action_2.assert_called_once_with({'a': 'body'})
def test_call_local_action_standard_request(self): action = mock.MagicMock() action.return_value.side_effect = ActionError( [Error('FOO', 'Foo error')]) server = mock.MagicMock() server.settings = {'a_setting': 'a_value'} server.action_class_map = {'other_foo': action} r = EnrichedActionRequest(action='foo', body={'bar': 'baz'}) r._server = server with pytest.raises(ActionError) as error_context: r.call_local_action('other_foo', {'color': 'red'}) assert error_context.value.errors[0].code == 'FOO' action.assert_called_once_with(server.settings) assert action.return_value.call_count == 1 other_r = action.return_value.call_args[0][0] assert other_r is not r assert other_r != r assert other_r.action == 'other_foo' assert other_r.body == {'color': 'red'} assert other_r.context == {} assert other_r.control == {} assert getattr(other_r, '_server') is server action.reset_mock() response = r.call_local_action('other_foo', {'color': 'red'}, raise_action_errors=False) assert response.action == 'other_foo' assert response.errors assert response.errors[0].code == 'FOO' action.assert_called_once_with(server.settings) assert action.return_value.call_count == 1 other_r = action.return_value.call_args[0][0] assert other_r is not r assert other_r != r assert other_r.action == 'other_foo' assert other_r.body == {'color': 'red'} assert other_r.context == {} assert other_r.control == {} assert getattr(other_r, '_server') is server
def _get_response_for_single_action(self, request_action_name): action_name = request_action_name switch = None if SWITCHED_ACTION_RE.match(action_name): match = SWITCHED_ACTION_RE.match(action_name) action_name = match.group(str('action')) if match.group(str('default')): switch = SwitchedAction.DEFAULT_ACTION else: switch = int(match.group(str('switch'))) if action_name not in self.server.action_class_map and action_name not in ('status', 'introspect'): raise ActionError(errors=[ Error(code=ERROR_CODE_INVALID, message='Action not defined in service', field='action_name'), ]) if action_name in self.server.action_class_map: action_class = self.server.action_class_map[action_name] if issubclass(action_class, SwitchedAction): if switch: if switch == SwitchedAction.DEFAULT_ACTION: action_class = action_class.switch_to_action_map[-1][1] else: for matching_switch, action_class in action_class.switch_to_action_map: if switch == matching_switch: break else: response = { 'action_names': [], 'actions': {} } for sub_name, sub_class in self._iterate_switched_actions(action_name, action_class): response['action_names'].append(sub_name) response['actions'][sub_name] = self._introspect_action(sub_class) response['action_names'] = list(sorted(response['action_names'])) return response elif action_name == 'introspect': action_class = self.__class__ else: action_class = BaseStatusAction return { 'action_names': [request_action_name], 'actions': {request_action_name: self._introspect_action(action_class)} }
def __call__(self, action_request): # type: (EnrichedActionRequest) -> ActionResponse """ Main entry point for actions from the `Server` (or potentially from tests). Validates that the request matches the `request_schema`, then calls `validate()`, then calls `run()` if `validate()` raised no errors, and then validates that the return value from `run()` matches the `response_schema` before returning it in an `ActionResponse`. :param action_request: The request object :return: The response object :raise: ActionError, ResponseValidationError """ # Validate the request if self.request_schema: errors = [ Error( code=error.code, message=error.message, field=error.pointer, is_caller_error=True, ) for error in (self.request_schema.errors(action_request.body) or []) ] if errors: raise ActionError(errors=errors, set_is_caller_error_to=None) # Run any custom validation self.validate(action_request) # Run the body of the action response_body = self.run(action_request) # Validate the response body. Errors in a response are the problem of # the service, and so we just raise a Python exception and let error # middleware catch it. The server will return a SERVER_ERROR response. if self.response_schema: conformity_errors = self.response_schema.errors(response_body) if conformity_errors: raise ResponseValidationError(action=action_request.action, errors=conformity_errors) # Make an ActionResponse and return it if response_body is not None: return ActionResponse( action=action_request.action, body=response_body, ) else: return ActionResponse(action=action_request.action)
def __call__(self, action_request): """ Main entry point for Actions from the Server (or potentially from tests) """ # Validate the request if self.request_schema: errors = [ Error( code=ERROR_CODE_INVALID, message=error.message, field=error.pointer, ) for error in ( self.request_schema.errors(action_request.body) or []) ] if errors: raise ActionError(errors=errors) # Run any custom validation self.validate(action_request) # Run the body of the action response_body = self.run(action_request) # Validate the response body. Errors in a response are the problem of # the service, and so we just raise a Python exception and let error # middleware catch it. The server will return a SERVER_ERROR response. if self.response_schema: errors = self.response_schema.errors(response_body) if errors: raise ResponseValidationError(action=action_request.action, errors=errors) # Make an ActionResponse and return it if response_body is not None: return ActionResponse( action=action_request.action, body=response_body, ) else: return ActionResponse(action=action_request.action)
def run(self, request): if self.errors: raise ActionError(errors=[ e if isinstance(e, Error) else Error(**e) for e in self.errors ]) return self.body
def test_send_receive_one_stub_one_real_call_mixture( self, stub_test_action_1): stub_test_action_1.side_effect = ( ActionResponse(action='does not matter', body={'look': 'menu'}), ActionResponse(action='no', errors=[ Error(code='WEIRD', field='pizza', message='Weird error about pizza') ]), ActionError(errors=[Error(code='COOL', message='Another error')]), ) request_id1 = self.client.send_request( 'test_service', [ { 'action': 'test_action_1', 'body': { 'menu': 'look' } }, { 'action': 'test_action_2' }, { 'action': 'test_action_1', 'body': { 'pizza': 'pepperoni' } }, { 'action': 'test_action_2' }, { 'action': 'test_action_2' }, ], continue_on_error=True, ) request_id2 = self.client.send_request('test_service', [ { 'action': 'test_action_1', 'body': { 'pizza': 'cheese' } }, ]) request_id3 = self.client.send_request('test_service', [ { 'action': 'test_action_2' }, ]) self.assertIsNotNone(request_id1) self.assertIsNotNone(request_id2) self.assertIsNotNone(request_id3) responses = list(self.client.get_all_responses('test_service')) self.assertEqual(3, len(responses)) response_dict = {k: v for k, v in responses} self.assertIn(request_id1, response_dict) self.assertIn(request_id2, response_dict) self.assertIn(request_id3, response_dict) response = response_dict[request_id1] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(5, len(response.actions)) self.assertEqual([], response.actions[0].errors) self.assertEqual({'look': 'menu'}, response.actions[0].body) self.assertEqual([], response.actions[1].errors) self.assertEqual({'value': 0}, response.actions[1].body) self.assertEqual( [ Error(code='WEIRD', field='pizza', message='Weird error about pizza') ], response.actions[2].errors, ) self.assertEqual([], response.actions[3].errors) self.assertEqual({'value': 0}, response.actions[3].body) self.assertEqual([], response.actions[4].errors) self.assertEqual({'value': 0}, response.actions[4].body) response = response_dict[request_id2] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(1, len(response.actions)) self.assertEqual([Error(code='COOL', message='Another error')], response.actions[0].errors) response = response_dict[request_id3] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(1, len(response.actions)) self.assertEqual([], response.actions[0].errors) self.assertEqual({'value': 0}, response.actions[0].body) stub_test_action_1.assert_has_calls( [ mock.call({'menu': 'look'}), mock.call({'pizza': 'pepperoni'}), mock.call({'pizza': 'cheese'}), ], any_order=True, )
def call_local_action(self, action, body, raise_action_errors=True, is_caller_error=False, context=None): # type: (six.text_type, Body, bool, bool, Optional[Context]) -> ActionResponse """ This helper calls another action, locally, that resides on the same service, using the provided action name and body. The called action will receive a copy of this request object with different action and body details. The use of this helper differs significantly from using the PySOA client to call an action. Notably: * The configured transport is not involved, so no socket activity or serialization/deserialization takes place. * PySOA server metrics are not recorded and post-action cleanup activities do not occur. * No "job request" is ever created or transacted. * No middleware is executed around this action (though, in the future, we might change this decision and add middleware execution to this helper). :param action: The action to call (must exist within the `action_class_map` from the `Server` class) :param body: The body to send to the action :param raise_action_errors: If `True` (the default), all action errors will be raised; otherwise, an `ActionResponse` containing the errors will be returned. :param is_caller_error: If `True` (defaults to `False`), raised action errors will be marked as the responsibility of the caller. Action errors are usually the responsibility of the caller, but the default here is the opposite since the responsibility usually lies in the service that is calling itself and should know better. :param context: If specified, any values in this dictionary will override conflicting values in the cloned context. :return: the action response. :raises: ActionError """ server = getattr(self, '_server', None) if not server: # This is never a caller error, because it can only happen due to a bug in PySOA or the service. errors = [ Error( code=ERROR_CODE_SERVER_ERROR, message= "No `_server` attribute set on action request object (and can't make request without it)", is_caller_error=False, ) ] if raise_action_errors: raise ActionError(errors, set_is_caller_error_to=None) return ActionResponse(action=action, errors=errors) if action not in server.action_class_map: # This is never a caller error, because it can only happen due to a bug in the service calling itself. errors = [ Error( code=ERROR_CODE_UNKNOWN, message='The action "{}" was not found on this server.'. format(action), field='action', is_caller_error=False, ) ] if raise_action_errors: raise ActionError(errors, set_is_caller_error_to=None) return ActionResponse(action=action, errors=errors) action_type = server.action_class_map[action] # type: ActionType action_callable = action_type(server.settings) new_context = copy.copy(self.context) if context: new_context.update(context) request = self.__class__( action=action, body=body, context=new_context, # Dynamically copy all Attrs attributes so that subclasses introducing other Attrs can still work properly **{ a.name: getattr(self, a.name) for a in getattr(self, '__attrs_attrs__') if a.name not in ('action', 'body', 'context') }) request._server = server try: response = action_callable(request) except ActionError as e: if raise_action_errors: raise return ActionResponse(action=action, errors=e.errors) if raise_action_errors and response.errors: raise ActionError(response.errors, set_is_caller_error_to=is_caller_error) return response
class TestStubAction(ServerTestCase): server_class = _TestServiceServer server_settings = {} def setUp(self): super(TestStubAction, self).setUp() self.secondary_stub_client = Client(_secondary_stub_client_settings) @stub_action('test_service', 'test_action_1') def test_one_stub_as_decorator(self, stub_test_action_1): stub_test_action_1.return_value = {'value': 1} response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) stub_test_action_1.assert_called_once_with({}) def test_one_stub_as_context_manager(self): with stub_action('test_service', 'test_action_1') as stub_test_action_1: stub_test_action_1.return_value = {'value': 1} response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) stub_test_action_1.assert_called_once_with({}) @stub_action('test_service', 'test_action_1', body={'value': 1}) def test_one_stub_as_decorator_with_body(self, stub_test_action_1): response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) stub_test_action_1.assert_called_once_with({}) def test_one_stub_as_context_manager_with_body(self): with stub_action('test_service', 'test_action_1', body={'value': 1}) as stub_test_action_1: response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) stub_test_action_1.assert_called_once_with({}) @stub_action('test_service', 'test_action_2') @stub_action('test_service', 'test_action_1') def test_two_stubs_same_service_as_decorator(self, stub_test_action_1, stub_test_action_2): stub_test_action_1.return_value = {'value': 1} stub_test_action_2.return_value = {'another_value': 2} response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) response = self.client.call_action('test_service', 'test_action_2', {'input_attribute': True}) self.assertEqual({'another_value': 2}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'input_attribute': True}, stub_test_action_2.call_body) stub_test_action_1.assert_called_once_with({}) stub_test_action_2.assert_called_once_with({'input_attribute': True}) @stub_action('test_service', 'test_action_2') @stub_action('test_service', 'test_action_1') def test_two_stubs_same_service_as_decorator_multiple_calls_to_one(self, stub_test_action_1, stub_test_action_2): stub_test_action_1.return_value = {'value': 1} stub_test_action_2.side_effect = ({'another_value': 2}, {'third_value': 3}) response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) response = self.client.call_action('test_service', 'test_action_2', {'input_attribute': True}) self.assertEqual({'another_value': 2}, response.body) response = self.client.call_action('test_service', 'test_action_2', {'another_attribute': False}) self.assertEqual({'third_value': 3}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) self.assertEqual(2, stub_test_action_2.call_count) self.assertEqual({'another_attribute': False}, stub_test_action_2.call_body) self.assertEqual(({'input_attribute': True}, {'another_attribute': False}), stub_test_action_2.call_bodies) stub_test_action_1.assert_called_once_with({}) stub_test_action_2.assert_has_calls([ mock.call({'input_attribute': True}), mock.call({'another_attribute': False}), ]) @stub_action('test_service', 'test_action_1') def test_two_stubs_same_service_split(self, stub_test_action_1): stub_test_action_1.return_value = {'value': 1} with stub_action('test_service', 'test_action_2') as stub_test_action_2: stub_test_action_2.return_value = {'another_value': 2} response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) response = self.client.call_action('test_service', 'test_action_2', {'input_attribute': True}) self.assertEqual({'another_value': 2}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'input_attribute': True}, stub_test_action_2.call_body) @stub_action('test_another_service', 'test_action_2') @stub_action('test_service', 'test_action_1') def test_two_stubs_different_services_as_decorator(self, stub_test_action_1, stub_test_action_2): stub_test_action_1.return_value = {'value': 1} stub_test_action_2.return_value = {'another_value': 2} response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) response = self.client.call_action('test_another_service', 'test_action_2', {'input_attribute': True}) self.assertEqual({'another_value': 2}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'input_attribute': True}, stub_test_action_2.call_body) @stub_action('test_service', 'test_action_1') def test_two_stubs_different_services_split(self, stub_test_action_1): stub_test_action_1.return_value = {'value': 1} with stub_action('test_another_service', 'test_action_2') as stub_test_action_2: stub_test_action_2.return_value = {'another_value': 2} response = self.client.call_action('test_service', 'test_action_1') self.assertEqual({'value': 1}, response.body) response = self.client.call_action('test_another_service', 'test_action_2', {'input_attribute': True}) self.assertEqual({'another_value': 2}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'input_attribute': True}, stub_test_action_2.call_body) @stub_action('test_service', 'test_action_1', body={'value': 1}) def test_one_stub_as_decorator_with_real_call_handled(self, stub_test_action_1): response = self.client.call_action('test_service', 'test_action_1') self.assertEqual(response.body, {'value': 1}) response = self.secondary_stub_client.call_action('cat', 'meow') self.assertEqual({'type': 'squeak'}, response.body) response = self.secondary_stub_client.call_action('dog', 'bark') self.assertEqual({'sound': 'woof'}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) def test_one_stub_as_context_manager_with_real_call_handled(self): with stub_action('test_service', 'test_action_1', body={'value': 1}) as stub_test_action_1: response = self.client.call_action('test_service', 'test_action_1') self.assertEqual(response.body, {'value': 1}) response = self.secondary_stub_client.call_action('cat', 'meow') self.assertEqual({'type': 'squeak'}, response.body) response = self.secondary_stub_client.call_action('dog', 'bark') self.assertEqual({'sound': 'woof'}, response.body) self.assertEqual(1, stub_test_action_1.call_count) self.assertEqual({}, stub_test_action_1.call_body) @stub_action('test_service', 'test_action_2') @mock.patch(__name__ + '._test_function', return_value=3) def test_as_decorator_with_patch_before(self, mock_randint, stub_test_action_2): stub_test_action_2.return_value = {'value': 99} response = self.client.call_actions( 'test_service', [ActionRequest(action='test_action_1'), ActionRequest(action='test_action_2')], ) self.assertEqual(2, len(response.actions)) self.assertEqual({'value': 3}, response.actions[0].body) self.assertEqual({'value': 99}, response.actions[1].body) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({}, stub_test_action_2.call_body) mock_randint.assert_called_once_with(0, 99) @mock.patch(__name__ + '._test_function', return_value=7) @stub_action('test_service', 'test_action_2') def test_as_decorator_with_patch_after(self, stub_test_action_2, mock_randint): stub_test_action_2.side_effect = ({'value': 122}, {'also': 157}) response = self.client.call_actions( 'test_service', [{'action': 'test_action_1'}, {'action': 'test_action_2'}, {'action': 'test_action_2'}], ) self.assertEqual(3, len(response.actions)) self.assertEqual({'value': 7}, response.actions[0].body) self.assertEqual({'value': 122}, response.actions[1].body) self.assertEqual({'also': 157}, response.actions[2].body) self.assertEqual(2, stub_test_action_2.call_count) self.assertEqual(({}, {}), stub_test_action_2.call_bodies) stub_test_action_2.assert_has_calls([mock.call({}), mock.call({})]) mock_randint.assert_called_once_with(0, 99) def test_using_start_stop(self): action_stubber = stub_action('test_service', 'test_action_1') stubbed_action = action_stubber.start() stubbed_action.return_value = {'what about': 'this'} response = self.client.call_action('test_service', 'test_action_1', {'burton': 'guster', 'sean': 'spencer'}) self.assertEqual({'what about': 'this'}, response.body) self.assertEqual(1, stubbed_action.call_count) self.assertEqual({'burton': 'guster', 'sean': 'spencer'}, stubbed_action.call_body) stubbed_action.assert_called_once_with({'burton': 'guster', 'sean': 'spencer'}) action_stubber.stop() @stub_action('test_service', 'test_action_2', errors=[ {'code': 'BAD_FOO', 'field': 'foo', 'message': 'Nope'}, ]) def test_mock_action_with_error_raises_exception(self, stub_test_action_2): with self.assertRaises(Client.CallActionError) as e: self.client.call_action('test_service', 'test_action_2', {'a': 'body'}) self.assertEqual('BAD_FOO', e.exception.actions[0].errors[0].code) self.assertEqual('foo', e.exception.actions[0].errors[0].field) self.assertEqual('Nope', e.exception.actions[0].errors[0].message) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'a': 'body'}, stub_test_action_2.call_body) stub_test_action_2.assert_called_once_with({'a': 'body'}) @stub_test_action() def test_stub_action_with_side_effect_callback(self, _stub_test_action): response = self.client.call_action('test_service', 'test_action', body={'id': 1, 'type': 'user'}) self.assertEqual(response.body, {'id': 1, 'type': 'user'}) response = self.client.call_action('test_service', 'test_action', body={'id': 2, 'type': 'admin'}) self.assertEqual(response.body, {'id': 2, 'type': 'admin', 'extra': 'data'}) @stub_test_action(add_extra=False) def test_stub_action_with_side_effect_callback_and_param(self, _stub_test_action): response = self.client.call_action('test_service', 'test_action', body={'id': 1, 'type': 'user'}) self.assertEqual(response.body, {'id': 1, 'type': 'user'}) response = self.client.call_action('test_service', 'test_action', body={'id': 2, 'type': 'admin'}) self.assertEqual(response.body, {'id': 2, 'type': 'admin'}) def test_stub_action_with_side_effect_callback_in_context_manager(self): with stub_test_action(): response = self.client.call_action('test_service', 'test_action', body={'id': 1, 'type': 'user'}) self.assertEqual(response.body, {'id': 1, 'type': 'user'}) with stub_test_action(): response = self.client.call_action('test_service', 'test_action', body={'id': 2, 'type': 'admin'}) self.assertEqual(response.body, {'id': 2, 'type': 'admin', 'extra': 'data'}) def test_stub_action_with_side_effect_callback_in_context_manager_and_param(self): with stub_test_action(add_extra=False): response = self.client.call_action('test_service', 'test_action', body={'id': 1, 'type': 'user'}) self.assertEqual(response.body, {'id': 1, 'type': 'user'}) with stub_test_action(add_extra=False): response = self.client.call_action('test_service', 'test_action', body={'id': 2, 'type': 'admin'}) self.assertEqual(response.body, {'id': 2, 'type': 'admin'}) @stub_action( 'test_service', 'test_action_2', side_effect=ActionError(errors=[Error(code='BAR_BAD', field='bar', message='Uh-uh')]), ) def test_stub_action_with_error_side_effect_raises_exception(self, stub_test_action_2): with self.assertRaises(Client.CallActionError) as e: self.client.call_action('test_service', 'test_action_2', {'a': 'body'}) self.assertEqual('BAR_BAD', e.exception.actions[0].errors[0].code) self.assertEqual('bar', e.exception.actions[0].errors[0].field) self.assertEqual('Uh-uh', e.exception.actions[0].errors[0].message) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'a': 'body'}, stub_test_action_2.call_body) stub_test_action_2.assert_called_once_with({'a': 'body'}) @stub_action( 'test_service', 'test_action_2', side_effect=JobError(errors=[Error(code='BAR_BAD_JOB', message='Uh-uh job')]), ) def test_stub_action_with_job_error_side_effect_raises_job_error_exception(self, stub_test_action_2): with self.assertRaises(Client.JobError) as e: self.client.call_action('test_service', 'test_action_2', {'a': 'body'}) self.assertEqual('BAR_BAD_JOB', e.exception.errors[0].code) self.assertIsNone(e.exception.errors[0].field) self.assertEqual('Uh-uh job', e.exception.errors[0].message) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'a': 'body'}, stub_test_action_2.call_body) stub_test_action_2.assert_called_once_with({'a': 'body'}) @stub_action('test_service', 'test_action_2') def test_mock_action_with_error_side_effect_raises_exception(self, stub_test_action_2): stub_test_action_2.side_effect = ActionError(errors=[Error(code='BAR_BAD', field='bar', message='Uh-uh')]) with self.assertRaises(Client.CallActionError) as e: self.client.call_action('test_service', 'test_action_2', {'a': 'body'}) self.assertEqual('BAR_BAD', e.exception.actions[0].errors[0].code) self.assertEqual('bar', e.exception.actions[0].errors[0].field) self.assertEqual('Uh-uh', e.exception.actions[0].errors[0].message) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'a': 'body'}, stub_test_action_2.call_body) stub_test_action_2.assert_called_once_with({'a': 'body'}) @stub_action('test_service', 'test_action_2') def test_mock_action_with_job_error_side_effect_raises_job_error_exception(self, stub_test_action_2): stub_test_action_2.side_effect = JobError(errors=[Error(code='BAR_BAD_JOB', message='Uh-uh job')]) with self.assertRaises(Client.JobError) as e: self.client.call_action('test_service', 'test_action_2', {'a': 'body'}) self.assertEqual('BAR_BAD_JOB', e.exception.errors[0].code) self.assertIsNone(e.exception.errors[0].field) self.assertEqual('Uh-uh job', e.exception.errors[0].message) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'a': 'body'}, stub_test_action_2.call_body) stub_test_action_2.assert_called_once_with({'a': 'body'}) @stub_action('test_service', 'test_action_2') def test_mock_action_with_job_error_response_raises_job_error_exception(self, stub_test_action_2): stub_test_action_2.return_value = JobResponse(errors=[Error(code='BAR_BAD_JOB', message='Uh-uh job')]) with self.assertRaises(Client.JobError) as e: self.client.call_action('test_service', 'test_action_2', {'a': 'body'}) self.assertEqual('BAR_BAD_JOB', e.exception.errors[0].code) self.assertIsNone(e.exception.errors[0].field) self.assertEqual('Uh-uh job', e.exception.errors[0].message) self.assertEqual(1, stub_test_action_2.call_count) self.assertEqual({'a': 'body'}, stub_test_action_2.call_body) stub_test_action_2.assert_called_once_with({'a': 'body'}) @stub_action('test_service', 'test_action_2', errors=[ {'code': 'INVALID_BAR', 'message': 'A bad message'}, ]) def test_multiple_actions_stop_on_error(self, stub_test_action_2): response = self.client.call_actions( 'test_service', [ ActionRequest(action='test_action_1'), ActionRequest(action='test_action_2'), ActionRequest(action='test_action_1'), ], raise_action_errors=False, ) # Called 3 actions, but expected to stop after the error in the second action self.assertEqual(2, len(response.actions)) self.assertEqual('INVALID_BAR', response.actions[1].errors[0].code) self.assertEqual('A bad message', response.actions[1].errors[0].message) self.assertTrue(stub_test_action_2.called) @stub_action('test_service', 'test_action_2', errors=[ {'code': 'MISSING_BAZ', 'field': 'entity_id', 'message': 'Your entity ID was missing'}, ]) def test_multiple_actions_continue_on_error(self, mock_test_action_2): response = self.client.call_actions( 'test_service', [{'action': 'test_action_1'}, {'action': 'test_action_2'}, {'action': 'test_action_1'}], raise_action_errors=False, continue_on_error=True, ) # Called 3 actions, and expected all three of them to be called, even with the interrupting error self.assertEqual(3, len(response.actions)) self.assertEqual('MISSING_BAZ', response.actions[1].errors[0].code) self.assertEqual('entity_id', response.actions[1].errors[0].field) self.assertEqual('Your entity ID was missing', response.actions[1].errors[0].message) self.assertTrue(mock_test_action_2.called) @stub_action('test_service', 'test_action_2', body={'three': 'four'}) @stub_action('test_service', 'test_action_1', body={'one': 'two'}) def test_two_stubs_with_parallel_calls_all_stubbed(self, stub_test_action_1, stub_test_action_2): job_responses = self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], ) self.assertIsNotNone(job_responses) self.assertEqual(2, len(job_responses)) self.assertEqual(1, len(job_responses[0].actions)) self.assertEqual({'one': 'two'}, job_responses[0].actions[0].body) self.assertEqual(1, len(job_responses[1].actions)) self.assertEqual({'three': 'four'}, job_responses[1].actions[0].body) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'}) @stub_action('test_service', 'test_action_2') @mock.patch(__name__ + '._test_function') def test_one_stub_with_parallel_calls(self, mock_randint, stub_test_action_2): mock_randint.side_effect = (42, 17, 31) stub_test_action_2.return_value = {'concert': 'tickets'} job_responses = self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1'}]}, {'service_name': 'test_service', 'actions': [ {'action': 'test_action_2', 'body': {'slide': 'rule'}}, {'action': 'test_action_1'}, ]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_1'}]}, ], ) self.assertIsNotNone(job_responses) self.assertEqual(3, len(job_responses)) self.assertEqual(1, len(job_responses[0].actions)) self.assertEqual({'value': 42}, job_responses[0].actions[0].body) self.assertEqual(2, len(job_responses[1].actions)) self.assertEqual({'concert': 'tickets'}, job_responses[1].actions[0].body) self.assertEqual({'value': 17}, job_responses[1].actions[1].body) self.assertEqual(1, len(job_responses[2].actions)) self.assertEqual({'value': 31}, job_responses[2].actions[0].body) stub_test_action_2.assert_called_once_with({'slide': 'rule'}) @stub_action('test_service', 'test_action_2') @stub_action('test_service', 'test_action_1') def test_two_stubs_with_parallel_calls(self, stub_test_action_1, stub_test_action_2): stub_test_action_1.return_value = {'value': 1} stub_test_action_2.return_value = {'another_value': 2} job_responses = Client(dict(self.client.config, **_secondary_stub_client_settings)).call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [ {'action': 'test_action_1', 'body': {'input_attribute': True}}, {'action': 'test_action_2', 'body': {'another_variable': 'Cool'}}, ]}, {'service_name': 'cat', 'actions': [{'action': 'meow'}]}, {'service_name': 'dog', 'actions': [{'action': 'bark'}]}, {'service_name': 'test_service', 'actions': [{'action': 'does_not_exist'}]}, ], raise_action_errors=False, ) self.assertIsNotNone(job_responses) self.assertEqual(4, len(job_responses)) self.assertEqual(2, len(job_responses[0].actions)) self.assertEqual({'value': 1}, job_responses[0].actions[0].body) self.assertEqual({'another_value': 2}, job_responses[0].actions[1].body) self.assertEqual(1, len(job_responses[1].actions)) self.assertEqual({'type': 'squeak'}, job_responses[1].actions[0].body) self.assertEqual(1, len(job_responses[2].actions)) self.assertEqual({'sound': 'woof'}, job_responses[2].actions[0].body) self.assertEqual(1, len(job_responses[3].actions)) self.assertEqual( [Error( code='UNKNOWN', message='The action "does_not_exist" was not found on this server.', field='action', )], job_responses[3].actions[0].errors ) stub_test_action_1.assert_called_once_with({'input_attribute': True}) stub_test_action_2.assert_called_once_with({'another_variable': 'Cool'}) @stub_action('test_service', 'test_action_2', body={'three': 'four'}) @stub_action('test_service', 'test_action_1') def test_two_stubs_with_parallel_calls_and_job_response_errors_raised(self, stub_test_action_1, stub_test_action_2): stub_test_action_1.return_value = JobResponse(errors=[Error(code='BAD_JOB', message='You are a bad job')]) with self.assertRaises(self.client.JobError) as error_context: self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], ) self.assertEqual([Error(code='BAD_JOB', message='You are a bad job')], error_context.exception.errors) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'}) @stub_action('test_service', 'test_action_2', body={'three': 'four'}) @stub_action( 'test_service', 'test_action_1', side_effect=JobError(errors=[Error(code='BAD_JOB', message='You are a bad job')]), ) def test_stub_action_with_two_stubs_with_parallel_calls_and_job_errors_not_raised( self, stub_test_action_1, stub_test_action_2, ): job_responses = self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], raise_job_errors=False, ) self.assertIsNotNone(job_responses) self.assertEqual(2, len(job_responses)) self.assertEqual(0, len(job_responses[0].actions)) self.assertEqual([Error(code='BAD_JOB', message='You are a bad job')], job_responses[0].errors) self.assertEqual(1, len(job_responses[1].actions)) self.assertEqual({'three': 'four'}, job_responses[1].actions[0].body) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'}) @stub_action('test_service', 'test_action_2', body={'three': 'four'}) @stub_action( 'test_service', 'test_action_1', side_effect=ActionError(errors=[Error(code='BAD_ACTION', message='You are a bad actor')]), ) def test_stub_action_with_two_stubs_with_parallel_calls_and_action_errors_raised( self, stub_test_action_1, stub_test_action_2, ): with self.assertRaises(self.client.CallActionError) as error_context: self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], ) self.assertEqual( [Error(code='BAD_ACTION', message='You are a bad actor')], error_context.exception.actions[0].errors, ) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'}) @stub_action('test_service', 'test_action_2', body={'three': 'four'}) @stub_action('test_service', 'test_action_1') def test_two_stubs_with_parallel_calls_and_job_errors_not_raised(self, stub_test_action_1, stub_test_action_2): stub_test_action_1.side_effect = JobError(errors=[Error(code='BAD_JOB', message='You are a bad job')]) job_responses = self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], raise_job_errors=False, ) self.assertIsNotNone(job_responses) self.assertEqual(2, len(job_responses)) self.assertEqual(0, len(job_responses[0].actions)) self.assertEqual([Error(code='BAD_JOB', message='You are a bad job')], job_responses[0].errors) self.assertEqual(1, len(job_responses[1].actions)) self.assertEqual({'three': 'four'}, job_responses[1].actions[0].body) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'}) @stub_action('test_service', 'test_action_2', body={'three': 'four'}) @stub_action('test_service', 'test_action_1') def test_two_stubs_with_parallel_calls_and_action_errors_raised(self, stub_test_action_1, stub_test_action_2): stub_test_action_1.side_effect = ActionError(errors=[Error(code='BAD_ACTION', message='You are a bad actor')]) with self.assertRaises(self.client.CallActionError) as error_context: self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], ) self.assertEqual( [Error(code='BAD_ACTION', message='You are a bad actor')], error_context.exception.actions[0].errors, ) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'}) @stub_action('test_service', 'test_action_2', body={'three': 'four'}) @stub_action('test_service', 'test_action_1') def test_two_stubs_with_parallel_calls_and_action_response_errors_not_raised( self, stub_test_action_1, stub_test_action_2, ): stub_test_action_1.return_value = ActionResponse( action='test_action_1', errors=[Error(code='BAD_ACTION', message='You are a bad actor')], ) job_responses = self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], raise_action_errors=False, ) self.assertIsNotNone(job_responses) self.assertEqual(2, len(job_responses)) self.assertEqual(1, len(job_responses[0].actions)) self.assertEqual([Error(code='BAD_ACTION', message='You are a bad actor')], job_responses[0].actions[0].errors) self.assertEqual(1, len(job_responses[1].actions)) self.assertEqual({'three': 'four'}, job_responses[1].actions[0].body) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'}) @stub_action('test_service', 'test_action_1', body={'food': 'chicken'}) def test_send_receive_one_stub_simple(self, stub_test_action_1): request_id = self.client.send_request('test_service', [{'action': 'test_action_1', 'body': {'menu': 'look'}}]) self.assertIsNotNone(request_id) responses = list(self.client.get_all_responses('test_service')) self.assertEqual(1, len(responses)) received_request_id, response = responses[0] self.assertEqual(request_id, received_request_id) self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(1, len(response.actions)) self.assertEqual([], response.actions[0].errors) self.assertEqual({'food': 'chicken'}, response.actions[0].body) stub_test_action_1.assert_called_once_with({'menu': 'look'}) @stub_action('test_service', 'test_action_1') def test_send_receive_one_stub_multiple_calls(self, stub_test_action_1): stub_test_action_1.side_effect = ({'look': 'menu'}, {'pepperoni': 'pizza'}, {'cheese': 'pizza'}) request_id1 = self.client.send_request( 'test_service', [ {'action': 'test_action_1', 'body': {'menu': 'look'}}, {'action': 'test_action_1', 'body': {'pizza': 'pepperoni'}}, ] ) request_id2 = self.client.send_request( 'test_service', [ {'action': 'test_action_1', 'body': {'pizza': 'cheese'}}, ] ) self.assertIsNotNone(request_id1) self.assertIsNotNone(request_id2) responses = list(self.client.get_all_responses('test_service')) self.assertEqual(2, len(responses)) response_dict = {k: v for k, v in responses} self.assertIn(request_id1, response_dict) self.assertIn(request_id2, response_dict) response = response_dict[request_id1] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(2, len(response.actions)) self.assertEqual([], response.actions[0].errors) self.assertEqual({'look': 'menu'}, response.actions[0].body) self.assertEqual({'pepperoni': 'pizza'}, response.actions[1].body) response = response_dict[request_id2] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(1, len(response.actions)) self.assertEqual([], response.actions[0].errors) self.assertEqual({'cheese': 'pizza'}, response.actions[0].body) stub_test_action_1.assert_has_calls( [ mock.call({'menu': 'look'}), mock.call({'pizza': 'pepperoni'}), mock.call({'pizza': 'cheese'}), ], any_order=True, ) @stub_action('test_service', 'test_action_1') def test_send_receive_one_stub_one_real_call_mixture(self, stub_test_action_1): stub_test_action_1.side_effect = ( ActionResponse(action='does not matter', body={'look': 'menu'}), ActionResponse(action='no', errors=[Error(code='WEIRD', field='pizza', message='Weird error about pizza')]), ActionError(errors=[Error(code='COOL', message='Another error')]), ) request_id1 = self.client.send_request( 'test_service', [ {'action': 'test_action_1', 'body': {'menu': 'look'}}, {'action': 'test_action_2'}, {'action': 'test_action_1', 'body': {'pizza': 'pepperoni'}}, {'action': 'test_action_2'}, {'action': 'test_action_2'}, ], continue_on_error=True, ) request_id2 = self.client.send_request( 'test_service', [ {'action': 'test_action_1', 'body': {'pizza': 'cheese'}}, ] ) request_id3 = self.client.send_request( 'test_service', [ {'action': 'test_action_2'}, ] ) self.assertIsNotNone(request_id1) self.assertIsNotNone(request_id2) self.assertIsNotNone(request_id3) responses = list(self.client.get_all_responses('test_service')) self.assertEqual(3, len(responses)) response_dict = {k: v for k, v in responses} self.assertIn(request_id1, response_dict) self.assertIn(request_id2, response_dict) self.assertIn(request_id3, response_dict) response = response_dict[request_id1] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(5, len(response.actions)) self.assertEqual([], response.actions[0].errors) self.assertEqual({'look': 'menu'}, response.actions[0].body) self.assertEqual([], response.actions[1].errors) self.assertEqual({'value': 0}, response.actions[1].body) self.assertEqual( [Error(code='WEIRD', field='pizza', message='Weird error about pizza')], response.actions[2].errors, ) self.assertEqual([], response.actions[3].errors) self.assertEqual({'value': 0}, response.actions[3].body) self.assertEqual([], response.actions[4].errors) self.assertEqual({'value': 0}, response.actions[4].body) response = response_dict[request_id2] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(1, len(response.actions)) self.assertEqual([Error(code='COOL', message='Another error')], response.actions[0].errors) response = response_dict[request_id3] self.assertIsNotNone(response) self.assertEqual([], response.errors) self.assertEqual(1, len(response.actions)) self.assertEqual([], response.actions[0].errors) self.assertEqual({'value': 0}, response.actions[0].body) stub_test_action_1.assert_has_calls( [ mock.call({'menu': 'look'}), mock.call({'pizza': 'pepperoni'}), mock.call({'pizza': 'cheese'}), ], any_order=True, )
def call_local_action(self, action, body, raise_action_errors=True): # type: (six.text_type, Dict, bool) -> ActionResponse """ This helper calls another action, locally, that resides on the same service, using the provided action name and body. The called action will receive a copy of this request object with different action and body details. The use of this helper differs significantly from using the PySOA client to call an action. Notably: * The configured transport is not involved, so no socket activity or serialization/deserialization takes place. * PySOA server metrics are not recorded and post-action cleanup activities do not occur. * No "job request" is ever created or transacted. * No middleware is executed around this action (though, in the future, we might change this decision and add middleware execution to this helper). :param action: The action to call (must exist within the `action_class_map` from the `Server` class) :type action: union[str, unicode] :param body: The body to send to the action :type body: dict :param raise_action_errors: If `True` (the default), all action errors will be raised; otherwise, an `ActionResponse` containing the errors will be returned. :type raise_action_errors: bool :return: the action response. :rtype: ActionResponse :raises: ActionError """ server = getattr(self, '_server', None) if not server: errors = [ Error( code=ERROR_CODE_SERVER_ERROR, message= "No `_server` attribute set on action request object (and can't make request without it)", ) ] if raise_action_errors: raise ActionError(errors) return ActionResponse(action=action, errors=errors) if action not in server.action_class_map: errors = [ Error( code=ERROR_CODE_UNKNOWN, message='The action "{}" was not found on this server.'. format(action), field='action', ) ] if raise_action_errors: raise ActionError(errors) return ActionResponse(action=action, errors=errors) action_callable = (server.action_class_map[action](server.settings) ) # type: Callable[[ActionRequest], ActionResponse] request = self.__class__( action=action, body=body, # Dynamically copy all Attrs attributes so that subclasses introducing other Attrs can still work properly **{ a.name: getattr(self, a.name) for a in self.__attrs_attrs__ if a.name not in ('action', 'body') }) request._server = server try: response = action_callable(request) except ActionError as e: if raise_action_errors: raise return ActionResponse(action=action, errors=e.errors) if raise_action_errors and response.errors: raise ActionError(response.errors) return response