# Copyright (c) 2021 Dell Inc. or its subsidiaries.
#
# 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
from unittest import mock

from oslo_config import cfg
import requests

from ironic.common import exception
from ironic.common import keystone
from ironic.common import molds
from ironic.common import swift
from ironic.conductor import task_manager
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils


class ConfigurationMoldTestCase(db_base.DbTestCase):

    def setUp(self):
        super(ConfigurationMoldTestCase, self).setUp()
        self.node = obj_utils.create_test_node(self.context)

    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
    @mock.patch.object(swift, 'get_swift_session', autospec=True)
    @mock.patch.object(requests, 'put', autospec=True)
    def test_save_configuration_swift(self, mock_put, mock_swift,
                                      mock_endpoint):
        mock_session = mock.Mock()
        mock_session.get_token.return_value = 'token'
        mock_swift.return_value = mock_session
        cfg.CONF.set_override('storage', 'swift', 'molds')
        url = 'https://example.com/file1'
        data = {'key': 'value'}
        mock_endpoint.return_value = 'https://example.com/v1'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            molds.save_configuration(task, url, data)

        mock_put.assert_called_once_with(url, '{\n  "key": "value"\n}',
                                         headers={'X-Auth-Token': 'token'},
                                         timeout=60)

    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
    @mock.patch.object(swift, 'get_swift_session', autospec=True)
    @mock.patch.object(requests, 'put', autospec=True)
    def test_save_configuration_swift_noauth(self, mock_put, mock_swift,
                                             mock_endpoint):
        mock_session = mock.Mock()
        mock_session.get_token.return_value = None
        mock_swift.return_value = mock_session
        cfg.CONF.set_override('storage', 'swift', 'molds')
        url = 'https://example.com/file1'
        data = {'key': 'value'}
        mock_endpoint.return_value = 'https://example.com/v1'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(
                exception.IronicException,
                molds.save_configuration,
                task, url, data)

    @mock.patch.object(requests, 'put', autospec=True)
    def test_save_configuration_http(self, mock_put):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        url = 'https://example.com/file1'
        data = {'key': 'value'}

        with task_manager.acquire(self.context, self.node.uuid) as task:
            molds.save_configuration(task, url, data)

            mock_put.assert_called_once_with(
                url, '{\n  "key": "value"\n}',
                headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)

    @mock.patch.object(requests, 'put', autospec=True)
    def test_save_configuration_http_noauth(self, mock_put):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', None, 'molds')
        cfg.CONF.set_override('password', None, 'molds')
        url = 'https://example.com/file1'
        data = {'key': 'value'}

        with task_manager.acquire(self.context, self.node.uuid) as task:
            molds.save_configuration(task, url, data)
            mock_put.assert_called_once_with(
                url, '{\n  "key": "value"\n}',
                headers=None,
                timeout=60)

    @mock.patch.object(requests, 'put', autospec=True)
    def test_save_configuration_http_error(self, mock_put):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        response = mock.MagicMock()
        response.status_code = 404
        response.raise_for_status.side_effect = requests.exceptions.HTTPError
        mock_put.return_value = response

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(
                requests.exceptions.HTTPError,
                molds.save_configuration,
                task,
                'https://example.com/file2',
                {'key': 'value'})
            mock_put.assert_called_once_with(
                'https://example.com/file2', '{\n  "key": "value"\n}',
                headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)

    @mock.patch.object(requests, 'put', autospec=True)
    def test_save_configuration_connection_error(self, mock_put):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        cfg.CONF.set_override('retry_interval', 0, 'molds')
        cfg.CONF.set_override('retry_attempts', 3, 'molds')
        response = mock.MagicMock()
        mock_put.side_effect = [
            requests.exceptions.ConnectTimeout,
            requests.exceptions.ConnectionError,
            response]

        with task_manager.acquire(self.context, self.node.uuid) as task:
            molds.save_configuration(
                task, 'https://example.com/file2', {'key': 'value'})
            mock_put.assert_called_with(
                'https://example.com/file2', '{\n  "key": "value"\n}',
                headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)
            self.assertEqual(mock_put.call_count, 3)

    @mock.patch.object(requests, 'put', autospec=True)
    def test_save_configuration_connection_error_exceeded(self, mock_put):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        cfg.CONF.set_override('retry_interval', 0, 'molds')
        cfg.CONF.set_override('retry_attempts', 2, 'molds')
        mock_put.side_effect = [
            requests.exceptions.ConnectTimeout,
            requests.exceptions.ConnectionError]

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(
                requests.exceptions.ConnectionError,
                molds.save_configuration,
                task,
                'https://example.com/file2',
                {'key': 'value'})
            mock_put.assert_called_with(
                'https://example.com/file2', '{\n  "key": "value"\n}',
                headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)
            self.assertEqual(mock_put.call_count, 2)

    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
    @mock.patch.object(swift, 'get_swift_session', autospec=True)
    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_swift(self, mock_get, mock_swift,
                                     mock_endpoint):
        mock_session = mock.Mock()
        mock_session.get_token.return_value = 'token'
        mock_swift.return_value = mock_session
        cfg.CONF.set_override('storage', 'swift', 'molds')
        response = mock.MagicMock()
        response.status_code = 200
        response.content = "{'key': 'value'}"
        response.json.return_value = {'key': 'value'}
        mock_get.return_value = response
        url = 'https://example.com/file1'
        mock_endpoint.return_value = 'https://example.com/v1'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            result = molds.get_configuration(task, url)

            mock_get.assert_called_once_with(
                url, headers={'X-Auth-Token': 'token'},
                timeout=60)
            self.assertJsonEqual({'key': 'value'}, result)
        mock_endpoint.assert_called_once_with('swift', session=mock_session)

    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
    @mock.patch.object(swift, 'get_swift_session', autospec=True)
    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_swift_url_mismatch(
            self, mock_get, mock_swift, mock_endpoint):
        mock_session = mock.Mock()
        mock_session.get_token.return_value = 'token'
        mock_swift.return_value = mock_session
        cfg.CONF.set_override('storage', 'swift', 'molds')
        response = mock.MagicMock()
        response.status_code = 200
        response.content = "{'key': 'value'}"
        response.json.return_value = {'key': 'value'}
        mock_get.return_value = response
        url = 'https://example.com/file1'
        mock_endpoint.return_value = 'https://cloud.foo/v1'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(exception.InvalidParameterValue,
                              molds.get_configuration,
                              task, url)

        mock_endpoint.assert_called_once_with('swift', session=mock_session)
        mock_get.assert_not_called()

    @mock.patch.object(keystone, 'get_endpoint', autospec=True)
    @mock.patch.object(swift, 'get_swift_session', autospec=True)
    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_swift_noauth(self, mock_get, mock_swift,
                                            mock_endpoint):
        mock_session = mock.Mock()
        mock_session.get_token.return_value = None
        mock_swift.return_value = mock_session
        cfg.CONF.set_override('storage', 'swift', 'molds')
        url = 'https://example.com/file1'
        mock_endpoint.return_value = 'https://example.com/v1'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(
                exception.IronicException,
                molds.get_configuration,
                task, url)

    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_http(self, mock_get):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        response = mock.MagicMock()
        response.status_code = 200
        response.content = "{'key': 'value'}"
        response.json.return_value = {'key': 'value'}
        mock_get.return_value = response
        url = 'https://example.com/file2'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            result = molds.get_configuration(task, url)

            mock_get.assert_called_once_with(
                url, headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)
            self.assertJsonEqual({"key": "value"}, result)

    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_http_noauth(self, mock_get):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', None, 'molds')
        cfg.CONF.set_override('password', None, 'molds')
        response = mock.MagicMock()
        response.status_code = 200
        response.content = "{'key': 'value'}"
        response.json.return_value = {'key': 'value'}
        mock_get.return_value = response
        url = 'https://example.com/file2'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            result = molds.get_configuration(task, url)

            mock_get.assert_called_once_with(url, headers=None, timeout=60)
            self.assertJsonEqual({"key": "value"}, result)

    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_http_error(self, mock_get):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        response = mock.MagicMock()
        response.status_code = 404
        response.raise_for_status.side_effect = requests.exceptions.HTTPError
        mock_get.return_value = response

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(
                requests.exceptions.HTTPError,
                molds.get_configuration,
                task,
                'https://example.com/file2')
            mock_get.assert_called_once_with(
                'https://example.com/file2',
                headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)

    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_connection_error(self, mock_get):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        cfg.CONF.set_override('retry_interval', 0, 'molds')
        cfg.CONF.set_override('retry_attempts', 3, 'molds')
        response = mock.MagicMock()
        mock_get.side_effect = [
            requests.exceptions.ConnectTimeout,
            requests.exceptions.ConnectionError,
            response]

        with task_manager.acquire(self.context, self.node.uuid) as task:
            molds.get_configuration(
                task, 'https://example.com/file2')
            mock_get.assert_called_with(
                'https://example.com/file2',
                headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)
            self.assertEqual(mock_get.call_count, 3)

    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_mold_connection_error_exceeded(self, mock_get):
        cfg.CONF.set_override('storage', 'http', 'molds')
        cfg.CONF.set_override('user', 'user', 'molds')
        cfg.CONF.set_override('password', 'password', 'molds')
        cfg.CONF.set_override('retry_interval', 0, 'molds')
        cfg.CONF.set_override('retry_attempts', 2, 'molds')
        mock_get.side_effect = [
            requests.exceptions.ConnectTimeout,
            requests.exceptions.ConnectionError]

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(
                requests.exceptions.ConnectionError,
                molds.get_configuration,
                task,
                'https://example.com/file2')
            mock_get.assert_called_with(
                'https://example.com/file2',
                headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='},
                timeout=60)
            self.assertEqual(mock_get.call_count, 2)

    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_empty(self, mock_get):
        cfg.CONF.set_override('storage', 'http', 'molds')
        response = mock.MagicMock()
        response.status_code = 200
        response.content = ''
        mock_get.return_value = response
        url = 'https://example.com/file2'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(exception.IronicException,
                              molds.get_configuration, task, url)

    @mock.patch.object(requests, 'get', autospec=True)
    def test_get_configuration_invalid_json(self, mock_get):
        cfg.CONF.set_override('storage', 'http', 'molds')
        response = mock.MagicMock()
        response.status_code = 200
        response.content = 'not json'
        response.json.side_effect = json.decoder.JSONDecodeError(
            'Expecting value', 'not json', 0)
        mock_get.return_value = response
        url = 'https://example.com/file2'

        with task_manager.acquire(self.context, self.node.uuid) as task:
            self.assertRaises(exception.IronicException,
                              molds.get_configuration, task, url)
