diff --git a/storage/google/cloud/storage/client.py b/storage/google/cloud/storage/client.py index 511be69bc45f..c46f1784fe25 100644 --- a/storage/google/cloud/storage/client.py +++ b/storage/google/cloud/storage/client.py @@ -26,6 +26,7 @@ from google.cloud.storage.batch import Batch from google.cloud.storage.bucket import Bucket from google.cloud.storage.blob import Blob +from google.cloud.storage.hmac_key import HMACKeyMetadata _marker = object() @@ -561,6 +562,98 @@ def list_buckets( extra_params=extra_params, ) + def create_hmac_key(self, service_account_email, project_id=None): + """Create an HMAC key for a service account. + + :type service_account_email: str + :param service_account_email: e-mail address of the service account + + :type project_id: str + :param project_id: (Optional) explicit project ID for the key. + Defaults to the client's project. + + :rtype: + Tuple[:class:`~google.cloud.storage.hmac_key.HMACKeyMetadata`, str] + :returns: metadata for the created key, plus the bytes of the key's secret, which is an 40-character base64-encoded string. + """ + if project_id is None: + project_id = self.project + + path = "/projects/{}/hmacKeys".format(project_id) + qs_params = {"serviceAccountEmail": service_account_email} + api_response = self._connection.api_request( + method="POST", path=path, query_params=qs_params + ) + metadata = HMACKeyMetadata(self) + metadata._properties = api_response["metadata"] + secret = api_response["secret"] + return metadata, secret + + def list_hmac_keys( + self, + max_results=None, + service_account_email=None, + show_deleted_keys=None, + project_id=None, + ): + """List HMAC keys for a project. + + :type max_results: int + :param max_results: + (Optional) max number of keys to return in a given page. + + :type service_account_email: str + :param service_account_email: + (Optional) limit keys to those created by the given service account. + + :type show_deleted_keys: bool + :param show_deleted_keys: + (Optional) included deleted keys in the list. Default is to + exclude them. + + :type project_id: str + :param project_id: (Optional) explicit project ID for the key. + Defaults to the client's project. + + :rtype: + Tuple[:class:`~google.cloud.storage.hmac_key.HMACKeyMetadata`, str] + :returns: metadata for the created key, plus the bytes of the key's secret, which is an 40-character base64-encoded string. + """ + if project_id is None: + project_id = self.project + + path = "/projects/{}/hmacKeys".format(project_id) + extra_params = {} + + if service_account_email is not None: + extra_params["serviceAccountEmail"] = service_account_email + + if show_deleted_keys is not None: + extra_params["showDeletedKeys"] = show_deleted_keys + + return page_iterator.HTTPIterator( + client=self, + api_request=self._connection.api_request, + path=path, + item_to_value=_item_to_hmac_key_metadata, + max_results=max_results, + extra_params=extra_params, + ) + + def get_hmac_key_metadata(self, access_id, project_id=None): + """Return a metadata instance for the given HMAC key. + + :type access_id: str + :param access_id: Unique ID of an existing key. + + :type project_id: str + :param project_id: (Optional) project ID of an existing key. + Defaults to client's project. + """ + metadata = HMACKeyMetadata(self, access_id, project_id) + metadata.reload() # raises NotFound for missing key + return metadata + def _item_to_bucket(iterator, item): """Convert a JSON bucket to the native object. @@ -578,3 +671,20 @@ def _item_to_bucket(iterator, item): bucket = Bucket(iterator.client, name) bucket._set_properties(item) return bucket + + +def _item_to_hmac_key_metadata(iterator, item): + """Convert a JSON key metadata resource to the native object. + + :type iterator: :class:`~google.api_core.page_iterator.Iterator` + :param iterator: The iterator that has retrieved the item. + + :type item: dict + :param item: An item to be converted to a key metadata instance. + + :rtype: :class:`~google.cloud.storage.hmac_key.HMACKeyMetadata` + :returns: The next key metadata instance in the page. + """ + metadata = HMACKeyMetadata(iterator.client) + metadata._properties = item + return metadata diff --git a/storage/google/cloud/storage/hmac_key.py b/storage/google/cloud/storage/hmac_key.py new file mode 100644 index 000000000000..257719ca80d0 --- /dev/null +++ b/storage/google/cloud/storage/hmac_key.py @@ -0,0 +1,207 @@ +# Copyright 2019 Google 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. + +from google.cloud.exceptions import NotFound +from google.cloud._helpers import _rfc3339_to_datetime + + +class HMACKeyMetadata(object): + """Metadata about an HMAC service account key withn Cloud Storage. + + :type client: :class:`~google.cloud.stoage.client.Client` + :param client: client associated with the key metadata. + + :type access_id: str + :param access_id: (Optional) unique ID of an existing key. + + :type project_id: str + :param project_id: (Optional) project ID of an existing key. + Defaults to client's project. + """ + + ACTIVE_STATE = "ACTIVE" + """Key is active, and may be used to sign requests.""" + INACTIVE_STATE = "INACTIVE" + """Key is inactive, and may not be used to sign requests. + + It can be re-activated via :meth:`update`. + """ + DELETED_STATE = "DELETED" + """Key is deleted. It cannot be re-activated.""" + + _SETTABLE_STATES = (ACTIVE_STATE, INACTIVE_STATE) + + def __init__(self, client, access_id=None, project_id=None): + self._client = client + self._properties = {} + + if access_id is not None: + self._properties["accessId"] = access_id + + if project_id is not None: + self._properties["projectId"] = project_id + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + + return self._client == other._client and self.access_id == other.access_id + + def __hash__(self): + return hash(self._client) + hash(self.access_id) + + @property + def access_id(self): + """ID of the key. + + :rtype: str or None + :returns: unique identifier of the key within a project. + """ + return self._properties.get("accessId") + + @property + def etag(self): + """ETag identifying the version of the key metadata. + + :rtype: str or None + :returns: ETag for the version of the key's metadata. + """ + return self._properties.get("etag") + + @property + def project(self): + """Project ID associated with the key. + + :rtype: str or None + :returns: project identfier for the key. + """ + return self._properties.get("projectId") + + @property + def service_account_email(self): + """Service account e-mail address associated with the key. + + :rtype: str or None + :returns: e-mail address for the service account which created the key. + """ + return self._properties.get("serviceAccountEmail") + + @property + def state(self): + """Get / set key's state. + + One of: + - ``ACTIVE`` + - ``INACTIVE`` + - ``DELETED`` + + :rtype: str or None + :returns: key's current state. + """ + return self._properties.get("state") + + @state.setter + def state(self, value): + if value not in self._SETTABLE_STATES: + raise ValueError( + "State may only be set to one of: {}".format( + ", ".join(self._SETTABLE_STATES) + ) + ) + + self._properties["state"] = value + + @property + def time_created(self): + """Retrieve the timestamp at which the HMAC key was created. + + :rtype: :class:`datetime.datetime` or ``NoneType`` + :returns: Datetime object parsed from RFC3339 valid timestamp, or + ``None`` if the bucket's resource has not been loaded + from the server. + """ + value = self._properties.get("timeCreated") + if value is not None: + return _rfc3339_to_datetime(value) + + @property + def updated(self): + """Retrieve the timestamp at which the HMAC key was created. + + :rtype: :class:`datetime.datetime` or ``NoneType`` + :returns: Datetime object parsed from RFC3339 valid timestamp, or + ``None`` if the bucket's resource has not been loaded + from the server. + """ + value = self._properties.get("updated") + if value is not None: + return _rfc3339_to_datetime(value) + + @property + def path(self): + """Resource path for the metadata's key.""" + + if self.access_id is None: + raise ValueError("No 'access_id' set.") + + project = self.project + if project is None: + project = self._client.project + + return "/projects/{}/hmacKeys/{}".format(project, self.access_id) + + def exists(self): + """Determine whether or not the key for this metadata exists. + + :rtype: bool + :returns: True if the key exists in Cloud Storage. + """ + try: + self._client._connection.api_request(method="GET", path=self.path) + except NotFound: + return False + else: + return True + + def reload(self): + """Reload properties from Cloud Storage. + + :raises :class:`~google.api_core.exceptions.NotFound`: + if the key does not exist on the back-end. + """ + self._properties = self._client._connection.api_request( + method="GET", path=self.path + ) + + def update(self): + """Save writable properties to Cloud Storage. + + :raises :class:`~google.api_core.exceptions.NotFound`: + if the key does not exist on the back-end. + """ + payload = {"state": self.state} + self._properties = self._client._connection.api_request( + method="PUT", path=self.path, data=payload + ) + + def delete(self): + """Delete the key from Cloud Storage. + + :raises :class:`~google.api_core.exceptions.NotFound`: + if the key does not exist on the back-end. + """ + if self.state != self.INACTIVE_STATE: + raise ValueError("Cannot delete key if not in 'INACTIVE' state.") + + self._client._connection.api_request(method="DELETE", path=self.path) diff --git a/storage/tests/system.py b/storage/tests/system.py index e2784fd5155a..1a63409fdd24 100644 --- a/storage/tests/system.py +++ b/storage/tests/system.py @@ -90,6 +90,19 @@ def tearDownModule(): class TestClient(unittest.TestCase): + def setUp(self): + self.case_hmac_keys_to_delete = [] + + def tearDown(self): + from google.cloud.storage.hmac_key import HMACKeyMetadata + + for hmac_key in self.case_hmac_keys_to_delete: + if hmac_key.state == HMACKeyMetadata.ACTIVE_STATE: + hmac_key.state = HMACKeyMetadata.INACTIVE_STATE + hmac_key.update() + if hmac_key.state == HMACKeyMetadata.INACTIVE_STATE: + retry_429_harder(hmac_key.delete)() + def test_get_service_account_email(self): domain = "gs-project-accounts.iam.gserviceaccount.com" @@ -102,6 +115,42 @@ def test_get_service_account_email(self): self.assertTrue(any(match for match in matches if match is not None)) + def test_hmac_key_crud(self): + from google.cloud.storage.hmac_key import HMACKeyMetadata + + credentials = Config.CLIENT._credentials + email = credentials.service_account_email + + before_keys = set(Config.CLIENT.list_hmac_keys()) + + metadata, secret = Config.CLIENT.create_hmac_key(email) + self.case_hmac_keys_to_delete.append(metadata) + + self.assertIsInstance(secret, six.text_type) + self.assertEqual(len(secret), 40) + + after_keys = set(Config.CLIENT.list_hmac_keys()) + self.assertFalse(metadata in before_keys) + self.assertTrue(metadata in after_keys) + + another = HMACKeyMetadata(Config.CLIENT) + + another._properties["accessId"] = "nonesuch" + self.assertFalse(another.exists()) + + another._properties["accessId"] = metadata.access_id + self.assertTrue(another.exists()) + + another.reload() + + self.assertEqual(another._properties, metadata._properties) + + metadata.state = HMACKeyMetadata.INACTIVE_STATE + metadata.update() + + metadata.delete() + self.case_hmac_keys_to_delete.remove(metadata) + class TestStorageBuckets(unittest.TestCase): def setUp(self): diff --git a/storage/tests/unit/test_client.py b/storage/tests/unit/test_client.py index b5597977bb17..7f5e4953d62d 100644 --- a/storage/tests/unit/test_client.py +++ b/storage/tests/unit/test_client.py @@ -874,7 +874,7 @@ def test_list_buckets_all_arguments(self): uri_parts = urlparse(requested_url) self.assertEqual(parse_qs(uri_parts.query), expected_query) - def test_page_empty_response(self): + def test_list_buckets_page_empty_response(self): from google.api_core import page_iterator project = "PROJECT" @@ -885,7 +885,7 @@ def test_page_empty_response(self): iterator._page = page self.assertEqual(list(page), []) - def test_page_non_empty_response(self): + def test_list_buckets_page_non_empty_response(self): import six from google.cloud.storage.bucket import Bucket @@ -908,3 +908,250 @@ def dummy_response(): self.assertEqual(page.remaining, 0) self.assertIsInstance(bucket, Bucket) self.assertEqual(bucket.name, blob_name) + + def _create_hmac_key_helper(self, explicit_project=None): + import datetime + from pytz import UTC + from six.moves.urllib.parse import urlencode + from google.cloud.storage.hmac_key import HMACKeyMetadata + + PROJECT = "PROJECT" + ACCESS_ID = "ACCESS-ID" + CREDENTIALS = _make_credentials() + EMAIL = "storage-user-123@example.com" + SECRET = "a" * 40 + now = datetime.datetime.utcnow().replace(tzinfo=UTC) + now_stamp = "{}Z".format(now.isoformat()) + + if explicit_project is not None: + expected_project = explicit_project + else: + expected_project = PROJECT + + RESOURCE = { + "kind": "storage#hmacKey", + "metadata": { + "accessId": ACCESS_ID, + "etag": "ETAG", + "id": "projects/{}/hmacKeys/{}".format(PROJECT, ACCESS_ID), + "project": expected_project, + "state": "ACTIVE", + "serviceAccountEmail": EMAIL, + "timeCreated": now_stamp, + "updated": now_stamp, + }, + "secret": SECRET, + } + + client = self._make_one(project=PROJECT, credentials=CREDENTIALS) + http = _make_requests_session([_make_json_response(RESOURCE)]) + client._http_internal = http + + kwargs = {} + if explicit_project is not None: + kwargs["project_id"] = explicit_project + + metadata, secret = client.create_hmac_key(service_account_email=EMAIL, **kwargs) + + self.assertIsInstance(metadata, HMACKeyMetadata) + self.assertIs(metadata._client, client) + self.assertEqual(metadata._properties, RESOURCE["metadata"]) + self.assertEqual(secret, RESOURCE["secret"]) + + URI = "/".join( + [ + client._connection.API_BASE_URL, + "storage", + client._connection.API_VERSION, + "projects", + expected_project, + "hmacKeys", + ] + ) + QS_PARAMS = {"serviceAccountEmail": EMAIL} + FULL_URI = "{}?{}".format(URI, urlencode(QS_PARAMS)) + http.request.assert_called_once_with( + method="POST", url=FULL_URI, data=None, headers=mock.ANY + ) + + def test_create_hmac_key_defaults(self): + self._create_hmac_key_helper() + + def test_create_hmac_key_explicit_project(self): + self._create_hmac_key_helper(explicit_project="other-project-456") + + def test_list_hmac_keys_defaults_empty(self): + PROJECT = "PROJECT" + CREDENTIALS = _make_credentials() + client = self._make_one(project=PROJECT, credentials=CREDENTIALS) + + http = _make_requests_session([_make_json_response({})]) + client._http_internal = http + + metadatas = list(client.list_hmac_keys()) + + self.assertEqual(len(metadatas), 0) + + URI = "/".join( + [ + client._connection.API_BASE_URL, + "storage", + client._connection.API_VERSION, + "projects", + PROJECT, + "hmacKeys", + ] + ) + http.request.assert_called_once_with( + method="GET", url=URI, data=None, headers=mock.ANY + ) + + def test_list_hmac_keys_explicit_non_empty(self): + from six.moves.urllib.parse import parse_qsl + from google.cloud.storage.hmac_key import HMACKeyMetadata + + PROJECT = "PROJECT" + OTHER_PROJECT = "other-project-456" + MAX_RESULTS = 3 + EMAIL = "storage-user-123@example.com" + ACCESS_ID = "ACCESS-ID" + CREDENTIALS = _make_credentials() + client = self._make_one(project=PROJECT, credentials=CREDENTIALS) + + response = { + "kind": "storage#hmacKeysMetadata", + "items": [ + { + "kind": "storage#hmacKeyMetadata", + "accessId": ACCESS_ID, + "serviceAccountEmail": EMAIL, + } + ], + } + + http = _make_requests_session([_make_json_response(response)]) + client._http_internal = http + + metadatas = list( + client.list_hmac_keys( + max_results=MAX_RESULTS, + service_account_email=EMAIL, + show_deleted_keys=True, + project_id=OTHER_PROJECT, + ) + ) + + self.assertEqual(len(metadatas), len(response["items"])) + + for metadata, resource in zip(metadatas, response["items"]): + self.assertIsInstance(metadata, HMACKeyMetadata) + self.assertIs(metadata._client, client) + self.assertEqual(metadata._properties, resource) + + URI = "/".join( + [ + client._connection.API_BASE_URL, + "storage", + client._connection.API_VERSION, + "projects", + OTHER_PROJECT, + "hmacKeys", + ] + ) + EXPECTED_QPARAMS = { + "maxResults": str(MAX_RESULTS), + "serviceAccountEmail": EMAIL, + "showDeletedKeys": "True", + } + http.request.assert_called_once_with( + method="GET", url=mock.ANY, data=None, headers=mock.ANY + ) + kwargs = http.request.mock_calls[0].kwargs + uri = kwargs["url"] + base, qparam_str = uri.split("?") + qparams = dict(parse_qsl(qparam_str)) + self.assertEqual(base, URI) + self.assertEqual(qparams, EXPECTED_QPARAMS) + + def test_get_hmac_key_metadata_wo_project(self): + from google.cloud.storage.hmac_key import HMACKeyMetadata + + PROJECT = "PROJECT" + EMAIL = "storage-user-123@example.com" + ACCESS_ID = "ACCESS-ID" + CREDENTIALS = _make_credentials() + client = self._make_one(project=PROJECT, credentials=CREDENTIALS) + + resource = { + "kind": "storage#hmacKeyMetadata", + "accessId": ACCESS_ID, + "projectId": PROJECT, + "serviceAccountEmail": EMAIL, + } + + http = _make_requests_session([_make_json_response(resource)]) + client._http_internal = http + + metadata = client.get_hmac_key_metadata(ACCESS_ID) + + self.assertIsInstance(metadata, HMACKeyMetadata) + self.assertIs(metadata._client, client) + self.assertEqual(metadata.access_id, ACCESS_ID) + self.assertEqual(metadata.project, PROJECT) + + URI = "/".join( + [ + client._connection.API_BASE_URL, + "storage", + client._connection.API_VERSION, + "projects", + PROJECT, + "hmacKeys", + ACCESS_ID, + ] + ) + http.request.assert_called_once_with( + method="GET", url=URI, data=None, headers=mock.ANY + ) + + def test_get_hmac_key_metadata_w_project(self): + from google.cloud.storage.hmac_key import HMACKeyMetadata + + PROJECT = "PROJECT" + OTHER_PROJECT = "other-project-456" + EMAIL = "storage-user-123@example.com" + ACCESS_ID = "ACCESS-ID" + CREDENTIALS = _make_credentials() + client = self._make_one(project=PROJECT, credentials=CREDENTIALS) + + resource = { + "kind": "storage#hmacKeyMetadata", + "accessId": ACCESS_ID, + "projectId": OTHER_PROJECT, + "serviceAccountEmail": EMAIL, + } + + http = _make_requests_session([_make_json_response(resource)]) + client._http_internal = http + + metadata = client.get_hmac_key_metadata(ACCESS_ID, project_id=OTHER_PROJECT) + + self.assertIsInstance(metadata, HMACKeyMetadata) + self.assertIs(metadata._client, client) + self.assertEqual(metadata.access_id, ACCESS_ID) + self.assertEqual(metadata.project, OTHER_PROJECT) + + URI = "/".join( + [ + client._connection.API_BASE_URL, + "storage", + client._connection.API_VERSION, + "projects", + OTHER_PROJECT, + "hmacKeys", + ACCESS_ID, + ] + ) + http.request.assert_called_once_with( + method="GET", url=URI, data=None, headers=mock.ANY + ) diff --git a/storage/tests/unit/test_hmac_key.py b/storage/tests/unit/test_hmac_key.py new file mode 100644 index 000000000000..ecee7d9865a2 --- /dev/null +++ b/storage/tests/unit/test_hmac_key.py @@ -0,0 +1,393 @@ +# Copyright 2019 Google 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 unittest + +import mock + + +class TestHMACKeyMetadata(unittest.TestCase): + @staticmethod + def _get_target_class(): + from google.cloud.storage.hmac_key import HMACKeyMetadata + + return HMACKeyMetadata + + def _make_one(self, client=None, *args, **kw): + if client is None: + client = _Client() + return self._get_target_class()(client, *args, **kw) + + def test_ctor_defaults(self): + client = object() + metadata = self._make_one(client) + self.assertIs(metadata._client, client) + self.assertEqual(metadata._properties, {}) + self.assertIsNone(metadata.access_id) + self.assertIsNone(metadata.etag) + self.assertIsNone(metadata.project) + self.assertIsNone(metadata.service_account_email) + self.assertIsNone(metadata.state) + self.assertIsNone(metadata.time_created) + self.assertIsNone(metadata.updated) + + def test_ctor_explicit(self): + OTHER_PROJECT = "other-project-456" + ACCESS_ID = "access-id-123456789" + client = _Client() + metadata = self._make_one(client, access_id=ACCESS_ID, project_id=OTHER_PROJECT) + self.assertIs(metadata._client, client) + expected = {"accessId": ACCESS_ID, "projectId": OTHER_PROJECT} + self.assertEqual(metadata._properties, expected) + self.assertEqual(metadata.access_id, ACCESS_ID) + self.assertIsNone(metadata.etag) + self.assertEqual(metadata.project, OTHER_PROJECT) + self.assertIsNone(metadata.service_account_email) + self.assertIsNone(metadata.state) + self.assertIsNone(metadata.time_created) + self.assertIsNone(metadata.updated) + + def test___eq___other_type(self): + metadata = self._make_one() + for bogus in (None, "bogus", 123, 456.78, [], (), {}, set()): + self.assertNotEqual(metadata, bogus) + + def test___eq___mismatched_client(self): + metadata = self._make_one() + other_client = _Client(project="other-project-456") + other = self._make_one(other_client) + self.assertNotEqual(metadata, other) + + def test___eq___mismatched_access_id(self): + metadata = self._make_one() + metadata._properties["accessId"] = "ABC123" + other = self._make_one(metadata._client) + metadata._properties["accessId"] = "DEF456" + self.assertNotEqual(metadata, other) + + def test___eq___hit(self): + metadata = self._make_one() + metadata._properties["accessId"] = "ABC123" + other = self._make_one(metadata._client) + other._properties["accessId"] = metadata.access_id + self.assertEqual(metadata, other) + + def test___hash__(self): + client = _Client() + metadata = self._make_one(client) + metadata._properties["accessId"] = "ABC123" + self.assertIsInstance(hash(metadata), int) + other = self._make_one(client) + metadata._properties["accessId"] = "DEF456" + self.assertNotEqual(hash(metadata), hash(other)) + + def test_access_id_getter(self): + metadata = self._make_one() + expected = "ACCESS-ID" + metadata._properties["accessId"] = expected + self.assertEqual(metadata.access_id, expected) + + def test_etag_getter(self): + metadata = self._make_one() + expected = "ETAG" + metadata._properties["etag"] = expected + self.assertEqual(metadata.etag, expected) + + def test_project_getter(self): + metadata = self._make_one() + expected = "PROJECT-ID" + metadata._properties["projectId"] = expected + self.assertEqual(metadata.project, expected) + + def test_service_account_email_getter(self): + metadata = self._make_one() + expected = "service_account@example.com" + metadata._properties["serviceAccountEmail"] = expected + self.assertEqual(metadata.service_account_email, expected) + + def test_state_getter(self): + metadata = self._make_one() + expected = "STATE" + metadata._properties["state"] = expected + self.assertEqual(metadata.state, expected) + + def test_state_setter_invalid_state(self): + metadata = self._make_one() + expected = "INVALID" + + with self.assertRaises(ValueError): + metadata.state = expected + + self.assertIsNone(metadata.state) + + def test_state_setter_inactive(self): + metadata = self._make_one() + metadata._properties["state"] = "ACTIVE" + expected = "INACTIVE" + metadata.state = expected + self.assertEqual(metadata.state, expected) + self.assertEqual(metadata._properties["state"], expected) + + def test_state_setter_active(self): + metadata = self._make_one() + metadata._properties["state"] = "INACTIVE" + expected = "ACTIVE" + metadata.state = expected + self.assertEqual(metadata.state, expected) + self.assertEqual(metadata._properties["state"], expected) + + def test_time_created_getter(self): + import datetime + from pytz import UTC + + metadata = self._make_one() + now = datetime.datetime.utcnow() + now_stamp = "{}Z".format(now.isoformat()) + metadata._properties["timeCreated"] = now_stamp + self.assertEqual(metadata.time_created, now.replace(tzinfo=UTC)) + + def test_updated_getter(self): + import datetime + from pytz import UTC + + metadata = self._make_one() + now = datetime.datetime.utcnow() + now_stamp = "{}Z".format(now.isoformat()) + metadata._properties["updated"] = now_stamp + self.assertEqual(metadata.updated, now.replace(tzinfo=UTC)) + + def test_path_wo_access_id(self): + metadata = self._make_one() + + with self.assertRaises(ValueError): + metadata.path + + def test_path_w_access_id_wo_project(self): + access_id = "ACCESS-ID" + client = _Client() + metadata = self._make_one() + metadata._properties["accessId"] = access_id + + expected_path = "/projects/{}/hmacKeys/{}".format( + client.DEFAULT_PROJECT, access_id + ) + self.assertEqual(metadata.path, expected_path) + + def test_path_w_access_id_w_explicit_project(self): + access_id = "ACCESS-ID" + project = "OTHER-PROJECT" + metadata = self._make_one() + metadata._properties["accessId"] = access_id + metadata._properties["projectId"] = project + + expected_path = "/projects/{}/hmacKeys/{}".format(project, access_id) + self.assertEqual(metadata.path, expected_path) + + def test_exists_miss_no_project_set(self): + from google.cloud.exceptions import NotFound + + access_id = "ACCESS-ID" + connection = mock.Mock(spec=["api_request"]) + connection.api_request.side_effect = NotFound("testing") + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + + self.assertFalse(metadata.exists()) + + expected_path = "/projects/{}/hmacKeys/{}".format( + client.DEFAULT_PROJECT, access_id + ) + expected_kwargs = {"method": "GET", "path": expected_path} + connection.api_request.assert_called_once_with(**expected_kwargs) + + def test_exists_hit_w_project_set(self): + project = "PROJECT-ID" + access_id = "ACCESS-ID" + email = "service-account@example.com" + resource = { + "kind": "storage#hmacKeyMetadata", + "accessId": access_id, + "serviceAccountEmail": email, + } + connection = mock.Mock(spec=["api_request"]) + connection.api_request.return_value = resource + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + metadata._properties["projectId"] = project + + self.assertTrue(metadata.exists()) + + expected_path = "/projects/{}/hmacKeys/{}".format(project, access_id) + expected_kwargs = {"method": "GET", "path": expected_path} + connection.api_request.assert_called_once_with(**expected_kwargs) + + def test_reload_miss_no_project_set(self): + from google.cloud.exceptions import NotFound + + access_id = "ACCESS-ID" + connection = mock.Mock(spec=["api_request"]) + connection.api_request.side_effect = NotFound("testing") + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + + with self.assertRaises(NotFound): + metadata.reload() + + expected_path = "/projects/{}/hmacKeys/{}".format( + client.DEFAULT_PROJECT, access_id + ) + expected_kwargs = {"method": "GET", "path": expected_path} + connection.api_request.assert_called_once_with(**expected_kwargs) + + def test_reload_hit_w_project_set(self): + project = "PROJECT-ID" + access_id = "ACCESS-ID" + email = "service-account@example.com" + resource = { + "kind": "storage#hmacKeyMetadata", + "accessId": access_id, + "serviceAccountEmail": email, + } + connection = mock.Mock(spec=["api_request"]) + connection.api_request.return_value = resource + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + metadata._properties["projectId"] = project + + metadata.reload() + + self.assertEqual(metadata._properties, resource) + + expected_path = "/projects/{}/hmacKeys/{}".format(project, access_id) + expected_kwargs = {"method": "GET", "path": expected_path} + connection.api_request.assert_called_once_with(**expected_kwargs) + + def test_update_miss_no_project_set(self): + from google.cloud.exceptions import NotFound + + access_id = "ACCESS-ID" + connection = mock.Mock(spec=["api_request"]) + connection.api_request.side_effect = NotFound("testing") + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + metadata.state = "INACTIVE" + + with self.assertRaises(NotFound): + metadata.update() + + expected_path = "/projects/{}/hmacKeys/{}".format( + client.DEFAULT_PROJECT, access_id + ) + expected_kwargs = { + "method": "PUT", + "path": expected_path, + "data": {"state": "INACTIVE"}, + } + connection.api_request.assert_called_once_with(**expected_kwargs) + + def test_update_hit_w_project_set(self): + project = "PROJECT-ID" + access_id = "ACCESS-ID" + email = "service-account@example.com" + resource = { + "kind": "storage#hmacKeyMetadata", + "accessId": access_id, + "serviceAccountEmail": email, + "state": "ACTIVE", + } + connection = mock.Mock(spec=["api_request"]) + connection.api_request.return_value = resource + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + metadata._properties["projectId"] = project + metadata.state = "ACTIVE" + + metadata.update() + + self.assertEqual(metadata._properties, resource) + + expected_path = "/projects/{}/hmacKeys/{}".format(project, access_id) + expected_kwargs = { + "method": "PUT", + "path": expected_path, + "data": {"state": "ACTIVE"}, + } + connection.api_request.assert_called_once_with(**expected_kwargs) + + def test_delete_not_inactive(self): + metadata = self._make_one() + for state in ("ACTIVE", "DELETED"): + metadata._properties["state"] = state + + with self.assertRaises(ValueError): + metadata.delete() + + def test_delete_miss_no_project_set(self): + from google.cloud.exceptions import NotFound + + access_id = "ACCESS-ID" + connection = mock.Mock(spec=["api_request"]) + connection.api_request.side_effect = NotFound("testing") + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + metadata.state = "INACTIVE" + + with self.assertRaises(NotFound): + metadata.delete() + + expected_path = "/projects/{}/hmacKeys/{}".format( + client.DEFAULT_PROJECT, access_id + ) + expected_kwargs = {"method": "DELETE", "path": expected_path} + connection.api_request.assert_called_once_with(**expected_kwargs) + + def test_delete_hit_w_project_set(self): + project = "PROJECT-ID" + access_id = "ACCESS-ID" + connection = mock.Mock(spec=["api_request"]) + connection.api_request.return_value = {} + client = _Client(connection) + metadata = self._make_one(client) + metadata._properties["accessId"] = access_id + metadata._properties["projectId"] = project + metadata.state = "INACTIVE" + + metadata.delete() + + expected_path = "/projects/{}/hmacKeys/{}".format(project, access_id) + expected_kwargs = {"method": "DELETE", "path": expected_path} + connection.api_request.assert_called_once_with(**expected_kwargs) + + +class _Client(object): + DEFAULT_PROJECT = "project-123" + + def __init__(self, connection=None, project=DEFAULT_PROJECT): + self._connection = connection + self.project = project + + def __eq__(self, other): + if not isinstance(other, self.__class__): # pragma: NO COVER + return NotImplemented + return self._connection == other._connection and self.project == other.project + + def __hash__(self): + return hash(self._connection) + hash(self.project)