예제 #1
0
    def ensure_no_documentation_if_intentionally_undocumented(
            self, url_pattern: str, method: str) -> None:
        try:
            get_openapi_parameters(url_pattern, method)
            raise AssertionError("We found some OpenAPI \
documentation for %s %s, so maybe we shouldn't mark it as intentionally \
undocumented in the urls." % (method, url_pattern))  # nocoverage
        except KeyError:
            return
예제 #2
0
    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 = """
We found some OpenAPI documentation for {method} {url_pattern},
so maybe we shouldn't mark it as intentionally undocumented in the urls.
""".format(method=method, url_pattern=url_pattern)
            raise AssertionError(msg)  # nocoverage
        except KeyError:
            return
    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 = []  # type: 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, 'r') as fp:
                        json_obj = ujson.load(fp)
                        arguments = json_obj[doc_name]

                if arguments:
                    text = self.render_table(arguments)
                else:
                    text = ['This endpoint does not consume any arguments.']
                # 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
    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 = []  # type: 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, 'r') as fp:
                        json_obj = ujson.load(fp)
                        arguments = json_obj[doc_name]

                if arguments:
                    text = self.render_table(arguments)
                else:
                    text = ['This endpoint does not consume any arguments.']
                # 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
예제 #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 ID of the message that you wish to edit/update.',
         'example': 42,
         'required': True,
         'schema': {'type': 'integer'}
     }
     assert(expected_item in actual)
예제 #6
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)
    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 match:
                    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))

                    try:
                        if is_openapi_format:
                            endpoint, method = doc_name.rsplit(':', 1)
                            arguments = get_openapi_parameters(
                                endpoint, method)
                        else:
                            with open(filename, 'r') as fp:
                                json_obj = ujson.load(fp)
                                arguments = json_obj[doc_name]

                        text = self.render_table(arguments)
                    except Exception as e:
                        print('Warning: could not find file {}. Ignoring '
                              'statement. Error: {}'.format(filename, e))
                        # If the file cannot be opened, just substitute an empty line
                        # in place of the macro include line
                        lines[loc] = REGEXP.sub('', line)
                        break

                    # 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
    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 match:
                    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))

                    try:
                        if is_openapi_format:
                            endpoint, method = doc_name.rsplit(':', 1)
                            arguments = get_openapi_parameters(endpoint, method)
                        else:
                            with open(filename, 'r') as fp:
                                json_obj = ujson.load(fp)
                                arguments = json_obj[doc_name]

                        text = self.render_table(arguments)
                    except Exception as e:
                        print('Warning: could not find file {}. Ignoring '
                              'statement. Error: {}'.format(filename, e))
                        # If the file cannot be opened, just substitute an empty line
                        # in place of the macro include line
                        lines[loc] = REGEXP.sub('', line)
                        break

                    # 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
예제 #9
0
def generate_curl_example(endpoint: str,
                          method: str,
                          auth_email: str = DEFAULT_AUTH_EMAIL,
                          auth_api_key: str = DEFAULT_AUTH_API_KEY,
                          api_url: str = DEFAULT_API_URL,
                          exclude: List[str] = []) -> List[str]:
    lines = ["```curl"]
    openapi_entry = openapi_spec.spec()['paths'][endpoint][method.lower()]

    curl_first_line_parts = ["curl"] + curl_method_arguments(
        endpoint, method, api_url)
    lines.append(" ".join(curl_first_line_parts))

    authentication_required = openapi_entry.get("security", False)
    if authentication_required:
        lines.append("    -u %s:%s" % (auth_email, auth_api_key))

    openapi_example_params = get_openapi_parameters(endpoint, method)
    for packet in openapi_example_params:
        param_name = packet["name"]
        if param_name in exclude:
            continue
        param_type = packet["schema"]["type"]
        if param_type in ["object", "array"]:
            example_value = packet.get("example", None)
            if not example_value:
                msg = """All array and object type request parameters must have
concrete examples. The openAPI documentation for {}/{} is missing an example
value for the {} parameter. Without this we cannot automatically generate a
cURL example.""".format(endpoint, method, param_name)
                raise ValueError(msg)
            ordered_ex_val_str = json.dumps(example_value, sort_keys=True)
            line = "    --data-urlencode {}='{}'".format(
                param_name, ordered_ex_val_str)
        else:
            example_value = packet.get("example", DEFAULT_EXAMPLE[param_type])
            if type(example_value) == bool:
                example_value = str(example_value).lower()
            line = "    -d '{}={}'".format(param_name, example_value)
        lines.append(line)

    for i in range(1, len(lines) - 1):
        lines[i] = lines[i] + " \\"

    lines.append("```")

    return lines
예제 #10
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.
        """

        import zproject.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()  # type: Set[str]
                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.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.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 = set(
                    [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()
예제 #11
0
    def test_openapi_arguments(self) -> None:
        # Verifies that every REQ-defined argument appears in our API
        # documentation for the target endpoint where possible.

        # These should have docs added
        PENDING_ENDPOINTS = set([
            '/users/me/avatar',
            '/user_uploads',
            '/settings/display',
            '/settings/notifications',
            '/users/me/profile_data',
            '/user_groups',
            '/user_groups/create',
            '/users/me/pointer',
            '/users/me/presence',
            '/users/me',
            '/bot_storage',
            '/users/me/api_key/regenerate',
            '/default_streams',
            '/default_stream_groups/create',
            '/users/me/alert_words',
            '/users/me/status',
            '/users/me/subscriptions',
            '/messages/matches_narrow',
            '/settings',
            '/submessage',
            '/attachments',
            '/calls/create',
            '/export/realm',
            '/mark_all_as_read',
            '/zcommand',
            '/realm',
            '/realm/deactivate',
            '/realm/domains',
            '/realm/emoji',
            '/realm/filters',
            '/realm/icon',
            '/realm/logo',
            '/realm/presence',
            '/realm/profile_fields',
            '/queue_id',
            '/invites',
            '/invites/multiuse',
            '/bots',
            # Mobile-app only endpoints
            '/users/me/android_gcm_reg_id',
            '/users/me/apns_device_token',
        ])

        # These endpoints have a mismatch between the documentation
        # and the actual API.  There are situations where we may want
        # to have undocumented parameters for e.g. backwards
        # compatibility, which could be the situation for some of
        # these, in which case we may want a more clever exclude
        # system.  This list can serve as a TODO list for such an
        # investigation.
        BUGGY_DOCUMENTATION_ENDPOINTS = set([
            '/events',
            '/register',
            '/messages',
            '/typing',
            '/users/me/subscriptions/muted_topics',
        ])

        # First, we import the fancy-Django version of zproject/urls.py
        urlconf = __import__(getattr(settings, "ROOT_URLCONF"), {}, {}, [''])

        # We loop through all the API patterns, looking in particular
        # those using the rest_dispatch decorator; we then parse its
        # mapping of (HTTP_METHOD -> FUNCTION).
        for p in urlconf.v1_api_and_json_patterns:
            if p.lookup_str != 'zerver.lib.rest.rest_dispatch':
                continue
            for method, value in p.default_args.items():
                if isinstance(value, str):
                    function = value
                    tags = set()  # type: Set[str]
                else:
                    function, tags = value
                # 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.
                #
                # TODO: Probably with a bit more work, we could get
                # the types, too; `check_int` -> `int`, etc., and
                # verify those too!
                accepted_arguments = set(arguments_map[function])

                # TODO: The purpose of this block is to match our URL
                # pattern regular expressions to the corresponding
                # configuration in OpenAPI.  The means matching
                #
                # /messages/{message_id} <-> r'^messages/(?P<message_id>[0-9]+)$'
                # /events <-> r'^events$'
                #
                # The below is a giant hack that only handles the simple case.
                regex_pattern = p.regex.pattern
                self.assertTrue(regex_pattern.startswith("^"))
                self.assertTrue(regex_pattern.endswith("$"))
                # Obviously this is a huge hack and won't work for
                # some URLs.
                url_pattern = '/' + regex_pattern[1:][:-1]

                if url_pattern in PENDING_ENDPOINTS:
                    # TODO: Once these endpoints have been aptly documented,
                    # we should remove this block and the associated List.
                    continue
                if "intentionally_undocumented" in tags:
                    # Don't do any validation on endpoints with no API
                    # documentation by design.
                    try:
                        get_openapi_parameters(url_pattern, method)
                        raise AssertionError("We found some OpenAPI \
documentation for %s %s, so maybe we shouldn't mark it as intentionally \
undocumented in the urls." % (method, url_pattern))
                    except KeyError:
                        continue
                if "(?P<" in url_pattern:
                    # See above TODO about our matching algorithm not
                    # handling captures in the regular expressions.
                    continue

                try:
                    openapi_parameters = get_openapi_parameters(
                        url_pattern, method)
                    # TODO: Record which entries in the OpenAPI file
                    # found a match, and throw an error for any that
                    # unexpectedly didn't.
                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.regex.pattern is the URL pattern; might require
                #   some processing to match with OpenAPI rules
                #
                # * accepted_arguments_list is the full set of arguments
                #   this method accepts.
                #
                # * 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 = set(
                    [parameter['name'] for parameter in openapi_parameters])

                if len(openapi_parameter_names - accepted_arguments) > 0:
                    print("Undocumented parameters for", url_pattern, method,
                          function)
                    print(" +", openapi_parameter_names)
                    print(" -", accepted_arguments)
                    assert (url_pattern in BUGGY_DOCUMENTATION_ENDPOINTS)
                elif len(accepted_arguments - openapi_parameter_names) > 0:
                    print("Documented invalid parameters for", url_pattern,
                          method, function)
                    print(" -", openapi_parameter_names)
                    print(" +", accepted_arguments)
                    assert (url_pattern in BUGGY_DOCUMENTATION_ENDPOINTS)
                else:
                    self.assertEqual(openapi_parameter_names,
                                     accepted_arguments)
예제 #12
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.
        """

        urlconf = __import__(getattr(settings, "ROOT_URLCONF"), {}, {}, [''])
        __import__('zerver.views.typing')
        __import__('zerver.views.events_register')
        __import__('zerver.views.realm_emoji')

        # 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:
            if p.lookup_str != 'zerver.lib.rest.rest_dispatch':
                continue
            for method, value in p.default_args.items():
                if isinstance(value, str):
                    function_name = value
                    tags = set()  # type: Set[str]
                else:
                    function_name, tags = value

                # 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.
                #
                # TODO: Probably with a bit more work, we could get
                # the types, too; `check_int` -> `int`, etc., and
                # verify those too!
                accepted_arguments = set(arguments_map[function_name])

                regex_pattern = p.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:
                    openapi_parameters = get_openapi_parameters(
                        url_pattern, method)
                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.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 = set(
                    [parameter['name'] for parameter in openapi_parameters])

                if len(openapi_parameter_names - accepted_arguments) > 0:
                    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(accepted_arguments - openapi_parameter_names) > 0:
                    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.checked_endpoints.add(url_pattern)

        self.check_for_non_existant_openapi_endpoints()
예제 #13
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.
        """

        urlconf = __import__(getattr(settings, "ROOT_URLCONF"), {}, {}, [''])
        __import__('zerver.views.typing')
        __import__('zerver.views.events_register')
        __import__('zerver.views.realm_emoji')

        # 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:
            if p.lookup_str != 'zerver.lib.rest.rest_dispatch':
                continue
            for method, value in p.default_args.items():
                if isinstance(value, str):
                    function = value
                    tags = set()  # type: Set[str]
                else:
                    function, tags = value

                # 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.
                #
                # TODO: Probably with a bit more work, we could get
                # the types, too; `check_int` -> `int`, etc., and
                # verify those too!
                accepted_arguments = set(arguments_map[function])

                # Convert regular expressions style URL patterns to their
                # corresponding OpenAPI style formats.
                # E.G.
                #     /messages/{message_id} <-> r'^messages/(?P<message_id>[0-9]+)$'
                #     /events <-> r'^events$'
                regex_pattern = p.regex.pattern
                self.assertTrue(regex_pattern.startswith("^"))
                self.assertTrue(regex_pattern.endswith("$"))
                url_pattern = '/' + regex_pattern[1:][:-1]
                # Deal with the conversion of named capturing groups:
                # Two possible ways to denote variables in urls exist.
                # {var_name} and <var_name>. So we need to consider both.
                url_patterns = [
                    re.sub(r"\(\?P<(\w+)>[^/]+\)", r"<\1>", url_pattern),
                    re.sub(r"\(\?P<(\w+)>[^/]+\)", r"{\1}", url_pattern)
                ]

                if any([
                        url_patterns[0] in self.pending_endpoints,
                        url_patterns[1] in self.pending_endpoints
                ]):
                    continue
                if "intentionally_undocumented" in tags:
                    error = AssertionError(
                        "We found some OpenAPI \
documentation for %s %s, so maybe we shouldn't mark it as intentionally \
undocumented in the urls." %
                        (method, url_patterns[0] + " or " + url_patterns[1]))

                    try:
                        get_openapi_parameters(url_patterns[0], method)
                        raise error  # nocoverage
                    except KeyError:
                        pass

                    try:
                        get_openapi_parameters(url_patterns[1], method)
                        raise error  # nocoverage
                    except KeyError:
                        pass

                    continue  # nocoverage # although, this *is* covered.

                try:
                    openapi_parameters = get_openapi_parameters(
                        url_patterns[0], method)
                except Exception:  # nocoverage
                    try:
                        openapi_parameters = get_openapi_parameters(
                            url_patterns[1], method)
                    except Exception:  # nocoverage
                        raise AssertionError(
                            "Could not find OpenAPI docs for %s %s" %
                            (method,
                             url_patterns[0] + " or " + url_patterns[1]))

                # 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.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 = set(
                    [parameter['name'] for parameter in openapi_parameters])

                if len(openapi_parameter_names - accepted_arguments) > 0:
                    print("Undocumented parameters for",
                          url_patterns[0] + " or " + url_patterns[1], method,
                          function)
                    print(" +", openapi_parameter_names)
                    print(" -", accepted_arguments)
                    assert (any([
                        url_patterns[0] in self.buggy_documentation_endpoints,
                        url_patterns[1] in self.buggy_documentation_endpoints
                    ]))
                elif len(accepted_arguments - openapi_parameter_names) > 0:
                    print("Documented invalid parameters for",
                          url_patterns[0] + " or " + url_patterns[1], method,
                          function)
                    print(" -", openapi_parameter_names)
                    print(" +", accepted_arguments)
                    assert (any([
                        url_patterns[0] in self.buggy_documentation_endpoints,
                        url_patterns[1] in self.buggy_documentation_endpoints
                    ]))
                else:
                    self.assertEqual(openapi_parameter_names,
                                     accepted_arguments)
                    self.checked_endpoints.add(url_patterns[0])
                    self.checked_endpoints.add(url_patterns[1])

        openapi_paths = set(get_openapi_paths())
        undocumented_paths = openapi_paths - self.checked_endpoints
        undocumented_paths -= self.buggy_documentation_endpoints
        undocumented_paths -= self.pending_endpoints
        try:
            self.assertEqual(len(undocumented_paths), 0)
        except AssertionError:  # nocoverage
            msg = "The following endpoints have been documented but can't be found in urls.py:"
            for undocumented_path in undocumented_paths:
                msg += "\n + {}".format(undocumented_path)
            raise AssertionError(msg)