Create rbac utils fixture and refactor tests

This commit creates a test fixture for rbac_utils.RbacUtils
and refactors test_rbac_utils tests accordingly. This
allows other tests to use the fixture as needed, improving
code maintenance and readability, as well as the ease
with which tests can be written.

Specifically:
  - adds fixtures file for overriding conf settings and
    for rbac_utils.RbacUtils
  - refactors rbac_utils tests to use the appropriate
    fixture

Change-Id: I2f49137ff71089ecf9764ee1f7887b64185249dd
diff --git a/patrole_tempest_plugin/tests/unit/fixtures.py b/patrole_tempest_plugin/tests/unit/fixtures.py
new file mode 100644
index 0000000..f25e05d
--- /dev/null
+++ b/patrole_tempest_plugin/tests/unit/fixtures.py
@@ -0,0 +1,109 @@
+# Copyright 2017 AT&T Corporation.
+# 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.
+
+"""Fixtures for Patrole tests."""
+from __future__ import absolute_import
+
+import fixtures
+import mock
+
+from tempest import config
+
+from patrole_tempest_plugin import rbac_utils
+
+
+CONF = config.CONF
+
+
+class ConfPatcher(fixtures.Fixture):
+    """Fixture to patch and restore global CONF. Adopted from Nova.
+
+    This also resets overrides for everything that is patched during
+    its teardown.
+    """
+
+    def __init__(self, **kwargs):
+        """Constructor
+
+        :params group: if specified all config options apply to that group.
+        :params **kwargs: the rest of the kwargs are processed as a
+            set of key/value pairs to be set as configuration override.
+        """
+        super(ConfPatcher, self).__init__()
+        self.group = kwargs.pop('group', None)
+        self.args = kwargs
+
+    def setUp(self):
+        super(ConfPatcher, self).setUp()
+        for k, v in self.args.items():
+            self.addCleanup(CONF.clear_override, k, self.group)
+            CONF.set_override(k, v, self.group)
+
+
+class RbacUtilsFixture(fixtures.Fixture):
+    """Fixture for RbacUtils class."""
+
+    USER_ID = mock.sentinel.user_id
+    PROJECT_ID = mock.sentinel.project_id
+
+    def __init__(self, **kwargs):
+        super(RbacUtilsFixture, self).__init__()
+        self.available_roles = None
+
+    def setUp(self):
+        super(RbacUtilsFixture, self).setUp()
+
+        self.useFixture(ConfPatcher(rbac_test_role='member', group='rbac'))
+        self.useFixture(ConfPatcher(
+            admin_role='admin', auth_version='v3', group='identity'))
+
+        test_obj_kwargs = {
+            'os_primary.credentials.user_id': self.USER_ID,
+            'os_primary.credentials.tenant_id': self.PROJECT_ID,
+            'os_primary.credentials.project_id': self.PROJECT_ID,
+            'get_identity_version.return_value': 'v3'
+        }
+        self.mock_test_obj = mock.Mock(**test_obj_kwargs)
+        self.mock_time = mock.patch.object(rbac_utils, 'time').start()
+
+        self.roles_v3_client = (
+            self.mock_test_obj.get_client_manager.return_value.roles_v3_client)
+
+    def switch_role(self, *role_toggles):
+        """Instantiate `rbac_utils.RbacUtils` and call `switch_role`.
+
+        Create an instance of `rbac_utils.RbacUtils` and call `switch_role`
+        for each boolean value in `role_toggles`. The number of calls to
+        `switch_role` is always 1 + len(role_toggles) because the
+        `rbac_utils.RbacUtils` constructor automatically calls `switch_role`.
+
+        :param role_toggles: the list of boolean values iterated over and
+            passed to `switch_role`.
+        """
+        if not self.available_roles:
+            self.set_roles('admin', 'member')
+
+        self.fake_rbac_utils = rbac_utils.RbacUtils(self.mock_test_obj)
+        for role_toggle in role_toggles:
+            self.fake_rbac_utils.switch_role(self.mock_test_obj, role_toggle)
+
+    def set_roles(self, *roles):
+        """Set the list of available roles in the system to `roles`."""
+        self.available_roles = {
+            'roles': [{'name': role, 'id': '%s_id' % role} for role in roles]
+        }
+        self.roles_v3_client.list_user_roles_on_project.return_value =\
+            self.available_roles
+        self.roles_v3_client.list_roles.return_value = self.available_roles
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index 3a26b00..79503b1 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -16,229 +16,155 @@
 import mock
 import testtools
 
-from tempest import config
-from tempest.lib import base as lib_base
-from tempest.lib import exceptions as lib_exc
 from tempest.tests import base
 
 from patrole_tempest_plugin import rbac_exceptions
 from patrole_tempest_plugin import rbac_utils
-
-CONF = config.CONF
+from patrole_tempest_plugin.tests.unit import fixtures as patrole_fixtures
 
 
 class RBACUtilsTest(base.TestCase):
 
-    available_roles = {
-        'roles': [
-            {'name': 'admin', 'id': 'admin_id'},
-            {'name': 'Member', 'id': 'member_id'}
-        ]
-    }
-
-    @mock.patch.object(rbac_utils, 'credentials', autospec=True,
-                       **{'is_admin_available.return_value': True})
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    def setUp(self, *args):
+    def setUp(self):
         super(RBACUtilsTest, self).setUp()
+        # Reset the role history after each test run to avoid validation
+        # errors between tests.
+        rbac_utils.RbacUtils.switch_role_history = {}
+        self.rbac_utils = self.useFixture(patrole_fixtures.RbacUtilsFixture())
 
-        self.mock_test_obj = mock.Mock(spec=lib_base.BaseTestCase)
-        self.mock_test_obj.os_primary = mock.Mock(
-            **{'credentials.user_id': mock.sentinel.user_id,
-               'credentials.tenant_id': mock.sentinel.project_id})
-        self.mock_test_obj.get_client_manager = mock.Mock(
-            **{'return_value.roles_v3_client.list_roles.return_value':
-                self.available_roles}
-        )
-        self.mock_test_obj.get_identity_version = mock.Mock(return_value='v3')
-
-        with mock.patch.object(rbac_utils.RbacUtils, '_validate_switch_role'):
-            self.rbac_utils = rbac_utils.RbacUtils(self.mock_test_obj)
-        self.rbac_utils.switch_role_history = {}
-        self.rbac_utils.admin_role_id = 'admin_id'
-        self.rbac_utils.rbac_role_id = 'member_id'
-
-        CONF.set_override('admin_role', 'admin', group='identity')
-        CONF.set_override('auth_version', 'v3', group='identity')
-        CONF.set_override('rbac_test_role', 'Member', group='rbac')
-
-        roles_client = self.mock_test_obj.get_client_manager().roles_v3_client
-        roles_client.create_user_role_on_project.reset_mock()
-        self.mock_test_obj.os_primary.reset_mock()
-
-        self.addCleanup(CONF.clear_override, 'rbac_test_role', group='rbac')
-        self.addCleanup(CONF.clear_override, 'admin_role', group='identity')
-        self.addCleanup(CONF.clear_override, 'auth_version', group='identity')
-        self.addCleanup(mock.patch.stopall)
-
-    def _mock_list_user_roles_on_project(self, return_value):
-        self.mock_test_obj.os_admin = mock.Mock(
-            **{'roles_client.list_user_roles_on_project.'
-               'return_value': {'roles': [{'id': return_value}]}})
-
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    def test_initialization_with_missing_admin_role(self, _):
-        self.rbac_utils.admin_role_id = None
-        self.rbac_utils.rbac_role_id = None
-        CONF.set_override('admin_role', None, group='identity')
-
+    def test_switch_role_with_missing_admin_role(self):
+        self.rbac_utils.set_roles('member')
         e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
-                              self.rbac_utils.switch_role, self.mock_test_obj,
-                              True)
+                              self.rbac_utils.switch_role, True)
         self.assertIn("Role with name 'admin' does not exist in the system.",
                       e.__str__())
 
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    def test_initialization_with_missing_rbac_role(self, _):
-        self.rbac_utils.admin_role_id = None
-        self.rbac_utils.rbac_role_id = None
-        CONF.set_override('rbac_test_role', None, group='rbac')
-
+    def test_switch_role_with_missing_rbac_role(self):
+        self.rbac_utils.set_roles('admin')
         e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
-                              self.rbac_utils.switch_role, self.mock_test_obj,
-                              True)
+                              self.rbac_utils.switch_role, True)
         self.assertIn("Role defined by rbac_test_role does not exist in the "
                       "system.", e.__str__())
 
+    def test_switch_role_to_admin_role(self):
+        self.rbac_utils.switch_role()
+
+        mock_test_obj = self.rbac_utils.mock_test_obj
+        roles_client = self.rbac_utils.roles_v3_client
+        mock_time = self.rbac_utils.mock_time
+
+        roles_client.create_user_role_on_project.assert_called_once_with(
+            self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID, 'admin_id')
+        mock_test_obj.os_primary.auth_provider.clear_auth\
+            .assert_called_once_with()
+        mock_test_obj.os_primary.auth_provider.set_auth\
+            .assert_called_once_with()
+        mock_time.sleep.assert_called_once_with(1)
+
+    def test_switch_role_to_member_role(self):
+        self.rbac_utils.switch_role(True)
+
+        mock_test_obj = self.rbac_utils.mock_test_obj
+        roles_client = self.rbac_utils.roles_v3_client
+        mock_time = self.rbac_utils.mock_time
+
+        roles_client.create_user_role_on_project.assert_has_calls([
+            mock.call(self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID,
+                      'admin_id'),
+            mock.call(self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID,
+                      'member_id')
+        ])
+        mock_test_obj.os_primary.auth_provider.clear_auth.assert_has_calls(
+            [mock.call()] * 2)
+        mock_test_obj.os_primary.auth_provider.set_auth.assert_has_calls(
+            [mock.call()] * 2)
+        mock_time.sleep.assert_has_calls([mock.call(1)] * 2)
+
+    def test_switch_role_to_member_role_then_admin_role(self):
+        self.rbac_utils.switch_role(True, False)
+
+        mock_test_obj = self.rbac_utils.mock_test_obj
+        roles_client = self.rbac_utils.roles_v3_client
+        mock_time = self.rbac_utils.mock_time
+
+        roles_client.create_user_role_on_project.assert_has_calls([
+            mock.call(self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID,
+                      'admin_id'),
+            mock.call(self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID,
+                      'member_id'),
+            mock.call(self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID,
+                      'admin_id')
+        ])
+        mock_test_obj.os_primary.auth_provider.clear_auth.assert_has_calls(
+            [mock.call()] * 3)
+        mock_test_obj.os_primary.auth_provider.set_auth.assert_has_calls(
+            [mock.call()] * 3)
+        mock_time.sleep.assert_has_calls([mock.call(1)] * 3)
+
+    def test_switch_role_without_boolean_value(self):
+        self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                          self.rbac_utils.switch_role, "admin")
+        self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                          self.rbac_utils.switch_role, None)
+
+    def test_switch_role_with_false_value_twice(self):
+        expected_error_message = (
+            '`toggle_rbac_role` must not be called with the same bool value '
+            'twice. Make sure that you included a rbac_utils.switch_role '
+            'method call inside the test.')
+
+        e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                              self.rbac_utils.switch_role, False)
+        self.assertIn(expected_error_message, str(e))
+
+        e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                              self.rbac_utils.switch_role, True, False, False)
+        self.assertIn(expected_error_message, str(e))
+
+    def test_switch_role_with_true_value_twice(self):
+        expected_error_message = (
+            '`toggle_rbac_role` must not be called with the same bool value '
+            'twice. Make sure that you included a rbac_utils.switch_role '
+            'method call inside the test.')
+
+        e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                              self.rbac_utils.switch_role, True, True)
+        self.assertIn(expected_error_message, str(e))
+
+        e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
+                              self.rbac_utils.switch_role, True, False, True,
+                              True)
+        self.assertIn(expected_error_message, str(e))
+
     def test_clear_user_roles(self):
-        roles_client = self.mock_test_obj.get_client_manager().roles_v3_client
-        roles_client.list_user_roles_on_project.return_value = {
-            'roles': [{'id': 'admin_id'}, {'id': 'member_id'}]
-        }
+        self.rbac_utils.switch_role(True)
 
-        self.rbac_utils.roles_client = roles_client
-        self.rbac_utils.project_id = mock.sentinel.project_id
-        self.rbac_utils.user_id = mock.sentinel.user_id
+        roles_client = self.rbac_utils.roles_v3_client
 
-        self.rbac_utils._clear_user_roles(None)
-
-        roles_client.list_user_roles_on_project.\
-            assert_called_once_with(mock.sentinel.project_id,
-                                    mock.sentinel.user_id)
+        roles_client.list_user_roles_on_project.assert_has_calls([
+            mock.call(self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID)
+        ] * 2)
         roles_client.delete_role_from_user_on_project.\
             assert_has_calls([
                 mock.call(mock.sentinel.project_id, mock.sentinel.user_id,
                           'admin_id'),
                 mock.call(mock.sentinel.project_id, mock.sentinel.user_id,
-                          'member_id'),
-            ])
+                          'member_id')] * 2)
 
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    @mock.patch.object(rbac_utils, 'time', autospec=True)
-    def test_rbac_utils_switch_role_to_admin_role(self, mock_time, _):
-        self.rbac_utils.prev_switch_role = True
-        self._mock_list_user_roles_on_project('admin_id')
-        roles_client = self.mock_test_obj.get_client_manager().roles_v3_client
-
-        self.rbac_utils.switch_role(self.mock_test_obj, False)
-
-        roles_client.create_user_role_on_project.assert_called_once_with(
-            mock.sentinel.project_id, mock.sentinel.user_id, 'admin_id')
-        self.mock_test_obj.os_primary.auth_provider.clear_auth\
-            .assert_called_once_with()
-        self.mock_test_obj.os_primary.auth_provider.set_auth\
-            .assert_called_once_with()
-        mock_time.sleep.assert_called_once_with(1)
-
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    @mock.patch.object(rbac_utils, 'time', autospec=True)
-    def test_rbac_utils_switch_role_to_rbac_role(self, mock_time, _):
-        self._mock_list_user_roles_on_project('member_id')
-        roles_client = self.mock_test_obj.get_client_manager().roles_v3_client
-
-        self.rbac_utils.switch_role(self.mock_test_obj, True)
-
-        roles_client.create_user_role_on_project.assert_called_once_with(
-            mock.sentinel.project_id, mock.sentinel.user_id, 'member_id')
-        self.mock_test_obj.os_primary.auth_provider.clear_auth\
-            .assert_called_once_with()
-        self.mock_test_obj.os_primary.auth_provider.set_auth\
-            .assert_called_once_with()
-        mock_time.sleep.assert_called_once_with(1)
-
-    def test_RBAC_utils_switch_roles_without_boolean_value(self):
-        self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
-                          self.rbac_utils.switch_role, self.mock_test_obj,
-                          "admin")
-        self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
-                          self.rbac_utils.switch_role, self.mock_test_obj,
-                          None)
-
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    def test_rbac_utils_switch_roles_with_false_value_twice(self, _):
-        self._mock_list_user_roles_on_project('admin_id')
-        self.rbac_utils.switch_role(self.mock_test_obj, False)
-        e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
-                              self.rbac_utils.switch_role, self.mock_test_obj,
-                              False)
-        self.assertIn(
-            '`toggle_rbac_role` must not be called with the same bool value '
-            'twice. Make sure that you included a rbac_utils.switch_role '
-            'method call inside the test.', str(e))
-
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    def test_rbac_utils_switch_roles_with_true_value_twice(self, _):
-        self._mock_list_user_roles_on_project('admin_id')
-        self.rbac_utils.switch_role(self.mock_test_obj, True)
-        e = self.assertRaises(rbac_exceptions.RbacResourceSetupFailed,
-                              self.rbac_utils.switch_role, self.mock_test_obj,
-                              True)
-        self.assertIn(
-            '`toggle_rbac_role` must not be called with the same bool value '
-            'twice. Make sure that you included a rbac_utils.switch_role '
-            'method call inside the test.', str(e))
-
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
     @mock.patch.object(rbac_utils, 'LOG', autospec=True)
     @mock.patch.object(rbac_utils, 'sys', autospec=True)
-    def test_rbac_utils_switch_roles_with_unhandled_exception(self, mock_sys,
-                                                              mock_log, _):
-        """Test whether throwing an unhandled exception doesn't throw error.
+    def test_switch_roles_with_unexpected_exception(self, mock_sys, mock_log):
+        """Test whether unexpected exceptions don't throw error.
 
-        If a skip exception, say, is thrown then this means that switch_role is
-        never called within the test function. But if an unhandled exception
-        or skip exception is thrown, then this should not result in an error
-        being raised.
+        If an unexpected exception or skip exception is raised, then that
+        should not result in an error being raised.
         """
-        self._mock_list_user_roles_on_project('member_id')
+        unexpected_exceptions = [testtools.TestCase.skipException,
+                                 AttributeError]
 
-        # Skip exception is an example of a legitimate case where `switch_role`
-        # is thrown. AttributeError, on the other hand, is an example of an
-        # unexpected exception being thrown that should be allowed to bubble
-        # up, rather than being obfuscated by `switch_role` error being thrown
-        # instead.
-        unhandled_exceptions = [testtools.TestCase.skipException,
-                                AttributeError]
-
-        for unhandled_exception in unhandled_exceptions:
-            mock_sys.exc_info.return_value = [unhandled_exception]
-
-            # Ordinarily switching to the same role would result in an error,
-            # but because the skipException is thrown before the test finishes,
-            # this is not treated as a failure.
-            self.rbac_utils.switch_role(self.mock_test_obj, False)
-            self.rbac_utils.switch_role(self.mock_test_obj, False)
+        for unexpected_exception in unexpected_exceptions:
+            mock_sys.exc_info.return_value = [unexpected_exception()]
+            # Ordinarily calling switch_role twice with the same value should
+            # result in an error being thrown -- but not in this case.
+            self.rbac_utils.switch_role(False)
             mock_log.error.assert_not_called()
-
-            self.rbac_utils.switch_role(self.mock_test_obj, True)
-            self.rbac_utils.switch_role(self.mock_test_obj, True)
-            mock_log.error.assert_not_called()
-
-    @mock.patch.object(rbac_utils.RbacUtils, '_clear_user_roles',
-                       autospec=True, return_value=False)
-    def test_rbac_utils_switch_role_except_exception(self,
-                                                     mock_clear_user_roles):
-        roles_client = self.mock_test_obj.get_client_manager().roles_v3_client
-        roles_client.create_user_role_on_project.side_effect =\
-            lib_exc.NotFound
-
-        self.assertRaises(lib_exc.NotFound, self.rbac_utils.switch_role,
-                          self.mock_test_obj, True)
diff --git a/test-requirements.txt b/test-requirements.txt
index 177d0fd..34b2238 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -6,6 +6,7 @@
 sphinx>=1.6.2 # BSD
 openstackdocstheme>=1.11.0 # Apache-2.0
 reno!=2.3.1,>=1.8.0 # Apache-2.0
+fixtures>=3.0.0  # Apache-2.0/BSD
 mock>=2.0 # BSD
 coverage!=4.4,>=4.0 # Apache-2.0
 nose # LGPL