import logging
import os
from typing import List
import unittest
from unittest.mock import Mock

from flask import Flask
from flask_appbuilder import AppBuilder, SQLA
from flask_appbuilder.security.manager import AUTH_LDAP
from flask_appbuilder.security.sqla.models import User
from flask_appbuilder.tests.const import USERNAME_ADMIN, USERNAME_READONLY
import jinja2
import ldap


logging.basicConfig(format="%(asctime)s:%(levelname)s:%(name)s:%(message)s")
logging.getLogger().setLevel(logging.DEBUG)
log = logging.getLogger(__name__)


class LDAPSearchTestCase(unittest.TestCase):
    def setUp(self):
        # start MockLdap
        # start Flask
        self.app = Flask(__name__)
        self.app.jinja_env.undefined = jinja2.StrictUndefined
        self.app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
            "SQLALCHEMY_DATABASE_URI"
        )
        self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
        self.app.config["AUTH_TYPE"] = AUTH_LDAP
        self.app.config["AUTH_LDAP_SERVER"] = "ldap://localhost:1389/"
        self.app.config["AUTH_LDAP_UID_FIELD"] = "uid"
        self.app.config["AUTH_LDAP_FIRSTNAME_FIELD"] = "givenName"
        self.app.config["AUTH_LDAP_LASTNAME_FIELD"] = "sn"
        self.app.config["AUTH_LDAP_EMAIL_FIELD"] = "mail"

        # start Database
        self.db = SQLA(self.app)

    def tearDown(self):
        # Remove test user
        user_alice = self.appbuilder.sm.find_user("alice")
        if user_alice:
            self.db.session.delete(user_alice)
            self.db.session.commit()
        user_natalie = self.appbuilder.sm.find_user("natalie")
        if user_natalie:
            self.db.session.delete(user_natalie)
            self.db.session.commit()

        # stop Flask
        self.app = None

        # stop Flask-AppBuilder
        self.appbuilder = None

        # stop Database
        self.db.session.remove()
        self.db = None

    def assertOnlyDefaultUsers(self):
        users = self.appbuilder.sm.get_all_users()
        user_names = [user.username for user in users]
        self.assertEqual(user_names, [USERNAME_ADMIN, USERNAME_READONLY])

    def assertUserContainsRoles(self, user: User, role_names: List[str]):
        user_role_names = sorted([role.name for role in user.roles])
        self.assertListEqual(user_role_names, sorted(role_names))

    # ----------------
    # Unit Tests
    # ----------------
    def test___search_ldap(self):
        """
        LDAP: test `_search_ldap` method
        """
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # prepare `con` object
        con = ldap.initialize("ldap://localhost:1389/")
        sm._ldap_bind_indirect(ldap, con)

        # run `_search_ldap` method
        user_dn, user_attributes = sm._search_ldap(ldap, con, "alice")

        # validate - search returned expected data
        self.assertEqual(user_dn, "cn=alice,ou=users,dc=example,dc=org")
        self.assertEqual(user_attributes["givenName"], [b"Alice"])
        self.assertEqual(user_attributes["sn"], [b"Doe"])
        self.assertEqual(user_attributes["mail"], [b"alice@example.org"])

    def test___search_ldap_filter(self):
        """
        LDAP: test `_search_ldap` method (with AUTH_LDAP_SEARCH_FILTER)
        """
        # MockLdap needs non-bytes for search filters, so we patch `memberOf`
        # to a string, only for this test

        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config[
            "AUTH_LDAP_SEARCH_FILTER"
        ] = "(memberOf=cn=staff,ou=groups,dc=example,dc=org)"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # prepare `con` object
        con = ldap.initialize("ldap://localhost:1389/")
        sm._ldap_bind_indirect(ldap, con)

        # run `_search_ldap` method
        user_dn, user_attributes = sm._search_ldap(ldap, con, "alice")

        # validate - search returned expected data
        self.assertEqual(user_dn, "cn=alice,ou=users,dc=example,dc=org")
        self.assertEqual(user_attributes["givenName"], [b"Alice"])
        self.assertEqual(user_attributes["sn"], [b"Doe"])
        self.assertEqual(user_attributes["mail"], [b"alice@example.org"])

    def test___search_ldap_with_search_referrals(self):
        """
        LDAP: test `_search_ldap` method w/returned search referrals
        """
        self.app.config["AUTH_LDAP_BIND_USER"] = "uid=admin,ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        user_alice = (
            "cn=alice,ou=users,dc=example,dc=org",
            {
                "uid": ["alice"],
                "userPassword": ["alice_password"],
                "memberOf": [b"cn=staff,ou=groups,o=test"],
                "givenName": [b"Alice"],
                "sn": [b"Doe"],
                "mail": [b"alice@example.org"],
            },
        )
        # run `_search_ldap` method w/mocked ldap connection
        mock_con = Mock()
        mock_con.search_s.return_value = [
            (
                None,
                [
                    "ldap://ForestDnsZones.mycompany.com/"
                    "DC=ForestDnsZones,DC=mycompany,DC=com"
                ],
            ),
            user_alice,
            (None, ["ldap://mycompany.com/CN=Configuration,DC=mycompany,DC=com"]),
        ]
        user_dn, user_attributes = sm._search_ldap(ldap, mock_con, "alice")

        # validate - search returned expected data
        self.assertEqual(user_dn, user_alice[0])
        self.assertEqual(user_attributes["givenName"], user_alice[1]["givenName"])
        self.assertEqual(user_attributes["sn"], user_alice[1]["sn"])
        self.assertEqual(user_attributes["mail"], user_alice[1]["mail"])

        mock_con.search_s.assert_called()

    def test__missing_credentials(self):
        """
        LDAP: test login flow for - missing credentials
        """
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # validate - login failure (missing username)
        self.assertIsNone(sm.auth_user_ldap(None, "password"))
        self.assertIsNone(sm.auth_user_ldap("", "password"))

        # validate - login failure (missing password)
        self.assertIsNone(sm.auth_user_ldap("username", None))
        self.assertIsNone(sm.auth_user_ldap("username", ""))

        # validate - login failure (missing username/password)
        self.assertIsNone(sm.auth_user_ldap(None, None))
        self.assertIsNone(sm.auth_user_ldap("", None))
        self.assertIsNone(sm.auth_user_ldap("", ""))
        self.assertIsNone(sm.auth_user_ldap(None, ""))

        # validate - no users were created
        self.assertOnlyDefaultUsers()

    def test__active_user(self):
        """
        LDAP: test login flow for - inactive user
        """
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.com",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # set user inactive
        new_user.active = True

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was not allowed to log in
        self.assertIsNotNone(user)

    def test__inactive_user(self):
        """
        LDAP: test login flow for - inactive user
        """
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.com",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # set user inactive
        new_user.active = False

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was not allowed to log in
        self.assertIsNone(user)

    def test__multi_group_user_mapping_to_same_role(self):
        """
        LDAP: test login flow for - user in multiple groups mapping to same role
        """
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin"],
            "cn=readers,ou=groups,dc=example,dc=org": ["User"],
        }
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("natalie", "natalie_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # validate - user was given the correct roles
        self.assertUserContainsRoles(user, ["Public", "User"])

        # validate - user was given the correct attributes (read from LDAP)
        self.assertEqual(user.first_name, "Natalie")
        self.assertEqual(user.last_name, "Smith")
        self.assertEqual(user.email, "natalie@example.org")

    def test__direct_bind__unregistered(self):
        """
        LDAP: test login flow for - direct bind - unregistered user
        """
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # validate - user was given the AUTH_USER_REGISTRATION_ROLE role
        self.assertEqual(user.roles, [sm.find_role("Public")])

        # validate - user was given the correct attributes (read from LDAP)
        self.assertEqual(user.first_name, "Alice")
        self.assertEqual(user.last_name, "Doe")
        self.assertEqual(user.email, "alice@example.org")

    def test__direct_bind__unregistered__no_self_register(self):
        """
        LDAP: test login flow for - direct bind - unregistered user - no self-registration
        """
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_USER_REGISTRATION"] = False
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was not allowed to log in
        self.assertIsNone(user)

        # validate - no users were registered
        self.assertOnlyDefaultUsers()

    def test__direct_bind__unregistered__no_search(self):
        """
        LDAP: test login flow for - direct bind - unregistered user - no ldap search
        """
        self.app.config["AUTH_LDAP_SEARCH"] = None
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was NOT allowed to log in (because registration requires search)
        self.assertIsNone(user)

    def test__direct_bind__registered(self):
        """
        LDAP: test login flow for - direct bind - registered user
        """
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

    def test__direct_bind__registered__no_search(self):
        """
        LDAP: test login flow for - direct bind - registered user - no ldap search
        """
        self.app.config["AUTH_LDAP_SEARCH"] = None
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in (because they are already registered)
        self.assertIsInstance(user, sm.user_model)

    def test__indirect_bind__unregistered(self):
        """
        LDAP: test login flow for - indirect bind - unregistered user
        """
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # validate - user was given the AUTH_USER_REGISTRATION_ROLE role
        self.assertListEqual(user.roles, [sm.find_role("Public")])

        # validate - user was given the correct attributes (read from LDAP)
        self.assertEqual(user.first_name, "Alice")
        self.assertEqual(user.last_name, "Doe")
        self.assertEqual(user.email, "alice@example.org")

    def test__indirect_bind__unregistered__no_self_register(self):
        """
        LDAP: test login flow for - indirect bind - unregistered user - no self-registration
        """  # noqa
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_USER_REGISTRATION"] = False
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was not allowed to log in
        self.assertIsNone(user)

        # validate - no users were registered
        self.assertOnlyDefaultUsers()

    def test__indirect_bind__unregistered__no_search(self):
        """
        LDAP: test login flow for - indirect bind - unregistered user - no ldap search
        """
        self.app.config["AUTH_LDAP_SEARCH"] = None
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was NOT allowed to log in
        # (because indirect bind requires search)
        self.assertIsNone(user)

    def test__indirect_bind__registered(self):
        """
        LDAP: test login flow for - indirect bind - registered user
        """
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

    def test__indirect_bind__registered__no_search(self):
        """
        LDAP: test login flow for - indirect bind - registered user - no ldap search
        """
        self.app.config["AUTH_LDAP_SEARCH"] = None
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was NOT allowed to log in
        # (because indirect bind requires search)
        self.assertIsNone(user)

    def test__direct_bind__unregistered__single_role(self):
        """
        LDAP: test login flow for - direct bind - unregistered user - single role mapping
        """
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin"]
        }
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # validate - user was given the correct roles
        self.assertUserContainsRoles(user, ["Admin", "Public"])

        # validate - user was given the correct attributes (read from LDAP)
        self.assertEqual(user.first_name, "Alice")
        self.assertEqual(user.last_name, "Doe")
        self.assertEqual(user.email, "alice@example.org")

    def test__direct_bind__unregistered__multi_role(self):
        """
        LDAP: test login flow for - direct bind - unregistered user - multi role mapping
        """
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"]
        }
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # validate - user was given the correct roles
        self.assertUserContainsRoles(user, ["Admin", "Public", "User"])

        # validate - user was given the correct attributes (read from LDAP)
        self.assertEqual(user.first_name, "Alice")
        self.assertEqual(user.last_name, "Doe")
        self.assertEqual(user.email, "alice@example.org")

    def test__direct_bind__registered__multi_role__no_role_sync(self):
        """
        LDAP: test login flow for - direct bind - registered user - multi role mapping - no login role-sync
        """  # noqa
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"]
        }
        self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = False
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was given no roles
        self.assertListEqual(user.roles, [])

    def test__direct_bind__registered__multi_role__with_role_sync(self):
        """
        LDAP: test login flow for - direct bind - registered user - multi role mapping - with login role-sync
        """  # noqa
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"]
        }
        self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = True
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was given the correct roles
        self.assertUserContainsRoles(user, ["Admin", "User"])

    def test__indirect_bind__unregistered__single_role(self):
        """
        LDAP: test login flow for - indirect bind - unregistered user - single role mapping
        """  # noqa
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["User"]
        }
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # validate - user was given the correct roles
        self.assertUserContainsRoles(user, ["Public", "User"])

        # validate - user was given the correct attributes (read from LDAP)
        self.assertEqual(user.first_name, "Alice")
        self.assertEqual(user.last_name, "Doe")
        self.assertEqual(user.email, "alice@example.org")

    def test__indirect_bind__unregistered__multi_role(self):
        """
        LDAP: test login flow for - indirect bind - unregistered user - multi role mapping
        """
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"]
        }
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # validate - user was given the correct roles
        self.assertUserContainsRoles(user, ["User", "Public", "Admin"])

        # validate - user was given the correct attributes (read from LDAP)
        self.assertEqual(user.first_name, "Alice")
        self.assertEqual(user.last_name, "Doe")
        self.assertEqual(user.email, "alice@example.org")

    def test__indirect_bind__registered__multi_role__no_role_sync(self):
        """
        LDAP: test login flow for - indirect bind - registered user - multi role mapping - no login role-sync
        """  # noqa
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"]
        }
        self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = False
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was given no roles
        self.assertListEqual(user.roles, [])

    def test__indirect_bind__registered__multi_role__with_role_sync(self):
        """
        LDAP: test login flow for - indirect bind - registered user - multi role mapping - with login role-sync
        """  # noqa
        self.app.config["AUTH_ROLES_MAPPING"] = {
            "cn=staff,ou=groups,dc=example,dc=org": ["Admin", "User"]
        }
        self.app.config["AUTH_ROLES_SYNC_AT_LOGIN"] = True
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_USER"] = "cn=admin,dc=example,dc=org"
        self.app.config["AUTH_LDAP_BIND_PASSWORD"] = "admin_password"
        self.appbuilder = AppBuilder(self.app, self.db.session)
        sm = self.appbuilder.sm

        # add User role
        sm.add_role("User")

        # validate - no users are registered
        self.assertOnlyDefaultUsers()

        # register a user
        new_user = sm.add_user(  # noqa
            username="alice",
            first_name="Alice",
            last_name="Doe",
            email="alice@example.org",
            role=[],
        )

        # validate - user was registered
        self.assertEqual(len(sm.get_all_users()), 3)

        # attempt login
        user = sm.auth_user_ldap("alice", "alice_password")

        # validate - user was allowed to log in
        self.assertIsInstance(user, sm.user_model)

        # validate - user was given the correct roles
        self.assertUserContainsRoles(user, ["User", "Admin"])

    def test_login_failed_keep_next_url(self):
        """
        LDAP: Keeping next url after failed login attempt
        """
        self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,dc=example,dc=org"
        self.app.config[
            "AUTH_LDAP_USERNAME_FORMAT"
        ] = "cn=%s,ou=users,dc=example,dc=org"
        self.app.config["AUTH_USER_REGISTRATION"] = True
        self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
        self.app.config["WTF_CSRF_ENABLED"] = False
        self.app.config["SECRET_KEY"] = "thisismyscretkey"

        self.appbuilder = AppBuilder(self.app, self.db.session)
        client = self.app.test_client()
        client.get("/logout/")

        response = client.post(
            "/login/?next=/users/userinfo/",
            data=dict(username="natalie", password="wrong_natalie_password"),
            follow_redirects=False,
        )
        response = client.post(
            response.location,
            data=dict(username="natalie", password="natalie_password"),
            follow_redirects=False,
        )
        assert response.location == "/users/userinfo/"
