コード例 #1
0
class TestDuoAPIAuthUnit(unittest.TestCase):
    """
        These are intended to exercise internal functions of the library's
        DuoAPIAuth class without going out to Duo.
    """

    testing_conffile = '/tmp/TestDuoAPIAuthUnit.txt'

    def setUp(self):
        """ Preparing test rig """
        # To get a decent test, we're going to need items from the config
        # file in order to test.
        config = configparser.ConfigParser()
        config.add_section('duo-credentials')
        config.set('duo-credentials', 'IKEY', 'DI9QQ99X9MK4H99RJ9FF')
        config.set('duo-credentials', 'SKEY',
                   '2md9rw5xeyxt8c648dgkmdrg3zpvnhj5b596mgku')
        config.set('duo-credentials', 'HOST', 'api-9f134ff9.duosekurity.com')
        with open(self.testing_conffile, 'w') as configfile:
            config.write(configfile)

        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[
                                   'duo_openvpn.conf',
                                   '/usr/local/etc/duo_openvpn.conf',
                                   '/etc/openvpn/duo_openvpn.conf',
                                   '/etc/duo_openvpn.conf',
                                   self.testing_conffile
                               ]):
            self.main_object = DuoOpenVPN()
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        user_creds = dict()
        for varname in OpenVPNCredentials.DUO_RESERVED_WORDS:
            os.environ['password'] = varname
            res = OpenVPNCredentials()
            res.load_variables_from_environment()
            user_creds[varname] = res
        self.user_data = user_creds
        #
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[
                                   'duo_openvpn.conf',
                                   '/usr/local/etc/duo_openvpn.conf',
                                   '/etc/openvpn/duo_openvpn.conf',
                                   '/etc/duo_openvpn.conf',
                                   self.testing_conffile
                               ]):
            self.library = DuoAPIAuth(**self.main_object.duo_client_args)

    def tearDown(self):
        """ Clear the env so we don't impact other tests """
        for varname in ['common_name', 'password', 'username', 'untrusted_ip']:
            if varname in os.environ:
                del os.environ[varname]
        try:
            os.unlink(self.testing_conffile)
        except OSError:  # pragma: no cover
            # ... else, there was nothing there (likely) ...
            if os.path.exists(self.testing_conffile):
                # ... but if there is, we couldn't delete it, so complain.
                raise

    def test_init(self):
        """ Verify init does the right thing """
        self.assertIsInstance(
            self.library, duo_client.Auth,
            'our DuoAPIAuth must be a descendant '
            'of duo_client.Auth')
        self.assertIsInstance(self.library.hostname, str,
                              'hostname must be set and a string')
        self.assertIsInstance(self.library._fail_open, bool,
                              '_fail_open must return a bool')
        self.assertIsNone(self.library.user_config,
                          'user_config must be empty on a plain init')
        self.assertIsNone(self.library.log_func,
                          'log_func must default to None')

    def test_fail_open_00(self):
        """ _fail_open performs as expected """
        res = self.library._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertFalse(res, '_fail_open defaults to False')

    def test_fail_open_01(self):
        """ _fail_open performs as expected on a True """
        tmplibrary = DuoAPIAuth(fail_open=True,
                                **self.main_object.duo_client_args)
        res = tmplibrary._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertTrue(res, '_fail_open must return an expected True')

    def test_fail_open_02(self):
        """ _fail_open performs as expected on a False """
        tmplibrary = DuoAPIAuth(fail_open=False,
                                **self.main_object.duo_client_args)
        res = tmplibrary._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertFalse(res, '_fail_open must return an expected False')

    def test_load_user_to_verify_00(self):
        """ load_user_to_verify can fail for garbage """
        res = self.library.load_user_to_verify({})
        self.assertFalse(res, 'load_user_to_verify must be False '
                         'for junk input')

    def test_load_user_to_verify_01(self):
        """ load_user_to_verify can fail for a bad username """
        setattr(self.user_data['push'], 'username', 0)
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertFalse(
            res, 'load_user_to_verify must be False '
            'for a bad username')

    def test_load_user_to_verify_02(self):
        """ load_user_to_verify can fail for a bad factor """
        setattr(self.user_data['push'], 'factor', 0)
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertFalse(
            res, 'load_user_to_verify must be False '
            'for a bad factor')

    def test_load_user_to_verify_03(self):
        """ load_user_to_verify can succeed and return True """
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertTrue(res, 'load_user_to_verify must be True')

    def test_log(self):
        """ Test the weird logging function """
        with mock.patch.object(self.library, 'log_func') as mock_dummy:
            self.library.log('foo', bar='quux')
        mock_dummy.assert_called_once_with('foo', bar='quux')

    def test_10_preflight(self):
        """ Test various levels of preflight checks """
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'ping',
                                   side_effect=socket.error):
                res = self.library._preflight()
            self.assertFalse(res, "Broken ping must return False")
            # Check the call_args - [1] is the kwargs.
            #self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            #self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library, 'ping', return_value=None):
                with mock.patch.object(self.library,
                                       'check',
                                       side_effect=socket.error):
                    res = self.library._preflight()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library, 'ping', return_value=None):
                with mock.patch.object(self.library,
                                       'check',
                                       side_effect=RuntimeError):
                    res = self.library._preflight()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'ping', return_value=None):
            with mock.patch.object(self.library, 'check', return_value=None):
                res = self.library._preflight()
            self.assertTrue(res, "Good preflight check must return True")

    def test_20_preauth(self):
        """ Test various levels of preauth checks """
        # a _preauth without users configured must fail hard:
        with self.assertRaises(Exception):
            self.library._preauth()

        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'preauth',
                                   side_effect=socket.error):
                res = self.library._preauth()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'preauth',
                                   side_effect=RuntimeError):
                res = self.library._preauth()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'preauth',
                                   side_effect=httplib.BadStatusLine('')):
                res = self.library._preauth()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 14, 'thing']
        with mock.patch.object(self.library,
                               'preauth',
                               return_value=upstream_return):
            res = self.library._preauth()
        self.assertEqual(res, upstream_return,
                         "Good preauth returns whatever upstream sent")

    def test_30_auth_fails(self):
        """ Test various failure levels of auth checks """
        # a _auth without users configured must fail hard:
        with self.assertRaises(Exception):
            self.library._auth()

        # SMS can't auth:
        with mock.patch.object(self.library, 'log') as mock_log:
            self.library.user_config = self.user_data['sms']
            res = self.library._auth()
            self.assertIsNone(res, "sms _auth must return None")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        # garbage factors can't auth:
        with mock.patch.object(self.library, 'log') as mock_log:
            self.library.user_config = self.user_data['push']
            self.library.user_config.factor = 'nonsense'
            res = self.library._auth()
            self.assertIsNone(res, "nonsense factor _auth must return None")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_32_auth_phone(self):
        """ Test phone on auth checks. """
        # Remember that 'phone' is a garbage code path.  If you break here,
        # feel free to attack this problem some other way.
        self.library.user_config = self.user_data['phone']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 32, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['device'], 'auto')

    def test_33_auth_auto(self):
        """ Test auto on auth checks. """
        # Remember that 'auto' is a garbage code path.  If you break here,
        # feel free to attack this problem some other way.
        self.library.user_config = self.user_data['auto']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 33, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['device'], 'auto')

    def test_34_auth_passcode(self):
        """ Test passcode on auth checks. """
        os.environ['password'] = '******'
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.library.user_config = creds
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 34, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['passcode'],
                         self.library.user_config.passcode)

    def test_35_auth_push(self):
        """ Test push on auth checks. """
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 35, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['device'], 'auto')

    def test_40_do_mfa(self):
        """ Unit test for _do_mfa_for_user """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_auth', return_value=None):
            res = self.library._do_mfa_for_user()
        self.assertFalse(
            res, "Failed _auth must cause _do_mfa_for_user to be False")

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_auth',
                                   return_value='weirdness'):
                res = self.library._do_mfa_for_user()
            self.assertFalse(
                res, "Garbage _auth must cause _do_mfa_for_user to be False")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_auth',
                                   return_value={'result': 'allow'}):
                res = self.library._do_mfa_for_user()
            self.assertTrue(
                res, "Allowed _auth must cause _do_mfa_for_user to be True")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'true')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_auth',
                                   return_value={
                                       'result': 'deny',
                                       'status_msg': 'Nope'
                                   }):
                res = self.library._do_mfa_for_user()
            self.assertFalse(
                res, "Denied _auth must cause _do_mfa_for_user to be False")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_50_main_auth_awful1(self):
        """ Test main_auth that fail preflight """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_preflight',
                                   return_value=False):
                self.library._fail_open = True
                #with mock.patch.object(self.library, '_fail_open', return_value=True):
                res = self.library.main_auth()
        self.assertTrue(res,
                        "main_auth follows fail_open when preflight fails")
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'true')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_preflight',
                                   return_value=False):
                self.library._fail_open = False
                res = self.library.main_auth()
        self.assertFalse(res,
                         "main_auth follows fail_open when preflight fails")
        mock_log.assert_not_called()

    def test_51_main_auth_awful2(self):
        """ Test main_auth that fail preauth """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value=None):
                    res = self.library.main_auth()
            self.assertFalse(res, "main_auth should fail when preauth fails")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value='junk'):
                    res = self.library.main_auth()
            self.assertFalse(
                res, "main_auth should fail when preauth has an API failure")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_52_main_auth_allow(self):
        """ Test main_auth when it gets an 'allow' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'allow'}):
                    res = self.library.main_auth()
            self.assertTrue(
                res, "main_auth should allow when preauth gets an allow")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'true')

    def test_53_main_auth_enroll(self):
        """ Test main_auth when it gets an 'enroll' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'enroll'}):
                    res = self.library.main_auth()
            self.assertFalse(
                res, "main_auth should deny when preauth gets an enroll")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_54_main_auth_deny(self):
        """ Test main_auth when it gets a 'deny' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'deny'}):
                    res = self.library.main_auth()
            self.assertFalse(res,
                             "main_auth should deny when preauth gets a deny")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_55_main_auth_auth(self):
        """ Test main_auth when it gets an 'auth' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'auth'}):
                    upstream_return = ['some', 55, 'thing']
                    with mock.patch.object(self.library,
                                           '_do_mfa_for_user',
                                           return_value=upstream_return):
                        res = self.library.main_auth()
            self.assertEqual(res, upstream_return,
                             "main_auth should deny when preauth gets a deny")
            mock_log.assert_not_called()

    def test_59_main_auth_nonsense(self):
        """ Test main_auth when it gets an unexpected response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(
                        self.library,
                        '_preauth',
                        return_value={'result': 'never_seen_this'}):
                    res = self.library.main_auth()
            self.assertFalse(
                res,
                "main_auth should deny when preauth gets something unexpected")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')
コード例 #2
0
class TestDuoAPIAuth(unittest.TestCase):
    # pylint: disable=too-many-public-methods
    """
        These are intended to exercise internal functions of the library's
        DuoOpenVPN class.
    """
    def setUp(self):
        """ Preparing test rig """
        # To get a decent test, we're going to need items from the config
        # file in order to test.
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[
                                   'duo_openvpn.conf',
                                   '/usr/local/etc/duo_openvpn.conf',
                                   '/etc/openvpn/duo_openvpn.conf',
                                   '/etc/duo_openvpn.conf'
                               ]):
            self.main_object = DuoOpenVPN()
        try:
            self.normal_user = self.main_object.configfile.get(
                'testing', 'normal_user')
        except (configparser.NoOptionError,
                configparser.NoSectionError):  # pragma: no cover
            return self.skipTest('No testing/normal_user defined')
        try:
            self.deep_test_rawauth = self.main_object.configfile.getboolean(
                'testing', 'deep_testing_rawauth')
        except (configparser.NoOptionError,
                configparser.NoSectionError):  # pragma: no cover
            self.deep_test_rawauth = False
        try:
            self.deep_test_mfa = self.main_object.configfile.getboolean(
                'testing', 'deep_testing_mfa')
        except (configparser.NoOptionError,
                configparser.NoSectionError):  # pragma: no cover
            self.deep_test_mfa = False
        try:
            self.deep_test_main = self.main_object.configfile.getboolean(
                'testing', 'deep_testing_mainauth')
        except (configparser.NoOptionError,
                configparser.NoSectionError):  # pragma: no cover
            self.deep_test_main = False
        #
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = self.normal_user
        user_creds = dict()
        for varname in OpenVPNCredentials.DUO_RESERVED_WORDS:
            os.environ['password'] = varname
            res = OpenVPNCredentials()
            res.load_variables_from_environment()
            user_creds[varname] = res
        self.user_data = user_creds
        #
        self.library = DuoAPIAuth(**self.main_object.duo_client_args)

    def tearDown(self):
        """ Clear the env so we don't impact other tests """
        for varname in ['common_name', 'password', 'username', 'untrusted_ip']:
            if varname in os.environ:
                del os.environ[varname]

    def test_init(self):
        """ Verify init does the right thing """
        self.assertIsInstance(
            self.library, duo_client.Auth,
            'our DuoAPIAuth must be a descendant '
            'of duo_client.Auth')
        self.assertIsInstance(self.library.hostname, str,
                              'hostname must be set and a string')
        self.assertIsInstance(self.library._fail_open, bool,
                              '_fail_open must return a bool')
        self.assertIsNone(self.library.user_config,
                          'user_config must be empty on a plain init')
        self.assertIsNone(self.library.log_func,
                          'log_func must default to None')

    def test_fail_open_00(self):
        """ _fail_open performs as expected """
        res = self.library._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertFalse(res, '_fail_open defaults to False')

    def test_fail_open_01(self):
        """ _fail_open performs as expected on a True """
        tmplibrary = DuoAPIAuth(fail_open=True,
                                **self.main_object.duo_client_args)
        res = tmplibrary._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertTrue(res, '_fail_open must return an expected True')

    def test_fail_open_02(self):
        """ _fail_open performs as expected on a False """
        tmplibrary = DuoAPIAuth(fail_open=False,
                                **self.main_object.duo_client_args)
        res = tmplibrary._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertFalse(res, '_fail_open must return an expected False')

    def test_load_user_to_verify_00(self):
        """ load_user_to_verify can fail for garbage """
        res = self.library.load_user_to_verify({})
        self.assertFalse(res, 'load_user_to_verify must be False '
                         'for junk input')

    def test_load_user_to_verify_01(self):
        """ load_user_to_verify can fail for a bad username """
        setattr(self.user_data['push'], 'username', 0)
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertFalse(
            res, 'load_user_to_verify must be False '
            'for a bad username')

    def test_load_user_to_verify_02(self):
        """ load_user_to_verify can fail for a bad factor """
        setattr(self.user_data['push'], 'factor', 0)
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertFalse(
            res, 'load_user_to_verify must be False '
            'for a bad factor')

    def test_load_user_to_verify_03(self):
        """ load_user_to_verify can succeed and return True """
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertTrue(res, 'load_user_to_verify must be True')

    def test_preflight_01(self):
        """ preflight fails for bad host """
        self.main_object.duo_client_args['host'] = 'badhost'
        tmplibrary = DuoAPIAuth(**self.main_object.duo_client_args)
        # No user data needs to be loaded to be preflighted
        res = tmplibrary._preflight()
        self.assertFalse(res, '_preflight must be False for a bad host')

    def test_preflight_02(self):
        """ preflight fails for bad skey """
        if not self.deep_test_rawauth:  # pragma: no cover
            return self.skipTest('because of .deep_testing preference')
        self.main_object.duo_client_args['skey'] = 'wrong-passcode'
        tmplibrary = DuoAPIAuth(**self.main_object.duo_client_args)
        # No user data needs to be loaded to be preflighted
        res = tmplibrary._preflight()
        self.assertFalse(res, '_preflight must be False for a bad skey')

    def test_preflight_03(self):
        """ preflight works in general """
        # No user data needs to be loaded to be preflighted
        res = self.library._preflight()
        self.assertTrue(res, '_preflight must be True '
                        'for our standard testing')

    def test_preauth_01(self):
        """ preauth that can't work returns None """
        self.main_object.duo_client_args['host'] = 'badhost'
        tmplibrary = DuoAPIAuth(**self.main_object.duo_client_args)
        tmplibrary.load_user_to_verify(self.user_data['push'])
        res = tmplibrary._preauth()
        self.assertIsNone(res, '_preauth on a damaged server returns None')

    def test_preauth_02(self):
        """ preauth errors on blank users """
        setattr(self.user_data['push'], 'username', '')
        self.library.load_user_to_verify(self.user_data['push'])
        res = self.library._preauth()
        self.assertIsNone(res, '_preauth for a blank user must return None')

    def test_preauth_03(self):
        """ preauth wants to enroll for unknown users """
        setattr(self.user_data['push'], 'username', 'baddie-bad-username')
        self.library.load_user_to_verify(self.user_data['push'])
        res = self.library._preauth()
        self.assertIsInstance(res, dict,
                              '_preauth returns a dict upon success')
        self.assertIn('result', res,
                      '_preauth return must have a "result" key')
        self.assertIn(res['result'], ['deny', 'enroll'],
                      ('_preauth for an unknown user '
                       'must return "deny" or "enroll"'))

    def test_preauth_04(self):
        """ preauth wants to auth standard user """
        self.library.load_user_to_verify(self.user_data['push'])
        res = self.library._preauth()
        self.assertIsInstance(res, dict,
                              '_preauth returns a dict upon success')
        self.assertIn('result', res,
                      '_preauth return must have a "result" key')
        self.assertEqual(res['result'], 'auth',
                         '_preauth for an known person must return "auth"')

    # We do not preauth-test a user that should only have 1FA.
    # The code should intercept a user and bypass checking before
    # getting to this point.  If you're into preauth, you've lost.

    def test_auth_00(self):
        """ _auth with garbage factor fails """
        setattr(self.user_data['auto'], 'factor', 'junk')
        self.library.load_user_to_verify(self.user_data['auto'])
        res = self.library._auth()
        self.assertIsNone(res, '_auth with junk factor must return None')

    def test_mfa_00(self):
        """ _do_mfa_for_user with garbage factor fails """
        setattr(self.user_data['auto'], 'factor', 'junk')
        self.library.load_user_to_verify(self.user_data['auto'])
        res = self.library._do_mfa_for_user()
        self.assertFalse(
            res, '_do_mfa_for_user with junk factor '
            'must return False')

    def test_auth_02(self):
        """ _auth with sms fails """
        self.library.load_user_to_verify(self.user_data['sms'])
        # We test this even with an incapable device.  It should just fail.
        res = self.library._auth()
        self.assertIsNone(res, '_auth with "sms" factor must return None')

    def test_mfa_02(self):
        """ _do_mfa_for_user with sms fails """
        self.library.load_user_to_verify(self.user_data['sms'])
        # We test this even with an incapable device.  It should just fail.
        res = self.library._do_mfa_for_user()
        self.assertFalse(
            res, '_do_mfa_for_user with "sms" factor '
            'must return False')

    def _can_we_run_a_test(self, testcase):
        res = self.library._preauth()
        for device in res['devices']:
            if 'capabilities' in device:
                if testcase in device['capabilities']:
                    return True
        return False

    def _auth_testing_run(self, testcase, answer):
        if not self.deep_test_rawauth:  # pragma: no cover
            return self.skipTest('because of .deep_testing preference')
        self.library.load_user_to_verify(self.user_data[testcase])
        if testcase != 'passcode' and not self._can_we_run_a_test(testcase):
            return self.skipTest(
                'incapable device for {tc}'.format(tc=testcase))
        res = self.library._auth()
        self.assertIsInstance(res, dict, '_auth must return a dict')
        self.assertIn('result', res, '_auth return must have a "result" key')
        self.assertIn(res['result'], ['allow', 'deny'],
                      '_auth result must be "allow" or "deny"')
        self.assertEqual(res['result'], answer,
                         '_auth result must be "{ans}"'.format(ans=answer))

    def _mfa_testing_run(self, testcase, answer):
        if not self.deep_test_mfa:  # pragma: no cover
            return self.skipTest('because of .deep_testing preference')
        self.library.load_user_to_verify(self.user_data[testcase])
        if testcase != 'passcode' and not self._can_we_run_a_test(testcase):
            return self.skipTest(
                'incapable device for {tc}'.format(tc=testcase))
        res = self.library._do_mfa_for_user()
        self.assertIsInstance(res, bool, '_do_mfa_for_user must return a bool')
        self.assertEqual(
            res, answer, '_do_mfa_for_user result must '
            'be "{ans}"'.format(ans=answer))

    def test_auth_03(self):
        """ _auth with auto - PLEASE ALLOW """
        return self._auth_testing_run('auto', 'allow')

    def test_mfa_03(self):
        """ _do_mfa_for_user with auto - PLEASE ALLOW """
        return self._mfa_testing_run('auto', True)

    def test_auth_04(self):
        """ _auth with phone - PLEASE ALLOW """
        return self._auth_testing_run('phone', 'allow')

    def test_mfa_04(self):
        """ _do_mfa_for_user with phone - PLEASE ALLOW """
        return self._mfa_testing_run('phone', True)

    def test_auth_05(self):
        """ _auth with push - PLEASE ALLOW """
        return self._auth_testing_run('push', 'allow')

    def test_mfa_05(self):
        """ _do_mfa_for_user with push - PLEASE ALLOW """
        return self._mfa_testing_run('push', True)

    def test_auth_06(self):
        """ _auth with VALID PASSCODE """
        if not self.deep_test_rawauth:  # pragma: no cover
            return self.skipTest('because of .deep_testing preference')
        passcode = six.moves.input('enter a valid passcode: ')
        os.environ['password'] = passcode
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.user_data['passcode'] = creds
        return self._auth_testing_run('passcode', 'allow')

    def test_mfa_06(self):
        """ _auth with VALID PASSCODE """
        if not self.deep_test_mfa:  # pragma: no cover
            return self.skipTest('because of .deep_testing preference')
        passcode = six.moves.input('enter a valid passcode: ')
        os.environ['password'] = passcode
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.user_data['passcode'] = creds
        return self._mfa_testing_run('passcode', True)

    def test_auth_13(self):
        """ _auth with auto - PLEASE DENY """
        return self._auth_testing_run('auto', 'deny')

    def test_mfa_13(self):
        """ _do_mfa_for_user with auto - PLEASE DENY """
        return self._mfa_testing_run('auto', False)

    def test_auth_14(self):
        """ _auth with phone - PLEASE DENY """
        return self._auth_testing_run('phone', 'deny')

    def test_mfa_14(self):
        """ _do_mfa_for_user with phone - PLEASE DENY """
        return self._mfa_testing_run('phone', False)

    def test_auth_15(self):
        """ _auth with push - PLEASE DENY """
        return self._auth_testing_run('push', 'deny')

    def test_mfa_15(self):
        """ _do_mfa_for_user with push - PLEASE DENY """
        return self._mfa_testing_run('push', False)

    def test_auth_16(self):
        """ _auth with INVALID PASSCODE """
        # We are going to play the 1-in-a-million odds here and save
        # a click.  Change up the lines if you hate this.
        # if not self.deep_test_rawauth:
        #     return self.skipTest('because of .deep_testing preference')
        # passcode = six.moves.input('enter an invalid passcode: ')
        passcode = '000000'
        os.environ['password'] = passcode
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.user_data['passcode'] = creds
        return self._auth_testing_run('passcode', 'deny')

    def test_mfa_16(self):
        """ _do_mfa_for_user with INVALID PASSCODE """
        # We are going to play the 1-in-a-million odds here and save
        # a click.  Change up the lines if you hate this.
        # if not self.deep_test_mfa:
        #     return self.skipTest('because of .deep_testing preference')
        # passcode = six.moves.input('enter an invalid passcode: ')
        passcode = '000000'
        os.environ['password'] = passcode
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.user_data['passcode'] = creds
        return self._mfa_testing_run('passcode', False)

    # A word on testing main_auth.
    # main_auth has exceptions in it, for handling the cases of a server
    # failing open, and bad runtime responses coming out of it.  We're
    # not doing a full test in here, because the sheer number of options
    # we'd have to test would be INSANE.  The subtests will have to handle
    # some of this, and trust that you'll eyeball the function for "if
    # we're broken at runtime, that 'if' statement near the top will save us.
    # As such, we are mostly testing the return fields coming out of
    # _auth, and that we're getting the right answers.  For the most part,
    # all of these checks will look uncannily similar to the _auth and
    # _mfa checks above.

    def test_main_00(self):
        """ main_auth with garbage factor fails """
        setattr(self.user_data['auto'], 'factor', 'junk')
        self.library.load_user_to_verify(self.user_data['auto'])
        res = self.library.main_auth()
        self.assertFalse(res, 'main_auth with junk factor '
                         'must return False')

    def test_main_01(self):
        """ main_auth with no connection goes fail_open """
        self.main_object.duo_client_args['host'] = 'badhost'
        for state in (True, False):
            tmplibrary = DuoAPIAuth(fail_open=state,
                                    **self.main_object.duo_client_args)
            tmplibrary.load_user_to_verify(self.user_data['auto'])
            res = tmplibrary.main_auth()
            self.assertEqual(
                res, state, 'main_auth with no connection must return '
                'the fail_open state')

    def test_main_02(self):
        """ main_auth with sms fails """
        self.library.load_user_to_verify(self.user_data['sms'])
        # We test this even with an incapable device.  It should just fail.
        res = self.library.main_auth()
        self.assertFalse(res, 'main_auth with "sms" factor '
                         'must return False')

    def _main_testing_run(self, testcase, answer):
        if not self.deep_test_main:  # pragma: no cover
            return self.skipTest('because of .deep_testing preference')
        self.library.load_user_to_verify(self.user_data[testcase])
        if testcase != 'passcode' and not self._can_we_run_a_test(testcase):
            return self.skipTest(
                'incapable device for {tc}'.format(tc=testcase))
        res = self.library.main_auth()
        self.assertIsInstance(res, bool, 'main_auth must return a bool')
        self.assertEqual(
            res, answer, 'main_auth result must '
            'be "{ans}"'.format(ans=answer))

    def test_main_03(self):
        """ main_auth with auto - PLEASE ALLOW """
        return self._main_testing_run('auto', True)

    def test_main_04(self):
        """ main_auth with phone - PLEASE ALLOW """
        return self._main_testing_run('phone', True)

    def test_main_05(self):
        """ main_auth with push - PLEASE ALLOW """
        return self._main_testing_run('push', True)

    def test_main_06(self):
        """ _auth with VALID PASSCODE """
        if not self.deep_test_main:  # pragma: no cover
            return self.skipTest('because of .deep_testing preference')
        passcode = six.moves.input('enter a valid passcode: ')
        os.environ['password'] = passcode
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.user_data['passcode'] = creds
        return self._main_testing_run('passcode', True)

    def test_main_13(self):
        """ main_auth with auto - PLEASE DENY """
        return self._main_testing_run('auto', False)

    def test_main_14(self):
        """ main_auth with phone - PLEASE DENY """
        return self._main_testing_run('phone', False)

    def test_main_15(self):
        """ main_auth with push - PLEASE DENY """
        return self._main_testing_run('push', False)

    def test_main_16(self):
        """ main_auth with INVALID PASSCODE """
        # We are going to play the 1-in-a-million odds here and save
        # a click.  Change up the lines if you hate this.
        # if not self.deep_test_main:
        #     return self.skipTest('because of .deep_testing preference')
        # passcode = six.moves.input('enter an invalid passcode: ')
        passcode = '000000'
        os.environ['password'] = passcode
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.user_data['passcode'] = creds
        return self._main_testing_run('passcode', False)

    def test_log_good(self):
        """ Test sending a log message - all good """
        # There is no raise or return.  We're just poking at
        # the function and making sure it doesn't raise.
        tmplibrary = DuoAPIAuth(log_func=self.main_object.log,
                                **self.main_object.duo_client_args)
        tmplibrary.log(
            summary='TEST message',
            severity='DEBUG',
        )