2018-05-15 19:28:42 +02:00
|
|
|
# Set of helper functions to manipulate the OpenAPI files that define our REST
|
|
|
|
# API's specification.
|
|
|
|
import os
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
from yamole import YamoleParser
|
|
|
|
|
|
|
|
OPENAPI_SPEC_PATH = os.path.abspath(os.path.join(
|
|
|
|
os.path.dirname(__file__),
|
|
|
|
'../openapi/zulip.yaml'))
|
|
|
|
|
|
|
|
with open(OPENAPI_SPEC_PATH) as file:
|
|
|
|
yaml_parser = YamoleParser(file)
|
|
|
|
|
|
|
|
OPENAPI_SPEC = yaml_parser.data
|
|
|
|
|
2018-06-20 19:31:24 +02:00
|
|
|
# A list of exceptions we allow when running validate_against_openapi_schema.
|
|
|
|
# The validator will ignore these keys when they appear in the "content"
|
|
|
|
# passed.
|
|
|
|
EXCLUDE_PROPERTIES = {
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-05-31 19:41:17 +02:00
|
|
|
class SchemaError(Exception):
|
|
|
|
pass
|
2018-05-15 19:28:42 +02:00
|
|
|
|
|
|
|
def get_openapi_fixture(endpoint: str, method: str,
|
|
|
|
response: Optional[str]='200') -> Dict[str, Any]:
|
2018-05-31 19:41:17 +02:00
|
|
|
"""Fetch a fixture from the full spec object.
|
|
|
|
"""
|
2018-05-15 19:28:42 +02:00
|
|
|
return (OPENAPI_SPEC['paths'][endpoint][method.lower()]['responses']
|
|
|
|
[response]['content']['application/json']['schema']
|
|
|
|
['example'])
|
|
|
|
|
|
|
|
def get_openapi_parameters(endpoint: str,
|
|
|
|
method: str) -> List[Dict[str, Any]]:
|
2018-05-31 19:41:17 +02:00
|
|
|
return (OPENAPI_SPEC['paths'][endpoint][method.lower()]['parameters'])
|
|
|
|
|
|
|
|
def validate_against_openapi_schema(content: Dict[str, Any], endpoint: str,
|
2018-06-18 16:32:30 +02:00
|
|
|
method: str, response: str) -> None:
|
2018-05-31 19:41:17 +02:00
|
|
|
"""Compare a "content" dict with the defined schema for a specific method
|
|
|
|
in an endpoint.
|
|
|
|
"""
|
|
|
|
schema = (OPENAPI_SPEC['paths'][endpoint][method.lower()]['responses']
|
2018-06-18 16:32:30 +02:00
|
|
|
[response]['content']['application/json']['schema'])
|
2018-05-31 19:41:17 +02:00
|
|
|
|
2018-06-20 19:31:24 +02:00
|
|
|
exclusion_list = (EXCLUDE_PROPERTIES.get(endpoint, {}).get(method, {})
|
|
|
|
.get(response, []))
|
|
|
|
|
2018-05-31 19:41:17 +02:00
|
|
|
for key, value in content.items():
|
2018-06-20 19:31:24 +02:00
|
|
|
# Ignore in the validation the keys in EXCLUDE_PROPERTIES
|
|
|
|
if key in exclusion_list:
|
|
|
|
continue
|
|
|
|
|
2018-05-31 19:41:17 +02:00
|
|
|
# Check that the key is defined in the schema
|
|
|
|
if key not in schema['properties']:
|
2018-06-18 16:47:20 +02:00
|
|
|
raise SchemaError('Extraneous key "{}" in the response\'s '
|
2018-05-31 19:41:17 +02:00
|
|
|
'content'.format(key))
|
|
|
|
|
|
|
|
# Check that the types match
|
|
|
|
expected_type = to_python_type(schema['properties'][key]['type'])
|
|
|
|
actual_type = type(value)
|
|
|
|
if expected_type is not actual_type:
|
|
|
|
raise SchemaError('Expected type {} for key "{}", but actually '
|
|
|
|
'got {}'.format(expected_type, key, actual_type))
|
|
|
|
|
|
|
|
# Check that at least all the required keys are present
|
|
|
|
for req_key in schema['required']:
|
|
|
|
if req_key not in content.keys():
|
|
|
|
raise SchemaError('Expected to find the "{}" required key')
|
|
|
|
|
|
|
|
def to_python_type(py_type: str) -> type:
|
|
|
|
"""Transform an OpenAPI-like type to a Pyton one.
|
|
|
|
https://swagger.io/docs/specification/data-models/data-types
|
|
|
|
"""
|
|
|
|
TYPES = {
|
|
|
|
'string': str,
|
|
|
|
'number': float,
|
|
|
|
'integer': int,
|
|
|
|
'boolean': bool,
|
|
|
|
'array': list,
|
|
|
|
'object': dict
|
|
|
|
}
|
|
|
|
|
|
|
|
return TYPES[py_type]
|