Esempio n. 1
0
    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)
Esempio n. 2
0
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
Esempio n. 3
0
    def test_format_missing_raises(self):
        to_format = {"a": "{b}"}

        with pytest.raises(exceptions.MissingFormatError):
            format_keys(to_format, {})
Esempio n. 4
0
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)
Esempio n. 5
0
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
Esempio n. 6
0
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