From 8a842139ddc78c7ccb7e93eb8749cba38359a078 Mon Sep 17 00:00:00 2001 From: Ziad Sawalha Date: Wed, 21 Dec 2011 22:46:32 -0600 Subject: Fixed version response (bug 891555 and bug 843052) 891555: Version response was supposed to be wrapped in a 'versions' element, instead there was only one version being returned at the root. Fixed and added tests to validate that. 843052: requests for version ATOM were ignoring the atom+xml as a valid content-type. Fix required adding a new atom template file and adding a new mapping for the content-type/extension in the URL normalizer. Fixed and added tests to validate that. Change-Id: I259c060e592e11cfc06f59ac5a540178c7f2def2 --- keystone/content/admin/version.atom.tpl | 29 ++++++ keystone/content/admin/version.json.tpl | 66 ++++++------ keystone/content/admin/version.xml.tpl | 44 ++++---- keystone/content/service/version.atom.tpl | 29 ++++++ keystone/content/service/version.json.tpl | 66 ++++++------ keystone/content/service/version.xml.tpl | 44 ++++---- keystone/controllers/version.py | 4 + keystone/middleware/url.py | 6 +- keystone/test/unit/test_controller_version.py | 139 ++++++++++++++++++++++++++ keystone/test/unit/test_normalizingfilter.py | 6 ++ keystone/utils.py | 5 + 11 files changed, 322 insertions(+), 116 deletions(-) create mode 100644 keystone/content/admin/version.atom.tpl create mode 100644 keystone/content/service/version.atom.tpl create mode 100644 keystone/test/unit/test_controller_version.py diff --git a/keystone/content/admin/version.atom.tpl b/keystone/content/admin/version.atom.tpl new file mode 100644 index 00000000..e39250d5 --- /dev/null +++ b/keystone/content/admin/version.atom.tpl @@ -0,0 +1,29 @@ + + + Available API Versions + 2010-12-22T00:00:00.00Z + http://identity.api.openstack.org/ + OpenStackhttp://www.openstack.org/ + + + http://identity.api.openstack.org/v2.0/ + Version v2.0 + 2011-09-30T00:00:00.00Z + + Version v2.0 BETA (2011-09-30T00:00:00.00Z) + + + http://identity.api.openstack.org/v1.1/ + Version v1.1 + 2011-01-21T11:33:21-06:00 + + Version v1.1 CURRENT (2011-01-21T11:33:21-06:00) + + + http://identity.api.openstack.org/v1.0/ + Version v1.0 + 2009-10-09T11:30:00Z + + Version v1.0 DEPRECATED (2009-10-09T11:30:00Z) + + diff --git a/keystone/content/admin/version.json.tpl b/keystone/content/admin/version.json.tpl index c7ca3a42..112ba1b8 100644 --- a/keystone/content/admin/version.json.tpl +++ b/keystone/content/admin/version.json.tpl @@ -1,38 +1,32 @@ { - "version" : { - "id" : "v{{API_VERSION}}", - "status" : "{{API_VERSION_STATUS}}", - "updated" : "{{API_VERSION_DATE}}", - "links": [ - { - "rel" : "self", - "href" : "http://{{HOST}}:{{PORT}}/v{{API_VERSION}}/" - }, - { - "rel" : "describedby", - "type" : "text/html", - "href" : "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/content/" - }, - { - "rel" : "describedby", - "type" : "application/pdf", - "href" : "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/identity-dev-guide-{{API_VERSION}}.pdf" - }, - { - "rel" : "describedby", - "type" : "application/vnd.sun.wadl+xml", - "href" : "http://{{HOST}}:{{PORT}}/v2.0/identity-admin.wadl" - } - ], - "media-types": [ - { - "base" : "application/xml", - "type" : "application/vnd.openstack.identity-v{{API_VERSION}}+xml" - }, - { - "base" : "application/json", - "type" : "application/vnd.openstack.identity-v{{API_VERSION}}+json" - } - ] + "versions": { + "values": [{ + "id": "v{{API_VERSION}}", + "status": "{{API_VERSION_STATUS}}", + "updated": "{{API_VERSION_DATE}}", + "links": [{ + "rel": "self", + "href": "http://{{HOST}}:{{PORT}}/v{{API_VERSION}}/" + }, { + "rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/content/" + }, { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/identity-dev-guide-{{API_VERSION}}.pdf" + }, { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://{{HOST}}:{{PORT}}/v2.0/identity-admin.wadl" + }], + "media-types": [{ + "base": "application/xml", + "type": "application/vnd.openstack.identity-v{{API_VERSION}}+xml" + }, { + "base": "application/json", + "type": "application/vnd.openstack.identity-v{{API_VERSION}}+json" + }] + }] } -} +} \ No newline at end of file diff --git a/keystone/content/admin/version.xml.tpl b/keystone/content/admin/version.xml.tpl index 5074d146..b62c9b6a 100644 --- a/keystone/content/admin/version.xml.tpl +++ b/keystone/content/admin/version.xml.tpl @@ -1,27 +1,29 @@ - + - - - - + - + + + + - + - + - - + + + + + \ No newline at end of file diff --git a/keystone/content/service/version.atom.tpl b/keystone/content/service/version.atom.tpl new file mode 100644 index 00000000..e39250d5 --- /dev/null +++ b/keystone/content/service/version.atom.tpl @@ -0,0 +1,29 @@ + + + Available API Versions + 2010-12-22T00:00:00.00Z + http://identity.api.openstack.org/ + OpenStackhttp://www.openstack.org/ + + + http://identity.api.openstack.org/v2.0/ + Version v2.0 + 2011-09-30T00:00:00.00Z + + Version v2.0 BETA (2011-09-30T00:00:00.00Z) + + + http://identity.api.openstack.org/v1.1/ + Version v1.1 + 2011-01-21T11:33:21-06:00 + + Version v1.1 CURRENT (2011-01-21T11:33:21-06:00) + + + http://identity.api.openstack.org/v1.0/ + Version v1.0 + 2009-10-09T11:30:00Z + + Version v1.0 DEPRECATED (2009-10-09T11:30:00Z) + + diff --git a/keystone/content/service/version.json.tpl b/keystone/content/service/version.json.tpl index 58d1fa4d..0eb824bf 100644 --- a/keystone/content/service/version.json.tpl +++ b/keystone/content/service/version.json.tpl @@ -1,38 +1,32 @@ { - "version" : { - "id" : "v{{API_VERSION}}", - "status" : "{{API_VERSION_STATUS}}", - "updated" : "{{API_VERSION_DATE}}", - "links": [ - { - "rel" : "self", - "href" : "http://{{HOST}}:{{PORT}}/v2.0/" - }, - { - "rel" : "describedby", - "type" : "text/html", - "href" : "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/content/" - }, - { - "rel" : "describedby", - "type" : "application/pdf", - "href" : "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/identity-dev-guide-{{API_VERSION}}.pdf" - }, - { - "rel" : "describedby", - "type" : "application/vnd.sun.wadl+xml", - "href" : "http://{{HOST}}:{{PORT}}/v2.0/identity.wadl" - } - ], - "media-types": [ - { - "base" : "application/xml", - "type" : "application/vnd.openstack.identity-v2.0+xml" - }, - { - "base" : "application/json", - "type" : "application/vnd.openstack.identity-v2.0+json" - } - ] + "versions": { + "values": [{ + "id": "v{{API_VERSION}}", + "status": "{{API_VERSION_STATUS}}", + "updated": "{{API_VERSION_DATE}}", + "links": [{ + "rel": "self", + "href": "http://{{HOST}}:{{PORT}}/v2.0/" + }, { + "rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/content/" + }, { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/openstack-identity-service/{{API_VERSION}}/identity-dev-guide-{{API_VERSION}}.pdf" + }, { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://{{HOST}}:{{PORT}}/v2.0/identity.wadl" + }], + "media-types": [{ + "base": "application/xml", + "type": "application/vnd.openstack.identity-v2.0+xml" + }, { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json" + }] + }] } -} +} \ No newline at end of file diff --git a/keystone/content/service/version.xml.tpl b/keystone/content/service/version.xml.tpl index 1d0dcc19..30a46489 100644 --- a/keystone/content/service/version.xml.tpl +++ b/keystone/content/service/version.xml.tpl @@ -1,27 +1,29 @@ - + - - - - + - + + + + - + - + - - + + + + + \ No newline at end of file diff --git a/keystone/controllers/version.py b/keystone/controllers/version.py index 4a4f8645..30fe1c31 100644 --- a/keystone/controllers/version.py +++ b/keystone/controllers/version.py @@ -25,6 +25,10 @@ class VersionController(wsgi.Controller): resp_file = os.path.join(possible_topdir, "keystone/content/%s.xml.tpl" % file) resp.content_type = "application/xml" + elif utils.is_atom_response(req): + resp_file = os.path.join(possible_topdir, + "keystone/content/%s.atom.tpl" % file) + resp.content_type = "application/atom+xml" else: resp_file = os.path.join(possible_topdir, "keystone/content/%s.json.tpl" % file) diff --git a/keystone/middleware/url.py b/keystone/middleware/url.py index 4bc214e4..fa2f6183 100644 --- a/keystone/middleware/url.py +++ b/keystone/middleware/url.py @@ -38,7 +38,8 @@ PATH_PREFIXES = { # Maps supported URL extensions to RESPONSE_ENCODING PATH_SUFFIXES = { '.json': 'json', - '.xml': 'xml'} + '.xml': 'xml', + '.atom': 'atom+xml'} # Maps supported Accept headers to RESPONSE_ENCODING and API_VERSION ACCEPT_HEADERS = { @@ -49,7 +50,8 @@ ACCEPT_HEADERS = { 'application/vnd.openstack.identity-v1.0+json': ('json', '1.0'), 'application/vnd.openstack.identity-v1.0+xml': ('xml', '1.0'), 'application/json': ('json', None), - 'application/xml': ('xml', None)} + 'application/xml': ('xml', None), + 'application/atom+xml': ('atom+xml', None)} DEFAULT_RESPONSE_ENCODING = 'json' DEFAULT_API_VERSION = '2.0' diff --git a/keystone/test/unit/test_controller_version.py b/keystone/test/unit/test_controller_version.py new file mode 100644 index 00000000..c935185e --- /dev/null +++ b/keystone/test/unit/test_controller_version.py @@ -0,0 +1,139 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 OpenStack, LLC. +# +# 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 json +import logging +from lxml import etree +import unittest2 as unittest +from webob import Request + +from keystone.controllers.version import VersionController + +LOGGER = logging.getLogger(__name__) + + +class TestVersionController(unittest.TestCase): + def setUp(self): + self.controller = VersionController({}) + + def _default_version(self, file=None): + """ Verify default response for versions is JSON """ + if file is None: + file = 'admin/version' + req = Request.blank('/') + req.environ = {} + response = self.controller.get_version_info(req, file=file) + self.assertEqual(response.content_type, 'application/json') + data = json.loads(response.body) + self.assertIsNotNone(data) + + def _json_version(self, file=None): + """ Verify JSON response for versions + + Checks that JSON is returned when Accept is set to application/json. + Also checks that verions and version exist and that + values are as expected + + """ + if file is None: + file = 'admin/version' + req = Request.blank('/') + req.headers['Accept'] = 'application/json' + req.environ = {} + response = self.controller.get_version_info(req, file=file) + self.assertEqual(response.content_type, 'application/json') + data = json.loads(response.body) + self.assertIn("versions", data) + versions = data['versions'] + self.assertIn("values", versions) + values = versions['values'] + self.assertIsInstance(values, type([])) + for version in values: + for item in version: + self.assertIn(item, ["id", "status", "updated", "links", + "media-types"]) + + def _xml_version(self, file=None): + """ Verify XML response for versions + + Checks that XML is returned when Accept is set to application/xml. + Also checks that verions and version tags exist and that + attributes are as expected + + """ + if file is None: + file = 'admin/version' + req = Request.blank('/') + req.headers['Accept'] = 'application/xml' + req.environ = {} + response = self.controller.get_version_info(req, file=file) + self.assertEqual(response.content_type, 'application/xml') + data = etree.fromstring(response.body) + self.assertEqual(data.tag, + '{http://docs.openstack.org/common/api/v2.0}versions') + for version in data: + self.assertEqual(version.tag, + '{http://docs.openstack.org/common/api/v2.0}version') + for attribute in version.attrib: + self.assertIn(attribute, ["id", "status", "updated", "links", + "media-types"]) + + def _atom_version(self, file=None): + """ Verify ATOM response for versions + + Checks that ATOM XML is returned when Accept is set to + aapplication/atom+xml. + Also checks that verions and version tags exist and that + attributes are as expected + + """ + if file is None: + file = 'admin/version' + req = Request.blank('/') + req.headers['Accept'] = 'application/atom+xml' + req.environ = {} + response = self.controller.get_version_info(req, file=file) + self.assertEqual(response.content_type, 'application/atom+xml') + data = etree.fromstring(response.body) + self.assertEqual(data.tag, + '{http://www.w3.org/2005/Atom}feed') + + def test_default_version_admin(self): + self._default_version(file='admin/version') + + def test_default_version_service(self): + self._default_version(file='service/version') + + def test_xml_version_admin(self): + self._xml_version(file='admin/version') + + def test_xml_version_service(self): + self._xml_version(file='service/version') + + def test_json_version_admin(self): + self._json_version(file='admin/version') + + def test_json_version_service(self): + self._json_version(file='service/version') + + def test_atom_version_admin(self): + self._atom_version(file='admin/version') + + def test_atom_version_service(self): + self._atom_version(file='service/version') + +if __name__ == '__main__': + unittest.main() diff --git a/keystone/test/unit/test_normalizingfilter.py b/keystone/test/unit/test_normalizingfilter.py index e83af762..1b608729 100644 --- a/keystone/test/unit/test_normalizingfilter.py +++ b/keystone/test/unit/test_normalizingfilter.py @@ -60,6 +60,12 @@ class NormalizingFilterTest(unittest.TestCase): self.assertEqual('/someresource', env['PATH_INFO']) self.assertEqual('application/xml', env['HTTP_ACCEPT']) + def test_atom_extension(self): + env = {'PATH_INFO': '/v2.0/someresource.atom'} + self.filter(env, _start_response) + self.assertEqual('/someresource', env['PATH_INFO']) + self.assertEqual('application/atom+xml', env['HTTP_ACCEPT']) + def test_json_extension(self): env = {'PATH_INFO': '/v2.0/someresource.json'} self.filter(env, _start_response) diff --git a/keystone/utils.py b/keystone/utils.py index 440070de..2660451f 100755 --- a/keystone/utils.py +++ b/keystone/utils.py @@ -33,6 +33,11 @@ def is_json_response(req): return "Accept" in req.headers and "application/json" in req.accept +def is_atom_response(req): + """Returns True when the request wants an ATOM response, False otherwise""" + return "Accept" in req.headers and "application/atom+xml" in req.accept + + def get_app_root(): return os.path.abspath(os.path.dirname(__file__)) -- cgit