コード例 #1
0
ファイル: test_openapi.py プロジェクト: mrchntia/zulip
    def ensure_no_documentation_if_intentionally_undocumented(
        self, url_pattern: str, method: str, msg: Optional[str] = None
    ) -> None:
        try:
            get_openapi_parameters(url_pattern, method)
            if not msg:  # nocoverage
                msg = f"""
We found some OpenAPI documentation for {method} {url_pattern},
so maybe we shouldn't mark it as intentionally undocumented in the URLs.
"""
            raise AssertionError(msg)  # nocoverage
        except KeyError:
            return
コード例 #2
0
    def run(self, lines: List[str]) -> List[str]:
        done = False
        while not done:
            for line in lines:
                loc = lines.index(line)
                match = REGEXP.search(line)

                if not match:
                    continue

                filename = match.group(1)
                doc_name = match.group(2)
                filename = os.path.expanduser(filename)

                is_openapi_format = filename.endswith(".yaml")

                if not os.path.isabs(filename):
                    parent_dir = self.base_path
                    filename = os.path.normpath(
                        os.path.join(parent_dir, filename))

                if is_openapi_format:
                    endpoint, method = doc_name.rsplit(":", 1)
                    arguments: List[Dict[str, Any]] = []

                    try:
                        arguments = get_openapi_parameters(endpoint, method)
                    except KeyError as e:
                        # Don't raise an exception if the "parameters"
                        # field is missing; we assume that's because the
                        # endpoint doesn't accept any parameters
                        if e.args != ("parameters", ):
                            raise e
                else:
                    with open(filename) as fp:
                        json_obj = json.load(fp)
                        arguments = json_obj[doc_name]

                if arguments:
                    text = self.render_table(arguments)
                # We want to show this message only if the parameters
                # description doesn't say anything else.
                elif is_openapi_format and get_parameters_description(
                        endpoint, method) == "":
                    text = ["This endpoint does not accept any parameters."]
                else:
                    text = []
                # The line that contains the directive to include the macro
                # may be preceded or followed by text or tags, in that case
                # we need to make sure that any preceding or following text
                # stays the same.
                line_split = REGEXP.split(line, maxsplit=0)
                preceding = line_split[0]
                following = line_split[-1]
                text = [preceding, *text, following]
                lines = lines[:loc] + text + lines[loc + 1:]
                break
            else:
                done = True
        return lines
コード例 #3
0
ファイル: test_openapi.py プロジェクト: mrchntia/zulip
 def test_get_openapi_parameters(self) -> None:
     actual = get_openapi_parameters(TEST_ENDPOINT, TEST_METHOD)
     expected_item = {
         "name": "message_id",
         "in": "path",
         "description": "The target message's ID.\n",
         "example": 42,
         "required": True,
         "schema": {"type": "integer"},
     }
     assert expected_item in actual
コード例 #4
0
 def test_get_openapi_parameters(self) -> None:
     actual = get_openapi_parameters(TEST_ENDPOINT, TEST_METHOD)
     expected_item = {
         'name': 'message_id',
         'in': 'path',
         'description':
             'The ID of the message that you wish to edit/update.',
         'example': 42,
         'required': True,
         'schema': {'type': 'integer'}
     }
     assert(expected_item in actual)
コード例 #5
0
 def test_get_openapi_parameters(self) -> None:
     actual = get_openapi_parameters(TEST_ENDPOINT, TEST_METHOD)
     expected_item = {
         'name': 'message_id',
         'in': 'path',
         'description':
             'The target message\'s ID.\n',
         'example': 42,
         'required': True,
         'schema': {'type': 'integer'},
     }
     assert(expected_item in actual)
コード例 #6
0
    def test_openapi_arguments(self) -> None:
        """This end-to-end API documentation test compares the arguments
        defined in the actual code using @has_request_variables and
        REQ(), with the arguments declared in our API documentation
        for every API endpoint in Zulip.

        First, we import the fancy-Django version of zproject/urls.py
        by doing this, each has_request_variables wrapper around each
        imported view function gets called to generate the wrapped
        view function and thus filling the global arguments_map variable.
        Basically, we're exploiting code execution during import.

            Then we need to import some view modules not already imported in
        urls.py. We use this different syntax because of the linters complaining
        of an unused import (which is correct, but we do this for triggering the
        has_request_variables decorator).

            At the end, we perform a reverse mapping test that verifies that
        every url pattern defined in the openapi documentation actually exists
        in code.
        """

        from zproject import urls as urlconf

        # We loop through all the API patterns, looking in particular
        # for those using the rest_dispatch decorator; we then parse
        # its mapping of (HTTP_METHOD -> FUNCTION).
        for p in urlconf.v1_api_and_json_patterns + urlconf.v1_api_mobile_patterns:
            if p.lookup_str != 'zerver.lib.rest.rest_dispatch':
                # Endpoints not using rest_dispatch don't have extra data.
                methods_endpoints = dict(GET=p.lookup_str, )
            else:
                methods_endpoints = p.default_args

            # since the module was already imported and is now residing in
            # memory, we won't actually face any performance penalties here.
            for method, value in methods_endpoints.items():
                if isinstance(value, str):
                    function_name = value
                    tags: Set[str] = set()
                else:
                    function_name, tags = value

                if function_name == 'zerver.tornado.views.get_events':
                    # Work around the fact that the registered
                    # get_events view function isn't where we do
                    # @has_request_variables.
                    #
                    # TODO: Make this configurable via an optional argument
                    # to has_request_variables, e.g.
                    # @has_request_variables(view_func_name="zerver.tornado.views.get_events")
                    function_name = 'zerver.tornado.views.get_events_backend'

                lookup_parts = function_name.split('.')
                module = __import__('.'.join(lookup_parts[:-1]), {}, {}, [''])
                function = getattr(module, lookup_parts[-1])

                # Our accounting logic in the `has_request_variables()`
                # code means we have the list of all arguments
                # accepted by every view function in arguments_map.
                accepted_arguments = set(arguments_map[function_name])

                regex_pattern = p.pattern.regex.pattern
                url_pattern = self.convert_regex_to_url_pattern(regex_pattern)

                if "intentionally_undocumented" in tags:
                    self.ensure_no_documentation_if_intentionally_undocumented(
                        url_pattern, method)
                    continue

                if url_pattern in self.pending_endpoints:
                    # HACK: After all pending_endpoints have been resolved, we should remove
                    # this segment and the "msg" part of the `ensure_no_...` method.
                    msg = """
We found some OpenAPI documentation for {method} {url_pattern},
so maybe we shouldn't include it in pending_endpoints.
""".format(method=method, url_pattern=url_pattern)
                    self.ensure_no_documentation_if_intentionally_undocumented(
                        url_pattern, method, msg)
                    continue

                try:
                    # Don't include OpenAPI parameters that live in
                    # the path; these are not extracted by REQ.
                    openapi_parameters = get_openapi_parameters(
                        url_pattern, method, include_url_parameters=False)
                except Exception:  # nocoverage
                    raise AssertionError(
                        "Could not find OpenAPI docs for %s %s" %
                        (method, url_pattern))

                # We now have everything we need to understand the
                # function as defined in our urls.py:
                #
                # * method is the HTTP method, e.g. GET, POST, or PATCH
                #
                # * p.pattern.regex.pattern is the URL pattern; might require
                #   some processing to match with OpenAPI rules
                #
                # * accepted_arguments is the full set of arguments
                #   this method accepts (from the REQ declarations in
                #   code).
                #
                # * The documented parameters for the endpoint as recorded in our
                #   OpenAPI data in zerver/openapi/zulip.yaml.
                #
                # We now compare these to confirm that the documented
                # argument list matches what actually appears in the
                # codebase.

                openapi_parameter_names = {
                    parameter['name']
                    for parameter in openapi_parameters
                }

                if len(accepted_arguments -
                       openapi_parameter_names) > 0:  # nocoverage
                    print("Undocumented parameters for", url_pattern, method,
                          function_name)
                    print(" +", openapi_parameter_names)
                    print(" -", accepted_arguments)
                    assert (url_pattern in self.buggy_documentation_endpoints)
                elif len(openapi_parameter_names -
                         accepted_arguments) > 0:  # nocoverage
                    print("Documented invalid parameters for", url_pattern,
                          method, function_name)
                    print(" -", openapi_parameter_names)
                    print(" +", accepted_arguments)
                    assert (url_pattern in self.buggy_documentation_endpoints)
                else:
                    self.assertEqual(openapi_parameter_names,
                                     accepted_arguments)
                    self.check_argument_types(function, openapi_parameters)
                    self.checked_endpoints.add(url_pattern)

        self.check_for_non_existant_openapi_endpoints()