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

from testtools import matchers

from keystoneauth1 import adapter
from keystoneauth1 import discover
from keystoneauth1 import exceptions
from keystoneauth1 import fixture
from keystoneauth1 import http_basic
from keystoneauth1 import noauth
from keystoneauth1 import session
from keystoneauth1.tests.unit import utils
from keystoneauth1 import token_endpoint


BASE_HOST = 'http://keystone.example.com'
BASE_URL = f"{BASE_HOST}:5000/"
UPDATED = '2013-03-06T00:00:00Z'

TEST_SERVICE_CATALOG = [
    {
        "endpoints": [
            {
                "adminURL": f"{BASE_HOST}:8774/v1.0",
                "region": "RegionOne",
                "internalURL": f"{BASE_HOST}://127.0.0.1:8774/v1.0",
                "publicURL": f"{BASE_HOST}:8774/v1.0/",
            }
        ],
        "type": "nova_compat",
        "name": "nova_compat",
    },
    {
        "endpoints": [
            {
                "adminURL": "http://nova/novapi/admin",
                "region": "RegionOne",
                "internalURL": "http://nova/novapi/internal",
                "publicURL": "http://nova/novapi/public",
            }
        ],
        "type": "compute",
        "name": "nova",
    },
    {
        "endpoints": [
            {
                "adminURL": "http://glance/glanceapi/admin",
                "region": "RegionOne",
                "internalURL": "http://glance/glanceapi/internal",
                "publicURL": "http://glance/glanceapi/public",
            }
        ],
        "type": "image",
        "name": "glance",
    },
    {
        "endpoints": [
            {
                "adminURL": f"{BASE_HOST}:35357/v2.0",
                "region": "RegionOne",
                "internalURL": f"{BASE_HOST}:5000/v2.0",
                "publicURL": f"{BASE_HOST}:5000/v2.0",
            }
        ],
        "type": "identity",
        "name": "keystone",
    },
    {
        "endpoints": [
            {
                "adminURL": "http://swift/swiftapi/admin",
                "region": "RegionOne",
                "internalURL": "http://swift/swiftapi/internal",
                "publicURL": "http://swift/swiftapi/public",
            }
        ],
        "type": "object-store",
        "name": "swift",
    },
]

V2_URL = f"{BASE_URL}v2.0"
V2_VERSION = fixture.V2Discovery(V2_URL)
V2_VERSION.updated_str = UPDATED

V2_AUTH_RESPONSE = json.dumps(
    {
        "access": {
            "token": {
                "expires": "2020-01-01T00:00:10.000123Z",
                "id": 'fakeToken',
                "tenant": {"id": '1'},
            },
            "user": {"id": 'test'},
            "serviceCatalog": TEST_SERVICE_CATALOG,
        }
    }
)

V3_URL = f"{BASE_URL}v3"
V3_VERSION = fixture.V3Discovery(V3_URL)
V3_MEDIA_TYPES = V3_VERSION.media_types
V3_VERSION.updated_str = UPDATED

V3_AUTH_RESPONSE = json.dumps(
    {
        "token": {
            "methods": ["token", "password"],
            "expires_at": "2020-01-01T00:00:10.000123Z",
            "project": {
                "domain": {"id": '1', "name": 'test-domain'},
                "id": '1',
                "name": 'test-project',
            },
            "user": {
                "domain": {"id": '1', "name": 'test-domain'},
                "id": '1',
                "name": 'test-user',
            },
            "issued_at": "2013-05-29T16:55:21.468960Z",
        }
    }
)

CINDER_EXAMPLES = {
    "versions": [
        {
            "status": discover.Status.CURRENT,
            "updated": "2012-01-04T11:33:21Z",
            "id": "v1.0",
            "links": [{"href": f"{BASE_URL}v1/", "rel": "self"}],
        },
        {
            "status": discover.Status.CURRENT,
            "updated": "2012-11-21T11:33:21Z",
            "id": "v2.0",
            "links": [{"href": f"{BASE_URL}v2/", "rel": "self"}],
        },
        {
            "status": discover.Status.CURRENT,
            "updated": "2012-11-21T11:33:21Z",
            "id": "v3.0",
            "version": "3.27",
            "min_version": "3.0",
            "next_min_version": "3.4",
            "not_before": "2019-12-31",
            "links": [
                {"href": BASE_URL, "rel": "collection"},
                {"href": f"{BASE_URL}v3/", "rel": "self"},
            ],
        },
    ]
}

GLANCE_EXAMPLES = {
    "versions": [
        {
            "status": discover.Status.CURRENT,
            "id": "v2.2",
            "links": [{"href": f"{BASE_URL}v2/", "rel": "self"}],
        },
        {
            "status": discover.Status.SUPPORTED,
            "id": "v2.1",
            "links": [{"href": f"{BASE_URL}v2/", "rel": "self"}],
        },
        {
            "status": discover.Status.SUPPORTED,
            "id": "v2.0",
            "links": [{"href": f"{BASE_URL}v2/", "rel": "self"}],
        },
        {
            "status": discover.Status.CURRENT,
            "id": "v1.1",
            "links": [{"href": f"{BASE_URL}v1/", "rel": "self"}],
        },
        {
            "status": discover.Status.SUPPORTED,
            "id": "v1.0",
            "links": [{"href": f"{BASE_URL}v1/", "rel": "self"}],
        },
    ]
}


def _create_version_list(versions):
    return {'versions': {'values': versions}}


def _create_single_version(version):
    return {'version': version}


V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION])
V2_VERSION_LIST = _create_version_list([V2_VERSION])

V3_VERSION_ENTRY = _create_single_version(V3_VERSION)
V2_VERSION_ENTRY = _create_single_version(V2_VERSION)


class CatalogHackTests(utils.TestCase):
    TEST_URL = 'http://keystone.server:5000/v2.0'
    OTHER_URL = 'http://other.server:5000/path'

    IDENTITY = 'identity'

    BASE_URL = 'http://keystone.server:5000/'
    V2_URL = BASE_URL + 'v2.0'
    V3_URL = BASE_URL + 'v3'

    def setUp(self):
        super().setUp()
        self.hacks = discover._VersionHacks()
        self.hacks.add_discover_hack(
            self.IDENTITY, re.compile('/v2.0/?$'), '/'
        )

    def test_version_hacks(self):
        self.assertEqual(
            self.BASE_URL,
            self.hacks.get_discover_hack(self.IDENTITY, self.V2_URL),
        )

        self.assertEqual(
            self.BASE_URL,
            self.hacks.get_discover_hack(self.IDENTITY, self.V2_URL + '/'),
        )

        self.assertEqual(
            self.OTHER_URL,
            self.hacks.get_discover_hack(self.IDENTITY, self.OTHER_URL),
        )

    def test_ignored_non_service_type(self):
        self.assertEqual(
            self.V2_URL, self.hacks.get_discover_hack('other', self.V2_URL)
        )


class DiscoverUtils(utils.TestCase):
    def test_version_number(self):
        def assertVersion(out, inp):
            self.assertEqual(out, discover.normalize_version_number(inp))

        def versionRaises(inp):
            self.assertRaises(
                TypeError, discover.normalize_version_number, inp
            )

        assertVersion((1, 2), 'v1.2')
        assertVersion((11, 0), 'v11')
        assertVersion((1, 2), '1.2')
        assertVersion((1, 5, 1), '1.5.1')
        assertVersion((1, 0), '1')
        assertVersion((1, 0), 1)
        assertVersion((5, 2), 5.2)
        assertVersion((3, 20), '3.20')
        assertVersion((6, 1), (6, 1))
        assertVersion((1, 40), [1, 40])
        assertVersion((1, 0), (1,))
        assertVersion((1, 0), ['1'])
        assertVersion((discover.LATEST, discover.LATEST), 'latest')
        assertVersion((discover.LATEST, discover.LATEST), ['latest'])
        assertVersion((discover.LATEST, discover.LATEST), discover.LATEST)
        assertVersion((discover.LATEST, discover.LATEST), (discover.LATEST,))
        assertVersion((10, discover.LATEST), '10.latest')
        assertVersion((10, discover.LATEST), (10, 'latest'))
        assertVersion((10, discover.LATEST), (10, discover.LATEST))

        versionRaises(None)
        versionRaises('hello')
        versionRaises('1.a')
        versionRaises('vacuum')
        versionRaises('')
        versionRaises(('1', 'a'))
        versionRaises([])
        versionRaises([None])

    def test_version_args(self):
        """Validate _normalize_version_args."""

        def assert_min_max(in_ver, in_min, in_max, in_type, out_min, out_max):
            self.assertEqual(
                (out_min, out_max),
                discover._normalize_version_args(
                    in_ver, in_min, in_max, service_type=in_type
                ),
            )

        def normalize_raises(ver, min, max, in_type):
            self.assertRaises(
                ValueError,
                discover._normalize_version_args,
                ver,
                min,
                max,
                service_type=in_type,
            )

        assert_min_max(None, None, None, None, None, None)
        assert_min_max(None, None, 'v1.2', None, None, (1, 2))
        assert_min_max(
            None,
            'v1.2',
            'latest',
            None,
            (1, 2),
            (discover.LATEST, discover.LATEST),
        )
        assert_min_max(None, 'v1.2', '1.6', None, (1, 2), (1, 6))
        assert_min_max(
            None, 'v1.2', '1.latest', None, (1, 2), (1, discover.LATEST)
        )
        assert_min_max(
            None,
            'latest',
            'latest',
            None,
            (discover.LATEST, discover.LATEST),
            (discover.LATEST, discover.LATEST),
        )
        assert_min_max(
            None,
            'latest',
            None,
            None,
            (discover.LATEST, discover.LATEST),
            (discover.LATEST, discover.LATEST),
        )
        assert_min_max(
            None,
            (1, 2),
            None,
            None,
            (1, 2),
            (discover.LATEST, discover.LATEST),
        )
        assert_min_max('', ('1', '2'), (1, 6), None, (1, 2), (1, 6))
        assert_min_max(
            None,
            ('1', '2'),
            (1, discover.LATEST),
            None,
            (1, 2),
            (1, discover.LATEST),
        )
        assert_min_max('v1.2', '', None, None, (1, 2), (1, discover.LATEST))
        assert_min_max(
            '1.latest',
            None,
            '',
            None,
            (1, discover.LATEST),
            (1, discover.LATEST),
        )
        assert_min_max('v1', None, None, None, (1, 0), (1, discover.LATEST))
        assert_min_max(
            'latest',
            None,
            None,
            None,
            (discover.LATEST, discover.LATEST),
            (discover.LATEST, discover.LATEST),
        )
        assert_min_max(
            None, None, 'latest', 'volumev2', (2, 0), (2, discover.LATEST)
        )
        assert_min_max(
            None, None, None, 'volumev2', (2, 0), (2, discover.LATEST)
        )

        normalize_raises('v1', 'v2', None, None)
        normalize_raises('v1', None, 'v2', None)
        normalize_raises(None, 'latest', 'v1', None)
        normalize_raises(None, 'v1.2', 'v1.1', None)
        normalize_raises(None, 'v1.2', 1, None)
        normalize_raises('v2', None, None, 'volumev3')
        normalize_raises('v3', None, None, 'volumev2')
        normalize_raises(None, 'v2', None, 'volumev3')
        normalize_raises(None, None, 'v3', 'volumev2')

    def test_version_to_string(self):
        def assert_string(out, inp):
            self.assertEqual(out, discover.version_to_string(inp))

        assert_string('latest', (discover.LATEST,))
        assert_string('latest', (discover.LATEST, discover.LATEST))
        assert_string(
            'latest', (discover.LATEST, discover.LATEST, discover.LATEST)
        )
        assert_string('1', (1,))
        assert_string('1.2', (1, 2))
        assert_string('1.latest', (1, discover.LATEST))

    def test_version_between(self):
        def good(minver, maxver, cand):
            self.assertTrue(discover.version_between(minver, maxver, cand))

        def bad(minver, maxver, cand):
            self.assertFalse(discover.version_between(minver, maxver, cand))

        def exc(excls, minver, maxver, cand):
            self.assertRaises(
                excls, discover.version_between, minver, maxver, cand
            )

        # candidate required
        exc(ValueError, (1, 0), (1, 0), None)
        exc(ValueError, 'v1.0', '1.0', '')
        # malformed candidate
        exc(TypeError, None, None, 'bogus')
        exc(TypeError, None, None, (1, 'two'))
        # malformed min_version
        exc(TypeError, 'bogus', None, (1, 0))
        exc(TypeError, (1, 'two'), None, (1, 0))
        # malformed max_version
        exc(TypeError, None, 'bogus', (1, 0))
        exc(TypeError, None, (1, 'two'), (1, 0))

        # fail on minimum
        bad((2, 4), None, (1, 55))
        bad('v2.4', '', '2.3')
        bad('latest', None, (2, 3000))
        bad((2, discover.LATEST), '', 'v2.3000')
        bad((2, 3000), '', (1, discover.LATEST))
        bad('latest', None, 'v1000.latest')
        # fail on maximum
        bad(None, (2, 4), (2, 5))
        bad('', 'v2.4', '2.5')
        bad(None, (2, discover.LATEST), (3, 0))
        bad('', '2000.latest', 'latest')

        # candidate matches a bound
        good((1, 0), (1, 0), (1, 0))
        good('1.0', '2.9', '1.0')
        good('v1.0', 'v2.9', 'v2.9')
        # properly in between
        good((1, 0), (1, 10), (1, 2))
        good('1', '2', '1.2')
        # no lower bound
        good(None, (2, 5), (2, 3))
        # no upper bound
        good('2.5', '', '2.6')
        # no bounds at all
        good('', '', 'v1')
        good(None, None, (999, 999))
        good(None, None, 'latest')
        # Various good 'latest' scenarios
        good(
            (discover.LATEST, discover.LATEST),
            (discover.LATEST, discover.LATEST),
            (discover.LATEST, discover.LATEST),
        )
        good(
            (discover.LATEST, discover.LATEST),
            None,
            (discover.LATEST, discover.LATEST),
        )
        good('', 'latest', 'latest')
        good('2.latest', '3.latest', '3.0')
        good('2.latest', None, (55, 66))
        good(None, '3.latest', '3.9999')


class VersionDataTests(utils.TestCase):
    def setUp(self):
        super().setUp()
        self.session = session.Session()

    def test_version_data_basics(self):
        examples = {
            'keystone': V3_VERSION_LIST,
            'cinder': CINDER_EXAMPLES,
            'glance': GLANCE_EXAMPLES,
        }

        for path, data in examples.items():
            url = f"{BASE_URL}{path}"

            mock = self.requests_mock.get(url, status_code=300, json=data)

            disc = discover.Discover(self.session, url)
            raw_data = disc.raw_version_data()
            clean_data = disc.version_data()

            for v in raw_data:
                for n in ('id', 'status', 'links'):
                    msg = f'{n} missing from {path} version data'
                    self.assertThat(
                        v, matchers.Annotate(msg, matchers.Contains(n))
                    )

            for v in clean_data:
                for n in ('version', 'url', 'raw_status'):
                    msg = f'{n} missing from {path} version data'
                    self.assertThat(
                        v, matchers.Annotate(msg, matchers.Contains(n))
                    )

            self.assertTrue(mock.called_once)

    def test_version_data_override_version_url(self):
        # if the request url is versioned already, just return it.
        self.requests_mock.get(
            V3_URL,
            status_code=200,
            json={
                'version': fixture.V3Discovery('http://override/identity/v3')
            },
        )

        disc = discover.Discover(self.session, V3_URL)
        version_data = disc.version_data()

        for v in version_data:
            self.assertEqual(v['version'], (3, 0))
            self.assertEqual(v['status'], discover.Status.CURRENT)
            self.assertEqual(v['raw_status'], 'stable')
            self.assertEqual(v['url'], V3_URL)

        # if the request url is not versioned, just add version info to it.(
        # do not changed the url's netloc or path)
        self.requests_mock.get(
            BASE_URL,
            status_code=200,
            json={
                'version': fixture.V3Discovery('http://override/identity/v3')
            },
        )

        disc = discover.Discover(self.session, BASE_URL)
        version_data = disc.version_data()

        for v in version_data:
            self.assertEqual(v['version'], (3, 0))
            self.assertEqual(v['status'], discover.Status.CURRENT)
            self.assertEqual(v['raw_status'], 'stable')
            self.assertEqual(v['url'], V3_URL)

    def test_version_data_unknown(self):
        discovery_fixture = fixture.V3Discovery(V3_URL)
        discovery_fixture.status = 'hungry'
        discovery_doc = _create_single_version(discovery_fixture)

        self.requests_mock.get(V3_URL, status_code=200, json=discovery_doc)

        disc = discover.Discover(self.session, V3_URL)
        clean_data = disc.version_data(allow_unknown=True)

        self.assertEqual(discover.Status.UNKNOWN, clean_data[0]['status'])

    def test_version_data_individual(self):
        mock = self.requests_mock.get(
            V3_URL, status_code=200, json=V3_VERSION_ENTRY
        )

        disc = discover.Discover(self.session, V3_URL)
        raw_data = disc.raw_version_data()
        clean_data = disc.version_data()

        for v in raw_data:
            self.assertEqual(v['id'], 'v3.0')
            self.assertEqual(v['status'], 'stable')
            self.assertIn('media-types', v)
            self.assertIn('links', v)

        for v in clean_data:
            self.assertEqual(v['version'], (3, 0))
            self.assertEqual(v['status'], discover.Status.CURRENT)
            self.assertEqual(v['raw_status'], 'stable')
            self.assertEqual(v['url'], V3_URL)

        self.assertTrue(mock.called_once)

    def test_version_data_legacy_ironic_no_override(self):
        """Validate detection of legacy Ironic microversion ranges."""
        ironic_url = 'https://bare-metal.example.com/v1/'
        self.requests_mock.get(
            ironic_url,
            status_code=200,
            json={'id': 'v1', 'links': [{"href": ironic_url, "rel": "self"}]},
            headers={
                'X-OpenStack-Ironic-API-Minimum-Version': '1.3',
                'X-OpenStack-Ironic-API-Maximum-Version': '1.21',
            },
        )

        plugin = noauth.NoAuth()
        a = adapter.Adapter(
            self.session, auth=plugin, service_type='baremetal'
        )

        self.assertIsNone(a.get_api_major_version())

    def test_version_data_ironic_microversions(self):
        """Validate detection of Ironic microversion ranges."""
        ironic_url = 'https://bare-metal.example.com/v1/'
        self.requests_mock.get(
            ironic_url,
            status_code=200,
            json={
                'id': 'v1',
                'version': {
                    'id': 'v1',
                    'links': [{"href": ironic_url, "rel": "self"}],
                    'version': '1.40',
                    'min_version': '1.10',
                    'status': 'CURRENT',
                },
                'links': [{"href": ironic_url, "rel": "self"}],
            },
            # Keep headers so we can verify that body trumps headers
            headers={
                'X-OpenStack-Ironic-API-Minimum-Version': '1.3',
                'X-OpenStack-Ironic-API-Maximum-Version': '1.21',
            },
        )

        self.assertEqual(
            [
                {
                    'collection': None,
                    'version': (1, 0),
                    'url': ironic_url,
                    'status': discover.Status.CURRENT,
                    'raw_status': discover.Status.CURRENT,
                    'min_microversion': (1, 10),
                    'max_microversion': (1, 40),
                    'next_min_version': None,
                    'not_before': None,
                }
            ],
            discover.Discover(self.session, ironic_url).version_data(),
        )

    def test_version_data_legacy_ironic_microversions(self):
        """Validate detection of legacy Ironic microversion ranges."""
        ironic_url = 'https://bare-metal.example.com/v1/'
        self.requests_mock.get(
            ironic_url,
            status_code=200,
            json={'id': 'v1', 'links': [{"href": ironic_url, "rel": "self"}]},
            headers={
                'X-OpenStack-Ironic-API-Minimum-Version': '1.3',
                'X-OpenStack-Ironic-API-Maximum-Version': '1.21',
            },
        )

        self.assertEqual(
            [
                {
                    'collection': None,
                    'version': (1, 0),
                    'url': ironic_url,
                    'status': discover.Status.CURRENT,
                    'raw_status': discover.Status.CURRENT,
                    'min_microversion': (1, 3),
                    'max_microversion': (1, 21),
                    'next_min_version': None,
                    'not_before': None,
                }
            ],
            discover.Discover(self.session, ironic_url).version_data(),
        )

    def test_version_data_microversions(self):
        """Validate [min_|max_]version conversion to {min|max}_microversion."""

        def setup_mock(versions_in):
            # Set up the test data with the input version data
            jsondata = {
                "versions": [
                    dict(
                        {
                            "status": discover.Status.CURRENT,
                            "id": "v2.2",
                            "links": [{"href": V3_URL, "rel": "self"}],
                        },
                        **versions_in,
                    )
                ]
            }
            self.requests_mock.get(V3_URL, status_code=200, json=jsondata)

        def test_ok(versions_in, versions_out):
            setup_mock(versions_in)
            # Ensure the output contains the expected microversions
            self.assertEqual(
                [
                    dict(
                        {
                            'collection': None,
                            'version': (2, 2),
                            'url': V3_URL,
                            'status': discover.Status.CURRENT,
                            'raw_status': discover.Status.CURRENT,
                        },
                        **versions_out,
                    )
                ],
                discover.Discover(self.session, V3_URL).version_data(),
            )

        def test_exc(versions_in):
            setup_mock(versions_in)
            # Ensure TypeError is raised
            self.assertRaises(
                TypeError, discover.Discover(self.session, V3_URL).version_data
            )

        # no version info in input
        test_ok(
            {},
            {
                'min_microversion': None,
                'max_microversion': None,
                'next_min_version': None,
                'not_before': None,
            },
        )

        # version => max_microversion
        test_ok(
            {'version': '2.2'},
            {
                'min_microversion': None,
                'max_microversion': (2, 2),
                'next_min_version': None,
                'not_before': None,
            },
        )

        # max_version supersedes version (even if malformed).  min_version &
        # normalization.
        test_ok(
            {'min_version': '2', 'version': 'foo', 'max_version': '2.2'},
            {
                'min_microversion': (2, 0),
                'max_microversion': (2, 2),
                'next_min_version': None,
                'not_before': None,
            },
        )

        # Edge case: min/max_version ignored if present but "empty"; version
        # used for max_microversion.
        test_ok(
            {'min_version': '', 'version': '2.1', 'max_version': ''},
            {
                'min_microversion': None,
                'max_microversion': (2, 1),
                'next_min_version': None,
                'not_before': None,
            },
        )

        # next_min_version set
        test_ok(
            {
                'min_version': '2',
                'max_version': '2.2',
                'next_min_version': '2.1',
                'not_before': '2019-07-01',
            },
            {
                'min_microversion': (2, 0),
                'max_microversion': (2, 2),
                'next_min_version': (2, 1),
                'not_before': '2019-07-01',
            },
        )

        # Badly-formatted min_version
        test_exc({'min_version': 'foo', 'max_version': '2.1'})

        # Badly-formatted max_version
        test_exc({'min_version': '2.1', 'max_version': 'foo'})

        # Badly-formatted version (when max_version omitted)
        test_exc({'min_version': '2.1', 'version': 'foo'})

        # Badly-formatted next_min_version
        test_exc({'next_min_version': 'bogus', 'not_before': '2019-07-01'})

    def test_endpoint_data_noauth_discover(self):
        mock = self.requests_mock.get(
            BASE_URL, status_code=200, json=V3_VERSION_LIST
        )
        self.requests_mock.get(V3_URL, status_code=200, json=V3_VERSION_ENTRY)

        plugin = noauth.NoAuth(endpoint=BASE_URL)
        data = plugin.get_endpoint_data(self.session)

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(plugin.get_api_major_version(self.session), (3, 0))
        self.assertEqual(plugin.get_endpoint(self.session), BASE_URL)

        self.assertTrue(mock.called_once)

    def test_endpoint_data_noauth_versioned_discover(self):
        self.requests_mock.get(BASE_URL, status_code=200, json=V3_VERSION_LIST)
        self.requests_mock.get(V3_URL, status_code=200, json=V3_VERSION_ENTRY)

        plugin = noauth.NoAuth(endpoint=V3_URL)
        data = plugin.get_endpoint_data(self.session)

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(plugin.get_api_major_version(self.session), (3, 0))
        self.assertEqual(plugin.get_endpoint(self.session), V3_URL)

    def test_endpoint_data_noauth_no_discover(self):
        plugin = noauth.NoAuth(endpoint=V3_URL)
        data = plugin.get_endpoint_data(self.session, discover_versions=False)

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(plugin.get_api_major_version(self.session), (3, 0))
        self.assertEqual(plugin.get_endpoint(self.session), V3_URL)

    def test_endpoint_data_noauth_override_no_discover(self):
        plugin = noauth.NoAuth()
        data = plugin.get_endpoint_data(
            self.session, endpoint_override=V3_URL, discover_versions=False
        )

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(
            plugin.get_endpoint(self.session, endpoint_override=V3_URL), V3_URL
        )

    def test_endpoint_data_http_basic_discover(self):
        self.requests_mock.get(BASE_URL, status_code=200, json=V3_VERSION_LIST)
        self.requests_mock.get(V3_URL, status_code=200, json=V3_VERSION_ENTRY)

        plugin = http_basic.HTTPBasicAuth(endpoint=V3_URL)
        data = plugin.get_endpoint_data(self.session)

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(plugin.get_api_major_version(self.session), (3, 0))
        self.assertEqual(plugin.get_endpoint(self.session), V3_URL)

    def test_endpoint_data_http_basic_no_discover(self):
        plugin = http_basic.HTTPBasicAuth(endpoint=V3_URL)
        data = plugin.get_endpoint_data(self.session, discover_versions=False)

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(plugin.get_api_major_version(self.session), (3, 0))
        self.assertEqual(plugin.get_endpoint(self.session), V3_URL)

    def test_endpoint_data_http_basic_override_no_discover(self):
        plugin = http_basic.HTTPBasicAuth()
        data = plugin.get_endpoint_data(
            self.session, endpoint_override=V3_URL, discover_versions=False
        )

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(
            plugin.get_api_major_version(
                self.session, endpoint_override=V3_URL
            ),
            (3, 0),
        )
        self.assertEqual(
            plugin.get_endpoint(self.session, endpoint_override=V3_URL), V3_URL
        )

    def test_endpoint_data_noauth_adapter(self):
        self.requests_mock.get(BASE_URL, status_code=200, json=V3_VERSION_LIST)
        self.requests_mock.get(V3_URL, status_code=200, json=V3_VERSION_ENTRY)

        client = adapter.Adapter(
            session.Session(noauth.NoAuth()), endpoint_override=BASE_URL
        )
        data = client.get_endpoint_data()

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(client.get_api_major_version(), (3, 0))
        self.assertEqual(client.get_endpoint(), BASE_URL)

    def test_endpoint_data_noauth_versioned_adapter(self):
        mock = self.requests_mock.get(
            V3_URL, status_code=200, json=V3_VERSION_ENTRY
        )

        client = adapter.Adapter(
            session.Session(noauth.NoAuth()), endpoint_override=V3_URL
        )
        data = client.get_endpoint_data()

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(client.get_api_major_version(), (3, 0))
        self.assertEqual(client.get_endpoint(), V3_URL)

        self.assertTrue(mock.called_once)

    def test_endpoint_data_token_endpoint_discover(self):
        mock = self.requests_mock.get(
            V3_URL, status_code=200, json=V3_VERSION_ENTRY
        )
        plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus')
        data = plugin.get_endpoint_data(self.session)

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(plugin.get_api_major_version(self.session), (3, 0))
        self.assertEqual(plugin.get_endpoint(self.session), V3_URL)

        self.assertTrue(mock.called_once)

    def test_endpoint_data_token_endpoint_no_discover(self):
        plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus')
        data = plugin.get_endpoint_data(self.session, discover_versions=False)

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(plugin.get_api_major_version(self.session), (3, 0))
        self.assertEqual(plugin.get_endpoint(self.session), V3_URL)

    def test_endpoint_data_token_endpoint_adapter(self):
        mock = self.requests_mock.get(
            V3_URL, status_code=200, json=V3_VERSION_ENTRY
        )
        plugin = token_endpoint.Token(endpoint=V3_URL, token='bogus')

        client = adapter.Adapter(session.Session(plugin))
        data = client.get_endpoint_data()

        self.assertEqual(data.api_version, (3, 0))
        self.assertEqual(data.url, V3_URL)
        self.assertEqual(client.get_api_major_version(), (3, 0))
        self.assertEqual(client.get_endpoint(), V3_URL)

        self.assertTrue(mock.called_once)

    def test_data_for_url(self):
        mock = self.requests_mock.get(
            V3_URL, status_code=200, json=V3_VERSION_ENTRY
        )

        disc = discover.Discover(self.session, V3_URL)
        for url in (V3_URL, V3_URL + '/'):
            data = disc.versioned_data_for(url=url)
            self.assertEqual(data['version'], (3, 0))
            self.assertEqual(data['raw_status'], 'stable')
            self.assertEqual(data['url'], V3_URL)

        self.assertTrue(mock.called_once)

    def test_data_for_no_version(self):
        mock = self.requests_mock.get(
            V3_URL, status_code=200, json=V3_VERSION_ENTRY
        )

        disc = discover.Discover(self.session, V3_URL)

        data = disc.versioned_data_for()
        self.assertEqual(data['version'], (3, 0))
        self.assertEqual(data['raw_status'], 'stable')
        self.assertEqual(data['url'], V3_URL)
        self.assertRaises(TypeError, disc.data_for, version=None)

        self.assertTrue(mock.called_once)

    def test_keystone_version_data(self):
        mock = self.requests_mock.get(
            BASE_URL, status_code=300, json=V3_VERSION_LIST
        )

        disc = discover.Discover(self.session, BASE_URL)
        raw_data = disc.raw_version_data()
        clean_data = disc.version_data()

        self.assertEqual(2, len(raw_data))
        self.assertEqual(2, len(clean_data))

        for v in raw_data:
            self.assertIn(v['id'], ('v2.0', 'v3.0'))
            self.assertEqual(v['updated'], UPDATED)
            self.assertEqual(v['status'], 'stable')

            if v['id'] == 'v3.0':
                self.assertEqual(v['media-types'], V3_MEDIA_TYPES)

        for v in clean_data:
            self.assertIn(v['version'], ((2, 0), (3, 0)))
            self.assertEqual(v['raw_status'], 'stable')

        valid_v3_versions = (
            disc.data_for('v3.0'),
            disc.data_for('3.latest'),
            disc.data_for('latest'),
            disc.versioned_data_for(
                min_version='v3.0', max_version='v3.latest'
            ),
            disc.versioned_data_for(min_version='3'),
            disc.versioned_data_for(min_version='3.latest'),
            disc.versioned_data_for(min_version='latest'),
            disc.versioned_data_for(
                min_version='3.latest', max_version='latest'
            ),
            disc.versioned_data_for(
                min_version='latest', max_version='latest'
            ),
            disc.versioned_data_for(min_version=2),
            disc.versioned_data_for(min_version='2.latest'),
        )
        for version in valid_v3_versions:
            self.assertEqual((3, 0), version['version'])
            self.assertEqual('stable', version['raw_status'])
            self.assertEqual(V3_URL, version['url'])

        valid_v2_versions = (
            disc.data_for(2),
            disc.data_for('2.latest'),
            disc.versioned_data_for(
                min_version=2, max_version=(2, discover.LATEST)
            ),
            disc.versioned_data_for(
                min_version='2.latest', max_version='2.latest'
            ),
        )
        for version in valid_v2_versions:
            self.assertEqual((2, 0), version['version'])
            self.assertEqual('stable', version['raw_status'])
            self.assertEqual(V2_URL, version['url'])

        self.assertIsNone(disc.url_for('v4'))
        self.assertIsNone(
            disc.versioned_url_for(min_version='v4', max_version='v4.latest')
        )
        self.assertEqual(V3_URL, disc.url_for('v3'))
        self.assertEqual(
            V3_URL,
            disc.versioned_url_for(min_version='v3', max_version='v3.latest'),
        )
        self.assertEqual(V2_URL, disc.url_for('v2'))
        self.assertEqual(
            V2_URL,
            disc.versioned_url_for(min_version='v2', max_version='v2.latest'),
        )

        self.assertTrue(mock.called_once)

    def test_cinder_version_data(self):
        mock = self.requests_mock.get(
            BASE_URL, status_code=300, json=CINDER_EXAMPLES
        )

        disc = discover.Discover(self.session, BASE_URL)
        raw_data = disc.raw_version_data()
        clean_data = disc.version_data()

        self.assertEqual(3, len(raw_data))

        for v in raw_data:
            self.assertEqual(v['status'], discover.Status.CURRENT)
            if v['id'] == 'v1.0':
                self.assertEqual(v['updated'], '2012-01-04T11:33:21Z')
            elif v['id'] == 'v2.0':
                self.assertEqual(v['updated'], '2012-11-21T11:33:21Z')
            elif v['id'] == 'v3.0':
                self.assertEqual(v['updated'], '2012-11-21T11:33:21Z')
            else:
                self.fail("Invalid version found")

        v1_url = f"{BASE_URL}v1/"
        v2_url = f"{BASE_URL}v2/"
        v3_url = f"{BASE_URL}v3/"

        self.assertEqual(
            clean_data,
            [
                {
                    'collection': None,
                    'max_microversion': None,
                    'min_microversion': None,
                    'next_min_version': None,
                    'not_before': None,
                    'version': (1, 0),
                    'url': v1_url,
                    'status': discover.Status.CURRENT,
                    'raw_status': discover.Status.CURRENT,
                },
                {
                    'collection': None,
                    'max_microversion': None,
                    'min_microversion': None,
                    'next_min_version': None,
                    'not_before': None,
                    'version': (2, 0),
                    'url': v2_url,
                    'status': discover.Status.CURRENT,
                    'raw_status': discover.Status.CURRENT,
                },
                {
                    'collection': BASE_URL,
                    'max_microversion': (3, 27),
                    'min_microversion': (3, 0),
                    'next_min_version': (3, 4),
                    'not_before': '2019-12-31',
                    'version': (3, 0),
                    'url': v3_url,
                    'status': discover.Status.CURRENT,
                    'raw_status': discover.Status.CURRENT,
                },
            ],
        )

        for version in (
            disc.data_for('v2.0'),
            disc.versioned_data_for(
                min_version='v2.0', max_version='v2.latest'
            ),
        ):
            self.assertEqual((2, 0), version['version'])
            self.assertEqual(discover.Status.CURRENT, version['raw_status'])
            self.assertEqual(v2_url, version['url'])

        for version in (
            disc.data_for(1),
            disc.versioned_data_for(
                min_version=(1,), max_version=(1, discover.LATEST)
            ),
        ):
            self.assertEqual((1, 0), version['version'])
            self.assertEqual(discover.Status.CURRENT, version['raw_status'])
            self.assertEqual(v1_url, version['url'])

        self.assertIsNone(disc.url_for('v4'))
        self.assertIsNone(
            disc.versioned_url_for(min_version='v4', max_version='v4.latest')
        )
        self.assertEqual(v3_url, disc.url_for('v3'))
        self.assertEqual(
            v3_url,
            disc.versioned_url_for(min_version='v3', max_version='v3.latest'),
        )
        self.assertEqual(v2_url, disc.url_for('v2'))
        self.assertEqual(
            v2_url,
            disc.versioned_url_for(min_version='v2', max_version='v2.latest'),
        )
        self.assertEqual(v1_url, disc.url_for('v1'))
        self.assertEqual(
            v1_url,
            disc.versioned_url_for(min_version='v1', max_version='v1.latest'),
        )

        self.assertTrue(mock.called_once)

    def test_glance_version_data(self):
        mock = self.requests_mock.get(
            BASE_URL, status_code=200, json=GLANCE_EXAMPLES
        )

        disc = discover.Discover(self.session, BASE_URL)
        raw_data = disc.raw_version_data()
        clean_data = disc.version_data()

        self.assertEqual(5, len(raw_data))

        for v in raw_data:
            if v['id'] in ('v2.2', 'v1.1'):
                self.assertEqual(v['status'], discover.Status.CURRENT)
            elif v['id'] in ('v2.1', 'v2.0', 'v1.0'):
                self.assertEqual(v['status'], discover.Status.SUPPORTED)
            else:
                self.fail("Invalid version found")

        v1_url = f'{BASE_URL}v1/'
        v2_url = f'{BASE_URL}v2/'

        self.assertEqual(
            clean_data,
            [
                {
                    'collection': None,
                    'max_microversion': None,
                    'min_microversion': None,
                    'next_min_version': None,
                    'not_before': None,
                    'version': (1, 0),
                    'url': v1_url,
                    'status': discover.Status.SUPPORTED,
                    'raw_status': discover.Status.SUPPORTED,
                },
                {
                    'collection': None,
                    'max_microversion': None,
                    'min_microversion': None,
                    'next_min_version': None,
                    'not_before': None,
                    'version': (1, 1),
                    'url': v1_url,
                    'status': discover.Status.CURRENT,
                    'raw_status': discover.Status.CURRENT,
                },
                {
                    'collection': None,
                    'max_microversion': None,
                    'min_microversion': None,
                    'next_min_version': None,
                    'not_before': None,
                    'version': (2, 0),
                    'url': v2_url,
                    'status': discover.Status.SUPPORTED,
                    'raw_status': discover.Status.SUPPORTED,
                },
                {
                    'collection': None,
                    'max_microversion': None,
                    'min_microversion': None,
                    'next_min_version': None,
                    'not_before': None,
                    'version': (2, 1),
                    'url': v2_url,
                    'status': discover.Status.SUPPORTED,
                    'raw_status': discover.Status.SUPPORTED,
                },
                {
                    'collection': None,
                    'max_microversion': None,
                    'min_microversion': None,
                    'next_min_version': None,
                    'not_before': None,
                    'version': (2, 2),
                    'url': v2_url,
                    'status': discover.Status.CURRENT,
                    'raw_status': discover.Status.CURRENT,
                },
            ],
        )

        for ver in (2, 2.1, 2.2):
            for version in (
                disc.data_for(ver),
                disc.versioned_data_for(
                    min_version=ver, max_version=(2, discover.LATEST)
                ),
            ):
                self.assertEqual((2, 2), version['version'])
                self.assertEqual(
                    discover.Status.CURRENT, version['raw_status']
                )
                self.assertEqual(v2_url, version['url'])
                self.assertEqual(v2_url, disc.url_for(ver))

        for ver in (1, 1.1):
            for version in (
                disc.data_for(ver),
                disc.versioned_data_for(
                    min_version=ver, max_version=(1, discover.LATEST)
                ),
            ):
                self.assertEqual((1, 1), version['version'])
                self.assertEqual(
                    discover.Status.CURRENT, version['raw_status']
                )
                self.assertEqual(v1_url, version['url'])
                self.assertEqual(v1_url, disc.url_for(ver))

        self.assertIsNone(disc.url_for('v3'))
        self.assertIsNone(
            disc.versioned_url_for(min_version='v3', max_version='v3.latest')
        )
        self.assertIsNone(disc.url_for('v2.3'))
        self.assertIsNone(
            disc.versioned_url_for(min_version='v2.3', max_version='v2.latest')
        )

        self.assertTrue(mock.called_once)

    def test_allow_deprecated(self):
        status = 'deprecated'
        version_list = [
            {
                'id': 'v3.0',
                'links': [{'href': V3_URL, 'rel': 'self'}],
                'media-types': V3_MEDIA_TYPES,
                'status': status,
                'updated': UPDATED,
            }
        ]
        self.requests_mock.get(BASE_URL, json={'versions': version_list})

        disc = discover.Discover(self.session, BASE_URL)

        # deprecated is allowed by default
        versions = disc.version_data(allow_deprecated=False)
        self.assertEqual(0, len(versions))

        versions = disc.version_data(allow_deprecated=True)
        self.assertEqual(1, len(versions))
        self.assertEqual(status, versions[0]['raw_status'])
        self.assertEqual(V3_URL, versions[0]['url'])
        self.assertEqual((3, 0), versions[0]['version'])

    def test_allow_experimental(self):
        status = 'experimental'
        version_list = [
            {
                'id': 'v3.0',
                'links': [{'href': V3_URL, 'rel': 'self'}],
                'media-types': V3_MEDIA_TYPES,
                'status': status,
                'updated': UPDATED,
            }
        ]
        self.requests_mock.get(BASE_URL, json={'versions': version_list})

        disc = discover.Discover(self.session, BASE_URL)

        versions = disc.version_data()
        self.assertEqual(0, len(versions))

        versions = disc.version_data(allow_experimental=True)
        self.assertEqual(1, len(versions))
        self.assertEqual(status, versions[0]['raw_status'])
        self.assertEqual(V3_URL, versions[0]['url'])
        self.assertEqual((3, 0), versions[0]['version'])

    def test_allow_unknown(self):
        status = 'abcdef'
        version_list = fixture.DiscoveryList(
            BASE_URL, v2=False, v3_status=status
        )
        self.requests_mock.get(BASE_URL, json=version_list)

        disc = discover.Discover(self.session, BASE_URL)

        versions = disc.version_data()
        self.assertEqual(0, len(versions))

        versions = disc.version_data(allow_unknown=True)
        self.assertEqual(1, len(versions))
        self.assertEqual(status, versions[0]['raw_status'])
        self.assertEqual(V3_URL, versions[0]['url'])
        self.assertEqual((3, 0), versions[0]['version'])

    def test_ignoring_invalid_links(self):
        version_list = [
            {
                'id': 'v3.0',
                'links': [{'href': V3_URL, 'rel': 'self'}],
                'media-types': V3_MEDIA_TYPES,
                'status': 'stable',
                'updated': UPDATED,
            },
            {
                'id': 'v3.1',
                'media-types': V3_MEDIA_TYPES,
                'status': 'stable',
                'updated': UPDATED,
            },
            {
                'media-types': V3_MEDIA_TYPES,
                'status': 'stable',
                'updated': UPDATED,
                'links': [{'href': V3_URL, 'rel': 'self'}],
            },
        ]

        self.requests_mock.get(BASE_URL, json={'versions': version_list})

        disc = discover.Discover(self.session, BASE_URL)

        # raw_version_data will return all choices, even invalid ones
        versions = disc.raw_version_data()
        self.assertEqual(3, len(versions))

        # only the version with both id and links will be actually returned
        versions = disc.version_data()
        self.assertEqual(1, len(versions))


class EndpointDataTests(utils.TestCase):
    def setUp(self):
        super().setUp()
        self.session = session.Session()

    @mock.patch('keystoneauth1.discover.get_discovery')
    @mock.patch(
        'keystoneauth1.discover.EndpointData._get_discovery_url_choices'
    )
    def test_run_discovery_cache(self, mock_url_choices, mock_get_disc):
        # get_discovery raises so we keep looping
        mock_get_disc.side_effect = exceptions.DiscoveryFailure()
        # Duplicate 'url1' in here to validate the cache behavior
        mock_url_choices.return_value = ('url1', 'url2', 'url1', 'url3')
        epd = discover.EndpointData()
        epd._run_discovery(
            session='sess',
            cache='cache',
            min_version='min',
            max_version='max',
            project_id='projid',
            allow_version_hack='allow_hack',
            discover_versions='disc_vers',
        )
        # Only one call with 'url1'
        self.assertEqual(3, mock_get_disc.call_count)
        mock_get_disc.assert_has_calls(
            [
                mock.call('sess', url, cache='cache', authenticated=False)
                for url in ('url1', 'url2', 'url3')
            ]
        )

    def test_run_discovery_auth(self):
        url = 'https://example.com'
        headers = {
            'Accept': 'application/json',
            'OpenStack-API-Version': 'version header test',
        }

        session = mock.Mock()
        session.get.side_effect = [
            exceptions.Unauthorized('unauthorized'),
            # Throw a different exception the second time so that we can
            # catch it in the test and verify the retry.
            exceptions.BadRequest('bad request'),
        ]

        try:
            discover.get_version_data(
                session, url, version_header='version header test'
            )
        except exceptions.BadRequest:
            pass
        # Only one call with 'url'
        self.assertEqual(2, session.get.call_count)
        session.get.assert_has_calls(
            [
                mock.call(url, headers=headers, authenticated=None),
                mock.call(url, headers=headers, authenticated=True),
            ]
        )

    def test_endpoint_data_str(self):
        """Validate EndpointData.__str__."""
        # Populate a few fields to make sure they come through.
        epd = discover.EndpointData(
            catalog_url='abc', service_type='123', api_version=(2, 3)
        )
        exp = (
            'EndpointData{api_version=(2, 3), catalog_url=abc,'
            ' endpoint_id=None, interface=None, major_version=None,'
            ' max_microversion=None, min_microversion=None,'
            ' next_min_version=None, not_before=None, raw_endpoint=None,'
            ' region_name=None, service_id=None, service_name=None,'
            ' service_type=123, service_url=None, url=abc}'
        )
        # Works with str()
        self.assertEqual(exp, str(epd))
        # Works with implicit stringification
        self.assertEqual(exp, f"{epd}")

    def test_project_id_int_fallback(self):
        bad_url = "https://compute.example.com/v2/123456"
        epd = discover.EndpointData(catalog_url=bad_url)
        self.assertEqual((2, 0), epd.api_version)

    def test_url_version_match_project_id_int(self):
        self.session = session.Session()
        discovery_fixture = fixture.V3Discovery(V3_URL)
        discovery_doc = _create_single_version(discovery_fixture)
        self.requests_mock.get(V3_URL, status_code=200, json=discovery_doc)
        epd = discover.EndpointData(catalog_url=V3_URL).get_versioned_data(
            session=self.session, project_id='3'
        )
        self.assertEqual(epd.catalog_url, epd.url)
