From 539d5050adc3b68c1bca6be4add66dcbe5cdce91 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna Surapureddi Date: Fri, 11 Aug 2017 22:23:10 +0530 Subject: [PATCH] Initial testing framework Change-Id: I6dffda68221faae3960230e5e3f9856fc0caba47 --- requirements.txt | 3 + setup.py | 13 +- shipyard_airflow/__init__.py | 2 +- shipyard_airflow/airflow_client.py | 17 ++ .../control/airflow_connections.py | 103 ++----- shipyard_airflow/control/api.py | 41 ++- shipyard_airflow/control/base.py | 37 ++- shipyard_airflow/errors.py | 52 ++++ shipyard_airflow/setup.py | 13 +- test-requirements.txt | 6 + tests/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/control/__init__.py | 0 .../unit/control/test_airflow_connections.py | 267 ++++++++++++++++++ tox.ini | 17 ++ 15 files changed, 462 insertions(+), 109 deletions(-) create mode 100644 requirements.txt create mode 100644 shipyard_airflow/airflow_client.py create mode 100644 shipyard_airflow/errors.py create mode 100644 test-requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/control/__init__.py create mode 100644 tests/unit/control/test_airflow_connections.py create mode 100644 tox.ini diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..60cf1f8e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +falcon==1.2.0 +python-dateutil==2.6.1 +requests==2.18.3 \ No newline at end of file diff --git a/setup.py b/setup.py index 85e81e05..630bbde7 100644 --- a/setup.py +++ b/setup.py @@ -24,10 +24,9 @@ setup(name='shipyard_airflow', packages=['shipyard_airflow', 'shipyard_airflow.control'], install_requires=[ - 'falcon', - 'requests', - 'configparser', - 'uwsgi>1.4' - ] - ) - + 'falcon', + 'requests', + 'configparser', + 'uwsgi>1.4', + 'python-dateutil' + ]) diff --git a/shipyard_airflow/__init__.py b/shipyard_airflow/__init__.py index 2a385a45..f10bbbf6 100644 --- a/shipyard_airflow/__init__.py +++ b/shipyard_airflow/__init__.py @@ -10,4 +10,4 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. diff --git a/shipyard_airflow/airflow_client.py b/shipyard_airflow/airflow_client.py new file mode 100644 index 00000000..bb1feabf --- /dev/null +++ b/shipyard_airflow/airflow_client.py @@ -0,0 +1,17 @@ +import requests + +from shipyard_airflow.errors import AirflowError + + +class AirflowClient(object): + def __init__(self, url): + self.url = url + + def get(self): + response = requests.get(self.url).json() + + # This gives us more freedom to handle the responses from airflow + if response["output"]["stderr"]: + raise AirflowError(response["output"]["stderr"]) + else: + return response["output"]["stdout"] diff --git a/shipyard_airflow/control/airflow_connections.py b/shipyard_airflow/control/airflow_connections.py index 6d138132..4dc633d4 100644 --- a/shipyard_airflow/control/airflow_connections.py +++ b/shipyard_airflow/control/airflow_connections.py @@ -11,12 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import falcon -import json -import requests -import urllib.parse +from urllib.parse import urlunsplit +from falcon import HTTPInvalidParam from .base import BaseResource +from shipyard_airflow.airflow_client import AirflowClient # We need to be able to add/delete connections so that we can create/delete # connection endpoints that Airflow needs to connect to @@ -25,35 +24,21 @@ class AirflowAddConnectionResource(BaseResource): authorized_roles = ['user'] def on_get(self, req, resp, action, conn_id, protocol, host, port): - # Retrieve URL web_server_url = self.retrieve_config('base', 'web_server') + if action != 'add': + raise HTTPInvalidParam( + 'Invalid Paremeters for Adding Airflow Connection', 'action') - if 'Error' in web_server_url: - resp.status = falcon.HTTP_500 - raise falcon.HTTPInternalServerError("Internal Server Error", "Missing Configuration File") - else: - if action == 'add': - # Concatenate to form the connection URL - netloc = ''.join([host, ':', port]) - url = (protocol, netloc, '','','') - conn_uri = urlparse.urlunsplit(url) + # Concatenate to form the connection URL + netloc = ''.join([host, ':', port]) + url = (protocol, netloc, '', '', '') + conn_uri = urlunsplit(url) + # Form the request URL towards Airflow + req_url = ('{}/admin/rest_api/api?api=connections&add=true&conn_id' + '={}&conn_uri={}'.format(web_server_url, conn_id, conn_uri)) - # Form the request URL towards Airflow - req_url = '{}/admin/rest_api/api?api=connections&add=true&conn_id={}&conn_uri={}'.format(web_server_url, conn_id, conn_uri) - else: - self.return_error(resp, falcon.HTTP_400, 'Invalid Paremeters for Adding Airflow Connection') - return - - response = requests.get(req_url).json() - - # Return output - if response["output"]["stderr"]: - resp.status = falcon.HTTP_400 - resp.body = response["output"]["stderr"] - return - else: - resp.status = falcon.HTTP_200 - resp.body = response["output"]["stdout"] + airflow_client = AirflowClient(req_url) + self.on_success(resp, airflow_client.get()) # Delete a particular connection endpoint @@ -64,28 +49,15 @@ class AirflowDeleteConnectionResource(BaseResource): def on_get(self, req, resp, action, conn_id): # Retrieve URL web_server_url = self.retrieve_config('base', 'web_server') + if action != 'delete': + raise HTTPInvalidParam( + 'Invalid Paremeters for Deleting Airflow Connection', 'action') - if 'Error' in web_server_url: - resp.status = falcon.HTTP_500 - raise falcon.HTTPInternalServerError("Internal Server Error", "Missing Configuration File") - else: - if action == 'delete': - # Form the request URL towards Airflow - req_url = '{}/admin/rest_api/api?api=connections&delete=true&conn_id={}'.format(web_server_url, conn_id) - else: - self.return_error(resp, falcon.HTTP_400, 'Invalid Paremeters for Deleting Airflow Connection') - return - - response = requests.get(req_url).json() - - # Return output - if response["output"]["stderr"]: - resp.status = falcon.HTTP_400 - resp.body = response["output"]["stderr"] - return - else: - resp.status = falcon.HTTP_200 - resp.body = response["output"]["stdout"] + # Form the request URL towards Airflow + req_url = ('{}/admin/rest_api/api?api=connections&delete=true&conn_id' + '={}'.format(web_server_url, conn_id)) + airflow_client = AirflowClient(req_url) + self.on_success(resp, airflow_client.get()) # List all current connection endpoints @@ -94,28 +66,13 @@ class AirflowListConnectionsResource(BaseResource): authorized_roles = ['user'] def on_get(self, req, resp, action): - # Retrieve URL web_server_url = self.retrieve_config('base', 'web_server') + if action != 'list': + raise HTTPInvalidParam( + 'Invalid Paremeters for listing Airflow Connections', 'action') - if 'Error' in web_server_url: - resp.status = falcon.HTTP_500 - raise falcon.HTTPInternalServerError("Internal Server Error", "Missing Configuration File") - else: - if action == 'list': - # Form the request URL towards Airflow - req_url = '{}/admin/rest_api/api?api=connections&list=true'.format(web_server_url) - else: - self.return_error(resp, falcon.HTTP_400, 'Invalid Paremeters for listing Airflow Connections') - return - - response = requests.get(req_url).json() - - # Return output - if response["output"]["stderr"]: - resp.status = falcon.HTTP_400 - resp.body = response["output"]["stderr"] - return - else: - resp.status = falcon.HTTP_200 - resp.body = response["output"]["stdout"] + req_url = '{}/admin/rest_api/api?api=connections&list=true'.format( + web_server_url) + airflow_client = AirflowClient(req_url) + self.on_success(resp, airflow_client.get()) diff --git a/shipyard_airflow/control/api.py b/shipyard_airflow/control/api.py index 891b6634..32e035cc 100644 --- a/shipyard_airflow/control/api.py +++ b/shipyard_airflow/control/api.py @@ -29,11 +29,17 @@ from .airflow_connections import AirflowDeleteConnectionResource from .airflow_connections import AirflowListConnectionsResource from .airflow_get_version import GetAirflowVersionResource from .middleware import AuthMiddleware, ContextMiddleware, LoggingMiddleware +from shipyard_airflow.errors import AppError + def start_api(): - - control_api = falcon.API(request_type=ShipyardRequest, - middleware=[AuthMiddleware(), ContextMiddleware(), LoggingMiddleware()]) + middlewares = [ + AuthMiddleware(), + ContextMiddleware(), + LoggingMiddleware(), + ] + control_api = falcon.API( + request_type=ShipyardRequest, middleware=middlewares) control_api.add_route('/versions', VersionsResource()) @@ -45,13 +51,19 @@ def start_api(): ('/dags/{dag_id}/tasks/{task_id}', TaskResource()), ('/dags/{dag_id}/dag_runs', DagRunResource()), ('/list_dags', ListDagsResource()), - ('/task_state/dags/{dag_id}/tasks/{task_id}/execution_date/{execution_date}', GetTaskStatusResource()), - ('/dag_state/dags/{dag_id}/execution_date/{execution_date}', GetDagStateResource()), + ('/task_state/dags/{dag_id}/tasks/{task_id}/execution_date/' + '{execution_date}', GetTaskStatusResource()), + ('/dag_state/dags/{dag_id}/execution_date/{execution_date}', + GetDagStateResource()), ('/list_tasks/dags/{dag_id}', ListTasksResource()), - ('/trigger_dag/dags/{dag_id}/run_id/{run_id}', TriggerDagRunResource()), - ('/trigger_dag/dags/{dag_id}/run_id/{run_id}/poll', TriggerDagRunPollResource()), - ('/connections/{action}/conn_id/{conn_id}/protocol/{protocol}/host/{host}/port/{port}', AirflowAddConnectionResource()), - ('/connections/{action}/conn_id/{conn_id}', AirflowDeleteConnectionResource()), + ('/trigger_dag/dags/{dag_id}/run_id/{run_id}', + TriggerDagRunResource()), + ('/trigger_dag/dags/{dag_id}/run_id/{run_id}/poll', + TriggerDagRunPollResource()), + ('/connections/{action}/conn_id/{conn_id}/protocol/{protocol}' + '/host/{host}/port/{port}', AirflowAddConnectionResource()), + ('/connections/{action}/conn_id/{conn_id}', + AirflowDeleteConnectionResource()), ('/connections/{action}', AirflowListConnectionsResource()), ('/airflow/version', GetAirflowVersionResource()), ] @@ -59,6 +71,7 @@ def start_api(): for path, res in v1_0_routes: control_api.add_route('/api/v1.0' + path, res) + control_api.add_error_handler(AppError, AppError.handle) return control_api class VersionsResource(BaseResource): @@ -66,9 +79,9 @@ class VersionsResource(BaseResource): authorized_roles = ['anyone'] def on_get(self, req, resp): - resp.body = json.dumps({'v1.0': { - 'path': '/api/v1.0', - 'status': 'stable' - }}) + resp.body = json.dumps({ + 'v1.0': { + 'path': '/api/v1.0', + 'status': 'stable' + }}) resp.status = falcon.HTTP_200 - diff --git a/shipyard_airflow/control/base.py b/shipyard_airflow/control/base.py index 9088044e..574695f3 100644 --- a/shipyard_airflow/control/base.py +++ b/shipyard_airflow/control/base.py @@ -11,11 +11,22 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import falcon, falcon.request as request +import falcon +import falcon.request as request import uuid import json import configparser import os +try: + from collections import OrderedDict +except ImportError: + OrderedDict = dict + +from shipyard_airflow.errors import ( + AppError, + ERR_UNKNOWN, +) + class BaseResource(object): @@ -43,17 +54,30 @@ class BaseResource(object): else: return True + def to_json(self, body_dict): + return json.dumps(body_dict) + + def on_success(self, res, message=None): + res.status = falcon.HTTP_200 + response_dict = OrderedDict() + response_dict['type'] = 'success' + response_dict['message'] = message + res.body = self.to_json(response_dict) + # Error Handling def return_error(self, resp, status_code, message="", retry=False): """ - Write a error message body and throw a Falcon exception to trigger an HTTP status + Write a error message body and throw a Falcon exception to trigger + an HTTP status :param resp: Falcon response object to update :param status_code: Falcon status_code constant :param message: Optional error message to include in the body - :param retry: Optional flag whether client should retry the operation. Can ignore if we rely solely on 4XX vs 5xx status codes + :param retry: Optional flag whether client should retry the operation. + Can ignore if we rely solely on 4XX vs 5xx status codes """ - resp.body = json.dumps({'type': 'error', 'message': message, 'retry': retry}) + resp.body = self.to_json( + {'type': 'error', 'message': message, 'retry': retry}) resp.content_type = 'application/json' resp.status = status_code @@ -62,7 +86,7 @@ class BaseResource(object): # Shipyard config will be located at /etc/shipyard/shipyard.conf path = '/etc/shipyard/shipyard.conf' - + # Check that shipyard.conf exists if os.path.isfile(path): config = configparser.ConfigParser() @@ -73,7 +97,7 @@ class BaseResource(object): return query_data else: - return 'Error - Missing Configuration File' + raise AppError(ERR_UNKNOWN, "Missing Configuration File") class ShipyardRequestContext(object): @@ -107,4 +131,3 @@ class ShipyardRequestContext(object): class ShipyardRequest(request.Request): context_type = ShipyardRequestContext - diff --git a/shipyard_airflow/errors.py b/shipyard_airflow/errors.py new file mode 100644 index 00000000..5174ad00 --- /dev/null +++ b/shipyard_airflow/errors.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +import json +import falcon + +try: + from collections import OrderedDict +except ImportError: + OrderedDict = dict + +ERR_UNKNOWN = { + 'status': falcon.HTTP_500, + 'title': 'Internal Server Error' +} + +ERR_AIRFLOW_RESPONSE = { + 'status': falcon.HTTP_400, + 'title': 'Error response from Airflow' +} + + +class AppError(Exception): + def __init__(self, error=ERR_UNKNOWN, description=None): + self.error = error + self.error['description'] = description + + @property + def title(self): + return self.error['title'] + + @property + def status(self): + return self.error['status'] + + @property + def description(self): + return self.error['description'] + + @staticmethod + def handle(exception, req, res, error=None): + res.status = exception.status + meta = OrderedDict() + meta['message'] = exception.title + if exception.description: + meta['description'] = exception.description + res.body = json.dumps(meta) + + +class AirflowError(AppError): + def __init__(self, description=None): + super().__init__(ERR_AIRFLOW_RESPONSE) + self.error['description'] = description diff --git a/shipyard_airflow/setup.py b/shipyard_airflow/setup.py index 85e81e05..630bbde7 100644 --- a/shipyard_airflow/setup.py +++ b/shipyard_airflow/setup.py @@ -24,10 +24,9 @@ setup(name='shipyard_airflow', packages=['shipyard_airflow', 'shipyard_airflow.control'], install_requires=[ - 'falcon', - 'requests', - 'configparser', - 'uwsgi>1.4' - ] - ) - + 'falcon', + 'requests', + 'configparser', + 'uwsgi>1.4', + 'python-dateutil' + ]) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..a1748afc --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,6 @@ +# Testing +pytest==3.2.1 +mock==2.0.0 + +# Linting +flake8==3.3.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/control/__init__.py b/tests/unit/control/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/control/test_airflow_connections.py b/tests/unit/control/test_airflow_connections.py new file mode 100644 index 00000000..9a6e3530 --- /dev/null +++ b/tests/unit/control/test_airflow_connections.py @@ -0,0 +1,267 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import falcon +from falcon import testing +import mock + +from shipyard_airflow.control import api +from shipyard_airflow.control.airflow_connections import ( + AirflowAddConnectionResource, + AirflowDeleteConnectionResource, + AirflowListConnectionsResource, +) + + +class BaseTesting(testing.TestCase): + def setUp(self): + super().setUp() + self.app = api.start_api() + self.conn_id = 1 + self.protocol = 'http' + self.host = '10.0.0.1' + self.port = '3000' + + @property + def _headers(self): + return { + 'X-Auth-Token': '10' + } + + +class AirflowAddConnectionResourceTestCase(BaseTesting): + def setUp(self): + super().setUp() + self.action = 'add' + + @property + def _url(self): + return ('/api/v1.0/connections/{}/conn_id/{}/' + 'protocol/{}/host/{}/port/{}'.format( + self.action, self.conn_id, + self.protocol, self.host, self.port)) + + def test_on_get_missing_config_file(self): + doc = { + 'description': 'Missing Configuration File', + 'message': 'Internal Server Error' + } + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_500 + + @mock.patch.object(AirflowAddConnectionResource, 'retrieve_config') + def test_on_get_invalid_action(self, mock_config): + self.action = 'invalid_action' + doc = { + 'title': 'Invalid parameter', + 'description': ('The "action" parameter is invalid.' + ' Invalid Paremeters for Adding Airflow' + ' Connection') + } + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_400 + mock_config.assert_called_once_with('base', 'web_server') + + @mock.patch('shipyard_airflow.airflow_client.requests') + @mock.patch.object(AirflowAddConnectionResource, 'retrieve_config') + def test_on_get_airflow_error(self, mock_config, mock_requests): + doc = { + 'message': 'Error response from Airflow', + 'description': "can't add connections in airflow" + } + mock_response = { + 'output': { + 'stderr': "can't add connections in airflow" + } + } + mock_requests.get.return_value.json.return_value = mock_response + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_400 + mock_config.assert_called_once_with('base', 'web_server') + + @mock.patch('shipyard_airflow.airflow_client.requests') + @mock.patch.object(AirflowAddConnectionResource, 'retrieve_config') + def test_on_get_airflow_success(self, mock_config, mock_requests): + doc = { + 'type': 'success', + 'message': 'Airflow Success', + } + mock_response = { + 'output': { + 'stderr': None, + 'stdout': 'Airflow Success' + } + } + mock_requests.get.return_value.json.return_value = mock_response + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_200 + mock_config.assert_called_once_with('base', 'web_server') + + +class AirflowDeleteConnectionResource(BaseTesting): + def setUp(self): + self.action = 'delete' + super().setUp() + + @property + def _url(self): + return '/api/v1.0/connections/{}/conn_id/{}'.format( + self.action, self.conn_id) + + def test_on_get_missing_config_file(self): + doc = { + 'description': 'Missing Configuration File', + 'message': 'Internal Server Error' + } + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_500 + + @mock.patch.object(AirflowDeleteConnectionResource, 'retrieve_config') + def test_on_get_invalid_action(self, mock_config): + self.action = 'invalid_action' + doc = { + 'title': 'Invalid parameter', + 'description': ('The "action" parameter is invalid.' + ' Invalid Paremeters for Deleting Airflow' + ' Connection') + } + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_400 + mock_config.assert_called_once_with('base', 'web_server') + + @mock.patch('shipyard_airflow.airflow_client.requests') + @mock.patch.object(AirflowDeleteConnectionResource, 'retrieve_config') + def test_on_get_airflow_error(self, mock_config, mock_requests): + doc = { + 'message': 'Error response from Airflow', + 'description': "can't delete connections in airflow" + } + mock_response = { + 'output': { + 'stderr': "can't delete connections in airflow" + } + } + mock_requests.get.return_value.json.return_value = mock_response + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + + assert result.json == doc + assert result.status == falcon.HTTP_400 + mock_config.assert_called_once_with('base', 'web_server') + + @mock.patch('shipyard_airflow.airflow_client.requests') + @mock.patch.object(AirflowDeleteConnectionResource, 'retrieve_config') + def test_on_get_airflow_success(self, mock_config, mock_requests): + doc = { + 'type': 'success', + 'message': 'Airflow Success', + } + mock_response = { + 'output': { + 'stderr': None, + 'stdout': 'Airflow Success' + } + } + mock_requests.get.return_value.json.return_value = mock_response + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_200 + mock_config.assert_called_once_with('base', 'web_server') + + +class AirflowListConnectionsResource(BaseTesting): + def setUp(self): + self.action = 'list' + super().setUp() + + @property + def _url(self): + return '/api/v1.0/connections/{}'.format(self.action) + + def test_on_get_missing_config_file(self): + doc = { + 'description': 'Missing Configuration File', + 'message': 'Internal Server Error' + } + result = self.simulate_get(self._url, headers=self._headers) + assert result.json == doc + assert result.status == falcon.HTTP_500 + + @mock.patch.object(AirflowListConnectionsResource, 'retrieve_config') + def test_on_get_invalid_action(self, mock_config): + self.action = 'invalid_action' + doc = { + 'title': 'Invalid parameter', + 'description': ('The "action" parameter is invalid.' + ' Invalid Paremeters for listing Airflow' + ' Connections') + } + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + + assert result.json == doc + assert result.status == falcon.HTTP_400 + mock_config.assert_called_once_with('base', 'web_server') + + @mock.patch('shipyard_airflow.airflow_client.requests') + @mock.patch.object(AirflowListConnectionsResource, 'retrieve_config') + def test_on_get_airflow_error(self, mock_config, mock_requests): + doc = { + 'message': 'Error response from Airflow', + 'description': "can't list connections in airlfow" + } + mock_response = { + 'output': { + 'stderr': "can't list connections in airlfow" + } + } + mock_requests.get.return_value.json.return_value = mock_response + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + + assert result.json == doc + assert result.status == falcon.HTTP_400 + mock_config.assert_called_once_with('base', 'web_server') + + @mock.patch('shipyard_airflow.airflow_client.requests') + @mock.patch.object(AirflowListConnectionsResource, 'retrieve_config') + def test_on_get_airflow_success(self, mock_config, mock_requests): + doc = { + 'type': 'success', + 'message': 'Airflow Success', + } + mock_response = { + 'output': { + 'stderr': None, + 'stdout': 'Airflow Success' + } + } + mock_requests.get.return_value.json.return_value = mock_response + mock_config.return_value = 'some_url' + result = self.simulate_get(self._url, headers=self._headers) + + assert result.json == doc + assert result.status == falcon.HTTP_200 + mock_config.assert_called_once_with('base', 'web_server') diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..fb104908 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = py35, pep8 + +[testenv] +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +setenv= + PYTHONWARNING=all +commands= + pytest \ + {posargs} + +[testenv:pep8] +commands = flake8 {posargs} + +[flake8] +ignore=E302,H306,D100,D101,D102 \ No newline at end of file