docker.images/ansible.awx/awx-17.1.0/awx/main/tests/docs/test_swagger_generation.py

184 lines
7.9 KiB
Python

import datetime
import json
import re
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.functional import Promise
from django.utils.encoding import force_text
from openapi_codec.encode import generate_swagger_object
import pytest
from awx.api.versioning import drf_reverse
class i18nEncoder(DjangoJSONEncoder):
def default(self, obj):
if isinstance(obj, Promise):
return force_text(obj)
if type(obj) == bytes:
return force_text(obj)
return super(i18nEncoder, self).default(obj)
@pytest.mark.django_db
class TestSwaggerGeneration():
"""
This class is used to generate a Swagger/OpenAPI document for the awx
API. A _prepare fixture generates a JSON blob containing OpenAPI data,
individual tests have the ability modify the payload.
Finally, the JSON content is written to a file, `swagger.json`, in the
current working directory.
$ py.test test_swagger_generation.py --version 3.3.0
To customize the `info.description` in the generated OpenAPI document,
modify the text in `awx.api.templates.swagger.description.md`
"""
JSON = {}
@pytest.fixture(autouse=True, scope='function')
def _prepare(self, get, admin):
if not self.__class__.JSON:
url = drf_reverse('api:swagger_view') + '?format=openapi'
response = get(url, user=admin)
data = generate_swagger_object(response.data)
if response.has_header('X-Deprecated-Paths'):
data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths'])
data.update(response.accepted_renderer.get_customizations() or {})
data['host'] = None
data['schemes'] = ['https']
data['consumes'] = ['application/json']
revised_paths = {}
deprecated_paths = data.pop('deprecated_paths', [])
for path, node in data['paths'].items():
# change {version} in paths to the actual default API version (e.g., v2)
revised_paths[path.replace(
'{version}',
settings.REST_FRAMEWORK['DEFAULT_VERSION']
)] = node
for method in node:
if path in deprecated_paths:
node[method]['deprecated'] = True
if 'description' in node[method]:
# Pop off the first line and use that as the summary
lines = node[method]['description'].splitlines()
node[method]['summary'] = lines.pop(0).strip('#:')
node[method]['description'] = '\n'.join(lines)
# remove the required `version` parameter
for param in node[method].get('parameters'):
if param['in'] == 'path' and param['name'] == 'version':
node[method]['parameters'].remove(param)
data['paths'] = revised_paths
self.__class__.JSON = data
def test_sanity(self, release, request):
JSON = self.__class__.JSON
JSON['info']['version'] = release
if not request.config.getoption('--genschema'):
JSON['modified'] = datetime.datetime.utcnow().isoformat()
# Make some basic assertions about the rendered JSON so we can
# be sure it doesn't break across DRF upgrades and view/serializer
# changes.
assert len(JSON['paths'])
# The number of API endpoints changes over time, but let's just check
# for a reasonable number here; if this test starts failing, raise/lower the bounds
paths = JSON['paths']
assert 250 < len(paths) < 350
assert list(paths['/api/'].keys()) == ['get']
assert list(paths['/api/v2/'].keys()) == ['get']
assert list(sorted(
paths['/api/v2/credentials/'].keys()
)) == ['get', 'post']
assert list(sorted(
paths['/api/v2/credentials/{id}/'].keys()
)) == ['delete', 'get', 'patch', 'put']
assert list(paths['/api/v2/settings/'].keys()) == ['get']
assert list(paths['/api/v2/settings/{category_slug}/'].keys()) == [
'get', 'put', 'patch', 'delete'
]
@pytest.mark.parametrize('path', [
'/api/',
'/api/v2/',
'/api/v2/ping/',
'/api/v2/config/',
])
def test_basic_paths(self, path, get, admin):
# hit a couple important endpoints so we always have example data
get(path, user=admin, expect=200)
def test_autogen_response_examples(self, swagger_autogen, request):
for pattern, node in TestSwaggerGeneration.JSON['paths'].items():
pattern = pattern.replace('{id}', '[0-9]+')
pattern = pattern.replace(r'{category_slug}', r'[a-zA-Z0-9\-]+')
for path, result in swagger_autogen.items():
if re.match(r'^{}$'.format(pattern), path):
for key, value in result.items():
method, status_code = key
content_type, resp, request_data = value
if method in node:
status_code = str(status_code)
if content_type:
produces = node[method].setdefault('produces', [])
if content_type not in produces:
produces.append(content_type)
if request_data and status_code.startswith('2'):
# DRF builds a schema based on the serializer
# fields. This is _pretty good_, but if we
# have _actual_ JSON examples, those are even
# better and we should use them instead
for param in node[method].get('parameters'):
if param['in'] == 'body':
node[method]['parameters'].remove(param)
if request.config.getoption("--genschema"):
pytest.skip("In schema generator skipping swagger generator", allow_module_level=True)
else:
node[method].setdefault('parameters', []).append({
'name': 'data',
'in': 'body',
'schema': {'example': request_data},
})
# Build response examples
if resp:
if content_type.startswith('text/html'):
continue
if content_type == 'application/json':
resp = json.loads(resp)
node[method]['responses'].setdefault(status_code, {}).setdefault(
'examples', {}
)[content_type] = resp
@classmethod
def teardown_class(cls):
with open('swagger.json', 'w') as f:
data = json.dumps(cls.JSON, cls=i18nEncoder, indent=2, sort_keys=True)
# replace ISO dates w/ the same value so we don't generate
# needless diffs
data = re.sub(
r'[0-9]{4}-[0-9]{2}-[0-9]{2}(T|\s)[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]+(Z|\+[0-9]{2}:[0-9]{2})?',
r'2018-02-01T08:00:00.000000Z',
data
)
data = re.sub(
r'''(\s+"client_id": ")([a-zA-Z0-9]{40})("\,\s*)''',
r'\1xxxx\3',
data
)
data = re.sub(
r'"action_node": "[^"]+"',
'"action_node": "awx"',
data
)
f.write(data)