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'})
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 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'})
def test_check_client_settings_with_settings(self): client = Client({ 'foo': {'transport': {'path': 'pysoa.test.stub_service:StubClientTransport'}}, 'bar': {'transport': {'path': 'pysoa.test.stub_service:StubClientTransport'}}, 'baz': {'transport': {'path': 'pysoa.test.stub_service:StubClientTransport'}}, 'qux': {'transport': {'path': 'pysoa.test.stub_service:StubClientTransport'}}, }) action_request = EnrichedActionRequest(action='status', body={}, switches=None, client=client) baz_body = { 'conformity': '1.2.3', 'pysoa': '1.0.2', 'python': '3.7.4', 'version': '9.7.8', } with stub_action('foo', 'status') as foo_stub,\ stub_action('bar', 'status', errors=[Error('BAR_ERROR', 'Bar error')]),\ stub_action('baz', 'status', body=baz_body),\ stub_action('qux', 'status') as qux_stub: foo_stub.return_value = JobResponse(errors=[Error('FOO_ERROR', 'Foo error')]) qux_stub.side_effect = MessageReceiveTimeout('Timeout calling qux') response = _CheckOtherServicesAction()(action_request) self.assertIsInstance(response, ActionResponse) self.assertEqual(six.text_type(conformity.__version__), response.body['conformity']) self.assertEqual(six.text_type(pysoa.__version__), response.body['pysoa']) self.assertEqual(six.text_type(platform.python_version()), response.body['python']) self.assertEqual('8.71.2', response.body['version']) self.assertIn('healthcheck', response.body) self.assertEqual([], response.body['healthcheck']['warnings']) self.assertIn( ('FOO_CALL_ERROR', six.text_type([Error('FOO_ERROR', 'Foo error')])), response.body['healthcheck']['errors'], ) self.assertIn( ('BAR_STATUS_ERROR', six.text_type([Error('BAR_ERROR', 'Bar error')])), response.body['healthcheck']['errors'], ) self.assertIn( ('QUX_TRANSPORT_ERROR', 'Timeout calling qux'), response.body['healthcheck']['errors'], ) self.assertEqual(3, len(response.body['healthcheck']['errors'])) self.assertEqual( { 'services': { 'baz': { 'conformity': '1.2.3', 'pysoa': '1.0.2', 'python': '3.7.4', 'version': '9.7.8', }, }, }, response.body['healthcheck']['diagnostics'], )
def test_assert_not_wanted_errors_mismatch_field(self): assertions.assert_actual_list_not_subset( [ Error(code='FOO', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), Error(code='BAR', message=AnyValue('str'), field='bar_field'), ], [ Error(code='BAR', message='Bar message', field=None), ], )
def test_assert_any_wanted_error_match_with_field(self): assertions.assert_expected_list_subset_of_actual( [ Error(code='FOO', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), ], [ Error(code='FOO', message='Foo message', field='foo_field'), Error(code='BAR', message='Bar message', field=None), ], )
def test_assert_all_wanted_errors_mismatch_empty_list_other_way(self): with self.assertRaises(AssertionError): assertions.assert_lists_match_any_order( [], [ Error(code='FOO', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), ], )
def test_assert_any_wanted_error_mismatch_empty_actual_list(self): with self.assertRaises(AssertionError): assertions.assert_expected_list_subset_of_actual( [ Error(code='FOO', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), ], [], )
def test_assert_all_wanted_errors_match_different_order(self): assertions.assert_lists_match_any_order( [ Error(code='FOO', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), ], [ Error(code='BAR', message='Bar message', field=None), Error(code='FOO', message='Foo message', field='foo_field'), ], )
def test_call_actions_parallel_with_extras(self): """ Test that call_actions_parallel works to call multiple actions run parallel on a single service using extra kwargs to more finely control behavior. """ action_responses = self.client.call_actions_parallel( 'service_2', [ ActionRequest(action='action_3'), ActionRequest(action='action_with_errors'), ActionRequest(action='action_4'), ], timeout=2, raise_action_errors=False, continue_on_error=True, ) self.assertIsNotNone(action_responses) action_responses = list(action_responses) self.assertEqual(3, len(action_responses)) self.assertEqual({'cat': 'dog'}, action_responses[0].body) self.assertEqual({}, action_responses[1].body) self.assertEqual( [Error(code=ERROR_CODE_INVALID, message='Invalid input', field='foo')], action_responses[1].errors, ) self.assertEqual({'selected': True, 'count': 7}, action_responses[2].body)
def test_raises_error_codes_missing(codes): errors = [Error(code=code, message='bam') for code in codes] with pytest.raises(pytest.raises.Exception): with raises_error_codes(['AUTH_MISSING']): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] )
def test_call_jobs_parallel_job_errors_not_raised(self): """ Test that call_jobs_parallel returns job errors instead of raising them when asked. """ job_responses = self.client.call_jobs_parallel( [ {'service_name': 'service_1', 'actions': [{'action': 'action_1'}, {'action': 'action_2'}]}, {'service_name': 'error_service', 'actions': [{'action': 'job_error'}]}, {'service_name': 'service_2', 'actions': [{'action': 'action_3'}]}, {'service_name': 'service_2', 'actions': [{'action': 'action_4'}]}, ], raise_job_errors=False, ) self.assertIsNotNone(job_responses) self.assertEqual(4, len(job_responses)) self.assertEqual(2, len(job_responses[0].actions)) self.assertEqual({'foo': 'bar'}, job_responses[0].actions[0].body) self.assertEqual({'baz': 3}, job_responses[0].actions[1].body) self.assertEqual(0, len(job_responses[1].actions)) self.assertEqual([Error(code='BAD_JOB', message='You are a bad job')], job_responses[1].errors) self.assertEqual(1, len(job_responses[2].actions)) self.assertEqual({'cat': 'dog'}, job_responses[2].actions[0].body) self.assertEqual(1, len(job_responses[3].actions)) self.assertEqual({'selected': True, 'count': 7}, job_responses[3].actions[0].body)
def test_call_actions_raises_exception_on_action_error(self): """Client.call_actions raises CallActionError when any action response is an error.""" action_request = [ { 'action': 'action_1', 'body': {'foo': 'bar'}, }, { 'action': 'action_2', 'body': {}, }, ] error_expected = [ Error( code=ERROR_CODE_INVALID, message='Invalid input', field='foo', ) ] self.client_settings[SERVICE_NAME]['transport']['kwargs']['action_map']['action_1'] = {'errors': error_expected} client = Client(self.client_settings) for actions in (action_request, [ActionRequest(**a) for a in action_request]): with self.assertRaises(Client.CallActionError) as e: client.call_actions(SERVICE_NAME, actions) self.assertEqual(len(e.value.actions), 1) self.assertEqual(e.value.actions[0].action, 'action_1') error_response = e.value.actions[0].errors self.assertEqual(len(error_response), 1) self.assertEqual(error_response[0].code, error_expected[0]['code']) self.assertEqual(error_response[0].message, error_expected[0]['message']) self.assertEqual(error_response[0].field, error_expected[0]['field'])
def test_call_actions_no_raise_action_errors(self): action_request = [ { 'action': 'action_1', 'body': {'foo': 'bar'}, }, { 'action': 'action_2', 'body': {}, }, ] error_expected = [ Error( code=ERROR_CODE_INVALID, message='Invalid input', field='foo' ) ] self.client_settings[SERVICE_NAME]['transport']['kwargs']['action_map']['action_2'] = {'errors': error_expected} client = Client(self.client_settings) for actions in (action_request, [ActionRequest(**a) for a in action_request]): response = client.call_actions(SERVICE_NAME, actions, raise_action_errors=False) self.assertEqual(response.actions[0].body, {'foo': 'bar'}) self.assertEqual(response.actions[1].errors, error_expected) self.assertIsNotNone(response.context['correlation_id'])
def test_action_one_switch_twelve_with_errors(self): settings = {'foo': 'bar'} action = SwitchedActionOne(settings) with self.assertRaises(ActionError) as error_context: action( EnrichedActionRequest(action='one', body={'animal': 'cat'}, switches=[12])) self.assertEqual(2, len(error_context.exception.errors)) self.assertIn(Error('MISSING', 'Missing key: planet', field='planet'), error_context.exception.errors) self.assertIn(Error('UNKNOWN', 'Extra keys present: animal'), error_context.exception.errors)
def test_assert_not_wanted_errors_match_list_with_field(self): with self.assertRaises(AssertionError): assertions.assert_actual_list_not_subset( [ Error(code='FOO', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), ], [ Error(code='FOO', message='Foo message', field='foo_field'), ], )
def test_raises_field_errors_match_multiple(codes): errors = [Error(code=code, field=field, message='bam') for field, code in codes] with raises_field_errors({'event_id': 'UNKNOWN', 'organization_id': 'INVALID'}) as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] ) assert exc_info.soa_errors == errors
def test_call_actions_parallel_job_errors_raised(self): """ Test that call_actions_parallel raises job errors when they occur """ with self.assertRaises(self.client.JobError) as error_context: self.client.call_actions_parallel('error_service', [{'action': 'job_error'}]) self.assertEqual([Error(code='BAD_JOB', message='You are a bad job')], error_context.exception.errors)
def test_raises_error_codes_multiple(codes): errors = [Error(code=code, message='bam') for code in codes] with raises_error_codes(['TEST', 'AUTH_MISSING']) as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] ) assert exc_info.soa_errors == errors
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, } 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 test_raises_field_errors_missing(code, field): errors = [ Error(code=code, message='test fail', field=field), ] with pytest.raises(pytest.raises.Exception): with raises_field_errors({'event_id': 'UNKNOWN'}): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] )
def test_assert_not_wanted_errors_array_empty(self): assertions.assert_actual_list_not_subset( [ Error(code='INVALID', message=AnyValue('str'), field=AnyValue('str', permit_none=True)) ], [], )
def execute_job(self, job_request): """ Processes and runs the ActionRequests on the Job. """ # Run the Job's Actions job_response = JobResponse() job_switches = RequestSwitchSet(job_request['context']['switches']) for i, raw_action_request in enumerate(job_request['actions']): action_request = EnrichedActionRequest( action=raw_action_request['action'], body=raw_action_request.get('body', None), switches=job_switches, context=job_request['context'], control=job_request['control'], client=job_request['client'], ) if action_request.action in self.action_class_map: # Get action to run action = self.action_class_map[action_request.action]( self.settings) # Wrap it in middleware wrapper = self.make_middleware_stack( [m.action for m in self.middleware], action, ) # Execute the middleware stack try: action_response = wrapper(action_request) except ActionError as e: # Error: an error was thrown while running the Action (or Action middleware) action_response = ActionResponse( action=action_request.action, errors=e.errors, ) else: # Error: Action not found. action_response = ActionResponse( action=action_request.action, errors=[ Error( code=ERROR_CODE_UNKNOWN, message= 'The action "{}" was not found on this server.'. format(action_request.action), field='action', ) ], ) job_response.actions.append(action_response) if (action_response.errors and not job_request['control'].get( 'continue_on_error', False)): # Quit running Actions if an error occurred and continue_on_error is False break return job_response
class ErrorServer(Server): service_name = 'error_service' # noinspection PyTypeChecker action_class_map = { 'job_error': lambda *_, **__: (_ for _ in ()).throw( JobError(errors=[Error(code='BAD_JOB', message='You are a bad job')]) ), 'okay_action': lambda *_, **__: lambda *_, **__: ActionResponse(action='okay_action', body={'no_error': True}), }
def process_job(self, job_request): """ Validate, execute, and run the job request, wrapping it with any applicable job middleware. :param job_request: The job request :type job_request: dict :return: A `JobResponse` object :rtype: JobResponse :raise: JobError """ try: # Validate JobRequest message validation_errors = [ Error( code=error.code, message=error.message, field=error.pointer, ) for error in (JobRequestSchema.errors(job_request) or []) ] if validation_errors: raise JobError(errors=validation_errors) # Add the client object in case a middleware wishes to use it job_request['client'] = self.make_client(job_request['context']) # Add the async event loop in case a middleware wishes to use it job_request['async_event_loop'] = self._async_event_loop if self._async_event_loop_thread: job_request[ 'run_coroutine'] = self._async_event_loop_thread.run_coroutine else: job_request['run_coroutine'] = None # Build set of middleware + job handler, then run job wrapper = self.make_middleware_stack( [m.job for m in self.middleware], self.execute_job, ) job_response = wrapper(job_request) if 'correlation_id' in job_request['context']: job_response.context['correlation_id'] = job_request[ 'context']['correlation_id'] except JobError as e: self.metrics.counter('server.error.job_error').increment() job_response = JobResponse(errors=e.errors, ) except Exception as e: # Send an error response if no middleware caught this. # Formatting the error might itself error, so try to catch that self.metrics.counter('server.error.unhandled_error').increment() return self.handle_job_exception(e) return job_response
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'})
def handle_job_error_code(self, code, message, request_for_logging, response_for_logging, extra=None): log_extra = {'data': {'request': request_for_logging, 'response': response_for_logging}} if extra: log_extra['data'].update(extra) self.logger.error( message, exc_info=True, extra=log_extra, ) return JobResponse(errors=[Error(code=code, message=message)])
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', ), ], )
def test_call_actions_raises_exception_on_job_error(self): """Client.call_actions raises Client.JobError when a JobError occurs on the server.""" client = Client(self.client_settings) errors = [Error(code=ERROR_CODE_SERVER_ERROR, message='Something went wrong!')] with mock.patch.object( client._get_handler(SERVICE_NAME).transport.server, 'execute_job', new=mock.Mock(side_effect=JobError(errors)), ): with self.assertRaises(Client.JobError) as e: client.call_action(SERVICE_NAME, 'action_1') self.assertEqual(e.errors, errors)