Skip to content

Commit

Permalink
Merge pull request #7 from zalando/6-tests
Browse files Browse the repository at this point in the history
Add unit tests and CI
  • Loading branch information
hjacobs committed Jun 14, 2015
2 parents 9c62e1a + 84c6042 commit 200b741
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 20 deletions.
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
language: python
python:
- "3.4"
install:
- pip install -e .
- pip install coveralls
- pip install flake8
script:
- python setup.py test
- python setup.py flake8
after_success:
- coveralls
31 changes: 17 additions & 14 deletions connexion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@
logger = logging.getLogger('connexion.api')


def swagger_ui_index(api_url):
return flask.render_template('index.html', api_url=api_url)


def swagger_ui_static(filename: str):
return flask.send_from_directory(str(SWAGGER_UI_PATH), filename)


class Api:
"""
Single API that corresponds to a flask blueprint
Expand All @@ -51,7 +43,7 @@ def __init__(self, swagger_yaml_path: pathlib.Path, base_url: str=None, argument
with swagger_yaml_path.open() as swagger_yaml:
swagger_template = swagger_yaml.read()
swagger_string = jinja2.Template(swagger_template).render(**arguments)
self.specification = yaml.load(swagger_string)
self.specification = yaml.load(swagger_string) # type: dict

# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields
# TODO Validate yaml
Expand All @@ -68,6 +60,7 @@ def __init__(self, swagger_yaml_path: pathlib.Path, base_url: str=None, argument

self.security = self.specification.get('security', [None]).pop()
self.security_definitions = self.specification.get('securityDefinitions', dict())
logger.debug('Security Definitions: %s', self.security_definitions)

# Create blueprint and enpoints
self.blueprint = self.create_blueprint()
Expand All @@ -89,9 +82,12 @@ def _get_produces_decorator(self, operation: dict) -> types.FunctionType:
A list of MIME types the operation can produce. This overrides the produces definition at the Swagger Object.
An empty value MAY be used to clear the global definition.
"""

produces = operation['produces'] if 'produces' in operation else self.produces
logger.debug('... Produces: %s', produces)

if produces == ['application/json']: # endpoint will return json
logger.debug('... Produces json')
return jsonify

# If we don't know how to handle the `produces` type then we will not decorate the function
Expand Down Expand Up @@ -119,6 +115,7 @@ def _get_security_decorator(self, operation: dict) -> types.FunctionType:
The name used for each property **MUST** correspond to a security scheme declared in the Security Definitions.
"""
security = operation['security'].pop() if 'security' in operation else self.security
logger.debug('... Security: %s', security)
if security:
if len(security) > 1:
logger.warning("... More than security requirement defined. **IGNORING SECURITY REQUIREMENTS**")
Expand Down Expand Up @@ -162,10 +159,10 @@ def add_operation(self, method: str, path: str, operation: dict):
security_decorator = self._get_security_decorator(operation)

if produces_decorator:
logger.debug('... Adding produces decorator')
logger.debug('... Adding produces decorator (%r)', produces_decorator)
function = produces_decorator(function)
if security_decorator:
logger.debug('... Adding security decorator')
logger.debug('... Adding security decorator (%r)', security_decorator)
function = security_decorator(function)

self.blueprint.add_url_rule(path, endpoint_name, function, methods=[method])
Expand Down Expand Up @@ -196,14 +193,20 @@ def add_swagger_ui(self):
"""
logger.debug('Adding swagger-ui: %s/ui/', self.base_url)
static_endpoint_name = "{name}_swagger_ui_static".format(name=self.blueprint.name)
self.blueprint.add_url_rule('/ui/<path:filename>', static_endpoint_name, swagger_ui_static)
self.blueprint.add_url_rule('/ui/<path:filename>', static_endpoint_name, self.swagger_ui_static)
index_endpoint_name = "{name}_swagger_ui_index".format(name=self.blueprint.name)
partial_index = functools.partial(swagger_ui_index, self.base_url)
self.blueprint.add_url_rule('/ui/', index_endpoint_name, partial_index)
self.blueprint.add_url_rule('/ui/', index_endpoint_name, self.swagger_ui_index)

def create_blueprint(self, base_url: str=None) -> flask.Blueprint:
base_url = base_url or self.base_url
logger.debug('Creating API blueprint: %s', base_url)
endpoint = utils.flaskify_endpoint(base_url)
blueprint = flask.Blueprint(endpoint, __name__, url_prefix=base_url, template_folder=str(SWAGGER_UI_PATH))
return blueprint

def swagger_ui_index(self):
return flask.render_template('index.html', api_url=self.base_url)

@staticmethod
def swagger_ui_static(filename: str):
return flask.send_from_directory(str(SWAGGER_UI_PATH), filename)
5 changes: 3 additions & 2 deletions connexion/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
class App:

def __init__(self, import_name: str, port: int=5000, specification_dir: pathlib.Path='', server: str=None,
arguments: dict=None):
arguments: dict=None, debug: bool=False):
"""
:param import_name: the name of the application package
:param port: port to listen to
Expand Down Expand Up @@ -58,6 +58,7 @@ def __init__(self, import_name: str, port: int=5000, specification_dir: pathlib.

self.port = port
self.server = server
self.debug = debug
self.arguments = arguments or {}

def add_api(self, swagger_file: pathlib.Path, base_path: str=None, arguments: dict=None):
Expand Down Expand Up @@ -86,7 +87,7 @@ def common_error_handler(e: werkzeug.exceptions.HTTPException):
def run(self):

if self.server is None:
self.app.run('0.0.0.0', port=self.port)
self.app.run('0.0.0.0', port=self.port, debug=self.debug)
elif self.server == 'tornado':
wsgi_container = tornado.wsgi.WSGIContainer(self.app)
http_server = tornado.httpserver.HTTPServer(wsgi_container)
Expand Down
2 changes: 1 addition & 1 deletion connexion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def flaskify_path(swagger_path: str) -> str:
return swagger_path.translate(translation_table)


def get_function_from_name(operation_id: str):
def get_function_from_name(operation_id: str) -> str:
module_name, function_name = operation_id.rsplit('.', maxsplit=1)
module = importlib.import_module(module_name)
function = getattr(module, function_name)
Expand Down
1 change: 0 additions & 1 deletion connexion/version.py

This file was deleted.

2 changes: 1 addition & 1 deletion examples/helloworld/swagger/helloworld-api.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
swagger: "2.0"

info:
title: {{title}}
title: "{{title}}"
version: "1.0"

basePath: /v1.0
Expand Down
25 changes: 24 additions & 1 deletion setup.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand

version = '0.4.1'


class PyTest(TestCommand):

def initialize_options(self):
TestCommand.initialize_options(self)
self.cov = None
self.pytest_args = ['--cov', 'connexion', '--cov-report', 'term-missing']

def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True

from connexion.version import version
def run_tests(self):
import pytest
errno = pytest.main(self.pytest_args)
sys.exit(errno)


setup(
Expand All @@ -16,6 +37,8 @@
url='https://github.com/zalando/connexion',
license='Apache License Version 2.0',
install_requires=['flask', 'PyYAML', 'tornado', 'requests'],
tests_require=['pytest-cov', 'pytest'],
cmdclass={'test': PyTest},
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 3.4',
Expand Down
1 change: 1 addition & 0 deletions tests/fakeapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__author__ = 'jmcs'
75 changes: 75 additions & 0 deletions tests/fakeapi/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
swagger: "2.0"

info:
title: "{{title}}"
version: "1.0"

basePath: /v1.0

securityDefinitions:
oauth:
type: oauth2
flow: password
tokenUrl: https://ouath.example/token
x-tokenInfoUrl: https://ouath.example/token_info
scopes:
myscope: can do stuff

paths:
/greeting/{name}:
post:
summary: Generate greeting
description: Generates a greeting message.
operationId: fakeapi.hello.post_greeting
produces:
- application/json
responses:
200:
description: greeting response
schema:
type: string
parameters:
- name: name
in: path
description: Name of the person to greet.
required: true
type: string
/bye/{name}:
get:
summary: Generate goodbye
description: Generates a goobye message.
operationId: fakeapi.hello.get_bye
produces:
- text/plain
responses:
200:
description: goodbye response
schema:
type: string
parameters:
- name: name
in: path
description: Name of the person to say bye.
required: true
type: string
/byesecure/{name}:
get:
summary: Generate goodbye
description: Generates a goobye message.
operationId: fakeapi.hello.get_bye_secure
security:
- oauth:
- myscope
produces:
- text/plain
responses:
200:
description: goodbye response
schema:
type: string
parameters:
- name: name
in: path
description: Name of the person to say bye.
required: true
type: string
12 changes: 12 additions & 0 deletions tests/fakeapi/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env python3


def post_greeting(name: str) -> dict:
data = {'greeting': 'Hello {name}'.format(name=name)}
return data

def get_bye(name: str) -> dict:
return 'Goodbye {name}'.format(name=name)

def get_bye_secure(name: str) -> dict:
return 'Goodbye {name} (Secure)'.format(name=name)
25 changes: 25 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

import pathlib

from connexion.api import Api

TEST_FOLDER = pathlib.Path(__file__).parent


def test_api():
api = Api(TEST_FOLDER / "fakeapi/api.yaml", "/api/v1.0", {})
assert api.blueprint.name == '/api/v1_0'
assert api.blueprint.url_prefix == '/api/v1.0'
# TODO test base_url in spec

api2 = Api(TEST_FOLDER / "fakeapi/api.yaml")
assert api2.blueprint.name == '/v1_0'
assert api2.blueprint.url_prefix == '/v1.0'


def test_template():
api1 = Api(TEST_FOLDER / "fakeapi/api.yaml", "/api/v1.0", {'title': 'test'})
assert api1.specification['info']['title'] == 'test'

api2 = Api(TEST_FOLDER / "fakeapi/api.yaml", "/api/v1.0", {'title': 'other test'})
assert api2.specification['info']['title'] == 'other test'
Loading

0 comments on commit 200b741

Please sign in to comment.