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')
         ])
示例#2
0
    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'])}
示例#3
0
    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'})
示例#4
0
 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)
示例#5
0
 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)
示例#6
0
 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
示例#7
0
    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',
                ),
            ], )
示例#8
0
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'})
示例#10
0
    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
示例#11
0
    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)}
        }
示例#12
0
    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)
示例#13
0
文件: base.py 项目: guoyu07/pysoa
 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)
示例#14
0
 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
示例#15
0
    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,
        )
示例#16
0
    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
示例#17
0
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,
        )
示例#18
0
    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