From 4f5bfd835c01ccc7b79d0bce048fe0684c6c945a Mon Sep 17 00:00:00 2001 From: Frank Sandoval Date: Mon, 17 Sep 2018 15:50:04 -0600 Subject: add AAF integration and AAF simulator Patch (#3) addresses commentse this patch fixes unit tests. Original commit added AAF permissions checks for HAS API Issue-ID: OPTFRA-331 Change-Id: I69519beee31f57e4ac5188604d21805266a06074 Signed-off-by: Frank Sandoval --- conductor/conductor/api/adapters/__init__.py | 17 +++ conductor/conductor/api/adapters/aaf/__init__.py | 18 +++ .../api/adapters/aaf/aaf_authentication.py | 151 +++++++++++++++++++++ conductor/conductor/api/controllers/v1/plans.py | 59 +++++--- .../tests/functional/simulators/aafsim/Dockerfile | 39 ++++++ .../tests/functional/simulators/aafsim/aafsim.py | 87 ++++++++++++ .../functional/simulators/aafsim/requirements.txt | 1 + .../aafsim/responses/get_perms_user.json | 6 + .../simulators/aafsim/responses/healthcheck.json | 1 + .../tests/functional/simulators/build_aafsim.sh | 23 ++++ .../tests/functional/simulators/destroy_aafsim.sh | 23 ++++ .../tests/functional/simulators/run_aafsim.sh | 22 +++ .../tests/unit/api/controller/v1/test_plans.py | 34 +++-- conductor/requirements.txt | 3 +- 14 files changed, 454 insertions(+), 30 deletions(-) create mode 100644 conductor/conductor/api/adapters/__init__.py create mode 100644 conductor/conductor/api/adapters/aaf/__init__.py create mode 100644 conductor/conductor/api/adapters/aaf/aaf_authentication.py create mode 100755 conductor/conductor/tests/functional/simulators/aafsim/Dockerfile create mode 100755 conductor/conductor/tests/functional/simulators/aafsim/aafsim.py create mode 100755 conductor/conductor/tests/functional/simulators/aafsim/requirements.txt create mode 100644 conductor/conductor/tests/functional/simulators/aafsim/responses/get_perms_user.json create mode 100644 conductor/conductor/tests/functional/simulators/aafsim/responses/healthcheck.json create mode 100755 conductor/conductor/tests/functional/simulators/build_aafsim.sh create mode 100755 conductor/conductor/tests/functional/simulators/destroy_aafsim.sh create mode 100755 conductor/conductor/tests/functional/simulators/run_aafsim.sh (limited to 'conductor') diff --git a/conductor/conductor/api/adapters/__init__.py b/conductor/conductor/api/adapters/__init__.py new file mode 100644 index 0000000..ba46563 --- /dev/null +++ b/conductor/conductor/api/adapters/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# diff --git a/conductor/conductor/api/adapters/aaf/__init__.py b/conductor/conductor/api/adapters/aaf/__init__.py new file mode 100644 index 0000000..4648e6c --- /dev/null +++ b/conductor/conductor/api/adapters/aaf/__init__.py @@ -0,0 +1,18 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# \ No newline at end of file diff --git a/conductor/conductor/api/adapters/aaf/aaf_authentication.py b/conductor/conductor/api/adapters/aaf/aaf_authentication.py new file mode 100644 index 0000000..fca84fd --- /dev/null +++ b/conductor/conductor/api/adapters/aaf/aaf_authentication.py @@ -0,0 +1,151 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property +# +# 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 base64 +from datetime import datetime, timedelta +import json + +from conductor.common import rest +from conductor.i18n import _LE, _LI + +from oslo_log import log +LOG = log.getLogger(__name__) + +from oslo_config import cfg +CONF = cfg.CONF + +# TBD - read values from conductor.conf +AAF_OPTS = [ + cfg.BoolOpt('is_aaf_enabled', + default=True, + help='is_aaf_enabled.'), + cfg.IntOpt('aaf_cache_expiry_hrs', + default='3', + help='aaf_cache_expiry_hrs.'), + cfg.StrOpt('aaf_url', + default='http://aaf-service:8100/authz/perms/user/', + help='aaf_url.'), + cfg.IntOpt('aaf_retries', + default='3', + help='aaf_retries.'), + cfg.IntOpt('aaf_timeout', + default='100', + help='aaf_timeout.'), + cfg.ListOpt('aaf_user_roles', + default=['{"type": "org.onap.oof","instance": "plans","action": "GET"}', + '{"type": "org.onap.oof","instance": "plans","action": "POST"}'], + help='aaf_user_roles.') +] + +CONF.register_opts(AAF_OPTS, group='aaf_authentication') + +AUTHZ_PERMS_USER = '{}/authz/perms/user/{}' + +EXPIRE_TIME = 'expire_time' + +perm_cache = {} + +def clear_cache(): + perm_cache.clear() + + +def authenticate(uid, passwd): + try: + perms = get_aaf_permissions(uid, passwd) + return has_valid_role(perms) + except Exception as exp: + LOG.error("Error Authenticating the user {} : {}: ".format(uid, exp)) + pass + return False + +""" +Check whether the user has valid permissions +return True if the user has valid permissions +else return false +""" + +def has_valid_role(perms): + aaf_user_roles = CONF.aaf_authentication.aaf_user_roles + + permObj = json.loads(perms) + permList = permObj["perm"] + for user_role in aaf_user_roles: + role = json.loads(user_role) + userType = role["type"] + userInstance = role["instance"] + userAction = role["action"] + for perm in permList: + permType = perm["type"] + permInstance = perm["instance"] + permAction = perm["action"] + if userType == permType and userInstance == permInstance and \ + (userAction == permAction or userAction == "*"): + return True + return False + +""" +Make the remote aaf api call if user is not in the cache. + +Return the perms +""" +def get_aaf_permissions(uid, passwd): + key = base64.b64encode("{}_{}".format(uid, passwd), "ascii") + time_delta = timedelta(hours = CONF.aaf_authentication.aaf_cache_expiry_hrs) + +# TBD - test cache logic + perms = perm_cache.get(key) + + if perms and datetime.now() < perms.get(EXPIRE_TIME): + LOG.debug("Returning cached value") + return perms + LOG.debug("Invoking AAF authentication API") + response = remote_api(passwd, uid) + perms = {EXPIRE_TIME: datetime.now() + time_delta, 'roles': response} + perm_cache[key] = perms + return response + +def remote_api(passwd, uid): + server_url = CONF.aaf_authentication.aaf_url.rstrip('/') + kwargs = { + "server_url": server_url, + "retries": CONF.aaf_authentication.aaf_retries, + "username": uid, + "password": passwd, + "log_debug": LOG.debug, + "read_timeout": CONF.aaf_authentication.aaf_timeout, + } + restReq = rest.REST(**kwargs) + + headers = {"Accept": "application/json"} + rkwargs = { + "method": 'GET', + "path": '', + "headers": headers, + } + response = restReq.request(**rkwargs) + + if response is None: + LOG.error(_LE("No response from AAF ")) + elif response.status_code != 200: + LOG.error(_LE("AAF request returned HTTP " + "status {} {}, link: {}"). + format(response.status_code, response.reason, + server_url)) + return response.content + diff --git a/conductor/conductor/api/controllers/v1/plans.py b/conductor/conductor/api/controllers/v1/plans.py index 3858069..f70b998 100644 --- a/conductor/conductor/api/controllers/v1/plans.py +++ b/conductor/conductor/api/controllers/v1/plans.py @@ -16,7 +16,6 @@ # # ------------------------------------------------------------------------- # - import six import yaml import base64 @@ -34,8 +33,9 @@ from conductor.api.controllers import validator from conductor.i18n import _, _LI from oslo_config import cfg -CONF = cfg.CONF +from conductor.api.adapters.aaf import aaf_authentication as aaf_auth +CONF = cfg.CONF LOG = log.getLogger(__name__) @@ -44,14 +44,14 @@ CONDUCTOR_API_OPTS = [ default='', help='Base URL for plans.'), cfg.StrOpt('username', - default='admin1', + default='', help='username for plans.'), cfg.StrOpt('password', - default='plan.15', + default='', help='password for plans.'), cfg.BoolOpt('basic_auth_secure', - default=True, - help='auth toggling.') + default=True, + help='auth toggling.'), ] CONF.register_opts(CONDUCTOR_API_OPTS, group='conductor_api') @@ -61,7 +61,6 @@ CREATE_SCHEMA = ( (decorators.optional('id'), types.string), (decorators.optional('limit'), types.integer), (decorators.optional('name'), types.string), - (decorators.optional('num_solution'), types.string), ('template', string_or_dict), (decorators.optional('template_url'), types.string), (decorators.optional('timeout'), types.integer), @@ -86,11 +85,12 @@ class PlansBaseController(object): def plans_get(self, plan_id=None): - basic_auth_flag = CONF.conductor_api.basic_auth_secure + auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_authentication.is_aaf_enabled + # TBD - is healthcheck properly supported? if plan_id == 'healthcheck' or \ - not basic_auth_flag or \ - (basic_auth_flag and check_basic_auth()): + not auth_flag or \ + (auth_flag and check_auth()): return self.plan_getid(plan_id) def plan_getid(self, plan_id): @@ -147,6 +147,7 @@ class PlansBaseController(object): args.get('name'))) client = pecan.request.controller + transaction_id = pecan.request.headers.get('transaction-id') if transaction_id: args['template']['transaction-id'] = transaction_id @@ -290,12 +291,13 @@ class PlansController(PlansBaseController): if args and args['name']: LOG.info('Plan name: {}'.format(args['name'])) - basic_auth_flag = CONF.conductor_api.basic_auth_secure + auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_authentication.is_aaf_enabled - # Create the plan only when the basic authentication is disabled or pass the authentication check - if not basic_auth_flag or \ - (basic_auth_flag and check_basic_auth()): + # Create the plan only when the basic authentication is disabled or pass the authenticaiton check + if not auth_flag or \ + (auth_flag and check_auth()): plan = self.plan_create(args) + if not plan: error('/errors/server_error', _('Unable to create Plan.')) else: @@ -307,11 +309,13 @@ class PlansController(PlansBaseController): """Pecan subcontroller routing callback""" return PlansItemController(uuid4), remainder -def check_basic_auth(): + +def check_auth(): """ Returns True/False if the username/password of Basic Auth match/not match + Will also check role-based access controls if AAF integration configured :return boolean value - """ + """ try: if pecan.request.headers['Authorization'] and verify_user(pecan.request.headers['Authorization']): @@ -323,19 +327,22 @@ def check_basic_auth(): user_pw = auth_str.split(' ')[1] decode_user_pw = base64.b64decode(user_pw) list_id_pw = decode_user_pw.split(':') - LOG.error("Incorrect username={}/password={}".format(list_id_pw[0],list_id_pw[1])) + LOG.error("Incorrect username={} / password={}".format(list_id_pw[0], list_id_pw[1])) except: error('/errors/basic_auth_error', _('Unauthorized: The request does not ' 'provide any HTTP authentication (basic authentication)')) + plan = False if not plan: error('/errors/authentication_error', _('Invalid credentials: username or password is incorrect')) + return plan def verify_user(authstr): """ - authenticate user as per config file + authenticate user as per config file or AAF authentication service + :param authstr: :return boolean value """ user_dict = dict() @@ -347,7 +354,17 @@ def verify_user(authstr): user_dict['password'] = list_id_pw[1] password = CONF.conductor_api.password username = CONF.conductor_api.username - if username == user_dict['username'] and password == user_dict['password']: - return True + +# print ("plans.verify_user(): Expected username/password: {}/{}".format(username, password)) +# print ("plans.verify_user(): Provided username/password: {}/{}".format(user_dict['username'], user_dict['password'])) + + retVal = False + + if CONF.aaf_authentication.is_aaf_enabled: + retVal = aaf_auth.authenticate(user_dict['username'], user_dict['password']) else: - return False + if username == user_dict['username'] and password == user_dict['password']: + retVal = True + + return retVal + diff --git a/conductor/conductor/tests/functional/simulators/aafsim/Dockerfile b/conductor/conductor/tests/functional/simulators/aafsim/Dockerfile new file mode 100755 index 0000000..5970b3b --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/aafsim/Dockerfile @@ -0,0 +1,39 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 AT&T Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# +# Use an official Python runtime as a parent image +FROM python:2.7 + +# Set the working directory to /su/python/webpy-rest-dockerized +WORKDIR /opt/aafsim + +# Copy the current directory contents into the container at /app +ADD ./ /opt/aafsim + +# Install any needed packages specified in requirements.txt +RUN pip install web.py + +# Make port 80 available to the world outside this container +EXPOSE 8081 + +# Define environment variable +ENV NAME aafsim + +# Run aafsim.py when the container launches +CMD ["/bin/sh", "-c", "python -u aafsim.py 8081 > /tmp/aafsim.log 2>&1"] + diff --git a/conductor/conductor/tests/functional/simulators/aafsim/aafsim.py b/conductor/conductor/tests/functional/simulators/aafsim/aafsim.py new file mode 100755 index 0000000..c3cea8f --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/aafsim/aafsim.py @@ -0,0 +1,87 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property +# +# 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 web +import web.webapi +import json + +from subprocess import Popen, PIPE +from oslo_log import log +LOG = log.getLogger(__name__) + +urls = ( + '/healthcheck','healthcheck', + '/authz/perms/user/','get_perms_user', +) + +myhelp = {"/conductorsim/help":"provides help"} +myok = {"ok":"ok"} +json_data={} + +replydir = "./responses/" + +def replyToAafGet(web, replydir, replyfile): + LOG.debug("------------------------------------------------------") + fullreply = replydir + replyfile + trid=web.ctx.env.get('X_TRANSACTIONID','111111') + #print ("X-TransactionId : {}".format(trid)) + LOG.debug("this is the context : {}".format(web.ctx.fullpath)) + with open(fullreply) as json_file: + json_data = json.load(json_file) + LOG.debug(json_data) + web.header('Content-Type', 'application/json') + web.header('X-TransactionId', trid) + return json.dumps(json_data) + +class healthcheck: + def GET(self): + LOG.debug("------------------------------------------------------") + replyfile = "healthcheck.json" + #replyToAaiGet (web, replydir, replyfile) + fullreply = replydir + replyfile + trid=web.ctx.env.get('X_TRANSACTIONID','111111') + #print ("X-TransactionId : {}".format(trid)) + LOG.debug("this is the context : {}".format(web.ctx.fullpath)) + with open(fullreply) as json_file: + json_data = json.load(json_file) + LOG.debug(json_data) + web.header('Content-Type', 'application/json') + web.header('X-TransactionId', trid) + return json.dumps(json_data) + +class get_perms_user: + def GET(self): + LOG.debug("------------------------------------------------------") + replyfile = "get_perms_user.json" + #replyToAafGet (web, replydir, replyfile) + fullreply = replydir + replyfile + trid=web.ctx.env.get('X_TRANSACTIONID','111111') + #print ("X-TransactionId : {}".format(trid)) + LOG.debug("this is the context : {}".format(web.ctx.fullpath)) + with open(fullreply) as json_file: + json_data = json.load(json_file) + LOG.debug(json_data) + web.header('Content-Type', 'application/json') + web.header('X-TransactionId', trid) + return json.dumps(json_data) + + + +if __name__ == "__main__": + app = web.application(urls, globals()) + app.run() diff --git a/conductor/conductor/tests/functional/simulators/aafsim/requirements.txt b/conductor/conductor/tests/functional/simulators/aafsim/requirements.txt new file mode 100755 index 0000000..c077218 --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/aafsim/requirements.txt @@ -0,0 +1 @@ +web diff --git a/conductor/conductor/tests/functional/simulators/aafsim/responses/get_perms_user.json b/conductor/conductor/tests/functional/simulators/aafsim/responses/get_perms_user.json new file mode 100644 index 0000000..d923ca5 --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/aafsim/responses/get_perms_user.json @@ -0,0 +1,6 @@ +{"perm": [{"instance": "plans", "action": "POST", "type": "org.onap.oof"}, + {"instance": "plans", "action": "GET", "type": "org.onap.oof"}] +} + + + diff --git a/conductor/conductor/tests/functional/simulators/aafsim/responses/healthcheck.json b/conductor/conductor/tests/functional/simulators/aafsim/responses/healthcheck.json new file mode 100644 index 0000000..b0bd6da --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/aafsim/responses/healthcheck.json @@ -0,0 +1 @@ +{"status":"success"} diff --git a/conductor/conductor/tests/functional/simulators/build_aafsim.sh b/conductor/conductor/tests/functional/simulators/build_aafsim.sh new file mode 100755 index 0000000..db6b4d1 --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/build_aafsim.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# +# depends on execution from ../simulators directly, parent of ./aafsim +cd ./aafsim +docker build -t aafsim . + diff --git a/conductor/conductor/tests/functional/simulators/destroy_aafsim.sh b/conductor/conductor/tests/functional/simulators/destroy_aafsim.sh new file mode 100755 index 0000000..b242dd0 --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/destroy_aafsim.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# +docker stop aafsim +docker rm aafsim +docker rmi aafsim + diff --git a/conductor/conductor/tests/functional/simulators/run_aafsim.sh b/conductor/conductor/tests/functional/simulators/run_aafsim.sh new file mode 100755 index 0000000..0039bbc --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/run_aafsim.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property +# +# 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. +# +# ------------------------------------------------------------------------- +# +# Note mapping to external port 8082. avoids conflict with aaisim +docker run -d --name aafsim -p 8082:8081 aafsim + diff --git a/conductor/conductor/tests/unit/api/controller/v1/test_plans.py b/conductor/conductor/tests/unit/api/controller/v1/test_plans.py index a0cd0c8..f0a28ec 100644 --- a/conductor/conductor/tests/unit/api/controller/v1/test_plans.py +++ b/conductor/conductor/tests/unit/api/controller/v1/test_plans.py @@ -35,21 +35,24 @@ class TestPlansController(base_api.BaseApiTest): self.assertEqual(204, actual_response.status_int) self.assertEqual("GET,POST", actual_response.headers['Allow']) + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch.object(plans.LOG, 'error') @mock.patch.object(plans.LOG, 'debug') @mock.patch.object(plans.LOG, 'warning') @mock.patch.object(plans.LOG, 'info') @mock.patch('conductor.common.music.messaging.component.RPCClient.call') def test_index_get(self, rpc_mock, info_mock, warning_mock, debug_mock, - error_mock): + error_mock, aaf_mock): req_json_file = './conductor/tests/unit/api/controller/v1/plans.json' params = json.loads(open(req_json_file).read()) plan_id = str(uuid.uuid4()) params['id'] = plan_id rpc_mock.return_value = {'plans': [params]} + aaf_mock.return_value = True actual_response = self.app.get('/v1/plans', extra_environ=self.extra_environment) self.assertEqual(200, actual_response.status_int) + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch.object(plans.LOG, 'error') @mock.patch.object(plans.LOG, 'debug') @mock.patch.object(plans.LOG, 'warning') @@ -57,14 +60,17 @@ class TestPlansController(base_api.BaseApiTest): @mock.patch('conductor.common.music.messaging.component.RPCClient.call') def test_index_post_error(self, rpc_mock, info_mock, warning_mock, debug_mock, - error_mock): + error_mock, + aaf_mock): req_json_file = './conductor/tests/unit/api/controller/v1/plans.json' params = jsonutils.dumps(json.loads(open(req_json_file).read())) rpc_mock.return_value = {} + aaf_mock.return_value = True response = self.app.post('/v1/plans', params=params, expect_errors=True, extra_environ=self.extra_environment) self.assertEqual(500, response.status_int) + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch.object(plans.LOG, 'error') @mock.patch.object(plans.LOG, 'debug') @mock.patch.object(plans.LOG, 'warning') @@ -72,7 +78,8 @@ class TestPlansController(base_api.BaseApiTest): @mock.patch('conductor.common.music.messaging.component.RPCClient.call') def test_index_post_success(self, rpc_mock, info_mock, warning_mock, debug_mock, - error_mock): + error_mock, + aaf_mock): req_json_file = './conductor/tests/unit/api/controller/v1/plans.json' params = json.loads(open(req_json_file).read()) mock_params = copy.deepcopy(params) @@ -80,6 +87,7 @@ class TestPlansController(base_api.BaseApiTest): mock_params['id'] = plan_id rpc_mock.return_value = {'plan': mock_params} + aaf_mock.return_value = True params = json.dumps(params) response = self.app.post('/v1/plans', params=params, expect_errors=True, extra_environ=self.extra_environment) @@ -94,26 +102,30 @@ class TestPlansController(base_api.BaseApiTest): class TestPlansItemController(base_api.BaseApiTest): + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch('conductor.common.music.messaging.component.RPCClient.call') - def test_index_options(self, rpc_mock): + def test_index_options(self, rpc_mock, aaf_mock): req_json_file = './conductor/tests/unit/api/controller/v1/plans.json' params = json.loads(open(req_json_file).read()) plan_id = str(uuid.uuid4()) params['id'] = plan_id rpc_mock.return_value = {'plans': [params]} + aaf_mock.return_value = True url = '/v1/plans/' + plan_id print(url) actual_response = self.app.options(url=url, expect_errors=True, extra_environ=self.extra_environment) self.assertEqual(204, actual_response.status_int) self.assertEqual("GET,DELETE", actual_response.headers['Allow']) + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch('conductor.common.music.messaging.component.RPCClient.call') - def test_index_httpmethod_notallowed(self, rpc_mock): + def test_index_httpmethod_notallowed(self, rpc_mock, aaf_mock): req_json_file = './conductor/tests/unit/api/controller/v1/plans.json' params = json.loads(open(req_json_file).read()) plan_id = str(uuid.uuid4()) params['id'] = plan_id rpc_mock.return_value = {'plans': [params]} + aaf_mock.return_value = True url = '/v1/plans/' + plan_id actual_response = self.app.put(url=url, expect_errors=True, extra_environ=self.extra_environment) self.assertEqual(405, actual_response.status_int) @@ -122,35 +134,41 @@ class TestPlansItemController(base_api.BaseApiTest): actual_response = self.app.post(url=url, expect_errors=True, extra_environ=self.extra_environment) self.assertEqual(405, actual_response.status_int) + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch('conductor.common.music.messaging.component.RPCClient.call') - def test_index_get(self, rpc_mock): + def test_index_get(self, rpc_mock, aaf_mock): req_json_file = './conductor/tests/unit/api/controller/v1/plans.json' params = json.loads(open(req_json_file).read()) plan_id = str(uuid.uuid4()) params['id'] = plan_id expected_response = {'plans': [params]} rpc_mock.return_value = {'plans': [params]} + aaf_mock.return_value = True url = '/v1/plans/' + plan_id actual_response = self.app.get(url=url, expect_errors=True, extra_environ=self.extra_environment) self.assertEqual(200, actual_response.status_int) self.assertJsonEqual(expected_response, json.loads(actual_response.body)) + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch('conductor.common.music.messaging.component.RPCClient.call') - def test_index_get_non_exist(self, rpc_mock): + def test_index_get_non_exist(self, rpc_mock, aaf_mock): rpc_mock.return_value = {'plans': []} + aaf_mock.return_value = True plan_id = str(uuid.uuid4()) url = '/v1/plans/' + plan_id actual_response = self.app.get(url=url, expect_errors=True, extra_environ=self.extra_environment) self.assertEqual(404, actual_response.status_int) + @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate') @mock.patch('conductor.common.music.messaging.component.RPCClient.call') - def test_index_delete(self, rpc_mock): + def test_index_delete(self, rpc_mock, aaf_mock): req_json_file = './conductor/tests/unit/api/controller/v1/plans.json' params = json.loads(open(req_json_file).read()) plan_id = str(uuid.uuid4()) params['id'] = plan_id rpc_mock.return_value = {'plans': [params]} + aaf_mock.return_value = True url = '/v1/plans/' + plan_id actual_response = self.app.delete(url=url, expect_errors=True, extra_environ=self.extra_environment) self.assertEqual(204, actual_response.status_int) diff --git a/conductor/requirements.txt b/conductor/requirements.txt index d09c960..6bc9dba 100644 --- a/conductor/requirements.txt +++ b/conductor/requirements.txt @@ -23,4 +23,5 @@ requests[security]!=2.9.0,>=2.8.1 # Apache-2.0 six>=1.9.0 # MIT, also required by futurist stevedore>=1.9.0 # Apache-2.0, also required by oslo.config WebOb>=1.2.3 # MIT -onapsmsclient>=0.0.3 \ No newline at end of file +onapsmsclient>=0.0.3 +Flask>=0.11.1 \ No newline at end of file -- cgit 1.2.3-korg