diff options
-rw-r--r-- | doc/source/cors.rst | 35 | ||||
-rw-r--r-- | doc/source/oslo_config.rst | 4 | ||||
-rw-r--r-- | oslo/__init__.py | 13 | ||||
-rw-r--r-- | oslo/middleware/__init__.py | 28 | ||||
-rw-r--r-- | oslo/middleware/base.py | 13 | ||||
-rw-r--r-- | oslo/middleware/catch_errors.py | 13 | ||||
-rw-r--r-- | oslo/middleware/correlation_id.py | 13 | ||||
-rw-r--r-- | oslo/middleware/debug.py | 13 | ||||
-rw-r--r-- | oslo/middleware/request_id.py | 13 | ||||
-rw-r--r-- | oslo/middleware/sizelimit.py | 13 | ||||
-rw-r--r-- | oslo_middleware/base.py | 10 | ||||
-rw-r--r-- | oslo_middleware/cors.py | 50 | ||||
-rw-r--r-- | oslo_middleware/tests/test_cors.py | 92 | ||||
-rw-r--r-- | requirements.txt | 4 | ||||
-rw-r--r-- | setup.cfg | 5 | ||||
-rw-r--r-- | tests/__init__.py | 0 | ||||
-rw-r--r-- | tests/test_catch_errors.py | 47 | ||||
-rw-r--r-- | tests/test_correlation_id.py | 53 | ||||
-rw-r--r-- | tests/test_request_id.py | 37 | ||||
-rw-r--r-- | tests/test_sizelimit.py | 108 | ||||
-rw-r--r-- | tests/test_warning.py | 61 | ||||
-rw-r--r-- | tox.ini | 14 |
22 files changed, 153 insertions, 486 deletions
diff --git a/doc/source/cors.rst b/doc/source/cors.rst index 890f7fd..99e5dc0 100644 --- a/doc/source/cors.rst +++ b/doc/source/cors.rst @@ -49,18 +49,19 @@ In your application's config file, then include a default configuration block something like this:: [cors] - allowed_origin=https://website.example.com:443 + allowed_origin=https://website.example.com:443,https://website2.example.com:443 max_age=3600 allow_methods=GET,POST,PUT,DELETE allow_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header expose_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header -This middleware permits you to define multiple `allowed_origin`'s. To express -this in your configuration file, first begin with a `[cors]` group as above, -into which you place your default configuration values. Then add as many -additional configuration groups as necessary, naming them `[cors.something]` -(each name must be unique). The purpose of the suffix to `cors.` is -legibility, we recommend using a reasonable human-readable string:: +This middleware permits you to override the rules for multiple +`allowed_origin`'s. To express this in your configuration file, first begin +with a `[cors]` group as above, into which you place your default +configuration values. Then add as many additional configuration groups as +necessary, naming them `[cors.something]` (each name must be unique). The +purpose of the suffix to `cors.` is legibility, we recommend using a +reasonable human-readable string:: [cors.ironic_webclient] # CORS Configuration for a hypothetical ironic webclient, which overrides @@ -94,16 +95,32 @@ Configuration for pastedeploy ----------------------------- If your application is using pastedeploy, the following configuration block -will add CORS support. To add multiple domains, simply add another filter.:: +will add CORS support.:: [filter:cors] paste.filter_factory = oslo_middleware.cors:filter_factory - allowed_origin=https://website.example.com:443 + allowed_origin=https://website.example.com:443,https://website2.example.com:443 max_age=3600 allow_methods=GET,POST,PUT,DELETE allow_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header expose_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header +If your application is using pastedeploy, but would also like to use the +existing configuration from oslo_config in order to simplify the points of +configuration, this may be done as follows.:: + + [filter:cors] + paste.filter_factory = oslo_middleware.cors:filter_factory + oslo_config_project = oslo_project_name + + # Optional field, in case the program name is different from the project: + oslo_config_program = oslo_project_name-api + + # This method also permits setting latent properties, for any origins set + # in oslo config. + latent_allow_headers=X-Auth-Token + latent_expose_headers=X-Auth-Token + latent_methods=GET,PUT,POST Configuration Options --------------------- diff --git a/doc/source/oslo_config.rst b/doc/source/oslo_config.rst index 09c83c6..6e772c6 100644 --- a/doc/source/oslo_config.rst +++ b/doc/source/oslo_config.rst @@ -29,6 +29,10 @@ The paste filter (in /etc/my_app/api-paste.ini) will looks like:: # the application configuration, by setting this: # oslo_config_project = my_app + # In some cases, you may need to specify the program name for the project + # as well. + # oslo_config_program = my_app-api + The oslo.config file of the application (eg: /etc/my_app/my_app.conf) will looks like:: [oslo_middleware] diff --git a/oslo/__init__.py b/oslo/__init__.py deleted file mode 100644 index dc130d6..0000000 --- a/oslo/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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__('pkg_resources').declare_namespace(__name__) diff --git a/oslo/middleware/__init__.py b/oslo/middleware/__init__.py deleted file mode 100644 index 1407ce1..0000000 --- a/oslo/middleware/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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 warnings - -from oslo_middleware import * - - -def deprecated(): - new_name = __name__.replace('.', '_') - warnings.warn( - ('The oslo namespace package is deprecated. Please use %s instead.' % - new_name), - DeprecationWarning, - stacklevel=3, - ) - - -deprecated() diff --git a/oslo/middleware/base.py b/oslo/middleware/base.py deleted file mode 100644 index 53e25e9..0000000 --- a/oslo/middleware/base.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 oslo_middleware.base import * # noqa diff --git a/oslo/middleware/catch_errors.py b/oslo/middleware/catch_errors.py deleted file mode 100644 index 81e4c6c..0000000 --- a/oslo/middleware/catch_errors.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 oslo_middleware.catch_errors import * # noqa diff --git a/oslo/middleware/correlation_id.py b/oslo/middleware/correlation_id.py deleted file mode 100644 index fff548c..0000000 --- a/oslo/middleware/correlation_id.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 oslo_middleware.correlation_id import * # noqa diff --git a/oslo/middleware/debug.py b/oslo/middleware/debug.py deleted file mode 100644 index 2907289..0000000 --- a/oslo/middleware/debug.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 oslo_middleware.debug import * # noqa diff --git a/oslo/middleware/request_id.py b/oslo/middleware/request_id.py deleted file mode 100644 index 81e3164..0000000 --- a/oslo/middleware/request_id.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 oslo_middleware.request_id import * # noqa diff --git a/oslo/middleware/sizelimit.py b/oslo/middleware/sizelimit.py deleted file mode 100644 index c04c1cd..0000000 --- a/oslo/middleware/sizelimit.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 oslo_middleware.sizelimit import * # noqa diff --git a/oslo_middleware/base.py b/oslo_middleware/base.py index 86f2249..a98a058 100644 --- a/oslo_middleware/base.py +++ b/oslo_middleware/base.py @@ -67,8 +67,16 @@ class ConfigurableMiddleware(object): default_config_files = [self.conf['oslo_config_file']] else: default_config_files = None + + if 'oslo_config_program' in self.conf: + program = self.conf['oslo_config_program'] + else: + program = None + self.oslo_conf = cfg.ConfigOpts() - self.oslo_conf([], project=self.conf['oslo_config_project'], + self.oslo_conf([], + project=self.conf['oslo_config_project'], + prog=program, default_config_files=default_config_files, validate_default_values=True) diff --git a/oslo_middleware/cors.py b/oslo_middleware/cors.py index 65d7be0..ac86bc1 100644 --- a/oslo_middleware/cors.py +++ b/oslo_middleware/cors.py @@ -25,10 +25,10 @@ import webob.response LOG = logging.getLogger(__name__) CORS_OPTS = [ - cfg.StrOpt('allowed_origin', - default=None, - help='Indicate whether this resource may be shared with the ' - 'domain received in the requests "origin" header.'), + cfg.ListOpt('allowed_origin', + default=None, + help='Indicate whether this resource may be shared with the ' + 'domain received in the requests "origin" header.'), cfg.BoolOpt('allow_credentials', default=True, help='Indicate that the actual request can include user ' @@ -55,6 +55,7 @@ CORS_OPTS = [ class InvalidOriginError(Exception): """Exception raised when Origin is invalid.""" + def __init__(self, origin): self.origin = origin super(InvalidOriginError, self).__init__( @@ -87,6 +88,18 @@ class CORS(base.ConfigurableMiddleware): self.allowed_origins = {} self._init_conf() + def sanitize(csv_list): + try: + return [str.strip(x) for x in csv_list.split(',')] + except Exception: + return None + + self.set_latent( + allow_headers=sanitize(self.conf.get('latent_allow_headers')), + expose_headers=sanitize(self.conf.get('latent_expose_headers')), + allow_methods=sanitize(self.conf.get('latent_allow_methods')) + ) + @classmethod def factory(cls, global_conf, **local_conf): """factory method for paste.deploy @@ -99,7 +112,7 @@ class CORS(base.ConfigurableMiddleware): allow_headers: List of HTTP headers to permit from the client. """ if ('allowed_origin' not in local_conf - and 'oslo_config_project' not in local_conf): + and 'oslo_config_project' not in local_conf): raise TypeError("allowed_origin or oslo_config_project " "is required") return super(CORS, cls).factory(global_conf, **local_conf) @@ -167,19 +180,20 @@ class CORS(base.ConfigurableMiddleware): :param allow_headers: List of HTTP headers to permit from the client. :return: ''' - - if allowed_origin in self.allowed_origins: - LOG.warn('Allowed origin [%s] already exists, skipping' % ( - allowed_origin,)) - return - - self.allowed_origins[allowed_origin] = { - 'allow_credentials': allow_credentials, - 'expose_headers': expose_headers, - 'max_age': max_age, - 'allow_methods': allow_methods, - 'allow_headers': allow_headers - } + for origin in allowed_origin: + + if origin in self.allowed_origins: + LOG.warn('Allowed origin [%s] already exists, skipping' % ( + allowed_origin,)) + continue + + self.allowed_origins[origin] = { + 'allow_credentials': allow_credentials, + 'expose_headers': expose_headers, + 'max_age': max_age, + 'allow_methods': allow_methods, + 'allow_headers': allow_headers + } def set_latent(self, allow_headers=None, allow_methods=None, expose_headers=None): diff --git a/oslo_middleware/tests/test_cors.py b/oslo_middleware/tests/test_cors.py index d1c1885..e47c5d8 100644 --- a/oslo_middleware/tests/test_cors.py +++ b/oslo_middleware/tests/test_cors.py @@ -136,6 +136,18 @@ class CORSTestFilterFactory(test_base.BaseTestCase): self.assertEqual(['GET'], config['allow_methods']) self.assertEqual([], config['allow_headers']) + def test_filter_factory_multiorigin(self): + self.useFixture(fixture.Config()).conf([]) + + # Test a valid filter. + filter = cors.filter_factory(None, + allowed_origin='http://valid.example.com,' + 'http://other.example.com') + application = filter(test_application) + + self.assertIn('http://valid.example.com', application.allowed_origins) + self.assertIn('http://other.example.com', application.allowed_origins) + def test_no_origin_fail(self): '''Assert that a filter factory with no allowed_origin fails.''' self.assertRaises(TypeError, @@ -152,6 +164,29 @@ class CORSTestFilterFactory(test_base.BaseTestCase): '''Assert that a filter factory with oslo_config_project succeed.''' cors.filter_factory(global_conf=None, oslo_config_project='foobar') + def test_factory_latent_properties(self): + '''Assert latent properties in paste.ini config. + + If latent_* properties are added to a paste.ini config, assert that + they are persisted in the middleware. + ''' + + # Spaces in config are deliberate to frobb the config parsing. + filter = cors.filter_factory(global_conf=None, + oslo_config_project='foobar', + latent_expose_headers=' X-Header-1 , X-2', + latent_allow_headers='X-Header-1 , X-2', + latent_allow_methods='GET,PUT, POST') + app = filter(test_application) + + # Ensure that the properties are in latent configuration. + self.assertEqual(['X-Header-1', 'X-2'], + app._latent_configuration['expose_headers']) + self.assertEqual(['X-Header-1', 'X-2'], + app._latent_configuration['allow_headers']) + self.assertEqual(['GET', 'PUT', 'POST'], + app._latent_configuration['methods']) + class CORSRegularRequestTest(CORSTestBase): """CORS Specification Section 6.1 @@ -197,6 +232,10 @@ class CORSRegularRequestTest(CORSTestBase): allowed_origin='http://all.example.com', allow_methods='GET,PUT,POST,DELETE,HEAD') + config.load_raw_values(group='cors.duplicate', + allowed_origin='http://domain1.example.com,' + 'http://domain2.example.com') + # Now that the config is set up, create our application. self.application = cors.CORS(test_application, cfg.CONF) @@ -205,7 +244,7 @@ class CORSRegularRequestTest(CORSTestBase): # Confirm global configuration gc = cfg.CONF.cors - self.assertEqual(gc.allowed_origin, 'http://valid.example.com') + self.assertEqual(gc.allowed_origin, ['http://valid.example.com']) self.assertEqual(gc.allow_credentials, False) self.assertEqual(gc.expose_headers, []) self.assertEqual(gc.max_age, None) @@ -214,7 +253,7 @@ class CORSRegularRequestTest(CORSTestBase): # Confirm credentials overrides. cc = cfg.CONF['cors.credentials'] - self.assertEqual(cc.allowed_origin, 'http://creds.example.com') + self.assertEqual(cc.allowed_origin, ['http://creds.example.com']) self.assertEqual(cc.allow_credentials, True) self.assertEqual(cc.expose_headers, gc.expose_headers) self.assertEqual(cc.max_age, gc.max_age) @@ -223,7 +262,7 @@ class CORSRegularRequestTest(CORSTestBase): # Confirm exposed-headers overrides. ec = cfg.CONF['cors.exposed-headers'] - self.assertEqual(ec.allowed_origin, 'http://headers.example.com') + self.assertEqual(ec.allowed_origin, ['http://headers.example.com']) self.assertEqual(ec.allow_credentials, gc.allow_credentials) self.assertEqual(ec.expose_headers, ['X-Header-1', 'X-Header-2']) self.assertEqual(ec.max_age, gc.max_age) @@ -232,7 +271,7 @@ class CORSRegularRequestTest(CORSTestBase): # Confirm cached overrides. chc = cfg.CONF['cors.cached'] - self.assertEqual(chc.allowed_origin, 'http://cached.example.com') + self.assertEqual(chc.allowed_origin, ['http://cached.example.com']) self.assertEqual(chc.allow_credentials, gc.allow_credentials) self.assertEqual(chc.expose_headers, gc.expose_headers) self.assertEqual(chc.max_age, 3600) @@ -241,7 +280,7 @@ class CORSRegularRequestTest(CORSTestBase): # Confirm get-only overrides. goc = cfg.CONF['cors.get-only'] - self.assertEqual(goc.allowed_origin, 'http://get.example.com') + self.assertEqual(goc.allowed_origin, ['http://get.example.com']) self.assertEqual(goc.allow_credentials, gc.allow_credentials) self.assertEqual(goc.expose_headers, gc.expose_headers) self.assertEqual(goc.max_age, gc.max_age) @@ -250,7 +289,7 @@ class CORSRegularRequestTest(CORSTestBase): # Confirm all-methods overrides. ac = cfg.CONF['cors.all-methods'] - self.assertEqual(ac.allowed_origin, 'http://all.example.com') + self.assertEqual(ac.allowed_origin, ['http://all.example.com']) self.assertEqual(ac.allow_credentials, gc.allow_credentials) self.assertEqual(ac.expose_headers, gc.expose_headers) self.assertEqual(ac.max_age, gc.max_age) @@ -258,6 +297,16 @@ class CORSRegularRequestTest(CORSTestBase): ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']) self.assertEqual(ac.allow_headers, gc.allow_headers) + # Confirm duplicate domains. + ac = cfg.CONF['cors.duplicate'] + self.assertEqual(ac.allowed_origin, ['http://domain1.example.com', + 'http://domain2.example.com']) + self.assertEqual(ac.allow_credentials, gc.allow_credentials) + self.assertEqual(ac.expose_headers, gc.expose_headers) + self.assertEqual(ac.max_age, gc.max_age) + self.assertEqual(ac.allow_methods, gc.allow_methods) + self.assertEqual(ac.allow_headers, gc.allow_headers) + def test_no_origin_header(self): """CORS Specification Section 6.1.1 @@ -329,6 +378,21 @@ class CORSRegularRequestTest(CORSTestBase): allow_credentials=None, expose_headers=None) + # Test valid header from list of duplicates. + for method in self.methods: + request = webob.Request.blank('/') + request.method = method + request.headers['Origin'] = 'http://domain2.example.com' + response = request.get_response(self.application) + self.assertCORSResponse(response, + status='200 OK', + allow_origin='http://domain2.example.com', + max_age=None, + allow_methods=None, + allow_headers=None, + allow_credentials=None, + expose_headers=None) + def test_supports_credentials(self): """CORS Specification Section 6.1.3 @@ -465,7 +529,7 @@ class CORSPreflightRequestTest(CORSTestBase): # Confirm global configuration gc = cfg.CONF.cors - self.assertEqual(gc.allowed_origin, 'http://valid.example.com') + self.assertEqual(gc.allowed_origin, ['http://valid.example.com']) self.assertEqual(gc.allow_credentials, False) self.assertEqual(gc.expose_headers, []) self.assertEqual(gc.max_age, None) @@ -474,7 +538,7 @@ class CORSPreflightRequestTest(CORSTestBase): # Confirm credentials overrides. cc = cfg.CONF['cors.credentials'] - self.assertEqual(cc.allowed_origin, 'http://creds.example.com') + self.assertEqual(cc.allowed_origin, ['http://creds.example.com']) self.assertEqual(cc.allow_credentials, True) self.assertEqual(cc.expose_headers, gc.expose_headers) self.assertEqual(cc.max_age, gc.max_age) @@ -483,7 +547,7 @@ class CORSPreflightRequestTest(CORSTestBase): # Confirm exposed-headers overrides. ec = cfg.CONF['cors.exposed-headers'] - self.assertEqual(ec.allowed_origin, 'http://headers.example.com') + self.assertEqual(ec.allowed_origin, ['http://headers.example.com']) self.assertEqual(ec.allow_credentials, gc.allow_credentials) self.assertEqual(ec.expose_headers, ['X-Header-1', 'X-Header-2']) self.assertEqual(ec.max_age, gc.max_age) @@ -492,7 +556,7 @@ class CORSPreflightRequestTest(CORSTestBase): # Confirm cached overrides. chc = cfg.CONF['cors.cached'] - self.assertEqual(chc.allowed_origin, 'http://cached.example.com') + self.assertEqual(chc.allowed_origin, ['http://cached.example.com']) self.assertEqual(chc.allow_credentials, gc.allow_credentials) self.assertEqual(chc.expose_headers, gc.expose_headers) self.assertEqual(chc.max_age, 3600) @@ -501,7 +565,7 @@ class CORSPreflightRequestTest(CORSTestBase): # Confirm get-only overrides. goc = cfg.CONF['cors.get-only'] - self.assertEqual(goc.allowed_origin, 'http://get.example.com') + self.assertEqual(goc.allowed_origin, ['http://get.example.com']) self.assertEqual(goc.allow_credentials, gc.allow_credentials) self.assertEqual(goc.expose_headers, gc.expose_headers) self.assertEqual(goc.max_age, gc.max_age) @@ -510,7 +574,7 @@ class CORSPreflightRequestTest(CORSTestBase): # Confirm all-methods overrides. ac = cfg.CONF['cors.all-methods'] - self.assertEqual(ac.allowed_origin, 'http://all.example.com') + self.assertEqual(ac.allowed_origin, ['http://all.example.com']) self.assertEqual(ac.allow_credentials, gc.allow_credentials) self.assertEqual(ac.expose_headers, gc.expose_headers) self.assertEqual(ac.max_age, gc.max_age) @@ -983,7 +1047,7 @@ class CORSTestWildcard(CORSTestBase): # Confirm global configuration gc = cfg.CONF.cors - self.assertEqual(gc.allowed_origin, 'http://default.example.com') + self.assertEqual(gc.allowed_origin, ['http://default.example.com']) self.assertEqual(gc.allow_credentials, True) self.assertEqual(gc.expose_headers, []) self.assertEqual(gc.max_age, None) @@ -993,7 +1057,7 @@ class CORSTestWildcard(CORSTestBase): # Confirm all-methods overrides. ac = cfg.CONF['cors.wildcard'] - self.assertEqual(ac.allowed_origin, '*') + self.assertEqual(ac.allowed_origin, ['*']) self.assertEqual(gc.allow_credentials, True) self.assertEqual(ac.expose_headers, gc.expose_headers) self.assertEqual(ac.max_age, gc.max_age) diff --git a/requirements.txt b/requirements.txt index 464eb56..dcaba68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,12 +4,12 @@ pbr>=1.6 Babel>=1.3 -Jinja2>=2.6 # BSD License (3 clause) +Jinja2>=2.8 # BSD License (3 clause) ordereddict oslo.config>=2.6.0 # Apache-2.0 oslo.context>=0.2.0 # Apache-2.0 oslo.i18n>=1.5.0 # Apache-2.0 -oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 +oslo.utils>=2.8.0 # Apache-2.0 six>=1.9.0 stevedore>=1.5.0 # Apache-2.0 WebOb>=1.2.3 @@ -15,17 +15,12 @@ classifier = Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 [files] packages = - oslo - oslo.middleware oslo_middleware -namespace_packages = - oslo [entry_points] oslo.config.opts = diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/tests/__init__.py +++ /dev/null diff --git a/tests/test_catch_errors.py b/tests/test_catch_errors.py deleted file mode 100644 index 9e71f5b..0000000 --- a/tests/test_catch_errors.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2013 NEC 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. - -import mock -from oslotest import base as test_base -import webob.dec -import webob.exc - -from oslo.middleware import catch_errors - - -class CatchErrorsTest(test_base.BaseTestCase): - - def _test_has_request_id(self, application, expected_code=None): - app = catch_errors.CatchErrors(application) - req = webob.Request.blank('/test') - res = req.get_response(app) - self.assertEqual(expected_code, res.status_int) - - def test_success_response(self): - @webob.dec.wsgify - def application(req): - return 'Hello, World!!!' - - self._test_has_request_id(application, webob.exc.HTTPOk.code) - - def test_internal_server_error(self): - @webob.dec.wsgify - def application(req): - raise Exception() - - with mock.patch.object(catch_errors.LOG, 'exception') as log_exc: - self._test_has_request_id(application, - webob.exc.HTTPInternalServerError.code) - self.assertEqual(1, log_exc.call_count) diff --git a/tests/test_correlation_id.py b/tests/test_correlation_id.py deleted file mode 100644 index b927e1d..0000000 --- a/tests/test_correlation_id.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2013 Rackspace Hosting -# 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 uuid - -import mock -from oslotest import base as test_base -from oslotest import moxstubout - -from oslo.middleware import correlation_id - - -class CorrelationIdTest(test_base.BaseTestCase): - - def setUp(self): - super(CorrelationIdTest, self).setUp() - self.stubs = self.useFixture(moxstubout.MoxStubout()).stubs - - def test_process_request(self): - app = mock.Mock() - req = mock.Mock() - req.headers = {} - - mock_uuid4 = mock.Mock() - mock_uuid4.return_value = "fake_uuid" - self.stubs.Set(uuid, 'uuid4', mock_uuid4) - - middleware = correlation_id.CorrelationId(app) - middleware(req) - - self.assertEqual(req.headers.get("X_CORRELATION_ID"), "fake_uuid") - - def test_process_request_should_not_regenerate_correlation_id(self): - app = mock.Mock() - req = mock.Mock() - req.headers = {"X_CORRELATION_ID": "correlation_id"} - - middleware = correlation_id.CorrelationId(app) - middleware(req) - - self.assertEqual(req.headers.get("X_CORRELATION_ID"), "correlation_id") diff --git a/tests/test_request_id.py b/tests/test_request_id.py deleted file mode 100644 index 549d7be..0000000 --- a/tests/test_request_id.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) 2013 NEC 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 oslotest import base as test_base -from testtools import matchers -import webob -import webob.dec - -from oslo.middleware import request_id - - -class RequestIdTest(test_base.BaseTestCase): - def test_generate_request_id(self): - @webob.dec.wsgify - def application(req): - return req.environ[request_id.ENV_REQUEST_ID] - - app = request_id.RequestId(application) - req = webob.Request.blank('/test') - res = req.get_response(app) - res_req_id = res.headers.get(request_id.HTTP_RESP_HEADER_REQUEST_ID) - self.assertThat(res_req_id, matchers.StartsWith(b'req-')) - # request-id in request environ is returned as response body - self.assertEqual(res_req_id, res.body) diff --git a/tests/test_sizelimit.py b/tests/test_sizelimit.py deleted file mode 100644 index b985f76..0000000 --- a/tests/test_sizelimit.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) 2012 Red Hat, Inc. -# -# 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 oslotest import base as test_base -import six -import webob - -from oslo.middleware import sizelimit -from oslo_config import fixture as config - - -class TestLimitingReader(test_base.BaseTestCase): - - def test_limiting_reader(self): - BYTES = 1024 - bytes_read = 0 - data = six.StringIO("*" * BYTES) - for chunk in sizelimit.LimitingReader(data, BYTES): - bytes_read += len(chunk) - - self.assertEqual(bytes_read, BYTES) - - bytes_read = 0 - data = six.StringIO("*" * BYTES) - reader = sizelimit.LimitingReader(data, BYTES) - byte = reader.read(1) - while len(byte) != 0: - bytes_read += 1 - byte = reader.read(1) - - self.assertEqual(bytes_read, BYTES) - - def test_read_default_value(self): - BYTES = 1024 - data_str = "*" * BYTES - data = six.StringIO(data_str) - reader = sizelimit.LimitingReader(data, BYTES) - res = reader.read() - self.assertEqual(data_str, res) - - def test_limiting_reader_fails(self): - BYTES = 1024 - - def _consume_all_iter(): - bytes_read = 0 - data = six.StringIO("*" * BYTES) - for chunk in sizelimit.LimitingReader(data, BYTES - 1): - bytes_read += len(chunk) - - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - _consume_all_iter) - - def _consume_all_read(): - bytes_read = 0 - data = six.StringIO("*" * BYTES) - reader = sizelimit.LimitingReader(data, BYTES - 1) - byte = reader.read(1) - while len(byte) != 0: - bytes_read += 1 - byte = reader.read(1) - - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - _consume_all_read) - - -class TestRequestBodySizeLimiter(test_base.BaseTestCase): - - def setUp(self): - super(TestRequestBodySizeLimiter, self).setUp() - self.useFixture(config.Config()) - - @webob.dec.wsgify() - def fake_app(req): - return webob.Response(req.body) - - self.middleware = sizelimit.RequestBodySizeLimiter(fake_app) - self.MAX_REQUEST_BODY_SIZE = ( - self.middleware.oslo_conf.oslo_middleware.max_request_body_size) - self.request = webob.Request.blank('/', method='POST') - - def test_content_length_acceptable(self): - self.request.headers['Content-Length'] = self.MAX_REQUEST_BODY_SIZE - self.request.body = b"0" * self.MAX_REQUEST_BODY_SIZE - response = self.request.get_response(self.middleware) - self.assertEqual(response.status_int, 200) - - def test_content_length_too_large(self): - self.request.headers['Content-Length'] = self.MAX_REQUEST_BODY_SIZE + 1 - self.request.body = b"0" * (self.MAX_REQUEST_BODY_SIZE + 1) - response = self.request.get_response(self.middleware) - self.assertEqual(response.status_int, 413) - - def test_request_too_large_no_content_length(self): - self.request.body = b"0" * (self.MAX_REQUEST_BODY_SIZE + 1) - self.request.headers['Content-Length'] = None - response = self.request.get_response(self.middleware) - self.assertEqual(response.status_int, 413) diff --git a/tests/test_warning.py b/tests/test_warning.py deleted file mode 100644 index 8e7d96c..0000000 --- a/tests/test_warning.py +++ /dev/null @@ -1,61 +0,0 @@ -# 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 imp -import os -import warnings - -import mock -from oslotest import base as test_base -import six - - -class DeprecationWarningTest(test_base.BaseTestCase): - - @mock.patch('warnings.warn') - def test_warning(self, mock_warn): - import oslo.middleware - imp.reload(oslo.middleware) - self.assertTrue(mock_warn.called) - args = mock_warn.call_args - self.assertIn('oslo_middleware', args[0][0]) - self.assertIn('deprecated', args[0][0]) - self.assertTrue(issubclass(args[0][1], DeprecationWarning)) - - def test_real_warning(self): - with warnings.catch_warnings(record=True) as warning_msgs: - warnings.resetwarnings() - warnings.simplefilter('always', DeprecationWarning) - import oslo.middleware - - # Use a separate function to get the stack level correct - # so we know the message points back to this file. This - # corresponds to an import or reload, which isn't working - # inside the test under Python 3.3. That may be due to a - # difference in the import implementation not triggering - # warnings properly when the module is reloaded, or - # because the warnings module is mostly implemented in C - # and something isn't cleanly resetting the global state - # used to track whether a warning needs to be - # emitted. Whatever the cause, we definitely see the - # warnings.warn() being invoked on a reload (see the test - # above) and warnings are reported on the console when we - # run the tests. A simpler test script run outside of - # testr does correctly report the warnings. - def foo(): - oslo.middleware.deprecated() - - foo() - self.assertEqual(1, len(warning_msgs)) - msg = warning_msgs[0] - self.assertIn('oslo_middleware', six.text_type(msg.message)) - self.assertEqual('test_warning.py', os.path.basename(msg.filename)) @@ -1,19 +1,9 @@ [tox] minversion = 1.6 -envlist = py34,py26,py27,pypy,pep8 -# NOTE(dhellmann): We cannot set skipdist=True -# for oslo libraries because of the namespace package. -#skipsdist = True +envlist = py34,py27,pypy,pep8 [testenv] -# NOTE(dhellmann): We cannot set usedevelop=True -# for oslo libraries because of the namespace package. -#usedevelop = True -install_command = pip install -U {opts} {packages} -setenv = - VIRTUAL_ENV={envdir} -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +deps = -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:pep8] |