Merge "Add identity v3 project tags client"
diff --git a/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
new file mode 100644
index 0000000..dfbcc7d
--- /dev/null
+++ b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - |
+    Add ``project_tags_client`` to the identity v3 library. This feature
+    enables the possibility of invoking the following API actions:
+
+    * update_project_tag
+    * list_project_tags
+    * update_all_project_tags
+    * check_project_tag_existence
+    * delete_project_tag
+    * delete_all_project_tags
diff --git a/tempest/api/identity/admin/v3/test_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
new file mode 100644
index 0000000..d05173b
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -0,0 +1,66 @@
+# Copyright 2018 AT&T Corporation.
+# 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 testtools
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+
+    @decorators.idempotent_id('7c123aac-999d-416a-a0fb-84b915ab10de')
+    @testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
+                          'Project tags not available.')
+    def test_list_update_delete_project_tags(self):
+        project = self.setup_test_project()
+
+        # Create a tag for testing.
+        tag = data_utils.rand_name('tag')
+        # NOTE(felipemonteiro): The response body for create is empty.
+        self.project_tags_client.update_project_tag(project['id'], tag)
+
+        # Verify that the tag was created.
+        self.project_tags_client.check_project_tag_existence(
+            project['id'], tag)
+
+        # Verify that updating the project tags works.
+        tags_to_update = [data_utils.rand_name('tag') for _ in range(3)]
+        updated_tags = self.project_tags_client.update_all_project_tags(
+            project['id'], tags_to_update)['tags']
+        self.assertEqual(sorted(tags_to_update), sorted(updated_tags))
+
+        # Verify that listing project tags works.
+        retrieved_tags = self.project_tags_client.list_project_tags(
+            project['id'])['tags']
+        self.assertEqual(sorted(tags_to_update), sorted(retrieved_tags))
+
+        # Verify that deleting a project tag works.
+        self.project_tags_client.delete_project_tag(
+            project['id'], tags_to_update[0])
+        self.assertRaises(lib_exc.NotFound,
+                          self.project_tags_client.check_project_tag_existence,
+                          project['id'], tags_to_update[0])
+
+        # Verify that deleting all project tags works.
+        self.project_tags_client.delete_all_project_tags(project['id'])
+        retrieved_tags = self.project_tags_client.list_project_tags(
+            project['id'])['tags']
+        self.assertEmpty(retrieved_tags)
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 9edccbb..6edb8f3 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -228,6 +228,7 @@
         cls.domain_config_client = cls.os_admin.domain_config_client
         cls.endpoint_filter_client = cls.os_admin.endpoint_filter_client
         cls.endpoint_groups_client = cls.os_admin.endpoint_groups_client
+        cls.project_tags_client = cls.os_admin.project_tags_client
 
         if CONF.identity.admin_domain_scope:
             # NOTE(andreaf) When keystone policy requires it, the identity
diff --git a/tempest/clients.py b/tempest/clients.py
index b06eafb..d75a712 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -197,6 +197,8 @@
         self.endpoint_groups_client = self.identity_v3.EndPointGroupsClient(
             **params_v3)
         self.catalog_client = self.identity_v3.CatalogClient(**params_v3)
+        self.project_tags_client = self.identity_v3.ProjectTagsClient(
+            **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/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index a539d08..f302455 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -33,6 +33,8 @@
 from tempest.lib.services.identity.v3.oauth_token_client import \
     OAUTHTokenClient
 from tempest.lib.services.identity.v3.policies_client import PoliciesClient
+from tempest.lib.services.identity.v3.project_tags_client import \
+    ProjectTagsClient
 from tempest.lib.services.identity.v3.projects_client import ProjectsClient
 from tempest.lib.services.identity.v3.regions_client import RegionsClient
 from tempest.lib.services.identity.v3.role_assignments_client import \
@@ -49,6 +51,6 @@
            'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient',
            'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient',
            'OAUTHTokenClient', 'PoliciesClient', 'ProjectsClient',
-           'RegionsClient', 'RoleAssignmentsClient', 'RolesClient',
-           'ServicesClient', 'V3TokenClient', 'TrustsClient', 'UsersClient',
-           'VersionsClient']
+           'ProjectTagsClient', 'RegionsClient', 'RoleAssignmentsClient',
+           'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient',
+           'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/project_tags_client.py b/tempest/lib/services/identity/v3/project_tags_client.py
new file mode 100644
index 0000000..dd1a2a5
--- /dev/null
+++ b/tempest/lib/services/identity/v3/project_tags_client.py
@@ -0,0 +1,80 @@
+# Copyright 2018 AT&T Corporation.
+# 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 oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class ProjectTagsClient(rest_client.RestClient):
+    api_version = "v3"
+
+    def update_project_tag(self, project_id, tag):
+        """Updates the specified tag and adds it to the project's list of tags.
+
+        """
+        url = 'projects/%s/tags/%s' % (project_id, tag)
+        resp, body = self.put(url, '{}')
+        # NOTE(felipemonteiro): This API endpoint returns 201 AND an empty
+        # response body, which is consistent with the spec:
+        # https://specs.openstack.org/openstack/api-wg/guidelines/tags.html#addressing-individual-tags
+        self.expected_success(201, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_project_tags(self, project_id):
+        """List tags for a project."""
+        url = "projects/%s/tags" % project_id
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def update_all_project_tags(self, project_id, tags, **kwargs):
+        """Updates all the tags for a project.
+
+        Any existing tags not specified will be deleted.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/identity/v3/#modify-tag-list-for-a-project
+        """
+        body = {'tags': tags}
+        if kwargs:
+            body.update(kwargs)
+        put_body = json.dumps(body)
+        resp, body = self.put('projects/%s/tags' % project_id, put_body)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
+    def check_project_tag_existence(self, project_id, tag):
+        """Check if a project contains a tag."""
+        url = 'projects/%s/tags/%s' % (project_id, tag)
+        resp, body = self.get(url)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_project_tag(self, project_id, tag):
+        """Delete a project tag."""
+        url = 'projects/%s/tags/%s' % (project_id, tag)
+        resp, body = self.delete(url)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_all_project_tags(self, project_id):
+        """Delete all tags from a project."""
+        resp, body = self.delete('projects/%s/tags' % project_id)
+        self.expected_success(204, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/tests/lib/services/identity/v3/test_project_tags_client.py b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
new file mode 100644
index 0000000..2d65a29
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
@@ -0,0 +1,104 @@
+# Copyright 2018 AT&T Corporation.
+#
+# 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 project_tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestProjectTagsClient(base.BaseServiceTest):
+
+    FAKE_PROJECT_ID = "0c4e939acacf4376bdcd1129f1a054ad"
+
+    FAKE_PROJECT_TAG = "foo"
+
+    FAKE_PROJECT_TAGS = ["foo", "bar"]
+
+    def setUp(self):
+        super(TestProjectTagsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = project_tags_client.ProjectTagsClient(fake_auth,
+                                                            'identity',
+                                                            'regionOne')
+
+    def _test_update_project_tag(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_project_tag,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            project_id=self.FAKE_PROJECT_ID,
+            tag=self.FAKE_PROJECT_TAG,
+            status=201)
+
+    def _test_list_project_tags(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_project_tags,
+            'tempest.lib.common.rest_client.RestClient.get',
+            {"tags": self.FAKE_PROJECT_TAGS},
+            bytes_body,
+            project_id=self.FAKE_PROJECT_ID)
+
+    def _test_update_all_project_tags(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.update_all_project_tags,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {"tags": self.FAKE_PROJECT_TAGS},
+            bytes_body,
+            project_id=self.FAKE_PROJECT_ID,
+            tags=self.FAKE_PROJECT_TAGS)
+
+    def test_update_project_tag_with_str_body(self):
+        self._test_update_project_tag()
+
+    def test_update_project_tag_with_bytes_body(self):
+        self._test_update_project_tag(bytes_body=True)
+
+    def test_list_project_tags_with_str_body(self):
+        self._test_list_project_tags()
+
+    def test_list_project_tags_with_bytes_body(self):
+        self._test_list_project_tags(bytes_body=True)
+
+    def test_update_all_project_tags_with_str_body(self):
+        self._test_update_all_project_tags()
+
+    def test_update_all_project_tags_with_bytes_body(self):
+        self._test_update_all_project_tags(bytes_body=True)
+
+    def test_check_project_project_tag_existence(self):
+        self.check_service_client_function(
+            self.client.check_project_tag_existence,
+            'tempest.lib.common.rest_client.RestClient.get',
+            {},
+            project_id=self.FAKE_PROJECT_ID,
+            tag=self.FAKE_PROJECT_TAG,
+            status=204)
+
+    def test_delete_project_tag(self):
+        self.check_service_client_function(
+            self.client.delete_project_tag,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            project_id=self.FAKE_PROJECT_ID,
+            tag=self.FAKE_PROJECT_TAG,
+            status=204)
+
+    def test_delete_all_project_tags(self):
+        self.check_service_client_function(
+            self.client.delete_all_project_tags,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            project_id=self.FAKE_PROJECT_ID,
+            status=204)