def test_action_one_switch_twelve_with_errors(self): settings = {'foo': 'bar'} action = SwitchedActionOne(cast(ServerSettings, 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', is_caller_error=True), error_context.exception.errors, ) self.assertIn( Error('UNKNOWN', 'Extra keys present: animal', is_caller_error=True), error_context.exception.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, # 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_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)), # type: ignore ], [ Error(code='FOO', message='Foo message', field='foo_field'), Error(code='BAR', message='Bar message', field=None), ], )
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)), # type: ignore Error(code='BAR', message=AnyValue('str'), field='bar_field'), # type: ignore ], [ Error(code='BAR', message='Bar message', field=None), ], )
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)), # type: ignore Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), # type: ignore ], [], )
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)), # type: ignore Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), # type: ignore ], [ Error(code='BAR', message='Bar message', field=None), Error(code='FOO', message='Foo message', field='foo_field'), ], )
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)), # type: ignore Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), # type: ignore ], )
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 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_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)), # type: ignore Error(code='BAR', message=AnyValue('str'), field=AnyValue('str', permit_none=True)), # type: ignore ], [ Error(code='FOO', message='Foo message', field='foo_field'), ], )
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)) ], # type: ignore [], )
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_local_action_other_request_details(self): action = mock.MagicMock() action.return_value.return_value = ActionResponse( action='another_foo', errors=[Error('BAR', 'Bar error')]) server = mock.MagicMock() server.settings = {'a_setting': 'a_value'} server.action_class_map = {'another_foo': action} r = EnrichedActionRequest(action='foo', body={'bar': 'baz'}, context={'auth': 'abc123'}, control={'speed': 5}) r._server = server with pytest.raises(ActionError) as error_context: r.call_local_action('another_foo', {'height': '10m'}) assert error_context.value.errors[0].code == 'BAR' 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 == 'another_foo' assert other_r.body == {'height': '10m'} assert other_r.context == {'auth': 'abc123'} assert other_r.control == {'speed': 5} assert getattr(other_r, '_server') is server action.reset_mock() response = r.call_local_action('another_foo', {'height': '10m'}, raise_action_errors=False) assert response.action == 'another_foo' assert response.errors assert response.errors[0].code == 'BAR' 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 == 'another_foo' assert other_r.body == {'height': '10m'} assert other_r.context == {'auth': 'abc123'} assert other_r.control == {'speed': 5} assert getattr(other_r, '_server') is server
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
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 __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 _start_stubbed_actions(test_target): if 'stubbed_actions' in test_target: # We must start the stubs in a predictable order... for service, action in sorted( six.iterkeys(test_target['stubbed_actions'])): stub_config = test_target['stubbed_actions'][service, action] if 'errors' in stub_config: stub = stub_action( service, action, errors=[Error(**e) for e in stub_config['errors']]) else: stub = stub_action(service, action, body=stub_config.get('body', {})) mock_action = stub.start() stub_config['started_stub'] = stub stub_config['mock_action'] = mock_action
def _replace_errors_if_necessary(errors, is_caller_error): # type: (Iterable[Error], bool) -> List[Error] new_errors = [] for e in errors: if e.is_caller_error == is_caller_error: new_errors.append(e) else: # Error is immutable, so return a new one new_errors.append( Error( code=e.code, message=e.message, field=e.field, traceback=e.traceback, variables=e.variables, denied_permissions=e.denied_permissions, is_caller_error=is_caller_error, )) return new_errors
def ingest_from_parsed_test_fixture(self, action_case, test_case, parse_results, file_name, line_number): path = 'expects_{not_q}{job_q}{exact_q}error'.format( not_q='not_' if getattr(parse_results, 'not', None) else '', job_q='job_' if parse_results.job else '', exact_q='exact_' if parse_results.exact else '', ) try: errors = path_get(action_case, path) except (KeyError, IndexError): errors = [] path_put(action_case, path, errors) # noinspection PyTypeChecker errors.append(Error( # type: ignore code=parse_results.error_code, message=getattr(parse_results, 'error_message', None) or AnyValue('str'), # type: ignore field=getattr(parse_results, 'field_name', None) or AnyValue('str', permit_none=True), # type: ignore traceback=AnyValue('str', permit_none=True), # type: ignore variables=AnyValue('dict', permit_none=True), # type: ignore denied_permissions=AnyValue('list', permit_none=True), # type: ignore is_caller_error=AnyValue('bool'), # type: ignore ))
def invalid_organization_id_field_error(): return Error(code='INVALID', field='organization_id', message='bad organization_id')
def auth_missing_error(): return Error(code='AUTH_MISSING', message='where did ur auth go')
def invalid_event_id_field_error(): return Error(code='INVALID', field='event_id', message='bad event_id')
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_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={}, 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 _convert_errors(errors): # type: (Union[Iterable[Mapping[six.text_type, Any]], Iterable[Error]]) -> List[Error] value = [] # type: List[Error] for a in errors: value.append(a if isinstance(a, Error) else Error(**a)) return value
def _process_stub_action_stub_job_error(body): raise JobError(errors=[ Error(code='CAT_ERROR', message='Your cat broke the vase'), Error(code='DOG_ERROR', message='Your dog ate the couch'), ])
def unknown_event_id_field_error(): return Error(code='UNKNOWN', field='event_id', message='bad event_id')
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
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)])