Merge "Helper for validating RBAC list actions"
diff --git a/.gitignore b/.gitignore
index 350e0da..324269e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@
# Sphinx
doc/build
doc/source/_static/patrole.conf.sample
+doc/source/framework/code/
# pbr generates these
AUTHORS
diff --git a/.zuul.yaml b/.zuul.yaml
index 085e775..bed19c4 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -16,8 +16,8 @@
- ^.*\.rst$
- ^doc/.*
- ^etc/.*$
- - ^patrole/patrole_tempest_plugin/tests/unit/.*$
- - ^patrole/patrole_tempest_plugin/hacking/.*$
+ - ^patrole_tempest_plugin/tests/unit/.*$
+ - ^patrole_tempest_plugin/hacking/.*$
- ^releasenotes/.*
- ^setup.cfg$
vars:
@@ -77,10 +77,7 @@
parent: patrole-base
description: Patrole job for member role.
# This currently works from stable/pike onward.
- branches:
- - master
- - stable/queens
- - stable/pike
+ branches: ^(?!stable/ocata).*$
vars:
devstack_localrc:
RBAC_TEST_ROLES: member
diff --git a/HACKING.rst b/HACKING.rst
index 9992017..3379292 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -43,6 +43,23 @@
.. _extension test class: https://github.com/openstack/patrole/tree/master/patrole_tempest_plugin/tests/api/network#neutron-extension-rbac-tests
+Supported OpenStack Components
+------------------------------
+
+Patrole only offers **in-tree** integration testing coverage for the following
+components:
+
+* Cinder
+* Glance
+* Keystone
+* Neutron
+* Nova
+
+Patrole currently has no stable library, so reliance upon Patrole's framework
+for external RBAC testing should be done with caution. Nonetheless, even when
+Patrole has a stable library, it will only offer in-tree RBAC testing for
+the components listed above.
+
Role Overriding
---------------
@@ -121,3 +138,34 @@
policies also applies to Patrole.
.. _Tempest logic: https://docs.openstack.org/tempest/latest/HACKING.html#new-tests-for-existing-features
+
+
+Black Box vs. White Box Testing
+-------------------------------
+
+Tempest is a `black box testing framework`_, meaning that it is concerned with
+testing public API endpoints and doesn't concern itself with testing internal
+implementation details. Patrole, as a Tempest plugin, also falls underneath
+the category of black box testing. However, even with policy in code
+documentation, some degree of white box testing is required in order to
+correctly write RBAC tests.
+
+This is because :ref:`policy-in-code` documentation, while useful in many
+respects, is usually quite brief and its main purpose is to help operators
+understand how to customize policy configuration rather than to help
+developers understand complex policy authorization work flows. For example,
+policy in code documentation doesn't make deriving
+:ref:`multiple policies <multiple-policies>` easy. Such documentation also
+doesn't usually mention that a specific parameter needs to be set, or that a
+particular microversion must be enabled, or that a particular set of
+prerequisite API or policy actions must be executed, in order for the policy
+under test to be enforced by the server. This means that test writers must
+account for the internal RBAC implementation in API code in order to correctly
+understand the complete RBAC work flow within an API.
+
+Besides, as mentioned :ref:`elsewhere <design-principles>` in this
+documentation, not all services currently implement policy in code, making
+some degree of white box testing a "necessary evil" for writing robust RBAC
+tests.
+
+.. _black box testing framework: https://docs.openstack.org/tempest/latest/HACKING.html#negative-tests
diff --git a/README.rst b/README.rst
index 31cd3b7..713756a 100644
--- a/README.rst
+++ b/README.rst
@@ -18,9 +18,6 @@
allowing deployments to verify that only intended roles have access to those
APIs.
-Patrole currently offers testing for the following OpenStack services: Nova,
-Neutron, Glance, Cinder and Keystone.
-
Patrole is currently undergoing heavy development. As more projects move
toward policy in code, Patrole will align its testing with the appropriate
documentation.
@@ -202,8 +199,14 @@
**admin** and **member** roles. However, other services may use entirely
different roles or role combinations.
-For more information about the member role and its nomenclature,
-please see: `<https://ask.openstack.org/en/question/4759/member-vs-_member_/>`__.
+For more information about RBAC, reference the `rbac-overview`_
+documentation page.
+
+For information regarding which projects Patrole offers RBAC testing for,
+reference the `HACKING`_ documentation page.
+
+.. _rbac-overview: https://docs.openstack.org/patrole/latest/rbac-overview.html
+.. _HACKING: https://docs.openstack.org/patrole/latest/HACKING.html#supported-openstack-components
Unit Tests
----------
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 012efb2..67530ff 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -4,3 +4,4 @@
sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
openstackdocstheme>=1.18.1 # Apache-2.0
reno>=2.5.0 # Apache-2.0
+sphinxcontrib-apidoc>=0.2.0 # BSD
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 88c1bea..45d8021 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -15,7 +15,13 @@
import os
import sys
-sys.path.insert(0, os.path.abspath('../..'))
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../../'))
+sys.path.insert(0, os.path.abspath('../'))
+sys.path.insert(0, os.path.abspath('./'))
+
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
@@ -26,8 +32,23 @@
'sphinx.ext.viewcode',
'openstackdocstheme',
'oslo_config.sphinxconfiggen',
+ 'sphinxcontrib.apidoc',
]
+# sphinxcontrib.apidoc options
+apidoc_module_dir = '../../patrole_tempest_plugin'
+apidoc_output_dir = 'framework/code'
+apidoc_excluded_paths = [
+ 'hacking',
+ 'hacking/*',
+ 'tests',
+ 'tests/*',
+ 'config.py',
+ 'plugin.py',
+ 'version.py'
+]
+apidoc_separate_modules = True
+
config_generator_config_file = '../../etc/config-generator.patrole.conf'
sample_config_basename = '_static/patrole'
diff --git a/doc/source/framework/policy_authority.rst b/doc/source/framework/policy_authority.rst
index 37b698c..f039692 100644
--- a/doc/source/framework/policy_authority.rst
+++ b/doc/source/framework/policy_authority.rst
@@ -57,7 +57,4 @@
Implementation
--------------
-.. automodule:: patrole_tempest_plugin.policy_authority
- :members:
- :undoc-members:
- :special-members:
+:py:mod:`Policy Authority Module <patrole_tempest_plugin.policy_authority>`
diff --git a/doc/source/framework/rbac_authority.rst b/doc/source/framework/rbac_authority.rst
index 40f2a8d..7ffe24f 100644
--- a/doc/source/framework/rbac_authority.rst
+++ b/doc/source/framework/rbac_authority.rst
@@ -32,7 +32,4 @@
Implementation
--------------
-.. automodule:: patrole_tempest_plugin.rbac_authority
- :members:
- :undoc-members:
- :special-members:
+:py:mod:`RBAC Authority Module <patrole_tempest_plugin.rbac_authority>`
diff --git a/doc/source/framework/rbac_utils.rst b/doc/source/framework/rbac_utils.rst
index b13a4a3..f7cb182 100644
--- a/doc/source/framework/rbac_utils.rst
+++ b/doc/source/framework/rbac_utils.rst
@@ -26,10 +26,7 @@
Implementation
--------------
-.. automodule:: patrole_tempest_plugin.rbac_utils
- :members:
- :private-members:
- :special-members:
+:py:mod:`RBAC Utils Module <patrole_tempest_plugin.rbac_utils>`
.. _Tempest credentials: https://docs.openstack.org/tempest/latest/library/credential_providers.html
.. _dynamic credentials: https://docs.openstack.org/tempest/latest/configuration.html#dynamic-credentials
diff --git a/doc/source/framework/rbac_validation.rst b/doc/source/framework/rbac_validation.rst
index 6cd1534..460c08c 100644
--- a/doc/source/framework/rbac_validation.rst
+++ b/doc/source/framework/rbac_validation.rst
@@ -14,7 +14,4 @@
Implementation
--------------
-.. automodule:: patrole_tempest_plugin.rbac_rule_validation
- :members:
- :private-members:
- :special-members:
+:py:mod:`RBAC Rule Validation Module <patrole_tempest_plugin.rbac_rule_validation>`
diff --git a/doc/source/framework/requirements_authority.rst b/doc/source/framework/requirements_authority.rst
index cf7c51c..daed319 100644
--- a/doc/source/framework/requirements_authority.rst
+++ b/doc/source/framework/requirements_authority.rst
@@ -153,7 +153,4 @@
Implementation
--------------
-.. automodule:: patrole_tempest_plugin.requirements_authority
- :members:
- :undoc-members:
- :special-members:
+:py:mod:`Requirements Authority Module <patrole_tempest_plugin.requirements_authority>`
diff --git a/doc/source/index.rst b/doc/source/index.rst
index a9dcdc0..816908a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -61,7 +61,7 @@
---------
.. toctree::
- :maxdepth: 2
+ :maxdepth: 3
framework/overview
framework/rbac_validation
@@ -69,6 +69,7 @@
framework/policy_authority
framework/requirements_authority
framework/rbac_utils
+ framework/code/modules
Indices and tables
==================
diff --git a/doc/source/rbac-overview.rst b/doc/source/rbac-overview.rst
index acfd66f..cc47f75 100644
--- a/doc/source/rbac-overview.rst
+++ b/doc/source/rbac-overview.rst
@@ -1,3 +1,5 @@
+.. _rbac-overview:
+
==================================
Role-Based Access Control Overview
==================================
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index 90d5fba..62337f7 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -117,7 +117,7 @@
cfg.StrOpt('report_log_path',
default='.',
help="Path (relative or absolute) where the output from "
- "'enable_reporting' is logged. This is combined with"
+ "'enable_reporting' is logged. This is combined with "
"report_log_name to generate the full path."),
]
diff --git a/patrole_tempest_plugin/tests/api/network/rbac_base.py b/patrole_tempest_plugin/tests/api/network/rbac_base.py
index 347651d..dc0ce7f 100644
--- a/patrole_tempest_plugin/tests/api/network/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/network/rbac_base.py
@@ -13,7 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
+from oslo_serialization import jsonutils as json
+
from tempest.api.network import base as network_base
+from tempest.lib.common.utils import test_utils
from patrole_tempest_plugin import rbac_utils
@@ -72,3 +75,13 @@
cls.ntp_client = neutron_tempest_manager.network_client
return manager
+
+ @classmethod
+ def create_service_profile(cls):
+ service_profile = cls.ntp_client.create_service_profile(
+ metainfo=json.dumps({'foo': 'bar'}))
+ service_profile_id = service_profile["service_profile"]["id"]
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.ntp_client.delete_service_profile, service_profile_id)
+ return service_profile_id
diff --git a/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py b/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py
new file mode 100644
index 0000000..db0b8f1
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py
@@ -0,0 +1,77 @@
+# Copyright 2018 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.
+
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class FlavorsServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
+ @classmethod
+ def resource_setup(cls):
+ super(FlavorsServiceProfileExtRbacTest, cls).resource_setup()
+ providers = cls.ntp_client.list_service_providers()
+ if not providers["service_providers"]:
+ raise cls.skipException("No service_providers available.")
+ cls.service_type = providers["service_providers"][0]["service_type"]
+
+ cls.flavor_id = cls.create_flavor()
+ cls.service_profile_id = cls.create_service_profile()
+
+ @classmethod
+ def create_flavor(cls):
+ flavor = cls.ntp_client.create_flavor(service_type=cls.service_type)
+ flavor_id = flavor["flavor"]["id"]
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.ntp_client.delete_flavor, flavor_id)
+ return flavor_id
+
+ def create_flavor_service_profile(self, flavor_id, service_profile_id):
+ self.ntp_client.create_flavor_service_profile(
+ flavor_id, service_profile_id)
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.ntp_client.delete_flavor_service_profile,
+ flavor_id, service_profile_id)
+
+ @decorators.idempotent_id('aa84b4c5-0dd6-4c34-aa81-3a76507f9b81')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_flavor_service_profile"])
+ def test_create_flavor_service_profile(self):
+ """Create flavor_service_profile.
+
+ RBAC test for the neutron "create_flavor_service_profile" policy
+ """
+ with self.rbac_utils.override_role(self):
+ self.create_flavor_service_profile(self.flavor_id,
+ self.service_profile_id)
+
+ @decorators.idempotent_id('3b680d9e-946a-4670-ab7f-0e4576675833')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["delete_flavor_service_profile"])
+ def test_delete_flavor_service_profile(self):
+ """Delete flavor_service_profile.
+
+ RBAC test for the neutron "delete_flavor_service_profile" policy
+ """
+ self.create_flavor_service_profile(self.flavor_id,
+ self.service_profile_id)
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_flavor_service_profile(
+ self.flavor_id, self.service_profile_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py b/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
index dea95ba..76c0db3 100644
--- a/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_serialization import jsonutils as json
-
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
@@ -118,72 +116,3 @@
with self.rbac_utils.override_role(self):
self.ntp_client.list_flavors()
-
-
-class FlavorsServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
- @classmethod
- def resource_setup(cls):
- super(FlavorsServiceProfileExtRbacTest, cls).resource_setup()
- providers = cls.ntp_client.list_service_providers()
- if not providers["service_providers"]:
- raise cls.skipException("No service_providers available.")
- cls.service_type = providers["service_providers"][0]["service_type"]
-
- cls.flavor_id = cls.create_flavor()
- cls.service_profile_id = cls.create_service_profile()
-
- @classmethod
- def create_flavor(cls):
- flavor = cls.ntp_client.create_flavor(service_type=cls.service_type)
- flavor_id = flavor["flavor"]["id"]
- cls.addClassResourceCleanup(
- test_utils.call_and_ignore_notfound_exc,
- cls.ntp_client.delete_flavor, flavor_id)
- return flavor_id
-
- @classmethod
- def create_service_profile(cls):
- service_profile = cls.ntp_client.create_service_profile(
- metainfo=json.dumps({'foo': 'bar'}))
- service_profile_id = service_profile["service_profile"]["id"]
- cls.addClassResourceCleanup(
- test_utils.call_and_ignore_notfound_exc,
- cls.ntp_client.delete_service_profile, service_profile_id)
- return service_profile_id
-
- def create_flavor_service_profile(self, flavor_id, service_profile_id):
- self.ntp_client.create_flavor_service_profile(
- flavor_id, service_profile_id)
- self.addCleanup(
- test_utils.call_and_ignore_notfound_exc,
- self.ntp_client.delete_flavor_service_profile,
- flavor_id, service_profile_id)
-
- @decorators.idempotent_id('aa84b4c5-0dd6-4c34-aa81-3a76507f9b81')
- @rbac_rule_validation.action(service="neutron",
- rules=["create_flavor_service_profile"])
- def test_create_flavor_service_profile(self):
- """Create flavor_service_profile.
-
- RBAC test for the neutron "create_flavor_service_profile" policy
- """
- with self.rbac_utils.override_role(self):
- self.create_flavor_service_profile(self.flavor_id,
- self.service_profile_id)
-
- @decorators.idempotent_id('3b680d9e-946a-4670-ab7f-0e4576675833')
- @rbac_rule_validation.action(service="neutron",
- rules=["get_flavor_service_profile",
- "delete_flavor_service_profile"],
- expected_error_codes=[404, 403])
- def test_delete_flavor_service_profile(self):
- """Delete flavor_service_profile.
-
- RBAC test for the neutron "delete_flavor_service_profile" policy
- """
- self.create_flavor_service_profile(self.flavor_id,
- self.service_profile_id)
-
- with self.rbac_utils.override_role(self):
- self.ntp_client.delete_flavor_service_profile(
- self.flavor_id, self.service_profile_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py b/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py
new file mode 100644
index 0000000..9e82835
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py
@@ -0,0 +1,73 @@
+# Copyright 2018 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.
+
+from tempest.lib import decorators
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class ServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
+ @decorators.idempotent_id('6ce76efa-7400-44c1-80ec-58f79b1d89ca')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_service_profile"])
+ def test_create_service_profile(self):
+ """Create service profile
+
+ RBAC test for the neutron "create_service_profile" policy
+ """
+ with self.rbac_utils.override_role(self):
+ self.create_service_profile()
+
+ @decorators.idempotent_id('e4c473b7-3ae9-4a2e-8cac-848f7b01187d')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_service_profile"],
+ expected_error_codes=[404])
+ def test_show_service_profile(self):
+ """Show service profile
+
+ RBAC test for the neutron "get_service_profile" policy
+ """
+ profile_id = self.create_service_profile()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.show_service_profile(profile_id)
+
+ @decorators.idempotent_id('a3dd719d-4cd3-40cc-b4f1-5642e2717adf')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_service_profile",
+ "update_service_profile"],
+ expected_error_codes=[404, 403])
+ def test_update_service_profile(self):
+ """Update service profile
+
+ RBAC test for the neutron "update_service_profile" policy
+ """
+ profile_id = self.create_service_profile()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.update_service_profile(profile_id, enabled=False)
+
+ @decorators.idempotent_id('926b60c2-04fe-4339-aa44-bf27121392e8')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_service_profile",
+ "delete_service_profile"],
+ expected_error_codes=[404, 403])
+ def test_delete_service_profile(self):
+ """Delete service profile
+
+ RBAC test for the neutron "delete_service_profile" policy
+ """
+ profile_id = self.create_service_profile()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_service_profile(profile_id)
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
index 9e547b8..73a34fc 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -641,6 +641,7 @@
m_authority.allowed.assert_has_calls([
mock.call(rule, CONF.patrole.rbac_test_roles) for rule in rules
])
+ m_authority.allowed.reset_mock()
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rule_validation_multi_policy_have_permission_success(
@@ -826,6 +827,44 @@
_do_test([True, False, False, True], 'mock.sentinel.action2')
_do_test([True, False, True, False], 'mock.sentinel.action2')
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_multi_policy_defaults_to_correct_error_codes(
+ self, mock_authority, mock_log):
+ """Test omission of expected_error_codes defaults to [403] * len(rules)
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.\
+ return_value = False
+ expected_log = "%s: Expecting %d to be raised for policy name: %s"
+
+ # Validate with single rule => expected_error_codes == [403].
+ rules = [mock.sentinel.action1]
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules)
+ def test_policy(*args):
+ raise exceptions.Forbidden()
+
+ test_policy(self.mock_test_args)
+ self._assert_policy_authority_called_with(rules, mock_authority)
+ # Assert that 403 is expected.
+ mock_calls = [x[1] for x in mock_log.debug.mock_calls]
+ self.assertTrue(
+ any([(expected_log, 'test_policy', 403, rules[0]) in mock_calls]))
+
+ # Validate with multiple rules => expected_error_codes == [403, 403].
+ rules = [mock.sentinel.action1, mock.sentinel.action2]
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules)
+ def test_policy(*args):
+ raise exceptions.Forbidden()
+
+ test_policy(self.mock_test_args)
+ self._assert_policy_authority_called_with(rules, mock_authority)
+ # Assert that 403 is expected.
+ mock_calls = [x[1] for x in mock_log.debug.mock_calls]
+ self.assertTrue(
+ any([(expected_log, 'test_policy', 403, rules[0]) in mock_calls]))
+
def test_prepare_multi_policy_allowed_usages(self):
def _do_test(rules, ecodes, exp_rules, exp_ecodes):
diff --git a/tox.ini b/tox.ini
index bc829d2..20a7779 100644
--- a/tox.ini
+++ b/tox.ini
@@ -60,7 +60,7 @@
-r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands =
- rm -rf doc/build
+ rm -rf doc/build doc/source/framework/code
sphinx-build -W -b html doc/source doc/build/html
whitelist_externals = rm