def test_any_mapping_requires_items(self):
        class TestObject(list):
            def items(self):
                yield from self  # pragma: no cover

        assert TestObject() == AnyMapping()
        assert dict() == AnyMapping()
Beispiel #2
0
    def with_headers(self, headers=...):
        """Specify the request must have headers.

        This method will remove 'Host' from the set of headers as some
        libraries add it before sending, and it's mostly noise.

        :param headers: A mappable of headers or matcher to match exactly
        """
        if headers is ...:
            self.headers = AnyMapping.of_size(at_least=1)

        elif isinstance(headers, Matcher):
            self.headers = headers

        elif headers is not None:
            self.headers = AnyMapping.containing(headers).only()
Beispiel #3
0
    def _set_query(self, query, exact_match=True):
        if query is not self.APPLY_DEFAULT:
            query = MultiValueQuery.normalise(query)
            if query and not isinstance(query, Matcher):
                # MultiValueQuery is guaranteed to return something we can
                # provide to AnyMapping for comparison
                query = AnyMapping.containing(query)
                if exact_match:
                    query = query.only()

        self.parts["query"] = query
Beispiel #4
0
class AnyURLCore(Matcher):
    """Matches any URL."""

    APPLY_DEFAULT = object()
    STRING_OR_NONE = NamedMatcher("<AnyStringOrNone>", AnyOf([None, AnyString()]))
    MAP_OR_NONE = NamedMatcher("<AnyMappingOrNone>", AnyOf([None, AnyMapping()]))

    DEFAULTS = {
        "scheme": STRING_OR_NONE,
        "host": STRING_OR_NONE,
        "path": STRING_OR_NONE,
        "params": STRING_OR_NONE,
        "query": MAP_OR_NONE,
        "fragment": STRING_OR_NONE,
    }

    # pylint: disable=too-many-arguments
    # I can't see a way around it. We could use kwargs, but then auto complete
    # would be hard
    def __init__(
        self,
        base_url=None,
        scheme=APPLY_DEFAULT,
        host=APPLY_DEFAULT,
        path=APPLY_DEFAULT,
        params=APPLY_DEFAULT,
        query=APPLY_DEFAULT,
        fragment=APPLY_DEFAULT,
    ):
        """Initialize a new URL matcher.

        :param base_url: URL (with scheme) to base the matcher on
        :param scheme: Scheme to match (e.g. http)
        :param host: Hostname to match
        :param path: URL path to match
        :param params: URL path params to match
        :param query: Query to match (string, dict or matcher)
        :param fragment: Anchor fragment to match (e.g. "name" for "#name")
        """
        self.parts = {
            # https://tools.ietf.org/html/rfc7230#section-2.7.3
            # scheme and host are case-insensitive
            "scheme": self._lower_if_string(scheme),
            "host": self._lower_if_string(host),
            # `path`, `query` and `fragment` are case-sensitive
            "path": self._get_path_matcher(path, scheme, host),
            "params": params,
            "fragment": fragment,
        }

        self._set_query(query)

        if base_url:
            self._set_base_url(base_url)
        else:
            # Apply default matchers for everything not provided
            self._apply_defaults(self.parts, self.DEFAULTS)

        super().__init__("dummy", self.assert_equal_to)

    def __str__(self):
        contraints = {
            key: value
            for key, value in self.parts.items()
            if value is not self.DEFAULTS[key]
        }

        if not contraints:
            return "* any URL *"

        return f"* any URL matching {contraints} *"

    @classmethod
    def parse_url(cls, url_string):
        """Parse a URL into a dict for comparison.

        Parses the given URL allowing you to see how AnyURL will understand it.
        This can be useful when debugging why a particular URL does or does
        not match.

        :param url_string: URL to parse
        :raise ValueError: If scheme is mandatory and not provided
        :return: A normalised string of comparison values
        """
        url = urlparse(url_string)

        if not url.scheme and not url.netloc:
            # Without a scheme `urlparse()` assumes that the hostname is part
            # of the path, so we have to try and guess what it really was

            host, path = cls._guess_hostname_and_path(url.path)
            url = url._replace(netloc=host, path=path)

        return {
            "scheme": url.scheme.lower() if url.scheme else None,
            "host": url.netloc.lower() if url.netloc else None,
            "path": url.path or None,
            "params": url.params or None,
            "query": MultiValueQuery.normalise(url.query),
            "fragment": url.fragment or None,
        }

    @staticmethod
    def _get_path_matcher(path, scheme, host):
        # If we are anything other than a plain string or None, use it directly
        if path is not None and not isinstance(path, str):
            return path

        # If we are matching paths alone, just return whatever we were given
        # so we match exactly. This lets the user distinguish between /path
        # and path which may be important
        if scheme is None and host is None:
            return path

        # If we got None, we need to allow ourselves to match either slash, ''
        # or None
        if path in (None, "/", ""):
            return NamedMatcher("<Path '/'>", AnyOf([None, AnyStringMatching(r"^/?$")]))

        # Otherwise construct a matcher which doesn't care about leading
        # slashes
        return NamedMatcher(
            f"'<Path '{path}'>", AnyStringMatching(f"^/?{re.escape(path)}$")
        )

    def _set_query(self, query, exact_match=True):
        if query is not self.APPLY_DEFAULT:
            query = MultiValueQuery.normalise(query)
            if query and not isinstance(query, Matcher):
                # MultiValueQuery is guaranteed to return something we can
                # provide to AnyMapping for comparison
                query = AnyMapping.containing(query)
                if exact_match:
                    query = query.only()

        self.parts["query"] = query

    def _set_base_url(self, base_url):
        # If we have a base URL, we'll take everything from there if it
        # wasn't explicitly provided in the constructor

        overlay = self.parse_url(base_url)

        path_matcher = self._get_path_matcher(
            overlay["path"], overlay["scheme"], overlay["host"]
        )
        overlay["path"] = path_matcher
        self._apply_defaults(self.parts, overlay)

    @staticmethod
    def _lower_if_string(value):
        if isinstance(value, str):
            return value.lower()

        return value

    @staticmethod
    def _apply_defaults(values, defaults, default_key=APPLY_DEFAULT):
        for key, default_value in defaults.items():
            if values[key] is default_key:
                values[key] = default_value

    @classmethod
    def _is_hostname(cls, host):
        if not host:
            return False

        if "." in host:
            return True

        return host.lower() == "localhost"

    @classmethod
    def _guess_hostname_and_path(cls, path):
        if "/" in path:
            head, tail = path.split("/", 1)
            if cls._is_hostname(head):
                return head, f"/{tail}"

        elif cls._is_hostname(path):
            return path, None

        return None, path

    def assert_equal_to(self, other):
        """Assert that the URL object is equal to another object.

        :raise AssertionError: If no match is found with details of why
        :return: True if equal
        """

        if not isinstance(other, str):
            raise AssertionError("Other URL is not a string")

        comparison = self.parse_url(other)

        for key, self_value in self.parts.items():
            other_value = comparison.get(key)

            if self_value != other_value:
                raise AssertionError(f"Other '{key}' {other_value} != {self_value}")

        return True
Beispiel #5
0
class TestAnyURL:
    def test_base_case(self):
        matcher = AnyURLCore()

        assert "" == matcher
        assert 3 != matcher

    BASE_URL = "http://www.example.com/path;params?a=1&b=2#fragment"

    # URLs where the specified part is different from BASE_URL
    PART_MODIFIED_URLS = {
        # We must use a URL scheme with `params` for this test
        "scheme": "ftp://www.example.com/path;params?a=1&b=2#fragment",
        "host": "http://MODIFIED/path;params?a=1&b=2#fragment",
        "path": "http://www.example.com/MODIFIED;params?a=1&b=2#fragment",
        "params": "http://www.example.com/path;MODIFIED?a=1&b=2#fragment",
        "query": "http://www.example.com/path;params?MODIFIED=1#fragment",
        "fragment": "http://www.example.com/path;params?a=1&b=2#MODIFIED",
    }

    @pytest.mark.parametrize("part,url_with_part_changed",
                             tuple(PART_MODIFIED_URLS.items()))
    def test_you_can_make_one_part_wild_with_a_base_url(
            self, part, url_with_part_changed):
        # Create a matcher with the specified part wild i.e. `scheme=Any()`

        matcher = AnyURLCore(self.BASE_URL, **{part: Any()})

        # Check it matches the original URL and the URL with that part changed
        assert self.BASE_URL == matcher
        assert url_with_part_changed == matcher

    @pytest.mark.parametrize(
        "base_url, url",
        (
            ("http://example.com", "http://example.com"),
            ("http://example.com", "http://example.com/"),
            ("http://example.com/", "http://example.com"),
            ("http://example.com/", "http://example.com/"),
        ),
    )
    def test_base_url_matches_with_or_without_path(self, base_url, url):
        matcher = AnyURLCore(base_url=base_url)

        assert matcher == url

    @pytest.mark.parametrize("part",
                             ["scheme", "host", "path", "query", "fragment"])
    def test_a_wild_part_does_not_just_match_everything(self, part):
        # Create a matcher with the specified part wild i.e. `scheme=Any()`
        matcher = AnyURLCore(self.BASE_URL, **{part: Any()})

        for modified_part, modified_url in self.PART_MODIFIED_URLS.items():
            # We expect to match the part where the modified part is the part
            # we have made wild so skip
            if modified_part == part:
                continue

            assert modified_url != matcher

    @pytest.mark.parametrize(
        "part,url_with_part_missing",
        (
            # URLs where the specified part is missing from BASE_URL
            ("scheme", "www.example.com/path;params?a=1&b=2#fragment"),
            ("host", "http:///path;params?a=1&b=2#fragment"),
            ("path", "http://www.example.com;params?a=1&b=2#fragment"),
            ("params", "http://www.example.com/path?a=1&b=2#fragment"),
            ("query", "http://www.example.com/path;params#fragment"),
            ("fragment", "http://www.example.com/path;params?a=1&b=2"),
        ),
    )
    def test_you_can_override_default_with_params(self, part,
                                                  url_with_part_missing):
        # Create a matcher with the specified part set to None
        # i.e. `scheme=None`
        matcher = AnyURLCore(**{part: None})

        # Check we match the URL with the part missing
        assert url_with_part_missing == matcher
        # ... but not the URL with it present
        assert self.BASE_URL != matcher

    def test_case_sensitivity_for_other(self):
        matcher = AnyURLCore(self.BASE_URL)

        # https://tools.ietf.org/html/rfc7230#section-2.7.3
        # scheme and host are case-insensitive
        assert matcher == "HTTP://www.example.com/path;params?a=1&b=2#fragment"
        assert matcher == "http://WWW.EXAMPLE.COM/path;params?a=1&b=2#fragment"

        # ... path, query string and fragment are case-sensitive
        assert matcher != "http://www.example.com/PATH;params?a=1&b=2#fragment"
        assert matcher != "http://www.example.com/path;PARAMS?a=1&b=2#fragment"
        assert matcher != "http://www.example.com/path;params?A=1&B=2#fragment"
        assert matcher != "http://www.example.com/path;params?a=1&b=2#FRAGMENT"

    @pytest.mark.parametrize(
        "matcher",
        (
            AnyURLCore(BASE_URL.upper()),
            AnyURLCore(BASE_URL.upper(), scheme="HTTP"),
            AnyURLCore(BASE_URL.upper(), host="WWW.EXAMPLE.COM"),
        ),
    )
    def test_case_sensitivity_for_self(self, matcher):
        # https://tools.ietf.org/html/rfc7230#section-2.7.3
        # scheme and host are case-insensitive
        assert matcher == "http://WWW.EXAMPLE.COM/PATH;PARAMS?A=1&B=2#FRAGMENT"
        assert matcher == "HTTP://www.example.com/PATH;PARAMS?A=1&B=2#FRAGMENT"

        # ... path, query string and fragment are case-sensitive
        assert matcher != "HTTP://WWW.EXAMPLE.COM/path;PARAMS?A=1&B=2#FRAGMENT"
        assert matcher != "HTTP://WWW.EXAMPLE.COM/PATH;params?A=1&B=2#FRAGMENT"
        assert matcher != "HTTP://WWW.EXAMPLE.COM/PATH;PARAMS?a=1&b=2#FRAGMENT"
        assert matcher != "HTTP://WWW.EXAMPLE.COM/PATH;PARAMS?A=1&B=2#fragment"

    @pytest.mark.parametrize(
        "part,value",
        (
            ("scheme", "http"),
            ("host", "www.example.com"),
            ("path", "/path"),
            ("query", "a=1&b=2"),
            ("fragment", "fragment"),
        ),
    )
    def test_generic_matching(self, part, value):
        matcher = AnyURLCore(**{part: value})

        for comparison_part, url in self.PART_MODIFIED_URLS.items():
            if comparison_part == part:
                # The URLs are different here and this is the part we specified
                # so we should spot the difference
                assert url != matcher
            else:
                # These are different too, but these should all match
                assert url == matcher

    @pytest.mark.parametrize(
        "_,query",
        (
            ("plain string", "a=1&b=2"),
            ("dict", {
                "a": "1",
                "b": "2"
            }),
            ("any mapping", AnyMapping.containing({
                "a": "1",
                "b": "2"
            }).only()),
            ("any dict", Any.dict.containing({
                "a": "1",
                "b": "2"
            }).only()),
        ),
    )
    def test_specifying_query_string(self, query, _):
        matcher = AnyURLCore(query=query)

        assert matcher == "http://example.com?b=2&a=1"

        assert matcher != "http://example.com?b=2"
        assert matcher != "http://example.com?b=2&a=1&c=3"
        assert matcher != "http://example.com?b=2&a=1&a=1"

    def test_multi_query_params(self):
        url = "http://example.com?a=1&a=1&a=2"

        assert url != AnyURLCore(query={"a": "1"})
        assert url != AnyURLCore(query=Any.dict.containing({"a": "1"}))
        assert url == AnyURLCore(query=Any.mapping.containing({"a": "1"}))

        assert url == AnyURLCore(
            query=Any.mapping.containing([("a", "1"), ("a",
                                                       "2"), ("a",
                                                              "1")]).only())
        assert url != AnyURLCore(query=Any.mapping.containing([(
            "a", "1"), ("a", "2"), ("a", "1"), ("b", 5)]).only())

    def test_stringification_changes_when_contents_change(self):
        matcher = AnyURLCore(scheme="foo")

        assert "foo" in repr(matcher)
        assert "foo" in str(matcher)

        matcher.parts["scheme"] = "boo"

        assert "boo" in repr(matcher)
        assert "boo" in str(matcher)

    def test_it_raises_with_assert_on_comparison_enabled(self):
        # Normally you'd turn this on for the whole class, but it has totally
        # non-local effects and explodes the tests
        matcher = AnyURLCore(scheme="missing")
        matcher.assert_on_comparison = True

        with pytest.raises(AssertionError):
            _ = "abc" == matcher

    @pytest.mark.parametrize("other", (None, 123, True))
    def test_it_refuses_to_compare_to_non_strings(self, other):
        assert AnyURLCore() != other

    def test_stringification_default(self):
        assert str(AnyURLCore()) == "* any URL *"
Beispiel #6
0
class AnyURL(AnyURLCore):
    """A URL matcher with a fluent style interface."""

    # pylint: disable=function-redefined

    PRESENT_DEFAULT = {
        "scheme": AnyString(),
        "host": AnyString(),
        "path": AnyString(),
        "params": AnyString(),
        "query": AnyMapping(),
        "fragment": AnyString(),
    }

    def _apply_field_default(self, field, value):
        if value is not AnyURLCore.APPLY_DEFAULT:
            return value

        return self.PRESENT_DEFAULT[field]

    @staticmethod
    def with_scheme(scheme=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_host(host=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_path(path=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_params(params=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def containing_query(query=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_query(query=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_fragment(fragment=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @classmethod
    def matching(cls, base_url):
        """Create a URL matcher based on the given URL.

        :param base_url: URL to base the matcher on
        :return: An instance of AnyURLFluent
        """
        return AnyURL(base_url)

    @fluent_entrypoint
    def with_scheme(self, scheme=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a scheme or None.

        If you pass None this will ensure the URL has no scheme.

        :param scheme: None, string or matcher for the scheme
        """

        self.parts["scheme"] = self._apply_field_default("scheme", scheme)

    @fluent_entrypoint
    def with_host(self, host=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a host or None.

        If you pass None this will ensure the URL has no host.

        :param host: None, string or matcher for the host
        """
        self.parts["host"] = self._apply_field_default("host", host)

    @fluent_entrypoint
    def with_path(self, path=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a path or None.

        If you pass None this will ensure the URL has no path.

        :param path: None, string or matcher for the path
        """
        if path is AnyURLCore.APPLY_DEFAULT:
            self.parts["path"] = AnyString()
        else:
            self.parts["path"] = self._get_path_matcher(
                path, self.parts["scheme"], self.parts["host"])

    @fluent_entrypoint
    def with_params(self, params=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a params or None.

        If you pass None this will ensure the URL has no params.

        :param params: None, string or matcher for the params
        """
        self.parts["params"] = self._apply_field_default("params", params)

    @fluent_entrypoint
    def containing_query(self, query):
        """Specify that the query must have at least the items specified.

        :param query: A mappable to check
        """
        self._set_query(query, exact_match=False)

    @fluent_entrypoint
    def with_query(self, query=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a query or None.

        If you pass None this will ensure the URL has no query.

        :param query: None, mapping or matcher for the query
        """
        query = self._apply_field_default("query", query)
        self._set_query(query)

    @fluent_entrypoint
    def with_fragment(self, fragment=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a fragment or None.

        If you pass None this will ensure the URL has no fragment.

        :param fragment: None, string or matcher for the fragment
        """
        self.parts["fragment"] = self._apply_field_default(
            "fragment", fragment)
 def test_non_matching_items(self, non_matching):
     assert non_matching != AnyMapping()
Beispiel #8
0
    def containing_headers(self, headers):
        """Specify the request must have at least the headers specified.

        :param headers: A mappable of headers to match
        """
        self.headers = AnyMapping.containing(headers)
Beispiel #9
0
    def test_it_stringifies_as_we_specify(self):
        matcher = NamedMatcher("string", AnyMapping())

        assert str(matcher) == "string"
        assert repr(matcher) == "string"
Beispiel #10
0
    def test_it_matches_like_its_contents(self):
        matcher = NamedMatcher("string", AnyMapping())

        assert matcher == {}
        assert matcher != []