From 225c85c45703c9721fc60311d5f7c3486d60e26f Mon Sep 17 00:00:00 2001 From: Prasanth Date: Mon, 30 Dec 2024 08:23:13 +0530 Subject: [PATCH] var separate syntax for variable (#348) * Feat: support var * support variable templating and randomGenerations --- .../server/handlers/basic_handlers.py | 57 +++--- dotextensions/server/handlers/http2postman.py | 6 +- dothttp/http.tx | 19 +- dothttp/models/parse_models.py | 6 + dothttp/parse/__init__.py | 43 ++++- dothttp/parse/dsl_jsonparser.py | 112 ++++++------ dothttp/parse/request_base.py | 46 +++-- dothttp/utils/property_util.py | 56 +++--- examples/misc/unix_socket.http | 2 +- pyproject.toml | 2 +- test/conftest.py | 10 +- test/core/var/.dothttp.json | 5 + test/core/var/__init__.py | 0 test/core/var/child.http | 16 ++ test/core/var/grand_child.http | 15 ++ test/core/var/host.http | 6 + test/core/var/json.http | 28 +++ test/core/var/math.http | 9 + test/core/var/override.http | 8 + test/core/var/par.http | 7 + test/core/var/templating.http | 10 ++ test/core/var/test_var.py | 167 ++++++++++++++++++ test/extensions/test_commands.py | 50 ++++-- test/extensions/test_content.py | 63 +++++++ 24 files changed, 592 insertions(+), 151 deletions(-) create mode 100644 test/core/var/.dothttp.json create mode 100644 test/core/var/__init__.py create mode 100644 test/core/var/child.http create mode 100644 test/core/var/grand_child.http create mode 100644 test/core/var/host.http create mode 100644 test/core/var/json.http create mode 100644 test/core/var/math.http create mode 100644 test/core/var/override.http create mode 100644 test/core/var/par.http create mode 100644 test/core/var/templating.http create mode 100644 test/core/var/test_var.py diff --git a/dotextensions/server/handlers/basic_handlers.py b/dotextensions/server/handlers/basic_handlers.py index 78cdd21..36472e6 100644 --- a/dotextensions/server/handlers/basic_handlers.py +++ b/dotextensions/server/handlers/basic_handlers.py @@ -151,7 +151,7 @@ def get_request_result(self, command, comp: RequestCompiler): # redirects can add cookies comp.httpdef.headers["cookie"] = resp.request.headers["cookie"] try: - data.update({"http": self.get_http_from_req(comp.httpdef)}) + data.update({"http": self.get_http_from_req(comp.httpdef, comp.property_util)}) except Exception as e: logger.error("ran into error regenerating http def from parsed object") data.update( @@ -172,9 +172,9 @@ def get_request_comp(self, config): return RequestCompiler(config) @staticmethod - def get_http_from_req(request: HttpDef): + def get_http_from_req(request: HttpDef, property_util: "PropertyProvider"): http_def = MultidefHttp(import_list=[], allhttps=[request.get_http_from_req()]) - return HttpFileFormatter.format(http_def) + return HttpFileFormatter.format(http_def, property_util=property_util) CONTEXT_SEP = """ @@ -201,34 +201,29 @@ def load_model(self): self.content = self.content + CONTEXT_SEP + CONTEXT_SEP.join(self.args.contexts) def select_target(self): - try: - # first try to resolve target from current context - super().select_target() - except UndefinedHttpToExtend as ex: - # if it weren't able to figure out context, try to resolve from - # contexts - for context in self.args.contexts: - try: - # if model is generated, try to figure out target - model: MultidefHttp = dothttp_model.model_from_str(context) - # by including targets in to model - self.model.allhttps = self.model.allhttps + model.allhttps - if model.import_list and model.import_list.filename: - if self.model.import_list and self.model.import_list.filename: - self.model.import_list.filename += ( - model.import_list.filename - ) - else: - self.model.import_list = model.import_list - self.load_imports() - self.content += context + "\n\n" + context - return super(ContentBase, self).select_target() - except Exception as e: - # contexts, can not always be correct syntax - # in such scenarios, don't complain, try to resolve with - # next contexts - logger.info("ignoring exception, context is not looking good") - raise ex + for context in self.args.contexts: + try: + # if model is generated, try to figure out target + model: MultidefHttp = dothttp_model.model_from_str(context) + # by including targets in to model + self.load_properties_from_var(model, self.property_util) + self.model.allhttps = self.model.allhttps + model.allhttps + if model.import_list and model.import_list.filename: + if self.model.import_list and self.model.import_list.filename: + self.model.import_list.filename += ( + model.import_list.filename + ) + else: + self.model.import_list = model.import_list + self.load_imports() + self.content += context + "\n\n" + context + + except Exception as e: + # contexts, can not always be correct syntax + # in such scenarios, don't complain, try to resolve with + # next contexts + logger.info("ignoring exception, context is not looking good") + return super(ContentBase, self).select_target() class ContentRequestCompiler(ContentBase, RequestCompiler): diff --git a/dotextensions/server/handlers/http2postman.py b/dotextensions/server/handlers/http2postman.py index 278c1f1..87a23c6 100644 --- a/dotextensions/server/handlers/http2postman.py +++ b/dotextensions/server/handlers/http2postman.py @@ -29,6 +29,7 @@ ) from dothttp.parse.request_base import RequestCompiler from dothttp.utils.common import json_to_urlencoded_array +from dothttp.utils.property_util import get_no_replace_property_provider from ..models import Command, Result from ..postman2_1 import ( POSTMAN_2_1, @@ -63,6 +64,7 @@ class PostManCompiler(RequestCompiler): def __init__(self, config, model): self.model = model super(PostManCompiler, self).__init__(config) + self.property_util = get_no_replace_property_provider() def load_content(self): return @@ -349,14 +351,14 @@ def get_http_to_postman_request(http: HttpDef, description="") -> RequestClass: # json to key value pairs json_to_urlencoded_array( # textx object to json - json_or_array_to_json(payload.data, lambda k: k) + json_or_array_to_json(payload.data) ) ] request.body = body elif json_payload := payload.json: body.mode = Mode.RAW body.options = {"language": "json"} - body.raw = json.dumps(json_or_array_to_json(json_payload, lambda x: x)) + body.raw = json.dumps(json_or_array_to_json(json_payload)) if not request.header: request.header = [] request.header.append( diff --git a/dothttp/http.tx b/dothttp/http.tx index be5e2bb..5de7934 100644 --- a/dothttp/http.tx +++ b/dothttp/http.tx @@ -1,5 +1,18 @@ -//HTTP: ram=HTTP2 -MULTISET: (import_list=IMPORT)? (allhttps=HTTP+)?; + +MULTISET: (import_list=IMPORT)? (variables=VAR*)? (allhttps=HTTP+)?; + + +VAR: + "var" name=ID ("=" ( func=FunctionCall | inter=InterpolatedString | value=Value ))? ';' +; + +InterpolatedString: + '$"' /[^"]*/ '"' +; + +FunctionCall: + name=ID ('(' args=INT ')')? +; IMPORT: ('import' filename=String ';')* ; @@ -208,7 +221,7 @@ Array: ; Value: - flt=Float | int=Int | bl=Bool | null="null" |strs += TRIPLE_OR_DOUBLE_STRING | var=VarString | object=Object | array=Array | expr=Expression + flt=Float | int=Int | bl=Bool | null="null" | strs += TRIPLE_OR_DOUBLE_STRING | var=VarString | object=Object | array=Array | expr=Expression ; Expression: diff --git a/dothttp/models/parse_models.py b/dothttp/models/parse_models.py index aab611f..1d9e130 100644 --- a/dothttp/models/parse_models.py +++ b/dothttp/models/parse_models.py @@ -6,6 +6,11 @@ from ..exceptions import DotHttpException +@dataclass +class Variable: + name: str + value: Union[None, str] + @dataclass class NameWrap: name: str @@ -241,6 +246,7 @@ class ImportStmt: class MultidefHttp: import_list: Optional[ImportStmt] allhttps: List[Http] + variables : List[Variable] = field(default_factory=lambda: []) # one can get list of services and regions from diff --git a/dothttp/parse/__init__.py b/dothttp/parse/__init__.py index f72ddd8..b913bcd 100644 --- a/dothttp/parse/__init__.py +++ b/dothttp/parse/__init__.py @@ -45,7 +45,7 @@ from ..utils.common import get_real_file_path, triple_or_double_tostring from ..utils.constants import * from ..utils.property_util import PropertyProvider -from .dsl_jsonparser import json_or_array_to_json +from .dsl_jsonparser import json_or_array_to_json, jsonmodel_to_json def install_unix_socket_scheme(): @@ -236,6 +236,7 @@ def _load_imports( model, filename ): import_list += model.allhttps + BaseModelProcessor.load_properties_from_var(model, property_util) property_util.add_infile_properties(content) @staticmethod @@ -352,7 +353,43 @@ def load_props_needed_for_content(self): self._load_props_from_content(self.content, self.property_util) def _load_props_from_content(self, content, property_util: PropertyProvider): + self.load_properties_from_var(self.model, property_util) property_util.add_infile_properties(content) + + @staticmethod + def load_properties_from_var(model:MultidefHttp, property_util: PropertyProvider): + ## this has to taken care by property util + ## but it will complicate the code + for variable in model.variables: + if variable.value: + var_value = jsonmodel_to_json(variable.value) + property_util.add_infile_property_from_var(variable.name, var_value) + elif variable.func: + func_name = f"${variable.func.name}" + if func_name in property_util.rand_map: + func = property_util.rand_map[func_name] + if variable.func.args: + args = variable.func.args + var_value = func(args) + else: + var_value = func() + else: + var_value = variable.func.name + property_util.add_infile_property_from_var(variable.name, var_value) + elif variable.inter: + class PropertyResolver: + # hassle of creating class is to make it work with format_map + # instead of format which can be used with dict and can cause memory leak + def __getitem__(self, key): + if key in property_util.command_line_properties: + return property_util.command_line_properties[key] + if key in property_util.env_properties: + return property_util.env_properties[key] + if key in property_util.infile_properties and property_util.infile_properties[key].value is not None: + return property_util.infile_properties[key].value + raise KeyError(key) + var_value = variable.inter[2:-1].format_map(PropertyResolver()) + property_util.add_infile_property_from_var(variable.name, var_value) class HttpDefBase(BaseModelProcessor): @@ -552,7 +589,7 @@ def _load_payload(self): request_logger.debug(f"payload for request is `{content}`") return Payload(content, header=mimetype) elif data_json := self.http.payload.datajson: - d = json_or_array_to_json(data_json, self.get_updated_content) + d = json_or_array_to_json(data_json, self.property_util) if isinstance(d, list): raise PayloadDataNotValidException( payload=f"data should be json/str, current: {d}" @@ -563,7 +600,7 @@ def _load_payload(self): elif upload_filename := self.http.payload.file: return self.load_payload_fileinput(upload_filename) elif json_data := self.http.payload.json: - d = json_or_array_to_json(json_data, self.get_updated_content) + d = json_or_array_to_json(json_data, self.property_util) return Payload(json=d, header=MIME_TYPE_JSON) elif files_wrap := self.http.payload.fileswrap: files = [] diff --git a/dothttp/parse/dsl_jsonparser.py b/dothttp/parse/dsl_jsonparser.py index 82c80e4..012721b 100644 --- a/dothttp/parse/dsl_jsonparser.py +++ b/dothttp/parse/dsl_jsonparser.py @@ -1,67 +1,67 @@ import json -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union +from ..utils.property_util import PropertyProvider, get_no_replace_property_provider from ..utils.common import triple_or_double_tostring -def json_or_array_to_json(model, update_content_func) -> Union[Dict, List]: - if isinstance(model, Dict) or isinstance(model, List): - # TODO - # this is bad - # hooking here could lead to other issues - return model - if array := model.array: - return [jsonmodel_to_json(value, update_content_func) for value in array.values] - elif json_object := model.object: - return { - # TODO i'm confused about key weather it should be string or int or float (value has float, number, - # boolean, null) but key is unsupported by requests - get_key(member, update_content_func): jsonmodel_to_json( - member.value, update_content_func - ) - for member in json_object.members - } +class JsonParser: + def __init__(self, property_util: PropertyProvider): + self.property_util = property_util + def json_or_array_to_json(self, model) -> Union[Dict, List]: + if isinstance(model, Dict) or isinstance(model, List): + return model + if array := model.array: + return [self.jsonmodel_to_json(value) for value in array.values] + elif json_object := model.object: + return { + self.get_key(member): self.jsonmodel_to_json(member.value) + for member in json_object.members + } -def get_key(member, update_content_func): - if member.key: - return triple_or_double_tostring(member.key, update_content_func) - elif member.var: - return update_content_func(member.var) + def get_key(self, member): + if member.key: + return triple_or_double_tostring(member.key, self.property_util.get_updated_content) + elif member.var: + return self.property_util.get_updated_obj_content(member.var) + def jsonmodel_to_json(self, model): + if str_value := model.strs: + return triple_or_double_tostring(str_value, self.property_util.get_updated_content) + elif var_value := model.var: + return self.property_util.get_updated_obj_content(var_value) + elif int_val := model.int: + return int_val.value + elif flt := model.flt: + return flt.value + elif bl := model.bl: + return bl.value + elif json_object := model.object: + return { + self.get_key(member): self.jsonmodel_to_json(member.value) + for member in json_object.members + } + elif array := model.array: + return [self.jsonmodel_to_json(value) for value in array.values] + elif model == "null": + return None + elif expr := model.expr: + return eval(expr) -def jsonmodel_to_json(model, update_content_func): - # if length if array is 0, which means, its not string - if str_value := model.strs: - return triple_or_double_tostring(str_value, update_content_func) - elif var_value := model.var: - return get_json_data(var_value, update_content_func) - elif int_val := model.int: - return int_val.value - elif flt := model.flt: - return flt.value - elif bl := model.bl: - return bl.value - elif json_object := model.object: - return { - get_key(member, update_content_func): jsonmodel_to_json( - member.value, update_content_func - ) - for member in json_object.members - } - elif array := model.array: - return [jsonmodel_to_json(value, update_content_func) for value in array.values] - elif model == "null": - return None - elif expr := model.expr: - return eval(expr) +# Supporting function +def json_or_array_to_json(model, property_util: Optional[PropertyProvider]=None) -> Union[Dict, List]: + if property_util is None: + # This is a hack to ignore replacement of variables where it is not needed + property_util = get_no_replace_property_provider() + parser = JsonParser(property_util) + return parser.json_or_array_to_json(model) -def get_json_data(var_value, update_content_func): - content: str = update_content_func(var_value) - if content == var_value: - return var_value - try: - return json.loads(content) - except ValueError: - return content + +def jsonmodel_to_json(model, property_util: Optional[PropertyProvider]=None) -> Union[Dict, List]: + if property_util is None: + # This is a hack to ignore replacement of variables where it is not needed + property_util = get_no_replace_property_provider() + parser = JsonParser(property_util) + return parser.jsonmodel_to_json(model) \ No newline at end of file diff --git a/dothttp/parse/request_base.py b/dothttp/parse/request_base.py index ea988a2..3f4ec66 100644 --- a/dothttp/parse/request_base.py +++ b/dothttp/parse/request_base.py @@ -3,7 +3,7 @@ import os from http.cookiejar import LWPCookieJar from pprint import pprint -from typing import Union +from typing import Optional, Union from urllib.parse import unquote, urlparse, urlunparse import jstyleson as json @@ -16,6 +16,8 @@ from requests_pkcs12 import Pkcs12Adapter from textx import metamodel_from_file +from ..utils.property_util import PropertyProvider, Property + from ..exceptions import DothttpUnSignedCertException from ..models.parse_models import Http, HttpFileType, MultidefHttp, ScriptType from ..parse import ( @@ -262,9 +264,31 @@ def load(self): self.prop_cache = {} @staticmethod - def format_http(http: Http): + def format_http(http: Http, property_util: Optional[PropertyProvider]=None): output_str = "" new_line = "\n" + + if property_util: + total_props = {} + total_props.update(property_util.infile_properties) + total_props.update(property_util.env_properties) + total_props.update(property_util.system_command_properties) + total_props.update(property_util.command_line_properties) + for key, value in total_props.items(): + if isinstance(value, Property): + if value.value: + if isinstance(value.value, str): + output_str += f"var {key} = '{value.value}' ;" + else: + output_str += f"var {key} = {value.value} ;" + else: + output_str += f"var {key} ;" + else: + if isinstance(value, str): + output_str += f"var {key} = '{value}' ;" + else: + output_str += f"var {key} = {value} ;" + output_str += new_line if getattr(http, "description", None): for line in http.description.splitlines(): output_str += "// " + line + new_line @@ -371,12 +395,12 @@ def check_for_quotes(line): data = "'" + data.replace("'", "\\'") + "'" p = f'text({data}{(" ," + mime_type) if mime_type else ""})' if datajson := payload.datajson: - parsed_data = json_or_array_to_json(datajson, lambda a: a) + parsed_data = json_or_array_to_json(datajson) p = f"urlencoded({json.dumps(parsed_data, indent=4)})" elif filetype := payload.file: p = f'< "{filetype}" {(" ;" + mime_type) if mime_type else ""}' elif json_data := payload.json: - parsed_data = json_or_array_to_json(json_data, lambda a: a) + parsed_data = json_or_array_to_json(json_data) p = f"json({JSON_ENCODER.encode(parsed_data)})" elif files_wrap := payload.fileswrap: @@ -413,7 +437,7 @@ def function(multipart): return output_str @staticmethod - def apply_httpbook(model: MultidefHttp): + def apply_httpbook(model: MultidefHttp, property_util: Optional[PropertyProvider]=None): arr = [] for http in model.allhttps: arr.append( @@ -427,20 +451,20 @@ def apply_httpbook(model: MultidefHttp): return json.dumps(arr) @staticmethod - def apply_http(model: MultidefHttp): + def apply_http(model: MultidefHttp, property_util=None): output_str = "" for http in model.allhttps: - output_str = output_str + HttpFileFormatter.format_http(http) + output_str = output_str + HttpFileFormatter.format_http(http, property_util) return output_str @staticmethod - def format(model: MultidefHttp, filetype=HttpFileType.Httpfile): + def format(model: MultidefHttp, filetype=HttpFileType.Httpfile, property_util=None): if filetype == HttpFileType.Httpfile: - return HttpFileFormatter.apply_http(model) - return HttpFileFormatter.apply_httpbook(model) + return HttpFileFormatter.apply_http(model, property_util) + return HttpFileFormatter.apply_httpbook(model, property_util) def run(self): - formatted = self.format(self.model) + formatted = self.format(self.model, property_util=self.property_util) if self.args.stdout: print(formatted) else: diff --git a/dothttp/utils/property_util.py b/dothttp/utils/property_util.py index 314c777..7af1697 100644 --- a/dothttp/utils/property_util.py +++ b/dothttp/utils/property_util.py @@ -150,12 +150,11 @@ class PropertyProvider: expression_regex = re.compile(r"\$expr:(?P.*)") def __init__(self, property_file=""): - self.command_line_properties = {} - self.env_properties = {} self.infile_properties: Dict[str, Property] = {} - self.property_file = property_file - self._resolvable_properties = set() + self.env_properties = {} self.system_command_properties = {} + self.command_line_properties = {} + self.property_file = property_file self.is_running_system_command_enabled = False def enable_system_command(self): @@ -242,17 +241,19 @@ def get_updated_content(self, content, type="str"): content_prop_needed, props_needed = self.check_properties_for_content( content) for var in props_needed: - # command line props take preference - func = ( - self.resolve_property_string - if type == "str" - else self.resolve_property_object - ) - value = func(var) - base_logger.debug(f"using `{value}` for property {var}") - for text_to_replace in content_prop_needed[var].text: - content = content.replace("{{" + text_to_replace + "}}", value) + if type == "str": + value = self.resolve_property_string(var) + for text_to_replace in content_prop_needed[var].text: + content = content.replace( + "{{" + text_to_replace + "}}", str(value) + ) + else: + content = self.resolve_property_object(var) + base_logger.debug(f"using `{content}` for property {var}") return content + + def get_updated_obj_content(self, content): + return self.get_updated_content(content, "obj") def validate_n_gen(self, prop, cache: Dict[str, Property]): p: Union[Property, None] = None @@ -333,6 +334,10 @@ def resolve_special(prop, match): def get_random_match(prop): match = PropertyProvider.random_string_regex.match(prop) return match + + def add_infile_property_from_var(self, key, value): + # with var, we support overriding + self.infile_properties[key] = Property([''], key, value) def resolve_system_command_prop(self, key): command = self.system_command_properties.get(key) @@ -380,10 +385,21 @@ def find_according_to_category(key): ) return prop_value - def resolve_property_object(self, key: str) -> object: - val = self.resolve_property_string(key) - try: - return json.loads(val) - except JSONDecodeError: - base_logger.debug(f"property `{key}` value non json decodable") + def resolve_property_object(self, content: str) -> object: + val = self.resolve_property_string(content) + # if val is string then try to convert to json + if isinstance(val, str): + try: + return json.loads(val) + except JSONDecodeError: + base_logger.debug(f"property `{content}` value non json decodable") + return val + else: return val + + +def get_no_replace_property_provider(): + property_util = PropertyProvider() + property_util.get_updated_content = lambda x: x + property_util.get_updated_obj_content = lambda x: x + return property_util \ No newline at end of file diff --git a/examples/misc/unix_socket.http b/examples/misc/unix_socket.http index 65f8af4..569593d 100644 --- a/examples/misc/unix_socket.http +++ b/examples/misc/unix_socket.http @@ -1,4 +1,4 @@ -@name("docker base") +@name("simple") # docker base GET "http+unix://%2Fvar%2Frun%2Fdocker.sock" diff --git a/pyproject.toml b/pyproject.toml index f1dda4c..8754bfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dothttp-req" -version = "0.0.43" +version = "0.0.44a1" description = "Dothttp is Simple http client for testing and development" authors = ["Prasanth "] license = "MIT" diff --git a/test/conftest.py b/test/conftest.py index f90dd2f..0ede717 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,10 +5,12 @@ @pytest.fixture(scope='session', autouse=True) def start_httpbin_container(): # Setup: Start the Docker container - container_id = subprocess.check_output( - ["docker", "run", "-d", "-p", "8000:80", "kennethreitz/httpbin"] - ).decode().strip() - + try: + container_id = subprocess.check_output( + ["docker", "run", "-d", "-p", "8000:80", "kennethreitz/httpbin"] + ).decode().strip() + except: + pass # Wait for the container to be ready time.sleep(5) # Adjust the sleep time as needed diff --git a/test/core/var/.dothttp.json b/test/core/var/.dothttp.json new file mode 100644 index 0000000..05aa211 --- /dev/null +++ b/test/core/var/.dothttp.json @@ -0,0 +1,5 @@ +{ + "test": { + "a": "b" + } +} \ No newline at end of file diff --git a/test/core/var/__init__.py b/test/core/var/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/core/var/child.http b/test/core/var/child.http new file mode 100644 index 0000000..199f53a --- /dev/null +++ b/test/core/var/child.http @@ -0,0 +1,16 @@ +import "./par.http"; + +var secInDay = (24 * 60 * 60); + +POST "https://httpbin.org/post" +? a = "{{a}}" +json( + { + "a": "{{a}}", + "b": "{{b}}", + "c": "{{c}}", + "d": {{d}}, + "d2": "{{d}}", + "jsonData": {{jsonData}} + } +) \ No newline at end of file diff --git a/test/core/var/grand_child.http b/test/core/var/grand_child.http new file mode 100644 index 0000000..78354ad --- /dev/null +++ b/test/core/var/grand_child.http @@ -0,0 +1,15 @@ +import "./child.http"; + +POST "https://httpbin.org/post" +? a = "{{a}}" +json( + { + "a": "{{a}}", + "b": "{{b}}", + "c": "{{c}}", + "d": {{d}}, + "d2": "{{d}}", + "jsonData": {{jsonData}}, + "secInDay": "{{secInDay}}", + } +) \ No newline at end of file diff --git a/test/core/var/host.http b/test/core/var/host.http new file mode 100644 index 0000000..0ff7174 --- /dev/null +++ b/test/core/var/host.http @@ -0,0 +1,6 @@ +var a = 10; +var b = 20; + +GET "https://httpbin.org" +? a = "{{a}}" +? b = "{{b}}" \ No newline at end of file diff --git a/test/core/var/json.http b/test/core/var/json.http new file mode 100644 index 0000000..ec5f30f --- /dev/null +++ b/test/core/var/json.http @@ -0,0 +1,28 @@ +var jsonData = { + "secInDay": (24 * 60 * 60), + "secInHour": (60 * 60), + "true": true, + "false": false, + "nested" : [ + 1, 2, 4.4, "string", true, false, null, { + "key": "value" + } + ], + "nestedObject": { + "key": { + "key": "value" + } + } +}; + +var okay = "okay"; + +POST "http://localhost:8000/post" +json({ + jsonData: {{jsonData}}, + {{okay}}: "okay", + "nested": { + "jsonData": {{jsonData}}, + {{okay}}: "okay" + } +}) diff --git a/test/core/var/math.http b/test/core/var/math.http new file mode 100644 index 0000000..9feb76f --- /dev/null +++ b/test/core/var/math.http @@ -0,0 +1,9 @@ +var secInDay = (24 * 60 * 60); +var secInHour = (60 * 60); + + +POST "http://localhost:8000/post" +json({ + "secInDay": "{{secInDay}}", + "secInHour": {{secInHour}} +}) diff --git a/test/core/var/override.http b/test/core/var/override.http new file mode 100644 index 0000000..3c29d73 --- /dev/null +++ b/test/core/var/override.http @@ -0,0 +1,8 @@ +import "./par.http"; +var a = 10; + + +POST "https://httpbin.org/post" +json({ + "a": {{a}} +}) \ No newline at end of file diff --git a/test/core/var/par.http b/test/core/var/par.http new file mode 100644 index 0000000..fad42ff --- /dev/null +++ b/test/core/var/par.http @@ -0,0 +1,7 @@ +var a = "a"; +var b = "b"; +var c = "c"; +var d = true; +var jsonData = { + "secInDay": (24 * 60 * 60), +}; \ No newline at end of file diff --git a/test/core/var/templating.http b/test/core/var/templating.http new file mode 100644 index 0000000..5b8c195 --- /dev/null +++ b/test/core/var/templating.http @@ -0,0 +1,10 @@ +var a = 10; +var c = randomStr(10); +var b = $"{a} + {a} = 20"; + +POST "http://localhost:8000/post" +? "c" = "{{c}}" +json({ + "a": {{a}}, + "b": {{b}}, +}) \ No newline at end of file diff --git a/test/core/var/test_var.py b/test/core/var/test_var.py new file mode 100644 index 0000000..537d0ab --- /dev/null +++ b/test/core/var/test_var.py @@ -0,0 +1,167 @@ +import json +from test import TestBase +from test.core.test_request import dir_path + +from urllib.parse import urlparse + +base_dir = f"{dir_path}/var" + + +class VarSubstitutionTest(TestBase): + def test_substitution(self): + req = self.get_request(f"{base_dir}/host.http") + self.assertEqual("https://httpbin.org/?a=10&b=20", + req.url, "incorrect url") + + def test_math(self): + req = self.get_request(f"{base_dir}/math.http") + self.assertEqual( + {"secInDay": "86400", "secInHour": 3600}, + json.loads(req.body), + "incorrect body", + ) + + def test_json(self): + req = self.get_request(f"{base_dir}/json.http") + self.assertEqual( + { + 'jsonData': { + 'secInDay': 86400, + 'secInHour': 3600, + "true": True, + "false": False, + "nested": [ + 1, 2, 4.4, "string", True, False, None, { + "key": "value" + } + ], + "nestedObject": { + "key": { + "key": "value" + } + } + }, + "okay": "okay", + "nested": { + 'jsonData': { + 'secInDay': 86400, + 'secInHour': 3600, + "true": True, + "false": False, + "nested": [ + 1, 2, 4.4, "string", True, False, None, { + "key": "value" + } + ], + "nestedObject": { + "key": { + "key": "value" + } + } + + }, + "okay": "okay" + } + }, + json.loads(req.body), + "incorrect body", + ) + + def test_import_sub(self): + req = self.get_request(f"{base_dir}/child.http") + self.assertEqual( + { + "a": "a", + "b": "b", + "c": "c", + "d": True, + "d2": "true", + "jsonData": { + "secInDay": 86400, + } + }, + json.loads(req.body), + "incorrect body", + ) + + def test_import_nested(self): + req = self.get_request(f"{base_dir}/grand_child.http") + self.assertEqual( + { + "a": "a", + "b": "b", + "c": "c", + "d": True, + "d2": "true", + "jsonData": { + "secInDay": 86400, + }, + "secInDay": '86400', + }, + json.loads(req.body), + "incorrect body", + ) + + def test_sub_external_command_properties(self): + req = self.get_request( + f"{base_dir}/grand_child.http", properties={"secInDay=0"}) + self.assertEqual( + { + "a": "a", + "b": "b", + "c": "c", + "d": True, + "d2": "true", + "jsonData": { + "secInDay": 86400, + }, + "secInDay": '0', + }, + json.loads(req.body), + "incorrect body", + ) + + def test_sub_external_env_properties(self): + req = self.get_request(f"{base_dir}/grand_child.http", env=["test"]) + self.assertEqual( + { + "a": "b", + "b": "b", + "c": "c", + "d": True, + "d2": "true", + "jsonData": { + "secInDay": 86400, + }, + "secInDay": '86400', + }, + json.loads(req.body), + "incorrect body", + ) + + def test_override(self): + req = self.get_request(f"{base_dir}/override.http", ) + self.assertEqual( + { + "a": 10, + }, + json.loads(req.body), + "incorrect body", + ) + + + def test_var_templating(self): + req = self.get_request(f"{base_dir}/templating.http") + self.assertEqual( + { + "a":10, + "b": "10 + 10 = 20" + }, + json.loads(req.body), + "incorrect body", + ) + # parse url and get query c + parsed = urlparse(req.url) + # c=randomstrs + # total 12 = 10 (generated) + 2 (c=) + self.assertEqual(len(parsed.query), 12) \ No newline at end of file diff --git a/test/extensions/test_commands.py b/test/extensions/test_commands.py index df47008..d42bab6 100644 --- a/test/extensions/test_commands.py +++ b/test/extensions/test_commands.py @@ -137,7 +137,8 @@ def test_complex_file(self): self.assertTrue("headers" in result.result) self.assertEqual("http://localhost:8000/post?startusing=dothttp", body["url"]) self.assertEqual( - """@name("2") + """var host = 'localhost:8000' ; +@name("2") POST "http://localhost:8000/post" ? "startusing"= "dothttp" @@ -163,7 +164,8 @@ def test_complex_file(self): self.assertTrue("headers" in result2.result) self.assertEqual("http://localhost:8000/post?startusing=dothttp", body["url"]) self.assertEqual( - """@name("2") + """var host = 'req.dothttp.dev' ; +@name("2") POST "http://req.dothttp.dev/post" ? "startusing"= "dothttp" @@ -185,7 +187,8 @@ def test_complex_file(self): ) ) self.assertEqual( - """@name("3") + """var host = 'localhost:8000' ; +@name("3") POST "http://localhost:8000/POST" ? "startusing"= "dothttp" @@ -209,7 +212,8 @@ def test_complex_file(self): ) self.assertEqual(200, result4.result["status"]) self.assertEqual( - """@name("4") + """var host = 'localhost:8000' ; +@name("4") GET "http://localhost:8000/get" basicauth("username", "password") @@ -276,10 +280,9 @@ def test_property(self): ) self.assertEqual(200, result.result["status"]) - # @skip(""" - # skipping as certificate has expired - # https://github.com/chromium/badssl.com/issues/482 - # """) + @skip(""" + can be skipped as path may vary + """) def test_cert_with_no_key(self): filename = f"{http_base}/no-password.http" cert_file = f"{cert_base}/no-password.pem" @@ -288,7 +291,11 @@ def test_cert_with_no_key(self): ) self.assertEqual(200, req_comp_success.result["status"]) self.assertEqual( - f"""@name("no-password") + f"""var cert = '/home/runner/work/dothttp/dothttp/test/core/root_cert/certs/no-password.pem' ; +var key ; +var p12 ; +var file = '' ; +@name("no-password") GET "https://client.badssl.com/" certificate(cert="{cert_file}") @@ -298,10 +305,9 @@ def test_cert_with_no_key(self): req_comp_success.result["http"], ) - # @skip(""" - # skipping as certificate has expired - # https://github.com/chromium/badssl.com/issues/482 - # """) + @skip(""" + can be skipped as path may vary + """) def test_cert_key(self): filename = f"{http_base}/no-password.http" cert_file = f"{cert_base}/cert.crt" @@ -313,7 +319,10 @@ def test_cert_key(self): ) self.assertEqual(200, req_comp2.result["status"]) self.assertEqual( - f"""@name("with-key-and-cert") + f"""var cert = '/home/runner/work/dothttp/dothttp/test/core/root_cert/certs/cert.crt' ; +var key = '/home/runner/work/dothttp/dothttp/test/core/root_cert/certs/key.key' ; +var p12 ; +@name("with-key-and-cert") @clear @insecure GET "https://client.badssl.com/" @@ -325,10 +334,9 @@ def test_cert_key(self): req_comp2.result["http"], ) - # @skip(""" - # skipping as certificate has expired - # https://github.com/chromium/badssl.com/issues/482 - # """) + @skip(""" + can be skipped as path may vary + """) def test_p12(self): filename = f"{http_base}/no-password.http" p12 = f"{cert_base}/badssl.com-client.p12" @@ -339,7 +347,11 @@ def test_p12(self): ) self.assertEqual(200, result.result["status"]) self.assertEqual( - f"""@name("with-p12") + f"""var cert ; +var key ; +var p12 = '/home/runner/work/dothttp/dothttp/test/core/root_cert/certs/badssl.com-client.p12' ; +var password = 'badssl.com' ; +@name("with-p12") @clear GET "https://client.badssl.com/" p12(file="{p12}", password="badssl.com") diff --git a/test/extensions/test_content.py b/test/extensions/test_content.py index 65e456f..7efae8f 100644 --- a/test/extensions/test_content.py +++ b/test/extensions/test_content.py @@ -138,3 +138,66 @@ def test_execute_content_context_multiple_base(self): """, result.result["body"], ) + + def test_execute_content_context_with_var_normal(self): + # tests to pick first context if multiple contexts are there + result = self.execute_and_result( + """ + @name('test') + POST "http://localhost:8000/post" + json({ + "a":{{a}}, + "b":"{{b}}", + }) + + """, + target="test", + contexts=[ + # as it is not single file + # there can be multiple targets + # defined with same name + # in such scenario + # only first one will be picked + """ + var a = 10; + """, + """ + var b = 20; + """ + ], + curl=False, + ) + self.assertEqual( + { + "a": 10, + "b": '20', + }, + json.loads(result.result["body"])["json"], + ) + + + def test_execute_content_context_with_var_with_json(self): + # tests to pick first context if multiple contexts are there + result = self.execute_and_result( + """ + @name('test') + POST "http://localhost:8000/post" + json({ + "c" : {{c}} + }) + + """, + target="test", + contexts=[ + """ + var c = {"ram":"ranga", "raja":[1, true, false, null]}; + """ + ], + curl=False, + ) + self.assertEqual( + { + "c" : {"ram":"ranga", "raja":[1, True, False, None]} + }, + json.loads(result.result["body"])["json"], + ) \ No newline at end of file