blob: cc71c92f30adacb475aee125a3451cd1a9df4a1a [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# Copyright 2014 IBM Corp.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import copy
17import datetime
18
19from oslotest import mockpatch
20
21from tempest.lib import auth
22from tempest.lib import exceptions
23from tempest.lib.services.identity.v2 import token_client as v2_client
24from tempest.lib.services.identity.v3 import token_client as v3_client
Matthew Treinishffad78a2016-04-16 14:39:52 -040025from tempest.tests import base
Matthew Treinish9e26ca82016-02-23 11:43:20 -050026from tempest.tests.lib import fake_credentials
Matthew Treinish9e26ca82016-02-23 11:43:20 -050027from tempest.tests.lib import fake_identity
28
29
30def fake_get_credentials(fill_in=True, identity_version='v2', **kwargs):
31 return fake_credentials.FakeCredentials()
32
33
34class BaseAuthTestsSetUp(base.TestCase):
35 _auth_provider_class = None
36 credentials = fake_credentials.FakeCredentials()
37
38 def _auth(self, credentials, auth_url, **params):
39 """returns auth method according to keystone"""
40 return self._auth_provider_class(credentials, auth_url, **params)
41
42 def setUp(self):
43 super(BaseAuthTestsSetUp, self).setUp()
Jordan Pittier0021c292016-03-29 21:33:34 +020044 self.patchobject(auth, 'get_credentials', fake_get_credentials)
Matthew Treinish9e26ca82016-02-23 11:43:20 -050045 self.auth_provider = self._auth(self.credentials,
46 fake_identity.FAKE_AUTH_URL)
47
48
49class TestBaseAuthProvider(BaseAuthTestsSetUp):
50 """Tests for base AuthProvider
51
52 This tests auth.AuthProvider class which is base for the other so we
53 obviously don't test not implemented method or the ones which strongly
54 depends on them.
55 """
56
57 class FakeAuthProviderImpl(auth.AuthProvider):
58 def _decorate_request(self):
59 pass
60
61 def _fill_credentials(self):
62 pass
63
64 def _get_auth(self):
65 pass
66
67 def base_url(self):
68 pass
69
70 def is_expired(self):
71 pass
72
73 _auth_provider_class = FakeAuthProviderImpl
74
75 def _auth(self, credentials, auth_url, **params):
76 """returns auth method according to keystone"""
77 return self._auth_provider_class(credentials, **params)
78
79 def test_check_credentials_bad_type(self):
80 self.assertFalse(self.auth_provider.check_credentials([]))
81
82 def test_auth_data_property_when_cache_exists(self):
83 self.auth_provider.cache = 'foo'
84 self.useFixture(mockpatch.PatchObject(self.auth_provider,
85 'is_expired',
86 return_value=False))
87 self.assertEqual('foo', getattr(self.auth_provider, 'auth_data'))
88
89 def test_delete_auth_data_property_through_deleter(self):
90 self.auth_provider.cache = 'foo'
91 del self.auth_provider.auth_data
92 self.assertIsNone(self.auth_provider.cache)
93
94 def test_delete_auth_data_property_through_clear_auth(self):
95 self.auth_provider.cache = 'foo'
96 self.auth_provider.clear_auth()
97 self.assertIsNone(self.auth_provider.cache)
98
99 def test_set_and_reset_alt_auth_data(self):
100 self.auth_provider.set_alt_auth_data('foo', 'bar')
101 self.assertEqual(self.auth_provider.alt_part, 'foo')
102 self.assertEqual(self.auth_provider.alt_auth_data, 'bar')
103
104 self.auth_provider.reset_alt_auth_data()
105 self.assertIsNone(self.auth_provider.alt_part)
106 self.assertIsNone(self.auth_provider.alt_auth_data)
107
108 def test_auth_class(self):
109 self.assertRaises(TypeError,
110 auth.AuthProvider,
111 fake_credentials.FakeCredentials)
112
113
114class TestKeystoneV2AuthProvider(BaseAuthTestsSetUp):
115 _endpoints = fake_identity.IDENTITY_V2_RESPONSE['access']['serviceCatalog']
116 _auth_provider_class = auth.KeystoneV2AuthProvider
117 credentials = fake_credentials.FakeKeystoneV2Credentials()
118
119 def setUp(self):
120 super(TestKeystoneV2AuthProvider, self).setUp()
Jordan Pittier0021c292016-03-29 21:33:34 +0200121 self.patchobject(v2_client.TokenClient, 'raw_request',
122 fake_identity._fake_v2_response)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500123 self.target_url = 'test_api'
124
125 def _get_fake_identity(self):
126 return fake_identity.IDENTITY_V2_RESPONSE['access']
127
128 def _get_fake_alt_identity(self):
129 return fake_identity.ALT_IDENTITY_V2_RESPONSE['access']
130
131 def _get_result_url_from_endpoint(self, ep, endpoint_type='publicURL',
132 replacement=None):
133 if replacement:
134 return ep[endpoint_type].replace('v2', replacement)
135 return ep[endpoint_type]
136
137 def _get_token_from_fake_identity(self):
138 return fake_identity.TOKEN
139
140 def _get_from_fake_identity(self, attr):
141 access = fake_identity.IDENTITY_V2_RESPONSE['access']
142 if attr == 'user_id':
143 return access['user']['id']
144 elif attr == 'tenant_id':
145 return access['token']['tenant']['id']
146
147 def _test_request_helper(self, filters, expected):
148 url, headers, body = self.auth_provider.auth_request('GET',
149 self.target_url,
150 filters=filters)
151
152 self.assertEqual(expected['url'], url)
153 self.assertEqual(expected['token'], headers['X-Auth-Token'])
154 self.assertEqual(expected['body'], body)
155
156 def _auth_data_with_expiry(self, date_as_string):
157 token, access = self.auth_provider.auth_data
158 access['token']['expires'] = date_as_string
159 return token, access
160
161 def test_request(self):
162 filters = {
163 'service': 'compute',
164 'endpoint_type': 'publicURL',
165 'region': 'FakeRegion'
166 }
167
168 url = self._get_result_url_from_endpoint(
169 self._endpoints[0]['endpoints'][1]) + '/' + self.target_url
170
171 expected = {
172 'body': None,
173 'url': url,
174 'token': self._get_token_from_fake_identity(),
175 }
176 self._test_request_helper(filters, expected)
177
178 def test_request_with_alt_auth_cleans_alt(self):
179 """Test alternate auth data for headers
180
181 Assert that when the alt data is provided for headers, after an
182 auth_request the data alt_data is cleaned-up.
183 """
184 self.auth_provider.set_alt_auth_data(
185 'headers',
186 (fake_identity.ALT_TOKEN, self._get_fake_alt_identity()))
187 filters = {
188 'service': 'compute',
189 'endpoint_type': 'publicURL',
190 'region': 'fakeRegion'
191 }
192 self.auth_provider.auth_request('GET', self.target_url,
193 filters=filters)
194
195 # Assert alt auth data is clear after it
196 self.assertIsNone(self.auth_provider.alt_part)
197 self.assertIsNone(self.auth_provider.alt_auth_data)
198
199 def _test_request_with_identical_alt_auth(self, part):
200 """Test alternate but identical auth data for headers
201
202 Assert that when the alt data is provided, but it's actually
203 identical, an exception is raised.
204 """
205 self.auth_provider.set_alt_auth_data(
206 part,
207 (fake_identity.TOKEN, self._get_fake_identity()))
208 filters = {
209 'service': 'compute',
210 'endpoint_type': 'publicURL',
211 'region': 'fakeRegion'
212 }
213
214 self.assertRaises(exceptions.BadAltAuth,
215 self.auth_provider.auth_request,
216 'GET', self.target_url, filters=filters)
217
218 def test_request_with_identical_alt_auth_headers(self):
219 self._test_request_with_identical_alt_auth('headers')
220
221 def test_request_with_identical_alt_auth_url(self):
222 self._test_request_with_identical_alt_auth('url')
223
224 def test_request_with_identical_alt_auth_body(self):
225 self._test_request_with_identical_alt_auth('body')
226
227 def test_request_with_alt_part_without_alt_data(self):
228 """Test empty alternate auth data
229
230 Assert that when alt_part is defined, the corresponding original
231 request element is kept the same.
232 """
233 filters = {
234 'service': 'compute',
235 'endpoint_type': 'publicURL',
236 'region': 'fakeRegion'
237 }
238 self.auth_provider.set_alt_auth_data('headers', None)
239
240 url, headers, body = self.auth_provider.auth_request('GET',
241 self.target_url,
242 filters=filters)
243 # The original headers where empty
244 self.assertNotEqual(url, self.target_url)
245 self.assertIsNone(headers)
246 self.assertEqual(body, None)
247
248 def _test_request_with_alt_part_without_alt_data_no_change(self, body):
249 """Test empty alternate auth data with no effect
250
251 Assert that when alt_part is defined, no auth_data is provided,
Anh Trand44a8be2016-03-25 09:49:14 +0700252 and the corresponding original request element was not going to
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500253 be changed anyways, and exception is raised
254 """
255 filters = {
256 'service': 'compute',
257 'endpoint_type': 'publicURL',
258 'region': 'fakeRegion'
259 }
260 self.auth_provider.set_alt_auth_data('body', None)
261
262 self.assertRaises(exceptions.BadAltAuth,
263 self.auth_provider.auth_request,
264 'GET', self.target_url, filters=filters)
265
266 def test_request_with_alt_part_without_alt_data_no_change_headers(self):
267 self._test_request_with_alt_part_without_alt_data_no_change('headers')
268
269 def test_request_with_alt_part_without_alt_data_no_change_url(self):
270 self._test_request_with_alt_part_without_alt_data_no_change('url')
271
272 def test_request_with_alt_part_without_alt_data_no_change_body(self):
273 self._test_request_with_alt_part_without_alt_data_no_change('body')
274
275 def test_request_with_bad_service(self):
276 filters = {
277 'service': 'BAD_SERVICE',
278 'endpoint_type': 'publicURL',
279 'region': 'fakeRegion'
280 }
281 self.assertRaises(exceptions.EndpointNotFound,
282 self.auth_provider.auth_request, 'GET',
283 self.target_url, filters=filters)
284
285 def test_request_without_service(self):
286 filters = {
287 'service': None,
288 'endpoint_type': 'publicURL',
289 'region': 'fakeRegion'
290 }
291 self.assertRaises(exceptions.EndpointNotFound,
292 self.auth_provider.auth_request, 'GET',
293 self.target_url, filters=filters)
294
295 def test_check_credentials_missing_attribute(self):
296 for attr in ['username', 'password']:
297 cred = copy.copy(self.credentials)
298 del cred[attr]
299 self.assertFalse(self.auth_provider.check_credentials(cred))
300
301 def test_fill_credentials(self):
302 self.auth_provider.fill_credentials()
303 creds = self.auth_provider.credentials
304 for attr in ['user_id', 'tenant_id']:
305 self.assertEqual(self._get_from_fake_identity(attr),
306 getattr(creds, attr))
307
308 def _test_base_url_helper(self, expected_url, filters,
309 auth_data=None):
310
311 url = self.auth_provider.base_url(filters, auth_data)
312 self.assertEqual(url, expected_url)
313
314 def test_base_url(self):
315 self.filters = {
316 'service': 'compute',
317 'endpoint_type': 'publicURL',
318 'region': 'FakeRegion'
319 }
320 expected = self._get_result_url_from_endpoint(
321 self._endpoints[0]['endpoints'][1])
322 self._test_base_url_helper(expected, self.filters)
323
324 def test_base_url_to_get_admin_endpoint(self):
325 self.filters = {
326 'service': 'compute',
327 'endpoint_type': 'adminURL',
328 'region': 'FakeRegion'
329 }
330 expected = self._get_result_url_from_endpoint(
331 self._endpoints[0]['endpoints'][1], endpoint_type='adminURL')
332 self._test_base_url_helper(expected, self.filters)
333
334 def test_base_url_unknown_region(self):
335 """If the region is unknown, the first endpoint is returned."""
336 self.filters = {
337 'service': 'compute',
338 'endpoint_type': 'publicURL',
339 'region': 'AintNoBodyKnowThisRegion'
340 }
341 expected = self._get_result_url_from_endpoint(
342 self._endpoints[0]['endpoints'][0])
343 self._test_base_url_helper(expected, self.filters)
344
345 def test_base_url_with_non_existent_service(self):
346 self.filters = {
347 'service': 'BAD_SERVICE',
348 'endpoint_type': 'publicURL',
349 'region': 'FakeRegion'
350 }
351 self.assertRaises(exceptions.EndpointNotFound,
352 self._test_base_url_helper, None, self.filters)
353
354 def test_base_url_without_service(self):
355 self.filters = {
356 'endpoint_type': 'publicURL',
357 'region': 'FakeRegion'
358 }
359 self.assertRaises(exceptions.EndpointNotFound,
360 self._test_base_url_helper, None, self.filters)
361
362 def test_base_url_with_api_version_filter(self):
363 self.filters = {
364 'service': 'compute',
365 'endpoint_type': 'publicURL',
366 'region': 'FakeRegion',
367 'api_version': 'v12'
368 }
369 expected = self._get_result_url_from_endpoint(
370 self._endpoints[0]['endpoints'][1], replacement='v12')
371 self._test_base_url_helper(expected, self.filters)
372
373 def test_base_url_with_skip_path_filter(self):
374 self.filters = {
375 'service': 'compute',
376 'endpoint_type': 'publicURL',
377 'region': 'FakeRegion',
378 'skip_path': True
379 }
380 expected = 'http://fake_url/'
381 self._test_base_url_helper(expected, self.filters)
382
Jamie Lennoxa934a702016-03-09 11:36:36 +1100383 def test_base_url_with_unversioned_endpoint(self):
384 auth_data = {
385 'serviceCatalog': [
386 {
387 'type': 'identity',
388 'endpoints': [
389 {
390 'region': 'FakeRegion',
391 'publicURL': 'http://fake_url'
392 }
393 ]
394 }
395 ]
396 }
397
398 filters = {
399 'service': 'identity',
400 'endpoint_type': 'publicURL',
401 'region': 'FakeRegion',
402 'api_version': 'v2.0'
403 }
404
405 expected = 'http://fake_url/v2.0'
406 self._test_base_url_helper(expected, filters, ('token', auth_data))
407
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500408 def test_token_not_expired(self):
409 expiry_data = datetime.datetime.utcnow() + datetime.timedelta(days=1)
410 self._verify_expiry(expiry_data=expiry_data, should_be_expired=False)
411
412 def test_token_expired(self):
413 expiry_data = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
414 self._verify_expiry(expiry_data=expiry_data, should_be_expired=True)
415
416 def test_token_not_expired_to_be_renewed(self):
417 expiry_data = (datetime.datetime.utcnow() +
418 self.auth_provider.token_expiry_threshold / 2)
419 self._verify_expiry(expiry_data=expiry_data, should_be_expired=True)
420
421 def _verify_expiry(self, expiry_data, should_be_expired):
422 for expiry_format in self.auth_provider.EXPIRY_DATE_FORMATS:
423 auth_data = self._auth_data_with_expiry(
424 expiry_data.strftime(expiry_format))
425 self.assertEqual(self.auth_provider.is_expired(auth_data),
426 should_be_expired)
427
428
429class TestKeystoneV3AuthProvider(TestKeystoneV2AuthProvider):
430 _endpoints = fake_identity.IDENTITY_V3_RESPONSE['token']['catalog']
431 _auth_provider_class = auth.KeystoneV3AuthProvider
432 credentials = fake_credentials.FakeKeystoneV3Credentials()
433
434 def setUp(self):
435 super(TestKeystoneV3AuthProvider, self).setUp()
Jordan Pittier0021c292016-03-29 21:33:34 +0200436 self.patchobject(v3_client.V3TokenClient, 'raw_request',
437 fake_identity._fake_v3_response)
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500438
439 def _get_fake_identity(self):
440 return fake_identity.IDENTITY_V3_RESPONSE['token']
441
442 def _get_fake_alt_identity(self):
443 return fake_identity.ALT_IDENTITY_V3['token']
444
445 def _get_result_url_from_endpoint(self, ep, replacement=None):
446 if replacement:
447 return ep['url'].replace('v3', replacement)
448 return ep['url']
449
450 def _auth_data_with_expiry(self, date_as_string):
451 token, access = self.auth_provider.auth_data
452 access['expires_at'] = date_as_string
453 return token, access
454
455 def _get_from_fake_identity(self, attr):
456 token = fake_identity.IDENTITY_V3_RESPONSE['token']
457 if attr == 'user_id':
458 return token['user']['id']
459 elif attr == 'project_id':
460 return token['project']['id']
461 elif attr == 'user_domain_id':
462 return token['user']['domain']['id']
463 elif attr == 'project_domain_id':
464 return token['project']['domain']['id']
465
466 def test_check_credentials_missing_attribute(self):
467 # reset credentials to fresh ones
468 self.credentials.reset()
469 for attr in ['username', 'password', 'user_domain_name',
470 'project_domain_name']:
471 cred = copy.copy(self.credentials)
472 del cred[attr]
473 self.assertFalse(self.auth_provider.check_credentials(cred),
474 "Credentials should be invalid without %s" % attr)
475
476 def test_check_domain_credentials_missing_attribute(self):
477 # reset credentials to fresh ones
478 self.credentials.reset()
479 domain_creds = fake_credentials.FakeKeystoneV3DomainCredentials()
480 for attr in ['username', 'password', 'user_domain_name']:
481 cred = copy.copy(domain_creds)
482 del cred[attr]
483 self.assertFalse(self.auth_provider.check_credentials(cred),
484 "Credentials should be invalid without %s" % attr)
485
486 def test_fill_credentials(self):
487 self.auth_provider.fill_credentials()
488 creds = self.auth_provider.credentials
489 for attr in ['user_id', 'project_id', 'user_domain_id',
490 'project_domain_id']:
491 self.assertEqual(self._get_from_fake_identity(attr),
492 getattr(creds, attr))
493
494 # Overwrites v2 test
495 def test_base_url_to_get_admin_endpoint(self):
496 self.filters = {
497 'service': 'compute',
498 'endpoint_type': 'admin',
499 'region': 'MiddleEarthRegion'
500 }
501 expected = self._get_result_url_from_endpoint(
502 self._endpoints[0]['endpoints'][2])
503 self._test_base_url_helper(expected, self.filters)
Jamie Lennoxa934a702016-03-09 11:36:36 +1100504
505 # Overwrites v2 test
506 def test_base_url_with_unversioned_endpoint(self):
507 auth_data = {
508 'catalog': [
509 {
510 'type': 'identity',
511 'endpoints': [
512 {
513 'region': 'FakeRegion',
514 'url': 'http://fake_url',
515 'interface': 'public'
516 }
517 ]
518 }
519 ]
520 }
521
522 filters = {
523 'service': 'identity',
524 'endpoint_type': 'publicURL',
525 'region': 'FakeRegion',
526 'api_version': 'v3'
527 }
528
529 expected = 'http://fake_url/v3'
530 self._test_base_url_helper(expected, filters, ('token', auth_data))
John Warrenb10c6ca2016-02-26 15:32:37 -0500531
532
533class TestKeystoneV3Credentials(base.TestCase):
534 def testSetAttrUserDomain(self):
535 creds = auth.KeystoneV3Credentials()
536 creds.user_domain_name = 'user_domain'
537 creds.domain_name = 'domain'
538 self.assertEqual('user_domain', creds.user_domain_name)
539 creds = auth.KeystoneV3Credentials()
540 creds.domain_name = 'domain'
541 creds.user_domain_name = 'user_domain'
542 self.assertEqual('user_domain', creds.user_domain_name)
543
544 def testSetAttrProjectDomain(self):
545 creds = auth.KeystoneV3Credentials()
546 creds.project_domain_name = 'project_domain'
547 creds.domain_name = 'domain'
548 self.assertEqual('project_domain', creds.user_domain_name)
549 creds = auth.KeystoneV3Credentials()
550 creds.domain_name = 'domain'
551 creds.project_domain_name = 'project_domain'
552 self.assertEqual('project_domain', creds.project_domain_name)
553
554 def testProjectTenantNoCollision(self):
555 creds = auth.KeystoneV3Credentials(tenant_id='tenant')
556 self.assertEqual('tenant', creds.project_id)
557 creds = auth.KeystoneV3Credentials(project_id='project')
558 self.assertEqual('project', creds.tenant_id)
559 creds = auth.KeystoneV3Credentials(tenant_name='tenant')
560 self.assertEqual('tenant', creds.project_name)
561 creds = auth.KeystoneV3Credentials(project_name='project')
562 self.assertEqual('project', creds.tenant_name)
563
564 def testProjectTenantCollision(self):
565 attrs = {'tenant_id': 'tenant', 'project_id': 'project'}
566 self.assertRaises(
567 exceptions.InvalidCredentials, auth.KeystoneV3Credentials, **attrs)
568 attrs = {'tenant_name': 'tenant', 'project_name': 'project'}
569 self.assertRaises(
570 exceptions.InvalidCredentials, auth.KeystoneV3Credentials, **attrs)
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500571
572
573class TestReplaceVersion(base.TestCase):
574 def test_version_no_trailing_path(self):
575 self.assertEqual(
576 'http://localhost:35357/v2.0',
577 auth.replace_version('http://localhost:35357/v3', 'v2.0'))
578
579 def test_version_no_trailing_path_solidus(self):
580 self.assertEqual(
581 'http://localhost:35357/v2.0/',
582 auth.replace_version('http://localhost:35357/v3/', 'v2.0'))
583
584 def test_version_trailing_path(self):
585 self.assertEqual(
586 'http://localhost:35357/v2.0/uuid',
587 auth.replace_version('http://localhost:35357/v3/uuid', 'v2.0'))
588
589 def test_version_trailing_path_solidus(self):
590 self.assertEqual(
591 'http://localhost:35357/v2.0/uuid/',
592 auth.replace_version('http://localhost:35357/v3/uuid/', 'v2.0'))
593
594 def test_no_version_base(self):
595 self.assertEqual(
596 'http://localhost:35357/v2.0',
597 auth.replace_version('http://localhost:35357', 'v2.0'))
598
599 def test_no_version_base_solidus(self):
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500600 self.assertEqual(
Brant Knudson77293802016-04-11 15:14:54 -0500601 'http://localhost:35357/v2.0',
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500602 auth.replace_version('http://localhost:35357/', 'v2.0'))
603
604 def test_no_version_path(self):
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500605 self.assertEqual(
Brant Knudson77293802016-04-11 15:14:54 -0500606 'http://localhost/identity/v2.0',
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500607 auth.replace_version('http://localhost/identity', 'v2.0'))
608
609 def test_no_version_path_solidus(self):
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500610 self.assertEqual(
Brant Knudson77293802016-04-11 15:14:54 -0500611 'http://localhost/identity/v2.0',
Brant Knudsonf2d1f572016-04-11 15:02:01 -0500612 auth.replace_version('http://localhost/identity/', 'v2.0'))
613
614 def test_path_version(self):
615 self.assertEqual(
616 'http://localhost/identity/v2.0',
617 auth.replace_version('http://localhost/identity/v3', 'v2.0'))
618
619 def test_path_version_solidus(self):
620 self.assertEqual(
621 'http://localhost/identity/v2.0/',
622 auth.replace_version('http://localhost/identity/v3/', 'v2.0'))
623
624 def test_path_version_trailing_path(self):
625 self.assertEqual(
626 'http://localhost/identity/v2.0/uuid',
627 auth.replace_version('http://localhost/identity/v3/uuid', 'v2.0'))
628
629 def test_path_version_trailing_path_solidus(self):
630 self.assertEqual(
631 'http://localhost/identity/v2.0/uuid/',
632 auth.replace_version('http://localhost/identity/v3/uuid/', 'v2.0'))