コード例 #1
0
def test_set_groups(app, slapd_connection, user, foo_group, bar_group):
    with app.app_context():
        Group.ldap_object_attributes(conn=slapd_connection)
        User.ldap_object_attributes(conn=slapd_connection)

        user = User.get(dn=user.dn, conn=slapd_connection)
        user.load_groups(conn=slapd_connection)
        assert set(Group.available_groups(conn=slapd_connection)) == {
            ("foo", foo_group.dn),
            ("bar", bar_group.dn),
        }
        foo_dns = {g.dn for g in foo_group.get_members(conn=slapd_connection)}
        assert user.dn in foo_dns
        assert user.groups[0].dn == foo_group.dn

        user.set_groups([foo_group, bar_group], conn=slapd_connection)

        bar_dns = {g.dn for g in bar_group.get_members(conn=slapd_connection)}
        assert user.dn in bar_dns
        assert user.groups[1].dn == bar_group.dn

        user.set_groups([foo_group], conn=slapd_connection)

        foo_dns = {g.dn for g in foo_group.get_members(conn=slapd_connection)}
        bar_dns = {g.dn for g in bar_group.get_members(conn=slapd_connection)}
        assert user.dn in foo_dns
        assert user.dn not in bar_dns
コード例 #2
0
def test_custom_config_format_claim_is_well_formated(testclient,
                                                     slapd_connection, user):
    User.ldap_object_classes(slapd_connection)
    jwt_mapping_config = DEFAULT_JWT_MAPPING_CONFIG.copy()
    jwt_mapping_config["EMAIL"] = "{{ user.uid[0] }}@mydomain.tld"

    with testclient.app.app_context():
        data = generate_user_claims(user, STANDARD_CLAIMS, jwt_mapping_config)

    assert data["email"] == "*****@*****.**"
コード例 #3
0
def test_claim_is_omitted_if_empty(testclient, slapd_connection, user):
    # According to https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
    # it's better to not insert a null or empty string value
    User.ldap_object_classes(slapd_connection)
    user.mail = ""
    user.save(slapd_connection)

    with testclient.app.app_context():
        data = generate_user_claims(user, STANDARD_CLAIMS,
                                    DEFAULT_JWT_MAPPING_CONFIG)

    assert "email" not in data
コード例 #4
0
ファイル: test_profile.py プロジェクト: yaal-fr/canaille
def test_photo_edition(
    testclient,
    slapd_server,
    slapd_connection,
    logged_user,
    jpeg_photo,
):

    # Add a photo
    res = testclient.get("/profile/user", status=200)
    res.form["jpegPhoto"] = Upload("logo.jpg", jpeg_photo)
    res.form["jpegPhoto_delete"] = False
    res = res.form.submit(name="action", value="edit", status=200)
    assert "Profile updated successfuly." in res, str(res)

    with testclient.app.app_context():
        logged_user = User.get(dn=logged_user.dn, conn=slapd_connection)

    assert [jpeg_photo] == logged_user.jpegPhoto

    # No change
    res = testclient.get("/profile/user", status=200)
    res.form["jpegPhoto_delete"] = False
    res = res.form.submit(name="action", value="edit", status=200)
    assert "Profile updated successfuly." in res, str(res)

    with testclient.app.app_context():
        logged_user = User.get(dn=logged_user.dn, conn=slapd_connection)

    assert [jpeg_photo] == logged_user.jpegPhoto

    # Photo deletion
    res = testclient.get("/profile/user", status=200)
    res.form["jpegPhoto_delete"] = True
    res = res.form.submit(name="action", value="edit", status=200)
    assert "Profile updated successfuly." in res, str(res)

    with testclient.app.app_context():
        logged_user = User.get(dn=logged_user.dn, conn=slapd_connection)

    assert [] == logged_user.jpegPhoto

    # Photo deletion AND upload, this should never happen
    res = testclient.get("/profile/user", status=200)
    res.form["jpegPhoto"] = Upload("logo.jpg", jpeg_photo)
    res.form["jpegPhoto_delete"] = True
    res = res.form.submit(name="action", value="edit", status=200)
    assert "Profile updated successfuly." in res, str(res)

    with testclient.app.app_context():
        logged_user = User.get(dn=logged_user.dn, conn=slapd_connection)

    assert [] == logged_user.jpegPhoto
コード例 #5
0
def test_custom_format_claim_is_formatted_with_empty_value_and_not_omitted(
        testclient, slapd_connection, user):
    # If the jwt mapping config is customized, it's not canaille's responsability to verify value consistency when one user attribute is not set or null.
    # Attribute field is left empty in the formatted string.
    User.ldap_object_classes(slapd_connection)
    jwt_mapping_config = DEFAULT_JWT_MAPPING_CONFIG.copy()
    jwt_mapping_config["EMAIL"] = "{{ user.givenName[0] }}@mydomain.tld"

    with testclient.app.app_context():
        data = generate_user_claims(user, STANDARD_CLAIMS, jwt_mapping_config)

    assert data["email"] == "@mydomain.tld"
コード例 #6
0
def test_generate_user_standard_claims_with_default_config(
        testclient, slapd_connection, user):
    User.ldap_object_classes(slapd_connection)

    with testclient.app.app_context():
        data = generate_user_claims(user, STANDARD_CLAIMS,
                                    DEFAULT_JWT_MAPPING_CONFIG)

    assert data == {
        "name": "John (johnny) Doe",
        "family_name": "Doe",
        "email": "*****@*****.**",
        "sub": "user",
    }
コード例 #7
0
ファイル: test_invitation.py プロジェクト: yaal-fr/canaille
def test_invitation(testclient, slapd_connection, logged_admin, foo_group,
                    smtpd):
    with testclient.app.app_context():
        assert User.get("someone", conn=slapd_connection) is None

    res = testclient.get("/invite", status=200)

    res.form["uid"] = "someone"
    res.form["uid_editable"] = False
    res.form["mail"] = "*****@*****.**"
    res.form["groups"] = [foo_group.dn]
    res = res.form.submit(name="action", value="send", status=200)
    assert len(smtpd.messages) == 1

    url = res.pyquery("#copy-text")[0].value

    # logout
    with testclient.session_transaction() as sess:
        del sess["user_dn"]

    res = testclient.get(url, status=200)

    assert res.form["uid"].value == "someone"
    assert res.form["uid"].attrs["readonly"]
    assert res.form["mail"].value == "*****@*****.**"
    assert res.form["groups"].value == [foo_group.dn]

    res.form["password1"] = "whatever"
    res.form["password2"] = "whatever"
    res.form["givenName"] = "George"
    res.form["sn"] = "Abitbol"

    res = res.form.submit(status=302)
    res = res.follow(status=200)

    assert "You account has been created successfuly." in res

    with testclient.app.app_context():
        user = User.get("someone", conn=slapd_connection)
        user.load_groups(conn=slapd_connection)
        foo_group.reload(slapd_connection)
        assert user.check_password("whatever")
        assert user.groups == [foo_group]

    with testclient.session_transaction() as sess:
        assert "user_dn" in sess
        del sess["user_dn"]

    res = testclient.get(url, status=302)
コード例 #8
0
ファイル: test_invitation.py プロジェクト: yaal-fr/canaille
def test_registration_no_password(testclient, slapd_connection, foo_group):
    with testclient.app.app_context():
        invitation = Invitation(
            datetime.now().isoformat(),
            "someoneelse",
            False,
            "*****@*****.**",
            foo_group.dn,
        )
        hash = invitation.profile_hash()
        b64 = invitation.b64()
        url = f"/register/{b64}/{hash}"

    res = testclient.get(url, status=200)
    assert "required" in res.form["password1"].attrs
    assert "required" in res.form["password2"].attrs

    res = res.form.submit(status=200)
    assert "This field is required." in res.text, res.text

    with testclient.app.app_context():
        assert not User.get("someoneelse", conn=slapd_connection)

    with testclient.session_transaction() as sess:
        assert "user_dn" not in sess
コード例 #9
0
def test_admin_self_deletion(testclient, slapd_connection):
    User.ldap_object_classes(slapd_connection)
    admin = User(
        objectClass=["inetOrgPerson"],
        cn="Temp admin",
        sn="admin",
        uid="temp",
        mail="*****@*****.**",
        userPassword="******",
    )
    admin.save(slapd_connection)
    with testclient.session_transaction() as sess:
        sess["user_dn"] = [admin.dn]

    res = testclient.get("/profile/temp")
    res = (
        res.form.submit(name="action", value="delete", status=302)
        .follow(status=302)
        .follow(status=200)
    )

    with testclient.app.app_context():
        assert User.get("temp", conn=slapd_connection) is None

    with testclient.session_transaction() as sess:
        assert not sess.get("user_dn")
コード例 #10
0
ファイル: test_invitation.py プロジェクト: yaal-fr/canaille
def test_generate_link(testclient, slapd_connection, logged_admin, foo_group,
                       smtpd):
    with testclient.app.app_context():
        assert User.get("sometwo", conn=slapd_connection) is None

    res = testclient.get("/invite", status=200)

    res.form["uid"] = "sometwo"
    res.form["mail"] = "*****@*****.**"
    res.form["groups"] = [foo_group.dn]
    res = res.form.submit(name="action", value="generate", status=200)
    assert len(smtpd.messages) == 0

    url = res.pyquery("#copy-text")[0].value

    # logout
    with testclient.session_transaction() as sess:
        del sess["user_dn"]

    res = testclient.get(url, status=200)

    assert res.form["uid"].value == "sometwo"
    assert res.form["mail"].value == "*****@*****.**"
    assert res.form["groups"].value == [foo_group.dn]

    res.form["password1"] = "whatever"
    res.form["password2"] = "whatever"
    res.form["givenName"] = "George"
    res.form["sn"] = "Abitbol"

    res = res.form.submit(status=302)
    res = res.follow(status=200)

    with testclient.app.app_context():
        user = User.get("sometwo", conn=slapd_connection)
        user.load_groups(conn=slapd_connection)
        foo_group.reload(slapd_connection)
        assert user.check_password("whatever")
        assert user.groups == [foo_group]

    with testclient.session_transaction() as sess:
        assert "user_dn" in sess
        del sess["user_dn"]

    res = testclient.get(url, status=302)
コード例 #11
0
def test_user_self_deletion(testclient, slapd_connection):
    User.ldap_object_classes(slapd_connection)
    user = User(
        objectClass=["inetOrgPerson"],
        cn="Temp user",
        sn="user",
        uid="temp",
        mail="*****@*****.**",
        userPassword="******",
    )
    user.save(slapd_connection)
    with testclient.session_transaction() as sess:
        sess["user_dn"] = [user.dn]

    testclient.app.config["ACL"]["DEFAULT"]["PERMISSIONS"] = []
    res = testclient.get("/profile/temp")
    assert "Delete my account" not in res

    testclient.app.config["ACL"]["DEFAULT"]["PERMISSIONS"] = ["delete_account"]
    res = testclient.get("/profile/temp")
    assert "Delete my account" in res
    res = (
        res.form.submit(name="action", value="delete", status=302)
        .follow(status=302)
        .follow(status=200)
    )

    with testclient.app.app_context():
        assert User.get("temp", conn=slapd_connection) is None

    with testclient.session_transaction() as sess:
        assert not sess.get("user_dn")

    testclient.app.config["ACL"]["DEFAULT"]["PERMISSIONS"] = []
コード例 #12
0
ファイル: test_hybrid_flow.py プロジェクト: yaal-fr/canaille
def test_oauth_hybrid(testclient, slapd_connection, user, client):
    User.ldap_object_attributes(slapd_connection)
    res = testclient.get(
        "/oauth/authorize",
        params=dict(
            response_type="code token",
            client_id=client.client_id,
            scope="openid profile",
            nonce="somenonce",
        ),
        status=200,
    )
    assert "text/html" == res.content_type, res.json

    res.form["login"] = user.name
    res.form["password"] = "******"
    res = res.form.submit(status=302)

    res = res.follow(status=200)
    assert "text/html" == res.content_type, res.json

    res = res.form.submit(name="answer", value="accept", status=302)

    assert res.location.startswith(client.redirect_uris[0])
    params = parse_qs(urlsplit(res.location).fragment)

    code = params["code"][0]
    authcode = AuthorizationCode.get(code, conn=slapd_connection)
    assert authcode is not None

    access_token = params["access_token"][0]
    token = Token.get(access_token, conn=slapd_connection)
    assert token is not None

    res = testclient.get(
        "/oauth/userinfo",
        headers={"Authorization": f"Bearer {access_token}"},
        status=200,
    )
    assert {
        "name": "John (johnny) Doe",
        "family_name": "Doe",
        "sub": "user",
        "groups": [],
    } == res.json
コード例 #13
0
def test_user_without_password_first_login(testclient, slapd_connection):
    User.ldap_object_classes(slapd_connection)
    u = User(
        objectClass=["inetOrgPerson"],
        cn="Temp User",
        sn="Temp",
        uid="temp",
        mail="*****@*****.**",
    )
    u.save(slapd_connection)

    res = testclient.get("/login", status=200)
    res.form["login"] = "******"
    res = res.form.submit(status=302).follow(status=200)

    assert "First login" in res
    u.delete(conn=slapd_connection)
コード例 #14
0
def user(app, slapd_connection):
    User.ldap_object_classes(slapd_connection)
    LDAPObject.ldap_object_attributes(slapd_connection)
    u = User(
        objectClass=["inetOrgPerson"],
        cn="John (johnny) Doe",
        sn="Doe",
        uid="user",
        mail="*****@*****.**",
        userPassword="******",
    )
    u.save(slapd_connection)
    return u
コード例 #15
0
def admin(app, slapd_connection):
    User.ldap_object_classes(slapd_connection)
    LDAPObject.ldap_object_attributes(slapd_connection)
    u = User(
        objectClass=["inetOrgPerson"],
        cn="Jane Doe",
        sn="Doe",
        uid="admin",
        mail="*****@*****.**",
        userPassword="******",
    )
    u.save(slapd_connection)
    return u
コード例 #16
0
def moderator(app, slapd_connection):
    User.ldap_object_classes(slapd_connection)
    LDAPObject.ldap_object_attributes(slapd_connection)
    u = User(
        objectClass=["inetOrgPerson"],
        cn="Jack Doe",
        sn="Doe",
        uid="moderator",
        mail="*****@*****.**",
        userPassword="******",
    )
    u.save(slapd_connection)
    return u
コード例 #17
0
ファイル: test_profile.py プロジェクト: yaal-fr/canaille
def test_field_permissions_none(testclient, slapd_server, slapd_connection,
                                logged_user):
    testclient.get("/profile/user", status=200)
    with testclient.app.app_context():
        logged_user.telephoneNumber = ["555-666-777"]
        logged_user.save(conn=slapd_connection)

    testclient.app.config["ACL"]["DEFAULT"] = {"READ": ["uid"], "WRITE": []}

    res = testclient.get("/profile/user", status=200)
    assert "telephoneNumber" not in res.form.fields

    testclient.post("/profile/user", {
        "action": "edit",
        "telephoneNumber": "000-000-000"
    })
    with testclient.app.app_context():
        user = User.get(dn=logged_user.dn, conn=slapd_connection)
        assert user.telephoneNumber == ["555-666-777"]
コード例 #18
0
def test_user_deleted_in_session(testclient, slapd_connection):
    User.ldap_object_classes(slapd_connection)
    u = User(
        objectClass=["inetOrgPerson"],
        cn="Jake Doe",
        sn="Jake",
        uid="jake",
        mail="*****@*****.**",
        userPassword="******",
    )
    u.save(slapd_connection)
    testclient.get("/profile/jake", status=403)

    with testclient.session_transaction() as session:
        session["user_dn"] = [u.dn]

    testclient.get("/profile/jake", status=200)
    u.delete(conn=slapd_connection)

    testclient.get("/profile/jake", status=403)
    with testclient.session_transaction() as session:
        assert not session.get("user_dn")
コード例 #19
0
ファイル: flaskutils.py プロジェクト: yaal-fr/canaille
def current_user():
    if not session.get("user_dn"):
        return None

    if not isinstance(session.get("user_dn"), list):
        del session["user_dn"]
        return None

    dn = session["user_dn"][-1]
    try:
        user = User.get(dn=dn)
    except ldap.LDAPError:
        return None

    if not user:
        try:
            session["user_dn"] = session["user_dn"][:-1]
        except IndexError:
            del session["user_dn"]

    return user
コード例 #20
0
ファイル: test_profile.py プロジェクト: yaal-fr/canaille
def test_email_reset_button(smtpd, testclient, slapd_connection, logged_admin):
    User.ldap_object_classes(slapd_connection)
    u = User(
        objectClass=["inetOrgPerson"],
        cn="Temp User",
        sn="Temp",
        uid="temp",
        mail="*****@*****.**",
        userPassword=["{SSHA}fw9DYeF/gHTHuVMepsQzVYAkffGcU8Fz"],
    )
    u.save(slapd_connection)

    res = testclient.get("/profile/temp", status=200)
    assert "If the user has forgotten his password" in res, res.text
    assert "Send" in res

    res = res.form.submit(name="action",
                          value="password-reset-mail",
                          status=200)
    assert (
        "A password reset link has been sent at the user email address. It should be received within 10 minutes."
        in res)
    assert "Send again" in res
    assert len(smtpd.messages) == 1
コード例 #21
0
ファイル: test_profile.py プロジェクト: yaal-fr/canaille
def test_edition(
    testclient,
    slapd_server,
    slapd_connection,
    logged_user,
    admin,
    foo_group,
    bar_group,
    jpeg_photo,
):
    res = testclient.get("/profile/user", status=200)
    assert set(res.form["groups"].options) == {
        ("cn=foo,ou=groups,dc=slapd-test,dc=python-ldap,dc=org", True, "foo"),
        ("cn=bar,ou=groups,dc=slapd-test,dc=python-ldap,dc=org", False, "bar"),
    }
    assert logged_user.groups == [foo_group]
    assert foo_group.member == [logged_user.dn]
    assert bar_group.member == [admin.dn]
    assert res.form["groups"].attrs["readonly"]
    assert res.form["uid"].attrs["readonly"]

    res.form["uid"] = "toto"
    res.form["givenName"] = "given_name"
    res.form["sn"] = "family_name"
    res.form["mail"] = "*****@*****.**"
    res.form["telephoneNumber"] = "555-666-777"
    res.form["employeeNumber"] = 666
    res.form["groups"] = [
        "cn=foo,ou=groups,dc=slapd-test,dc=python-ldap,dc=org",
        "cn=bar,ou=groups,dc=slapd-test,dc=python-ldap,dc=org",
    ]
    res.form["jpegPhoto"] = Upload("logo.jpg", jpeg_photo)
    res = res.form.submit(name="action", value="edit", status=200)
    assert "Profile updated successfuly." in res, str(res)

    with testclient.app.app_context():
        logged_user = User.get(dn=logged_user.dn, conn=slapd_connection)
        logged_user.load_groups(conn=slapd_connection)

    assert ["user"] == logged_user.uid
    assert ["given_name"] == logged_user.givenName
    assert ["family_name"] == logged_user.sn
    assert ["*****@*****.**"] == logged_user.mail
    assert ["555-666-777"] == logged_user.telephoneNumber
    assert "666" == logged_user.employeeNumber
    assert [jpeg_photo] == logged_user.jpegPhoto

    foo_group.reload(slapd_connection)
    bar_group.reload(slapd_connection)
    assert logged_user.groups == [foo_group]
    assert foo_group.member == [logged_user.dn]
    assert bar_group.member == [admin.dn]

    with testclient.app.app_context():
        assert logged_user.check_password("correct horse battery staple")

    logged_user.uid = ["user"]
    logged_user.cn = ["John (johnny) Doe"]
    logged_user.sn = ["Doe"]
    logged_user.mail = ["*****@*****.**"]
    logged_user.givenName = None
    logged_user.jpegPhoto = None
    logged_user.save(conn=slapd_connection)
コード例 #22
0
ファイル: test_profile.py プロジェクト: yaal-fr/canaille
def test_first_login_mail_button(smtpd, testclient, slapd_connection,
                                 logged_admin):
    User.ldap_object_classes(slapd_connection)
    u = User(
        objectClass=["inetOrgPerson"],
        cn="Temp User",
        sn="Temp",
        uid="temp",
        mail="*****@*****.**",
    )
    u.save(slapd_connection)

    res = testclient.get("/profile/temp", status=200)
    assert "This user does not have a password yet" in res
    assert "Send" in res

    res = res.form.submit(name="action",
                          value="password-initialization-mail",
                          status=200)
    assert (
        "A password initialization link has been sent at the user email address. It should be received within 10 minutes."
        in res)
    assert "Send again" in res
    assert len(smtpd.messages) == 1

    u.reload(slapd_connection)
    u.userPassword = ["{SSHA}fw9DYeF/gHTHuVMepsQzVYAkffGcU8Fz"]
    u.save(slapd_connection)

    res = testclient.get("/profile/temp", status=200)
    assert "This user does not have a password yet" not in res
コード例 #23
0
def validate_configuration(config):
    from canaille.models import User, Group

    try:
        conn = ldap.initialize(config["LDAP"]["URI"])
        if config["LDAP"].get("TIMEOUT"):
            conn.set_option(ldap.OPT_NETWORK_TIMEOUT,
                            config["LDAP"]["TIMEOUT"])
        conn.simple_bind_s(config["LDAP"]["BIND_DN"],
                           config["LDAP"]["BIND_PW"])

    except ldap.SERVER_DOWN as exc:
        raise ConfigurationException(
            f'Could not connect to the LDAP server \'{config["LDAP"]["URI"]}\''
        ) from exc

    except ldap.INVALID_CREDENTIALS as exc:
        raise ConfigurationException(
            f'LDAP authentication failed with user \'{config["LDAP"]["BIND_DN"]}\''
        ) from exc

    try:
        User.ldap_object_classes(conn)
        user = User(
            objectClass=["inetOrgPerson"],
            cn=f"canaille_{uuid.uuid4()}",
            sn=f"canaille_{uuid.uuid4()}",
            uid=f"canaille_{uuid.uuid4()}",
            mail=f"canaille_{uuid.uuid4()}@mydomain.tld",
            userPassword="******",
        )
        user.save(conn)
        user.delete(conn)

    except ldap.INSUFFICIENT_ACCESS as exc:
        raise ConfigurationException(
            f'LDAP user \'{config["LDAP"]["BIND_DN"]}\' cannot create '
            f'users at \'{config["LDAP"]["USER_BASE"]}\'') from exc

    try:
        Group.ldap_object_classes(conn)

        user = User(
            objectClass=["inetOrgPerson"],
            cn=f"canaille_{uuid.uuid4()}",
            sn=f"canaille_{uuid.uuid4()}",
            uid=f"canaille_{uuid.uuid4()}",
            mail=f"canaille_{uuid.uuid4()}@mydomain.tld",
            userPassword="******",
        )
        user.save(conn)

        group = Group(
            objectClass=["groupOfNames"],
            cn=f"canaille_{uuid.uuid4()}",
            member=[user.dn],
        )
        group.save(conn)
        group.delete(conn)

    except ldap.INSUFFICIENT_ACCESS as exc:
        raise ConfigurationException(
            f'LDAP user \'{config["LDAP"]["BIND_DN"]}\' cannot create '
            f'groups at \'{config["LDAP"]["GROUP_BASE"]}\'') from exc

    finally:
        user.delete(conn)

    conn.unbind_s()
コード例 #24
0
ファイル: test_profile.py プロジェクト: yaal-fr/canaille
def test_user_creation_edition_and_deletion(testclient, slapd_connection,
                                            logged_moderator, foo_group,
                                            bar_group):
    # The user does not exist.
    res = testclient.get("/users", status=200)
    with testclient.app.app_context():
        assert User.get("george", conn=slapd_connection) is None
    assert "george" not in res.text

    # Fill the profile for a new user.
    res = testclient.get("/profile", status=200)
    res.form["uid"] = "george"
    res.form["givenName"] = "George"
    res.form["sn"] = "Abitbol"
    res.form["mail"] = "*****@*****.**"
    res.form["telephoneNumber"] = "555-666-888"
    res.form["groups"] = [
        "cn=foo,ou=groups,dc=slapd-test,dc=python-ldap,dc=org"
    ]
    res.form["password1"] = "totoyolo"
    res.form["password2"] = "totoyolo"

    # User have been created
    res = res.form.submit(name="action", value="edit",
                          status=302).follow(status=200)
    with testclient.app.app_context():
        george = User.get("george", conn=slapd_connection)
        george.load_groups(conn=slapd_connection)
        foo_group.reload(slapd_connection)
        assert "George" == george.givenName[0]
        assert george.groups == [foo_group]
        assert george.check_password("totoyolo")

    assert "george" in testclient.get("/users", status=200).text
    assert "readonly" not in res.form["groups"].attrs

    res.form["givenName"] = "Georgio"
    res.form["groups"] = [
        "cn=foo,ou=groups,dc=slapd-test,dc=python-ldap,dc=org",
        "cn=bar,ou=groups,dc=slapd-test,dc=python-ldap,dc=org",
    ]

    # User have been edited
    res = res.form.submit(name="action", value="edit", status=200)
    with testclient.app.app_context():
        george = User.get("george", conn=slapd_connection)
        george.load_groups(conn=slapd_connection)
        assert "Georgio" == george.givenName[0]
        assert george.check_password("totoyolo")

    foo_group.reload(slapd_connection)
    bar_group.reload(slapd_connection)
    assert george.dn in set(foo_group.member)
    assert george.dn in set(bar_group.member)
    assert set(george.groups) == {foo_group, bar_group}
    assert "george" in testclient.get("/users", status=200).text
    assert "george" in testclient.get("/users", status=200).text

    # User have been deleted.
    res = res.form.submit(name="action", value="delete",
                          status=302).follow(status=200)
    with testclient.app.app_context():
        assert User.get("george", conn=slapd_connection) is None
    assert "george" not in res.text