def recurse_check_key_match(self, expected_block, block, blockname): """Valid returned data against expected data Todo: Optionally use a validation library too Args: expected_block (dict): expected data block (dict): actual data blockname (str): 'name' of this block (params, mqtt, etc) for error messages """ if not expected_block: logger.debug("Nothing to check against") return expected_block = format_keys(expected_block, self.test_block_config["variables"]) if block is None: self._adderr( "expected %s in the %s, but there was no response body", expected_block, blockname) return logger.debug("expected = %s, actual = %s", expected_block, block) if blockname == "body" and (not isinstance(expected_block, type(block))): if isinstance(block, list): block_type = "list" else: block_type = "dict" if isinstance(expected_block, list): expected_type = "list" else: expected_type = "dict" self._adderr("Expected %s to be returned, but a %s was returned", block_type, expected_type) # Fatal return for split_key, joined_key, expected_val in yield_keyvals( expected_block): try: actual_val = recurse_access_key(block, list(split_key)) except KeyError as e: self._adderr("Key not present: %s", joined_key, e=e) continue except IndexError as e: self._adderr( "Expected returned list to be of at least length %s but length was %s", int(joined_key) + 1, len(block), e=e) continue logger.debug("%s: %s vs %s", joined_key, expected_val, actual_val) try: check_keys_match_recursive(expected_val, actual_val, []) except exceptions.KeyMismatchError as e: logger.error("Key mismatch on %s", joined_key) self._adderr( "Value mismatch: got '%s' (type: %s), expected '%s' (type: %s)", actual_val, type(actual_val), expected_val, type(expected_val), e=e) else: logger.debug("Key %s was present and matched", joined_key)
def get_request_args(rspec, test_block_config): """Format the test spec given values inthe global config Todo: Add similar functionality to validate/save $ext functions so input can be generated from a function Args: rspec (dict): Test spec test_block_config (dict): Test block config Returns: dict: Formatted test spec Raises: BadSchemaError: Tried to pass a body in a GET request """ request_args = {} # Ones that are required and are enforced to be present by the schema required_in_file = [ "method", "url", ] optional_in_file = [ "json", "data", "params", "headers", "files" # Ideally this would just be passed through but requests seems to error # if we pass a list instead of a tuple, so we have to manually convert # it further down # "auth", ] optional_with_default = { "verify": True, } if "method" not in rspec: logger.debug("Using default GET method") rspec["method"] = "GET" headers = rspec.get("headers") if headers: has_explicit_content_type = "content-type" in [ h.lower() for h in headers.keys() ] is_multipart_request = rspec.get("files") is not None if not has_explicit_content_type and not is_multipart_request: rspec["headers"]["content-type"] = "application/json" elif has_explicit_content_type and is_multipart_request: warnings.warn( "You are trying to send a request with files and set it's content-type at the same time. " "This is not supported. When files are present, " "the multipart/form-data content-type is used automatically" "Do not set the Content-Type in the yaml, too", RuntimeWarning) fspec = format_keys(rspec, test_block_config["variables"]) def add_request_args(keys, optional): for key in keys: try: request_args[key] = fspec[key] except KeyError: if optional or (key in request_args): continue # This should never happen raise add_request_args(required_in_file, False) add_request_args(optional_in_file, True) if "auth" in fspec: request_args["auth"] = tuple(fspec["auth"]) for key in optional_in_file: try: func = get_wrapped_create_function(request_args[key].pop("$ext")) except (KeyError, TypeError): pass else: request_args[key] = func() # If there's any nested json in parameters, urlencode it # if you pass nested json to 'params' then requests silently fails and just # passes the 'top level' key, ignoring all the nested json. I don't think # there's a standard way to do this, but urlencoding it seems sensible # eg https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter # > ...represented in an OAuth 2.0 request as UTF-8 encoded JSON (which ends # > up being form-urlencoded when passed as an OAuth parameter) for key, value in request_args.get("params", {}).items(): if isinstance(value, dict): request_args["params"][key] = quote_plus(json.dumps(value)) for key, val in optional_with_default.items(): request_args[key] = fspec.get(key, val) # TODO # requests takes all of these - we need to parse the input to get them # "cookies", # These verbs _can_ send a body but the body _should_ be ignored according # to the specs - some info here: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods if request_args["method"] in ["GET", "HEAD", "OPTIONS"]: if any(i in request_args for i in ["json", "data"]): warnings.warn( "You are trying to send a body with a HTTP verb that has no semantic use for it", RuntimeWarning) return request_args
def test_format_missing_raises(self): to_format = {"a": "{b}"} with pytest.raises(exceptions.MissingFormatError): format_keys(to_format, {})
def _read_expected_cookies(session, rspec, test_block_config): """ Read cookies to inject into request, ignoring others which are present Args: session (Session): session object rspec (dict): test spec test_block_config (dict): config available for test Returns: dict: cookies to use in request, if any """ # Need to do this down here - it is separate from getting request args as # it depends on the state of the session existing_cookies = session.cookies.get_dict() cookies_to_use = format_keys(rspec.get("cookies", None), test_block_config["variables"]) if cookies_to_use is None: logger.debug("No cookies specified in request, sending all") return None elif cookies_to_use in ([], {}): logger.debug("Not sending any cookies with request") return {} def partition(pred, iterable): """From itertools documentation""" t1, t2 = tee(iterable) return list(filterfalse(pred, t1)), list(filter(pred, t2)) # Cookies are either a single list item, specitying which cookie to send, or # a mapping, specifying cookies to override expected, extra = partition(lambda x: isinstance(x, dict), cookies_to_use) missing = set(expected) - set(existing_cookies.keys()) if missing: logger.error("Missing cookies") raise exceptions.MissingCookieError( "Tried to use cookies '{}' in request but only had '{}' available". format(expected, existing_cookies)) # 'extra' should be a list of dictionaries - merge them into one here from_extra = {k: v for mapping in extra for (k, v) in mapping.items()} if len(extra) != len(from_extra): logger.error("Duplicate cookie override values specified") raise exceptions.DuplicateCookieError( "Tried to override the value of a cookie multiple times in one request" ) overwritten = [i for i in expected if i in from_extra] if overwritten: logger.error("Duplicate cookies found in request") raise exceptions.DuplicateCookieError( "Asked to use cookie {} from previous request but also redefined it as {}" .format(overwritten, from_extra)) from_cookiejar = {c: existing_cookies.get(c) for c in expected} return deep_dict_merge(from_cookiejar, from_extra)
def get_request_args(rspec, test_block_config): """Format the test spec given values inthe global config Todo: Add similar functionality to validate/save $ext functions so input can be generated from a function Args: rspec (dict): Test spec test_block_config (dict): Test block config Returns: dict: Formatted test spec Raises: BadSchemaError: Tried to pass a body in a GET request """ # pylint: disable=too-many-locals,too-many-statements if "method" not in rspec: logger.debug("Using default GET method") rspec["method"] = "GET" if "headers" not in rspec: rspec["headers"] = {} content_keys = ["data", "json", "files", "file_body"] in_request = [c for c in content_keys if c in rspec] if len(in_request) > 1: # Explicitly raise an error here # From requests docs: # Note, the json parameter is ignored if either data or files is passed. # However, we allow the data + files case, as requests handles it correctly if set(in_request) != {"data", "files"}: raise exceptions.BadSchemaError( "Can only specify one type of request data in HTTP request (tried to " "send {})".format(" and ".join(in_request))) normalised_headers = {k.lower(): v for k, v in rspec["headers"].items()} def get_header(name): return normalised_headers.get(name, None) content_header = get_header("content-type") encoding_header = get_header("content-encoding") if "files" in rspec: if content_header: logger.warning( "Tried to specify a content-type header while sending multipart files - this will be ignored" ) rspec["headers"] = { i: j for i, j in normalised_headers.items() if i.lower() != "content-type" } fspec = format_keys(rspec, test_block_config["variables"]) if fspec["method"] not in valid_http_methods: raise exceptions.BadSchemaError("Unknown HTTP method {}".format( fspec["method"])) # If the user is using the file_body key, try to guess what type of file/encoding it is. filename = fspec.get("file_body") if filename: with ExitStack() as stack: file_spec = guess_filespec(filename, stack, test_block_config) fspec["file_body"] = filename if len(file_spec) == 2: logger.debug( "No content type or encoding inferred from file_body for %s", filename, ) if len(file_spec) >= 3: inferred_content_type = file_spec[2] if content_header: logger.info( "inferred content type '%s' from %s, but using user specified content type '%s'", inferred_content_type, filename, content_header, ) else: fspec["headers"]["content-type"] = inferred_content_type if len(file_spec) == 4: inferred_content_encoding = file_spec[3] if encoding_header: logger.info( "inferred content encoding '%s' from %s, but using user specified encoding '%s", inferred_content_encoding, filename, encoding_header, ) else: fspec["headers"].update(**inferred_content_encoding) ######################################### request_args = {} def add_request_args(keys, optional): for key in keys: try: request_args[key] = fspec[key] except KeyError: if optional or (key in request_args): continue # This should never happen raise # Ones that are required and are enforced to be present by the schema required_in_file = ["method", "url"] optional_with_default = {"verify": True, "stream": False} add_request_args(["file_body"], True) add_request_args(required_in_file, False) add_request_args(RestRequest.optional_in_file, True) if "auth" in fspec: request_args["auth"] = tuple(fspec["auth"]) if "cert" in fspec: if isinstance(fspec["cert"], list): request_args["cert"] = tuple(fspec["cert"]) if "timeout" in fspec: # Needs to be a tuple, it being a list doesn't work if isinstance(fspec["timeout"], list): request_args["timeout"] = tuple(fspec["timeout"]) # If there's any nested json in parameters, urlencode it # if you pass nested json to 'params' then requests silently fails and just # passes the 'top level' key, ignoring all the nested json. I don't think # there's a standard way to do this, but urlencoding it seems sensible # eg https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter # > ...represented in an OAuth 2.0 request as UTF-8 encoded JSON (which ends # > up being form-urlencoded when passed as an OAuth parameter) for key, value in request_args.get("params", {}).items(): if not isinstance(value, str): if key == "$ext": logger.debug("Skipping converting of ext function (%s)", value) continue if isinstance(value, dict): request_args["params"][key] = quote_plus(json.dumps(value)) for key, val in optional_with_default.items(): request_args[key] = fspec.get(key, val) # TODO # requests takes all of these - we need to parse the input to get them # "cookies", # These verbs _can_ send a body but the body _should_ be ignored according # to the specs - some info here: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods if request_args["method"] in ["GET", "HEAD", "OPTIONS"]: if any(i in request_args for i in ["json", "data"]): warnings.warn( "You are trying to send a body with a HTTP verb that has no semantic use for it", RuntimeWarning, ) return request_args
def get_request_args(rspec, test_block_config): """Format the test spec given values inthe global config Todo: Add similar functionality to validate/save $ext functions so input can be generated from a function Args: rspec (dict): Test spec test_block_config (dict): Test block config Returns: dict: Formatted test spec Raises: BadSchemaError: Tried to pass a body in a GET request """ # pylint: disable=too-many-locals,too-many-statements request_args = {} # Ones that are required and are enforced to be present by the schema required_in_file = ["method", "url"] optional_in_file = [ "json", "data", "params", "headers", "files", "timeout", "cert", # Ideally this would just be passed through but requests seems to error # if we pass a list instead of a tuple, so we have to manually convert # it further down # "auth" ] optional_with_default = {"verify": True, "stream": False} if "method" not in rspec: logger.debug("Using default GET method") rspec["method"] = "GET" content_keys = ["data", "json", "files", "file_body"] in_request = [c for c in content_keys if c in rspec] if len(in_request) > 1: # Explicitly raise an error here # From requests docs: # Note, the json parameter is ignored if either data or files is passed. # However, we allow the data + files case, as requests handles it correctly if set(in_request) != {"data", "files"}: raise exceptions.BadSchemaError( "Can only specify one type of request data in HTTP request (tried to " "send {})".format(" and ".join(in_request))) headers = rspec.get("headers", {}) has_content_header = "content-type" in [h.lower() for h in headers.keys()] if "files" in rspec: if has_content_header: logger.warning( "Tried to specify a content-type header while sending a file - this will be ignored" ) rspec["headers"] = { i: j for i, j in headers.items() if i.lower() != "content-type" } fspec = format_keys(rspec, test_block_config["variables"]) send_in_body = fspec.get("file_body") if send_in_body: request_args["file_body"] = send_in_body def add_request_args(keys, optional): for key in keys: try: request_args[key] = fspec[key] except KeyError: if optional or (key in request_args): continue # This should never happen raise add_request_args(required_in_file, False) add_request_args(optional_in_file, True) if "auth" in fspec: request_args["auth"] = tuple(fspec["auth"]) if "cert" in fspec: if isinstance(fspec["cert"], list): request_args["cert"] = tuple(fspec["cert"]) if "timeout" in fspec: # Needs to be a tuple, it being a list doesn't work if isinstance(fspec["timeout"], list): request_args["timeout"] = tuple(fspec["timeout"]) for key in optional_in_file: try: func = get_wrapped_create_function(request_args[key].pop("$ext")) except (KeyError, TypeError, AttributeError): pass else: merge_ext_values = test_block_config.get("merge_ext_values") logger.debug("Will merge ext values? %s", merge_ext_values) if merge_ext_values: request_args[key] = deep_dict_merge(request_args[key], func()) else: request_args[key] = func() # If there's any nested json in parameters, urlencode it # if you pass nested json to 'params' then requests silently fails and just # passes the 'top level' key, ignoring all the nested json. I don't think # there's a standard way to do this, but urlencoding it seems sensible # eg https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter # > ...represented in an OAuth 2.0 request as UTF-8 encoded JSON (which ends # > up being form-urlencoded when passed as an OAuth parameter) for key, value in request_args.get("params", {}).items(): if isinstance(value, dict): request_args["params"][key] = quote_plus(json.dumps(value)) for key, val in optional_with_default.items(): request_args[key] = fspec.get(key, val) # TODO # requests takes all of these - we need to parse the input to get them # "cookies", # These verbs _can_ send a body but the body _should_ be ignored according # to the specs - some info here: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods if request_args["method"] in ["GET", "HEAD", "OPTIONS"]: if any(i in request_args for i in ["json", "data"]): warnings.warn( "You are trying to send a body with a HTTP verb that has no semantic use for it", RuntimeWarning, ) return request_args