Merge tag '0.2.1' into debian/rocky
tag for 0.2.1 release
Ondřej Nový
5 years ago
0 | [DEFAULT] | |
1 | test_path=./microversion_parse/tests | |
2 | top_dir=./ | |
3 | # This regex ensures each yaml file used by gabbi is run in only one | |
4 | # process. | |
5 | group_regex=microversion_parse\.tests\.test_middleware(?:\.|_)([^_]+) |
0 | [DEFAULT] | |
1 | test_command=${PYTHON:-python} -m subunit.run discover -t . ${OS_TEST_PATH:-microversion_parse} $LISTOPT $IDOPTION | |
2 | test_id_option=--load-list $IDFILE | |
3 | test_list_option=--list |
0 | 0 | microversion_parse |
1 | 1 | ================== |
2 | ||
3 | A small set of functions to manage OpenStack `microversion`_ headers that can | |
4 | be used in middleware, application handlers and decorators to effectively | |
5 | manage microversions. | |
6 | ||
7 | Also included, in the ``middleware`` module, is a ``MicroversionMiddleware`` | |
8 | that will process incoming microversion headers. | |
9 | ||
10 | get_version | |
11 | ----------- | |
2 | 12 | |
3 | 13 | A simple parser for OpenStack microversion headers:: |
4 | 14 | |
10 | 20 | headers, service_type='compute', |
11 | 21 | legacy_headers=['x-openstack-nova-api-version']) |
12 | 22 | |
23 | # If headers are not already available, a dict of headers | |
24 | # can be extracted from the WSGI environ | |
25 | headers = microversion_parse.headers_from_wsgi_environ(environ) | |
26 | version = microversion_parse.get_version( | |
27 | headers, service_type='placement') | |
28 | ||
13 | 29 | It processes microversion headers with the standard form:: |
14 | 30 | |
15 | 31 | OpenStack-API-Version: compute 2.1 |
16 | 32 | |
33 | In that case, the response will be '2.1'. | |
34 | ||
17 | 35 | If provided with a ``legacy_headers`` argument, this is treated as |
18 | a list of headers to check for microversions. Some examples of | |
36 | a list of additional headers to check for microversions. Some examples of | |
19 | 37 | headers include:: |
20 | 38 | |
21 | 39 | OpenStack-telemetry-api-version: 2.1 |
25 | 43 | If a version string cannot be found, ``None`` will be returned. If |
26 | 44 | the input is incorrect usual Python exceptions (ValueError, |
27 | 45 | TypeError) are allowed to raise to the caller. |
46 | ||
47 | parse_version_string | |
48 | -------------------- | |
49 | ||
50 | A function to turn a version string into a ``Version``, a comparable | |
51 | ``namedtuple``:: | |
52 | ||
53 | version_tuple = microversion_parse.parse_version_string('2.1') | |
54 | ||
55 | If the provided string is not a valid microversion string, ``TypeError`` | |
56 | is raised. | |
57 | ||
58 | extract_version | |
59 | --------------- | |
60 | ||
61 | Combines ``get_version`` and ``parse_version_string`` to find and validate | |
62 | a microversion for a given service type in a collection of headers:: | |
63 | ||
64 | version_tuple = microversion_parse.extract_version( | |
65 | headers, # a representation of headers, as accepted by get_version | |
66 | service_type, # service type identify to match in headers | |
67 | versions_list, # an ordered list of strings of version numbers that | |
68 | # are the valid versions presented by this service | |
69 | ) | |
70 | ||
71 | ``latest`` will be translated to whatever the max version is in versions_list. | |
72 | ||
73 | If the found version is not in versions_list a ``ValueError`` is raised. | |
74 | ||
75 | Note that ``extract_version`` does not support ``legacy_headers``. | |
76 | ||
77 | MicroversionMiddleware | |
78 | ---------------------- | |
79 | ||
80 | A WSGI middleware that can wrap an application that needs to be microversion | |
81 | aware. The application will get a WSGI environ with a | |
82 | 'SERVICE_TYPE.microversion' key that has a value of the microversion found at | |
83 | an 'openstack-api-version' header that matches SERVICE_TYPE. If no header is | |
84 | found, the minimum microversion will be set. If the special keyword 'latest' is | |
85 | used, the maximum microversion will be set. | |
86 | ||
87 | If the requested microversion is not available a 406 response is returned. | |
88 | ||
89 | If there is an error parsing a provided header, a 400 response is returned. | |
90 | ||
91 | Otherwise the application is called. | |
92 | ||
93 | The middleware is configured when it is created. Three parameters are required: | |
94 | ||
95 | app | |
96 | The next WSGI middleware or application in the stack. | |
97 | ||
98 | service_type | |
99 | The service type of the application, used to identify microversion headers. | |
100 | ||
101 | versions_list | |
102 | An ordered list of legitimate microversions (as strings) for the application. | |
103 | It's assumed that any application that is using microversions will have such | |
104 | a list for its own housekeeping and documentation. | |
105 | ||
106 | One named parameter is optional: | |
107 | ||
108 | json_error_formatter | |
109 | A Webob error formatter that can be used to structure the response when JSON | |
110 | is expected. | |
111 | ||
112 | For example:: | |
113 | ||
114 | def app(): | |
115 | app = middleware.MicroversionMiddleware( | |
116 | MyWSGIApp(), 'cats', ['1.0', '1.1', '1.2']) | |
117 | return app | |
118 | ||
119 | ||
120 | .. _microversion: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html |
10 | 10 | # See the License for the specific language governing permissions and |
11 | 11 | # limitations under the License. |
12 | 12 | |
13 | __version__ = '0.1.1' | |
14 | ||
15 | 13 | import collections |
16 | 14 | |
17 | 15 | |
16 | ENVIRON_HTTP_HEADER_FMT = 'http_{}' | |
18 | 17 | STANDARD_HEADER = 'openstack-api-version' |
18 | ||
19 | ||
20 | class Version(collections.namedtuple('Version', 'major minor')): | |
21 | """A namedtuple containing major and minor values. | |
22 | ||
23 | Since it is a tuple, it is automatically comparable. | |
24 | """ | |
25 | ||
26 | def __new__(cls, major, minor): | |
27 | """Add mix and max version attributes to the tuple.""" | |
28 | self = super(Version, cls).__new__(cls, major, minor) | |
29 | self.max_version = (-1, 0) | |
30 | self.min_version = (-1, 0) | |
31 | return self | |
32 | ||
33 | def __str__(self): | |
34 | return '%s.%s' % (self.major, self.minor) | |
35 | ||
36 | def matches(self, min_version=None, max_version=None): | |
37 | """Is this version within min_version and max_version. | |
38 | """ | |
39 | # NOTE(cdent): min_version and max_version are expected | |
40 | # to be set by the code that is creating the Version, if | |
41 | # they are known. | |
42 | if min_version is None: | |
43 | min_version = self.min_version | |
44 | if max_version is None: | |
45 | max_version = self.max_version | |
46 | return min_version <= self <= max_version | |
19 | 47 | |
20 | 48 | |
21 | 49 | def get_version(headers, service_type, legacy_headers=None): |
62 | 90 | """Gather values from old headers.""" |
63 | 91 | for legacy_header in legacy_headers: |
64 | 92 | try: |
65 | value = headers[legacy_header.lower()] | |
93 | value = _extract_header_value(headers, legacy_header.lower()) | |
66 | 94 | return value.split(',')[-1].strip() |
67 | 95 | except KeyError: |
68 | 96 | pass |
72 | 100 | def check_standard_header(headers, service_type): |
73 | 101 | """Parse the standard header to get value for service.""" |
74 | 102 | try: |
75 | header = headers[STANDARD_HEADER] | |
103 | header = _extract_header_value(headers, STANDARD_HEADER) | |
76 | 104 | for header_value in reversed(header.split(',')): |
77 | 105 | try: |
78 | 106 | service, version = header_value.strip().split(None, 1) |
88 | 116 | """Turn a list of headers into a folded dict.""" |
89 | 117 | # If it behaves like a dict, return it. Webob uses objects which |
90 | 118 | # are not dicts, but behave like them. |
91 | if hasattr(headers, 'keys'): | |
119 | try: | |
92 | 120 | return dict((k.lower(), v) for k, v in headers.items()) |
121 | except AttributeError: | |
122 | pass | |
93 | 123 | header_dict = collections.defaultdict(list) |
94 | 124 | for header, value in headers: |
95 | 125 | header_dict[header.lower()].append(value.strip()) |
99 | 129 | folded_headers[header] = ','.join(value) |
100 | 130 | |
101 | 131 | return folded_headers |
132 | ||
133 | ||
134 | def headers_from_wsgi_environ(environ): | |
135 | """Extract all the HTTP_ keys and values from environ to a new dict. | |
136 | ||
137 | Note that this does not change the keys in any way in the returned | |
138 | dict. Nor is the incoming environ modified. | |
139 | ||
140 | :param environ: A PEP 3333 compliant WSGI environ dict. | |
141 | """ | |
142 | return {key: environ[key] for key in environ if key.startswith('HTTP_')} | |
143 | ||
144 | ||
145 | def _extract_header_value(headers, header_name): | |
146 | """Get the value of a header. | |
147 | ||
148 | The provided headers is a dict. If a key doesn't exist for | |
149 | header_name, try using the WSGI environ form of the name. | |
150 | ||
151 | Raises KeyError if neither key is found. | |
152 | """ | |
153 | try: | |
154 | value = headers[header_name] | |
155 | except KeyError: | |
156 | wsgi_header_name = ENVIRON_HTTP_HEADER_FMT.format( | |
157 | header_name.replace('-', '_')) | |
158 | value = headers[wsgi_header_name] | |
159 | return value | |
160 | ||
161 | ||
162 | def parse_version_string(version_string): | |
163 | """Turn a version string into a Version | |
164 | ||
165 | :param version_string: A string of two numerals, X.Y. | |
166 | :returns: a Version | |
167 | :raises: TypeError | |
168 | """ | |
169 | try: | |
170 | # The combination of int and a limited split with the | |
171 | # named tuple means that this incantation will raise | |
172 | # ValueError, TypeError or AttributeError when the incoming | |
173 | # data is poorly formed but will, however, naturally adapt to | |
174 | # extraneous whitespace. | |
175 | return Version(*(int(value) for value | |
176 | in version_string.split('.', 1))) | |
177 | except (ValueError, TypeError, AttributeError) as exc: | |
178 | raise TypeError('invalid version string: %s; %s' % ( | |
179 | version_string, exc)) | |
180 | ||
181 | ||
182 | def extract_version(headers, service_type, versions_list): | |
183 | """Extract the microversion from the headers. | |
184 | ||
185 | There may be multiple headers and some which don't match our | |
186 | service. | |
187 | ||
188 | If no version is found then the extracted version is the minimum | |
189 | available version. | |
190 | ||
191 | :param headers: Request headers as dict list or WSGI environ | |
192 | :param service_type: The service_type as a string | |
193 | :param versions_list: List of all possible microversions as strings, | |
194 | sorted from earliest to latest version. | |
195 | :returns: a Version with the optional min_version and max_version | |
196 | attributes set. | |
197 | :raises: ValueError | |
198 | """ | |
199 | found_version = get_version(headers, service_type=service_type) | |
200 | min_version_string = versions_list[0] | |
201 | max_version_string = versions_list[-1] | |
202 | ||
203 | # If there was no version found in the headers, choose the minimum | |
204 | # available version. | |
205 | version_string = found_version or min_version_string | |
206 | if version_string == 'latest': | |
207 | version_string = max_version_string | |
208 | request_version = parse_version_string(version_string) | |
209 | request_version.max_version = parse_version_string(max_version_string) | |
210 | request_version.min_version = parse_version_string(min_version_string) | |
211 | # We need a version that is in versions_list. This gives us the option | |
212 | # to administratively disable a version if we really need to. | |
213 | if str(request_version) in versions_list: | |
214 | return request_version | |
215 | raise ValueError('Unacceptable version header: %s' % version_string) |
0 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
1 | # you may not use this file except in compliance with the License. | |
2 | # You may obtain a copy of the License at | |
3 | # | |
4 | # http://www.apache.org/licenses/LICENSE-2.0 | |
5 | # | |
6 | # Unless required by applicable law or agreed to in writing, software | |
7 | # distributed under the License is distributed on an "AS IS" BASIS, | |
8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
9 | # implied. | |
10 | # See the License for the specific language governing permissions and | |
11 | # limitations under the License. | |
12 | """WSGI middleware for getting microversion info.""" | |
13 | ||
14 | import webob | |
15 | import webob.dec | |
16 | ||
17 | import microversion_parse | |
18 | ||
19 | ||
20 | class MicroversionMiddleware(object): | |
21 | """WSGI middleware for getting microversion info. | |
22 | ||
23 | The application will get a WSGI environ with a | |
24 | 'SERVICE_TYPE.microversion' key that has a value of the microversion | |
25 | found at an 'openstack-api-version' header that matches SERVICE_TYPE. If | |
26 | no header is found, the minimum microversion will be set. If the | |
27 | special keyword 'latest' is used, the maximum microversion will be | |
28 | set. | |
29 | ||
30 | If the requested microversion is not available a 406 response is | |
31 | returned. | |
32 | ||
33 | If there is an error parsing a provided header, a 400 response is | |
34 | returned. | |
35 | ||
36 | Otherwise the application is called. | |
37 | """ | |
38 | ||
39 | def __init__(self, application, service_type, versions, | |
40 | json_error_formatter=None): | |
41 | """Create the WSGI middleware. | |
42 | ||
43 | :param application: The application hosting the service. | |
44 | :param service_type: The service type (entry in keystone catalog) | |
45 | of the application. | |
46 | :param versions: An ordered list of legitimate versions for the | |
47 | application. | |
48 | :param json_error_formatter: A Webob exception error formatter. | |
49 | See Webob for details. | |
50 | """ | |
51 | self.application = application | |
52 | self.service_type = service_type | |
53 | self.microversion_environ = '%s.microversion' % service_type | |
54 | self.versions = versions | |
55 | self.json_error_formatter = json_error_formatter | |
56 | ||
57 | @webob.dec.wsgify | |
58 | def __call__(self, req): | |
59 | try: | |
60 | microversion = microversion_parse.extract_version( | |
61 | req.headers, self.service_type, self.versions) | |
62 | # TODO(cdent): These error response are not formatted according to | |
63 | # api-sig guidelines, unless a json_error_formatter is provided | |
64 | # that can do it. For an example, see the placement service. | |
65 | except ValueError as exc: | |
66 | raise webob.exc.HTTPNotAcceptable( | |
67 | ('Invalid microversion: %(error)s') % {'error': exc}, | |
68 | json_formatter=self.json_error_formatter) | |
69 | except TypeError as exc: | |
70 | raise webob.exc.HTTPBadRequest( | |
71 | ('Invalid microversion: %(error)s') % {'error': exc}, | |
72 | json_formatter=self.json_error_formatter) | |
73 | ||
74 | req.environ[self.microversion_environ] = microversion | |
75 | microversion_header = '%s %s' % (self.service_type, microversion) | |
76 | standard_header = microversion_parse.STANDARD_HEADER | |
77 | ||
78 | try: | |
79 | response = req.get_response(self.application) | |
80 | except webob.exc.HTTPError as exc: | |
81 | # If there was an HTTPError in the application we still need | |
82 | # to send the microversion header, so add the header and | |
83 | # re-raise the exception. | |
84 | exc.headers.add(standard_header, microversion_header) | |
85 | raise exc | |
86 | ||
87 | response.headers.add(standard_header, microversion_header) | |
88 | response.headers.add('vary', standard_header) | |
89 | return response |
0 | # Tests that the middleware does microversioning as we expect | |
1 | # The min version of the service is 1.0, the max is 1.2, | |
2 | # the service type is "cats" (because the internet is for cats). | |
3 | ||
4 | defaults: | |
5 | request_headers: | |
6 | # We must guard against webob requiring an accept header. | |
7 | # We don't want to do this in the middleware itself as | |
8 | # we don't know what the application would prefer as a | |
9 | # default. | |
10 | accept: application/json | |
11 | ||
12 | tests: | |
13 | ||
14 | - name: min default | |
15 | GET: /good | |
16 | response_headers: | |
17 | openstack-api-version: cats 1.0 | |
18 | ||
19 | - name: max latest | |
20 | GET: /good | |
21 | request_headers: | |
22 | openstack-api-version: cats latest | |
23 | response_headers: | |
24 | openstack-api-version: cats 1.2 | |
25 | ||
26 | - name: explict | |
27 | GET: /good | |
28 | request_headers: | |
29 | openstack-api-version: cats 1.1 | |
30 | response_headers: | |
31 | openstack-api-version: cats 1.1 | |
32 | ||
33 | - name: out of range | |
34 | GET: /good | |
35 | request_headers: | |
36 | openstack-api-version: cats 1.9 | |
37 | status: 406 | |
38 | response_strings: | |
39 | - Unacceptable version header | |
40 | ||
41 | - name: invalid format | |
42 | GET: /good | |
43 | request_headers: | |
44 | openstack-api-version: cats 1.9.5 | |
45 | status: 400 | |
46 | response_strings: | |
47 | - invalid literal | |
48 | ||
49 | - name: different service | |
50 | desc: end up with default microversion | |
51 | GET: /good | |
52 | request_headers: | |
53 | openstack-api-version: dogs 1.9 | |
54 | response_headers: | |
55 | openstack-api-version: cats 1.0 | |
56 | ||
57 | - name: multiple services | |
58 | GET: /good | |
59 | request_headers: | |
60 | openstack-api-version: dogs 1.9, cats 1.1 | |
61 | response_headers: | |
62 | openstack-api-version: cats 1.1 | |
63 | ||
64 | - name: header present on exception | |
65 | GET: /bad | |
66 | request_headers: | |
67 | openstack-api-version: dogs 1.9, cats 1.1 | |
68 | response_headers: | |
69 | openstack-api-version: cats 1.1 | |
70 | status: 404 | |
71 | response_strings: | |
72 | - /bad not found | |
73 | ||
74 |
0 | # tests that the SimpleWSGI app is present | |
1 | ||
2 | tests: | |
3 | ||
4 | - name: get good | |
5 | GET: /good | |
6 | status: 200 | |
7 | response_strings: | |
8 | - good | |
9 | ||
10 | - name: get bad | |
11 | GET: /bad | |
12 | status: 404 | |
13 | response_strings: | |
14 | - not found |
0 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
1 | # you may not use this file except in compliance with the License. | |
2 | # You may obtain a copy of the License at | |
3 | # | |
4 | # http://www.apache.org/licenses/LICENSE-2.0 | |
5 | # | |
6 | # Unless required by applicable law or agreed to in writing, software | |
7 | # distributed under the License is distributed on an "AS IS" BASIS, | |
8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
9 | # implied. | |
10 | # See the License for the specific language governing permissions and | |
11 | # limitations under the License. | |
12 | ||
13 | ||
14 | import testtools | |
15 | ||
16 | import microversion_parse | |
17 | ||
18 | ||
19 | class TestVersion(testtools.TestCase): | |
20 | ||
21 | def setUp(self): | |
22 | super(TestVersion, self).setUp() | |
23 | self.version = microversion_parse.Version(1, 5) | |
24 | ||
25 | def test_version_is_tuple(self): | |
26 | self.assertEqual((1, 5), self.version) | |
27 | ||
28 | def test_version_stringifies(self): | |
29 | self.assertEqual('1.5', str(self.version)) | |
30 | ||
31 | def test_version_matches(self): | |
32 | max_version = microversion_parse.Version(1, 20) | |
33 | min_version = microversion_parse.Version(1, 3) | |
34 | ||
35 | self.assertTrue(self.version.matches(min_version, max_version)) | |
36 | self.assertFalse(self.version.matches(max_version, min_version)) | |
37 | ||
38 | def test_version_matches_inclusive(self): | |
39 | max_version = microversion_parse.Version(1, 5) | |
40 | min_version = microversion_parse.Version(1, 5) | |
41 | ||
42 | self.assertTrue(self.version.matches(min_version, max_version)) | |
43 | ||
44 | def test_version_matches_no_extremes(self): | |
45 | """If no extremes are present, never match.""" | |
46 | self.assertFalse(self.version.matches()) | |
47 | ||
48 | def test_version_zero_can_match(self): | |
49 | """If a version is '0.0' we want to it be able to match.""" | |
50 | version = microversion_parse.Version(0, 0) | |
51 | min_version = microversion_parse.Version(0, 0) | |
52 | max_version = microversion_parse.Version(0, 0) | |
53 | version.min_version = min_version | |
54 | version.max_version = max_version | |
55 | ||
56 | self.assertTrue(version.matches()) | |
57 | ||
58 | def test_version_zero_no_defaults(self): | |
59 | """Any version, even 0.0, should never match without a min | |
60 | and max being set. | |
61 | """ | |
62 | version = microversion_parse.Version(0, 0) | |
63 | ||
64 | self.assertFalse(version.matches()) | |
65 | ||
66 | def test_version_init_failure(self): | |
67 | self.assertRaises(TypeError, microversion_parse.Version, 1, 2, 3) | |
68 | ||
69 | ||
70 | class TestParseVersionString(testtools.TestCase): | |
71 | ||
72 | def test_good_version(self): | |
73 | version = microversion_parse.parse_version_string('1.1') | |
74 | self.assertEqual((1, 1), version) | |
75 | self.assertEqual(microversion_parse.Version(1, 1), version) | |
76 | ||
77 | def test_adapt_whitespace(self): | |
78 | version = microversion_parse.parse_version_string(' 1.1 ') | |
79 | self.assertEqual((1, 1), version) | |
80 | self.assertEqual(microversion_parse.Version(1, 1), version) | |
81 | ||
82 | def test_non_numeric(self): | |
83 | self.assertRaises(TypeError, | |
84 | microversion_parse.parse_version_string, | |
85 | 'hello') | |
86 | ||
87 | def test_mixed_alphanumeric(self): | |
88 | self.assertRaises(TypeError, | |
89 | microversion_parse.parse_version_string, | |
90 | '1.a') | |
91 | ||
92 | def test_too_many_numeric(self): | |
93 | self.assertRaises(TypeError, | |
94 | microversion_parse.parse_version_string, | |
95 | '1.1.1') | |
96 | ||
97 | def test_not_string(self): | |
98 | self.assertRaises(TypeError, | |
99 | microversion_parse.parse_version_string, | |
100 | 1.1) | |
101 | ||
102 | ||
103 | class TestExtractVersion(testtools.TestCase): | |
104 | ||
105 | def setUp(self): | |
106 | super(TestExtractVersion, self).setUp() | |
107 | self.headers = [ | |
108 | ('OpenStack-API-Version', 'service1 1.2'), | |
109 | ('OpenStack-API-Version', 'service2 1.5'), | |
110 | ('OpenStack-API-Version', 'service3 latest'), | |
111 | ('OpenStack-API-Version', 'service4 2.5'), | |
112 | ] | |
113 | self.version_list = ['1.1', '1.2', '1.3', '1.4', | |
114 | '2.1', '2.2', '2.3', '2.4'] | |
115 | ||
116 | def test_simple_extract(self): | |
117 | version = microversion_parse.extract_version( | |
118 | self.headers, 'service1', self.version_list) | |
119 | self.assertEqual((1, 2), version) | |
120 | ||
121 | def test_default_min(self): | |
122 | version = microversion_parse.extract_version( | |
123 | self.headers, 'notlisted', self.version_list) | |
124 | self.assertEqual((1, 1), version) | |
125 | ||
126 | def test_latest(self): | |
127 | version = microversion_parse.extract_version( | |
128 | self.headers, 'service3', self.version_list) | |
129 | self.assertEqual((2, 4), version) | |
130 | ||
131 | def test_min_max_extract(self): | |
132 | version = microversion_parse.extract_version( | |
133 | self.headers, 'service1', self.version_list) | |
134 | ||
135 | # below min | |
136 | self.assertFalse(version.matches((1, 3))) | |
137 | # at min | |
138 | self.assertTrue(version.matches((1, 2))) | |
139 | # within extremes | |
140 | self.assertTrue(version.matches()) | |
141 | # explicit max | |
142 | self.assertTrue(version.matches(max_version=(2, 3))) | |
143 | # explicit min | |
144 | self.assertFalse(version.matches(min_version=(2, 3))) | |
145 | # explicit both | |
146 | self.assertTrue(version.matches(min_version=(0, 3), | |
147 | max_version=(1, 5))) | |
148 | ||
149 | def test_version_disabled(self): | |
150 | self.assertRaises(ValueError, microversion_parse.extract_version, | |
151 | self.headers, 'service2', self.version_list) | |
152 | ||
153 | def test_version_out_of_range(self): | |
154 | self.assertRaises(ValueError, microversion_parse.extract_version, | |
155 | self.headers, 'service4', self.version_list) |
0 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
1 | # you may not use this file except in compliance with the License. | |
2 | # You may obtain a copy of the License at | |
3 | # | |
4 | # http://www.apache.org/licenses/LICENSE-2.0 | |
5 | # | |
6 | # Unless required by applicable law or agreed to in writing, software | |
7 | # distributed under the License is distributed on an "AS IS" BASIS, | |
8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
9 | # implied. | |
10 | # See the License for the specific language governing permissions and | |
11 | # limitations under the License. | |
12 | ||
13 | import testtools | |
14 | ||
15 | import microversion_parse | |
16 | ||
17 | ||
18 | class TestHeadersFromWSGIEnviron(testtools.TestCase): | |
19 | ||
20 | def test_empty_environ(self): | |
21 | environ = {} | |
22 | expected = {} | |
23 | self.assertEqual( | |
24 | expected, | |
25 | microversion_parse.headers_from_wsgi_environ(environ)) | |
26 | ||
27 | def test_non_empty_no_headers(self): | |
28 | environ = {'PATH_INFO': '/foo/bar'} | |
29 | expected = {} | |
30 | found_headers = microversion_parse.headers_from_wsgi_environ(environ) | |
31 | self.assertEqual(expected, found_headers) | |
32 | ||
33 | def test_headers(self): | |
34 | environ = {'PATH_INFO': '/foo/bar', | |
35 | 'HTTP_OPENSTACK_API_VERSION': 'placement 2.1', | |
36 | 'HTTP_CONTENT_TYPE': 'application/json'} | |
37 | expected = {'HTTP_OPENSTACK_API_VERSION': 'placement 2.1', | |
38 | 'HTTP_CONTENT_TYPE': 'application/json'} | |
39 | found_headers = microversion_parse.headers_from_wsgi_environ(environ) | |
40 | self.assertEqual(expected, found_headers) | |
41 | ||
42 | def test_get_version_from_environ(self): | |
43 | environ = {'PATH_INFO': '/foo/bar', | |
44 | 'HTTP_OPENSTACK_API_VERSION': 'placement 2.1', | |
45 | 'HTTP_CONTENT_TYPE': 'application/json'} | |
46 | expected_version = '2.1' | |
47 | headers = microversion_parse.headers_from_wsgi_environ(environ) | |
48 | version = microversion_parse.get_version(headers, 'placement') | |
49 | self.assertEqual(expected_version, version) | |
50 | ||
51 | def test_get_version_from_environ_legacy(self): | |
52 | environ = {'PATH_INFO': '/foo/bar', | |
53 | 'HTTP_X_OPENSTACK_PLACEMENT_API_VERSION': '2.1', | |
54 | 'HTTP_CONTENT_TYPE': 'application/json'} | |
55 | expected_version = '2.1' | |
56 | headers = microversion_parse.headers_from_wsgi_environ(environ) | |
57 | version = microversion_parse.get_version( | |
58 | headers, 'placement', | |
59 | legacy_headers=['x-openstack-placement-api-version']) | |
60 | self.assertEqual(expected_version, version) |
0 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
1 | # you may not use this file except in compliance with the License. | |
2 | # You may obtain a copy of the License at | |
3 | # | |
4 | # http://www.apache.org/licenses/LICENSE-2.0 | |
5 | # | |
6 | # Unless required by applicable law or agreed to in writing, software | |
7 | # distributed under the License is distributed on an "AS IS" BASIS, | |
8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
9 | # implied. | |
10 | # See the License for the specific language governing permissions and | |
11 | # limitations under the License. | |
12 | ||
13 | # The microversion_parse middlware is tests using gabbi to run real | |
14 | # http requests through it. To do that, we need a simple WSGI | |
15 | # application running under wsgi-intercept (handled by gabbi). | |
16 | ||
17 | import os | |
18 | ||
19 | from gabbi import driver | |
20 | import webob | |
21 | ||
22 | from microversion_parse import middleware | |
23 | ||
24 | ||
25 | TESTS_DIR = 'gabbits' | |
26 | SERVICE_TYPE = 'cats' | |
27 | VERSIONS = [ | |
28 | '1.0', # initial version | |
29 | '1.1', # now with kittens | |
30 | '1.2', # added breeds | |
31 | ] | |
32 | ||
33 | ||
34 | class SimpleWSGI(object): | |
35 | """A WSGI application that can be contiained within a middlware.""" | |
36 | ||
37 | def __call__(self, environ, start_response): | |
38 | path_info = environ['PATH_INFO'] | |
39 | if path_info == '/good': | |
40 | start_response('200 OK', [('content-type', 'text/plain')]) | |
41 | return [b'good'] | |
42 | ||
43 | raise webob.exc.HTTPNotFound('%s not found' % path_info) | |
44 | ||
45 | ||
46 | def app(): | |
47 | app = middleware.MicroversionMiddleware( | |
48 | SimpleWSGI(), SERVICE_TYPE, VERSIONS) | |
49 | return app | |
50 | ||
51 | ||
52 | def load_tests(loader, tests, pattern): | |
53 | """Provide a TestSuite to the discovery process.""" | |
54 | test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) | |
55 | return driver.build_tests( | |
56 | test_dir, loader, test_loader_name=__name__, intercept=app) |
13 | 13 | Programming Language :: Python :: 2 |
14 | 14 | Programming Language :: Python :: 2.7 |
15 | 15 | Programming Language :: Python :: 3 |
16 | Programming Language :: Python :: 3.4 | |
17 | 16 | Programming Language :: Python :: 3.5 |
17 | Programming Language :: Python :: 3.6 | |
18 | 18 | |
19 | 19 | [files] |
20 | 20 | packages = |
24 | 24 | all_files = 1 |
25 | 25 | build-dir = doc/build |
26 | 26 | source-dir = doc/source |
27 | ||
28 | [bdist_wheel] | |
29 | universal=1 |
1 | 1 | coverage>=3.6 # Apache-2.0 |
2 | 2 | sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD |
3 | 3 | oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 |
4 | testrepository>=0.0.18 # Apache-2.0/BSD | |
4 | stestr>=1.0.0 # Apache-2.0 | |
5 | 5 | testtools>=1.4.0 # MIT |
6 | WebOb>=1.2.3 # MIT | |
6 | gabbi>=1.35.0 # Apache-2.0 |
2 | 2 | skipsdist = True |
3 | 3 | # If you want pypy or pypy3, do 'tox -epypy,pypy3', it might work! |
4 | 4 | # And you can get coverage with 'tox -ecover'. |
5 | envlist = py27,py34,py35,pep8 | |
5 | envlist = py27,py36,py35,pep8 | |
6 | 6 | |
7 | 7 | [testenv] |
8 | 8 | deps = -r{toxinidir}/requirements.txt |
9 | 9 | -r{toxinidir}/test-requirements.txt |
10 | 10 | install_command = pip install -U {opts} {packages} |
11 | setenv = OS_TEST_PATH=microversion_parse/tests/ | |
12 | 11 | usedevelop = True |
13 | commands = python setup.py testr --testr-args="{posargs}" | |
12 | commands = stestr run {posargs} | |
14 | 13 | |
15 | 14 | [testenv:venv] |
16 | 15 | deps = -r{toxinidir}/requirements.txt |
24 | 23 | flake8 |
25 | 24 | |
26 | 25 | [testenv:cover] |
27 | commands = python setup.py testr --coverage --testr-args="{posargs}" | |
26 | setenv = PYTHON=coverage run --source microversion_parse --parallel-mode | |
27 | commands = | |
28 | coverage erase | |
29 | find . -type f -name "*.pyc" -delete | |
30 | stestr run {posargs} | |
31 | coverage combine | |
32 | coverage html -d cover | |
33 | whitelist_externals = | |
34 | find | |
28 | 35 | |
29 | 36 | [testenv:docs] |
30 | 37 | commands = |