Add tests for application credentials

Application credentials were implemented in keystone in Queens. This
patch adds test for create, retrieval, and deleting application
credentials and ensures that application credentials that are created
can be used for authentication. Updating application credentials is not
supported.

bp application-credentials

Change-Id: I3272fee2881fb918fe83961774f4bd27e30cee02
diff --git a/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml
new file mode 100644
index 0000000..53125ef
--- /dev/null
+++ b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    [`blueprint application-credentials <https://blueprints.launchpad.net/keystone/+spec/application-credentials>`_]
+    Tempest can test keystone's application credentials interface. A new client
+    library is added for application credentials, and a new config option,
+    ``[identity-feature-enabled]/application_credentials``, can control whether
+    the application credentials feature is tested (defaults to False,
+    indicating the feature is not enabled in the cloud under test).
diff --git a/tempest/api/identity/admin/v3/test_application_credentials.py b/tempest/api/identity/admin/v3/test_application_credentials.py
new file mode 100644
index 0000000..4a74ef8
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_application_credentials.py
@@ -0,0 +1,48 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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 tempest.api.identity import base
+from tempest import config
+from tempest.lib import decorators
+
+
+CONF = config.CONF
+
+
+class ApplicationCredentialsV3AdminTest(base.BaseApplicationCredentialsV3Test,
+                                        base.BaseIdentityV3AdminTest):
+
+    @decorators.idempotent_id('3b3dd48f-3388-406a-a9e6-4d078a552d0e')
+    def test_create_application_credential_with_roles(self):
+        role = self.setup_test_role()
+        self.os_admin.roles_v3_client.create_user_role_on_project(
+            self.project_id,
+            self.user_id,
+            role['id']
+        )
+
+        app_cred = self.create_application_credential(
+            roles=[{'id': role['id']}])
+        secret = app_cred['secret']
+
+        # Check that the application credential is functional
+        token_id, resp = self.non_admin_token.get_token(
+            app_cred_id=app_cred['id'],
+            app_cred_secret=secret,
+            auth_data=True
+        )
+        self.assertEqual(resp['project']['id'], self.project_id)
+        self.assertEqual(resp['roles'][0]['id'], role['id'])
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 6edb8f3..68f2c07 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -190,6 +190,8 @@
         cls.non_admin_catalog_client = cls.os_primary.catalog_client
         cls.non_admin_versions_client =\
             cls.os_primary.identity_versions_v3_client
+        cls.non_admin_app_creds_client = \
+            cls.os_primary.application_credentials_client
 
 
 class BaseIdentityV3AdminTest(BaseIdentityV3Test):
@@ -289,3 +291,30 @@
             test_utils.call_and_ignore_notfound_exc,
             self.delete_domain, domain['id'])
         return domain
+
+
+class BaseApplicationCredentialsV3Test(BaseIdentityV3Test):
+
+    @classmethod
+    def skip_checks(cls):
+        super(BaseApplicationCredentialsV3Test, cls).skip_checks()
+        if not CONF.identity_feature_enabled.application_credentials:
+            raise cls.skipException("Application credentials are not available"
+                                    " in this environment")
+
+    @classmethod
+    def resource_setup(cls):
+        super(BaseApplicationCredentialsV3Test, cls).resource_setup()
+        cls.user_id = cls.os_primary.credentials.user_id
+        cls.project_id = cls.os_primary.credentials.project_id
+
+    def create_application_credential(self, name=None, **kwargs):
+        name = name or data_utils.rand_name('application_credential')
+        application_credential = (
+            self.non_admin_app_creds_client.create_application_credential(
+                self.user_id, name=name, **kwargs))['application_credential']
+        self.addCleanup(
+            self.non_admin_app_creds_client.delete_application_credential,
+            self.user_id,
+            application_credential['id'])
+        return application_credential
diff --git a/tempest/api/identity/v3/test_application_credentials.py b/tempest/api/identity/v3/test_application_credentials.py
new file mode 100644
index 0000000..caf0b1e
--- /dev/null
+++ b/tempest/api/identity/v3/test_application_credentials.py
@@ -0,0 +1,85 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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 datetime
+
+from oslo_utils import timeutils
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib import decorators
+
+
+CONF = config.CONF
+
+
+class ApplicationCredentialsV3Test(base.BaseApplicationCredentialsV3Test):
+
+    def _list_app_creds(self, name=None):
+        kwargs = dict(user_id=self.user_id)
+        if name:
+            kwargs.update(name=name)
+        return self.non_admin_app_creds_client.list_application_credentials(
+            **kwargs)['application_credentials']
+
+    @decorators.idempotent_id('8080c75c-eddc-4786-941a-c2da7039ae61')
+    def test_create_application_credential(self):
+        app_cred = self.create_application_credential()
+
+        # Check that the secret appears in the create response
+        secret = app_cred['secret']
+
+        # Check that the secret is not retrievable after initial create
+        app_cred = self.non_admin_app_creds_client.show_application_credential(
+            user_id=self.user_id,
+            application_credential_id=app_cred['id']
+        )['application_credential']
+        self.assertNotIn('secret', app_cred)
+
+        # Check that the application credential is functional
+        token_id, resp = self.non_admin_token.get_token(
+            app_cred_id=app_cred['id'],
+            app_cred_secret=secret,
+            auth_data=True
+        )
+        self.assertEqual(resp['project']['id'], self.project_id)
+
+    @decorators.idempotent_id('852daf0c-42b5-4239-8466-d193d0543ed3')
+    def test_create_application_credential_expires(self):
+        expires_at = timeutils.utcnow() + datetime.timedelta(hours=1)
+
+        app_cred = self.create_application_credential(expires_at=expires_at)
+
+        expires_str = expires_at.isoformat()
+        self.assertEqual(expires_str, app_cred['expires_at'])
+
+    @decorators.idempotent_id('ff0cd457-6224-46e7-b79e-0ada4964a8a6')
+    def test_list_application_credentials(self):
+        self.create_application_credential()
+        self.create_application_credential()
+
+        app_creds = self._list_app_creds()
+        self.assertEqual(2, len(app_creds))
+
+    @decorators.idempotent_id('9bb5e5cc-5250-493a-8869-8b665f6aa5f6')
+    def test_query_application_credentials(self):
+        self.create_application_credential()
+        app_cred_two = self.create_application_credential()
+        app_cred_two_name = app_cred_two['name']
+
+        app_creds = self._list_app_creds(name=app_cred_two_name)
+        self.assertEqual(1, len(app_creds))
+        self.assertEqual(app_cred_two_name, app_creds[0]['name'])
diff --git a/tempest/clients.py b/tempest/clients.py
index d75a712..0d16748 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -199,6 +199,8 @@
         self.catalog_client = self.identity_v3.CatalogClient(**params_v3)
         self.project_tags_client = self.identity_v3.ProjectTagsClient(
             **params_v3)
+        self.application_credentials_client = \
+            self.identity_v3.ApplicationCredentialsClient(**params_v3)
 
         # Token clients do not use the catalog. They only need default_params.
         # They read auth_url, so they should only be set if the corresponding
diff --git a/tempest/config.py b/tempest/config.py
index a2ccb84..7133b3d 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -240,7 +240,13 @@
                      'settings enabled?'),
     cfg.BoolOpt('project_tags',
                 default=False,
-                help='Is the project tags identity v3 API available?')
+                help='Is the project tags identity v3 API available?'),
+    # Application credentials is a default feature in Queens. This config
+    # option can removed once Pike is EOL.
+    cfg.BoolOpt('application_credentials',
+                default=False,
+                help='Does the environment have application credentials '
+                     'enabled?')
 ]
 
 compute_group = cfg.OptGroup(name='compute',
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index f302455..da1c51c 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+from tempest.lib.services.identity.v3.application_credentials_client import \
+    ApplicationCredentialsClient
 from tempest.lib.services.identity.v3.catalog_client import \
     CatalogClient
 from tempest.lib.services.identity.v3.credentials_client import \
@@ -46,11 +48,11 @@
 from tempest.lib.services.identity.v3.users_client import UsersClient
 from tempest.lib.services.identity.v3.versions_client import VersionsClient
 
-__all__ = ['CatalogClient', 'CredentialsClient', 'DomainsClient',
-           'DomainConfigurationClient', 'EndPointGroupsClient',
-           'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient',
-           'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient',
-           'OAUTHTokenClient', 'PoliciesClient', 'ProjectsClient',
-           'ProjectTagsClient', 'RegionsClient', 'RoleAssignmentsClient',
-           'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient',
-           'UsersClient', 'VersionsClient']
+__all__ = ['ApplicationCredentialsClient', 'CatalogClient',
+           'CredentialsClient', 'DomainsClient', 'DomainConfigurationClient',
+           'EndPointGroupsClient', 'EndPointsClient', 'EndPointsFilterClient',
+           'GroupsClient', 'IdentityClient', 'InheritedRolesClient',
+           'OAUTHConsumerClient', 'OAUTHTokenClient', 'PoliciesClient',
+           'ProjectsClient', 'ProjectTagsClient', 'RegionsClient',
+           'RoleAssignmentsClient', 'RolesClient', 'ServicesClient',
+           'V3TokenClient', 'TrustsClient', 'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/application_credentials_client.py b/tempest/lib/services/identity/v3/application_credentials_client.py
new file mode 100644
index 0000000..557aa9e
--- /dev/null
+++ b/tempest/lib/services/identity/v3/application_credentials_client.py
@@ -0,0 +1,83 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3/index.html#application-credentials
+"""
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class ApplicationCredentialsClient(rest_client.RestClient):
+    api_version = "v3"
+
+    def create_application_credential(self, user_id, **kwargs):
+        """Creates an application credential.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/identity/v3/index.html#create-application-credential
+        """
+        post_body = json.dumps({'application_credential': kwargs})
+        resp, body = self.post('users/%s/application_credentials' % user_id,
+                               post_body)
+        self.expected_success(201, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def show_application_credential(self, user_id, application_credential_id):
+        """Gets details of an application credential.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/identity/v3/index.html#show-application-credential-details
+        """
+        resp, body = self.get('users/%s/application_credentials/%s' %
+                              (user_id, application_credential_id))
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_application_credentials(self, user_id, **params):
+        """Lists out all of a user's application credentials.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/identity/v3/index.html#list-application-credentials
+        """
+        url = 'users/%s/application_credentials' % user_id
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_application_credential(self, user_id,
+                                      application_credential_id):
+        """Deletes an application credential.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/identity/v3/index.html#delete-application-credential
+        """
+        resp, body = self.delete('users/%s/application_credentials/%s' %
+                                 (user_id, application_credential_id))
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py
index 33f6f16..d591f03 100644
--- a/tempest/lib/services/identity/v3/token_client.py
+++ b/tempest/lib/services/identity/v3/token_client.py
@@ -51,7 +51,8 @@
     def auth(self, user_id=None, username=None, password=None, project_id=None,
              project_name=None, user_domain_id=None, user_domain_name=None,
              project_domain_id=None, project_domain_name=None, domain_id=None,
-             domain_name=None, token=None):
+             domain_name=None, token=None, app_cred_id=None,
+             app_cred_secret=None):
         """Obtains a token from the authentication service
 
         :param user_id: user id
@@ -109,6 +110,13 @@
             if _domain:
                 id_obj['password']['user']['domain'] = _domain
 
+        if app_cred_id and app_cred_secret:
+            id_obj['methods'].append('application_credential')
+            id_obj['application_credential'] = {
+                'id': app_cred_id,
+                'secret': app_cred_secret,
+            }
+
         if (project_id or project_name):
             _project = dict()
 
diff --git a/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
new file mode 100644
index 0000000..9bf9b68
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
@@ -0,0 +1,156 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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 tempest.lib.services.identity.v3 import application_credentials_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestApplicationCredentialsClient(base.BaseServiceTest):
+    FAKE_CREATE_APP_CRED = {
+        "application_credential": {
+            "description": "fake application credential",
+            "roles": [
+                {
+                    "id": "c60fdd45",
+                    "domain_id": None,
+                    "name": "Member"
+                }
+            ],
+            "expires_at": "2019-02-27T18:30:59.999999Z",
+            "secret": "_BVq0xU5L",
+            "unrestricted": None,
+            "project_id": "ddef321",
+            "id": "5499a186",
+            "name": "one"
+        }
+    }
+
+    FAKE_LIST_APP_CREDS = {
+        "application_credentials": [
+            {
+                "description": "fake application credential",
+                "roles": [
+                    {
+                        "domain_id": None,
+                        "name": "Member",
+                        "id": "c60fdd45",
+                    }
+                ],
+                "expires_at": "2018-02-27T18:30:59.999999Z",
+                "unrestricted": None,
+                "project_id": "ddef321",
+                "id": "5499a186",
+                "name": "one"
+            },
+            {
+                "description": None,
+                "roles": [
+                    {
+                        "id": "0f1837c8",
+                        "domain_id": None,
+                        "name": "anotherrole"
+                    },
+                    {
+                        "id": "c60fdd45",
+                        "domain_id": None,
+                        "name": "Member"
+                    }
+                ],
+                "expires_at": None,
+                "unrestricted": None,
+                "project_id": "c5403d938",
+                "id": "d441c904f",
+                "name": "two"
+            }
+        ]
+    }
+
+    FAKE_APP_CRED_INFO = {
+        "application_credential": {
+            "description": None,
+            "roles": [
+                {
+                    "domain_id": None,
+                    "name": "Member",
+                    "id": "c60fdd45",
+                }
+            ],
+            "expires_at": None,
+            "unrestricted": None,
+            "project_id": "ddef321",
+            "id": "5499a186",
+            "name": "one"
+        }
+    }
+
+    def setUp(self):
+        super(TestApplicationCredentialsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = \
+            application_credentials_client.ApplicationCredentialsClient(
+                fake_auth, 'identity', 'regionOne')
+
+    def _test_create_app_cred(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_application_credential,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_APP_CRED,
+            bytes_body,
+            status=201,
+            user_id="123456")
+
+    def _test_show_app_cred(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_application_credential,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_APP_CRED_INFO,
+            bytes_body,
+            user_id="123456",
+            application_credential_id="5499a186")
+
+    def _test_list_app_creds(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_application_credentials,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_APP_CREDS,
+            bytes_body,
+            user_id="123456")
+
+    def test_create_application_credential_with_str_body(self):
+        self._test_create_app_cred()
+
+    def test_create_application_credential_with_bytes_body(self):
+        self._test_create_app_cred(bytes_body=True)
+
+    def test_show_application_credential_with_str_body(self):
+        self._test_show_app_cred()
+
+    def test_show_application_credential_with_bytes_body(self):
+        self._test_show_app_cred(bytes_body=True)
+
+    def test_list_application_credential_with_str_body(self):
+        self._test_list_app_creds()
+
+    def test_list_application_credential_with_bytes_body(self):
+        self._test_list_app_creds(bytes_body=True)
+
+    def test_delete_trust(self):
+        self.check_service_client_function(
+            self.client.delete_application_credential,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            user_id="123456",
+            application_credential_id="5499a186",
+            status=204)