Esempio n. 1
0
    def _add_openapi_integration(self, api, function, manage_swagger=False):
        """Adds the path and method for this Api event source to the OpenApi body for the provided RestApi.

        :param model.apigateway.ApiGatewayRestApi rest_api: the RestApi to which the path and method should be added.
        """
        open_api_body = api.get("DefinitionBody")
        if open_api_body is None:
            return

        function_arn = function.get_runtime_attr('arn')
        uri = fnSub('arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/' +
                    make_shorthand(function_arn) + '/invocations')

        editor = OpenApiEditor(open_api_body)

        if manage_swagger and editor.has_integration(self.Path, self.Method):
            # Cannot add the Lambda Integration, if it is already present
            raise InvalidEventException(
                self.relative_id,
                "API method '{method}' defined multiple times for path '{path}'.".format(
                    method=self.Method, path=self.Path))

        condition = None
        if CONDITION in function.resource_attributes:
            condition = function.resource_attributes[CONDITION]

        editor.add_lambda_integration(self.Path, self.Method, uri, self.Auth, api.get('Auth'), condition=condition)
        if self.Auth:
            self._add_auth_to_openapi_integration(api, editor)
        api["DefinitionBody"] = editor.openapi
Esempio n. 2
0
    def _add_endpoint_configuration(self):
        """Add disableExecuteApiEndpoint if it is set in SAM
        HttpApi doesn't have vpcEndpointIds

        Note:
        DisableExecuteApiEndpoint as a property of AWS::ApiGatewayV2::Api needs both DefinitionBody and
        DefinitionUri to be None. However, if neither DefinitionUri nor DefinitionBody are specified,
        SAM will generate a openapi definition body based on template configuration.
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-definitionbody
        For this reason, we always put DisableExecuteApiEndpoint into openapi object.

        """
        if self.disable_execute_api_endpoint and not self.definition_body:
            raise InvalidResourceException(
                self.logical_id,
                "DisableExecuteApiEndpoint works only within 'DefinitionBody' property."
            )
        editor = OpenApiEditor(self.definition_body)

        # if DisableExecuteApiEndpoint is set in both definition_body and as a property,
        # SAM merges and overrides the disableExecuteApiEndpoint in definition_body with headers of
        # "x-amazon-apigateway-endpoint-configuration"
        editor.add_endpoint_config(self.disable_execute_api_endpoint)

        # Assign the OpenApi back to template
        self.definition_body = editor.openapi
    def _add_auth(self):
        """
        Add Auth configuration to the OAS file, if necessary
        """
        if not self.auth:
            return

        if self.auth and not self.definition_body:
            raise InvalidResourceException(
                self.logical_id,
                "Auth works only with inline OpenApi specified in the 'DefinitionBody' property."
            )

        # Make sure keys in the dict are recognized
        if not all(key in AuthProperties._fields for key in self.auth.keys()):
            raise InvalidResourceException(
                self.logical_id, "Invalid value for 'Auth' property")

        if not OpenApiEditor.is_valid(self.definition_body):
            raise InvalidResourceException(
                self.logical_id,
                "Unable to add Auth configuration because 'DefinitionBody' does not contain a valid OpenApi definition.",
            )
        open_api_editor = OpenApiEditor(self.definition_body)
        auth_properties = AuthProperties(**self.auth)
        authorizers = self._get_authorizers(auth_properties.Authorizers,
                                            auth_properties.DefaultAuthorizer)

        # authorizers is guaranteed to return a value or raise an exception
        open_api_editor.add_authorizers_security_definitions(authorizers)
        self._set_default_authorizer(open_api_editor, authorizers,
                                     auth_properties.DefaultAuthorizer,
                                     auth_properties.Authorizers)
        self.definition_body = open_api_editor.openapi
Esempio n. 4
0
    def setUp(self):

        self.original_openapi = {
            "openapi": "3.0.1",
            "paths": {"/foo": {"get": {"a": "b"}}, "/bar": {}, "/badpath": "string value"},
        }

        self.editor = OpenApiEditor(self.original_openapi)
Esempio n. 5
0
class TestOpenApiEditor_get_integration_function(TestCase):
    def setUp(self):

        self.original_openapi = {
            "openapi": "3.0.1",
            "paths": {
                "$default": {
                    "x-amazon-apigateway-any-method": {
                        "Fn::If": [
                            "condition",
                            {
                                "security": [{
                                    "OpenIdAuth": ["scope1", "scope2"]
                                }],
                                "isDefaultRoute": True,
                                "x-amazon-apigateway-integration": {
                                    "httpMethod": "POST",
                                    "type": "aws_proxy",
                                    "uri": {
                                        "Fn::If": [
                                            "condition",
                                            {
                                                "Fn::Sub":
                                                "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HttpApiFunction.Arn}/invocations"
                                            },
                                            {
                                                "Ref": "AWS::NoValue"
                                            },
                                        ]
                                    },
                                    "payloadFormatVersion": "1.0",
                                },
                                "responses": {},
                            },
                            {
                                "Ref": "AWS::NoValue"
                            },
                        ]
                    }
                },
                "/bar": {},
                "/badpath": "string value",
            },
        }

        self.editor = OpenApiEditor(self.original_openapi)

    def test_must_get_integration_function_if_exists(self):

        self.assertEqual(
            self.editor.get_integration_function_logical_id(
                OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD),
            "HttpApiFunction",
        )
        self.assertFalse(
            self.editor.get_integration_function_logical_id("/bar", "get"))
Esempio n. 6
0
    def setUp(self):
        self.openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {"get": {}, "somemethod": {}},
                "/bar": {"post": {}, _X_ANY_METHOD: {}},
                "badpath": "string value",
            },
        }

        self.editor = OpenApiEditor(self.openapi)
Esempio n. 7
0
    def setUp(self):

        self.original_openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {"post": {"a": [1, 2, "b"], "responses": {"something": "is already here"}}},
                "/bar": {"get": {_X_INTEGRATION: {"a": "b"}}},
            },
        }

        self.editor = OpenApiEditor(self.original_openapi)
Esempio n. 8
0
    def test_must_return_copy_of_openapi(self):

        input = {"openapi": "3.0.1", "paths": {}}

        editor = OpenApiEditor(input)
        self.assertEqual(input, editor.openapi)  # They are equal in content
        input["openapi"] = "3"
        self.assertEqual("3.0.1", editor.openapi["openapi"])  # Editor works on a diff copy of input

        editor.add_path("/foo", "get")
        self.assertEqual({"/foo": {"get": {}}}, editor.openapi["paths"])
        self.assertEqual({}, input["paths"])  # Editor works on a diff copy of input
Esempio n. 9
0
    def setUp(self):

        self.original_openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {},
                "/bar": {},
                "/baz": "some value"
            }
        }

        self.editor = OpenApiEditor(self.original_openapi)
    def on_before_transform_template(self, template_dict):
        """
        Hook method that gets called before the SAM template is processed.
        The template has passed the validation and is guaranteed to contain a non-empty "Resources" section.

        :param dict template_dict: Dictionary of the SAM template
        :return: Nothing
        """
        template = SamTemplate(template_dict)

        for api_type in [
                SamResourceType.Api.value, SamResourceType.HttpApi.value
        ]:
            for logicalId, api in template.iterate({api_type}):
                if api.properties.get("DefinitionBody") or api.properties.get(
                        "DefinitionUri"):
                    continue

                if api_type is SamResourceType.HttpApi.value:
                    # If "Properties" is not set in the template, set them here
                    if not api.properties:
                        template.set(logicalId, api)
                    api.properties[
                        "DefinitionBody"] = OpenApiEditor.gen_skeleton()

                if api_type is SamResourceType.Api.value:
                    api.properties[
                        "DefinitionBody"] = SwaggerEditor.gen_skeleton()

                api.properties["__MANAGE_SWAGGER"] = True
Esempio n. 11
0
class TestOpenApiEditor_is_valid(TestCase):
    @parameterized.expand([
        param(OpenApiEditor.gen_skeleton()),
        # Dict can contain any other unrecognized properties
        param({
            "openapi": "3.1.1",
            "paths": {},
            "foo": "bar",
            "baz": "bar"
        })
        # TODO check and update the regex accordingly
        # Fails for this: param({"openapi": "3.1.10", "paths": {}, "foo": "bar", "baz": "bar"})
    ])
    def test_must_work_on_valid_values(self, openapi):
        self.assertTrue(OpenApiEditor.is_valid(openapi))

    @parameterized.expand([
        ({}, "empty dictionary"),
        ([1, 2, 3], "array data type"),
        ({
            "paths": {}
        }, "missing openapi property"),
        ({
            "openapi": "hello"
        }, "missing paths property"),
        ({
            "openapi": "hello",
            "paths": [1, 2, 3]
        }, "array value for paths property"),
    ])
    def test_must_fail_for_invalid_values(self, data, case):
        self.assertFalse(
            OpenApiEditor.is_valid(data),
            "openapi dictionary with {} must not be valid".format(case))
Esempio n. 12
0
    def setUp(self):
        self.openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {
                    "get": {_X_INTEGRATION: {"a": "b"}},
                    "post": {"Fn::If": ["Condition", {_X_INTEGRATION: {"a": "b"}}, {"Ref": "AWS::NoValue"}]},
                    "delete": {"Fn::If": ["Condition", {"Ref": "AWS::NoValue"}, {_X_INTEGRATION: {"a": "b"}}]},
                    "somemethod": {"foo": "value"},
                    "emptyintegration": {_X_INTEGRATION: {}},
                    "badmethod": "string value",
                }
            },
        }

        self.editor = OpenApiEditor(self.openapi)
Esempio n. 13
0
    def test_must_succeed_on_valid_openapi3(self):
        valid_openapi = {"openapi": "3.0.1", "paths": {"/foo": {}, "/bar": {}}}

        editor = OpenApiEditor(valid_openapi)
        self.assertIsNotNone(editor)

        self.assertEqual(editor.paths, {"/foo": {}, "/bar": {}})
Esempio n. 14
0
    def setUp(self):

        self.original_openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {
                    "get": {
                        _X_INTEGRATION: {
                            "a": "b"
                        }
                    },
                    "post": {
                        _X_INTEGRATION: {
                            "a": "b"
                        }
                    }
                },
                "/bar": {
                    "get": {
                        _X_INTEGRATION: {
                            "a": "b"
                        }
                    }
                },
            },
        }

        self.editor = OpenApiEditor(self.original_openapi)
Esempio n. 15
0
    def test_must_raise_on_valid_swagger(self):

        valid_swagger = {
            "swagger": "2.0",  # "openapi": "2.1.0"
            "paths": {"/foo": {}, "/bar": {}},
        }  # missing openapi key word
        with self.assertRaises(ValueError):
            OpenApiEditor(valid_swagger)
Esempio n. 16
0
    def setUp(self):

        self.original_openapi = {
            "openapi": "3.0.1",
            "paths": {
                "$default": {
                    "x-amazon-apigateway-any-method": {
                        "Fn::If": [
                            "condition",
                            {
                                "security": [{
                                    "OpenIdAuth": ["scope1", "scope2"]
                                }],
                                "isDefaultRoute": True,
                                "x-amazon-apigateway-integration": {
                                    "httpMethod": "POST",
                                    "type": "aws_proxy",
                                    "uri": {
                                        "Fn::If": [
                                            "condition",
                                            {
                                                "Fn::Sub":
                                                "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HttpApiFunction.Arn}/invocations"
                                            },
                                            {
                                                "Ref": "AWS::NoValue"
                                            },
                                        ]
                                    },
                                    "payloadFormatVersion": "1.0",
                                },
                                "responses": {},
                            },
                            {
                                "Ref": "AWS::NoValue"
                            },
                        ]
                    }
                },
                "/bar": {},
                "/badpath": "string value",
            },
        }

        self.editor = OpenApiEditor(self.original_openapi)
Esempio n. 17
0
class TestOpenApiEditor_add_path(TestCase):
    def setUp(self):

        self.original_openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {
                    "get": {
                        "a": "b"
                    }
                },
                "/bar": {},
                "/badpath": "string value"
            },
        }

        self.editor = OpenApiEditor(self.original_openapi)

    @parameterized.expand([
        param("/new", "get", "new path, new method"),
        param("/foo", "new method", "existing path, new method"),
        param("/bar", "get", "existing path, new method"),
    ])
    def test_must_add_new_path_and_method(self, path, method, case):

        self.assertFalse(self.editor.has_path(path, method))
        self.editor.add_path(path, method)
        self.assertTrue(self.editor.has_path(path, method),
                        "must add for " + case)
        self.assertEqual(self.editor.openapi["paths"][path][method], {})

    def test_must_raise_non_dict_path_values(self):

        path = "/badpath"
        method = "get"

        with self.assertRaises(InvalidDocumentException):
            self.editor.add_path(path, method)

    def test_must_skip_existing_path(self):
        """
        Given an existing path/method, this must
        :return:
        """

        path = "/foo"
        method = "get"
        original_value = copy.deepcopy(
            self.original_openapi["paths"][path][method])

        self.editor.add_path(path, method)
        modified_openapi = self.editor.openapi
        self.assertEqual(original_value,
                         modified_openapi["paths"][path][method])
    def _get_permission(self, resources_to_link, stage):
        # It turns out that APIGW doesn't like trailing slashes in paths (#665)
        # and removes as a part of their behaviour, but this isn't documented.
        # The regex removes the tailing slash to ensure the permission works as intended
        path = re.sub(r"^(.+)/$", r"\1", self.Path)

        editor = None
        if resources_to_link["explicit_api"].get("DefinitionBody"):
            try:
                editor = OpenApiEditor(resources_to_link["explicit_api"].get("DefinitionBody"))
            except ValueError as e:
                api_logical_id = self.ApiId.get("Ref") if isinstance(self.ApiId, dict) else self.ApiId
                raise InvalidResourceException(api_logical_id, e)

        # If this is using the new $default path, keep path blank and add a * permission
        if path == OpenApiEditor._DEFAULT_PATH:
            path = ""
        elif editor and resources_to_link.get("function").logical_id == editor.get_integration_function_logical_id(
            OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD
        ):
            # Case where default exists for this function, and so the permissions for that will apply here as well
            # This can save us several CFN resources (not duplicating permissions)
            return
        else:
            path = OpenApiEditor.get_path_without_trailing_slash(path)

        # Handle case where Method is already the ANY ApiGateway extension
        if self.Method.lower() == "any" or self.Method.lower() == OpenApiEditor._X_ANY_METHOD:
            method = "*"
        else:
            method = self.Method.upper()

        api_id = self.ApiId

        # ApiId can be a simple string or intrinsic function like !Ref. Using Fn::Sub will handle both cases
        resource = "${__ApiId__}/" + "${__Stage__}/" + method + path
        source_arn = fnSub(
            ArnGenerator.generate_arn(partition="${AWS::Partition}", service="execute-api", resource=resource),
            {"__ApiId__": api_id, "__Stage__": stage},
        )

        return self._construct_permission(resources_to_link["function"], source_arn=source_arn)
    def _add_description(self):
        """Add description to DefinitionBody if Description property is set in SAM"""
        if not self.description:
            return

        if not self.definition_body:
            raise InvalidResourceException(
                self.logical_id,
                "Description works only with inline OpenApi specified in the 'DefinitionBody' property.",
            )
        if self.definition_body.get("info", {}).get("description"):
            raise InvalidResourceException(
                self.logical_id,
                "Unable to set Description because it is already defined within inline OpenAPI specified in the "
                "'DefinitionBody' property.",
            )

        open_api_editor = OpenApiEditor(self.definition_body)
        open_api_editor.add_description(self.description)
        self.definition_body = open_api_editor.openapi
Esempio n. 20
0
    def test_must_fail_on_invalid_openapi_version_2(self):
        invalid_openapi = {
            "openapi": "3.1.1.1",
            "paths": {
                "/foo": {},
                "/bar": {}
            }
        }

        with self.assertRaises(ValueError):
            OpenApiEditor(invalid_openapi)
    def _add_tags(self):
        """
        Adds tags to the Http Api, including a default SAM tag.
        """
        if self.tags and not self.definition_body:
            raise InvalidResourceException(
                self.logical_id,
                "Tags works only with inline OpenApi specified in the 'DefinitionBody' property."
            )

        if not self.definition_body:
            return

        if self.tags and not OpenApiEditor.is_valid(self.definition_body):
            raise InvalidResourceException(
                self.logical_id,
                "Unable to add `Tags` because 'DefinitionBody' does not contain a valid OpenApi definition.",
            )
        elif not OpenApiEditor.is_valid(self.definition_body):
            return

        if not self.tags:
            self.tags = {}
        self.tags[HttpApiTagName] = "SAM"

        open_api_editor = OpenApiEditor(self.definition_body)

        # authorizers is guaranteed to return a value or raise an exception
        open_api_editor.add_tags(self.tags)
        self.definition_body = open_api_editor.openapi
Esempio n. 22
0
class TestOpenApiEditor_has_integration(TestCase):
    def setUp(self):
        self.openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {
                    "get": {_X_INTEGRATION: {"a": "b"}},
                    "post": {"Fn::If": ["Condition", {_X_INTEGRATION: {"a": "b"}}, {"Ref": "AWS::NoValue"}]},
                    "delete": {"Fn::If": ["Condition", {"Ref": "AWS::NoValue"}, {_X_INTEGRATION: {"a": "b"}}]},
                    "somemethod": {"foo": "value"},
                    "emptyintegration": {_X_INTEGRATION: {}},
                    "badmethod": "string value",
                }
            },
        }

        self.editor = OpenApiEditor(self.openapi)

    def test_must_find_integration(self):
        self.assertTrue(self.editor.has_integration("/foo", "get"))

    def test_must_find_integration_with_condition(self):
        self.assertTrue(self.editor.has_integration("/foo", "post"))

    def test_must_find_integration_with_condition2(self):
        self.assertTrue(self.editor.has_integration("/foo", "delete"))

    def test_must_not_find_integration(self):
        self.assertFalse(self.editor.has_integration("/foo", "somemethod"))

    def test_must_not_find_empty_integration(self):
        self.assertFalse(self.editor.has_integration("/foo", "emptyintegration"))

    def test_must_handle_bad_value_for_method(self):
        self.assertFalse(self.editor.has_integration("/foo", "badmethod"))
Esempio n. 23
0
class TestOpenApiEditor_iter_on_path(TestCase):
    def setUp(self):

        self.original_openapi = {"openapi": "3.0.1", "paths": {"/foo": {}, "/bar": {}, "/baz": "some value"}}

        self.editor = OpenApiEditor(self.original_openapi)

    def test_must_iterate_on_paths(self):

        expected = {"/foo", "/bar", "/baz"}
        actual = set([path for path in self.editor.iter_on_path()])

        self.assertEqual(expected, actual)
Esempio n. 24
0
 def test_must_fail_for_invalid_values(self, data, case):
     self.assertFalse(OpenApiEditor.is_valid(data), "openapi dictionary with {} must not be valid".format(case))
    def _add_cors(self):
        """
        Add CORS configuration if CORSConfiguration property is set in SAM.
        Adds CORS configuration only if DefinitionBody is present and
        APIGW extension for CORS is not present in the DefinitionBody
        """

        if self.cors_configuration and not self.definition_body:
            raise InvalidResourceException(
                self.logical_id,
                "Cors works only with inline OpenApi specified in 'DefinitionBody' property."
            )

        # If cors configuration is set to true add * to the allow origins.
        # This also support referencing the value as a parameter
        if isinstance(self.cors_configuration, bool):
            # if cors config is true add Origins as "'*'"
            properties = CorsProperties(AllowOrigins=[_CORS_WILDCARD])

        elif is_intrinsic(self.cors_configuration):
            # Just set Origin property. Intrinsics will be handledOthers will be defaults
            properties = CorsProperties(AllowOrigins=self.cors_configuration)

        elif isinstance(self.cors_configuration, dict):
            # Make sure keys in the dict are recognized
            if not all(key in CorsProperties._fields
                       for key in self.cors_configuration.keys()):
                raise InvalidResourceException(
                    self.logical_id, "Invalid value for 'Cors' property.")

            properties = CorsProperties(**self.cors_configuration)

        else:
            raise InvalidResourceException(
                self.logical_id, "Invalid value for 'Cors' property.")

        if not OpenApiEditor.is_valid(self.definition_body):
            raise InvalidResourceException(
                self.logical_id,
                "Unable to add Cors configuration because "
                "'DefinitionBody' does not contain a valid "
                "OpenApi definition.",
            )

        if properties.AllowCredentials is True and properties.AllowOrigins == [
                _CORS_WILDCARD
        ]:
            raise InvalidResourceException(
                self.logical_id,
                "Unable to add Cors configuration because "
                "'AllowCredentials' can not be true when "
                "'AllowOrigin' is \"'*'\" or not set.",
            )

        editor = OpenApiEditor(self.definition_body)
        # if CORS is set in both definition_body and as a CorsConfiguration property,
        # SAM merges and overrides the cors headers in definition_body with headers of CorsConfiguration
        editor.add_cors(
            properties.AllowOrigins,
            properties.AllowHeaders,
            properties.AllowMethods,
            properties.ExposeHeaders,
            properties.MaxAge,
            properties.AllowCredentials,
        )

        # Assign the OpenApi back to template
        self.definition_body = editor.openapi
 def test_auth_missing_default_auth(self):
     self.kwargs["auth"] = self.authorizers
     self.kwargs["auth"]["DefaultAuthorizer"] = "DNE"
     self.kwargs["definition_body"] = OpenApiEditor.gen_skeleton()
     with pytest.raises(InvalidResourceException):
         HttpApiGenerator(**self.kwargs)._construct_http_api()
 def test_auth_invalid_auth_strategy(self):
     self.kwargs["auth"] = {"Authorizers": {"Auth1": "invalid"}}
     self.kwargs["definition_body"] = OpenApiEditor.gen_skeleton()
     with pytest.raises(InvalidResourceException):
         HttpApiGenerator(**self.kwargs)._construct_http_api()
 def test_auth_wrong_properties(self):
     self.kwargs["auth"] = {"Invalid": "auth"}
     self.kwargs["definition_body"] = OpenApiEditor.gen_skeleton()
     with pytest.raises(InvalidResourceException):
         HttpApiGenerator(**self.kwargs)._construct_http_api()
Esempio n. 29
0
 def test_must_not_add_description_if_already_defined(self):
     editor = OpenApiEditor(self.original_openapi_with_description)
     editor.add_description("New Description")
     self.assertEqual(editor.openapi["info"]["description"], "Existing Description")
Esempio n. 30
0
class TestOpenApiEditor_has_path(TestCase):
    def setUp(self):
        self.openapi = {
            "openapi": "3.0.1",
            "paths": {
                "/foo": {"get": {}, "somemethod": {}},
                "/bar": {"post": {}, _X_ANY_METHOD: {}},
                "badpath": "string value",
            },
        }

        self.editor = OpenApiEditor(self.openapi)

    def test_must_find_path_and_method(self):
        self.assertTrue(self.editor.has_path("/foo"))
        self.assertTrue(self.editor.has_path("/foo", "get"))
        self.assertTrue(self.editor.has_path("/foo", "somemethod"))
        self.assertTrue(self.editor.has_path("/bar"))
        self.assertTrue(self.editor.has_path("/bar", "post"))

    def test_must_find_with_method_case_insensitive(self):
        self.assertTrue(self.editor.has_path("/foo", "GeT"))
        self.assertTrue(self.editor.has_path("/bar", "POST"))

        # Only Method is case insensitive. Path is case sensitive
        self.assertFalse(self.editor.has_path("/FOO"))

    def test_must_work_with_any_method(self):
        """
        Method name "ANY" is special. It must be converted to the x-amazon style value before search
        """
        self.assertTrue(self.editor.has_path("/bar", "any"))
        self.assertTrue(self.editor.has_path("/bar", "AnY"))  # Case insensitive
        self.assertTrue(self.editor.has_path("/bar", _X_ANY_METHOD))
        self.assertFalse(self.editor.has_path("/foo", "any"))

    def test_must_not_find_path(self):
        self.assertFalse(self.editor.has_path("/foo/other"))
        self.assertFalse(self.editor.has_path("/bar/xyz"))
        self.assertFalse(self.editor.has_path("/abc"))

    def test_must_not_find_path_and_method(self):
        self.assertFalse(self.editor.has_path("/foo", "post"))
        self.assertFalse(self.editor.has_path("/foo", "abc"))
        self.assertFalse(self.editor.has_path("/bar", "get"))
        self.assertFalse(self.editor.has_path("/bar", "xyz"))

    def test_must_not_fail_on_bad_path(self):

        self.assertTrue(self.editor.has_path("badpath"))
        self.assertFalse(self.editor.has_path("badpath", "somemethod"))