diff --git a/lower-constraints.txt b/lower-constraints.txt index 4fe56a87b..2558fe197 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -127,6 +127,7 @@ reno==2.7.0 repoze.lru==0.7 requests-oauthlib==0.8.0 requests==2.14.2 +requests-mock==1.2.0 requestsexceptions==1.4.0 restructuredtext-lint==1.1.3 rfc3986==1.1.0 diff --git a/tacker/tests/unit/base.py b/tacker/tests/unit/base.py index d4f7a18bf..e17647805 100644 --- a/tacker/tests/unit/base.py +++ b/tacker/tests/unit/base.py @@ -16,6 +16,7 @@ import mock from oslo_config import cfg from oslo_config import fixture as config_fixture +from requests_mock.contrib import fixture as requests_mock_fixture from tacker.tests import base @@ -32,3 +33,15 @@ class TestCase(base.BaseTestCase): def _mock(self, target, new=mock.DEFAULT): patcher = mock.patch(target, new) return patcher.start() + + +class FixturedTestCase(TestCase): + client_fixture_class = None + + def setUp(self): + super(FixturedTestCase, self).setUp() + if self.client_fixture_class: + self.requests_mock = self.useFixture(requests_mock_fixture. + Fixture()) + fix = self.client_fixture_class(self.requests_mock) + self.cs = self.useFixture(fix).client diff --git a/tacker/tests/unit/db/utils.py b/tacker/tests/unit/db/utils.py index 8202cfa18..89f204f25 100644 --- a/tacker/tests/unit/db/utils.py +++ b/tacker/tests/unit/db/utils.py @@ -129,8 +129,9 @@ def get_dummy_vnf_config_obj(): 'config': {'firewall': 'dummy_firewall_values'}}}}}}} -def get_dummy_vnf(): - return {'status': 'PENDING_CREATE', 'instance_id': None, 'name': +def get_dummy_vnf(status='PENDING_CREATE', scaling_group=False, + instance_id=None): + dummy_vnf = {'status': status, 'instance_id': instance_id, 'name': u'test_openwrt', 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', 'vnfd_id': u'eb094833-995e-49f0-a047-dfb56aaf7c4e', 'vnfd': { @@ -146,6 +147,11 @@ def get_dummy_vnf(): 'attributes': {u'param_values': u''}, 'id': 'eb84260e-5ff7-4332-b032-50a14d6c1123', 'description': u'OpenWRT with services'} + if scaling_group: + dummy_vnf['attributes'].update({'scaling_group_names': + '{"SP1": "SP1_group"}', + 'heat_template': 'test'}) + return dummy_vnf def get_dummy_vnf_config_attr(): diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/__init__.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/client.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/client.py new file mode 100644 index 000000000..9ca28524b --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/client.py @@ -0,0 +1,56 @@ +# Copyright 2019 NTT DATA +# +# 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 fixtures + +from heatclient import client +from keystoneauth1 import fixture +from keystoneauth1 import loading +from keystoneauth1 import session + + +IDENTITY_URL = 'http://identityserver:5000/v3' +HEAT_URL = 'http://heat-api' + + +class ClientFixture(fixtures.Fixture): + + def __init__(self, requests_mock, heat_url=HEAT_URL, + identity_url=IDENTITY_URL): + super(ClientFixture, self).__init__() + self.identity_url = identity_url + self.client = None + self.token = fixture.V2Token() + self.token.set_scope() + self.requests_mock = requests_mock + self.discovery = fixture.V2Discovery(href=self.identity_url) + s = self.token.add_service('orchestration') + s.add_endpoint(heat_url) + + def setUp(self): + super(ClientFixture, self).setUp() + auth_url = '%s/tokens' % self.identity_url + headers = {'X-Content-Type': 'application/json'} + self.requests_mock.post(auth_url, + json=self.token, headers=headers) + self.requests_mock.get(self.identity_url, + json=self.discovery, headers=headers) + self.client = self.new_client() + + def new_client(self): + self.session = session.Session() + loader = loading.get_plugin_loader('password') + self.session.auth = loader.load_from_options( + auth_url=self.identity_url, username='xx', password='xx') + return client.Client("1", session=self.session) diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py new file mode 100644 index 000000000..287b38cfa --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py @@ -0,0 +1,70 @@ +# Copyright 2019 NTT DATA +# +# 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 tacker.tests import uuidsentinel + + +def get_dummy_stack(outputs=True, status='CREATE_COMPELETE'): + outputs_value = [{}] + if outputs: + outputs_value = [{'output_value': '192.168.120.216', + 'output_key': 'mgmt_ip-VDU1', + 'description': 'No description given'}] + + dummy_stack = {'parent': None, 'disable_rollback': True, + 'description': 'Demo example\n', + 'deletion_time': None, 'stack_name': + 'vnf-6_3f089d15-0000-4dc0-8519-a613d577a07b', + 'stack_status_reason': 'Stack CREATE completed successfully', + 'creation_time': '2019-02-28T15:17:48Z', + 'outputs': outputs_value, + 'timeout_mins': 10, 'stack_status': status, + 'stack_owner': None, + 'updated_time': None, + 'id': uuidsentinel.instance_id} + return dummy_stack + + +def get_dummy_resource(resource_status='CREATE_COMPLETE'): + return {'resource_name': 'SP1_group', + 'logical_resource_id': 'SP1_group', + 'creation_time': '2019-03-06T08:57:47Z', + 'resource_status_reason': 'state changed', + 'updated_time': '2019-03-06T08:57:47Z', + 'required_by': ['SP1_scale_out', 'SP1_scale_in'], + 'resource_status': resource_status, + 'physical_resource_id': uuidsentinel.stack_id, + 'attributes': {'outputs_list': None, 'refs': None, + 'refs_map': None, 'outputs': None, + 'current_size': None, 'mgmt_ip-vdu1': 'test1'}, + 'resource_type': 'OS::Heat::AutoScalingGroup'} + + +def get_dummy_event(resource_status='CREATE_COMPLETE'): + return {'resource_name': 'SP1_scale_out', + 'event_time': '2019-03-06T05:44:27Z', + 'logical_resource_id': 'SP1_scale_out', + 'resource_status': resource_status, + 'resource_status_reason': 'state changed', + 'id': uuidsentinel.event_id} + + +def get_dummy_policy_dict(): + return {'instance_id': uuidsentinel.instance_id, + 'vnf': {'attributes': {'scaling_group_names': '{"SP1": "G1"}'}, + 'id': uuidsentinel.vnf_id}, + 'name': 'SP1', + 'action': 'out', + 'type': 'tosca.policies.tacker.Scaling', + 'properties': {}} diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py index 43b8c400e..0125df997 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py @@ -13,29 +13,293 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock +from tacker import context from tacker.extensions import vnfm from tacker.tests.unit import base +from tacker.tests.unit.db import utils +from tacker.tests.unit.vnfm.infra_drivers.openstack.fixture_data import client +from tacker.tests.unit.vnfm.infra_drivers.openstack.fixture_data import \ + fixture_data_utils as fd_utils +from tacker.tests import uuidsentinel from tacker.vnfm.infra_drivers.openstack import openstack -class TestOpenStack(base.TestCase): +@ddt.ddt +class TestOpenStack(base.FixturedTestCase): + client_fixture_class = client.ClientFixture - @mock.patch("tacker.vnfm.infra_drivers.openstack.heat_client.HeatClient") - def test_create_wait_with_heat_connection_exception(self, mocked_hc): - stack = {"stack_status", "CREATE_IN_PROGRESS"} - mocked_hc.get.side_effect = [stack, Exception("any stuff")] - openstack_driver = openstack.OpenStack() + def setUp(self): + super(TestOpenStack, self).setUp() + self.openstack = openstack.OpenStack() + self.context = context.get_admin_context() + self.url = client.HEAT_URL + self.instance_uuid = uuidsentinel.instance_id + self.stack_id = uuidsentinel.stack_id + self.json_headers = {'content-type': 'application/json', + 'location': 'http://heat-api/stacks/' + + self.instance_uuid + '/myStack/60f83b5e'} + self._mock('tacker.common.clients.OpenstackClients.heat', self.cs) + self.mock_log = mock.patch('tacker.vnfm.infra_drivers.openstack.' + 'openstack.LOG').start() + mock.patch('time.sleep', return_value=None).start() + + def _response_in_wait_until_stack_ready(self, status_list, + stack_outputs=True): + # response for heat_client's get() + for status in status_list: + url = self.url + '/stacks/' + self.instance_uuid + json = {'stack': fd_utils.get_dummy_stack(stack_outputs, + status=status)} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + + def _response_in_resource_get(self, id, res_name=None): + # response for heat_client's resource_get() + if res_name: + url = self.url + '/stacks/' + id + ('/myStack/60f83b5e/' + 'resources/') + res_name + else: + url = self.url + '/stacks/' + id + + json = {'resource': fd_utils.get_dummy_resource()} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + + def test_create_wait(self): + self._response_in_wait_until_stack_ready(["CREATE_IN_PROGRESS", + "CREATE_COMPLETE"]) + vnf_dict = utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.openstack.create_wait(None, None, + vnf_dict, self.instance_uuid, None) + self.mock_log.debug.assert_called_with('outputs %s', + fd_utils.get_dummy_stack()['outputs']) + self.assertEqual('{"VDU1": "192.168.120.216"}', + vnf_dict['mgmt_ip_address']) + + def test_create_wait_without_mgmt_ips(self): + self._response_in_wait_until_stack_ready(["CREATE_IN_PROGRESS", + "CREATE_COMPLETE"], + stack_outputs=False) + vnf_dict = utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.openstack.create_wait(None, None, + vnf_dict, self.instance_uuid, None) + self.mock_log.debug.assert_called_with('outputs %s', + fd_utils.get_dummy_stack(outputs=False)['outputs']) + self.assertIsNone(vnf_dict['mgmt_ip_address']) + + def test_create_wait_with_scaling_group_names(self): + self._response_in_wait_until_stack_ready(["CREATE_IN_PROGRESS", + "CREATE_COMPLETE"]) + self._response_in_resource_get(self.instance_uuid, + res_name='SP1_group') + url = self.url + '/stacks/' + self.stack_id + '/resources' + json = {'resources': [fd_utils.get_dummy_resource()]} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + self._response_in_resource_get(self.stack_id) + vnf_dict = utils.get_dummy_vnf(scaling_group=True) + self.openstack.create_wait(None, None, vnf_dict, self.instance_uuid, + None) + self.assertEqual('{"vdu1": ["test1"]}', vnf_dict['mgmt_ip_address']) + + def test_create_wait_failed_with_stack_retries_0(self): + self._response_in_wait_until_stack_ready(["CREATE_IN_PROGRESS"]) + vnf_dict = utils.get_dummy_vnf(instance_id=self.instance_uuid) self.assertRaises(vnfm.VNFCreateWaitFailed, - openstack_driver.create_wait, - None, None, {}, 'vnf_id', None) + self.openstack.create_wait, + None, None, vnf_dict, self.instance_uuid, None) - @mock.patch("tacker.vnfm.infra_drivers.openstack.heat_client.HeatClient") - def test_delete_wait_with_heat_connection_exception(self, mocked_hc): - stack = {"stack_status", "DELETE_IN_PROGRESS"} - mocked_hc.get.side_effect = [stack, Exception("any stuff")] - openstack_driver = openstack.OpenStack() + def test_create_wait_failed_with_stack_retries_not_0(self): + self._response_in_wait_until_stack_ready(["CREATE_IN_PROGRESS", + "FAILED"]) + vnf_dict = utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.assertRaises(vnfm.VNFCreateWaitFailed, + self.openstack.create_wait, + None, None, vnf_dict, self.instance_uuid, {}) + + def _exception_response(self): + url = self.url + '/stacks/' + self.instance_uuid + body = {"error": Exception("any stuff")} + self.requests_mock.register_uri('GET', url, body=body, + status_code=404, headers=self.json_headers) + + def test_create_wait_with_exception(self): + self._exception_response() + vnf_dict = utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.assertRaises(vnfm.VNFCreateWaitFailed, + self.openstack.create_wait, + None, None, vnf_dict, self.instance_uuid, None) + + def test_delete_wait_failed_with_stack_retries_0(self): + self._response_in_wait_until_stack_ready(["DELETE_IN_PROGRESS"]) self.assertRaises(vnfm.VNFDeleteWaitFailed, - openstack_driver.delete_wait, - None, None, 'vnf_id', None, None) + self.openstack.delete_wait, + None, None, self.instance_uuid, None, None) + + def test_delete_wait_stack_retries_not_0(self): + self._response_in_wait_until_stack_ready(["DELETE_IN_PROGRESS", + "FAILED"]) + self.assertRaises(vnfm.VNFDeleteWaitFailed, + self.openstack.delete_wait, + None, None, self.instance_uuid, None, None) + self.mock_log.warning.assert_called_once() + + def test_update_wait(self): + self._response_in_wait_until_stack_ready(["UPDATE_IN_PROGRESS", + "UPDATE_COMPLETE"]) + vnf_dict = utils.get_dummy_vnf(status='PENDING_UPDATE', + instance_id=self.instance_uuid) + self.openstack.update_wait(None, None, vnf_dict, None) + self.mock_log.debug.assert_called_with('outputs %s', + fd_utils.get_dummy_stack()['outputs']) + self.assertEqual('{"VDU1": "192.168.120.216"}', + vnf_dict['mgmt_ip_address']) + + def test_update_wait_without_mgmt_ips(self): + self._response_in_wait_until_stack_ready(["UPDATE_IN_PROGRESS", + "UPDATE_COMPLETE"], + stack_outputs=False) + vnf_dict = utils.get_dummy_vnf(status='PENDING_UPDATE', + instance_id=self.instance_uuid) + self.openstack.update_wait(None, None, vnf_dict, None) + self.mock_log.debug.assert_called_with('outputs %s', + fd_utils.get_dummy_stack(outputs=False)['outputs']) + self.assertIsNone(vnf_dict['mgmt_ip_address']) + + def test_update_wait_failed_with_retries_0(self): + self._response_in_wait_until_stack_ready(["UPDATE_IN_PROGRESS"]) + vnf_dict = utils.get_dummy_vnf(status='PENDING_UPDATE', + instance_id=self.instance_uuid) + self.assertRaises(vnfm.VNFUpdateWaitFailed, + self.openstack.update_wait, + None, None, vnf_dict, + None) + + def test_update_wait_failed_stack_retries_not_0(self): + self._response_in_wait_until_stack_ready(["UPDATE_IN_PROGRESS", + "FAILED"]) + vnf_dict = utils.get_dummy_vnf(status='PENDING_UPDATE', + instance_id=self.instance_uuid) + self.assertRaises(vnfm.VNFUpdateWaitFailed, + self.openstack.update_wait, + None, None, vnf_dict, + None) + + def _responses_in_resource_event_list(self, dummy_event): + # response for heat_client's resource_event_list() + url = self.url + '/stacks/' + self.instance_uuid + json = {'stack': [fd_utils.get_dummy_stack()]} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + url = self.url + '/stacks/' + self.instance_uuid + ('/myStack/60f83b5e' + '/resources/SP1_scale_out/events?limit=1&sort_dir=desc&sort_keys=' + 'event_time') + json = {'events': [dummy_event]} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + + def test_scale(self): + dummy_event = fd_utils.get_dummy_event() + self._responses_in_resource_event_list(dummy_event) + # response for heat_client's resource_signal() + url = self.url + '/stacks/' + self.instance_uuid + ('/myStack/60f83b5e' + '/resources/SP1_scale_out/signal') + self.requests_mock.register_uri('POST', url, json={}, + headers=self.json_headers) + event_id = self.openstack.scale(plugin=self, context=self.context, + auth_attr=None, + policy=fd_utils.get_dummy_policy_dict(), + region_name=None) + self.assertEqual(dummy_event['id'], event_id) + + def _response_in_resource_get_list(self): + # response for heat_client's resource_get_list() + url = self.url + '/stacks/' + self.stack_id + '/resources' + json = {'resources': [fd_utils.get_dummy_resource()]} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + + def _test_scale(self, resource_status): + dummy_event = fd_utils.get_dummy_event(resource_status) + self._responses_in_resource_event_list(dummy_event) + self._response_in_resource_get(self.instance_uuid, res_name='G1') + self._response_in_resource_get_list() + self._response_in_resource_get(self.stack_id) + self._response_in_resource_get(self.instance_uuid, + res_name='SP1_group') + + def test_scale_wait_with_different_last_event_id(self): + self._test_scale("SIGNAL_COMPLETE") + mgmt_ip = self.openstack.scale_wait(plugin=self, context=self.context, + auth_attr=None, + policy=fd_utils.get_dummy_policy_dict(), + region_name=None, + last_event_id=uuidsentinel. + non_last_event_id) + self.assertEqual('{"vdu1": ["test1"]}', mgmt_ip) + + @ddt.data("SIGNAL_COMPLETE", "CREATE_COMPLETE") + def test_scale_wait_with_same_last_event_id(self, resource_status): + self._test_scale(resource_status) + mgmt_ip = self.openstack.scale_wait(plugin=self, + context=self.context, + auth_attr=None, + policy=fd_utils.get_dummy_policy_dict(), + region_name=None, + last_event_id=fd_utils.get_dummy_event()['id']) + self.assertEqual('{"vdu1": ["test1"]}', mgmt_ip) + + @mock.patch('tacker.vnfm.infra_drivers.openstack.openstack.LOG') + def test_scale_wait_failed_with_exception(self, mock_log): + self._exception_response() + self.assertRaises(vnfm.VNFScaleWaitFailed, + self.openstack.scale_wait, + plugin=self, context=self.context, auth_attr=None, + policy=fd_utils.get_dummy_policy_dict(), + region_name=None, + last_event_id=fd_utils.get_dummy_event()['id']) + mock_log.warning.assert_called_once() + + def _response_in_resource_metadata(self, metadata=None): + # response for heat_client's resource_metadata() + url = self.url + '/stacks/' + self.instance_uuid + \ + '/myStack/60f83b5e/resources/SP1_scale_out/metadata' + json = {'metadata': {'scaling_in_progress': metadata}} + self.requests_mock.register_uri('GET', url, json=json, + headers=self.json_headers) + + def test_scale_wait_failed_with_stack_retries_0(self): + dummy_event = fd_utils.get_dummy_event("CREATE_IN_PROGRESS") + self._responses_in_resource_event_list(dummy_event) + self._response_in_resource_metadata(True) + self.assertRaises(vnfm.VNFScaleWaitFailed, + self.openstack.scale_wait, + plugin=self, context=self.context, auth_attr=None, + policy=fd_utils.get_dummy_policy_dict(), + region_name=None, + last_event_id=dummy_event['id']) + self.mock_log.warning.assert_called_once() + + def test_scale_wait_without_resource_metadata(self): + dummy_event = fd_utils.get_dummy_event("CREATE_IN_PROGRESS") + self._responses_in_resource_event_list(dummy_event) + self._response_in_resource_metadata() + self._response_in_resource_get(self.instance_uuid, res_name='G1') + self._response_in_resource_get_list() + self._response_in_resource_get(self.stack_id) + self._response_in_resource_get(self.instance_uuid, + res_name='SP1_group') + mgmt_ip = self.openstack.scale_wait(plugin=self, context=self.context, + auth_attr=None, + policy=fd_utils.get_dummy_policy_dict(), + region_name=None, + last_event_id=fd_utils.get_dummy_event() + ['id']) + error_reason = ('When signal occurred within cool down ' + 'window, no events generated from heat, ' + 'so ignore it') + self.mock_log.warning.assert_called_once_with(error_reason) + self.assertEqual('{"vdu1": ["test1"]}', mgmt_ip) diff --git a/tacker/tests/uuidsentinel.py b/tacker/tests/uuidsentinel.py new file mode 100644 index 000000000..9dae89667 --- /dev/null +++ b/tacker/tests/uuidsentinel.py @@ -0,0 +1,33 @@ +# Copyright 2019 NTT DATA +# All 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 sys + + +class UUIDSentinels(object): + def __init__(self): + from oslo_utils import uuidutils + self._uuid_module = uuidutils + self._sentinels = {} + + def __getattr__(self, name): + if name.startswith('_'): + raise ValueError('Sentinels must not start with _') + if name not in self._sentinels: + self._sentinels[name] = self._uuid_module.generate_uuid() + return self._sentinels[name] + + +sys.modules[__name__] = UUIDSentinels() diff --git a/tacker/vnfm/infra_drivers/openstack/openstack.py b/tacker/vnfm/infra_drivers/openstack/openstack.py index e0548b7a8..4dc21ed5d 100644 --- a/tacker/vnfm/infra_drivers/openstack/openstack.py +++ b/tacker/vnfm/infra_drivers/openstack/openstack.py @@ -180,13 +180,7 @@ class OpenStack(abstract_driver.VnfAbstractDriver, stack=vnf_id) raise exception_class(reason=error_reason) elif stack_retries != 0 and status != wait_status: - if stack: - error_reason = stack.stack_status_reason - else: - error_reason = _("action on VNF %(vnf_id)s is not " - "completed. Current status of stack is " - "%(stack_status)s") % {'vnf_id': vnf_id, - 'stack_status': status} + error_reason = stack.stack_status_reason LOG.warning(error_reason) raise exception_class(reason=error_reason) diff --git a/test-requirements.txt b/test-requirements.txt index 442e5a463..6d15758ad 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -19,3 +19,4 @@ testtools>=2.2.0 # MIT WebTest>=2.0.27 # MIT python-barbicanclient>=4.5.2 # Apache-2.0 python-blazarclient>=1.0.1 # Apache-2.0 +requests-mock>=1.2.0 # Apache-2.0 \ No newline at end of file