mirror of
https://github.com/ansible/awx.git
synced 2026-05-06 08:57:35 -02:30
build example Swagger request and response bodies from our API tests
This commit is contained in:
2
Makefile
2
Makefile
@@ -367,7 +367,7 @@ swagger: reports
|
|||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
(set -o pipefail && py.test awx/main/tests/docs --release=$(RELEASE_VERSION) | tee reports/$@.report)
|
(set -o pipefail && py.test awx/conf/tests/functional awx/main/tests/functional/api awx/main/tests/docs --release=$(SWAGGER_RELEASE_VERSION:-$RELEASE_VERSION) | tee reports/$@.report)
|
||||||
|
|
||||||
check: flake8 pep8 # pyflakes pylint
|
check: flake8 pep8 # pyflakes pylint
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Retrieve Information about the current User
|
||||||
|
|
||||||
Make a GET request to retrieve user information about the current user.
|
Make a GET request to retrieve user information about the current user.
|
||||||
|
|
||||||
One result should be returned containing the following fields:
|
One result should be returned containing the following fields:
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
import os
|
import os
|
||||||
|
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 coreapi.compat import force_bytes
|
from coreapi.compat import force_bytes
|
||||||
from django.conf import settings
|
|
||||||
from openapi_codec.encode import generate_swagger_object
|
from openapi_codec.encode import generate_swagger_object
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -19,6 +24,13 @@ config_file = os.sep.join([config_dest, 'config.yml'])
|
|||||||
description_file = os.sep.join([config_dest, 'description.md'])
|
description_file = os.sep.join([config_dest, 'description.md'])
|
||||||
|
|
||||||
|
|
||||||
|
class i18nEncoder(DjangoJSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, Promise):
|
||||||
|
return force_text(obj)
|
||||||
|
return super(i18nEncoder, self).default(obj)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestSwaggerGeneration():
|
class TestSwaggerGeneration():
|
||||||
"""
|
"""
|
||||||
@@ -45,58 +57,58 @@ class TestSwaggerGeneration():
|
|||||||
if response.has_header('X-Deprecated-Paths'):
|
if response.has_header('X-Deprecated-Paths'):
|
||||||
data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths'])
|
data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths'])
|
||||||
data.update(response.accepted_renderer.get_customizations() or {})
|
data.update(response.accepted_renderer.get_customizations() or {})
|
||||||
|
|
||||||
|
data['host'] = None
|
||||||
|
data['schemes'] = ['https']
|
||||||
|
data['consumes'] = ['application/json']
|
||||||
|
|
||||||
|
# Inject a top-level description into the OpenAPI document
|
||||||
|
if os.path.exists(description_file):
|
||||||
|
with open(description_file, 'r') as f:
|
||||||
|
data['info']['description'] = f.read()
|
||||||
|
|
||||||
|
# Write tags in the order we want them sorted
|
||||||
|
if os.path.exists(config_file):
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
config = yaml.load(f.read())
|
||||||
|
for category in config.get('categories', []):
|
||||||
|
tag = {'name': category['name']}
|
||||||
|
if 'description' in category:
|
||||||
|
tag['description'] = category['description']
|
||||||
|
data.setdefault('tags', []).append(tag)
|
||||||
|
|
||||||
|
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
|
self.__class__.JSON = data
|
||||||
|
|
||||||
def test_transform_metadata(self, release):
|
def test_sanity(self, release):
|
||||||
"""
|
|
||||||
This test takes the JSON output from the swagger endpoint and applies
|
|
||||||
various transformations to it.
|
|
||||||
"""
|
|
||||||
JSON = self.__class__.JSON
|
JSON = self.__class__.JSON
|
||||||
JSON['info']['version'] = release
|
JSON['info']['version'] = release
|
||||||
JSON['host'] = None
|
|
||||||
JSON['schemes'] = ['https']
|
|
||||||
JSON['produces'] = ['application/json']
|
|
||||||
JSON['consumes'] = ['application/json']
|
|
||||||
|
|
||||||
# Inject a top-level description into the OpenAPI document
|
|
||||||
if os.path.exists(description_file):
|
|
||||||
with open(description_file, 'r') as f:
|
|
||||||
JSON['info']['description'] = f.read()
|
|
||||||
|
|
||||||
# Write tags in the order we want them sorted
|
|
||||||
if os.path.exists(config_file):
|
|
||||||
with open(config_file, 'r') as f:
|
|
||||||
config = yaml.load(f.read())
|
|
||||||
for category in config.get('categories', []):
|
|
||||||
tag = {'name': category['name']}
|
|
||||||
if 'description' in category:
|
|
||||||
tag['description'] = category['description']
|
|
||||||
JSON.setdefault('tags', []).append(tag)
|
|
||||||
|
|
||||||
revised_paths = {}
|
|
||||||
deprecated_paths = JSON.pop('deprecated_paths', [])
|
|
||||||
for path, node in JSON['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)
|
|
||||||
JSON['paths'] = revised_paths
|
|
||||||
|
|
||||||
# Make some basic assertions about the rendered JSON so we can
|
# Make some basic assertions about the rendered JSON so we can
|
||||||
# be sure it doesn't break across DRF upgrades and view/serializer
|
# be sure it doesn't break across DRF upgrades and view/serializer
|
||||||
# changes.
|
# changes.
|
||||||
assert len(JSON['tags'])
|
assert len(JSON['tags'])
|
||||||
assert JSON['info']['version'] == release
|
|
||||||
assert len(JSON['paths'])
|
assert len(JSON['paths'])
|
||||||
|
|
||||||
# The number of API endpoints changes over time, but let's just check
|
# The number of API endpoints changes over time, but let's just check
|
||||||
@@ -119,7 +131,55 @@ class TestSwaggerGeneration():
|
|||||||
# Test deprecated paths
|
# Test deprecated paths
|
||||||
assert paths['/api/v2/jobs/{id}/extra_credentials/']['get']['deprecated'] is True
|
assert paths['/api/v2/jobs/{id}/extra_credentials/']['get']['deprecated'] is True
|
||||||
|
|
||||||
|
@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):
|
||||||
|
for pattern, node in TestSwaggerGeneration.JSON['paths'].items():
|
||||||
|
pattern = pattern.replace('{id}', '[0-9]+')
|
||||||
|
pattern = pattern.replace('{category_slug}', '[a-zA-Z0-9\-]+')
|
||||||
|
for path, result in swagger_autogen.items():
|
||||||
|
if re.match('^{}$'.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)
|
||||||
|
node[method].setdefault('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)
|
||||||
|
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
|
@classmethod
|
||||||
def teardown_class(cls):
|
def teardown_class(cls):
|
||||||
with open('swagger.json', 'w') as f:
|
with open('swagger.json', 'w') as f:
|
||||||
f.write(force_bytes(json.dumps(cls.JSON)))
|
f.write(force_bytes(
|
||||||
|
json.dumps(cls.JSON, cls=i18nEncoder, indent=5)
|
||||||
|
))
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ from awx.main.models.notifications import (
|
|||||||
from awx.main.models.workflow import WorkflowJobTemplate
|
from awx.main.models.workflow import WorkflowJobTemplate
|
||||||
from awx.main.models.ad_hoc_commands import AdHocCommand
|
from awx.main.models.ad_hoc_commands import AdHocCommand
|
||||||
|
|
||||||
|
__SWAGGER_REQUESTS__ = {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def swagger_autogen(requests=__SWAGGER_REQUESTS__):
|
||||||
|
return requests
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def clear_cache():
|
def clear_cache():
|
||||||
@@ -546,6 +553,9 @@ def _request(verb):
|
|||||||
assert response.status_code == expect
|
assert response.status_code == expect
|
||||||
if hasattr(response, 'render'):
|
if hasattr(response, 'render'):
|
||||||
response.render()
|
response.render()
|
||||||
|
__SWAGGER_REQUESTS__.setdefault(request.path, {})[
|
||||||
|
(request.method.lower(), response.status_code)
|
||||||
|
] = (response.get('Content-Type', None), response.content, kwargs.get('data'))
|
||||||
return response
|
return response
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user