diff --git a/Pipfile.lock b/Pipfile.lock index 3ddd9519..c7cb14c3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,11 +18,11 @@ "default": { "astroid": { "hashes": [ - "sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581", - "sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5" + "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83", + "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f" ], "index": "pypi", - "version": "==2.12.10" + "version": "==2.12.12" }, "cerberus": { "hashes": [ @@ -194,11 +194,11 @@ }, "setuptools": { "hashes": [ - "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012", - "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e" + "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17", + "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356" ], "markers": "python_version >= '3.7'", - "version": "==65.4.1" + "version": "==65.5.0" }, "toml": { "hashes": [ @@ -398,11 +398,11 @@ }, "astroid": { "hashes": [ - "sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581", - "sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5" + "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83", + "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f" ], "index": "pypi", - "version": "==2.12.10" + "version": "==2.12.12" }, "async-timeout": { "hashes": [ @@ -781,11 +781,11 @@ }, "pylint": { "hashes": [ - "sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b", - "sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1" + "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df", + "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004" ], "index": "pypi", - "version": "==2.15.3" + "version": "==2.15.5" }, "pyparsing": { "hashes": [ diff --git a/chk/infrastructure/containers.py b/chk/infrastructure/containers.py index e436d060..01514676 100644 --- a/chk/infrastructure/containers.py +++ b/chk/infrastructure/containers.py @@ -5,7 +5,7 @@ from typing import NamedTuple from chk.infrastructure.file_loader import FileContext, ChkFileLoader -from chk.infrastructure.helper import dict_get, dict_set +from chk.infrastructure.helper import dict_get, dict_set, data_set class CompiledDocBlockType(Enum): @@ -14,6 +14,7 @@ class CompiledDocBlockType(Enum): REQUEST = "request" VERSION = "version" VARIABLES = "variables" + EXPOSE = "expose" LOCAL = "__local" @staticmethod @@ -26,6 +27,10 @@ def allowed_keys() -> set: def all_keys() -> set: return {e.value for e in CompiledDocBlockType} + @staticmethod + def default() -> dict: + return {item: {} for item in CompiledDocBlockType.all_keys()} + class App(NamedTuple): """Global app container""" @@ -34,9 +39,7 @@ class App(NamedTuple): compiled_doc: dict = {} environment_ctx: dict = { - "config": { - "buffer_access_off": True - }, + "config": {"buffer_access_off": True}, } def __str__(self) -> str: @@ -58,16 +61,13 @@ def get_original_doc(self, key: str) -> dict: return self.original_doc.get(key, {}) - def set_compiled_doc(self, key: str, value: dict, part: str | None = None) -> None: + def set_compiled_doc(self, key: str, value: object, part: str | None = None) -> None: """Set compiled file doc""" - if not isinstance(value, dict): - raise SystemExit("Unsupported format for compiled doc") - allowed_keys = CompiledDocBlockType.all_keys() if key not in self.compiled_doc: - self.compiled_doc[key] = {item: {} for item in allowed_keys} + self.compiled_doc[key] = CompiledDocBlockType.default() if part is not None: if part not in allowed_keys: @@ -106,7 +106,18 @@ def load_original_doc_from_file_context(self, file_ctx: FileContext) -> None: self.set_original_doc(file_ctx.filepath_hash, document) def config(self, key: str, val: object = None) -> object: + """Set and retrieve config""" if val is not None: dict_set(self.environment_ctx, f"config.{key}", val) return dict_get(self.environment_ctx, f"config.{key}", None) + + def set_local(self, key: str, val: object, part: str) -> bool: + """Set local variable values in compiled_doc dict""" + if key not in self.compiled_doc: + self.compiled_doc[key] = CompiledDocBlockType.default() + return data_set(self.compiled_doc[key], f"__local.{part}", val) + + def get_local(self, key: str, part: str) -> object: + """Set local variable values in compiled_doc dict""" + return dict_get(self.compiled_doc[key], f"__local.{part}") diff --git a/chk/infrastructure/exception.py b/chk/infrastructure/exception.py index eafbfb90..601f89c1 100644 --- a/chk/infrastructure/exception.py +++ b/chk/infrastructure/exception.py @@ -15,7 +15,7 @@ V0006="File exception: Validation failed", V0007="System exception: Not a Callable", V0008="System exception: Wrong spec class", - V0009="Document exception: variable name starts or ends with __", + V0009="Document exception: ", # testcase errors V0020="None or many request found", V0021="Pass data on same context", diff --git a/chk/modules/http/constants.py b/chk/modules/http/constants.py index cf680b23..8f5e2f41 100644 --- a/chk/modules/http/constants.py +++ b/chk/modules/http/constants.py @@ -7,39 +7,42 @@ class HttpMethod(Enum): """Constants of wellknown http methods""" - GET = 'GET' - POST = 'POST' - PUT = 'PUT' - PATCH = 'PATCH' - DELETE = 'DELETE' - HEAD = 'HEAD' - OPTIONS = 'OPTIONS' + + GET = "GET" + POST = "POST" + PUT = "PUT" + PATCH = "PATCH" + DELETE = "DELETE" + HEAD = "HEAD" + OPTIONS = "OPTIONS" @dataclass class RequestConfigNode: """represent request config section""" - ROOT = 'request' - RETURN = 'return' + + ROOT = "request" + LOCAL = "_response" + RETURN = "return" # common request - URL = 'url' - METHOD = 'method' - HEADERS = 'headers' - PARAMS = 'url_params' + URL = "url" + METHOD = "method" + HEADERS = "headers" + PARAMS = "url_params" # Basic - AUTH_BA = 'auth[basic]' - AUTH_BA_USR = 'username' - AUTH_BA_PAS = 'password' + AUTH_BA = "auth[basic]" + AUTH_BA_USR = "username" + AUTH_BA_PAS = "password" # Bearer - AUTH_BE = 'auth[bearer]' - AUTH_BE_TOK = 'token' + AUTH_BE = "auth[bearer]" + AUTH_BE_TOK = "token" # Body - BODY_FRM = 'body[form]' - BODY_FRM_DAT = 'body[form-data]' - BODY_JSN = 'body[json]' - BODY_XML = 'body[xml]' - BODY_TXT = 'body[text]' + BODY_FRM = "body[form]" + BODY_FRM_DAT = "body[form-data]" + BODY_JSN = "body[json]" + BODY_XML = "body[xml]" + BODY_TXT = "body[text]" diff --git a/chk/modules/http/entities.py b/chk/modules/http/entities.py index 947f6262..e26b29bb 100644 --- a/chk/modules/http/entities.py +++ b/chk/modules/http/entities.py @@ -4,19 +4,22 @@ from typing import NamedTuple from chk.infrastructure.contexts import app -from chk.infrastructure.exception import err_message from chk.infrastructure.file_loader import FileContext from chk.infrastructure.helper import dict_get from chk.infrastructure.work import WorkerContract from chk.modules.http.presentation import Presentation -from chk.modules.version.support import VersionMixin, RawFileVersionParser +from chk.modules.version.support import VersionMixin from chk.modules.http.request_helper import RequestProcessorPyRequests from chk.modules.http.support import RequestMixin from chk.modules.http.constants import RequestConfigNode as RConst -from chk.modules.variables.entities import DefaultVariableDoc, DefaultReturnableDoc +from chk.modules.variables.entities import ( + DefaultVariableDoc, + DefaultReturnableDoc, + DefaultExposableDoc, +) from chk.modules.variables.support import VariableMixin @@ -33,24 +36,7 @@ def merged(self, doc: dict) -> dict: return {RConst.ROOT: {**self.returnable, **dict_get(doc, RConst.ROOT, {})}} -class HttpSpec: - """Http versioned functionality wrapper""" - - def __new__(cls, file_ctx: FileContext) -> WorkerContract: - """Create a http spec utility based on version""" - - version = RawFileVersionParser.convert_version_str_to_num( - RawFileVersionParser.find_version_str(file_ctx.filepath) - ) - - if version == "072": - return HttpSpec_072(file_ctx) - - # if none of the version matches - raise SystemExit(err_message("fatal.V0001")) - - -class HttpSpec_072( +class HttpSpec( VersionMixin, RequestMixin, VariableMixin, @@ -78,14 +64,16 @@ def __before_main__(self) -> None: version_doc = self.version_validated() request_doc = self.request_validated() variable_doc = self.variable_validated() + expose_doc = self.expose_validated() # compile data with defaults app.set_compiled_doc( self.file_ctx.filepath_hash, ( version_doc - | DefaultRequestDoc().merged(request_doc) | DefaultVariableDoc().merged(variable_doc) + | DefaultExposableDoc({"expose": ["_response"]}).merged(expose_doc) + | request_doc ), ) @@ -94,17 +82,19 @@ def __main__(self) -> None: self.variable_prepare_value_table() self.lexical_analysis_for_request() - request_doc = app.get_compiled_doc(self.file_ctx.filepath_hash, RConst.ROOT) try: - response = RequestProcessorPyRequests.perform(request_doc) + request_doc = dict_get(self.request_as_dict(True), RConst.ROOT) + self.store_local_vars_for_request( + RequestProcessorPyRequests.perform(request_doc) + ) + Presentation.buffer_msg("- Making request [Success]") - except Exception: + except RuntimeError as err: Presentation.buffer_msg("- Making request [Fail]") + raise err - app.set_compiled_doc( - self.file_ctx.filepath_hash, part="__local", value={RConst.ROOT: response} - ) - - def __after_main__(self) -> dict: + def __after_main__(self) -> list: """Prepare response for http document""" - return self.assemble_local_vars_for_request() + Presentation.buffer_msg("- Prepare exposable [Success]") + self.make_exposable() + return self.get_exposable() diff --git a/chk/modules/http/presentation.py b/chk/modules/http/presentation.py index e6b78ec1..93be887e 100644 --- a/chk/modules/http/presentation.py +++ b/chk/modules/http/presentation.py @@ -6,6 +6,7 @@ class Presentation: """Handle presentation of the outputs of chkware commands.""" + display_buffer: list[str] = [] @classmethod @@ -16,18 +17,26 @@ def buffer_msg(cls, message: str) -> None: cls.display_buffer.append(message) @classmethod - def present_result(cls, data: dict | BaseException) -> None: + def present_result(cls, data: list | BaseException) -> None: """Shows result of execution.""" if isinstance(data, dict): if not app.config("buffer_access_off"): print(cls.displayable_summary()) print(cls.displayable_result(data)) + if isinstance(data, list): + if not app.config("buffer_access_off"): + print(cls.displayable_summary()) + print(cls.displayable_expose(data)) else: if not app.config("buffer_access_off"): print(cls.displayable_summary()) print(str(data)) + @classmethod + def displayable_expose(cls, exposable: list) -> str: + return "\r\n\r\n".join([str(item) for item in exposable]) + @classmethod def displayable_result(cls, response: dict[str, object]) -> str: """Return result in presentable format.""" diff --git a/chk/modules/http/request_helper.py b/chk/modules/http/request_helper.py index babf094b..2f805313 100644 --- a/chk/modules/http/request_helper.py +++ b/chk/modules/http/request_helper.py @@ -4,7 +4,14 @@ from types import MappingProxyType from urllib.parse import unquote, urlparse -from requests import request +from requests import ( + request, + TooManyRedirects, + ConnectionError, + ConnectTimeout, + ReadTimeout, + RequestException, +) from requests.auth import HTTPBasicAuth from chk.modules.http.constants import RequestConfigNode as ConfElem @@ -25,7 +32,18 @@ def perform(request_data: dict) -> dict: request_args, ) - response = request(**request_args) + try: + response = request(**request_args) + except ConnectTimeout as err: + raise RuntimeError("Connection time out") from err + except ConnectionError as err: + raise RuntimeError("Connection error") from err + except ReadTimeout as err: + raise RuntimeError("Read time out") from err + except TooManyRedirects as err: + raise RuntimeError("Too many redirects") from err + except RequestException as err: + raise RuntimeError("Request error") from err def version(proto_ver: int) -> str: """parse version""" diff --git a/chk/modules/http/support.py b/chk/modules/http/support.py index efd0210d..f70b2548 100644 --- a/chk/modules/http/support.py +++ b/chk/modules/http/support.py @@ -16,10 +16,12 @@ class RequestValueHandler: - """ Handle variables and values regarding request """ + """Handle variables and values regarding request""" @staticmethod - def request_fill_val(document: dict, symbol_table: dict, replace_method: Callable[[dict, dict], dict]): + def request_fill_val( + document: dict, symbol_table: dict, replace_method: Callable[[dict, dict], dict] + ): """Convert request block variables""" request_document = document.get(RequestConfigNode.ROOT, {}) @@ -31,33 +33,33 @@ def request_fill_val(document: dict, symbol_table: dict, replace_method: Callabl def request_get_return(document: dict, response: dict) -> dict: """Return request block variables""" returnable = response - returnable['have_all'] = True + returnable["have_all"] = True if req := document.get(RequestConfigNode.ROOT, {}): if ret := req.get(RequestConfigNode.RETURN): ret = str(ret) - if not ret.startswith('.'): - raise ValueError('Unsupported key format in request.return') + if not ret.startswith("."): + raise ValueError("Unsupported key format in request.return") - ret = ret.lstrip('.') - if ret not in ('version', 'code', 'reason', 'headers', 'body'): - raise ValueError('Unsupported key in request.return') + ret = ret.lstrip(".") + if ret not in ("version", "code", "reason", "headers", "body"): + raise ValueError("Unsupported key in request.return") def fx(k: object, v: object) -> object: return None if k != ret else v returnable = {key: fx(key, value) for key, value in response.items()} - returnable['have_all'] = False + returnable["have_all"] = False return returnable class RequestMixin: - """ Mixin for request spec. for v0.7.2 """ + """Mixin for request spec. for v0.7.2""" @abc.abstractmethod def get_file_context(self) -> FileContext: - """ Abstract method to get file context """ + """Abstract method to get file context""" def request_validated(self) -> dict[str, dict]: """Validate the schema against config""" @@ -65,19 +67,27 @@ def request_validated(self) -> dict[str, dict]: try: request_doc = self.request_as_dict() if not validator.validate(request_doc, request_schema): - raise SystemExit(err_message('fatal.V0006', extra=validator.errors)) + raise SystemExit(err_message("fatal.V0006", extra=validator.errors)) except DocumentError as doc_err: - raise SystemExit(err_message('fatal.V0001', extra=doc_err)) from doc_err + raise SystemExit(err_message("fatal.V0001", extra=doc_err)) from doc_err else: return request_doc # or is a success - def request_as_dict(self) -> dict[str, dict]: - """ Get request as a dictionary """ + def request_as_dict(self, compiled=False) -> dict: + """Get request as a dictionary""" - file_ctx = self.get_file_context() - document = app.get_original_doc(file_ctx.filepath_hash).copy() + hf_name = self.get_file_context().filepath_hash + document = ( + app.get_original_doc(hf_name).copy() + if compiled is False + else app.get_compiled_doc(hf_name).copy() + ) try: - return {key: document[key] for key in (RequestConfigNode.ROOT,) if key in document} + return { + key: document[key] + for key in (RequestConfigNode.ROOT,) + if key in document + } except Exception as ex: - raise SystemExit(err_message('fatal.V0005', extra=ex)) from ex + raise SystemExit(err_message("fatal.V0005", extra=ex)) from ex diff --git a/chk/modules/variables/constants.py b/chk/modules/variables/constants.py index c85d1ac7..555d06a0 100644 --- a/chk/modules/variables/constants.py +++ b/chk/modules/variables/constants.py @@ -10,9 +10,12 @@ class VariableConfigNode: """ Represent variables config section """ - ROOT = 'variables' - RETURN = 'return' - RESULT = 'result' + + ROOT = "variables" + RETURN = "return" + RESULT = "result" + EXPOSE = "expose" + LOCAL = "__local" class LexicalAnalysisType(Enum): diff --git a/chk/modules/variables/entities.py b/chk/modules/variables/entities.py index 9498d8b5..01e8dbb2 100644 --- a/chk/modules/variables/entities.py +++ b/chk/modules/variables/entities.py @@ -9,14 +9,14 @@ class DefaultVariableDoc(NamedTuple): - """ Default variable doc """ + """Default variable doc""" doc: dict[str, object] = { VConst.ROOT: {}, } def merged(self, doc: dict) -> dict: - """ Merge given doc with default one """ + """Merge given doc with default one""" if not doc: doc = {} @@ -24,16 +24,31 @@ def merged(self, doc: dict) -> dict: class DefaultReturnableDoc(NamedTuple): - """ Default return-able doc """ + """Default return-able doc""" doc: dict[str, object] = { VConst.RETURN: None, } +class DefaultExposableDoc(NamedTuple): + """Default expose-able doc""" + + doc: dict[str, object] = { + VConst.EXPOSE: None, + } + + def merged(self, doc: dict) -> dict: + """Merge given doc with default one""" + if not doc: + doc = {} + + return self.doc | doc + + @dataclass class ApiResponse: - """ Generic class to hold API response data and parsing """ + """Generic class to hold API response data and parsing""" code: int version: str @@ -43,7 +58,7 @@ class ApiResponse: @staticmethod def from_dict(response: dict) -> "ApiResponse": - """ Convert response dict to ApiResponse """ + """Convert response dict to ApiResponse""" if code := response.get("code"): response["code"] = int(code) @@ -52,7 +67,7 @@ def from_dict(response: dict) -> "ApiResponse": if body := response.get("body"): response["body"] = loads(body) except decoder.JSONDecodeError: - SystemExit('`json` data not found') + SystemExit("`json` data not found") return ApiResponse(**response) diff --git a/chk/modules/variables/lexicon.py b/chk/modules/variables/lexicon.py index 1666b3fc..c74bfff0 100644 --- a/chk/modules/variables/lexicon.py +++ b/chk/modules/variables/lexicon.py @@ -11,16 +11,48 @@ class StringLexicalAnalyzer: @staticmethod def replace_in_str(container: str, replace_with: dict) -> str: - if type(container) is not str: raise TypeError - if len(replace_with) == 0: return container + if not isinstance(container, str): + raise TypeError - line_split = re.split(r'({\s*\$\w+\s*})', container) - line_strip = [''.join(item.split()) for item in line_split if item] + if len(replace_with) == 0: + return container + + line_split = re.split(r"({\s*\$[a-zA-Z0-9_.]+\s*})|(\s*\$[a-zA-Z0-9_.]+)", container) + line_strip = ["".join(item.split()) for item in line_split if item] + + for i, item in enumerate(line_strip): + item = item.strip("{}") + if item.startswith("$"): + value = data_get(replace_with, item.lstrip("$"), None) + line_strip[i] = str(value) if value else "{" + item + "}" + + return "".join(line_strip) + + @staticmethod + def replace(container: str, replace_with: dict) -> object: + """replace values found in string with typed return""" + + if not isinstance(container, str): + raise TypeError + + if len(replace_with) == 0: + return container + + line_split = re.split( + r"({\s*\$[a-zA-Z0-9_.]+\s*})|(\s*\$[a-zA-Z0-9_.]+)", container + ) + line_strip = ["".join(item.split()) for item in line_split if item] + + if len(line_strip) == 1: + item = line_strip.pop() + t_item = item.strip("{}").lstrip("$") + value = data_get(replace_with, t_item) + return value if value else "{ $" + t_item + " }" for i, item in enumerate(line_strip): - item = item.strip('{}') - if item.startswith('$'): - value = data_get(replace_with, item.lstrip('$'), None) - line_strip[i] = str(value) if value else '{' + item + '}' + item = item.strip("{}") + if item.startswith("$"): + value = data_get(replace_with, item.lstrip("$"), None) + line_strip[i] = str(value) if value else "{" + item + "}" - return ''.join(line_strip) + return "".join(line_strip) diff --git a/chk/modules/variables/support.py b/chk/modules/variables/support.py index fdd77204..8a49103f 100644 --- a/chk/modules/variables/support.py +++ b/chk/modules/variables/support.py @@ -10,7 +10,7 @@ from chk.infrastructure.contexts import app, validator from chk.infrastructure.exception import err_message from chk.infrastructure.file_loader import FileContext -from chk.infrastructure.helper import dict_set +from chk.infrastructure.helper import dict_set, data_get, dict_get from chk.modules.version.constants import DocumentType @@ -23,6 +23,7 @@ ) from chk.modules.variables.validation_rules import ( variable_schema, + expose_schema, allowed_variable_name, ) from chk.modules.variables.lexicon import StringLexicalAnalyzer @@ -53,10 +54,10 @@ def replace_values(doc: dict, var_s: dict) -> dict[str, object]: """ for key in doc.keys(): - if type(doc[key]) is str: + if isinstance(doc[key], str): item = str(doc[key]) doc[key] = StringLexicalAnalyzer.replace_in_str(item, var_s) - elif type(doc[key]) is dict: + elif isinstance(doc[key], dict): doc[key] = replace_values(doc[key], var_s) return doc @@ -112,6 +113,30 @@ def variable_as_dict(self) -> dict[str, dict]: except Exception as ex: raise SystemExit(err_message("fatal.V0005", extra=ex)) from ex + def expose_validated(self) -> dict: + """Validate the schema against config""" + + try: + expose_doc = self.expose_as_dict() + + if not validator.validate(expose_doc, expose_schema): + raise SystemExit(err_message("fatal.V0006", extra=validator.errors)) + + except DocumentError as doc_err: + raise SystemExit(err_message("fatal.V0001", extra=doc_err)) from doc_err + + return expose_doc + + def expose_as_dict(self) -> dict: + """Get expose dict""" + + document = app.get_original_doc(self.file_ctx.filepath_hash) + + try: + return {key: document[key] for key in (VarConf.EXPOSE,) if key in document} + except Exception as ex: + raise SystemExit(err_message("fatal.V0005", extra=ex)) from ex + def variable_process(self, la_type: LexicalAnalysisType, symbol_table=None) -> dict: self.__init___(symbol_table) return self.lexical_analysis_for(la_type, self.symbol_table) @@ -213,16 +238,37 @@ def variable_assemble_values(self, document: dict, response: dict) -> dict: raise ValueError(f"variable_assemble_values: `{document_type}` not allowed") - def assemble_local_vars_for_request(self) -> dict: - """Assemble value based on return statement""" - document = self.request_as_dict() - response = dict(app.get_compiled_doc(self.file_ctx.filepath_hash, "__local")) + def store_local_vars_for_request(self, data: object) -> bool: + """Store value based on return statement""" + return app.set_local(self.file_ctx.filepath_hash, data, RequestConfigNode.LOCAL) - compiled_return: dict = RequestValueHandler.request_get_return( - document, response.get(RequestConfigNode.ROOT, {}) - ) + def make_exposable(self) -> None: + """Prepare exposable data""" + + hf_name = self.get_file_context().filepath_hash + + symbol_table = app.get_compiled_doc( + hf_name, VarConf.LOCAL + ) | app.get_compiled_doc(hf_name, VarConf.ROOT) + + expose_items = app.get_compiled_doc(hf_name, VarConf.EXPOSE) + + if not isinstance(expose_items, list): + raise ValueError + + exposables = [ + StringLexicalAnalyzer.replace(item, symbol_table) + for item in expose_items + if isinstance(item, str) + ] + + app.set_compiled_doc(hf_name, part=VarConf.EXPOSE, value=exposables) + + def get_exposable(self) -> list: + """Get exposable data""" - return compiled_return + hf_name = self.get_file_context().filepath_hash + return app.get_compiled_doc(hf_name, VarConf.EXPOSE) def variable_prepare_value_table(self) -> None: updated_vars: dict = {} diff --git a/chk/modules/variables/validation_rules.py b/chk/modules/variables/validation_rules.py index 42289e38..8f7effc1 100644 --- a/chk/modules/variables/validation_rules.py +++ b/chk/modules/variables/validation_rules.py @@ -1,14 +1,15 @@ """ Validation rules and supporting libs for variables module """ +import re + from chk.modules.variables.constants import VariableConfigNode as VarConf def allowed_variable_name(name: str) -> bool: """Check if the name start or ends with __""" - - if name.startswith("__") or name.endswith("__"): - raise ValueError(f"Unsupported variable naming: {name}",) + if not re.findall(r"(^[a-zA-Z0-9]+)\w$", name): + raise ValueError(f"Unsupported variable naming: `{name}`") return True @@ -21,3 +22,13 @@ def allowed_variable_name(name: str) -> bool: "empty": True, } } + +# cerberus validation rules for variables +expose_schema = { + VarConf.EXPOSE: { + "required": False, + "type": "list", + "empty": True, + "nullable": True, + } +} diff --git a/chk/modules/version/constants.py b/chk/modules/version/constants.py index 5cb8ee43..bad94a09 100644 --- a/chk/modules/version/constants.py +++ b/chk/modules/version/constants.py @@ -20,7 +20,6 @@ def from_value(cls, value=None): class VersionConfigNode: """represent the base of all kind of documents""" VERSION = 'version' - EXPOSE = 'expose' class VersionStore: diff --git a/requirements.txt b/requirements.txt index 38839b13..3b869f74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -i https://pypi.org/simple -astroid==2.12.10 +astroid==2.12.12 cerberus==1.3.4 certifi==2022.9.24 charset-normalizer==2.1.1 @@ -12,7 +12,7 @@ platformdirs==2.5.2 pyyaml==6.0 requests==2.28.1 requests-toolbelt==0.10.0 -setuptools==65.4.1 ; python_version >= '3.7' +setuptools==65.5.0 ; python_version >= '3.7' toml==0.10.2 typing-extensions==4.4.0 urllib3==1.26.12 diff --git a/tests/infrastructure/app_container_test.py b/tests/infrastructure/app_container_test.py index 197af04a..609b3406 100644 --- a/tests/infrastructure/app_container_test.py +++ b/tests/infrastructure/app_container_test.py @@ -30,59 +30,96 @@ class TestApp: @staticmethod def test_app_set_original_doc_pass(): app = App() - app.set_original_doc("ab12", {'a': 1}) - assert app.original_doc["ab12"] == {'a': 1} + app.set_original_doc("ab12", {"a": 1}) + assert app.original_doc["ab12"] == {"a": 1} @staticmethod def test_app_set_original_doc_fail(): app = App() with pytest.raises(SystemExit): - app.set_original_doc("ab12", ['a', 1]) - assert app.original_doc["ab12"] == ['a', 1] + app.set_original_doc("ab12", ["a", 1]) + assert app.original_doc["ab12"] == ["a", 1] @staticmethod def test_app_get_original_doc_pass(): app = App() ret_doc = app.get_original_doc("ab12") - assert ret_doc == {'a': 1} + assert ret_doc == {"a": 1} @staticmethod def test_app_set_compiled_doc_fail_key_len(): app = App() with pytest.raises(SystemExit): - app.set_compiled_doc("ab22", {'a': 1}) + app.set_compiled_doc("ab22", {"a": 1}) @staticmethod def test_app_set_compiled_doc_fail_allowed_key(): app = App() with pytest.raises(SystemExit): - app.set_compiled_doc("ab22", {'a': 1, 'b': 2, 'c': 3, }) + app.set_compiled_doc( + "ab22", + { + "a": 1, + "b": 2, + "c": 3, + }, + ) @staticmethod def test_app_set_compiled_doc_pass(): app = App() - app.set_compiled_doc("ab22", {'request': 1, 'version': 2, 'variables': 3, }) + app.set_compiled_doc( + "ab22", {"request": 1, "version": 2, "variables": 3, "expose": None} + ) @staticmethod def test_app_set_compiled_doc_part_fail(): app = App() with pytest.raises(SystemExit): - app.set_compiled_doc("ab22", part="re", value={'a': 1, 'b': 2, 'c': 3, }) + app.set_compiled_doc( + "ab22", + part="re", + value={ + "a": 1, + "b": 2, + "c": 3, + }, + ) @staticmethod def test_app_set_compiled_doc_part_pass(): app = App() - app.set_compiled_doc("ab22", part="request", value={'a': 1, 'b': 2, 'c': 3, }) + app.set_compiled_doc( + "ab22", + part="request", + value={ + "a": 1, + "b": 2, + "c": 3, + }, + ) @staticmethod def test_app_set_compiled_doc_local_part_pass(): app = App() - app.set_compiled_doc("ab22", part="__local", value={'a': 1, 'b': 2, 'c': 3, }) + app.set_compiled_doc( + "ab22", + part="__local", + value={ + "a": 1, + "b": 2, + "c": 3, + }, + ) @staticmethod def test_app_get_compiled_doc_local_part_pass(): app = App() - assert app.get_compiled_doc("ab22", part="__local") == {'a': 1, 'b': 2, 'c': 3, } + assert app.get_compiled_doc("ab22", part="__local") == { + "a": 1, + "b": 2, + "c": 3, + } @staticmethod def test_app_get_compiled_doc_part_fail(): @@ -93,7 +130,11 @@ def test_app_get_compiled_doc_part_fail(): @staticmethod def test_app_get_compiled_doc_part_pass(): app = App() - assert app.get_compiled_doc("ab22", part="request") == {'a': 1, 'b': 2, 'c': 3, } + assert app.get_compiled_doc("ab22", part="request") == { + "a": 1, + "b": 2, + "c": 3, + } @staticmethod def test_app_load_original_doc_from_file_context_pass(): @@ -108,7 +149,9 @@ def test_app_load_original_doc_from_file_context_pass(): ) app.load_original_doc_from_file_context(ctx) - assert app.original_doc[ctx.filepath_hash] == ChkFileLoader.to_dict(ctx.filepath) + assert app.original_doc[ctx.filepath_hash] == ChkFileLoader.to_dict( + ctx.filepath + ) @staticmethod def test_config_pass(): @@ -116,3 +159,13 @@ def test_config_pass(): assert app.config("buffer_access_off") is True assert app.config("buffer_access_off", {"d": 1}) == {"d": 1} assert app.config("buffer_access_off") == {"d": 1} + + @staticmethod + def test_app_set_local_pass(): + app = App() + assert app.set_local("ab22", part="re", val=12) is True + + @staticmethod + def test_app_get_local_pass(): + app = App() + assert app.get_local("ab22", "re") == 12 diff --git a/tests/modules/http/http_entities_test.py b/tests/modules/http/http_entities_test.py index 390a5b38..00175d76 100644 --- a/tests/modules/http/http_entities_test.py +++ b/tests/modules/http/http_entities_test.py @@ -25,4 +25,4 @@ def test_get_plain_pass(self): handle_worker(http) response = handle_worker(http) - assert isinstance(response, dict) + assert isinstance(response, list) diff --git a/tests/modules/variables/string_lexical_analyzer_test.py b/tests/modules/variables/string_lexical_analyzer_test.py index 621beca5..91d85ce8 100644 --- a/tests/modules/variables/string_lexical_analyzer_test.py +++ b/tests/modules/variables/string_lexical_analyzer_test.py @@ -4,35 +4,101 @@ def test_replace_in_str_success(): - container = 'https://httpbin.org/put?foo={ $var_1 }&bar={$var_2}' - replace_with = {'var_1': 1, 'var_2': 'my name'} + container = "https://httpbin.org/put?foo={ $var_1 }&bar={$var_2}" + replace_with = {"var_1": 1, "var_2": "my name"} - assert 'https://httpbin.org/put?foo=1&bar=my name' == \ - StringLexicalAnalyzer.replace_in_str(container, replace_with) + assert ( + "https://httpbin.org/put?foo=1&bar=my name" + == StringLexicalAnalyzer.replace_in_str(container, replace_with) + ) def test_replace_in_str_empty_container_success(): - container = '' - replace_with = {'var_1': 1, 'var_2': 'my name'} + container = "" + replace_with = {"var_1": 1, "var_2": "my name"} - assert '' == StringLexicalAnalyzer.replace_in_str(container, replace_with) + assert "" == StringLexicalAnalyzer.replace_in_str(container, replace_with) def test_replace_in_str_integer_container_fail(): with pytest.raises(TypeError): - assert '' == StringLexicalAnalyzer.replace_in_str(12, {}) + assert "" == StringLexicalAnalyzer.replace_in_str(12, {}) def test_replace_in_str_empty_replace_with_fail(): - container = 'https://httpbin.org/put?foo={ $var_1 }&bar={$var_2}' + container = "https://httpbin.org/put?foo={ $var_1 }&bar={$var_2}" replace_with = {} assert container == StringLexicalAnalyzer.replace_in_str(container, replace_with) def test_replace_in_str_alter_replace_with_fail(): - container = 'https://httpbin.org/put?foo={ $var_1 }&bar={$var_2}' - replace_with = {'var_1': 1, 'var_3': 'my name'} + container = "https://httpbin.org/put?foo={ $var_1 }&bar={$var_2}" + replace_with = {"var_1": 1, "var_3": "my name"} - assert 'https://httpbin.org/put?foo=1&bar={$var_2}' == \ - StringLexicalAnalyzer.replace_in_str(container, replace_with) + assert ( + "https://httpbin.org/put?foo=1&bar={$var_2}" + == StringLexicalAnalyzer.replace_in_str(container, replace_with) + ) + + +def test_replace_in_str_when_combined_pass(): + container = "$var1:|$var_3" + replace_with = {"var1": 1, "var_3": "my name"} + # print(StringLexicalAnalyzer.replace_in_str(container, replace_with)) + + assert "1:|my name" == StringLexicalAnalyzer.replace_in_str(container, replace_with) + + +class TestStringLexicalAnalyzerReplace: + @staticmethod + def test_replace_pass_when_single_variable(): + replace_with = {"var1": 1, "var_3": "my name"} + + container = "$var1" + assert 1 == StringLexicalAnalyzer.replace(container, replace_with) + + container = "$var2" + assert "{ $var2 }" == StringLexicalAnalyzer.replace(container, replace_with) + + replace_with = {"var1": [1, 2], "var_3": "my name"} + + container = "{$var1}" + assert [1, 2] == StringLexicalAnalyzer.replace(container, replace_with) + + container = "{ $var1 }" + assert [1, 2] == StringLexicalAnalyzer.replace(container, replace_with) + + container = "{$var2}" + assert "{ $var2 }" == StringLexicalAnalyzer.replace(container, replace_with) + + @staticmethod + def test_replace_pass_when_multiple_variable(): + container = "$var1:$var_3" + + replace_with = {"var1": 2, "var_3": "my_name"} + assert "2:my_name" == StringLexicalAnalyzer.replace(container, replace_with) + + replace_with = {"var1": [1, 2], "var_3": "my_name"} + assert "[1, 2]:my_name" == StringLexicalAnalyzer.replace( + container, replace_with + ) + + container = "$var1:$var_3:$var4" + replace_with = {"var1": [1, 2], "var_3": "my_name", "var4": {"a": 1}} + assert "[1, 2]:my_name:{'a': 1}" == StringLexicalAnalyzer.replace( + container, replace_with + ) + + @staticmethod + def test_replace_pass_when_multiple_variable_with_curly_braces(): + replace_with = {"var1": 2, "var_3": "my_name"} + + container = "$var1:{$var_3}" + assert "2:my_name" == StringLexicalAnalyzer.replace(container, replace_with) + + container = "{$var1}:{$var_3}" + assert "2:my_name" == StringLexicalAnalyzer.replace(container, replace_with) + + container = "{$var1}:{{$var_3}}" + assert "2:{my_name}" == StringLexicalAnalyzer.replace(container, replace_with) diff --git a/tests/modules/variables/support_test.py b/tests/modules/variables/support_test.py index 167a7893..30bbd616 100644 --- a/tests/modules/variables/support_test.py +++ b/tests/modules/variables/support_test.py @@ -1,18 +1,19 @@ from chk.infrastructure.containers import App from chk.infrastructure.file_loader import FileContext +from chk.infrastructure.helper import data_set, data_get from chk.modules.variables.support import parse_args, VariableMixin class TestParseArgs: def test_parse_args_pass_for_unique(self): - argv_s = ['Var1=Val1', 'Var2=Val2', 'Var3=Val3', 'Var=Val'] + argv_s = ["Var1=Val1", "Var2=Val2", "Var3=Val3", "Var=Val"] response = parse_args(argv_s) assert isinstance(response, dict) assert len(response) == 4 def test_parse_args_pass_for_override(self): - argv_s = ['Var1=Val1', 'Var2=Val2', 'Var3=Val3', 'Var1=Val4'] + argv_s = ["Var1=Val1", "Var2=Val2", "Var3=Val3", "Var1=Val4"] response = parse_args(argv_s) assert isinstance(response, dict) @@ -28,18 +29,17 @@ def get_file_context(self) -> FileContext: class TestVariablePrepareValueTable: - def test_variable_prepare_value_table_pass(self): app = App() config = { - 'var_1': "bar", - 'var_2': 2, - 'var_3': 'ajax{$var_1}', - 'var_4': 'ajax{$Var_1}', - 'var_5': '{$var_2}', + "var_1": "bar", + "var_2": 2, + "var_3": "ajax{$var_1}", + "var_4": "ajax{$Var_1}", + "var_5": "{$var_2}", } - file_ctx = FileContext(filepath_hash='ab12') + file_ctx = FileContext(filepath_hash="ab12") app.set_compiled_doc(file_ctx.filepath_hash, part="variables", value=config) ver = HavingVariables(file_ctx) @@ -48,25 +48,25 @@ def test_variable_prepare_value_table_pass(self): variables = app.get_compiled_doc(file_ctx.filepath_hash, part="variables") assert variables == { - 'var_1': "bar", - 'var_2': 2, - 'var_3': 'ajaxbar', - 'var_4': 'ajax{$Var_1}', - 'var_5': '2', + "var_1": "bar", + "var_2": 2, + "var_3": "ajaxbar", + "var_4": "ajax{$Var_1}", + "var_5": "2", } def test_variable_handle_value_table_for_absolute_pass(self): app = App() config = { - 'var_1': "bar", - 'var_2': 2, - 'var_3': 'ajax{$var_1}', - 'var_4': 'ajax{$Var_1}', - 'var_5': '{$var_2}', + "var_1": "bar", + "var_2": 2, + "var_3": "ajax{$var_1}", + "var_4": "ajax{$Var_1}", + "var_5": "{$var_2}", } - file_ctx = FileContext(filepath_hash='ab12') + file_ctx = FileContext(filepath_hash="ab12") app.set_compiled_doc(file_ctx.filepath_hash, part="variables", value=config) ver = HavingVariables(file_ctx) @@ -75,26 +75,104 @@ def test_variable_handle_value_table_for_absolute_pass(self): ver.variable_handle_value_table_for_absolute(variables_orig, variables) assert len(variables) == 2 - assert variables == {'var_1': "bar", 'var_2': 2} + assert variables == {"var_1": "bar", "var_2": 2} def test_variable_handle_value_table_for_composite_pass(self): app = App() config = { - 'var_1': "bar", - 'var_2': 2, - 'var_3': 'ajax{$var_1}', - 'var_4': 'ajax{$Var_1}', - 'var_5': '{$var_2}', + "var_1": "bar", + "var_2": 2, + "var_3": "ajax{$var_1}", + "var_4": "ajax{$Var_1}", + "var_5": "{$var_2}", } - file_ctx = FileContext(filepath_hash='ab12') + file_ctx = FileContext(filepath_hash="ab12") app.set_compiled_doc(file_ctx.filepath_hash, part="variables", value=config) ver = HavingVariables(file_ctx) - variables: dict = {'var_1': "bar", 'var_2': 2} + variables: dict = {"var_1": "bar", "var_2": 2} variables_orig = app.get_compiled_doc(file_ctx.filepath_hash, part="variables") ver.variable_handle_value_table_for_composite(variables_orig, variables) assert len(variables) == 5 - assert variables == {'var_1': "bar", 'var_2': 2, 'var_3': 'ajaxbar', 'var_4': 'ajax{$Var_1}', 'var_5': '2',} + assert variables == { + "var_1": "bar", + "var_2": 2, + "var_3": "ajaxbar", + "var_4": "ajax{$Var_1}", + "var_5": "2", + } + + +class TestVariableMixin: + """Test VariableMixin""" + + def test_expose_as_dict_pass_for_null_doc(self): + app = App() + config = {"expose": []} + + file_ctx = FileContext(filepath_hash="ab31") + data_set(app.original_doc, file_ctx.filepath_hash, config) + + var = HavingVariables(file_ctx) + + assert var.expose_as_dict() == config + + def test_expose_as_dict_pass_for_doc_value_none(self): + app = App() + config = {"expose": None} + + file_ctx = FileContext(filepath_hash="ab31") + data_set(app.original_doc, file_ctx.filepath_hash, config) + + var = HavingVariables(file_ctx) + + assert var.expose_as_dict() == config + + def test_expose_as_dict_pass_for_doc(self): + app = App() + config = {"expose": [".response.code", ".response.headers"]} + + file_ctx = FileContext(filepath_hash="ab31") + data_set(app.original_doc, file_ctx.filepath_hash, config) + + var = HavingVariables(file_ctx) + + assert var.expose_as_dict() == config + + def test_expose_validated_for_doc(self): + app = App() + config = {"expose": [".response.code", ".response.headers"]} + + file_ctx = FileContext(filepath_hash="ab31") + data_set(app.original_doc, file_ctx.filepath_hash, config) + var = HavingVariables(file_ctx) + + assert var.expose_validated() == config + + def test_get_exposable_pass(self): + app = App() + compiled_doc = { + "variables": {"var1": 1, "var2": 2}, + "__local": { + "_response": { + "code": 201, + "headers": ["Header 1: Head val 1", "Header 2: Head val 2"], + } + }, + "expose": ["$_response.code", "$_response.headers", "$var1:$var2"] + } + + file_ctx = FileContext(filepath_hash="ab31") + data_set(app.compiled_doc, file_ctx.filepath_hash, compiled_doc) + + var = HavingVariables(file_ctx) + var.make_exposable() + assert isinstance(var.get_exposable(), list) + assert var.get_exposable() == [ + 201, + ["Header 1: Head val 1", "Header 2: Head val 2"], + "1:2", + ] diff --git a/tests/resources/storage/sample_config/UserOk.chk b/tests/resources/storage/sample_config/UserOk.chk index dfc3b025..9c6b204b 100644 --- a/tests/resources/storage/sample_config/UserOk.chk +++ b/tests/resources/storage/sample_config/UserOk.chk @@ -1,5 +1,12 @@ version: 'default:http:0.7.2' +variables: + Id: 1 + request: - url: https://reqres.in/api/users/1 + url: https://reqres.in/api/users/{$Id} method: GET + +expose: + - $_response.code + - $_response.body \ No newline at end of file