# -*- coding: utf-8 -*-
# Copyright 2014 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for signurl command."""

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division
from __future__ import unicode_literals

from datetime import datetime
from datetime import timedelta
import os
import pkgutil

import boto

import gslib.commands.signurl
from gslib.commands.signurl import HAVE_CRYPTO
from gslib.exception import CommandException
from gslib.gcs_json_api import GcsJsonApi
from gslib.iamcredentials_api import IamcredentailsApi
from gslib.impersonation_credentials import ImpersonationCredentials
import gslib.tests.testcase as testcase
from gslib.tests.testcase.integration_testcase import (SkipForS3, SkipForXML)
from gslib.tests.util import ObjectToURI as suri
from gslib.tests.util import SetBotoConfigForTest
from gslib.tests.util import SetEnvironmentForTest
from gslib.tests.util import unittest
import gslib.tests.signurl_signatures as sigs
from oauth2client import client
from oauth2client.service_account import ServiceAccountCredentials

from six import add_move, MovedModule

add_move(MovedModule('mock', 'mock', 'unittest.mock'))
from six.moves import mock

SERVICE_ACCOUNT = boto.config.get_value('GSUtil',
                                        'test_impersonate_service_account')
TEST_EMAIL = 'test%40developer.gserviceaccount.com'


# pylint: disable=protected-access
@unittest.skipUnless(HAVE_CRYPTO, 'signurl requires cryptography library.')
@SkipForS3('Signed URLs are only supported for gs:// URLs.')
class TestSignUrl(testcase.GsUtilIntegrationTestCase):
  """Integration tests for signurl command."""

  def _GetJSONKsFile(self):
    if not hasattr(self, 'json_ks_file'):
      # Dummy json keystore constructed from test.p12.
      contents = pkgutil.get_data('gslib', 'tests/test_data/test.json')
      self.json_ks_file = self.CreateTempFile(contents=contents)
    return self.json_ks_file

  def _GetKsFile(self):
    if not hasattr(self, 'ks_file'):
      # Dummy pkcs12 keystore generated with the command

      # openssl req -new -passout pass:notasecret -batch \
      # -x509 -keyout signed_url_test.key -out signed_url_test.pem \
      # -subj '/CN=test.apps.googleusercontent.com'

      # &&

      # openssl pkcs12 -export -passin pass:notasecret \
      # -passout pass:notasecret -inkey signed_url_test.key \
      # -in signed_url_test.pem -out test.p12

      # &&

      # rm signed_url_test.key signed_url_test.pem
      contents = pkgutil.get_data('gslib', 'tests/test_data/test.p12')
      self.ks_file = self.CreateTempFile(contents=contents)
    return self.ks_file

  def testSignUrlInvalidDuration(self):
    """Tests signurl fails with out of bounds value for valid duration."""
    if self._use_gcloud_storage:
      expected_status = 2
    else:
      expected_status = 1
    stderr = self.RunGsUtil(['signurl', '-d', '123d', 'ks_file', 'gs://uri'],
                            return_stderr=True,
                            expected_status=expected_status)
    if self._use_gcloud_storage:
      self.assertIn('value must be less than or equal to 7d', stderr)
    else:
      self.assertIn('CommandException: Max valid duration allowed is 7 days',
                    stderr)

  def testSignUrlInvalidDurationWithUseServiceAccount(self):
    """Tests signurl with -u flag fails duration > 12 hours."""
    stderr = self.RunGsUtil(['signurl', '-d', '13h', '-u', 'gs://uri'],
                            return_stderr=True,
                            expected_status=1)
    self.assertIn('CommandException: Max valid duration allowed is 12:00:00',
                  stderr)

  @unittest.skipUnless(not HAVE_CRYPTO, 'signurl requires crypto to decode .p12 keys')
  def testSignUrlRaiseErrorForP12KeysWithoutCrypto(self):
    bucket_uri = self.CreateBucket()
    object_uri = self.CreateObject(bucket_uri=bucket_uri, contents=b'z')
    cmd = [
        'signurl', '-p', 'notasecret', '-m', 'PUT',
        self._GetKsFile(),
        suri(object_uri)
    ]
    stderr = self.RunGsUtil(cmd, return_stderr=1, expected_status=1)
    self.assertIn('CommandException: pyca/cryptography is not available. Either install it, or please consider using the .json keyfile', stderr)

  @unittest.skipUnless(HAVE_CRYPTO, 'signurl requires crypto to decode .p12 keys.')
  def testSignUrlOutputP12(self):
    """Tests signurl output of a sample object with pkcs12 keystore."""
    bucket_uri = self.CreateBucket()
    object_uri = self.CreateObject(bucket_uri=bucket_uri, contents=b'z')
    cmd = [
        'signurl', '-p', 'notasecret', '-m', 'PUT',
        self._GetKsFile(),
        suri(object_uri)
    ]
    stdout = self.RunGsUtil(cmd, return_stdout=True)
    self.assertIn('x-goog-credential=test.apps.googleusercontent.com', stdout)
    self.assertIn('x-goog-expires=3600', stdout)
    self.assertIn('%2Fus-central1%2F', stdout)
    self.assertIn('\tPUT\t', stdout)

  def testSignUrlOutputJSON(self):
    """Tests signurl output of a sample object with JSON keystore."""
    bucket_uri = self.CreateBucket()
    object_uri = self.CreateObject(bucket_uri=bucket_uri, contents=b'z')
    cmd = ['signurl', '-m', 'PUT', self._GetJSONKsFile(), suri(object_uri)]
    stdout = self.RunGsUtil(cmd, return_stdout=True)
    self.assertIn('x-goog-credential=' + TEST_EMAIL, stdout)
    self.assertIn('x-goog-expires=3600', stdout)
    self.assertIn('%2Fus-central1%2F', stdout)
    self.assertIn('\tPUT\t', stdout)

  def testSignUrlWithJSONKeyFileAndObjectGeneration(self):
    """Tests signurl output of a sample object version with JSON keystore."""
    bucket_uri = self.CreateBucket(versioning_enabled=True)
    object_uri = self.CreateObject(bucket_uri=bucket_uri, contents=b'z')
    cmd = ['signurl', self._GetJSONKsFile(), object_uri.version_specific_uri]
    stdout = self.RunGsUtil(cmd, return_stdout=True)
    self.assertIn('x-goog-credential=' + TEST_EMAIL, stdout)
    self.assertIn('generation=' + object_uri.generation, stdout)

  def testSignUrlWithURLEncodeRequiredChars(self):
    objs = [
        'gs://example.org/test 1', 'gs://example.org/test/test 2',
        'gs://example.org/Аудиоарi хив'
    ]
    expected_partial_urls = [
        'https://storage.googleapis.com/example.org/test%201?x-goog-signature=',
        ('https://storage.googleapis.com/example.org/test/test%202'
         '?x-goog-signature='),
        ('https://storage.googleapis.com/example.org/%D0%90%D1%83%D0%B4%D0%B8%D'
         '0%BE%D0%B0%D1%80i%20%D1%85%D0%B8%D0%B2?x-goog-signature=')
    ]

    self.assertEqual(len(objs), len(expected_partial_urls))

    cmd_args = [
        'signurl', '-m', 'PUT', '-r', 'us',
        self._GetJSONKsFile()
    ]
    cmd_args.extend(objs)

    stdout = self.RunGsUtil(cmd_args, return_stdout=True)

    lines = stdout.split('\n')
    # Header, signed urls, trailing newline.
    self.assertEqual(len(lines), len(objs) + 2)

    # Strip the header line to make the indices line up.
    lines = lines[1:]

    for obj, line, partial_url in zip(objs, lines, expected_partial_urls):
      self.assertIn(obj, line)
      self.assertIn(partial_url, line)
      self.assertIn('x-goog-credential='+TEST_EMAIL, line)
    self.assertIn('%2Fus%2F', stdout)

  def testSignUrlWithWildcard(self):
    objs = ['test1', 'test2', 'test3']
    obj_urls = []
    bucket = self.CreateBucket()

    for obj_name in objs:
      obj_urls.append(
          self.CreateObject(bucket_uri=bucket,
                            object_name=obj_name,
                            contents=b''))

    stdout = self.RunGsUtil(
        ['signurl',
         self._GetJSONKsFile(),
         suri(bucket) + '/*'],
        return_stdout=True)

    # Header, 3 signed urls, trailing newline
    self.assertEqual(len(stdout.split('\n')), 5)

    for obj_url in obj_urls:
      self.assertIn(suri(obj_url), stdout)

  @unittest.skipUnless(SERVICE_ACCOUNT,
                       'Test requires test_impersonate_service_account.')
  @SkipForS3('Tests only uses gs credentials.')
  @SkipForXML('Tests only run on JSON API.')
  def testSignUrlWithServiceAccount(self):
    with SetBotoConfigForTest([('Credentials', 'gs_impersonate_service_account',
                                SERVICE_ACCOUNT)]):
      stdout, stderr = self.RunGsUtil(
          ['signurl', '-r', 'us-east1', '-u', 'gs://pub'],
          return_stdout=True,
          return_stderr=True)
    # The signed url returned in stdout relies on current time.
    # We are not able to mock the datetime here because RunGsUtil creates
    # a separate process and runs the command.
    self.assertIn('https://storage.googleapis.com/pub', stdout)
    self.assertIn('All API calls will be executed as [%s]' % SERVICE_ACCOUNT,
                  stderr)

  def testSignUrlOfNonObjectUrl(self):
    """Tests the signurl output of a non-existent file."""
    self.RunGsUtil(['signurl', self._GetJSONKsFile(), 'gs://'],
                   expected_status=1)
    self.RunGsUtil(['signurl', 'file://tmp/abc', 'gs://bucket'],
                   expected_status=1,
                   return_stderr=True)
  
  def testSignUrlMutuallyExclusiveFlags(self):
    """Tests signurl with mutually exclusive -u and -b flags."""
    stderr = self.RunGsUtil(
        ['signurl', '-u', '-b', 'project', 'gs://bucket/obj'],
        return_stderr=True,
        expected_status=1)
    self.assertIn('Specifying both the -b and --use-service-account options',
                  stderr)

  def testSignUrlBucketMissingRegion(self):
    """Tests signurl for a bucket without specifying a region."""
    stderr = self.RunGsUtil(
        ['signurl', self._GetJSONKsFile(), 'gs://bucket'],
        return_stderr=True,
        expected_status=1)
    self.assertIn('Generating signed URLs for creating buckets requires a '
                  'region be specified via the -r option', stderr)

  def testSignUrlInvalidMethod(self):
    """Tests signurl with an unsupported HTTP method."""
    stderr = self.RunGsUtil(
        ['signurl', '-m', 'PATCH', self._GetJSONKsFile(), 'gs://bucket/obj'],
        return_stderr=True,
        expected_status=1)
    self.assertIn('HTTP method must be one of', stderr)

  def testSignUrlMissingUrlArgs(self):
    """Tests signurl with a key file but no URL arguments."""
    stderr = self.RunGsUtil(
        ['signurl', self._GetJSONKsFile()],
        return_stderr=True,
        expected_status=1)
    self.assertIn('The command requires a key file argument and one or more '
                  'URL arguments', stderr)


@unittest.skipUnless(HAVE_CRYPTO, 'signurl requires cryptography library.')
class UnitTestSignUrl(testcase.GsUtilUnitTestCase):
  """Unit tests for the signurl command."""

  # Helpful for comparing mismatched signed URLs that would be truncated.
  # https://stackoverflow.com/questions/14493670/how-to-set-self-maxdiff-in-nose-to-get-full-diff-output
  maxDiff = None

  def setUp(self):
    super(UnitTestSignUrl, self).setUp()
    ks_contents = pkgutil.get_data('gslib', 'tests/test_data/test.p12')
    self.key, self.client_email = gslib.commands.signurl._ReadKeystore(
        ks_contents, 'notasecret')

    def fake_now():
      return datetime(1900, 1, 1, 0, 5, 55)

    gslib.utils.signurl_helper._NowUTC = fake_now

  def _get_mock_api_delegator(self):
    mock_api_delegator = self.MakeGsUtilApi()

    # The MAkeGsUtilAPi maps apiclass.gs.JSON to BotoTranslation
    # instead of GcsJsonApi
    # Issue https://github.com/GoogleCloudPlatform/gsutil/issues/970
    # SignUrl relies on the GcsJsonApi so we are replacing the mapping here.
    mock_api_delegator.api_map['apiclass']['gs']['JSON'] = GcsJsonApi
    return mock_api_delegator

  def testDurationSpec(self):
    tests = [
        ('1h', timedelta(hours=1)),
        ('2d', timedelta(days=2)),
        ('5D', timedelta(days=5)),
        ('35s', timedelta(seconds=35)),
        ('1h', timedelta(hours=1)),
        ('33', timedelta(hours=33)),
        ('22m', timedelta(minutes=22)),
        ('3.7', None),
        ('27Z', None),
    ]

    for inp, expected in tests:
      try:
        td = gslib.commands.signurl._DurationToTimeDelta(inp)
        self.assertEqual(td, expected)
      except CommandException:
        if expected is not None:
          self.fail('{0} failed to parse')

  def testSignPutUsingKeyFile(self):
    """Tests the _GenSignedUrl function with a PUT method using Key file."""
    expected = sigs.TEST_SIGN_PUT_SIG

    duration = timedelta(seconds=3600)
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          self.key,
          api=None,
          use_service_account=False,
          provider='gs',
          client_id=self.client_email,
          method='RESUMABLE',
          gcs_path='test/test.txt',
          duration=duration,
          logger=self.logger,
          region='us-east',
          content_type='')
    self.assertEqual(expected, signed_url)

  @SkipForS3('Tests only uses gs credentials.')
  @SkipForXML('Tests only run on JSON API.')
  def testSignPutUsingServiceAccount(self):
    """Tests the _GenSignedUrl function PUT method with service account."""
    expected = sigs.TEST_SIGN_URL_PUT_WITH_SERVICE_ACCOUNT
    duration = timedelta(seconds=3600)

    mock_api_delegator = self._get_mock_api_delegator()
    json_api = mock_api_delegator._GetApi('gs')
    # patch a service account credentials
    mock_credentials = mock.Mock(spec=ServiceAccountCredentials)
    mock_credentials.service_account_email = 'fake_service_account_email'
    mock_credentials.sign_blob.return_value = ('fake_key', b'fake_signature')
    json_api.credentials = mock_credentials
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          None,
          api=mock_api_delegator,
          use_service_account=True,
          provider='gs',
          client_id=self.client_email,
          method='PUT',
          gcs_path='test/test.txt',
          duration=duration,
          logger=self.logger,
          region='us-east1',
          content_type='')
    self.assertEqual(expected, signed_url)
    mock_credentials.sign_blob.assert_called_once_with(
        b'GOOG4-RSA-SHA256\n19000101T000555Z\n19000101/us-east1/storage/'
        b'goog4_request\n7f110b30eeca7fdd8846e876bceee85384d8e4c7388b359'
        b'6544b1b503f9e2320')

  @SkipForS3('Tests only uses gs credentials.')
  @SkipForXML('Tests only run on JSON API.')
  def testSignUrlWithIncorrectAccountType(self):
    """Tests the _GenSignedUrl with incorrect account type.

    Test that GenSignedUrl function with 'use_service_account' set to True
    and a service account not used for credentials raises an error.
    """
    expected = sigs.TEST_SIGN_URL_PUT_WITH_SERVICE_ACCOUNT
    duration = timedelta(seconds=3600)

    mock_api_delegator = self._get_mock_api_delegator()
    json_api = mock_api_delegator._GetApi('gs')
    # patch a service account credentials
    mock_credentials = mock.Mock(spec=client.OAuth2Credentials)
    mock_credentials.service_account_email = 'fake_service_account_email'
    json_api.credentials = mock_credentials
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      self.assertRaises(CommandException,
                        gslib.commands.signurl._GenSignedUrl,
                        None,
                        api=mock_api_delegator,
                        use_service_account=True,
                        provider='gs',
                        client_id=self.client_email,
                        method='PUT',
                        gcs_path='test/test.txt',
                        duration=duration,
                        logger=self.logger,
                        region='us-east1',
                        content_type='')

  @SkipForS3('Tests only uses gs credentials.')
  @SkipForXML('Tests only run on JSON API.')
  @mock.patch('gslib.iamcredentials_api.apitools_client')
  @mock.patch('gslib.iamcredentials_api.apitools_messages')
  def testSignPutUsingImersonatedServiceAccount(self, mock_api_messages,
                                                mock_apiclient):
    """Tests the _GenSignedUrl function PUT method with impersonation.

    Test _GenSignedUrl function using an impersonated service account.
    """
    expected = sigs.TEST_SIGN_URL_PUT_WITH_SERVICE_ACCOUNT
    duration = timedelta(seconds=3600)

    mock_api_delegator = self._get_mock_api_delegator()
    json_api = mock_api_delegator._GetApi('gs')

    # A mock object of type ImpersonationCredentials.
    mock_credentials = mock.Mock(spec=ImpersonationCredentials)

    api_client_obj = mock.Mock()
    mock_apiclient.IamcredentialsV1.return_value = api_client_obj

    # The api_client.IamcredntialsV1 get's in IamCredentialsApi's init
    mock_iam_cred_api = IamcredentailsApi(credentials=mock.Mock())
    mock_credentials.api = mock_iam_cred_api
    mock_credentials.service_account_id = 'fake_service_account_email'

    # Mock the response and assign it as a  return value for the SignBlob func.
    mock_resp = mock.Mock()
    mock_resp.signedBlob = b'fake_signature'
    api_client_obj.projects_serviceAccounts.SignBlob.return_value = mock_resp

    json_api.credentials = mock_credentials

    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          None,
          api=mock_api_delegator,
          use_service_account=True,
          provider='gs',
          client_id=self.client_email,
          method='PUT',
          gcs_path='test/test.txt',
          duration=duration,
          logger=self.logger,
          region='us-east1',
          content_type='')
    self.assertEqual(expected, signed_url)
    mock_api_messages.SignBlobRequest.assert_called_once_with(
        payload=b'GOOG4-RSA-SHA256\n19000101T000555Z\n19000101/us-east1'
        b'/storage/goog4_request\n7f110b30eeca7fdd8846e876bceee'
        b'85384d8e4c7388b3596544b1b503f9e2320')

  def testSignResumableWithKeyFile(self):
    """Tests _GenSignedUrl using key file with a RESUMABLE method."""
    expected = sigs.TEST_SIGN_RESUMABLE

    class MockLogger(object):

      def __init__(self):
        self.warning_issued = False

      def warn(self, unused_msg):
        self.warning_issued = True

    mock_logger = MockLogger()
    duration = timedelta(seconds=3600)
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          self.key,
          api=None,
          use_service_account=False,
          provider='gs',
          client_id=self.client_email,
          method='RESUMABLE',
          gcs_path='test/test.txt',
          duration=duration,
          logger=mock_logger,
          region='us-east',
          content_type='')
    self.assertEqual(expected, signed_url)
    # Resumable uploads with no content-type should issue a warning.
    self.assertTrue(mock_logger.warning_issued)

    mock_logger2 = MockLogger()
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          self.key,
          api=None,
          use_service_account=False,
          provider='gs',
          client_id=self.client_email,
          method='RESUMABLE',
          gcs_path='test/test.txt',
          duration=duration,
          logger=mock_logger2,
          region='us-east',
          content_type='image/jpeg')
    # No warning, since content type was included.
    self.assertFalse(mock_logger2.warning_issued)

  def testSignurlPutContentypeUsingKeyFile(self):
    """Tests _GenSignedUrl using key file with a PUT method and content type."""
    expected = sigs.TEST_SIGN_URL_PUT_CONTENT

    duration = timedelta(seconds=3600)
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          self.key,
          api=None,
          use_service_account=False,
          provider='gs',
          client_id=self.client_email,
          method='PUT',
          gcs_path='test/test.txt',
          duration=duration,
          logger=self.logger,
          region='eu',
          content_type='text/plain')
    self.assertEqual(expected, signed_url)

  def testSignurlGetUsingKeyFile(self):
    """Tests the _GenSignedUrl function using key file with a GET method."""
    expected = sigs.TEST_SIGN_URL_GET

    duration = timedelta(seconds=0)
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          self.key,
          api=None,
          use_service_account=False,
          provider='gs',
          client_id=self.client_email,
          method='GET',
          gcs_path='test/test.txt',
          duration=duration,
          logger=self.logger,
          region='asia',
          content_type='')
    self.assertEqual(expected, signed_url)

  def testSignurlGetWithJSONKeyUsingKeyFile(self):
    """Tests _GenSignedUrl with a GET method and the test JSON private key."""
    expected = sigs.TEST_SIGN_URL_GET_WITH_JSON_KEY

    json_contents = pkgutil.get_data('gslib',
                                     'tests/test_data/test.json').decode()
    key, client_email = gslib.commands.signurl._ReadJSONKeystore(json_contents)

    duration = timedelta(seconds=0)
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          key,
          api=None,
          use_service_account=False,
          provider='gs',
          client_id=client_email,
          method='GET',
          gcs_path='test/test.txt',
          duration=duration,
          logger=self.logger,
          region='asia',
          content_type='')
    self.assertEqual(expected, signed_url)

  def testSignurlGetWithUserProject(self):
    """Tests the _GenSignedUrl function with a userproject."""
    expected = sigs.TEST_SIGN_URL_GET_USERPROJECT

    duration = timedelta(seconds=0)
    with SetBotoConfigForTest([('Credentials', 'gs_host',
                                'storage.googleapis.com')]):
      signed_url = gslib.commands.signurl._GenSignedUrl(
          self.key,
          api=None,
          use_service_account=False,
          provider='gs',
          client_id=self.client_email,
          method='GET',
          gcs_path='test/test.txt',
          duration=duration,
          logger=self.logger,
          region='asia',
          content_type='',
          billing_project='myproject')
    self.assertEqual(expected, signed_url)


@unittest.skipUnless(HAVE_CRYPTO, 'signurl requires cryptography library.')
class UnitTestSignUrlWithShim(testcase.ShimUnitTestBase):

  def testShimTranslatesFlags(self):
    key_contents = pkgutil.get_data('gslib', 'tests/test_data/test.json')
    key_path = self.CreateTempFile(contents=key_contents)

    with SetBotoConfigForTest([('GSUtil', 'use_gcloud_storage', 'True'),
                               ('GSUtil', 'hidden_shim_mode', 'dry_run')]):
      with SetEnvironmentForTest({
          'CLOUDSDK_CORE_PASS_CREDENTIALS_TO_GSUTIL': 'True',
          'CLOUDSDK_ROOT_DIR': 'fake_dir',
      }):
        mock_log_handler = self.RunCommand('signurl', [
            '-d', '2m', '-m', 'RESUMABLE', '-r', 'US', '-b', 'project', '-c',
            'application/octet-stream', key_path, 'gs://bucket/object'
        ],
                                           return_log_handler=True)
        info_lines = '\n'.join(mock_log_handler.messages['info'])
        self.assertIn(
            'storage sign-url'
            ' --format=csv[separator="\\t"](resource:label="URL", http_verb:label="HTTP Method", expiration:label="Expiration", signed_url:label="Signed URL")'
            ' --private-key-file={}'
            ' --headers=x-goog-resumable=start'
            ' --duration 120s'
            ' --http-verb POST'
            ' --region US'
            ' --query-params userProject=project'
            ' --headers content-type=application/octet-stream'
            ' gs://bucket/object'.format(key_path), info_lines)

  def testShimTranslatesFlagsWithP12Key(self):
    key_contents = pkgutil.get_data('gslib', 'tests/test_data/test.p12')
    key_path = self.CreateTempFile(contents=key_contents)
    key_password = 'notasecret'

    with SetBotoConfigForTest([('GSUtil', 'use_gcloud_storage', 'True'),
                               ('GSUtil', 'hidden_shim_mode', 'dry_run')]):
      with SetEnvironmentForTest({
          'CLOUDSDK_CORE_PASS_CREDENTIALS_TO_GSUTIL': 'True',
          'CLOUDSDK_ROOT_DIR': 'fake_dir',
      }):
        mock_log_handler = self.RunCommand('signurl', [
            '-d', '2m', '-m', 'RESUMABLE', '-p', key_password, '-r', 'US', '-b', 'project', '-c',
            'application/octet-stream', key_path, 'gs://bucket/object'
        ],
                                           return_log_handler=True)
        info_lines = '\n'.join(mock_log_handler.messages['info'])
        self.assertIn(
            'storage sign-url'
            ' --format=csv[separator="\\t"](resource:label="URL", http_verb:label="HTTP Method", expiration:label="Expiration", signed_url:label="Signed URL")'
            ' --private-key-file={}'
            ' --headers=x-goog-resumable=start'
            ' --duration 120s'
            ' --http-verb POST'
            ' --private-key-password {}'
            ' --region US'
            ' --query-params userProject=project'
            ' --headers content-type=application/octet-stream'
            ' gs://bucket/object'.format(key_path, key_password), info_lines)

class TestSignUrlHelper(testcase.GsUtilUnitTestCase):
  """Unit tests for signurl_helper functions."""

  def testToBytes(self):
    from gslib.utils.signurl_helper import to_bytes
    self.assertEqual(to_bytes('test'), b'test')
    self.assertEqual(to_bytes(b'test'), b'test')
    self.assertIsNone(to_bytes(None))
    self.assertEqual(to_bytes('Аудио'), 'Аудио'.encode('utf-8'))

  def testGetFinalUrl(self):
    from gslib.utils.signurl_helper import GetFinalUrl
    raw_sig = b'\x01\x02\x03'
    # b16 of \x01\x02\x03 is 010203
    url = GetFinalUrl(raw_sig, 'host', 'bucket/obj', 'query=val')
    self.assertEqual(
        url,
        'https://host/bucket/obj?x-goog-signature=010203&query=val')


@unittest.skipUnless(HAVE_CRYPTO, 'signurl requires cryptography library.')
class TestSignUrlKeyReaders(testcase.GsUtilUnitTestCase):
  """Unit tests for keystore reader functions in signurl command."""

  def testReadJSONKeystoreValid(self):
    json_contents = pkgutil.get_data('gslib',
                                     'tests/test_data/test.json').decode()
    key, email = gslib.commands.signurl._ReadJSONKeystore(json_contents)
    self.assertIsNotNone(key)
    self.assertEqual(email, 'test@developer.gserviceaccount.com')

  def testReadJSONKeystoreMissingFields(self):
    invalid_json = '{"foo": "bar"}'
    with self.assertRaisesRegex(ValueError, 'JSON keystore doesn\'t contain required fields'):
      gslib.commands.signurl._ReadJSONKeystore(invalid_json)

  def testReadKeystoreInvalidData(self):
    with self.assertRaisesRegex(CommandException, 'Unable to load the keyfile'):
      gslib.commands.signurl._ReadKeystore(b'not a p12', 'password')

  def testReadKeystoreRobustness(self):
    # Test with empty data
    with self.assertRaises(CommandException):
      gslib.commands.signurl._ReadKeystore(b'', 'password')
