2018-05-31 19:41:17 +02:00
# -*- coding: utf-8 -*-
2019-07-04 18:12:53 +02:00
import re
2019-07-10 13:23:25 +02:00
import sys
2018-08-08 01:35:41 +02:00
import mock
2019-07-10 13:23:25 +02:00
import inspect
import typing
2019-08-04 15:55:32 +02:00
from typing import Dict , Any , Set , Union , List , Callable , Tuple , Optional , Iterable , Mapping
2019-07-29 15:46:48 +02:00
from unittest . mock import patch , MagicMock
2018-05-31 19:41:17 +02:00
2019-06-06 22:22:21 +02:00
from django . conf import settings
2019-07-10 13:23:25 +02:00
from django . http import HttpResponse
2019-06-06 22:22:21 +02:00
2018-06-20 19:31:24 +02:00
import zerver . lib . openapi as openapi
2019-07-29 15:46:48 +02:00
from zerver . lib . bugdown . api_code_examples import generate_curl_example , \
2019-08-04 08:14:08 +02:00
render_curl_example , parse_language_and_options
2019-08-07 11:15:46 +02:00
from zerver . lib . request import _REQ
2018-05-31 19:41:17 +02:00
from zerver . lib . test_classes import ZulipTestCase
from zerver . lib . openapi import (
get_openapi_fixture , get_openapi_parameters ,
2019-07-08 14:08:02 +02:00
validate_against_openapi_schema , to_python_type ,
SchemaError , openapi_spec , get_openapi_paths
2018-05-31 19:41:17 +02:00
)
2019-06-06 22:22:21 +02:00
from zerver . lib . request import arguments_map
2018-05-31 19:41:17 +02:00
TEST_ENDPOINT = ' /messages/ {message_id} '
TEST_METHOD = ' patch '
2018-06-18 16:32:30 +02:00
TEST_RESPONSE_BAD_REQ = ' 400 '
TEST_RESPONSE_SUCCESS = ' 200 '
2018-05-31 19:41:17 +02:00
2019-07-10 13:23:25 +02:00
VARMAP = {
' integer ' : int ,
' string ' : str ,
' boolean ' : bool ,
' array ' : list ,
' Typing.List ' : list ,
2019-08-04 15:55:32 +02:00
' object ' : dict ,
2019-07-10 13:23:25 +02:00
' NoneType ' : None ,
}
2018-05-31 19:41:17 +02:00
class OpenAPIToolsTest ( ZulipTestCase ) :
""" Make sure that the tools we use to handle our OpenAPI specification
( located in zerver / lib / openapi . py ) work as expected .
These tools are mostly dedicated to fetching parts of the - already parsed -
specification , and comparing them to objects returned by our REST API .
"""
def test_get_openapi_fixture ( self ) - > None :
2018-06-18 16:32:30 +02:00
actual = get_openapi_fixture ( TEST_ENDPOINT , TEST_METHOD ,
TEST_RESPONSE_BAD_REQ )
2018-05-31 19:41:17 +02:00
expected = {
' code ' : ' BAD_REQUEST ' ,
' msg ' : ' You don \' t have permission to edit this message ' ,
' result ' : ' error '
}
self . assertEqual ( actual , expected )
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 test_validate_against_openapi_schema ( self ) - > None :
with self . assertRaises ( SchemaError ,
msg = ( ' Extraneous key " foo " in '
' the response \' scontent ' ) ) :
bad_content = {
' msg ' : ' ' ,
' result ' : ' success ' ,
' foo ' : ' bar '
} # type: Dict[str, Any]
validate_against_openapi_schema ( bad_content ,
TEST_ENDPOINT ,
2018-06-18 16:32:30 +02:00
TEST_METHOD ,
TEST_RESPONSE_SUCCESS )
2018-05-31 19:41:17 +02:00
with self . assertRaises ( SchemaError ,
msg = ( " Expected type <class ' str ' > for key "
" \" msg \" , but actually got "
" <class ' int ' > " ) ) :
bad_content = {
' msg ' : 42 ,
' result ' : ' success ' ,
}
validate_against_openapi_schema ( bad_content ,
TEST_ENDPOINT ,
2018-06-18 16:32:30 +02:00
TEST_METHOD ,
TEST_RESPONSE_SUCCESS )
2018-05-31 19:41:17 +02:00
with self . assertRaises ( SchemaError ,
msg = ' Expected to find the " msg " required key ' ) :
bad_content = {
' result ' : ' success ' ,
}
validate_against_openapi_schema ( bad_content ,
TEST_ENDPOINT ,
2018-06-18 16:32:30 +02:00
TEST_METHOD ,
TEST_RESPONSE_SUCCESS )
2018-05-31 19:41:17 +02:00
# No exceptions should be raised here.
good_content = {
' msg ' : ' ' ,
' result ' : ' success ' ,
}
validate_against_openapi_schema ( good_content ,
TEST_ENDPOINT ,
2018-06-18 16:32:30 +02:00
TEST_METHOD ,
TEST_RESPONSE_SUCCESS )
2018-05-31 19:41:17 +02:00
2018-06-20 19:31:24 +02:00
# Overwrite the exception list with a mocked one
openapi . EXCLUDE_PROPERTIES = {
TEST_ENDPOINT : {
TEST_METHOD : {
TEST_RESPONSE_SUCCESS : [ ' foo ' ]
}
}
}
good_content = {
' msg ' : ' ' ,
' result ' : ' success ' ,
' foo ' : ' bar '
}
validate_against_openapi_schema ( good_content ,
TEST_ENDPOINT ,
TEST_METHOD ,
TEST_RESPONSE_SUCCESS )
2018-05-31 19:41:17 +02:00
def test_to_python_type ( self ) - > None :
TYPES = {
' string ' : str ,
' number ' : float ,
' integer ' : int ,
' boolean ' : bool ,
' array ' : list ,
' object ' : dict
}
for oa_type , py_type in TYPES . items ( ) :
self . assertEqual ( to_python_type ( oa_type ) , py_type )
2018-08-07 23:40:07 +02:00
def test_live_reload ( self ) - > None :
# Force the reload by making the last update date < the file's last
# modified date
openapi_spec . last_update = 0
get_openapi_fixture ( TEST_ENDPOINT , TEST_METHOD )
# Check that the file has been reloaded by verifying that the last
# update date isn't zero anymore
self . assertNotEqual ( openapi_spec . last_update , 0 )
2018-08-08 01:35:41 +02:00
# Now verify calling it again doesn't call reload
with mock . patch ( ' zerver.lib.openapi.openapi_spec.reload ' ) as mock_reload :
get_openapi_fixture ( TEST_ENDPOINT , TEST_METHOD )
self . assertFalse ( mock_reload . called )
2019-06-06 22:22:21 +02:00
class OpenAPIArgumentsTest ( ZulipTestCase ) :
2019-07-07 08:54:19 +02:00
# This will be filled during test_openapi_arguments:
checked_endpoints = set ( ) # type: Set[str]
# TODO: These endpoints need to be documented:
pending_endpoints = set ( [
' /users/me/avatar ' ,
' /settings/display ' ,
' /users/me/profile_data ' ,
' /users/me/pointer ' ,
' /users/me/presence ' ,
' /bot_storage ' ,
' /users/me/api_key/regenerate ' ,
' /default_streams ' ,
' /default_stream_groups/create ' ,
' /users/me/alert_words ' ,
' /users/me/status ' ,
' /messages/matches_narrow ' ,
' /settings ' ,
' /submessage ' ,
' /attachments ' ,
' /calls/create ' ,
' /export/realm ' ,
' /zcommand ' ,
' /realm ' ,
' /realm/deactivate ' ,
' /realm/domains ' ,
' /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 ' ,
# Regex based urls
2019-07-11 12:45:26 +02:00
' /realm/domains/ {domain} ' ,
' /realm/profile_fields/ {field_id} ' ,
' /users/ {user_id} /reactivate ' ,
' /users/ {user_id} ' ,
' /bots/ {bot_id} /api_key/regenerate ' ,
' /bots/ {bot_id} ' ,
' /invites/ {prereg_id} ' ,
' /invites/ {prereg_id} /resend ' ,
' /invites/multiuse/ {invite_id} ' ,
2019-07-08 14:08:02 +02:00
' /users/me/subscriptions/ {stream_id} ' ,
2019-07-11 12:45:26 +02:00
' /messages/ {message_id} /reactions ' ,
' /messages/ {message_id} /emoji_reactions/ {emoji_name} ' ,
' /attachments/ {attachment_id} ' ,
' /user_groups/ {user_group_id} /members ' ,
' /streams/ {stream_id} /members ' ,
' /streams/ {stream_id} /delete_topic ' ,
' /default_stream_groups/ {group_id} ' ,
' /default_stream_groups/ {group_id} /streams ' ,
2019-07-07 08:54:19 +02:00
# Regex with an unnamed capturing group.
' /users/(?!me/)(?P<email>[^/]*)/presence ' ,
2019-07-08 14:08:02 +02:00
# Actually '/user_groups/<user_group_id>' in urls.py but fails the reverse mapping
# test because of the variable name mismatch. So really, it's more of a buggy endpoint.
' /user_groups/ {group_id} ' , # Equivalent of what's in urls.py
' /user_groups/ {user_group_id} ' , # What's in the OpenAPI docs
2019-07-07 08:54:19 +02:00
] )
# TODO: These endpoints have a mismatch between the
# documentation and the actual API and need to be fixed:
buggy_documentation_endpoints = set ( [
' /events ' ,
' /users/me/subscriptions/muted_topics ' ,
2019-07-15 22:33:16 +02:00
# List of flags is broader in actual code; fix is to just add them
' /settings/notifications ' ,
# Endpoint is documented; parameters aren't detected properly.
' /realm/filters ' ,
' /realm/filters/ {filter_id} ' ,
# Docs need update for subject -> topic migration
' /messages/ {message_id} ' ,
# stream_id parameter incorrectly appears in both URL and endpoint parameters?
' /streams/ {stream_id} ' ,
2019-07-08 14:08:02 +02:00
# pattern starts with /api/v1 and thus fails reverse mapping test.
' /dev_fetch_api_key ' ,
' /server_settings ' ,
# Because of the unnamed capturing group, this fails the reverse mapping test.
' /users/ {email} /presence ' ,
2019-07-07 08:54:19 +02:00
] )
2019-07-09 08:28:29 +02:00
def convert_regex_to_url_pattern ( self , regex_pattern : str ) - > str :
""" Convert regular expressions style URL patterns to their
corresponding OpenAPI style formats . All patterns are
expected to start with ^ and end with $ .
Examples :
1. / messages / { message_id } < - > r ' ^messages/(?P<message_id>[0-9]+)$ '
2. / events < - > r ' ^events$ '
"""
self . assertTrue ( regex_pattern . startswith ( " ^ " ) )
self . assertTrue ( regex_pattern . endswith ( " $ " ) )
url_pattern = ' / ' + regex_pattern [ 1 : ] [ : - 1 ]
url_pattern = re . sub ( r " \ ( \ ?P<( \ w+)>[^/]+ \ ) " , r " { \ 1} " , url_pattern )
return url_pattern
def ensure_no_documentation_if_intentionally_undocumented ( self , url_pattern : str ,
2019-07-20 20:16:47 +02:00
method : str ,
msg : Optional [ str ] = None ) - > None :
2019-07-09 08:28:29 +02:00
try :
get_openapi_parameters ( url_pattern , method )
2019-07-19 07:02:10 +02:00
if not msg : # nocoverage
2019-07-20 20:16:47 +02:00
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
2019-07-09 08:28:29 +02:00
except KeyError :
return
def check_for_non_existant_openapi_endpoints ( self ) - > None :
""" Here, we check to see if every endpoint documented in the openapi
documentation actually exists in urls . py and thus in actual code .
Note : We define this as a helper called at the end of
test_openapi_arguments instead of as a separate test to ensure that
this test is only executed after test_openapi_arguments so that it ' s
results can be used here in the set operations . """
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 )
2019-08-04 15:55:32 +02:00
def get_type_by_priority ( self , types : List [ Union [ type , Tuple [ type , type ] ] ] ) - > Union [ type , Tuple [ type , type ] ] :
priority = { list : 1 , dict : 2 , str : 3 , int : 4 , bool : 5 }
tyiroirp = { 1 : list , 2 : dict , 3 : str , 4 : int , 5 : bool }
val = 6
2019-07-10 13:23:25 +02:00
for t in types :
2019-08-04 15:55:32 +02:00
if type ( t ) is tuple :
return t # e.g. (list, dict) or (list ,str)
v = priority . get ( t , 6 ) # type: ignore # if t was a Tuple then we would have returned by this point
2019-07-10 13:23:25 +02:00
if v < val :
val = v
return tyiroirp . get ( val , types [ 0 ] )
2019-08-04 15:55:32 +02:00
def get_standardized_argument_type ( self , t : Any ) - > Union [ type , Tuple [ type , type ] ] :
2019-07-10 13:23:25 +02:00
""" Given a type from the typing module such as List[str] or Union[str, int],
convert it into a corresponding Python type . Unions are mapped to a canonical
choice among the options .
E . g . typing . Union [ typing . List [ typing . Dict [ str , typing . Any ] ] , NoneType ]
needs to be mapped to list . """
if sys . version [ : 3 ] == " 3.5 " and type ( t ) == typing . UnionMeta : # nocoverage # in python3.6+
origin = Union
else : # nocoverage # in python3.5. I.E. this is used in python3.6+
origin = getattr ( t , " __origin__ " , None )
if not origin :
# Then it's most likely one of the fundamental data types
# I.E. Not one of the data types from the "typing" module.
return t
elif origin == Union :
subtypes = [ ]
if sys . version [ : 3 ] == " 3.5 " : # nocoverage # in python3.6+
args = t . __union_params__
else : # nocoverage # in python3.5
args = t . __args__
for st in args :
2019-08-04 15:55:32 +02:00
subtypes . append ( self . get_standardized_argument_type ( st ) )
2019-07-10 13:23:25 +02:00
return self . get_type_by_priority ( subtypes )
2019-08-04 15:55:32 +02:00
elif origin in [ List , Iterable ] :
subtypes = [ self . get_standardized_argument_type ( st ) for st in t . __args__ ]
return ( list , self . get_type_by_priority ( subtypes ) ) # type: ignore # this might be a Tuple[Type[List[Any]], Union[type, Tuple[type, type]]] but that means that it's a Tuple[type, Union[type, Tuple[type, type]]] too.
elif origin in [ Dict , Mapping ] :
return dict
return self . get_standardized_argument_type ( t . __args__ [ 0 ] )
2019-07-10 13:23:25 +02:00
def render_openapi_type_exception ( self , function : Callable [ . . . , HttpResponse ] ,
2019-08-04 15:55:32 +02:00
openapi_params : Set [ Tuple [ str , Union [ type , Tuple [ type , type ] ] ] ] ,
function_params : Set [ Tuple [ str , Union [ type , Tuple [ type , type ] ] ] ] ,
diff : Set [ Tuple [ str , Union [ type , Tuple [ type , type ] ] ] ] ) - > None : # nocoverage
2019-07-10 13:23:25 +02:00
""" Print a *VERY* clear and verbose error message for when the types
( between the OpenAPI documentation and the function declaration ) don ' t match. " " "
msg = """
The types for the request parameters in zerver / openapi / zulip . yaml
do not match the types declared in the implementation of { } . \n """ .format(function.__name__)
msg + = ' = ' * 65 + ' \n '
msg + = " {:<10s} {:^30s} {:>10s} \n " . format ( " Parameter " , " OpenAPI Type " ,
" Function Declaration Type " )
msg + = ' = ' * 65 + ' \n '
opvtype = None
fdvtype = None
for element in diff :
vname = element [ 0 ]
for element in openapi_params :
if element [ 0 ] == vname :
opvtype = element [ 1 ]
break
for element in function_params :
if element [ 0 ] == vname :
fdvtype = element [ 1 ]
break
msg + = " {:<10s} {:^30s} {:>10s} \n " . format ( vname , str ( opvtype ) , str ( fdvtype ) )
raise AssertionError ( msg )
def check_argument_types ( self , function : Callable [ . . . , HttpResponse ] ,
openapi_parameters : List [ Dict [ str , Any ] ] ) - > None :
""" We construct for both the OpenAPI data and the function ' s definition a set of
tuples of the form ( var_name , type ) and then compare those sets to see if the
OpenAPI data defines a different type than that actually accepted by the function .
Otherwise , we print out the exact differences for convenient debugging and raise an
AssertionError . """
2019-08-04 15:55:32 +02:00
openapi_params = set ( ) # type: Set[Tuple[str, Union[type, Tuple[type, type]]]]
for element in openapi_parameters :
name = element [ " name " ] # type: str
_type = VARMAP [ element [ " schema " ] [ " type " ] ]
if _type == list :
items = element [ " schema " ] [ " items " ]
if " anyOf " in items . keys ( ) :
subtypes = [ ]
for st in items [ " anyOf " ] :
st = st [ " type " ]
subtypes . append ( VARMAP [ st ] )
self . assertTrue ( len ( subtypes ) > 1 )
sub_type = self . get_type_by_priority ( subtypes ) # type: ignore # sub_type is not List[Optional[type]], it's a List[type], handled in above step.
else :
sub_type = VARMAP [ element [ " schema " ] [ " items " ] [ " type " ] ] # type: ignore # we handle the case of None in the next step.
self . assertIsNotNone ( sub_type )
openapi_params . add ( ( name , ( _type , sub_type ) ) ) # type: ignore # things are pretty dynamic at this point and we make some inferences so mypy gets confused.
else :
openapi_params . add ( ( name , _type ) ) # type: ignore # in our given case, in this block, _type will always be a regular type.
function_params = set ( ) # type: Set[Tuple[str, Union[type, Tuple[type, type]]]]
2019-07-10 13:23:25 +02:00
# Iterate through the decorators to find the original
# function, wrapped by has_request_variables, so we can parse
# its arguments.
while getattr ( function , " __wrapped__ " , None ) :
function = getattr ( function , " __wrapped__ " , None )
# Tell mypy this is never None.
assert function is not None
# Now, we do inference mapping each REQ parameter's
# declaration details to the Python/mypy types for the
# arguments passed to it.
#
# Because the mypy types are the types used inside the inner
# function (after the original data is processed by any
# validators, converters, etc.), they will not always match
# the API-level argument types. The main case where this
# happens is when a `converter` is used that changes the types
# of its parameters.
for vname , defval in inspect . signature ( function ) . parameters . items ( ) :
defval = defval . default
2019-08-07 11:15:46 +02:00
if defval . __class__ is _REQ :
2019-07-10 13:23:25 +02:00
# TODO: The below inference logic in cases where
# there's a converter function declared is incorrect.
# Theoretically, we could restructure the converter
# function model so that we can check what type it
# excepts to be passed to make validation here
# possible.
vtype = self . get_standardized_argument_type ( function . __annotations__ [ vname ] )
2019-08-07 11:15:46 +02:00
vname = defval . post_var_name # type: ignore # See zerver/lib/request.py
2019-07-10 13:23:25 +02:00
function_params . add ( ( vname , vtype ) )
diff = openapi_params - function_params
if diff : # nocoverage
self . render_openapi_type_exception ( function , openapi_params , function_params , diff )
2019-06-06 22:22:21 +02:00
def test_openapi_arguments ( self ) - > None :
2019-07-07 08:54:19 +02:00
""" 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 ) .
2019-07-08 14:08:02 +02:00
At the end , we perform a reverse mapping test that verifies that
every url pattern defined in the openapi documentation actually exists
in code .
2019-07-07 08:54:19 +02:00
"""
2019-06-06 22:22:21 +02:00
urlconf = __import__ ( getattr ( settings , " ROOT_URLCONF " ) , { } , { } , [ ' ' ] )
# We loop through all the API patterns, looking in particular
2019-07-07 08:54:19 +02:00
# for those using the rest_dispatch decorator; we then parse
# its mapping of (HTTP_METHOD -> FUNCTION).
2019-06-06 22:22:21 +02:00
for p in urlconf . v1_api_and_json_patterns :
if p . lookup_str != ' zerver.lib.rest.rest_dispatch ' :
continue
2019-07-10 13:23:25 +02:00
# since the module was already imported and is now residing in
# memory, we won't actually face any performance penalties here.
2019-06-06 22:22:21 +02:00
for method , value in p . default_args . items ( ) :
if isinstance ( value , str ) :
2019-07-15 17:53:51 +02:00
function_name = value
2019-07-01 13:22:54 +02:00
tags = set ( ) # type: Set[str]
2019-06-06 22:22:21 +02:00
else :
2019-07-15 17:53:51 +02:00
function_name , tags = value
2019-07-07 08:54:19 +02:00
2019-07-10 13:23:25 +02:00
lookup_parts = function_name . split ( ' . ' )
module = __import__ ( ' . ' . join ( lookup_parts [ : - 1 ] ) , { } , { } , [ ' ' ] )
function = getattr ( module , lookup_parts [ - 1 ] )
2019-06-06 22:22:21 +02:00
# 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.
2019-07-15 17:53:51 +02:00
accepted_arguments = set ( arguments_map [ function_name ] )
2019-06-06 22:22:21 +02:00
regex_pattern = p . regex . pattern
2019-07-09 08:28:29 +02:00
url_pattern = self . convert_regex_to_url_pattern ( regex_pattern )
2019-07-04 18:12:53 +02:00
2019-07-20 20:16:47 +02:00
if " intentionally_undocumented " in tags :
2019-07-09 08:28:29 +02:00
self . ensure_no_documentation_if_intentionally_undocumented ( url_pattern , method )
continue
2019-06-06 22:22:21 +02:00
2019-07-20 20:16:47 +02:00
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
2019-06-06 22:22:21 +02:00
try :
2019-07-11 19:05:48 +02:00
openapi_parameters = get_openapi_parameters ( url_pattern , method )
2019-06-06 22:22:21 +02:00
except Exception : # nocoverage
2019-07-11 19:05:48 +02:00
raise AssertionError ( " Could not find OpenAPI docs for %s %s " %
( method , url_pattern ) )
2019-06-06 22:22:21 +02:00
# We now have everything we need to understand the
2019-07-07 08:54:19 +02:00
# function as defined in our urls.py:
2019-06-06 22:22:21 +02:00
#
# * 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
#
2019-07-07 08:54:19 +02:00
# * accepted_arguments is the full set of arguments
# this method accepts (from the REQ declarations in
# code).
2019-06-06 22:22:21 +02:00
#
# * 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 :
2019-07-15 17:53:51 +02:00
print ( " Undocumented parameters for " ,
url_pattern , method , function_name )
2019-06-06 22:22:21 +02:00
print ( " + " , openapi_parameter_names )
print ( " - " , accepted_arguments )
2019-07-11 19:05:48 +02:00
assert ( url_pattern in self . buggy_documentation_endpoints )
2019-06-06 22:22:21 +02:00
elif len ( accepted_arguments - openapi_parameter_names ) > 0 :
2019-07-15 17:53:51 +02:00
print ( " Documented invalid parameters for " ,
url_pattern , method , function_name )
2019-06-06 22:22:21 +02:00
print ( " - " , openapi_parameter_names )
print ( " + " , accepted_arguments )
2019-07-11 19:05:48 +02:00
assert ( url_pattern in self . buggy_documentation_endpoints )
2019-06-06 22:22:21 +02:00
else :
self . assertEqual ( openapi_parameter_names , accepted_arguments )
2019-07-10 13:23:25 +02:00
self . check_argument_types ( function , openapi_parameters )
2019-07-11 19:05:48 +02:00
self . checked_endpoints . add ( url_pattern )
2019-07-08 14:08:02 +02:00
2019-07-09 08:28:29 +02:00
self . check_for_non_existant_openapi_endpoints ( )
2019-07-29 15:46:48 +02:00
2019-08-04 08:14:08 +02:00
class ModifyExampleGenerationTestCase ( ZulipTestCase ) :
def test_no_mod_argument ( self ) - > None :
res = parse_language_and_options ( " python " )
self . assertEqual ( res , ( " python " , { } ) )
def test_single_simple_mod_argument ( self ) - > None :
res = parse_language_and_options ( " curl, mod=1 " )
self . assertEqual ( res , ( " curl " , { " mod " : 1 } ) )
res = parse_language_and_options ( " curl, mod= ' somevalue ' " )
self . assertEqual ( res , ( " curl " , { " mod " : " somevalue " } ) )
res = parse_language_and_options ( " curl, mod= \" somevalue \" " )
self . assertEqual ( res , ( " curl " , { " mod " : " somevalue " } ) )
def test_multiple_simple_mod_argument ( self ) - > None :
res = parse_language_and_options ( " curl, mod1=1, mod2= ' a ' " )
self . assertEqual ( res , ( " curl " , { " mod1 " : 1 , " mod2 " : " a " } ) )
res = parse_language_and_options ( " curl, mod1= \" asdf \" , mod2= ' thing ' , mod3=3 " )
self . assertEqual ( res , ( " curl " , { " mod1 " : " asdf " , " mod2 " : " thing " , " mod3 " : 3 } ) )
def test_single_list_mod_argument ( self ) - > None :
res = parse_language_and_options ( " curl, exclude=[ ' param1 ' , ' param2 ' ] " )
self . assertEqual ( res , ( " curl " , { " exclude " : [ " param1 " , " param2 " ] } ) )
res = parse_language_and_options ( " curl, exclude=[ \" param1 \" , \" param2 \" ] " )
self . assertEqual ( res , ( " curl " , { " exclude " : [ " param1 " , " param2 " ] } ) )
res = parse_language_and_options ( " curl, exclude=[ ' param1 ' , \" param2 \" ] " )
self . assertEqual ( res , ( " curl " , { " exclude " : [ " param1 " , " param2 " ] } ) )
def test_multiple_list_mod_argument ( self ) - > None :
res = parse_language_and_options ( " curl, exclude=[ ' param1 ' , \" param2 \" ], special=[ ' param3 ' ] " )
self . assertEqual ( res , ( " curl " , { " exclude " : [ " param1 " , " param2 " ] , " special " : [ " param3 " ] } ) )
def test_multiple_mixed_mod_arguments ( self ) - > None :
res = parse_language_and_options ( " curl, exclude=[ \" asdf \" , ' sdfg ' ], other_key= ' asdf ' , more_things= \" asdf \" , another_list=[1, \" 2 \" ] " )
self . assertEqual ( res , ( " curl " , { " exclude " : [ " asdf " , " sdfg " ] , " other_key " : " asdf " , " more_things " : " asdf " , " another_list " : [ 1 , " 2 " ] } ) )
2019-07-29 15:46:48 +02:00
class TestCurlExampleGeneration ( ZulipTestCase ) :
spec_mock_without_examples = {
" paths " : {
" /mark_stream_as_read " : {
" post " : {
" description " : " Mark all the unread messages in a stream as read. " ,
" parameters " : [
{
" name " : " stream_id " ,
" in " : " query " ,
" description " : " The ID of the stream whose messages should be marked as read. " ,
" schema " : {
" type " : " integer "
} ,
" required " : True
} ,
{
" name " : " bool_param " ,
" in " : " query " ,
" description " : " Just a boolean parameter. " ,
" schema " : {
" type " : " boolean "
} ,
" required " : True
}
] ,
}
}
}
}
spec_mock_with_invalid_method = {
" paths " : {
" /endpoint " : {
" brew " : { } # the data is irrelevant as is should be rejected.
}
}
} # type: Dict[str, Any]
spec_mock_using_object = {
" paths " : {
" /endpoint " : {
" get " : {
" description " : " Get some info. " ,
" parameters " : [
{
" name " : " param1 " ,
" in " : " path " ,
" description " : " An object " ,
" schema " : {
" type " : " object "
} ,
" example " : {
" key " : " value "
} ,
" required " : True
}
]
}
}
}
}
spec_mock_using_object_without_example = {
" paths " : {
" /endpoint " : {
" get " : {
" description " : " Get some info. " ,
" parameters " : [
{
" name " : " param1 " ,
" in " : " path " ,
" description " : " An object " ,
" schema " : {
" type " : " object "
} ,
" required " : True
}
]
}
}
}
}
spec_mock_using_array_without_example = {
" paths " : {
" /endpoint " : {
" get " : {
" description " : " Get some info. " ,
" parameters " : [
{
" name " : " param1 " ,
" in " : " path " ,
" description " : " An array " ,
" schema " : {
" type " : " array "
} ,
" required " : True
}
]
}
}
}
}
def test_generate_and_render_curl_example ( self ) - > None :
generated_curl_example = generate_curl_example ( " /get_stream_id " , " GET " )
expected_curl_example = [
" ```curl " ,
" curl -X GET -G localhost:9991/api/v1/get_stream_id \\ " ,
" -u BOT_EMAIL_ADDRESS:BOT_API_KEY \\ " ,
" -d ' stream=Denmark ' " ,
" ``` "
]
self . assertEqual ( generated_curl_example , expected_curl_example )
def test_generate_and_render_curl_example_with_nonexistant_endpoints ( self ) - > None :
with self . assertRaises ( KeyError ) :
generate_curl_example ( " /mark_this_stream_as_read " , " POST " )
with self . assertRaises ( KeyError ) :
generate_curl_example ( " /mark_stream_as_read " , " GET " )
def test_generate_and_render_curl_without_auth ( self ) - > None :
generated_curl_example = generate_curl_example ( " /dev_fetch_api_key " , " POST " )
expected_curl_example = [
" ```curl " ,
" curl -X POST localhost:9991/api/v1/dev_fetch_api_key \\ " ,
" -d ' username=iago@zulip.com ' " ,
" ``` "
]
self . assertEqual ( generated_curl_example , expected_curl_example )
@patch ( " zerver.lib.openapi.OpenAPISpec.spec " )
def test_generate_and_render_curl_with_default_examples ( self , spec_mock : MagicMock ) - > None :
spec_mock . return_value = self . spec_mock_without_examples
generated_curl_example = generate_curl_example ( " /mark_stream_as_read " , " POST " )
expected_curl_example = [
" ```curl " ,
" curl -X POST localhost:9991/api/v1/mark_stream_as_read \\ " ,
" -d ' stream_id=1 ' \\ " ,
" -d ' bool_param=false ' " ,
" ``` "
]
self . assertEqual ( generated_curl_example , expected_curl_example )
@patch ( " zerver.lib.openapi.OpenAPISpec.spec " )
def test_generate_and_render_curl_with_invalid_method ( self , spec_mock : MagicMock ) - > None :
spec_mock . return_value = self . spec_mock_with_invalid_method
with self . assertRaises ( ValueError ) :
generate_curl_example ( " /endpoint " , " BREW " ) # see: HTCPCP
def test_generate_and_render_curl_with_array_example ( self ) - > None :
generated_curl_example = generate_curl_example ( " /messages " , " GET " )
expected_curl_example = [
' ```curl ' ,
' curl -X GET -G localhost:9991/api/v1/messages \\ ' ,
' -u BOT_EMAIL_ADDRESS:BOT_API_KEY \\ ' ,
" -d ' anchor=42 ' \\ " ,
" -d ' use_first_unread_anchor=true ' \\ " ,
" -d ' num_before=4 ' \\ " ,
" -d ' num_after=8 ' \\ " ,
2019-08-02 14:47:18 +02:00
' --data-urlencode narrow= \' [ { " operand " : " Denmark " , " operator " : " stream " }] \' \\ ' ,
2019-07-29 15:46:48 +02:00
" -d ' client_gravatar=true ' \\ " ,
" -d ' apply_markdown=false ' " ,
' ``` '
]
self . assertEqual ( generated_curl_example , expected_curl_example )
@patch ( " zerver.lib.openapi.OpenAPISpec.spec " )
def test_generate_and_render_curl_with_object ( self , spec_mock : MagicMock ) - > None :
spec_mock . return_value = self . spec_mock_using_object
generated_curl_example = generate_curl_example ( " /endpoint " , " GET " )
expected_curl_example = [
' ```curl ' ,
' curl -X GET -G localhost:9991/api/v1/endpoint \\ ' ,
' --data-urlencode param1= \' { " key " : " value " } \' ' ,
' ``` '
]
self . assertEqual ( generated_curl_example , expected_curl_example )
@patch ( " zerver.lib.openapi.OpenAPISpec.spec " )
def test_generate_and_render_curl_with_object_without_example ( self , spec_mock : MagicMock ) - > None :
spec_mock . return_value = self . spec_mock_using_object_without_example
with self . assertRaises ( ValueError ) :
generate_curl_example ( " /endpoint " , " GET " )
@patch ( " zerver.lib.openapi.OpenAPISpec.spec " )
def test_generate_and_render_curl_with_array_without_example ( self , spec_mock : MagicMock ) - > None :
spec_mock . return_value = self . spec_mock_using_array_without_example
with self . assertRaises ( ValueError ) :
generate_curl_example ( " /endpoint " , " GET " )
def test_generate_and_render_curl_wrapper ( self ) - > None :
generated_curl_example = render_curl_example ( " /get_stream_id:GET:email:key:chat.zulip.org/api " )
expected_curl_example = [
" ```curl " ,
" curl -X GET -G chat.zulip.org/api/v1/get_stream_id \\ " ,
" -u email:key \\ " ,
" -d ' stream=Denmark ' " ,
" ``` "
]
self . assertEqual ( generated_curl_example , expected_curl_example )
2019-08-04 08:14:08 +02:00
def test_generate_and_render_curl_example_with_excludes ( self ) - > None :
generated_curl_example = generate_curl_example ( " /messages " , " GET " , exclude = [ " client_gravatar " , " apply_markdown " ] )
expected_curl_example = [
' ```curl ' ,
' curl -X GET -G localhost:9991/api/v1/messages \\ ' ,
' -u BOT_EMAIL_ADDRESS:BOT_API_KEY \\ ' ,
" -d ' anchor=42 ' \\ " ,
" -d ' use_first_unread_anchor=true ' \\ " ,
" -d ' num_before=4 ' \\ " ,
" -d ' num_after=8 ' \\ " ,
2019-08-02 14:47:18 +02:00
' --data-urlencode narrow= \' [ { " operand " : " Denmark " , " operator " : " stream " }] \' ' ,
2019-08-04 08:14:08 +02:00
' ``` '
]
self . assertEqual ( generated_curl_example , expected_curl_example )