Add tests for getting nova api version details

This commit adds the necessary client and test bits to verify that there is
an api version detail response from all the versioned urls presented by the
GET of the top level version document.

Co-Authored-By: Sean Dague <sean@dague.net>

Change-Id: I70294329870de46b2c4c8775a1d7920bee403954
Depends-On: If2ce6c9c9e2fde554b4e44dc99ec70d087c3f682
Related-Bug: #1491579
diff --git a/tempest/api/compute/test_versions.py b/tempest/api/compute/test_versions.py
index 369cf31..f94cee6 100644
--- a/tempest/api/compute/test_versions.py
+++ b/tempest/api/compute/test_versions.py
@@ -20,5 +20,53 @@
 
     @test.idempotent_id('6c0a0990-43b6-4529-9b61-5fd8daf7c55c')
     def test_list_api_versions(self):
+        """Test that a get of the unversioned url returns the choices doc.
+
+        A key feature in OpenStack services is the idea that you can
+        GET / on the service and get a list of the versioned endpoints
+        that you can access. This comes back as a status 300
+        request. It's important that this is available to API
+        consumers to discover the API they can use.
+
+        """
         result = self.versions_client.list_versions()
-        self.assertIsNotNone(result)
+        versions = result['versions']
+        # NOTE(sdague): at a later point we may want to loosen this
+        # up, but for now this should be true of all Novas deployed.
+        self.assertEqual(versions[0]['id'], 'v2.0',
+                         "The first listed version should be v2.0")
+
+    @test.idempotent_id('b953a29e-929c-4a8e-81be-ec3a7e03cb76')
+    def test_get_version_details(self):
+        """Test individual version endpoints info works.
+
+        In addition to the GET / version request, there is also a
+        version info document stored at the top of the versioned
+        endpoints. This provides access to details about that
+        endpoint, including min / max version if that implements
+        microversions.
+
+        This test starts with the version list, iterates all the
+        returned endpoints, and fetches them. This will also ensure
+        that all the version links are followable constructs which
+        will help detect configuration issues when SSL termination
+        isn't done completely for a site.
+
+        """
+        result = self.versions_client.list_versions()
+        versions = result['versions']
+
+        # iterate through all the versions that are returned and
+        # attempt to get their version documents.
+        for version in versions:
+            links = [x for x in version['links'] if x['rel'] == 'self']
+            self.assertEqual(
+                len(links), 1,
+                "There should only be 1 self link in %s" % version)
+            link = links[0]
+            # this is schema validated
+            result = self.versions_client.get_version_by_url(link['href'])
+            # ensure the new self link matches the old one
+            newlinks = [x for x in result['version']['links']
+                        if x['rel'] == 'self']
+            self.assertEqual(links, newlinks)
diff --git a/tempest/api_schema/response/compute/v2_1/versions.py b/tempest/api_schema/response/compute/v2_1/versions.py
index f08695c..08a9fab 100644
--- a/tempest/api_schema/response/compute/v2_1/versions.py
+++ b/tempest/api_schema/response/compute/v2_1/versions.py
@@ -12,6 +12,9 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import copy
+
+
 _version = {
     'type': 'object',
     'properties': {
@@ -23,6 +26,7 @@
                 'properties': {
                     'href': {'type': 'string', 'format': 'uri'},
                     'rel': {'type': 'string'},
+                    'type': {'type': 'string'},
                 },
                 'required': ['href', 'rel'],
                 'additionalProperties': False
@@ -31,10 +35,18 @@
         'status': {'type': 'string'},
         'updated': {'type': 'string', 'format': 'date-time'},
         'version': {'type': 'string'},
-        'min_version': {'type': 'string'}
+        'min_version': {'type': 'string'},
+        'media-types': {
+            'type': 'array',
+            'properties': {
+                'base': {'type': 'string'},
+                'type': {'type': 'string'},
+            }
+        },
     },
     # NOTE: version and min_version have been added since Kilo,
     # so they should not be required.
+    # NOTE(sdague): media-types only shows up in single version requests.
     'required': ['id', 'links', 'status', 'updated'],
     'additionalProperties': False
 }
@@ -53,3 +65,46 @@
         'additionalProperties': False
     }
 }
+
+
+_detail_get_version = copy.deepcopy(_version)
+_detail_get_version['properties'].pop('min_version')
+_detail_get_version['properties'].pop('version')
+_detail_get_version['properties'].pop('updated')
+_detail_get_version['properties']['media-types'] = {
+    'type': 'array',
+    'items': {
+        'type': 'object',
+        'properties': {
+            'base': {'type': 'string'},
+            'type': {'type': 'string'}
+        }
+    }
+}
+_detail_get_version['required'] = ['id', 'links', 'status', 'media-types']
+
+get_version = {
+    'status_code': [300],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'choices': {
+                'type': 'array',
+                'items': _detail_get_version
+            }
+        },
+        'required': ['choices'],
+        'additionalProperties': False
+    }
+}
+
+get_one_version = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'version': _version
+        },
+        'additionalProperties': False
+    }
+}
diff --git a/tempest/services/compute/json/versions_client.py b/tempest/services/compute/json/versions_client.py
index cbad02c..48c0e8d 100644
--- a/tempest/services/compute/json/versions_client.py
+++ b/tempest/services/compute/json/versions_client.py
@@ -21,7 +21,7 @@
 
 class VersionsClient(service_client.ServiceClient):
 
-    def list_versions(self):
+    def _get_base_version_url(self):
         # NOTE: The URL which is gotten from keystone's catalog contains
         # API version and project-id like "v2/{project-id}", but we need
         # to access the URL which doesn't contain them for getting API
@@ -29,9 +29,27 @@
         # get().
         endpoint = self.base_url
         url = urllib.parse.urlparse(endpoint)
-        version_url = '%s://%s/' % (url.scheme, url.netloc)
+        return '%s://%s/' % (url.scheme, url.netloc)
 
+    def list_versions(self):
+        version_url = self._get_base_version_url()
         resp, body = self.raw_request(version_url, 'GET')
         body = json.loads(body)
         self.validate_response(schema.list_versions, resp, body)
         return service_client.ResponseBody(resp, body)
+
+    def get_version_by_url(self, version_url):
+        """Get the version document by url.
+
+        This gets the version document for a url, useful in testing
+        the contents of things like /v2/ or /v2.1/ in Nova. That
+        controller needs authenticated access, so we have to get
+        ourselves a token before making the request.
+
+        """
+        # we need a token for this request
+        resp, body = self.raw_request(version_url, 'GET',
+                                      {'X-Auth-Token': self.token})
+        body = json.loads(body)
+        self.validate_response(schema.get_one_version, resp, body)
+        return service_client.ResponseBody(resp, body)