<reponame>flowpl/env_conf<gh_stars>1-10
import logging
from time import sleep
from unittest import TestCase
from os import environ
from testfixtures import LogCapture

import re
import snapshottest
from ddt import ddt, data
from validators import email, ValidationFailure

from env_config import Config, ConfigValueError, parse_str, parse_int, parse_float, parse_str_list, \
    parse_int_list, parse_float_list, parse_bool, parse_bool_list, ConfigParseError, ConfigMissingError, \
    AggregateConfigError, ConfigNotInCurrentTagError, ConfigFileEmptyError, ConfigError


def delete_environment_variable(name):
    try:
        del environ[name]
    except KeyError:
        pass


class ConfigParseErrorTest(TestCase):
    def test_parse_error(self):
        prev = RuntimeError('something happened')
        err = ConfigParseError('some_key', prev)
        self.assertEqual('some_key', err.key)
        self.assertEqual(prev, err. previous_error)
        self.assertIn('some_key', err.message)
        self.assertIn('some_key', str(err))


class ConfigValueErrorTest(TestCase):
    def test_value_error(self):
        err = ConfigValueError('some_key')
        self.assertEqual('some_key', err.variable_name)
        self.assertIn('some_key', err.message)
        self.assertIn('some_key', str(err))


class ConfigTestCase(TestCase):
    namespace = 'namespace'
    log_level_prefix = 'LOG_LEVEL'

    def setUp(self):
        super().setUp()
        self.config = Config(defer_raise=False)
        delete_environment_variable('KEY')
        for key in list(environ.keys()):
            if key.startswith(self.namespace.upper()):
                del environ[key]
            if key.startswith(self.log_level_prefix):
                del environ[key]


@ddt
class SkalarValuesTest(ConfigTestCase):

    def test_string(self):
        environ['KEY'] = 'some_value'
        self.config.declare('key', parse_str())
        result = self.config.get('key')
        self.assertEqual(result, 'some_value')

    def test_string_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_str())

    def test_string_return_default(self):
        self.config.declare('key', parse_str('default'))
        result = self.config.get('key')
        self.assertEqual('default', result)

    def test_string_return_default_when_is_falsy(self):
        self.config.declare('key', parse_str(''))
        result = self.config.get('key')
        self.assertEqual('', result)

    def test_string_validator_fails(self):
        environ['KEY'] = 'some_value'
        with self.assertRaises(ConfigParseError):
            def validator(input):
                self.assertEqual('some_value', input)
                raise RuntimeError('some message')

            self.config.declare('key', parse_str(validator=validator))

    def test_int(self):
        environ['KEY'] = '1'
        self.config.declare('key', parse_int())
        result = self.config.get('key')
        self.assertEqual(result, 1)

    def test_int_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_int())

    def test_int_return_default(self):
        self.config.declare('key', parse_int(89))
        result = self.config.get('key')
        self.assertEqual(89, result)

    def test_int_return_default_when_is_falsy(self):
        self.config.declare('key', parse_int(0))
        result = self.config.get('key')
        self.assertEqual(0, result)

    def test_int_validator_fails(self):
        environ['KEY'] = '13'
        with self.assertRaises(ConfigParseError) as context:
            def validator(input):
                self.assertEqual('13', input)
                raise RuntimeError('some message')

            self.config.declare('key', parse_int(validator=validator))
        self.assertEqual(context.exception.key, 'KEY')

    def test_int_value_is_invalid(self):
        environ['KEY'] = 'some_invalid_int'
        with self.assertRaises(ConfigParseError):
            self.config.declare('key', parse_int())

    def test_float(self):
        environ['KEY'] = '1.4'
        self.config.declare('key', parse_float())
        result = self.config.get('key')
        self.assertEqual(result, 1.4)

    def test_float_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_float())

    def test_float_return_default(self):
        self.config.declare('key', parse_float(1.4))
        result = self.config.get('key')
        self.assertEqual(1.4, result)

    def test_float_return_default_when_is_falsy(self):
        self.config.declare('key', parse_float(0))
        result = self.config.get('key')
        self.assertEqual(0, result)

    def test_float_validator_fails(self):
        environ['KEY'] = '1.4'
        with self.assertRaises(ConfigParseError) as context:
            def validator(input):
                self.assertEqual('1.4', input)
                raise RuntimeError('some message')

            self.config.declare('key', parse_float(validator=validator))

        self.assertEqual(context.exception.key, 'KEY')

    def test_float_value_is_invalid(self):
        environ['KEY'] = 'some_invalid_float'
        with self.assertRaises(ConfigParseError):
            self.config.declare('key', parse_float())

    @data(
        ['TRUE', True],
        ['true', True],
        ['yes', True],
        ['1', True],
        ['false', False],
        ['FALSE', False],
        ['no', False],
        ['0', False],
    )
    def test_bool(self, test_data):
        environ['KEY'] = test_data[0]
        self.config.declare('key', parse_bool())
        result = self.config.get('key')
        self.assertEqual(result, test_data[1])

    def test_bool_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_bool())

    def test_bool_return_default(self):
        self.config.declare('key', parse_bool(True))
        result = self.config.get('key')
        self.assertEqual(True, result)

    def test_bool_return_default_when_is_false(self):
        self.config.declare('key', parse_bool(False))
        result = self.config.get('key')
        self.assertEqual(False, result)

    def test_bool_validator_fails(self):
        environ['KEY'] = 'true'
        with self.assertRaises(ConfigParseError) as context:
            def validator(input):
                self.assertEqual('true', input)
                raise RuntimeError('some message')

            self.config.declare('key', parse_bool(validator=validator))

        self.assertEqual(context.exception.key, 'KEY')

    def test_bool_value_is_invalid(self):
        environ['KEY'] = 'some_invalid_boolean'
        with self.assertRaises(ConfigParseError):
            self.config.declare('key', parse_bool())


class ListValuesTest(ConfigTestCase):
    def setUp(self):
        super().setUp()
        delete_environment_variable('KEY')

    def test_string_list(self):
        environ['KEY'] = 'one,two,three'
        self.config.declare('key', parse_str_list())
        result = self.config.get('key')
        self.assertEqual(result, ['one', 'two', 'three'])

    def test_string_list_strip_whitespace(self):
        environ['KEY'] = 'one ,\ntwo\t,   three  \t'
        self.config.declare('key', parse_str_list())
        result = self.config.get('key')
        self.assertEqual(result, ['one', 'two', 'three'])

    def test_string_list_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_str_list())

    def test_string_list_different_separator(self):
        environ['KEY'] = 'one-two-three'
        self.config.declare('key', parse_str_list(separator='-'))
        result = self.config.get('key')
        self.assertEqual(['one', 'two', 'three'], result)

    def test_string_list_return_default(self):
        default_value = ['default1', 'default2']
        self.config.declare('key', parse_str_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_string_list_return_default_when_falsy(self):
        default_value = []
        self.config.declare('key', parse_str_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_string_list_validator_fails(self):
        environ['KEY'] = 'true,false'
        with self.assertRaises(ConfigParseError) as context:
            def validator(input):
                raise RuntimeError('some message')

            self.config.declare('key', parse_bool_list(validator=validator))

        self.assertEqual(context.exception.key, 'KEY')

    def test_string_list_validator_applied_to_each_element(self):
        environ['KEY'] = 'true,false'
        values_validated = []

        def validator(input):
            nonlocal values_validated
            values_validated.append(input)

        self.config.declare('key', parse_bool_list(validator=validator))
        self.assertListEqual([True, False], values_validated)

    def test_int_list(self):
        environ['KEY'] = '123,456,789'
        self.config.declare('key', parse_int_list())
        result = self.config.get('key')
        self.assertEqual(result, [123, 456, 789])

    def test_int_list_trim_whitespace(self):
        environ['KEY'] = '123\t, 456\n,  789    \t'
        self.config.declare('key', parse_int_list())
        result = self.config.get('key')
        self.assertEqual(result, [123, 456, 789])

    def test_int_list_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_int_list())

    def test_int_list_return_default(self):
        default_value = [1, 4, 7]
        self.config.declare('key', parse_int_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_int_list_return_default_when_falsy(self):
        default_value = []
        self.config.declare('key', parse_int_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_int_list_different_separator(self):
        environ['KEY'] = '1-2-3'
        self.config.declare('key', parse_int_list(separator='-'))
        result = self.config.get('key')
        self.assertEqual([1, 2, 3], result)

    def test_int_list_validator_fails(self):
        environ['KEY'] = '1,2'
        with self.assertRaises(ConfigParseError) as context:
            def validator(input):
                raise RuntimeError('some message')

            self.config.declare('key', parse_int_list(validator=validator))

        self.assertEqual(context.exception.key, 'KEY')

    def test_int_list_validator_applied_to_each_element(self):
        environ['KEY'] = '1,2'
        values_validated = []

        def validator(input):
            nonlocal values_validated
            values_validated.append(input)

        self.config.declare('key', parse_int_list(validator=validator))
        self.assertListEqual([1, 2], values_validated)

    def test_int_list_one_value_invalid(self):
        environ['KEY'] = '1,em,45'
        with self.assertRaises(ConfigParseError):
            self.config.declare('key', parse_int_list())

    def test_float_list(self):
        environ['KEY'] = '123,456,789'
        self.config.declare('key', parse_float_list())
        result = self.config.get('key')
        self.assertEqual(result, [123, 456, 789])

    def test_float_list_trim_whitespace(self):
        environ['KEY'] = '123\t, 456\n,  789    \t'
        self.config.declare('key', parse_float_list())
        result = self.config.get('key')
        self.assertEqual(result, [123, 456, 789])

    def test_float_list_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_float_list())

    def test_float_list_return_default(self):
        default_value = [1.3, 5.8, 5.89008]
        self.config.declare('key', parse_float_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_float_list_return_default_when_falsy(self):
        default_value = []
        self.config.declare('key', parse_float_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_float_list_different_separator(self):
        environ['KEY'] = '1.4-4.3-9.56'
        self.config.declare('key', parse_float_list(separator='-'))
        result = self.config.get('key')
        self.assertEqual([1.4, 4.3, 9.56], result)

    def test_float_list_validator_fails(self):
        environ['KEY'] = '1.2,4.6'
        with self.assertRaises(ConfigParseError) as context:
            def validator(input):
                raise RuntimeError('some message')

            self.config.declare('key', parse_float_list(validator=validator))

        self.assertEqual(context.exception.key, 'KEY')

    def test_float_list_validator_applied_to_each_element(self):
        environ['KEY'] = '1.2,4.6'
        values_validated = []
        def validator(input):
            nonlocal values_validated
            values_validated.append(input)

        self.config.declare('key', parse_float_list(validator=validator))
        self.assertListEqual([1.2, 4.6], values_validated)

    def test_float_list_one_value_invalid(self):
        environ['KEY'] = '1.9,em,45.0'
        with self.assertRaises(ConfigParseError):
            self.config.declare('key', parse_float_list())

    def test_bool_list(self):
        environ['KEY'] = 'yes,FALSE,1'
        self.config.declare('key', parse_bool_list())
        result = self.config.get('key')
        self.assertEqual(result, [True, False, True])

    def test_bool_list_trim_whitespace(self):
        environ['KEY'] = 'yes\t, FALSE\n,  1    \t'
        self.config.declare('key', parse_bool_list())
        result = self.config.get('key')
        self.assertEqual(result, [True, False, True])

    def test_bool_list_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', parse_bool_list())

    def test_bool_list_return_default(self):
        default_value = [True, True]
        self.config.declare('key', parse_bool_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_bool_list_return_default_when_falsy(self):
        default_value = []
        self.config.declare('key', parse_bool_list(default_value))
        result = self.config.get('key')
        self.assertEqual(default_value, result)

    def test_bool_list_different_separator(self):
        environ['KEY'] = 'yes-FALSE-1'
        self.config.declare('key', parse_bool_list(separator='-'))
        result = self.config.get('key')
        self.assertEqual([True, False, True], result)

    def test_bool_list_validator_fails(self):
        environ['KEY'] = 'true,false'
        with self.assertRaises(ConfigParseError) as context:
            def validator(input):
                self.assertEqual(True, input)
                raise RuntimeError('some message')

            self.config.declare('key', parse_bool_list(validator=validator))

        self.assertEqual(context.exception.key, 'KEY')

    def test_bool_list_validator_applied_to_each_element(self):
        environ['KEY'] = 'true,false'
        values_validated = []

        def validator(input):
            nonlocal values_validated
            values_validated.append(input)

        self.config.declare('key', parse_bool_list(validator=validator))
        self.assertListEqual([True, False], values_validated)

    def test_bool_list_one_value_invalid(self):
        environ['KEY'] = 'True,em,No'
        with self.assertRaises(ConfigParseError):
            self.config.declare('key', parse_bool_list())


class FlatDictValuesTest(ConfigTestCase):
    def setUp(self):
        super().setUp()
        delete_environment_variable('KEY_STRING')
        delete_environment_variable('KEY_INT')
        delete_environment_variable('KEY_FLOAT')
        delete_environment_variable('KEY_STRING_LIST')
        delete_environment_variable('KEY_FLOAT_LIST')
        delete_environment_variable('KEY_INT_LIST')
        delete_environment_variable('KEY_ONE')

    def test_dict(self):
        environ['KEY_STRING'] = 'string'
        environ['KEY_INT'] = '1'
        environ['KEY_FLOAT'] = '1.4'
        environ['KEY_BOOL'] = 'false'
        environ['KEY_STRING_LIST'] = 'one,two'
        environ['KEY_FLOAT_LIST'] = '1.4, 3.89'
        environ['KEY_INT_LIST'] = '1,2,3'
        environ['KEY_BOOL_LIST'] = '1,FALSE,no'
        self.config.declare(
            'key',
            {
                'string': parse_str(),
                'int': parse_int(),
                'float': parse_float(),
                'bool': parse_bool(),
                'string_list': parse_str_list(),
                'int_list': parse_int_list(),
                'float_list': parse_float_list(),
                'bool_list': parse_bool_list(),
            },
        )

        result = self.config.get('key')

        self.assertDictEqual(
            {
                'string': 'string',
                'int': 1,
                'float': 1.4,
                'bool': False,
                'string_list': ['one', 'two'],
                'int_list': [1, 2, 3],
                'float_list': [1.4, 3.89],
                'bool_list': [True, False, False],
            },
            result
        )

    def test_dict_variable_missing(self):
        with self.assertRaises(ConfigValueError):
            self.config.declare('key', {'one': parse_str()})

    def test_dict_element_missing(self):
        environ['KEY_STRING'] = 'string'
        with self.assertRaises(ConfigValueError):
            self.config.declare(
                'key',
                {
                    'string': parse_str(),
                    'int': parse_int(),
                },
            )


class NestedDictValuesTest(ConfigTestCase):
    def setUp(self):
        super().setUp()
        delete_environment_variable('KEY_STRING')
        delete_environment_variable('KEY_DICT2_INT')
        delete_environment_variable('KEY_DICT2_DICT3_FLOAT')
        delete_environment_variable('KEY_STRING_LIST')
        delete_environment_variable('KEY_FLOAT_LIST')
        delete_environment_variable('KEY_INT_LIST')

    def test_recursive_dict(self):
        environ['KEY_STRING'] = 'string'
        environ['KEY_DICT2_INT'] = '1'
        environ['KEY_DICT2_DICT3_FLOAT'] = '1.4'
        environ['KEY_STRING_LIST'] = 'one,two'
        environ['KEY_FLOAT_LIST'] = '1.4, 3.89'
        environ['KEY_INT_LIST'] = '1,2,3'
        self.config.declare(
            'key',
            {
                'string': parse_str(),
                'dict2': {
                    'int': parse_int(),
                    'dict3': {
                        'float': parse_float(),
                    },
                },
                'string_list': parse_str_list(),
                'int_list': parse_int_list(),
                'float_list': parse_float_list()
            },
        )

        result = self.config.get('key')

        self.assertDictEqual(
            {
                'string': 'string',
                'dict2': {
                    'int': 1,
                    'dict3': {
                        'float': 1.4,
                    },
                },
                'string_list': ['one', 'two'],
                'int_list': [1, 2, 3],
                'float_list': [1.4, 3.89]
            },
            result
        )

    def test_recursive_dict_nested_element_missing(self):
        environ['KEY_STRING'] = 'string'
        with self.assertRaises(ConfigValueError):
            self.config.declare(
                'key',
                {
                    'string': parse_str(),
                    'dict2': {
                        'dict3': {
                            'float': parse_float(),
                        },
                    }
                },
            )


class GetTest(ConfigTestCase):
    def test_get_without_declare(self):
        with self.assertRaises(ConfigMissingError):
            self.config.get('something')


class ExternalValidationTest(ConfigTestCase):
    def setUp(self):
        super().setUp()
        delete_environment_variable('KEY')

    def test_validate_str(self):
        environ['KEY'] = 'someinvalidemail'

        def email_validator(value):
            result = email(value)
            if isinstance(result, ValidationFailure):
                raise ValueError('"{}" is not a valid email address'.format(value))

        with self.assertRaises(ConfigParseError):
            self.config.declare('key', parse_str(validator=email_validator))


class ReloadTest(ConfigTestCase):
    def setUp(self):
        super().setUp()
        delete_environment_variable('KEY')

    def test_reload(self):
        environ['KEY'] = 'original value'
        self.config.declare('key', parse_str())
        value1 = self.config.get('key')
        self.assertEqual(value1, 'original value')
        environ['KEY'] = 'new value'
        value2 = self.config.get('key')
        self.assertEqual(value2, 'original value')

        self.config.reload()

        value3 = self.config.get('key')
        self.assertEqual(value3, 'new value')


class ErrorReportingTest(ConfigTestCase, snapshottest.TestCase):
    def setUp(self):
        super().setUp()
        self.config = Config(defer_raise=True)
        delete_environment_variable('ERR_KEY_1')
        delete_environment_variable('ERR_KEY_2')
        delete_environment_variable('INT_VALUE')
        delete_environment_variable('UNDECLARED')
        delete_environment_variable('ERR_KEY_1_VALUE1')

    def test_single_value(self):
        self.config.declare('err_key_1', parse_str())
        with self.assertRaises(AggregateConfigError):
            self.config.get('err_key_1')

    def test_multiple_values(self):
        self.config.declare('err_key_1', parse_str())
        self.config.declare('err_key_2', parse_str())
        with self.assertRaises(AggregateConfigError) as context:
            self.config.get('err_key_1')

        self.assertEqual(len(context.exception.exceptions), 3)

    def test_single_dict_value(self):
        self.config.declare('err_key_1', {'value1': parse_str()})
        with self.assertRaises(AggregateConfigError):
            self.config.get('err_key_1')

    def test_nested_dict_values(self):
        self.config.declare(
            'err_key_1',
            {
                'value1': parse_str(),
                'dict2': {
                    'key3': parse_str(),
                    'dict3': {
                        'key4': parse_str()
                    }
                }
            })
        with self.assertRaises(AggregateConfigError) as context:
            self.config.get('err_key_1')

        self.assertEqual(len(context.exception.exceptions), 3)

    def test_report_message_for_missing_environment_variables(self):
        environ['INT_VALUE'] = 'some int value'
        environ['UNDECLARED'] = 'some value'
        environ['ERR_KEY_1_VALUE1'] = 'some int value'

        self.config.declare('err_key_1', parse_int())
        self.config.declare(
            'err_key_1',
            {
                'value1': parse_int(),
                'dict2': {
                    'key3': parse_str(),
                    'dict3': {
                        'key4': parse_str()
                    }
                }
            })
        self.config.declare('int_value', parse_int())
        with self.assertRaises(AggregateConfigError) as context:
            self.config.get('undeclared')

        self.assertMatchSnapshot(str(context.exception))

    def test_do_not_report_the_same_error_multiple_times(self):
        self.config.declare('err_key_1', parse_int())
        self.config.declare('err_key_1', parse_int())
        self.config.declare('err_key_1', parse_int())

        with self.assertRaises(AggregateConfigError) as context:
            self.config.get('undeclared')

        self.assertMatchSnapshot(str(context.exception))


class ConfigTagsTest(ConfigTestCase):

    def test_do_not_raise_when_declaring_a_variable_in_another_environment(self):
        self.config.declare('optional', parse_str(), ('default',), 'other')

    def test_raise_declare_error_when_getting_a_variable_from_another_environment(self):
        self.config.declare('optional', parse_str(), ('default',), 'other')
        with self.assertRaises(ConfigNotInCurrentTagError):
            self.config.get('optional')

    def test_do_not_raise_when_declaring_a_dict_in_another_environment(self):
        self.config.declare('optional', {'value': parse_str()}, ('default',), 'other')

    def test_raise_declare_error_when_getting_a_dict_from_another_environment(self):
        self.config.declare('optional', {'value': parse_str()}, ('default',), 'other')
        with self.assertRaises(ConfigNotInCurrentTagError):
            self.config.get('optional')

    def test_do_not_raise_when_declaring_a_list_in_another_environment(self):
        self.config.declare('optional', parse_str_list(), ('default',), 'other')

    def test_raise_declare_error_when_getting_a_list_from_another_environment(self):
        self.config.declare('optional', parse_str_list(), ('default',), 'other')
        with self.assertRaises(ConfigNotInCurrentTagError):
            self.config.get('optional')


class LoadConfigFromFileTest(ConfigTestCase, snapshottest.TestCase):
    def test_load_bash_file(self):
        environ['CONFIG_FILE'] = 'test/env'
        self.config = Config(filename_variable='CONFIG_FILE')
        self.config.declare('first_variable', parse_int(), ('test',), 'test')
        self.config.declare('second_variable', parse_int(), ('test',), 'test')
        self.config.declare('third_variable', parse_int(), ('test',), 'test')
        self.config.declare('fourth_variable', parse_int(), ('test',), 'test')
        self.config.declare('fifth_variable', parse_int(), ('test',), 'test')
        self.config.declare('sixth_variable', parse_int(), ('test',), 'test')
        self.config.declare('dict1', {'value1': parse_int(), 'value2': parse_int()}, ('test',), 'test')
        with self.assertRaises(AggregateConfigError) as context:
            self.config.get('first_variable')

        first_line, *rest = str(context.exception).split('\n')
        # the absolute path to the config file varies depending on environment. It's not suitable for snapshot testing.
        self.assertTrue(re.match(r'^Errors in config file [/\-_A-Za-z0-9]+?/test/env:$', first_line))
        self.assertMatchSnapshot('\n'.join(rest))

    def test_dont_raise_if_config_file_does_not_exist(self):
        environ['CONFIG_FILE'] = 'test/missing'
        self.config = Config(filename_variable='CONFIG_FILE')
        with self.assertLogs(logger=self.config.logger, level='WARNING') as context:
            self.config.declare('missing_value', parse_int(), ('test',), 'test')

        self.assertIn('Config file not found', context[1][0])

    def test_raise_error_if_config_file_is_empty(self):
        environ['CONFIG_FILE'] = 'test/empty'
        self.config = Config(filename_variable='CONFIG_FILE')
        with self.assertRaises(ConfigFileEmptyError):
            self.config.declare('variable1', parse_int(), ('test',), 'test')

    def test_dont_raise_error_if_filename_variable_does_not_exist(self):
        self.config = Config(filename_variable='MISSING_VARIABLE')
        self.config.declare('variable1', parse_int(), ('test',), 'test')


class NamespaceTest(ConfigTestCase, snapshottest.TestCase):

    def test_load_prefixed_environment_variable(self):
        environ['NAMESPACE_KEY'] = '14'
        config = Config(namespace='namespace')
        config.declare('key', parse_int())
        self.assertEqual(14, config.get('key'))

    def test_load_prefixed_nested_variable(self):
        environ['NAMESPACE_KEY1_KEY2'] = '14'
        environ['NAMESPACE_KEY1_KEY3'] = '25'
        config = Config(namespace='namespace')
        config.declare(
            'key1',
            {
                'key2': parse_int(),
                'key3': parse_int(),
            }
        )
        self.assertEqual(14, config.get('key1').get('key2'))
        self.assertEqual(25, config.get('key1').get('key3'))

    def test_raise_config_value_error_when_prefixed_variable_does_not_exist(self):
        environ['KEY'] = '14'
        config = Config(namespace='namespace', defer_raise=False)
        with self.assertRaises(ConfigValueError) as context:
            config.declare('key', parse_int())

        self.assertMatchSnapshot(str(context.exception))

    def test_raise_confic_missing_error_when_prefixed_variable_is_not_declared(self):
        environ['KEY'] = '14'
        config = Config(namespace='namespace', defer_raise=False)
        with self.assertRaises(ConfigMissingError) as context:
            config.get('key')

        self.assertMatchSnapshot(str(context.exception))


class LoggerConfigTest(ConfigTestCase, snapshottest.TestCase):
    def test_set_main_logger_to_warning(self):
        environ['LOG_LEVEL'] = 'WArNInG'
        self.config.apply_log_levels()

        logger = logging.getLogger()
        log_level = logging.getLevelName(logger.getEffectiveLevel())
        self.assertEqual('WARNING', log_level)

    def test_set_log_level_for_multiple_loggers(self):
        # import these modules to make their loggers become available
        import asyncio
        import concurrent.futures
        environ['LOG_LEVEL_ASYNCIO'] = 'INFO'
        environ['LOG_LEVEL_CONCURRENT.FUTURES'] = 'INFO'
        self.config.apply_log_levels()

        asyncio_logger = logging.getLogger('asyncio')
        asyncio_level = logging.getLevelName(asyncio_logger.getEffectiveLevel())

        futures_logger = logging.getLogger('concurrent.futures')
        futures_level = logging.getLevelName(futures_logger.getEffectiveLevel())

        self.assertEqual(asyncio_level, 'INFO')
        self.assertEqual(futures_level, 'INFO')

    def test_raise_config_error_when_logger_does_not_exist(self):
        environ['LOG_LEVEL_MISSING'] = 'WArNInG'
        with self.assertRaises(ConfigError) as context:
            self.config.apply_log_levels()
        self.assertMatchSnapshot(str(context.exception))
