# 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 json
import logging
from unittest import mock

import ddt
import fixtures
from keystoneauth1 import adapter
from keystoneauth1 import exceptions as keystone_exception
from oslo_serialization import jsonutils

from cinderclient import api_versions
import cinderclient.client
from cinderclient import exceptions
from cinderclient.tests.unit import utils
from cinderclient.tests.unit.v3 import fakes


@ddt.ddt
class ClientTest(utils.TestCase):

    def test_get_client_class_v2(self):
        self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
                          cinderclient.client.get_client_class,
                          '2')

    def test_get_client_class_unknown(self):
        self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
                          cinderclient.client.get_client_class, '0')

    @mock.patch.object(cinderclient.client.HTTPClient, '__init__')
    @mock.patch('cinderclient.client.SessionClient')
    def test_construct_http_client_endpoint_url(
            self, session_mock, httpclient_mock):
        os_endpoint = 'http://example.com/'
        httpclient_mock.return_value = None
        cinderclient.client._construct_http_client(
            os_endpoint=os_endpoint)
        self.assertTrue(httpclient_mock.called)
        self.assertEqual(os_endpoint,
                         httpclient_mock.call_args[1].get('os_endpoint'))
        session_mock.assert_not_called()

    def test_log_req(self):
        self.logger = self.useFixture(
            fixtures.FakeLogger(
                format="%(message)s",
                level=logging.DEBUG,
                nuke_handlers=True
            )
        )

        kwargs = {
            'headers': {"X-Foo": "bar"},
            'data': ('{"auth": {"tenantName": "fakeService",'
                     ' "passwordCredentials": {"username": "fakeUser",'
                     ' "password": "fakePassword"}}}')
        }

        cs = cinderclient.client.HTTPClient("user", None, None,
                                            "http://127.0.0.1:5000")
        cs.http_log_debug = True
        cs.http_log_req('PUT', kwargs)

        output = self.logger.output.split('\n')

        self.assertNotIn("fakePassword", output[1])
        self.assertIn("fakeUser", output[1])

    def test_versions(self):
        v2_url = 'http://fakeurl/v2/tenants'
        v3_url = 'http://fakeurl/v3/tenants'
        unknown_url = 'http://fakeurl/v9/tenants'

        self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
                          cinderclient.client.get_volume_api_from_url,
                          v2_url)
        self.assertEqual('3',
                         cinderclient.client.get_volume_api_from_url(v3_url))
        self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
                          cinderclient.client.get_volume_api_from_url,
                          unknown_url)

    @mock.patch('cinderclient.client.SessionClient.get_endpoint')
    @ddt.data(
        ('http://192.168.1.1:8776/v2', 'http://192.168.1.1:8776/'),
        ('http://192.168.1.1:8776/v3/e5526285ebd741b1819393f772f11fc3',
         'http://192.168.1.1:8776/'),
        ('https://192.168.1.1:8080/volumes/v3/'
         'e5526285ebd741b1819393f772f11fc3',
         'https://192.168.1.1:8080/volumes/'),
        ('http://192.168.1.1/volumes/v3/e5526285ebd741b1819393f772f11fc3',
         'http://192.168.1.1/volumes/'),
        ('https://volume.example.com/', 'https://volume.example.com/'))
    @ddt.unpack
    def test_get_base_url(self, url, expected_base, mock_get_endpoint):
        mock_get_endpoint.return_value = url
        cs = cinderclient.client.SessionClient(self, api_version='3.0')
        self.assertEqual(expected_base, cs._get_base_url())

    @mock.patch.object(adapter.LegacyJsonAdapter, '_request')
    @mock.patch.object(exceptions, 'from_response')
    def test_sessionclient_request_method(
            self, mock_from_resp, mock_request):
        kwargs = {
            "body": {
                "volume": {
                    "status": "creating",
                    "imageRef": "username",
                    "attach_status": "detached"
                },
                "authenticated": "True"
            }
        }

        resp = {
            "text": {
                "volume": {
                    "status": "creating",
                    "id": "431253c0-e203-4da2-88df-60c756942aaf",
                    "size": 1
                }
            },
            "code": 202
        }

        request_id = "req-f551871a-4950-4225-9b2c-29a14c8f075e"
        mock_response = utils.TestResponse({
            "status_code": 202,
            "text": json.dumps(resp).encode("latin-1"),
            "headers": {"x-openstack-request-id": request_id},
        })

        # 'request' method of Adaptor will return 202 response
        mock_request.return_value = mock_response
        session_client = cinderclient.client.SessionClient(session=mock.Mock())
        response, body = session_client.request(mock.sentinel.url,
                                                'POST', **kwargs)
        self.assertIsNotNone(session_client._logger)

        # In this case, from_response method will not get called
        # because response status_code is < 400
        self.assertEqual(202, response.status_code)
        self.assertFalse(mock_from_resp.called)

    @mock.patch.object(adapter.LegacyJsonAdapter, '_request')
    def test_sessionclient_request_method_raises_badrequest(
            self, mock_request):
        kwargs = {
            "body": {
                "volume": {
                    "status": "creating",
                    "imageRef": "username",
                    "attach_status": "detached"
                },
                "authenticated": "True"
            }
        }

        resp = {
            "badRequest": {
                "message": "Invalid image identifier or unable to access "
                           "requested image.",
                "code": 400
            }
        }

        mock_response = utils.TestResponse({
            "status_code": 400,
            "text": json.dumps(resp).encode("latin-1"),
        })

        # 'request' method of Adaptor will return 400 response
        mock_request.return_value = mock_response
        session_client = cinderclient.client.SessionClient(
            session=mock.Mock())

        # 'from_response' method will raise BadRequest because
        # resp.status_code is 400
        self.assertRaises(exceptions.BadRequest, session_client.request,
                          mock.sentinel.url, 'POST', **kwargs)
        self.assertIsNotNone(session_client._logger)

    @mock.patch.object(adapter.LegacyJsonAdapter, '_request')
    def test_sessionclient_request_method_raises_overlimit(
            self, mock_request):
        resp = {
            "overLimitFault": {
                "message": "This request was rate-limited.",
                "code": 413
            }
        }

        mock_response = utils.TestResponse({
            "status_code": 413,
            "text": json.dumps(resp).encode("latin-1"),
        })

        # 'request' method of Adaptor will return 413 response
        mock_request.return_value = mock_response
        session_client = cinderclient.client.SessionClient(
            session=mock.Mock())

        self.assertRaises(exceptions.OverLimit, session_client.request,
                          mock.sentinel.url, 'GET')
        self.assertIsNotNone(session_client._logger)

    @mock.patch.object(exceptions, 'from_response')
    def test_keystone_request_raises_auth_failure_exception(
            self, mock_from_resp):

        kwargs = {
            "body": {
                "volume": {
                    "status": "creating",
                    "imageRef": "username",
                    "attach_status": "detached"
                },
                "authenticated": "True"
            }
        }

        with mock.patch.object(adapter.LegacyJsonAdapter, '_request',
                               side_effect=
                               keystone_exception.AuthorizationFailure()):
            session_client = cinderclient.client.SessionClient(
                session=mock.Mock())
            self.assertRaises(keystone_exception.AuthorizationFailure,
                              session_client.request,
                              mock.sentinel.url, 'POST', **kwargs)

        # As keystonesession.request method will raise
        # AuthorizationFailure exception, check exceptions.from_response
        # is not getting called.
        self.assertFalse(mock_from_resp.called)

    @mock.patch('keystoneauth1.adapter.LegacyJsonAdapter.request',
                return_value=(mock.Mock(), mock.Mock()))
    @ddt.data(True, False, None)
    def test_http_log_debug_request(self, http_log_debug, mock_request):
        args_req = (mock.sentinel.url, mock.sentinel.OP)
        kwargs_req = {'raise_exc': False}
        kwargs_expect = {'authenticated': False}
        kwargs_expect.update(kwargs_req)

        kwargs = {'api_version': '3.0'}
        if isinstance(http_log_debug, bool):
            kwargs['http_log_debug'] = http_log_debug
            if http_log_debug:
                kwargs_expect['logger'] = mock.ANY

        cs = cinderclient.client.SessionClient(self, **kwargs)

        res = cs.request(*args_req, **kwargs_req)

        mock_request.assert_called_once_with(*args_req, **kwargs_expect)
        self.assertEqual(mock_request.return_value, res)


class ClientTestSensitiveInfo(utils.TestCase):
    def test_req_does_not_log_sensitive_info(self):
        self.logger = self.useFixture(
            fixtures.FakeLogger(
                format="%(message)s",
                level=logging.DEBUG,
                nuke_handlers=True
            )
        )

        secret_auth_token = "MY_SECRET_AUTH_TOKEN"
        kwargs = {
            'headers': {"X-Auth-Token": secret_auth_token},
            'data': ('{"auth": {"tenantName": "fakeService",'
                     ' "passwordCredentials": {"username": "fakeUser",'
                     ' "password": "fakePassword"}}}')
        }

        cs = cinderclient.client.HTTPClient("user", None, None,
                                            "http://127.0.0.1:5000")
        cs.http_log_debug = True
        cs.http_log_req('PUT', kwargs)

        output = self.logger.output.split('\n')
        self.assertNotIn(secret_auth_token, output[1])

    def test_resp_does_not_log_sensitive_info(self):
        self.logger = self.useFixture(
            fixtures.FakeLogger(
                format="%(message)s",
                level=logging.DEBUG,
                nuke_handlers=True
            )
        )
        cs = cinderclient.client.HTTPClient("user", None, None,
                                            "http://127.0.0.1:5000")
        resp = mock.Mock()
        resp.status_code = 200
        resp.headers = {
            'x-compute-request-id': 'req-f551871a-4950-4225-9b2c-29a14c8f075e'
        }
        auth_password = "kk4qD6CpKFLyz9JD"
        body = {
            "connection_info": {
                "driver_volume_type": "iscsi",
                "data": {
                    "auth_password": auth_password,
                    "target_discovered": False,
                    "encrypted": False,
                    "qos_specs": None,
                    "target_iqn": ("iqn.2010-10.org.openstack:volume-"
                                   "a2f33dcc-1bb7-45ba-b8fc-5b38179120f8"),
                    "target_portal": "10.0.100.186:3260",
                    "volume_id": "a2f33dcc-1bb7-45ba-b8fc-5b38179120f8",
                    "target_lun": 1,
                    "access_mode": "rw",
                    "auth_username": "s4BfSfZ67Bo2mnpuFWY8",
                    "auth_method": "CHAP"
                }
            }
        }
        resp.text = jsonutils.dumps(body)
        cs.http_log_debug = True
        cs.http_log_resp(resp)

        output = self.logger.output.split('\n')
        self.assertIn('***', output[1], output)
        self.assertNotIn(auth_password, output[1], output)


@ddt.ddt
class GetAPIVersionTestCase(utils.TestCase):

    @mock.patch('cinderclient.client.requests.get')
    def test_get_server_version_v2(self, mock_request):
        # Why are we testing this? Because we can!

        mock_response = utils.TestResponse({
            "status_code": 200,
            "text": json.dumps(fakes.fake_request_get_no_v3())
        })

        mock_request.return_value = mock_response

        url = "http://192.168.122.127:8776/v2/e5526285ebd741b1819393f772f11fc3"

        min_version, max_version = cinderclient.client.get_server_version(url)

        self.assertEqual(api_versions.APIVersion('2.0'), min_version)
        self.assertEqual(api_versions.APIVersion('2.0'), max_version)

    @mock.patch('cinderclient.client.requests.get')
    @ddt.data(
        'http://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3',
        'https://192.168.122.127:8776/v3/e55285ebd741b1819393f772f11fc3',
        'http://192.168.122.127/volumesv3/e5526285ebd741b1819393f772f11fc3'
        )
    def test_get_server_version(self, url, mock_request):
        mock_response = utils.TestResponse({
            "status_code": 200,
            "text": json.dumps(fakes.fake_request_get())
        })

        mock_request.return_value = mock_response

        min_version, max_version = cinderclient.client.get_server_version(url)
        self.assertEqual(min_version, api_versions.APIVersion('3.0'))
        self.assertEqual(max_version, api_versions.APIVersion('3.16'))

    @mock.patch('cinderclient.client.requests.get')
    def test_get_server_version_insecure(self, mock_request):
        mock_response = utils.TestResponse({
            "status_code": 200,
            "text": json.dumps(fakes.fake_request_get_no_v3())
        })

        mock_request.return_value = mock_response

        url = (
            "https://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3")
        expected_url = "https://192.168.122.127:8776/"

        cinderclient.client.get_server_version(url, True)

        mock_request.assert_called_once_with(expected_url,
                                             verify=False,
                                             cert=None)

    @mock.patch('cinderclient.client.requests.get')
    def test_get_server_version_cacert(self, mock_request):
        mock_response = utils.TestResponse({
            "status_code": 200,
            "text": json.dumps(fakes.fake_request_get_no_v3())
        })

        mock_request.return_value = mock_response

        url = (
            "https://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3")
        expected_url = "https://192.168.122.127:8776/"

        cacert = '/path/to/cert'
        cinderclient.client.get_server_version(url, cacert=cacert)

        mock_request.assert_called_once_with(expected_url,
                                             verify=cacert,
                                             cert=None)

    @mock.patch('cinderclient.client.requests.get')
    def test_get_server_version_cert(self, mock_request):
        mock_response = utils.TestResponse({
            "status_code": 200,
            "text": json.dumps(fakes.fake_request_get_no_v3())
        })

        mock_request.return_value = mock_response

        url = (
            "https://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3")
        expected_url = "https://192.168.122.127:8776/"

        client_cert = '/path/to/cert'
        cinderclient.client.get_server_version(url, cert=client_cert)

        mock_request.assert_called_once_with(expected_url,
                                             verify=True,
                                             cert=client_cert)

    @mock.patch('cinderclient.client.requests.get')
    @ddt.data('3.12', '3.40')
    def test_get_highest_client_server_version(self, version, mock_request):

        mock_response = utils.TestResponse({
            "status_code": 200,
            "text": json.dumps(fakes.fake_request_get())
        })

        mock_request.return_value = mock_response

        url = "http://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3"

        with mock.patch.object(api_versions, 'MAX_VERSION', version):
            highest = (
                cinderclient.client.get_highest_client_server_version(url))
        expected = version if version == '3.12' else '3.16'
        self.assertEqual(expected, highest)

    @mock.patch('cinderclient.client.requests.get')
    def test_get_highest_client_server_version_negative(self,
                                                        mock_request):

        mock_response = utils.TestResponse({
            "status_code": 200,
            "text": json.dumps(fakes.fake_request_get_no_v3())
        })

        mock_request.return_value = mock_response

        url = "http://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3"

        self.assertRaises(exceptions.UnsupportedVersion,
                          cinderclient.client.
                          get_highest_client_server_version,
                          url)
