diff --git a/shipyard_airflow/control/base.py b/shipyard_airflow/control/base.py index f8d27042..d9aae4ca 100644 --- a/shipyard_airflow/control/base.py +++ b/shipyard_airflow/control/base.py @@ -32,9 +32,7 @@ class BaseResource(object): self.logger = logging.getLogger('shipyard.control') def on_options(self, req, resp, **kwargs): - """ - Handle options requests - """ + """Handle options requests""" method_map = routing.create_http_method_map(self) for method in method_map: if method_map.get(method).__name__ != 'method_not_allowed': @@ -50,7 +48,8 @@ class BaseResource(object): validation """ has_input = False - if req.content_length > 0 and 'application/json' in req.content_type: + if (req.content_length is not None and 'application/json' in + req.content_type): raw_body = req.stream.read(req.content_length or 0) if raw_body is not None: has_input = True @@ -86,17 +85,12 @@ class BaseResource(object): return None def to_json(self, body_dict): - """ - Thin wrapper around json.dumps, providing the default=str config - """ + """Thin wrapper around json.dumps, providing the default=str config""" return json.dumps(body_dict, default=str) def log_message(self, ctx, level, msg): - """ - Logs a message with context, and extra populated. - """ + """Logs a message with context, and extra populated.""" extra = {'user': 'N/A', 'req_id': 'N/A', 'external_ctx': 'N/A'} - if ctx is not None: extra = { 'user': ctx.user, diff --git a/tests/unit/control/test_base_resource.py b/tests/unit/control/test_base_resource.py new file mode 100644 index 00000000..ab034c0e --- /dev/null +++ b/tests/unit/control/test_base_resource.py @@ -0,0 +1,189 @@ +# 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 json +import logging +from mock import patch +import pytest + +import falcon +from falcon import testing + +from shipyard_airflow.control.base import BaseResource, ShipyardRequestContext +from shipyard_airflow.control.json_schemas import ACTION +from shipyard_airflow.errors import InvalidFormatError + + +def create_req(ctx, body): + '''creates a falcon request''' + env = testing.create_environ( + path='/', + query_string='', + protocol='HTTP/1.1', + scheme='http', + host='falconframework.org', + port=None, + headers={'Content-Type': 'application/json'}, + app='', + body=body, + method='POST', + wsgierrors=None, + file_wrapper=None) + req = falcon.Request(env) + req.context = ctx + return req + + +def create_resp(): + '''creates a falcon response''' + resp = falcon.Response() + return resp + + +def test_on_options(): + '''tests on_options''' + baseResource = BaseResource() + ctx = ShipyardRequestContext() + + req = create_req(ctx, body=None) + resp = create_resp() + + baseResource.on_options(req, resp) + assert 'OPTIONS' in resp.get_header('Allow') + assert resp.status == '200 OK' + + +def test_req_json_no_body(): + '''test req_json when there is no body in the request''' + baseResource = BaseResource() + ctx = ShipyardRequestContext() + + req = create_req(ctx, body=None) + + with pytest.raises(InvalidFormatError) as expected_exc: + baseResource.req_json(req, validate_json_schema=ACTION) + assert 'Json body is required' in str(expected_exc) + assert str(req.path) in str(expected_exc) + + result = baseResource.req_json(req, validate_json_schema=None) + assert result is None + + +@patch('shipyard_airflow.control.base.BaseResource.log_message') +def test_req_json_with_body(mock_logger): + '''test req_json when there is a body''' + baseResource = BaseResource() + ctx = ShipyardRequestContext() + + json_body = json.dumps({ + 'user': "test_user", + 'req_id': "test_req_id", + 'external_ctx': "test_ext_ctx", + 'name': "test_name" + }).encode('utf-8') + + req = create_req(ctx, body=json_body) + + result = baseResource.req_json(req, validate_json_schema=ACTION) + mock_logger.assert_called_with( + ctx, logging.INFO, + 'Input message body: b\'' + json_body.decode('utf-8') + '\'') + assert result == json.loads(json_body.decode('utf-8')) + + req = create_req(ctx, body=json_body) + + json_body = ('testing json').encode('utf-8') + req = create_req(ctx, body=json_body) + + with pytest.raises(InvalidFormatError) as expected_exc: + baseResource.req_json(req) + mock_logger.assert_called_with( + ctx, logging.ERROR, + 'Invalid JSON in request: \n' + json_body.decode('utf-8')) + assert 'JSON could not be decoded' in str(expected_exc) + assert str(req.path) in str(expected_exc) + + +def test_to_json(): + '''test to_json''' + baseResource = BaseResource() + body_dict = { + 'user': 'test_user', + 'req_id': 'test_req_id', + 'external_ctx': 'test_ext_ctx', + 'name': 'test_name' + } + results = baseResource.to_json(body_dict) + assert results == json.dumps(body_dict) + + +@patch('logging.Logger.log') +def test_log_message(mock_log): + '''test log_message''' + baseResource = BaseResource() + ctx = None + level = logging.ERROR + msg = 'test_message' + extra = {'user': 'N/A', 'req_id': 'N/A', 'external_ctx': 'N/A'} + baseResource.log_message(ctx, level, msg) + mock_log.assert_called_with(level, msg, extra=extra) + + ctx = ShipyardRequestContext() + extra = { + 'user': ctx.user, + 'req_id': ctx.request_id, + 'external_ctx': ctx.external_marker, + } + baseResource.log_message(ctx, level, msg) + mock_log.assert_called_with(level, msg, extra=extra) + + +def test_debug(): + '''test debug''' + baseResource = BaseResource() + ctx = ShipyardRequestContext() + msg = 'test_msg' + with patch.object(BaseResource, 'log_message') as mock_method: + baseResource.debug(ctx, msg) + mock_method.assert_called_once_with(ctx, logging.DEBUG, msg) + + +def test_info(): + '''test info''' + baseResource = BaseResource() + ctx = ShipyardRequestContext() + msg = 'test_msg' + with patch.object(BaseResource, 'log_message') as mock_method: + baseResource.info(ctx, msg) + mock_method.assert_called_once_with(ctx, logging.INFO, msg) + + +def test_warn(): + '''test warn ''' + baseResource = BaseResource() + ctx = ShipyardRequestContext() + msg = 'test_msg' + with patch.object(BaseResource, 'log_message') as mock_method: + baseResource.warn(ctx, msg) + mock_method.assert_called_once_with(ctx, logging.WARN, msg) + + +def test_error(): + '''test error''' + baseResource = BaseResource() + ctx = ShipyardRequestContext() + msg = 'test_msg' + with patch.object(BaseResource, 'log_message') as mock_method: + baseResource.error(ctx, msg) + mock_method.assert_called_once_with(ctx, logging.ERROR, msg) diff --git a/tests/unit/control/test_shipyard_request_context.py b/tests/unit/control/test_shipyard_request_context.py new file mode 100644 index 00000000..f65bae39 --- /dev/null +++ b/tests/unit/control/test_shipyard_request_context.py @@ -0,0 +1,90 @@ +# 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. + +from shipyard_airflow.control.base import ShipyardRequestContext + + +def test_set_log_level(): + '''test set_log_level''' + ctx = ShipyardRequestContext() + ctx.set_log_level('error') + assert ctx.log_level == 'error' + + ctx.set_log_level('info') + assert ctx.log_level == 'info' + + ctx.set_log_level('debug') + assert ctx.log_level == 'debug' + + +def test_set_user(): + '''test set_user ''' + ctx = ShipyardRequestContext() + ctx.set_user('test_user') + assert ctx.user == 'test_user' + + +def test_set_project(): + '''test set_project''' + ctx = ShipyardRequestContext() + ctx.set_project('test_project') + assert ctx.project == 'test_project' + + +def test_add_role(): + '''test add_role''' + ctx = ShipyardRequestContext() + ctx.add_role('test_role') + assert 'test_role' in ctx.roles + + +def test_add_roles(): + '''test add_roles''' + ctx = ShipyardRequestContext() + print(ctx.roles) + test_roles = ['Waiter', 'Host', 'Chef'] + ctx.add_roles(test_roles) + assert ['Chef', 'Host', 'Waiter', 'anyone'] == sorted(ctx.roles) + + +def test_remove_roles(): + '''test remove_roles''' + ctx = ShipyardRequestContext() + ctx.remove_role('anyone') + assert ctx.roles == [] + + +def test_set_external_marker(): + '''test set_external_marker''' + ctx = ShipyardRequestContext() + ctx.set_external_marker('test_ext_marker') + assert ctx.external_marker == 'test_ext_marker' + + +def test_set_policy_engine(): + '''test set_policy_engine''' + ctx = ShipyardRequestContext() + ctx.set_policy_engine('test_policy_engine') + assert ctx.policy_engine == 'test_policy_engine' + + +def test_to_policy_view(): + '''test to_policy_view''' + ctx = ShipyardRequestContext() + policy_dict = ctx.to_policy_view() + assert policy_dict['user_id'] == ctx.user_id + assert policy_dict['user_domain_id'] == ctx.user_domain_id + assert policy_dict['project_domain_id'] == ctx.project_domain_id + assert policy_dict['roles'] == ctx.roles + assert policy_dict['is_admin_project'] == ctx.is_admin_project diff --git a/tox.ini b/tox.ini index b6260aa7..36983ee5 100644 --- a/tox.ini +++ b/tox.ini @@ -49,5 +49,5 @@ commands = --cov-branch \ --cov-report term-missing:skip-covered \ --cov-config .coveragerc \ - --cov=shipyard_client/api_client \ + --cov=shipyard_client \ --cov=shipyard_airflow