From 45af414eff866d209bac4f3b976719e76511a446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20G=C3=B6ransson?= Date: Tue, 5 Dec 2023 16:04:51 +0100 Subject: [PATCH] handle property validation on flat objects (#289) this is not supported by jsonpath-ng by default. so if input is a dict, expression contains "`this`" and does not contain "@.", then covert input to a one item list, which then makes it possible. --- grizzly_extras/async_message/sb.py | 2 +- grizzly_extras/transformer.py | 10 ++++++++-- tests/e2e/steps/scenario/test_response.py | 8 ++++---- tests/unit/test_grizzly/tasks/test_until.py | 16 +++++++--------- .../unit/test_grizzly_extras/test_transformer.py | 11 +++++++++++ 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/grizzly_extras/async_message/sb.py b/grizzly_extras/async_message/sb.py index 987f3e3b..ffc6fa8f 100644 --- a/grizzly_extras/async_message/sb.py +++ b/grizzly_extras/async_message/sb.py @@ -533,7 +533,7 @@ def request(self, request: AsyncMessageRequest) -> AsyncMessageResponse: # noqa try: content_type = TransformerContentType.from_string(cast(str, request.get('context', {})['content_type'])) transform = transformer.available[content_type] - get_values = transform.parser(request_arguments['expression']) + get_values = transform.parser(expression) except Exception as e: raise AsyncMessageError(str(e)) from e diff --git a/grizzly_extras/transformer.py b/grizzly_extras/transformer.py index 05aa28cb..e11673ec 100644 --- a/grizzly_extras/transformer.py +++ b/grizzly_extras/transformer.py @@ -10,7 +10,7 @@ from json import JSONEncoder from json import dumps as jsondumps from json import loads as jsonloads -from typing import Any, Callable, ClassVar, Dict, List, Optional, Type +from typing import Any, Callable, ClassVar, Dict, List, Optional, Type, Union from jsonpath_ng.ext import parse as jsonpath_parse from lxml import etree as XML # noqa: N812 @@ -135,7 +135,13 @@ def parser(cls, expression: str) -> Callable[[Any], List[str]]: jsonpath = jsonpath_parse(expression) - def _parser(input_payload: Any) -> List[str]: + def _parser(input_payload: Union[list, dict]) -> List[str]: + # we need to fool jsonpath-ng to allow "validation" queries on objects on multiple properties + # this shouldn't be done if the input is a nested object, and the query looks for any properties + # recursively under the root (`@.`) + if isinstance(input_payload, dict) and '`this`' in expression and '@.' not in expression: + input_payload = [input_payload] + values: List[str] = [] for m in jsonpath.find(input_payload): if m is None or m.value is None: diff --git a/tests/e2e/steps/scenario/test_response.py b/tests/e2e/steps/scenario/test_response.py index 461fc49e..ab723618 100644 --- a/tests/e2e/steps/scenario/test_response.py +++ b/tests/e2e/steps/scenario/test_response.py @@ -38,7 +38,7 @@ def validator(context: Context) -> None: handlers = getattr(request.response.handlers, handler_type) assert f'{{{{ expression_{index} }}}}' in grizzly.scenario.orphan_templates, f'{{{{ expression_{index} }}}} not in {grizzly.scenario.orphan_templates}' - assert grizzly.state.variables.get(f'expression_{index}', None) == f'$.`this`.{attr_name}', f'variable expression_{index} is not $.`this`.{attr_name}' + assert grizzly.state.variables.get(f'expression_{index}', None) == f'$.{attr_name}', f'variable expression_{index} is not $.{attr_name}' assert len(handlers) == 1, f'unexpected number of {target} handlers' handler = handlers[0] assert isinstance(handler, SaveHandlerAction), f'{handler.__class__.__name__} != SaveHandlerAction' @@ -55,7 +55,7 @@ def validator(context: Context) -> None: table.append({'target': target.name.lower(), 'index': str(index), 'attr_name': 'foobar'}) scenario += [ - f'Given value for variable "expression_{index}" is "$.`this`.foobar"', + f'Given value for variable "expression_{index}" is "$.foobar"', f'Given value for variable "tmp_{index}" is "none"', f'Then get request with name "{target.name.lower()}-handler" from endpoint "/api/echo?foobar=foo | content_type=json"', 'And metadata "foobar" is "foobar"', @@ -108,7 +108,7 @@ def validator(context: Context) -> None: handler = handlers[0] assert isinstance(handler, SaveHandlerAction), f'{handler.__class__.__name__} != SaveHandlerAction' assert handler.variable == f'tmp_{index}', f'{handler.variable} != tmp_{index}' - assert handler.expression == f'$.`this`.{attr_name}', f'{handler.expression} != $.`this`.{attr_name}' + assert handler.expression == f'$.{attr_name}', f'{handler.expression} != $.{attr_name}' assert handler.match_with == '.*', f'{handler.match_with} != .*' assert handler.expected_matches == f'{{{{ expected_matches_{index} }}}}', f'{handler.expected_matches} != 1' @@ -125,7 +125,7 @@ def validator(context: Context) -> None: f'And value for variable "expected_matches_{index}" is "1"', f'Then get request with name "{target.name.lower()}-handler" from endpoint "/api/echo?foobar=foo | content_type=json"', 'And metadata "foobar" is "foobar"', - f'Then save response {target.name.lower()} "$.`this`.{attr_name} | expected_matches=\'{{{{ expected_matches_{index} }}}}\'" in variable "tmp_{index}"', + f'Then save response {target.name.lower()} "$.{attr_name} | expected_matches=\'{{{{ expected_matches_{index} }}}}\'" in variable "tmp_{index}"', f'Then log message "expected_matches_{index}={{{{ expected_matches_{index} }}}}, tmp_{index}={{{{ tmp_{index} }}}}"', ] diff --git a/tests/unit/test_grizzly/tasks/test_until.py b/tests/unit/test_grizzly/tasks/test_until.py index 0cbf34c8..f438d8b5 100644 --- a/tests/unit/test_grizzly/tasks/test_until.py +++ b/tests/unit/test_grizzly/tasks/test_until.py @@ -82,9 +82,7 @@ def test___call__( # noqa: PLR0915 def create_response(status: str) -> str: return jsondumps({ - 'response': { - 'status': status, - }, + 'status': status, }) request_spy = mocker.patch.object( @@ -157,7 +155,7 @@ def create_response(status: str) -> str: request_type='UNTL', name=f'{parent.user._scenario.identifier} test-request, w=100.0s, r=10, em=1', response_time=153500, - response_length=103, + response_length=61, context=parent.user._context, exception=None, ) @@ -186,7 +184,7 @@ def create_response(status: str) -> str: request_type='UNTL', name=f'{parent.user._scenario.identifier} test-request, w=100.0s, r=10, em=1', response_time=12250, - response_length=33, + response_length=19, context=parent.user._context, exception=None, ) @@ -213,7 +211,7 @@ def create_response(status: str) -> str: request_type='UNTL', name=f'{parent.user._scenario.identifier} test-request, w=10.0s, r=2, em=1', response_time=12250, - response_length=70, + response_length=42, context=parent.user._context, exception=ANY(RuntimeError, message='found 0 matching values for $.`this`[?status="ready"] in payload'), ) @@ -222,7 +220,7 @@ def create_response(status: str) -> str: assert len(caplog.messages) == 1 assert caplog.messages[-1] == ( f'{parent.user._scenario.identifier} test-request, w=10.0s, r=2, em=1: endpoint={meta_request_task.endpoint}, number_of_matches=0, ' - f'condition=\'$.`this`[?status="ready"]\', retry=2, response_time=12250 payload=\n{jsondumps({"response": {"status": "working"}}, indent=2)}' + f'condition=\'$.`this`[?status="ready"]\', retry=2, response_time=12250 payload=\n{jsondumps({"status": "working"}, indent=2)}' ) caplog.clear() @@ -245,7 +243,7 @@ def create_response(status: str) -> str: request_type='UNTL', name=f'{parent.user._scenario.identifier} test-request, w=10.0s, r=2, em=1', response_time=1500, - response_length=35, + response_length=21, context=parent.user._context, exception=ANY(RuntimeError, message='foo bar'), ) @@ -286,7 +284,7 @@ def create_response(status: str) -> str: request_type='UNTL', name=f'{parent.user._scenario.identifier} test-request, w=4.0s, r=4, em=1', response_time=800, - response_length=103, + response_length=61, context=parent.user._context, exception=None, ) diff --git a/tests/unit/test_grizzly_extras/test_transformer.py b/tests/unit/test_grizzly_extras/test_transformer.py index 276070bb..fa80c504 100644 --- a/tests/unit/test_grizzly_extras/test_transformer.py +++ b/tests/unit/test_grizzly_extras/test_transformer.py @@ -198,6 +198,17 @@ def test_parser(self) -> None: # noqa: PLR0915 assert len(actual) > 0 + document = { + 'name': 'foobar', + 'id': 1, + 'description': 'foo bar', + } + + get_values = JsonTransformer.parser('$.`this`[?name="foobar" & id=1]') + actual = get_values(document) + + assert actual != [] + class TestXmlTransformer: def test_transform(self) -> None: