# Copyright 2014 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 base64
import datetime
import hashlib
import io
import json
import os
import tempfile
import unittest

import mock
import six
from six.moves import http_client


def _make_credentials():
    import google.auth.credentials

    return mock.Mock(spec=google.auth.credentials.Credentials)


class Test_Blob(unittest.TestCase):

    @staticmethod
    def _make_one(*args, **kw):
        from google.cloud.storage.blob import Blob

        properties = kw.pop('properties', None)
        blob = Blob(*args, **kw)
        blob._properties = properties or {}
        return blob

    def test_ctor_wo_encryption_key(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        properties = {'key': 'value'}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertIs(blob.bucket, bucket)
        self.assertEqual(blob.name, BLOB_NAME)
        self.assertEqual(blob._properties, properties)
        self.assertFalse(blob._acl.loaded)
        self.assertIs(blob._acl.blob, blob)
        self.assertEqual(blob._encryption_key, None)

    def test_ctor_with_encoded_unicode(self):
        blob_name = b'wet \xe2\x9b\xb5'
        blob = self._make_one(blob_name, bucket=None)
        unicode_name = u'wet \N{sailboat}'
        self.assertNotIsInstance(blob.name, bytes)
        self.assertIsInstance(blob.name, six.text_type)
        self.assertEqual(blob.name, unicode_name)

    def test_ctor_w_encryption_key(self):
        KEY = b'01234567890123456789012345678901'  # 32 bytes
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket, encryption_key=KEY)
        self.assertEqual(blob._encryption_key, KEY)

    def test_chunk_size_ctor(self):
        from google.cloud.storage.blob import Blob

        BLOB_NAME = 'blob-name'
        BUCKET = object()
        chunk_size = 10 * Blob._CHUNK_SIZE_MULTIPLE
        blob = self._make_one(BLOB_NAME, bucket=BUCKET, chunk_size=chunk_size)
        self.assertEqual(blob._chunk_size, chunk_size)

    def test_chunk_size_getter(self):
        BLOB_NAME = 'blob-name'
        BUCKET = object()
        blob = self._make_one(BLOB_NAME, bucket=BUCKET)
        self.assertIsNone(blob.chunk_size)
        VALUE = object()
        blob._chunk_size = VALUE
        self.assertIs(blob.chunk_size, VALUE)

    def test_chunk_size_setter(self):
        BLOB_NAME = 'blob-name'
        BUCKET = object()
        blob = self._make_one(BLOB_NAME, bucket=BUCKET)
        self.assertIsNone(blob._chunk_size)
        blob._CHUNK_SIZE_MULTIPLE = 10
        blob.chunk_size = 20
        self.assertEqual(blob._chunk_size, 20)

    def test_chunk_size_setter_bad_value(self):
        BLOB_NAME = 'blob-name'
        BUCKET = object()
        blob = self._make_one(BLOB_NAME, bucket=BUCKET)
        self.assertIsNone(blob._chunk_size)
        blob._CHUNK_SIZE_MULTIPLE = 10
        with self.assertRaises(ValueError):
            blob.chunk_size = 11

    def test_acl_property(self):
        from google.cloud.storage.acl import ObjectACL

        fake_bucket = _Bucket()
        blob = self._make_one(u'name', bucket=fake_bucket)
        acl = blob.acl
        self.assertIsInstance(acl, ObjectACL)
        self.assertIs(acl, blob._acl)

    def test_path_bad_bucket(self):
        fake_bucket = object()
        name = u'blob-name'
        blob = self._make_one(name, bucket=fake_bucket)
        self.assertRaises(AttributeError, getattr, blob, 'path')

    def test_path_no_name(self):
        bucket = _Bucket()
        blob = self._make_one(u'', bucket=bucket)
        self.assertRaises(ValueError, getattr, blob, 'path')

    def test_path_normal(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertEqual(blob.path, '/b/name/o/%s' % BLOB_NAME)

    def test_path_w_slash_in_name(self):
        BLOB_NAME = 'parent/child'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertEqual(blob.path, '/b/name/o/parent%2Fchild')

    def test_path_with_non_ascii(self):
        blob_name = u'Caf\xe9'
        bucket = _Bucket()
        blob = self._make_one(blob_name, bucket=bucket)
        self.assertEqual(blob.path, '/b/name/o/Caf%C3%A9')

    def test_client(self):
        blob_name = 'BLOB'
        bucket = _Bucket()
        blob = self._make_one(blob_name, bucket=bucket)
        self.assertIs(blob.client, bucket.client)

    def test_user_project(self):
        user_project = 'user-project-123'
        blob_name = 'BLOB'
        bucket = _Bucket(user_project=user_project)
        blob = self._make_one(blob_name, bucket=bucket)
        self.assertEqual(blob.user_project, user_project)

    def test_public_url(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertEqual(blob.public_url,
                         'https://storage.googleapis.com/name/%s' %
                         BLOB_NAME)

    def test_public_url_w_slash_in_name(self):
        BLOB_NAME = 'parent/child'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertEqual(
            blob.public_url,
            'https://storage.googleapis.com/name/parent%2Fchild')

    def test_public_url_with_non_ascii(self):
        blob_name = u'winter \N{snowman}'
        bucket = _Bucket()
        blob = self._make_one(blob_name, bucket=bucket)
        expected_url = 'https://storage.googleapis.com/name/winter%20%E2%98%83'
        self.assertEqual(blob.public_url, expected_url)

    def _basic_generate_signed_url_helper(self, credentials=None):
        BLOB_NAME = 'blob-name'
        EXPIRATION = '2014-10-16T20:34:37.000Z'
        connection = _Connection()
        client = _Client(connection)
        bucket = _Bucket(client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        URI = ('http://example.com/abucket/a-blob-name?Signature=DEADBEEF'
               '&Expiration=2014-10-16T20:34:37.000Z')

        SIGNER = _Signer()
        with mock.patch('google.cloud.storage.blob.generate_signed_url',
                        new=SIGNER):
            signed_uri = blob.generate_signed_url(EXPIRATION,
                                                  credentials=credentials)
            self.assertEqual(signed_uri, URI)

        PATH = '/name/%s' % (BLOB_NAME,)
        if credentials is None:
            EXPECTED_ARGS = (_Connection.credentials,)
        else:
            EXPECTED_ARGS = (credentials,)
        EXPECTED_KWARGS = {
            'api_access_endpoint': 'https://storage.googleapis.com',
            'expiration': EXPIRATION,
            'method': 'GET',
            'resource': PATH,
            'content_type': None,
            'response_type': None,
            'response_disposition': None,
            'generation': None,
        }
        self.assertEqual(SIGNER._signed, [(EXPECTED_ARGS, EXPECTED_KWARGS)])

    def test_generate_signed_url_w_default_method(self):
        self._basic_generate_signed_url_helper()

    def test_generate_signed_url_w_content_type(self):
        BLOB_NAME = 'blob-name'
        EXPIRATION = '2014-10-16T20:34:37.000Z'
        connection = _Connection()
        client = _Client(connection)
        bucket = _Bucket(client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        URI = ('http://example.com/abucket/a-blob-name?Signature=DEADBEEF'
               '&Expiration=2014-10-16T20:34:37.000Z')

        SIGNER = _Signer()
        CONTENT_TYPE = "text/html"
        with mock.patch('google.cloud.storage.blob.generate_signed_url',
                        new=SIGNER):
            signed_url = blob.generate_signed_url(EXPIRATION,
                                                  content_type=CONTENT_TYPE)
            self.assertEqual(signed_url, URI)

        PATH = '/name/%s' % (BLOB_NAME,)
        EXPECTED_ARGS = (_Connection.credentials,)
        EXPECTED_KWARGS = {
            'api_access_endpoint': 'https://storage.googleapis.com',
            'expiration': EXPIRATION,
            'method': 'GET',
            'resource': PATH,
            'content_type': CONTENT_TYPE,
            'response_type': None,
            'response_disposition': None,
            'generation': None,
        }
        self.assertEqual(SIGNER._signed, [(EXPECTED_ARGS, EXPECTED_KWARGS)])

    def test_generate_signed_url_w_credentials(self):
        credentials = object()
        self._basic_generate_signed_url_helper(credentials=credentials)

    def test_generate_signed_url_w_slash_in_name(self):
        BLOB_NAME = 'parent/child'
        EXPIRATION = '2014-10-16T20:34:37.000Z'
        connection = _Connection()
        client = _Client(connection)
        bucket = _Bucket(client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        URI = ('http://example.com/abucket/a-blob-name?Signature=DEADBEEF'
               '&Expiration=2014-10-16T20:34:37.000Z')

        SIGNER = _Signer()
        with mock.patch('google.cloud.storage.blob.generate_signed_url',
                        new=SIGNER):
            signed_url = blob.generate_signed_url(EXPIRATION)
            self.assertEqual(signed_url, URI)

        EXPECTED_ARGS = (_Connection.credentials,)
        EXPECTED_KWARGS = {
            'api_access_endpoint': 'https://storage.googleapis.com',
            'expiration': EXPIRATION,
            'method': 'GET',
            'resource': '/name/parent%2Fchild',
            'content_type': None,
            'response_type': None,
            'response_disposition': None,
            'generation': None,
        }
        self.assertEqual(SIGNER._signed, [(EXPECTED_ARGS, EXPECTED_KWARGS)])

    def test_generate_signed_url_w_method_arg(self):
        BLOB_NAME = 'blob-name'
        EXPIRATION = '2014-10-16T20:34:37.000Z'
        connection = _Connection()
        client = _Client(connection)
        bucket = _Bucket(client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        URI = ('http://example.com/abucket/a-blob-name?Signature=DEADBEEF'
               '&Expiration=2014-10-16T20:34:37.000Z')

        SIGNER = _Signer()
        with mock.patch('google.cloud.storage.blob.generate_signed_url',
                        new=SIGNER):
            signed_uri = blob.generate_signed_url(EXPIRATION, method='POST')
            self.assertEqual(signed_uri, URI)

        PATH = '/name/%s' % (BLOB_NAME,)
        EXPECTED_ARGS = (_Connection.credentials,)
        EXPECTED_KWARGS = {
            'api_access_endpoint': 'https://storage.googleapis.com',
            'expiration': EXPIRATION,
            'method': 'POST',
            'resource': PATH,
            'content_type': None,
            'response_type': None,
            'response_disposition': None,
            'generation': None,
        }
        self.assertEqual(SIGNER._signed, [(EXPECTED_ARGS, EXPECTED_KWARGS)])

    def test_exists_miss(self):
        NONESUCH = 'nonesuch'
        not_found_response = ({'status': http_client.NOT_FOUND}, b'')
        connection = _Connection(not_found_response)
        client = _Client(connection)
        bucket = _Bucket(client)
        blob = self._make_one(NONESUCH, bucket=bucket)
        self.assertFalse(blob.exists())
        self.assertEqual(len(connection._requested), 1)
        self.assertEqual(connection._requested[0], {
            'method': 'GET',
            'path': '/b/name/o/{}'.format(NONESUCH),
            'query_params': {'fields': 'name'},
            '_target_object': None,
        })

    def test_exists_hit_w_user_project(self):
        BLOB_NAME = 'blob-name'
        USER_PROJECT = 'user-project-123'
        found_response = ({'status': http_client.OK}, b'')
        connection = _Connection(found_response)
        client = _Client(connection)
        bucket = _Bucket(client, user_project=USER_PROJECT)
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        bucket._blobs[BLOB_NAME] = 1
        self.assertTrue(blob.exists())
        self.assertEqual(len(connection._requested), 1)
        self.assertEqual(connection._requested[0], {
            'method': 'GET',
            'path': '/b/name/o/{}'.format(BLOB_NAME),
            'query_params': {'fields': 'name', 'userProject': USER_PROJECT},
            '_target_object': None,
        })

    def test_delete(self):
        BLOB_NAME = 'blob-name'
        not_found_response = ({'status': http_client.NOT_FOUND}, b'')
        connection = _Connection(not_found_response)
        client = _Client(connection)
        bucket = _Bucket(client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        bucket._blobs[BLOB_NAME] = 1
        blob.delete()
        self.assertFalse(blob.exists())
        self.assertEqual(bucket._deleted, [(BLOB_NAME, None)])

    def test__get_transport(self):
        client = mock.Mock(spec=[u'_credentials', '_http'])
        client._http = mock.sentinel.transport
        blob = self._make_one(u'blob-name', bucket=None)

        transport = blob._get_transport(client)

        self.assertIs(transport, mock.sentinel.transport)

    def test__get_download_url_with_media_link(self):
        blob_name = 'something.txt'
        bucket = _Bucket(name='IRRELEVANT')
        blob = self._make_one(blob_name, bucket=bucket)
        media_link = 'http://test.invalid'
        # Set the media link on the blob
        blob._properties['mediaLink'] = media_link

        download_url = blob._get_download_url()
        self.assertEqual(download_url, media_link)

    def test__get_download_url_with_media_link_w_user_project(self):
        blob_name = 'something.txt'
        user_project = 'user-project-123'
        bucket = _Bucket(name='IRRELEVANT', user_project=user_project)
        blob = self._make_one(blob_name, bucket=bucket)
        media_link = 'http://test.invalid'
        # Set the media link on the blob
        blob._properties['mediaLink'] = media_link

        download_url = blob._get_download_url()
        self.assertEqual(
            download_url, '{}?userProject={}'.format(media_link, user_project))

    def test__get_download_url_on_the_fly(self):
        blob_name = 'bzzz-fly.txt'
        bucket = _Bucket(name='buhkit')
        blob = self._make_one(blob_name, bucket=bucket)

        self.assertIsNone(blob.media_link)
        download_url = blob._get_download_url()
        expected_url = (
            'https://www.googleapis.com/download/storage/v1/b/'
            'buhkit/o/bzzz-fly.txt?alt=media')
        self.assertEqual(download_url, expected_url)

    def test__get_download_url_on_the_fly_with_generation(self):
        blob_name = 'pretend.txt'
        bucket = _Bucket(name='fictional')
        blob = self._make_one(blob_name, bucket=bucket)
        generation = 1493058489532987
        # Set the media link on the blob
        blob._properties['generation'] = str(generation)

        self.assertIsNone(blob.media_link)
        download_url = blob._get_download_url()
        expected_url = (
            'https://www.googleapis.com/download/storage/v1/b/'
            'fictional/o/pretend.txt?alt=media&generation=1493058489532987')
        self.assertEqual(download_url, expected_url)

    def test__get_download_url_on_the_fly_with_user_project(self):
        blob_name = 'pretend.txt'
        user_project = 'user-project-123'
        bucket = _Bucket(name='fictional', user_project=user_project)
        blob = self._make_one(blob_name, bucket=bucket)

        self.assertIsNone(blob.media_link)
        download_url = blob._get_download_url()
        expected_url = (
            'https://www.googleapis.com/download/storage/v1/b/'
            'fictional/o/pretend.txt?alt=media&userProject={}'.format(
                user_project))
        self.assertEqual(download_url, expected_url)

    @staticmethod
    def _mock_requests_response(
            status_code, headers, content=b'', stream=False):
        import requests

        response = requests.Response()
        response.status_code = status_code
        response.headers.update(headers)
        if stream:
            raw = io.BytesIO(content)
            raw.headers = headers
            response.raw = raw
            response._content = False
        else:
            response.raw = None
            response._content = content

        response.request = requests.Request(
            'POST', 'http://example.com').prepare()
        return response

    def _mock_download_transport(self):
        fake_transport = mock.Mock(spec=['request'])
        # Give the transport two fake responses.
        chunk1_response = self._mock_requests_response(
            http_client.PARTIAL_CONTENT,
            {'content-length': '3', 'content-range': 'bytes 0-2/6'},
            content=b'abc')
        chunk2_response = self._mock_requests_response(
            http_client.PARTIAL_CONTENT,
            {'content-length': '3', 'content-range': 'bytes 3-5/6'},
            content=b'def')
        fake_transport.request.side_effect = [chunk1_response, chunk2_response]
        return fake_transport

    def _check_session_mocks(self, client, transport,
                             expected_url, headers=None):
        # Check that the transport was called exactly twice.
        self.assertEqual(transport.request.call_count, 2)
        if headers is None:
            headers = {}
        # NOTE: bytes=0-2 never shows up because the mock was called with
        #       **MUTABLE** headers and it was mutated before the
        #       second request.
        headers['range'] = 'bytes=3-5'
        headers['accept-encoding'] = 'gzip'
        call = mock.call(
            'GET', expected_url, data=None, headers=headers)
        self.assertEqual(transport.request.mock_calls, [call, call])

    def test__do_download_simple(self):
        blob_name = 'blob-name'
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(
            _credentials=_make_credentials(), spec=['_credentials'])
        bucket = _Bucket(client)
        blob = self._make_one(blob_name, bucket=bucket)

        # Make sure this will not be chunked.
        self.assertIsNone(blob.chunk_size)

        transport = mock.Mock(spec=['request'])
        transport.request.return_value = self._mock_requests_response(
            http_client.OK,
            {'content-length': '6', 'content-range': 'bytes 0-5/6'},
            content=b'abcdef',
            stream=True,
        )
        file_obj = io.BytesIO()
        download_url = 'http://test.invalid'
        headers = {}
        blob._do_download(transport, file_obj, download_url, headers)
        # Make sure the download was as expected.
        self.assertEqual(file_obj.getvalue(), b'abcdef')

        transport.request.assert_called_once_with(
            'GET', download_url, data=None, headers=headers, stream=True)

    def test__do_download_chunked(self):
        blob_name = 'blob-name'
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(
            _credentials=_make_credentials(), spec=['_credentials'])
        bucket = _Bucket(client)
        blob = self._make_one(blob_name, bucket=bucket)

        # Modify the blob so there there will be 2 chunks of size 3.
        blob._CHUNK_SIZE_MULTIPLE = 1
        blob.chunk_size = 3

        transport = self._mock_download_transport()
        file_obj = io.BytesIO()
        download_url = 'http://test.invalid'
        headers = {}
        blob._do_download(transport, file_obj, download_url, headers)
        # Make sure the download was as expected.
        self.assertEqual(file_obj.getvalue(), b'abcdef')

        # Check that the transport was called exactly twice.
        self.assertEqual(transport.request.call_count, 2)
        # ``headers`` was modified (in place) once for each API call.
        self.assertEqual(headers, {'range': 'bytes=3-5'})
        call = mock.call(
            'GET', download_url, data=None, headers=headers)
        self.assertEqual(transport.request.mock_calls, [call, call])

    def test_download_to_file_with_failure(self):
        from google.cloud import exceptions

        blob_name = 'blob-name'
        transport = mock.Mock(spec=['request'])
        bad_response_headers = {
            'Content-Length': '9',
            'Content-Type': 'text/html; charset=UTF-8',
        }
        transport.request.return_value = self._mock_requests_response(
            http_client.NOT_FOUND, bad_response_headers, content=b'Not found')
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(_http=transport, spec=[u'_http'])
        bucket = _Bucket(client)
        blob = self._make_one(blob_name, bucket=bucket)
        # Set the media link on the blob
        blob._properties['mediaLink'] = 'http://test.invalid'

        file_obj = io.BytesIO()
        with self.assertRaises(exceptions.NotFound):
            blob.download_to_file(file_obj)

        self.assertEqual(file_obj.tell(), 0)
        # Check that the transport was called once.
        transport.request.assert_called_once_with(
            'GET', blob.media_link, data=None,
            headers={'accept-encoding': 'gzip'}, stream=True)

    def test_download_to_file_wo_media_link(self):
        blob_name = 'blob-name'
        transport = self._mock_download_transport()
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(_http=transport, spec=[u'_http'])
        bucket = _Bucket(client)
        blob = self._make_one(blob_name, bucket=bucket)
        # Modify the blob so there there will be 2 chunks of size 3.
        blob._CHUNK_SIZE_MULTIPLE = 1
        blob.chunk_size = 3

        file_obj = io.BytesIO()
        blob.download_to_file(file_obj)
        self.assertEqual(file_obj.getvalue(), b'abcdef')
        # Make sure the media link is still unknown.
        self.assertIsNone(blob.media_link)

        expected_url = (
            'https://www.googleapis.com/download/storage/v1/b/'
            'name/o/blob-name?alt=media')
        self._check_session_mocks(client, transport, expected_url)

    def _download_to_file_helper(self, use_chunks=False):
        blob_name = 'blob-name'
        transport = self._mock_download_transport()
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(_http=transport, spec=[u'_http'])
        bucket = _Bucket(client)
        media_link = 'http://example.com/media/'
        properties = {'mediaLink': media_link}
        blob = self._make_one(blob_name, bucket=bucket, properties=properties)
        if use_chunks:
            # Modify the blob so there there will be 2 chunks of size 3.
            blob._CHUNK_SIZE_MULTIPLE = 1
            blob.chunk_size = 3
        else:
            # Modify the response.
            single_chunk_response = self._mock_requests_response(
                http_client.OK,
                {'content-length': '6', 'content-range': 'bytes 0-5/6'},
                content=b'abcdef',
                stream=True,
            )
            transport.request.side_effect = [single_chunk_response]

        file_obj = io.BytesIO()
        blob.download_to_file(file_obj)
        self.assertEqual(file_obj.getvalue(), b'abcdef')

        if use_chunks:
            self._check_session_mocks(client, transport, media_link)
        else:
            transport.request.assert_called_once_with(
                'GET', media_link, data=None,
                headers={'accept-encoding': 'gzip'}, stream=True)

    def test_download_to_file_default(self):
        self._download_to_file_helper()

    def test_download_to_file_with_chunk_size(self):
        self._download_to_file_helper(use_chunks=True)

    def _download_to_filename_helper(self, updated=None):
        import os
        import time
        from google.cloud._testing import _NamedTemporaryFile

        blob_name = 'blob-name'
        transport = self._mock_download_transport()
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(_http=transport, spec=['_http'])
        bucket = _Bucket(client)
        media_link = 'http://example.com/media/'
        properties = {'mediaLink': media_link}
        if updated is not None:
            properties['updated'] = updated

        blob = self._make_one(blob_name, bucket=bucket, properties=properties)
        # Modify the blob so there there will be 2 chunks of size 3.
        blob._CHUNK_SIZE_MULTIPLE = 1
        blob.chunk_size = 3

        with _NamedTemporaryFile() as temp:
            blob.download_to_filename(temp.name)
            with open(temp.name, 'rb') as file_obj:
                wrote = file_obj.read()
                if updated is None:
                    self.assertIsNone(blob.updated)
                else:
                    mtime = os.path.getmtime(temp.name)
                    updated_time = time.mktime(blob.updated.timetuple())
                    self.assertEqual(mtime, updated_time)

        self.assertEqual(wrote, b'abcdef')

        self._check_session_mocks(client, transport, media_link)

    def test_download_to_filename(self):
        updated = '2014-12-06T13:13:50.690Z'
        self._download_to_filename_helper(updated=updated)

    def test_download_to_filename_wo_updated(self):
        self._download_to_filename_helper()

    def test_download_to_filename_corrupted(self):
        from google.resumable_media import DataCorruption
        from google.resumable_media.requests.download import _CHECKSUM_MISMATCH

        blob_name = 'blob-name'
        transport = mock.Mock(spec=['request'])
        empty_hash = base64.b64encode(
            hashlib.md5(b'').digest()).decode(u'utf-8')
        headers = {'x-goog-hash': 'md5=' + empty_hash}
        mock_raw = mock.Mock(headers=headers, spec=['headers'])
        response = mock.MagicMock(
            headers=headers,
            status_code=http_client.OK,
            raw=mock_raw,
            spec=[
                '__enter__',
                '__exit__',
                'headers',
                'iter_content',
                'status_code',
                'raw',
            ],
        )
        # i.e. context manager returns ``self``.
        response.__enter__.return_value = response
        response.__exit__.return_value = None
        chunks = (b'noms1', b'coooookies2')
        response.iter_content.return_value = iter(chunks)

        transport.request.return_value = response
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(_http=transport, spec=['_http'])
        bucket = mock.Mock(
            client=client,
            user_project=None,
            spec=['client', 'user_project'],
        )
        media_link = 'http://example.com/media/'
        properties = {'mediaLink': media_link}
        blob = self._make_one(blob_name, bucket=bucket, properties=properties)
        # Make sure the download is **not** chunked.
        self.assertIsNone(blob.chunk_size)

        # Make sure the hash will be wrong.
        content = b''.join(chunks)
        expected_hash = base64.b64encode(
            hashlib.md5(content).digest()).decode(u'utf-8')
        self.assertNotEqual(empty_hash, expected_hash)

        # Try to download into a temporary file (don't use
        # `_NamedTemporaryFile` it will try to remove after the file is
        # already removed)
        filehandle, filename = tempfile.mkstemp()
        os.close(filehandle)
        with self.assertRaises(DataCorruption) as exc_info:
            blob.download_to_filename(filename)

        msg = _CHECKSUM_MISMATCH.format(media_link, empty_hash, expected_hash)
        self.assertEqual(exc_info.exception.args, (msg,))
        # Make sure the file was cleaned up.
        self.assertFalse(os.path.exists(filename))

        # Check the mocks.
        response.__enter__.assert_called_once_with()
        response.__exit__.assert_called_once_with(None, None, None)
        response.iter_content.assert_called_once_with(
            chunk_size=8192, decode_unicode=False)
        transport.request.assert_called_once_with(
            'GET',
            media_link,
            data=None,
            headers={'accept-encoding': 'gzip'},
            stream=True,
        )

    def test_download_to_filename_w_key(self):
        import os
        import time
        from google.cloud._testing import _NamedTemporaryFile

        blob_name = 'blob-name'
        transport = self._mock_download_transport()
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(_http=transport, spec=['_http'])
        bucket = _Bucket(client)
        media_link = 'http://example.com/media/'
        properties = {'mediaLink': media_link,
                      'updated': '2014-12-06T13:13:50.690Z'}
        key = b'aa426195405adee2c8081bb9e7e74b19'
        blob = self._make_one(
            blob_name, bucket=bucket, properties=properties,
            encryption_key=key)
        # Modify the blob so there there will be 2 chunks of size 3.
        blob._CHUNK_SIZE_MULTIPLE = 1
        blob.chunk_size = 3

        with _NamedTemporaryFile() as temp:
            blob.download_to_filename(temp.name)
            with open(temp.name, 'rb') as file_obj:
                wrote = file_obj.read()
                mtime = os.path.getmtime(temp.name)
                updated_time = time.mktime(blob.updated.timetuple())

        self.assertEqual(wrote, b'abcdef')
        self.assertEqual(mtime, updated_time)

        header_key_value = 'YWE0MjYxOTU0MDVhZGVlMmM4MDgxYmI5ZTdlNzRiMTk='
        header_key_hash_value = 'V3Kwe46nKc3xLv96+iJ707YfZfFvlObta8TQcx2gpm0='
        key_headers = {
            'X-Goog-Encryption-Key-Sha256': header_key_hash_value,
            'X-Goog-Encryption-Algorithm': 'AES256',
            'X-Goog-Encryption-Key': header_key_value,
            'accept-encoding': 'gzip',
        }
        self._check_session_mocks(
            client, transport, media_link, headers=key_headers)

    def test_download_as_string(self):
        blob_name = 'blob-name'
        transport = self._mock_download_transport()
        # Create a fake client/bucket and use them in the Blob() constructor.
        client = mock.Mock(_http=transport, spec=['_http'])
        bucket = _Bucket(client)
        media_link = 'http://example.com/media/'
        properties = {'mediaLink': media_link}
        blob = self._make_one(blob_name, bucket=bucket, properties=properties)
        # Modify the blob so there there will be 2 chunks of size 3.
        blob._CHUNK_SIZE_MULTIPLE = 1
        blob.chunk_size = 3

        fetched = blob.download_as_string()
        self.assertEqual(fetched, b'abcdef')

        self._check_session_mocks(client, transport, media_link)

    def test__get_content_type_explicit(self):
        blob = self._make_one(u'blob-name', bucket=None)

        content_type = u'text/plain'
        return_value = blob._get_content_type(content_type)
        self.assertEqual(return_value, content_type)

    def test__get_content_type_from_blob(self):
        blob = self._make_one(u'blob-name', bucket=None)
        blob.content_type = u'video/mp4'

        return_value = blob._get_content_type(None)
        self.assertEqual(return_value, blob.content_type)

    def test__get_content_type_from_filename(self):
        blob = self._make_one(u'blob-name', bucket=None)

        return_value = blob._get_content_type(None, filename='archive.tar')
        self.assertEqual(return_value, 'application/x-tar')

    def test__get_content_type_default(self):
        blob = self._make_one(u'blob-name', bucket=None)

        return_value = blob._get_content_type(None)
        self.assertEqual(return_value, u'application/octet-stream')

    def test__get_writable_metadata_no_changes(self):
        name = u'blob-name'
        blob = self._make_one(name, bucket=None)

        object_metadata = blob._get_writable_metadata()
        expected = {'name': name}
        self.assertEqual(object_metadata, expected)

    def test__get_writable_metadata_with_changes(self):
        name = u'blob-name'
        blob = self._make_one(name, bucket=None)
        blob.storage_class = 'NEARLINE'
        blob.cache_control = 'max-age=3600'
        blob.metadata = {'color': 'red'}

        object_metadata = blob._get_writable_metadata()
        expected = {
            'cacheControl': blob.cache_control,
            'metadata': blob.metadata,
            'name': name,
            'storageClass': blob.storage_class,
        }
        self.assertEqual(object_metadata, expected)

    def test__get_writable_metadata_unwritable_field(self):
        name = u'blob-name'
        properties = {'updated': '2016-10-16T18:18:18.181Z'}
        blob = self._make_one(name, bucket=None, properties=properties)
        # Fake that `updated` is in changes.
        blob._changes.add('updated')

        object_metadata = blob._get_writable_metadata()
        expected = {'name': name}
        self.assertEqual(object_metadata, expected)

    def test__get_upload_arguments(self):
        name = u'blob-name'
        key = b'[pXw@,p@@AfBfrR3x-2b2SCHR,.?YwRO'
        blob = self._make_one(name, bucket=None, encryption_key=key)
        blob.content_disposition = 'inline'

        content_type = u'image/jpeg'
        info = blob._get_upload_arguments(content_type)

        headers, object_metadata, new_content_type = info
        header_key_value = 'W3BYd0AscEBAQWZCZnJSM3gtMmIyU0NIUiwuP1l3Uk8='
        header_key_hash_value = 'G0++dxF4q5rG4o9kE8gvEKn15RH6wLm0wXV1MgAlXOg='
        expected_headers = {
            'X-Goog-Encryption-Algorithm': 'AES256',
            'X-Goog-Encryption-Key': header_key_value,
            'X-Goog-Encryption-Key-Sha256': header_key_hash_value,
        }
        self.assertEqual(headers, expected_headers)
        expected_metadata = {
            'contentDisposition': blob.content_disposition,
            'name': name,
        }
        self.assertEqual(object_metadata, expected_metadata)
        self.assertEqual(new_content_type, content_type)

    def _mock_transport(self, status_code, headers, content=b''):
        fake_transport = mock.Mock(spec=['request'])
        fake_response = self._mock_requests_response(
            status_code, headers, content=content)
        fake_transport.request.return_value = fake_response
        return fake_transport

    def _do_multipart_success(self, mock_get_boundary, size=None,
                              num_retries=None, user_project=None):
        bucket = _Bucket(name='w00t', user_project=user_project)
        blob = self._make_one(u'blob-name', bucket=bucket)
        self.assertIsNone(blob.chunk_size)

        # Create mocks to be checked for doing transport.
        transport = self._mock_transport(http_client.OK, {})

        # Create some mock arguments.
        client = mock.Mock(_http=transport, spec=['_http'])
        data = b'data here hear hier'
        stream = io.BytesIO(data)
        content_type = u'application/xml'
        response = blob._do_multipart_upload(
            client, stream, content_type, size, num_retries)

        # Check the mocks and the returned value.
        self.assertIs(response, transport.request.return_value)
        if size is None:
            data_read = data
            self.assertEqual(stream.tell(), len(data))
        else:
            data_read = data[:size]
            self.assertEqual(stream.tell(), size)

        mock_get_boundary.assert_called_once_with()

        upload_url = (
            'https://www.googleapis.com/upload/storage/v1' +
            bucket.path +
            '/o?uploadType=multipart')
        if user_project is not None:
            upload_url += '&userProject={}'.format(user_project)
        payload = (
            b'--==0==\r\n' +
            b'content-type: application/json; charset=UTF-8\r\n\r\n' +
            b'{"name": "blob-name"}\r\n' +
            b'--==0==\r\n' +
            b'content-type: application/xml\r\n\r\n' +
            data_read +
            b'\r\n--==0==--')
        headers = {'content-type': b'multipart/related; boundary="==0=="'}
        transport.request.assert_called_once_with(
            'POST', upload_url, data=payload, headers=headers)

    @mock.patch(u'google.resumable_media._upload.get_boundary',
                return_value=b'==0==')
    def test__do_multipart_upload_no_size(self, mock_get_boundary):
        self._do_multipart_success(mock_get_boundary)

    @mock.patch(u'google.resumable_media._upload.get_boundary',
                return_value=b'==0==')
    def test__do_multipart_upload_with_size(self, mock_get_boundary):
        self._do_multipart_success(mock_get_boundary, size=10)

    @mock.patch(u'google.resumable_media._upload.get_boundary',
                return_value=b'==0==')
    def test__do_multipart_upload_with_user_project(self, mock_get_boundary):
        user_project = 'user-project-123'
        self._do_multipart_success(
            mock_get_boundary, user_project=user_project)

    @mock.patch(u'google.resumable_media._upload.get_boundary',
                return_value=b'==0==')
    def test__do_multipart_upload_with_retry(self, mock_get_boundary):
        self._do_multipart_success(mock_get_boundary, num_retries=8)

    def test__do_multipart_upload_bad_size(self):
        blob = self._make_one(u'blob-name', bucket=None)

        data = b'data here hear hier'
        stream = io.BytesIO(data)
        size = 50
        self.assertGreater(size, len(data))

        with self.assertRaises(ValueError) as exc_info:
            blob._do_multipart_upload(None, stream, None, size, None)

        exc_contents = str(exc_info.exception)
        self.assertIn(
            'was specified but the file-like object only had', exc_contents)
        self.assertEqual(stream.tell(), len(data))

    def _initiate_resumable_helper(
            self, size=None, extra_headers=None, chunk_size=None,
            num_retries=None, user_project=None):
        from google.resumable_media.requests import ResumableUpload

        bucket = _Bucket(name='whammy', user_project=user_project)
        blob = self._make_one(u'blob-name', bucket=bucket)
        blob.metadata = {'rook': 'takes knight'}
        blob.chunk_size = 3 * blob._CHUNK_SIZE_MULTIPLE
        self.assertIsNotNone(blob.chunk_size)

        # Need to make sure **same** dict is used because ``json.dumps()``
        # will depend on the hash order.
        object_metadata = blob._get_writable_metadata()
        blob._get_writable_metadata = mock.Mock(
            return_value=object_metadata, spec=[])

        # Create mocks to be checked for doing transport.
        resumable_url = 'http://test.invalid?upload_id=hey-you'
        response_headers = {'location': resumable_url}
        transport = self._mock_transport(http_client.OK, response_headers)

        # Create some mock arguments and call the method under test.
        client = mock.Mock(_http=transport, spec=[u'_http'])
        data = b'hello hallo halo hi-low'
        stream = io.BytesIO(data)
        content_type = u'text/plain'
        upload, transport = blob._initiate_resumable_upload(
            client, stream, content_type, size, num_retries,
            extra_headers=extra_headers, chunk_size=chunk_size)

        # Check the returned values.
        self.assertIsInstance(upload, ResumableUpload)
        upload_url = (
            'https://www.googleapis.com/upload/storage/v1' +
            bucket.path +
            '/o?uploadType=resumable')
        if user_project is not None:
            upload_url += '&userProject={}'.format(user_project)
        self.assertEqual(upload.upload_url, upload_url)
        if extra_headers is None:
            self.assertEqual(upload._headers, {})
        else:
            self.assertEqual(upload._headers, extra_headers)
            self.assertIsNot(upload._headers, extra_headers)
        self.assertFalse(upload.finished)
        if chunk_size is None:
            self.assertEqual(upload._chunk_size, blob.chunk_size)
        else:
            self.assertNotEqual(blob.chunk_size, chunk_size)
            self.assertEqual(upload._chunk_size, chunk_size)
        self.assertIs(upload._stream, stream)
        if size is None:
            self.assertIsNone(upload._total_bytes)
        else:
            self.assertEqual(upload._total_bytes, size)
        self.assertEqual(upload._content_type, content_type)
        self.assertEqual(upload.resumable_url, resumable_url)
        retry_strategy = upload._retry_strategy
        self.assertEqual(retry_strategy.max_sleep, 64.0)
        if num_retries is None:
            self.assertEqual(retry_strategy.max_cumulative_retry, 600.0)
            self.assertIsNone(retry_strategy.max_retries)
        else:
            self.assertIsNone(retry_strategy.max_cumulative_retry)
            self.assertEqual(retry_strategy.max_retries, num_retries)
        self.assertIs(transport, transport)
        # Make sure we never read from the stream.
        self.assertEqual(stream.tell(), 0)

        # Check the mocks.
        blob._get_writable_metadata.assert_called_once_with()
        payload = json.dumps(object_metadata).encode('utf-8')
        expected_headers = {
            'content-type': 'application/json; charset=UTF-8',
            'x-upload-content-type': content_type,
        }
        if size is not None:
            expected_headers['x-upload-content-length'] = str(size)
        if extra_headers is not None:
            expected_headers.update(extra_headers)
        transport.request.assert_called_once_with(
            'POST', upload_url, data=payload, headers=expected_headers)

    def test__initiate_resumable_upload_no_size(self):
        self._initiate_resumable_helper()

    def test__initiate_resumable_upload_with_size(self):
        self._initiate_resumable_helper(size=10000)

    def test__initiate_resumable_upload_with_user_project(self):
        user_project = 'user-project-123'
        self._initiate_resumable_helper(user_project=user_project)

    def test__initiate_resumable_upload_with_chunk_size(self):
        one_mb = 1048576
        self._initiate_resumable_helper(chunk_size=one_mb)

    def test__initiate_resumable_upload_with_extra_headers(self):
        extra_headers = {'origin': 'http://not-in-kansas-anymore.invalid'}
        self._initiate_resumable_helper(extra_headers=extra_headers)

    def test__initiate_resumable_upload_with_retry(self):
        self._initiate_resumable_helper(num_retries=11)

    def _make_resumable_transport(self, headers1, headers2,
                                  headers3, total_bytes):
        from google import resumable_media

        fake_transport = mock.Mock(spec=['request'])

        fake_response1 = self._mock_requests_response(
            http_client.OK, headers1)
        fake_response2 = self._mock_requests_response(
            resumable_media.PERMANENT_REDIRECT, headers2)
        json_body = '{{"size": "{:d}"}}'.format(total_bytes)
        fake_response3 = self._mock_requests_response(
            http_client.OK, headers3,
            content=json_body.encode('utf-8'))

        responses = [fake_response1, fake_response2, fake_response3]
        fake_transport.request.side_effect = responses
        return fake_transport, responses

    @staticmethod
    def _do_resumable_upload_call0(blob, content_type, size=None):
        # First mock transport.request() does initiates upload.
        upload_url = (
            'https://www.googleapis.com/upload/storage/v1' +
            blob.bucket.path +
            '/o?uploadType=resumable')
        expected_headers = {
            'content-type': 'application/json; charset=UTF-8',
            'x-upload-content-type': content_type,
        }
        if size is not None:
            expected_headers['x-upload-content-length'] = str(size)
        payload = json.dumps({'name': blob.name}).encode('utf-8')
        return mock.call(
            'POST', upload_url, data=payload, headers=expected_headers)

    @staticmethod
    def _do_resumable_upload_call1(blob, content_type, data,
                                   resumable_url, size=None):
        # Second mock transport.request() does sends first chunk.
        if size is None:
            content_range = 'bytes 0-{:d}/*'.format(blob.chunk_size - 1)
        else:
            content_range = 'bytes 0-{:d}/{:d}'.format(
                blob.chunk_size - 1, size)

        expected_headers = {
            'content-type': content_type,
            'content-range': content_range,
        }
        payload = data[:blob.chunk_size]
        return mock.call(
            'PUT', resumable_url, data=payload, headers=expected_headers)

    @staticmethod
    def _do_resumable_upload_call2(blob, content_type, data,
                                   resumable_url, total_bytes):
        # Third mock transport.request() does sends last chunk.
        content_range = 'bytes {:d}-{:d}/{:d}'.format(
            blob.chunk_size, total_bytes - 1, total_bytes)
        expected_headers = {
            'content-type': content_type,
            'content-range': content_range,
        }
        payload = data[blob.chunk_size:]
        return mock.call(
            'PUT', resumable_url, data=payload, headers=expected_headers)

    def _do_resumable_helper(self, use_size=False, num_retries=None):
        bucket = _Bucket(name='yesterday')
        blob = self._make_one(u'blob-name', bucket=bucket)
        blob.chunk_size = blob._CHUNK_SIZE_MULTIPLE
        self.assertIsNotNone(blob.chunk_size)

        # Data to be uploaded.
        data = b'<html>' + (b'A' * blob.chunk_size) + b'</html>'
        total_bytes = len(data)
        if use_size:
            size = total_bytes
        else:
            size = None

        # Create mocks to be checked for doing transport.
        resumable_url = 'http://test.invalid?upload_id=and-then-there-was-1'
        headers1 = {'location': resumable_url}
        headers2 = {'range': 'bytes=0-{:d}'.format(blob.chunk_size - 1)}
        transport, responses = self._make_resumable_transport(
            headers1, headers2, {}, total_bytes)

        # Create some mock arguments and call the method under test.
        client = mock.Mock(_http=transport, spec=['_http'])
        stream = io.BytesIO(data)
        content_type = u'text/html'
        response = blob._do_resumable_upload(
            client, stream, content_type, size, num_retries)

        # Check the returned values.
        self.assertIs(response, responses[2])
        self.assertEqual(stream.tell(), total_bytes)

        # Check the mocks.
        call0 = self._do_resumable_upload_call0(blob, content_type, size=size)
        call1 = self._do_resumable_upload_call1(
            blob, content_type, data, resumable_url, size=size)
        call2 = self._do_resumable_upload_call2(
            blob, content_type, data, resumable_url, total_bytes)
        self.assertEqual(
            transport.request.mock_calls, [call0, call1, call2])

    def test__do_resumable_upload_no_size(self):
        self._do_resumable_helper()

    def test__do_resumable_upload_with_size(self):
        self._do_resumable_helper(use_size=True)

    def test__do_resumable_upload_with_retry(self):
        self._do_resumable_helper(num_retries=6)

    def _do_upload_helper(self, chunk_size=None, num_retries=None):
        blob = self._make_one(u'blob-name', bucket=None)

        # Create a fake response.
        response = mock.Mock(spec=[u'json'])
        response.json.return_value = mock.sentinel.json
        # Mock **both** helpers.
        blob._do_multipart_upload = mock.Mock(return_value=response, spec=[])
        blob._do_resumable_upload = mock.Mock(return_value=response, spec=[])

        if chunk_size is None:
            self.assertIsNone(blob.chunk_size)
        else:
            blob.chunk_size = chunk_size
            self.assertIsNotNone(blob.chunk_size)

        client = mock.sentinel.client
        stream = mock.sentinel.stream
        content_type = u'video/mp4'
        size = 12345654321

        # Make the request and check the mocks.
        created_json = blob._do_upload(
            client, stream, content_type, size, num_retries)
        self.assertIs(created_json, mock.sentinel.json)
        response.json.assert_called_once_with()
        if chunk_size is None:
            blob._do_multipart_upload.assert_called_once_with(
                client, stream, content_type, size, num_retries)
            blob._do_resumable_upload.assert_not_called()
        else:
            blob._do_multipart_upload.assert_not_called()
            blob._do_resumable_upload.assert_called_once_with(
                client, stream, content_type, size, num_retries)

    def test__do_upload_without_chunk_size(self):
        self._do_upload_helper()

    def test__do_upload_with_chunk_size(self):
        chunk_size = 1024 * 1024 * 1024  # 1GB
        self._do_upload_helper(chunk_size=chunk_size)

    def test__do_upload_with_retry(self):
        self._do_upload_helper(num_retries=20)

    def _upload_from_file_helper(self, side_effect=None, **kwargs):
        from google.cloud._helpers import UTC

        blob = self._make_one('blob-name', bucket=None)

        # Mock low-level upload helper on blob (it is tested elsewhere).
        created_json = {'updated': '2017-01-01T09:09:09.081Z'}
        blob._do_upload = mock.Mock(return_value=created_json, spec=[])
        if side_effect is not None:
            blob._do_upload.side_effect = side_effect
        # Make sure `updated` is empty before the request.
        self.assertIsNone(blob.updated)

        data = b'data is here'
        stream = io.BytesIO(data)
        stream.seek(2)  # Not at zero.
        content_type = u'font/woff'
        client = mock.sentinel.client
        ret_val = blob.upload_from_file(
            stream, size=len(data), content_type=content_type,
            client=client, **kwargs)

        # Check the response and side-effects.
        self.assertIsNone(ret_val)
        new_updated = datetime.datetime(
            2017, 1, 1, 9, 9, 9, 81000, tzinfo=UTC)
        self.assertEqual(blob.updated, new_updated)

        # Check the mock.
        num_retries = kwargs.get('num_retries')
        blob._do_upload.assert_called_once_with(
            client, stream, content_type, len(data), num_retries)

        return stream

    def test_upload_from_file_success(self):
        stream = self._upload_from_file_helper()
        assert stream.tell() == 2

    @mock.patch('warnings.warn')
    def test_upload_from_file_with_retries(self, mock_warn):
        from google.cloud.storage import blob as blob_module

        self._upload_from_file_helper(num_retries=20)
        mock_warn.assert_called_once_with(
            blob_module._NUM_RETRIES_MESSAGE, DeprecationWarning)

    def test_upload_from_file_with_rewind(self):
        stream = self._upload_from_file_helper(rewind=True)
        assert stream.tell() == 0

    def test_upload_from_file_failure(self):
        import requests

        from google.resumable_media import InvalidResponse
        from google.cloud import exceptions

        message = b'Someone is already in this spot.'
        response = requests.Response()
        response._content = message
        response.status_code = http_client.CONFLICT
        response.request = requests.Request(
            'POST', 'http://example.com').prepare()
        side_effect = InvalidResponse(response)

        with self.assertRaises(exceptions.Conflict) as exc_info:
            self._upload_from_file_helper(side_effect=side_effect)

        self.assertIn(message.decode('utf-8'), exc_info.exception.message)
        self.assertEqual(exc_info.exception.errors, [])

    def _do_upload_mock_call_helper(self, blob, client, content_type, size):
        self.assertEqual(blob._do_upload.call_count, 1)
        mock_call = blob._do_upload.mock_calls[0]
        call_name, pos_args, kwargs = mock_call
        self.assertEqual(call_name, '')
        self.assertEqual(len(pos_args), 5)
        self.assertEqual(pos_args[0], client)
        self.assertEqual(pos_args[2], content_type)
        self.assertEqual(pos_args[3], size)
        self.assertIsNone(pos_args[4])  # num_retries
        self.assertEqual(kwargs, {})

        return pos_args[1]

    def test_upload_from_filename(self):
        from google.cloud._testing import _NamedTemporaryFile

        blob = self._make_one('blob-name', bucket=None)
        # Mock low-level upload helper on blob (it is tested elsewhere).
        created_json = {'metadata': {'mint': 'ice-cream'}}
        blob._do_upload = mock.Mock(return_value=created_json, spec=[])
        # Make sure `metadata` is empty before the request.
        self.assertIsNone(blob.metadata)

        data = b'soooo much data'
        content_type = u'image/svg+xml'
        client = mock.sentinel.client
        with _NamedTemporaryFile() as temp:
            with open(temp.name, 'wb') as file_obj:
                file_obj.write(data)

            ret_val = blob.upload_from_filename(
                temp.name, content_type=content_type, client=client)

        # Check the response and side-effects.
        self.assertIsNone(ret_val)
        self.assertEqual(blob.metadata, created_json['metadata'])

        # Check the mock.
        stream = self._do_upload_mock_call_helper(
            blob, client, content_type, len(data))
        self.assertTrue(stream.closed)
        self.assertEqual(stream.mode, 'rb')
        self.assertEqual(stream.name, temp.name)

    def _upload_from_string_helper(self, data, **kwargs):
        from google.cloud._helpers import _to_bytes

        blob = self._make_one('blob-name', bucket=None)

        # Mock low-level upload helper on blob (it is tested elsewhere).
        created_json = {'componentCount': '5'}
        blob._do_upload = mock.Mock(return_value=created_json, spec=[])
        # Make sure `metadata` is empty before the request.
        self.assertIsNone(blob.component_count)

        client = mock.sentinel.client
        ret_val = blob.upload_from_string(data, client=client, **kwargs)

        # Check the response and side-effects.
        self.assertIsNone(ret_val)
        self.assertEqual(blob.component_count, 5)

        # Check the mock.
        payload = _to_bytes(data, encoding='utf-8')
        stream = self._do_upload_mock_call_helper(
            blob, client, 'text/plain', len(payload))
        self.assertIsInstance(stream, io.BytesIO)
        self.assertEqual(stream.getvalue(), payload)

    def test_upload_from_string_w_bytes(self):
        data = b'XB]jb\xb8tad\xe0'
        self._upload_from_string_helper(data)

    def test_upload_from_string_w_text(self):
        data = u'\N{snowman} \N{sailboat}'
        self._upload_from_string_helper(data)

    def _create_resumable_upload_session_helper(self, origin=None,
                                                side_effect=None):
        bucket = _Bucket(name='alex-trebek')
        blob = self._make_one('blob-name', bucket=bucket)
        chunk_size = 99 * blob._CHUNK_SIZE_MULTIPLE
        blob.chunk_size = chunk_size

        # Create mocks to be checked for doing transport.
        resumable_url = 'http://test.invalid?upload_id=clean-up-everybody'
        response_headers = {'location': resumable_url}
        transport = self._mock_transport(
            http_client.OK, response_headers)
        if side_effect is not None:
            transport.request.side_effect = side_effect

        # Create some mock arguments and call the method under test.
        content_type = u'text/plain'
        size = 10000
        client = mock.Mock(_http=transport, spec=[u'_http'])
        new_url = blob.create_resumable_upload_session(
            content_type=content_type, size=size,
            origin=origin, client=client)

        # Check the returned value and (lack of) side-effect.
        self.assertEqual(new_url, resumable_url)
        self.assertEqual(blob.chunk_size, chunk_size)

        # Check the mocks.
        upload_url = (
            'https://www.googleapis.com/upload/storage/v1' +
            bucket.path +
            '/o?uploadType=resumable')
        payload = b'{"name": "blob-name"}'
        expected_headers = {
            'content-type': 'application/json; charset=UTF-8',
            'x-upload-content-length': str(size),
            'x-upload-content-type': content_type,
        }
        if origin is not None:
            expected_headers['Origin'] = origin
        transport.request.assert_called_once_with(
            'POST', upload_url, data=payload, headers=expected_headers)

    def test_create_resumable_upload_session(self):
        self._create_resumable_upload_session_helper()

    def test_create_resumable_upload_session_with_origin(self):
        self._create_resumable_upload_session_helper(
            origin='http://google.com')

    def test_create_resumable_upload_session_with_failure(self):
        from google.resumable_media import InvalidResponse
        from google.cloud import exceptions

        message = b'5-oh-3 woe is me.'
        response = self._mock_requests_response(
            content=message, status_code=http_client.SERVICE_UNAVAILABLE,
            headers={})
        side_effect = InvalidResponse(response)

        with self.assertRaises(exceptions.ServiceUnavailable) as exc_info:
            self._create_resumable_upload_session_helper(
                side_effect=side_effect)

        self.assertIn(message.decode('utf-8'), exc_info.exception.message)
        self.assertEqual(exc_info.exception.errors, [])

    def test_get_iam_policy(self):
        from google.cloud.storage.iam import STORAGE_OWNER_ROLE
        from google.cloud.storage.iam import STORAGE_EDITOR_ROLE
        from google.cloud.storage.iam import STORAGE_VIEWER_ROLE
        from google.cloud.iam import Policy

        BLOB_NAME = 'blob-name'
        PATH = '/b/name/o/%s' % (BLOB_NAME,)
        ETAG = 'DEADBEEF'
        VERSION = 17
        OWNER1 = 'user:phred@example.com'
        OWNER2 = 'group:cloud-logs@google.com'
        EDITOR1 = 'domain:google.com'
        EDITOR2 = 'user:phred@example.com'
        VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
        VIEWER2 = 'user:phred@example.com'
        RETURNED = {
            'resourceId': PATH,
            'etag': ETAG,
            'version': VERSION,
            'bindings': [
                {'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]},
                {'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
                {'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
            ],
        }
        after = ({'status': http_client.OK}, RETURNED)
        EXPECTED = {
            binding['role']: set(binding['members'])
            for binding in RETURNED['bindings']}
        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)

        policy = blob.get_iam_policy()

        self.assertIsInstance(policy, Policy)
        self.assertEqual(policy.etag, RETURNED['etag'])
        self.assertEqual(policy.version, RETURNED['version'])
        self.assertEqual(dict(policy), EXPECTED)

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0], {
            'method': 'GET',
            'path': '%s/iam' % (PATH,),
            'query_params': {},
            '_target_object': None,
        })

    def test_get_iam_policy_w_user_project(self):
        from google.cloud.iam import Policy

        BLOB_NAME = 'blob-name'
        USER_PROJECT = 'user-project-123'
        PATH = '/b/name/o/%s' % (BLOB_NAME,)
        ETAG = 'DEADBEEF'
        VERSION = 17
        RETURNED = {
            'resourceId': PATH,
            'etag': ETAG,
            'version': VERSION,
            'bindings': [],
        }
        after = ({'status': http_client.OK}, RETURNED)
        EXPECTED = {}
        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client, user_project=USER_PROJECT)
        blob = self._make_one(BLOB_NAME, bucket=bucket)

        policy = blob.get_iam_policy()

        self.assertIsInstance(policy, Policy)
        self.assertEqual(policy.etag, RETURNED['etag'])
        self.assertEqual(policy.version, RETURNED['version'])
        self.assertEqual(dict(policy), EXPECTED)

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0], {
            'method': 'GET',
            'path': '%s/iam' % (PATH,),
            'query_params': {'userProject': USER_PROJECT},
            '_target_object': None,
        })

    def test_set_iam_policy(self):
        import operator
        from google.cloud.storage.iam import STORAGE_OWNER_ROLE
        from google.cloud.storage.iam import STORAGE_EDITOR_ROLE
        from google.cloud.storage.iam import STORAGE_VIEWER_ROLE
        from google.cloud.iam import Policy

        BLOB_NAME = 'blob-name'
        PATH = '/b/name/o/%s' % (BLOB_NAME,)
        ETAG = 'DEADBEEF'
        VERSION = 17
        OWNER1 = 'user:phred@example.com'
        OWNER2 = 'group:cloud-logs@google.com'
        EDITOR1 = 'domain:google.com'
        EDITOR2 = 'user:phred@example.com'
        VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
        VIEWER2 = 'user:phred@example.com'
        BINDINGS = [
            {'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]},
            {'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
            {'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
        ]
        RETURNED = {
            'etag': ETAG,
            'version': VERSION,
            'bindings': BINDINGS,
        }
        after = ({'status': http_client.OK}, RETURNED)
        policy = Policy()
        for binding in BINDINGS:
            policy[binding['role']] = binding['members']

        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)

        returned = blob.set_iam_policy(policy)

        self.assertEqual(returned.etag, ETAG)
        self.assertEqual(returned.version, VERSION)
        self.assertEqual(dict(returned), dict(policy))

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'PUT')
        self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,))
        self.assertEqual(kw[0]['query_params'], {})
        sent = kw[0]['data']
        self.assertEqual(sent['resourceId'], PATH)
        self.assertEqual(len(sent['bindings']), len(BINDINGS))
        key = operator.itemgetter('role')
        for found, expected in zip(
                sorted(sent['bindings'], key=key),
                sorted(BINDINGS, key=key)):
            self.assertEqual(found['role'], expected['role'])
            self.assertEqual(
                sorted(found['members']), sorted(expected['members']))

    def test_set_iam_policy_w_user_project(self):
        from google.cloud.iam import Policy

        BLOB_NAME = 'blob-name'
        USER_PROJECT = 'user-project-123'
        PATH = '/b/name/o/%s' % (BLOB_NAME,)
        ETAG = 'DEADBEEF'
        VERSION = 17
        BINDINGS = []
        RETURNED = {
            'etag': ETAG,
            'version': VERSION,
            'bindings': BINDINGS,
        }
        after = ({'status': http_client.OK}, RETURNED)
        policy = Policy()

        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client, user_project=USER_PROJECT)
        blob = self._make_one(BLOB_NAME, bucket=bucket)

        returned = blob.set_iam_policy(policy)

        self.assertEqual(returned.etag, ETAG)
        self.assertEqual(returned.version, VERSION)
        self.assertEqual(dict(returned), dict(policy))

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'PUT')
        self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,))
        self.assertEqual(kw[0]['query_params'], {'userProject': USER_PROJECT})
        self.assertEqual(kw[0]['data'], {'resourceId': PATH})

    def test_test_iam_permissions(self):
        from google.cloud.storage.iam import STORAGE_OBJECTS_LIST
        from google.cloud.storage.iam import STORAGE_BUCKETS_GET
        from google.cloud.storage.iam import STORAGE_BUCKETS_UPDATE

        BLOB_NAME = 'blob-name'
        PATH = '/b/name/o/%s' % (BLOB_NAME,)
        PERMISSIONS = [
            STORAGE_OBJECTS_LIST,
            STORAGE_BUCKETS_GET,
            STORAGE_BUCKETS_UPDATE,
        ]
        ALLOWED = PERMISSIONS[1:]
        RETURNED = {'permissions': ALLOWED}
        after = ({'status': http_client.OK}, RETURNED)
        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)

        allowed = blob.test_iam_permissions(PERMISSIONS)

        self.assertEqual(allowed, ALLOWED)

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'GET')
        self.assertEqual(kw[0]['path'], '%s/iam/testPermissions' % (PATH,))
        self.assertEqual(kw[0]['query_params'], {'permissions': PERMISSIONS})

    def test_test_iam_permissions_w_user_project(self):
        from google.cloud.storage.iam import STORAGE_OBJECTS_LIST
        from google.cloud.storage.iam import STORAGE_BUCKETS_GET
        from google.cloud.storage.iam import STORAGE_BUCKETS_UPDATE

        BLOB_NAME = 'blob-name'
        USER_PROJECT = 'user-project-123'
        PATH = '/b/name/o/%s' % (BLOB_NAME,)
        PERMISSIONS = [
            STORAGE_OBJECTS_LIST,
            STORAGE_BUCKETS_GET,
            STORAGE_BUCKETS_UPDATE,
        ]
        ALLOWED = PERMISSIONS[1:]
        RETURNED = {'permissions': ALLOWED}
        after = ({'status': http_client.OK}, RETURNED)
        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client, user_project=USER_PROJECT)
        blob = self._make_one(BLOB_NAME, bucket=bucket)

        allowed = blob.test_iam_permissions(PERMISSIONS)

        self.assertEqual(allowed, ALLOWED)

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'GET')
        self.assertEqual(kw[0]['path'], '%s/iam/testPermissions' % (PATH,))
        self.assertEqual(
            kw[0]['query_params'],
            {'permissions': PERMISSIONS, 'userProject': USER_PROJECT})

    def test_make_public(self):
        from google.cloud.storage.acl import _ACLEntity

        BLOB_NAME = 'blob-name'
        permissive = [{'entity': 'allUsers', 'role': _ACLEntity.READER_ROLE}]
        after = ({'status': http_client.OK}, {'acl': permissive})
        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        blob.acl.loaded = True
        blob.make_public()
        self.assertEqual(list(blob.acl), permissive)
        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'PATCH')
        self.assertEqual(kw[0]['path'], '/b/name/o/%s' % BLOB_NAME)
        self.assertEqual(kw[0]['data'], {'acl': permissive})
        self.assertEqual(kw[0]['query_params'], {'projection': 'full'})

    def test_compose_wo_content_type_set(self):
        SOURCE_1 = 'source-1'
        SOURCE_2 = 'source-2'
        DESTINATION = 'destinaton'
        connection = _Connection()
        client = _Client(connection)
        bucket = _Bucket(client=client)
        source_1 = self._make_one(SOURCE_1, bucket=bucket)
        source_2 = self._make_one(SOURCE_2, bucket=bucket)
        destination = self._make_one(DESTINATION, bucket=bucket)

        with self.assertRaises(ValueError):
            destination.compose(sources=[source_1, source_2])

    def test_compose_minimal_w_user_project(self):
        SOURCE_1 = 'source-1'
        SOURCE_2 = 'source-2'
        DESTINATION = 'destinaton'
        RESOURCE = {
            'etag': 'DEADBEEF'
        }
        USER_PROJECT = 'user-project-123'
        after = ({'status': http_client.OK}, RESOURCE)
        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client, user_project=USER_PROJECT)
        source_1 = self._make_one(SOURCE_1, bucket=bucket)
        source_2 = self._make_one(SOURCE_2, bucket=bucket)
        destination = self._make_one(DESTINATION, bucket=bucket)
        destination.content_type = 'text/plain'

        destination.compose(sources=[source_1, source_2])

        self.assertEqual(destination.etag, 'DEADBEEF')

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0], {
            'method': 'POST',
            'path': '/b/name/o/%s/compose' % DESTINATION,
            'query_params': {'userProject': USER_PROJECT},
            'data':  {
                'sourceObjects': [
                    {'name': source_1.name},
                    {'name': source_2.name},
                ],
                'destination': {
                    'contentType': 'text/plain',
                },
            },
            '_target_object': destination,
        })

    def test_compose_w_additional_property_changes(self):
        SOURCE_1 = 'source-1'
        SOURCE_2 = 'source-2'
        DESTINATION = 'destinaton'
        RESOURCE = {
            'etag': 'DEADBEEF'
        }
        after = ({'status': http_client.OK}, RESOURCE)
        connection = _Connection(after)
        client = _Client(connection)
        bucket = _Bucket(client=client)
        source_1 = self._make_one(SOURCE_1, bucket=bucket)
        source_2 = self._make_one(SOURCE_2, bucket=bucket)
        destination = self._make_one(DESTINATION, bucket=bucket)
        destination.content_type = 'text/plain'
        destination.content_language = 'en-US'
        destination.metadata = {'my-key': 'my-value'}

        destination.compose(sources=[source_1, source_2])

        self.assertEqual(destination.etag, 'DEADBEEF')

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0], {
            'method': 'POST',
            'path': '/b/name/o/%s/compose' % DESTINATION,
            'query_params': {},
            'data':  {
                'sourceObjects': [
                    {'name': source_1.name},
                    {'name': source_2.name},
                ],
                'destination': {
                    'contentType': 'text/plain',
                    'contentLanguage': 'en-US',
                    'metadata': {
                        'my-key': 'my-value',
                    }
                },
            },
            '_target_object': destination,
        })

    def test_rewrite_response_without_resource(self):
        SOURCE_BLOB = 'source'
        DEST_BLOB = 'dest'
        DEST_BUCKET = 'other-bucket'
        TOKEN = 'TOKEN'
        RESPONSE = {
            'totalBytesRewritten': 33,
            'objectSize': 42,
            'done': False,
            'rewriteToken': TOKEN,
        }
        response = ({'status': http_client.OK}, RESPONSE)
        connection = _Connection(response)
        client = _Client(connection)
        source_bucket = _Bucket(client=client)
        source_blob = self._make_one(SOURCE_BLOB, bucket=source_bucket)
        dest_bucket = _Bucket(client=client, name=DEST_BUCKET)
        dest_blob = self._make_one(DEST_BLOB, bucket=dest_bucket)

        token, rewritten, size = dest_blob.rewrite(source_blob)

        self.assertEqual(token, TOKEN)
        self.assertEqual(rewritten, 33)
        self.assertEqual(size, 42)

    def test_rewrite_other_bucket_other_name_no_encryption_partial(self):
        SOURCE_BLOB = 'source'
        DEST_BLOB = 'dest'
        DEST_BUCKET = 'other-bucket'
        TOKEN = 'TOKEN'
        RESPONSE = {
            'totalBytesRewritten': 33,
            'objectSize': 42,
            'done': False,
            'rewriteToken': TOKEN,
            'resource': {'etag': 'DEADBEEF'},
        }
        response = ({'status': http_client.OK}, RESPONSE)
        connection = _Connection(response)
        client = _Client(connection)
        source_bucket = _Bucket(client=client)
        source_blob = self._make_one(SOURCE_BLOB, bucket=source_bucket)
        dest_bucket = _Bucket(client=client, name=DEST_BUCKET)
        dest_blob = self._make_one(DEST_BLOB, bucket=dest_bucket)

        token, rewritten, size = dest_blob.rewrite(source_blob)

        self.assertEqual(token, TOKEN)
        self.assertEqual(rewritten, 33)
        self.assertEqual(size, 42)

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'POST')
        PATH = '/b/name/o/%s/rewriteTo/b/%s/o/%s' % (
            SOURCE_BLOB, DEST_BUCKET, DEST_BLOB)
        self.assertEqual(kw[0]['path'], PATH)
        self.assertEqual(kw[0]['query_params'], {})
        SENT = {}
        self.assertEqual(kw[0]['data'], SENT)

        headers = {
            key.title(): str(value) for key, value in kw[0]['headers'].items()}
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Algorithm', headers)
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Key', headers)
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Key-Sha256', headers)
        self.assertNotIn('X-Goog-Encryption-Algorithm', headers)
        self.assertNotIn('X-Goog-Encryption-Key', headers)
        self.assertNotIn('X-Goog-Encryption-Key-Sha256', headers)

    def test_rewrite_same_name_no_old_key_new_key_done_w_user_project(self):
        import base64
        import hashlib

        KEY = b'01234567890123456789012345678901'  # 32 bytes
        KEY_B64 = base64.b64encode(KEY).rstrip().decode('ascii')
        KEY_HASH = hashlib.sha256(KEY).digest()
        KEY_HASH_B64 = base64.b64encode(KEY_HASH).rstrip().decode('ascii')
        BLOB_NAME = 'blob'
        USER_PROJECT = 'user-project-123'
        RESPONSE = {
            'totalBytesRewritten': 42,
            'objectSize': 42,
            'done': True,
            'resource': {'etag': 'DEADBEEF'},
        }
        response = ({'status': http_client.OK}, RESPONSE)
        connection = _Connection(response)
        client = _Client(connection)
        bucket = _Bucket(client=client, user_project=USER_PROJECT)
        plain = self._make_one(BLOB_NAME, bucket=bucket)
        encrypted = self._make_one(BLOB_NAME, bucket=bucket,
                                   encryption_key=KEY)

        token, rewritten, size = encrypted.rewrite(plain)

        self.assertIsNone(token)
        self.assertEqual(rewritten, 42)
        self.assertEqual(size, 42)

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'POST')
        PATH = '/b/name/o/%s/rewriteTo/b/name/o/%s' % (BLOB_NAME, BLOB_NAME)
        self.assertEqual(kw[0]['path'], PATH)
        self.assertEqual(kw[0]['query_params'], {'userProject': USER_PROJECT})
        SENT = {}
        self.assertEqual(kw[0]['data'], SENT)

        headers = {
            key.title(): str(value) for key, value in kw[0]['headers'].items()}
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Algorithm', headers)
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Key', headers)
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Key-Sha256', headers)
        self.assertEqual(headers['X-Goog-Encryption-Algorithm'], 'AES256')
        self.assertEqual(headers['X-Goog-Encryption-Key'], KEY_B64)
        self.assertEqual(headers['X-Goog-Encryption-Key-Sha256'], KEY_HASH_B64)

    def test_rewrite_same_name_no_key_new_key_w_token(self):
        import base64
        import hashlib

        SOURCE_KEY = b'01234567890123456789012345678901'  # 32 bytes
        SOURCE_KEY_B64 = base64.b64encode(SOURCE_KEY).rstrip().decode('ascii')
        SOURCE_KEY_HASH = hashlib.sha256(SOURCE_KEY).digest()
        SOURCE_KEY_HASH_B64 = base64.b64encode(
            SOURCE_KEY_HASH).rstrip().decode('ascii')
        DEST_KEY = b'90123456789012345678901234567890'  # 32 bytes
        DEST_KEY_B64 = base64.b64encode(DEST_KEY).rstrip().decode('ascii')
        DEST_KEY_HASH = hashlib.sha256(DEST_KEY).digest()
        DEST_KEY_HASH_B64 = base64.b64encode(
            DEST_KEY_HASH).rstrip().decode('ascii')
        BLOB_NAME = 'blob'
        TOKEN = 'TOKEN'
        RESPONSE = {
            'totalBytesRewritten': 42,
            'objectSize': 42,
            'done': True,
            'resource': {'etag': 'DEADBEEF'},
        }
        response = ({'status': http_client.OK}, RESPONSE)
        connection = _Connection(response)
        client = _Client(connection)
        bucket = _Bucket(client=client)
        source = self._make_one(
            BLOB_NAME, bucket=bucket, encryption_key=SOURCE_KEY)
        dest = self._make_one(BLOB_NAME, bucket=bucket,
                              encryption_key=DEST_KEY)

        token, rewritten, size = dest.rewrite(source, token=TOKEN)

        self.assertIsNone(token)
        self.assertEqual(rewritten, 42)
        self.assertEqual(size, 42)

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'POST')
        PATH = '/b/name/o/%s/rewriteTo/b/name/o/%s' % (BLOB_NAME, BLOB_NAME)
        self.assertEqual(kw[0]['path'], PATH)
        self.assertEqual(kw[0]['query_params'], {'rewriteToken': TOKEN})
        SENT = {}
        self.assertEqual(kw[0]['data'], SENT)

        headers = {
            key.title(): str(value) for key, value in kw[0]['headers'].items()}
        self.assertEqual(
            headers['X-Goog-Copy-Source-Encryption-Algorithm'], 'AES256')
        self.assertEqual(
            headers['X-Goog-Copy-Source-Encryption-Key'], SOURCE_KEY_B64)
        self.assertEqual(
            headers['X-Goog-Copy-Source-Encryption-Key-Sha256'],
            SOURCE_KEY_HASH_B64)
        self.assertEqual(
            headers['X-Goog-Encryption-Algorithm'], 'AES256')
        self.assertEqual(
            headers['X-Goog-Encryption-Key'], DEST_KEY_B64)
        self.assertEqual(
            headers['X-Goog-Encryption-Key-Sha256'], DEST_KEY_HASH_B64)

    def test_update_storage_class_invalid(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        with self.assertRaises(ValueError):
            blob.update_storage_class(u'BOGUS')

    def test_update_storage_class_wo_encryption_key(self):
        BLOB_NAME = 'blob-name'
        STORAGE_CLASS = u'NEARLINE'
        RESPONSE = {
            'resource': {'storageClass': STORAGE_CLASS},
        }
        response = ({'status': http_client.OK}, RESPONSE)
        connection = _Connection(response)
        client = _Client(connection)
        bucket = _Bucket(client=client)
        blob = self._make_one(BLOB_NAME, bucket=bucket)

        blob.update_storage_class('NEARLINE')

        self.assertEqual(blob.storage_class, 'NEARLINE')

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'POST')
        PATH = '/b/name/o/%s/rewriteTo/b/name/o/%s' % (BLOB_NAME, BLOB_NAME)
        self.assertEqual(kw[0]['path'], PATH)
        self.assertEqual(kw[0]['query_params'], {})
        SENT = {'storageClass': STORAGE_CLASS}
        self.assertEqual(kw[0]['data'], SENT)

        headers = {
            key.title(): str(value) for key, value in kw[0]['headers'].items()}
        # Blob has no key, and therefore the relevant headers are not sent.
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Algorithm', headers)
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Key', headers)
        self.assertNotIn('X-Goog-Copy-Source-Encryption-Key-Sha256', headers)
        self.assertNotIn('X-Goog-Encryption-Algorithm', headers)
        self.assertNotIn('X-Goog-Encryption-Key', headers)
        self.assertNotIn('X-Goog-Encryption-Key-Sha256', headers)

    def test_update_storage_class_w_encryption_key_w_user_project(self):
        import base64
        import hashlib

        BLOB_NAME = 'blob-name'
        BLOB_KEY = b'01234567890123456789012345678901'  # 32 bytes
        BLOB_KEY_B64 = base64.b64encode(BLOB_KEY).rstrip().decode('ascii')
        BLOB_KEY_HASH = hashlib.sha256(BLOB_KEY).digest()
        BLOB_KEY_HASH_B64 = base64.b64encode(
            BLOB_KEY_HASH).rstrip().decode('ascii')
        STORAGE_CLASS = u'NEARLINE'
        USER_PROJECT = 'user-project-123'
        RESPONSE = {
            'resource': {'storageClass': STORAGE_CLASS},
        }
        response = ({'status': http_client.OK}, RESPONSE)
        connection = _Connection(response)
        client = _Client(connection)
        bucket = _Bucket(client=client, user_project=USER_PROJECT)
        blob = self._make_one(
            BLOB_NAME, bucket=bucket, encryption_key=BLOB_KEY)

        blob.update_storage_class('NEARLINE')

        self.assertEqual(blob.storage_class, 'NEARLINE')

        kw = connection._requested
        self.assertEqual(len(kw), 1)
        self.assertEqual(kw[0]['method'], 'POST')
        PATH = '/b/name/o/%s/rewriteTo/b/name/o/%s' % (BLOB_NAME, BLOB_NAME)
        self.assertEqual(kw[0]['path'], PATH)
        self.assertEqual(kw[0]['query_params'], {'userProject': USER_PROJECT})
        SENT = {'storageClass': STORAGE_CLASS}
        self.assertEqual(kw[0]['data'], SENT)

        headers = {
            key.title(): str(value) for key, value in kw[0]['headers'].items()}
        # Blob has key, and therefore the relevant headers are sent.
        self.assertEqual(
            headers['X-Goog-Copy-Source-Encryption-Algorithm'], 'AES256')
        self.assertEqual(
            headers['X-Goog-Copy-Source-Encryption-Key'], BLOB_KEY_B64)
        self.assertEqual(
            headers['X-Goog-Copy-Source-Encryption-Key-Sha256'],
            BLOB_KEY_HASH_B64)
        self.assertEqual(
            headers['X-Goog-Encryption-Algorithm'], 'AES256')
        self.assertEqual(
            headers['X-Goog-Encryption-Key'], BLOB_KEY_B64)
        self.assertEqual(
            headers['X-Goog-Encryption-Key-Sha256'], BLOB_KEY_HASH_B64)

    def test_cache_control_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        CACHE_CONTROL = 'no-cache'
        properties = {'cacheControl': CACHE_CONTROL}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.cache_control, CACHE_CONTROL)

    def test_cache_control_setter(self):
        BLOB_NAME = 'blob-name'
        CACHE_CONTROL = 'no-cache'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.cache_control)
        blob.cache_control = CACHE_CONTROL
        self.assertEqual(blob.cache_control, CACHE_CONTROL)

    def test_component_count(self):
        BUCKET = object()
        COMPONENT_COUNT = 42
        blob = self._make_one('blob-name', bucket=BUCKET,
                              properties={'componentCount': COMPONENT_COUNT})
        self.assertEqual(blob.component_count, COMPONENT_COUNT)

    def test_component_count_unset(self):
        BUCKET = object()
        blob = self._make_one('blob-name', bucket=BUCKET)
        self.assertIsNone(blob.component_count)

    def test_component_count_string_val(self):
        BUCKET = object()
        COMPONENT_COUNT = 42
        blob = self._make_one(
            'blob-name', bucket=BUCKET,
            properties={'componentCount': str(COMPONENT_COUNT)})
        self.assertEqual(blob.component_count, COMPONENT_COUNT)

    def test_content_disposition_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        CONTENT_DISPOSITION = 'Attachment; filename=example.jpg'
        properties = {'contentDisposition': CONTENT_DISPOSITION}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.content_disposition, CONTENT_DISPOSITION)

    def test_content_disposition_setter(self):
        BLOB_NAME = 'blob-name'
        CONTENT_DISPOSITION = 'Attachment; filename=example.jpg'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.content_disposition)
        blob.content_disposition = CONTENT_DISPOSITION
        self.assertEqual(blob.content_disposition, CONTENT_DISPOSITION)

    def test_content_encoding_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        CONTENT_ENCODING = 'gzip'
        properties = {'contentEncoding': CONTENT_ENCODING}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.content_encoding, CONTENT_ENCODING)

    def test_content_encoding_setter(self):
        BLOB_NAME = 'blob-name'
        CONTENT_ENCODING = 'gzip'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.content_encoding)
        blob.content_encoding = CONTENT_ENCODING
        self.assertEqual(blob.content_encoding, CONTENT_ENCODING)

    def test_content_language_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        CONTENT_LANGUAGE = 'pt-BR'
        properties = {'contentLanguage': CONTENT_LANGUAGE}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.content_language, CONTENT_LANGUAGE)

    def test_content_language_setter(self):
        BLOB_NAME = 'blob-name'
        CONTENT_LANGUAGE = 'pt-BR'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.content_language)
        blob.content_language = CONTENT_LANGUAGE
        self.assertEqual(blob.content_language, CONTENT_LANGUAGE)

    def test_content_type_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        CONTENT_TYPE = 'image/jpeg'
        properties = {'contentType': CONTENT_TYPE}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.content_type, CONTENT_TYPE)

    def test_content_type_setter(self):
        BLOB_NAME = 'blob-name'
        CONTENT_TYPE = 'image/jpeg'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.content_type)
        blob.content_type = CONTENT_TYPE
        self.assertEqual(blob.content_type, CONTENT_TYPE)

    def test_crc32c_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        CRC32C = 'DEADBEEF'
        properties = {'crc32c': CRC32C}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.crc32c, CRC32C)

    def test_crc32c_setter(self):
        BLOB_NAME = 'blob-name'
        CRC32C = 'DEADBEEF'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.crc32c)
        blob.crc32c = CRC32C
        self.assertEqual(blob.crc32c, CRC32C)

    def test_etag(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        ETAG = 'ETAG'
        properties = {'etag': ETAG}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.etag, ETAG)

    def test_generation(self):
        BUCKET = object()
        GENERATION = 42
        blob = self._make_one('blob-name', bucket=BUCKET,
                              properties={'generation': GENERATION})
        self.assertEqual(blob.generation, GENERATION)

    def test_generation_unset(self):
        BUCKET = object()
        blob = self._make_one('blob-name', bucket=BUCKET)
        self.assertIsNone(blob.generation)

    def test_generation_string_val(self):
        BUCKET = object()
        GENERATION = 42
        blob = self._make_one('blob-name', bucket=BUCKET,
                              properties={'generation': str(GENERATION)})
        self.assertEqual(blob.generation, GENERATION)

    def test_id(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        ID = 'ID'
        properties = {'id': ID}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.id, ID)

    def test_md5_hash_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        MD5_HASH = 'DEADBEEF'
        properties = {'md5Hash': MD5_HASH}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.md5_hash, MD5_HASH)

    def test_md5_hash_setter(self):
        BLOB_NAME = 'blob-name'
        MD5_HASH = 'DEADBEEF'
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.md5_hash)
        blob.md5_hash = MD5_HASH
        self.assertEqual(blob.md5_hash, MD5_HASH)

    def test_media_link(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        MEDIA_LINK = 'http://example.com/media/'
        properties = {'mediaLink': MEDIA_LINK}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.media_link, MEDIA_LINK)

    def test_metadata_getter(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        METADATA = {'foo': 'Foo'}
        properties = {'metadata': METADATA}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.metadata, METADATA)

    def test_metadata_setter(self):
        BLOB_NAME = 'blob-name'
        METADATA = {'foo': 'Foo'}
        bucket = _Bucket()
        blob = self._make_one(BLOB_NAME, bucket=bucket)
        self.assertIsNone(blob.metadata)
        blob.metadata = METADATA
        self.assertEqual(blob.metadata, METADATA)

    def test_metageneration(self):
        BUCKET = object()
        METAGENERATION = 42
        blob = self._make_one('blob-name', bucket=BUCKET,
                              properties={'metageneration': METAGENERATION})
        self.assertEqual(blob.metageneration, METAGENERATION)

    def test_metageneration_unset(self):
        BUCKET = object()
        blob = self._make_one('blob-name', bucket=BUCKET)
        self.assertIsNone(blob.metageneration)

    def test_metageneration_string_val(self):
        BUCKET = object()
        METAGENERATION = 42
        blob = self._make_one(
            'blob-name', bucket=BUCKET,
            properties={'metageneration': str(METAGENERATION)})
        self.assertEqual(blob.metageneration, METAGENERATION)

    def test_owner(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        OWNER = {'entity': 'project-owner-12345', 'entityId': '23456'}
        properties = {'owner': OWNER}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        owner = blob.owner
        self.assertEqual(owner['entity'], 'project-owner-12345')
        self.assertEqual(owner['entityId'], '23456')

    def test_self_link(self):
        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        SELF_LINK = 'http://example.com/self/'
        properties = {'selfLink': SELF_LINK}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.self_link, SELF_LINK)

    def test_size(self):
        BUCKET = object()
        SIZE = 42
        blob = self._make_one('blob-name', bucket=BUCKET,
                              properties={'size': SIZE})
        self.assertEqual(blob.size, SIZE)

    def test_size_unset(self):
        BUCKET = object()
        blob = self._make_one('blob-name', bucket=BUCKET)
        self.assertIsNone(blob.size)

    def test_size_string_val(self):
        BUCKET = object()
        SIZE = 42
        blob = self._make_one('blob-name', bucket=BUCKET,
                              properties={'size': str(SIZE)})
        self.assertEqual(blob.size, SIZE)

    def test_storage_class_getter(self):
        blob_name = 'blob-name'
        bucket = _Bucket()
        storage_class = 'MULTI_REGIONAL'
        properties = {'storageClass': storage_class}
        blob = self._make_one(blob_name, bucket=bucket, properties=properties)
        self.assertEqual(blob.storage_class, storage_class)

    def test_storage_class_setter(self):
        blob_name = 'blob-name'
        bucket = _Bucket()
        storage_class = 'COLDLINE'
        blob = self._make_one(blob_name, bucket=bucket)
        self.assertIsNone(blob.storage_class)
        blob.storage_class = storage_class
        self.assertEqual(blob.storage_class, storage_class)
        self.assertEqual(blob._properties, {'storageClass': storage_class})

    def test_time_deleted(self):
        from google.cloud._helpers import _RFC3339_MICROS
        from google.cloud._helpers import UTC

        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
        TIME_DELETED = TIMESTAMP.strftime(_RFC3339_MICROS)
        properties = {'timeDeleted': TIME_DELETED}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.time_deleted, TIMESTAMP)

    def test_time_deleted_unset(self):
        BUCKET = object()
        blob = self._make_one('blob-name', bucket=BUCKET)
        self.assertIsNone(blob.time_deleted)

    def test_time_created(self):
        from google.cloud._helpers import _RFC3339_MICROS
        from google.cloud._helpers import UTC

        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
        TIME_CREATED = TIMESTAMP.strftime(_RFC3339_MICROS)
        properties = {'timeCreated': TIME_CREATED}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.time_created, TIMESTAMP)

    def test_time_created_unset(self):
        BUCKET = object()
        blob = self._make_one('blob-name', bucket=BUCKET)
        self.assertIsNone(blob.time_created)

    def test_updated(self):
        from google.cloud._helpers import _RFC3339_MICROS
        from google.cloud._helpers import UTC

        BLOB_NAME = 'blob-name'
        bucket = _Bucket()
        TIMESTAMP = datetime.datetime(2014, 11, 5, 20, 34, 37, tzinfo=UTC)
        UPDATED = TIMESTAMP.strftime(_RFC3339_MICROS)
        properties = {'updated': UPDATED}
        blob = self._make_one(BLOB_NAME, bucket=bucket, properties=properties)
        self.assertEqual(blob.updated, TIMESTAMP)

    def test_updated_unset(self):
        BUCKET = object()
        blob = self._make_one('blob-name', bucket=BUCKET)
        self.assertIsNone(blob.updated)


class Test__quote(unittest.TestCase):

    @staticmethod
    def _call_fut(value):
        from google.cloud.storage.blob import _quote

        return _quote(value)

    def test_bytes(self):
        quoted = self._call_fut(b'\xDE\xAD\xBE\xEF')
        self.assertEqual(quoted, '%DE%AD%BE%EF')

    def test_unicode(self):
        helicopter = u'\U0001f681'
        quoted = self._call_fut(helicopter)
        self.assertEqual(quoted, '%F0%9F%9A%81')

    def test_bad_type(self):
        with self.assertRaises(TypeError):
            self._call_fut(None)


class Test__maybe_rewind(unittest.TestCase):

    @staticmethod
    def _call_fut(*args, **kwargs):
        from google.cloud.storage.blob import _maybe_rewind

        return _maybe_rewind(*args, **kwargs)

    def test_default(self):
        stream = mock.Mock(spec=[u'seek'])
        ret_val = self._call_fut(stream)
        self.assertIsNone(ret_val)

        stream.seek.assert_not_called()

    def test_do_not_rewind(self):
        stream = mock.Mock(spec=[u'seek'])
        ret_val = self._call_fut(stream, rewind=False)
        self.assertIsNone(ret_val)

        stream.seek.assert_not_called()

    def test_do_rewind(self):
        stream = mock.Mock(spec=[u'seek'])
        ret_val = self._call_fut(stream, rewind=True)
        self.assertIsNone(ret_val)

        stream.seek.assert_called_once_with(0, os.SEEK_SET)


class Test__raise_from_invalid_response(unittest.TestCase):

    @staticmethod
    def _call_fut(*args, **kwargs):
        from google.cloud.storage.blob import _raise_from_invalid_response

        return _raise_from_invalid_response(*args, **kwargs)

    def _helper(self, message, **kwargs):
        import requests

        from google.resumable_media import InvalidResponse
        from google.cloud import exceptions

        response = requests.Response()
        response.request = requests.Request(
            'GET', 'http://example.com').prepare()
        response.status_code = http_client.BAD_REQUEST
        response._content = message
        error = InvalidResponse(response)

        with self.assertRaises(exceptions.BadRequest) as exc_info:
            self._call_fut(error, **kwargs)

        return exc_info

    def test_default(self):
        message = b'Failure'
        exc_info = self._helper(message)
        message_str = message.decode('utf-8')
        expected = 'GET http://example.com/: {}'.format(message_str)
        self.assertEqual(exc_info.exception.message, expected)
        self.assertEqual(exc_info.exception.errors, [])


class Test__add_query_parameters(unittest.TestCase):

    @staticmethod
    def _call_fut(*args, **kwargs):
        from google.cloud.storage.blob import _add_query_parameters

        return _add_query_parameters(*args, **kwargs)

    def test_w_empty_list(self):
        BASE_URL = 'https://test.example.com/base'
        self.assertEqual(self._call_fut(BASE_URL, []), BASE_URL)

    def test_wo_existing_qs(self):
        BASE_URL = 'https://test.example.com/base'
        NV_LIST = [('one', 'One'), ('two', 'Two')]
        expected = '&'.join([
            '{}={}'.format(name, value) for name, value in NV_LIST])
        self.assertEqual(
            self._call_fut(BASE_URL, NV_LIST),
            '{}?{}'.format(BASE_URL, expected))

    def test_w_existing_qs(self):
        BASE_URL = 'https://test.example.com/base?one=Three'
        NV_LIST = [('one', 'One'), ('two', 'Two')]
        expected = '&'.join([
            '{}={}'.format(name, value) for name, value in NV_LIST])
        self.assertEqual(
            self._call_fut(BASE_URL, NV_LIST),
            '{}&{}'.format(BASE_URL, expected))


class _Connection(object):

    API_BASE_URL = 'http://example.com'
    USER_AGENT = 'testing 1.2.3'
    credentials = object()

    def __init__(self, *responses):
        self._responses = responses[:]
        self._requested = []
        self._signed = []

    def _respond(self, **kw):
        self._requested.append(kw)
        response, self._responses = self._responses[0], self._responses[1:]
        return response

    def api_request(self, **kw):
        from google.cloud.exceptions import NotFound

        info, content = self._respond(**kw)
        if info.get('status') == http_client.NOT_FOUND:
            raise NotFound(info)
        return content


class _Bucket(object):

    def __init__(self, client=None, name='name', user_project=None):
        if client is None:
            connection = _Connection()
            client = _Client(connection)
        self.client = client
        self._blobs = {}
        self._copied = []
        self._deleted = []
        self.name = name
        self.path = '/b/' + name
        self.user_project = user_project

    def delete_blob(self, blob_name, client=None):
        del self._blobs[blob_name]
        self._deleted.append((blob_name, client))


class _Signer(object):

    def __init__(self):
        self._signed = []

    def __call__(self, *args, **kwargs):
        self._signed.append((args, kwargs))
        return ('http://example.com/abucket/a-blob-name?Signature=DEADBEEF'
                '&Expiration=%s' % kwargs.get('expiration'))


class _Client(object):

    def __init__(self, connection):
        self._base_connection = connection

    @property
    def _connection(self):
        return self._base_connection

    @property
    def _credentials(self):
        return self._base_connection.credentials
