On Mon, 7 Dec 2015 17:49:18 +0100
Milan Kubík <mku...@redhat.com> wrote:

> On 12/03/2015 08:15 PM, Filip Škola wrote:
> > On Mon, 30 Nov 2015 17:18:30 +0100
> > Milan Kubík <mku...@redhat.com> wrote:
> >
> >> On 11/23/2015 04:42 PM, Filip Škola wrote:
> >>> Sending updated patch.
> >>>
> >>> F.
> >>>
> >>> On Mon, 23 Nov 2015 14:59:34 +0100
> >>> Filip Škola <fsk...@redhat.com> wrote:
> >>>
> >>>> Found couple of issues (broke some dependencies).
> >>>>
> >>>> NACK
> >>>>
> >>>> F.
> >>>>
> >>>> On Fri, 20 Nov 2015 13:56:36 +0100
> >>>> Filip Škola <fsk...@redhat.com> wrote:
> >>>>
> >>>>> Another one.
> >>>>>
> >>>>> F.
> >>>
> >> Hi, the tests look good. Few remarks, though.
> >>
> >> 1. Please, use the shortes copyright notice in new modules.
> >>
> >>       #
> >>       # Copyright (C) 2015  FreeIPA Contributors see COPYING for
> >> license #
> >>
> >> 2. The tests `test_group_remove_group_from_protected_group` and
> >> `test_group_full_set_of_objectclass_not_available_post_detach`
> >> were not ported. Please, include them in the patch.
> >>
> >> Also, for less hassle, please rebase your patches on top of
> >> freeipa-mkubik-0025-3-Separated-Tracker-implementations-into-standalone-pa.patch
> >> Which changes the location of tracker implementations and prevents
> >> circular imports.
> >>
> >> Thanks.
> >>
> >
> >
> > Hi,
> >
> > these cases are there, in corresponding classes. They are marked
> > with the original comments. (However I can move them to separate
> > class if desirable.)
> >
> > The copyright notice is changed. Also included a few changes in the
> > test with user without private group.
> >
> > Filip
> NACK
> 
> linter:
> ************* Module tracker.group_plugin
> ipatests/test_xmlrpc/tracker/group_plugin.py:257: 
> [E0102(function-redefined), GroupTracker.check_remove_member] method 
> already defined line 253)
> 
> Probably a leftover after the rebase made on top of my patch. Please
> fix it. You can check youch changes by make-lint script before
> sending them.
> 
> Thanks
> 


Hi,

I learned to use make-lint!

Thanks,
F.
>From 2e231e285215818bbe1e06aeba573d43c86fab8b Mon Sep 17 00:00:00 2001
From: Filip Skola <fsk...@redhat.com>
Date: Mon, 9 Nov 2015 16:48:55 +0100
Subject: [PATCH] Refactor test_group_plugin, use GroupTracker for tests

---
 ipatests/test_xmlrpc/test_group_plugin.py     | 1728 +++++++++----------------
 ipatests/test_xmlrpc/test_stageuser_plugin.py |    4 +-
 ipatests/test_xmlrpc/tracker/group_plugin.py  |  149 ++-
 3 files changed, 736 insertions(+), 1145 deletions(-)

diff --git a/ipatests/test_xmlrpc/test_group_plugin.py b/ipatests/test_xmlrpc/test_group_plugin.py
index f2bd0f4b9c8d517500b63cf49a8a7bc7c29aab6e..1ac278c7e224b8dd8793dc56c299dcae88aa78a3 100644
--- a/ipatests/test_xmlrpc/test_group_plugin.py
+++ b/ipatests/test_xmlrpc/test_group_plugin.py
@@ -1,6 +1,7 @@
 # Authors:
 #   Rob Crittenden <rcrit...@redhat.com>
 #   Pavel Zuna <pz...@redhat.com>
+#   Filip Skola <fsk...@redhat.com>
 #
 # Copyright (C) 2008  Red Hat
 # see file 'COPYING' for use and warranty information
@@ -26,1137 +27,648 @@ import pytest
 
 from ipalib import api, errors
 from ipatests.test_xmlrpc import objectclasses
-from ipatests.test_xmlrpc.xmlrpc_test import (Declarative, fuzzy_digits, fuzzy_uuid, fuzzy_set_ci,
-                         add_sid, add_oc, XMLRPC_test, raises_exact)
+from ipatests.test_xmlrpc.xmlrpc_test import (
+    fuzzy_digits, fuzzy_uuid, fuzzy_set_ci, add_sid, add_oc,
+    XMLRPC_test, raises_exact
+)
 from ipapython.dn import DN
-from ipatests.test_xmlrpc.test_user_plugin import get_user_result
 
+from ipatests.test_xmlrpc.tracker.group_plugin import GroupTracker
 from ipatests.test_xmlrpc.tracker.user_plugin import UserTracker
-from ipatests.util import assert_deepequal
+from ipatests.test_xmlrpc.test_user_plugin import user, user_npg2
+from ipatests.util import assert_deepequal, get_group_dn
 
 
-group1 = u'testgroup1'
-group2 = u'testgroup2'
-group3 = u'testgroup3'
-renamedgroup1 = u'testgroup'
-user1 = u'tuser1'
+notagroup = u'notagroup'
+renamedgroup1 = u'renamedgroup'
+invalidgroup1 = u'+tgroup1'
+external_sid1 = u'S-1-1-123456-789-1'
 
-invalidgroup1=u'+tgroup1'
 
-# When adding external SID member to a group we can't test
-# it fully due to possibly missing Samba 4 python bindings
-# and/or not configured AD trusts. Thus, we'll use incorrect
-# SID value to merely test that proper exceptions are raised
-external_sid1=u'S-1-1-123456-789-1'
+@pytest.fixture(scope='class')
+def group(request):
+    tracker = GroupTracker(name=u'testgroup1', description=u'Test desc1')
+    return tracker.make_fixture(request)
 
-def get_group_dn(cn):
-    return DN(('cn', cn), api.env.container_group, api.env.basedn)
+
+@pytest.fixture(scope='class')
+def group2(request):
+    tracker = GroupTracker(name=u'testgroup2', description=u'Test desc2')
+    return tracker.make_fixture(request)
+
+
+@pytest.fixture(scope='class')
+def managed_group(request, user):
+    user.ensure_exists()
+    tracker = GroupTracker(
+        name=user.uid, description=u'User private group for %s' % user.uid
+    )
+    tracker.exists = True
+    # Managed group gets created when user is created
+    tracker.track_create()
+    return tracker
+
+
+@pytest.fixture(scope='class')
+def admins(request):
+    # Track the admins group
+    tracker = GroupTracker(
+        name=u'admins', description=u'Account administrators group'
+    )
+    tracker.exists = True
+    tracker.track_create()
+    tracker.attrs.update(member_user=[u'admin'])
+    return tracker
+
+
+@pytest.fixture(scope='class')
+def trustadmins(request):
+    # Track the 'trust admins' group
+    tracker = GroupTracker(
+        name=u'trust admins', description=u'Trusts administrators group'
+    )
+    tracker.exists = True
+    tracker.track_create()
+    tracker.attrs.update(member_user=[u'admin'])
+    return tracker
 
 
 @pytest.mark.tier1
-class test_group(Declarative):
-    cleanup_commands = [
-        ('group_del', [group1], {}),
-        ('group_del', [group2], {}),
-        ('group_del', [group3], {}),
-        ('group_del', [renamedgroup1], {}),
-        ('user_del', [user1], {}),
-    ]
-
-    tests = [
-
-        ################
-        # create group1:
-        dict(
-            desc='Try to retrieve non-existent %r' % group1,
-            command=('group_show', [group1], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group1),
-        ),
-
-
-        dict(
-            desc='Try to update non-existent %r' % group1,
-            command=('group_mod', [group1], dict(description=u'Foo')),
-            expected=errors.NotFound(reason=u'%s: group not found' % group1),
-        ),
-
-
-        dict(
-            desc='Try to delete non-existent %r' % group1,
-            command=('group_del', [group1], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group1),
-        ),
-
-
-        dict(
-            desc='Try to rename non-existent %r' % group1,
-            command=('group_mod', [group1], dict(setattr=u'cn=%s' % renamedgroup1)),
-            expected=errors.NotFound(reason=u'%s: group not found' % group1),
-        ),
-
-
-        dict(
-            desc='Create non-POSIX %r' % group1,
-            command=(
-                'group_add', [group1], dict(description=u'Test desc 1',nonposix=True)
-            ),
-            expected=dict(
-                value=group1,
-                summary=u'Added group "testgroup1"',
-                result=dict(
-                    cn=[group1],
-                    description=[u'Test desc 1'],
-                    objectclass=objectclasses.group,
-                    ipauniqueid=[fuzzy_uuid],
-                    dn=get_group_dn('testgroup1'),
-                ),
-            ),
-        ),
-
-
-        dict(
-            desc='Try to create duplicate %r' % group1,
-            command=(
-                'group_add', [group1], dict(description=u'Test desc 1')
-            ),
-            expected=errors.DuplicateEntry(
-                message=u'group with name "%s" already exists' % group1),
-        ),
-
-
-        dict(
-            desc='Retrieve non-POSIX %r' % group1,
-            command=('group_show', [group1], {}),
-            expected=dict(
-                value=group1,
-                summary=None,
-                result=dict(
-                    cn=[group1],
-                    description=[u'Test desc 1'],
-                    dn=get_group_dn('testgroup1'),
-                ),
-            ),
-        ),
-
-
-        dict(
-            desc='Updated non-POSIX %r' % group1,
-            command=(
-                'group_mod', [group1], dict(description=u'New desc 1')
-            ),
-            expected=dict(
-                result=dict(
-                    cn=[group1],
-                    description=[u'New desc 1'],
-                ),
-                summary=u'Modified group "testgroup1"',
-                value=group1,
-            ),
-        ),
-
-
-        dict(
-            desc='Retrieve %r to verify update' % group1,
-            command=('group_show', [group1], {}),
-            expected=dict(
-                value=group1,
-                result=dict(
-                    cn=[group1],
-                    description=[u'New desc 1'],
-                    dn=get_group_dn('testgroup1'),
-                ),
-                summary=None,
-            ),
-        ),
-
-
-        # FIXME: The return value is totally different here than from the above
-        # group_mod() test.  I think that for all *_mod() commands we should
-        # just return the entry exactly as *_show() does.
-        dict(
-            desc='Updated %r to promote it to a POSIX group' % group1,
-            command=('group_mod', [group1], dict(posix=True)),
-            expected=dict(
-                result=dict(
-                    cn=[group1],
-                    description=[u'New desc 1'],
-                    gidnumber=[fuzzy_digits],
-                ),
-                value=group1,
-                summary=u'Modified group "testgroup1"',
-            ),
-        ),
-
-
-        dict(
-            desc="Retrieve %r to verify it's a POSIX group" % group1,
-            command=('group_show', [group1], {}),
-            expected=dict(
-                value=group1,
-                result=dict(
-                    cn=[group1],
-                    description=(u'New desc 1',),
-                    dn=get_group_dn('testgroup1'),
-                    gidnumber=[fuzzy_digits],
-                ),
-                summary=None,
-            ),
-        ),
-
-
-        dict(
-            desc='Search for %r' % group1,
-            command=('group_find', [], dict(cn=group1)),
-            expected=dict(
-                count=1,
-                truncated=False,
-                result=[
-                    dict(
-                        dn=get_group_dn(group1),
-                        cn=[group1],
-                        description=[u'New desc 1'],
-                        gidnumber=[fuzzy_digits],
-                    ),
-                ],
-                summary=u'1 group matched',
-            ),
-        ),
-
-
-
-        ################
-        # create group2:
-        dict(
-            desc='Try to retrieve non-existent %r' % group2,
-            command=('group_show', [group2], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group2),
-        ),
-
-
-        dict(
-            desc='Try to update non-existent %r' % group2,
-            command=('group_mod', [group2], dict(description=u'Foo')),
-            expected=errors.NotFound(reason=u'%s: group not found' % group2),
-        ),
-
-
-        dict(
-            desc='Try to delete non-existent %r' % group2,
-            command=('group_del', [group2], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group2),
-        ),
-
-
-        dict(
-            desc='Create %r' % group2,
-            command=(
-                'group_add', [group2], dict(description=u'Test desc 2')
-            ),
-            expected=dict(
-                value=group2,
-                summary=u'Added group "testgroup2"',
-                result=dict(
-                    cn=[group2],
-                    description=[u'Test desc 2'],
-                    gidnumber=[fuzzy_digits],
-                    objectclass=objectclasses.posixgroup,
-                    ipauniqueid=[fuzzy_uuid],
-                    dn=get_group_dn('testgroup2'),
-                ),
-            ),
-        ),
-
-
-        dict(
-            desc='Try to create duplicate %r' % group2,
-            command=(
-                'group_add', [group2], dict(description=u'Test desc 2')
-            ),
-            expected=errors.DuplicateEntry(
-                message=u'group with name "%s" already exists' % group2),
-        ),
-
-
-        dict(
-            desc='Retrieve %r' % group2,
-            command=('group_show', [group2], {}),
-            expected=dict(
-                value=group2,
-                summary=None,
-                result=dict(
-                    cn=[group2],
-                    description=[u'Test desc 2'],
-                    gidnumber=[fuzzy_digits],
-                    dn=get_group_dn('testgroup2'),
-                ),
-            ),
-        ),
-
-
-        dict(
-            desc='Updated %r' % group2,
-            command=(
-                'group_mod', [group2], dict(description=u'New desc 2')
-            ),
-            expected=dict(
-                result=dict(
-                    cn=[group2],
-                    gidnumber=[fuzzy_digits],
-                    description=[u'New desc 2'],
-                ),
-                summary=u'Modified group "testgroup2"',
-                value=group2,
-            ),
-        ),
-
-
-        dict(
-            desc='Retrieve %r to verify update' % group2,
-            command=('group_show', [group2], {}),
-            expected=dict(
-                value=group2,
-                result=dict(
-                    cn=[group2],
-                    description=[u'New desc 2'],
-                    gidnumber=[fuzzy_digits],
-                    dn=get_group_dn('testgroup2'),
-                ),
-                summary=None,
-            ),
-        ),
-
-
-        dict(
-            desc='Search for %r' % group2,
-            command=('group_find', [], dict(cn=group2)),
-            expected=dict(
-                count=1,
-                truncated=False,
-                result=[
-                    dict(
-                        dn=get_group_dn('testgroup2'),
-                        cn=[group2],
-                        description=[u'New desc 2'],
-                        gidnumber=[fuzzy_digits],
-                    ),
-                ],
-                summary=u'1 group matched',
-            ),
-        ),
-
-
-        dict(
-            desc='Search for all groups',
-            command=('group_find', [], {}),
-            expected=dict(
-                summary=u'6 groups matched',
-                count=6,
-                truncated=False,
-                result=[
-                    {
-                        'dn': get_group_dn('admins'),
-                        'member_user': [u'admin'],
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [u'admins'],
-                        'description': [u'Account administrators group'],
-                    },
-                    {
-                        'dn': get_group_dn('editors'),
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [u'editors'],
-                        'description': [u'Limited admins who can edit other users'],
-                    },
-                    {
-                        'dn': get_group_dn('ipausers'),
-                        'cn': [u'ipausers'],
-                        'description': [u'Default group for all users'],
-                    },
-                    dict(
-                        dn=get_group_dn(group1),
-                        cn=[group1],
-                        description=[u'New desc 1'],
-                        gidnumber=[fuzzy_digits],
-                    ),
-                    dict(
-                        dn=get_group_dn(group2),
-                        cn=[group2],
-                        description=[u'New desc 2'],
-                        gidnumber=[fuzzy_digits],
-                    ),
-                    {
-                        'dn': get_group_dn('trust admins'),
-                        'member_user': [u'admin'],
-                        'cn': [u'trust admins'],
-                        'description': [u'Trusts administrators group'],
-                    },
-                ],
-            ),
-        ),
-
-        dict(
-            desc='Search for non-POSIX groups',
-            command=('group_find', [], dict(nonposix=True, all=True)),
-            expected=dict(
-                summary=u'2 groups matched',
-                count=2,
-                truncated=False,
-                result=[
-                    {
-                        'dn': get_group_dn('ipausers'),
-                        'cn': [u'ipausers'],
-                        'description': [u'Default group for all users'],
-                        'objectclass': fuzzy_set_ci(objectclasses.group),
-                        'ipauniqueid': [fuzzy_uuid],
-                    },
-                    {
-                        'dn': get_group_dn('trust admins'),
-                        'member_user': [u'admin'],
-                        'cn': [u'trust admins'],
-                        'description': [u'Trusts administrators group'],
-                        'objectclass': fuzzy_set_ci(objectclasses.group),
-                        'ipauniqueid': [fuzzy_uuid],
-                    },
-                ],
-            ),
-        ),
-
-        dict(
-            desc='Search for non-POSIX groups with criteria filter',
-            command=('group_find', [u'users'], dict(nonposix=True, all=True)),
-            expected=dict(
-                summary=u'1 group matched',
-                count=1,
-                truncated=False,
-                result=[
-                    {
-                        'dn': get_group_dn('ipausers'),
-                        'cn': [u'ipausers'],
-                        'description': [u'Default group for all users'],
-                        'objectclass': fuzzy_set_ci(objectclasses.group),
-                        'ipauniqueid': [fuzzy_uuid],
-                    },
-                ],
-            ),
-        ),
-
-        dict(
-            desc='Search for POSIX groups',
-            command=('group_find', [], dict(posix=True, all=True)),
-            expected=dict(
-                summary=u'4 groups matched',
-                count=4,
-                truncated=False,
-                result=[
-                    add_sid({
-                        'dn': get_group_dn('admins'),
-                        'member_user': [u'admin'],
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [u'admins'],
-                        'description': [u'Account administrators group'],
-                        'objectclass': fuzzy_set_ci(add_oc(
-                            objectclasses.posixgroup, u'ipantgroupattrs')),
-                        'ipauniqueid': [fuzzy_uuid],
-                    }),
-                    add_sid({
-                        'dn': get_group_dn('editors'),
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [u'editors'],
-                        'description': [u'Limited admins who can edit other users'],
-                        'objectclass': fuzzy_set_ci(add_oc(
-                            objectclasses.posixgroup,
-                            u'ipantgroupattrs',
-                            check_sidgen=True)),
-                        'ipauniqueid': [fuzzy_uuid],
-                    }, check_sidgen=True),
-                    dict(
-                        dn=get_group_dn(group1),
-                        cn=[group1],
-                        description=[u'New desc 1'],
-                        gidnumber=[fuzzy_digits],
-                        objectclass=fuzzy_set_ci(objectclasses.posixgroup),
-                        ipauniqueid=[fuzzy_uuid],
-                    ),
-                    add_sid(dict(
-                        dn=get_group_dn(group2),
-                        cn=[group2],
-                        description=[u'New desc 2'],
-                        gidnumber=[fuzzy_digits],
-                        objectclass=fuzzy_set_ci(add_oc(
-                            objectclasses.posixgroup, u'ipantgroupattrs')),
-                        ipauniqueid=[fuzzy_uuid],
-                    )),
-                ],
-            ),
-        ),
-
-
-        ###############
-        # test external SID members for group3:
-        dict(
-            desc='Create external %r' % group3,
-            command=(
-                'group_add', [group3], dict(description=u'Test desc 3',external=True)
-            ),
-            expected=dict(
-                value=group3,
-                summary=u'Added group "testgroup3"',
-                result=dict(
-                    cn=[group3],
-                    description=[u'Test desc 3'],
-                    objectclass=objectclasses.externalgroup,
-                    ipauniqueid=[fuzzy_uuid],
-                    dn=get_group_dn(group3),
-                ),
-            ),
-        ),
-
-        dict(
-            desc='Search for external groups',
-            command=('group_find', [], dict(external=True, all=True)),
-            expected=dict(
-                summary=u'1 group matched',
-                count=1,
-                truncated=False,
-                result=[
-                    dict(
-                        cn=[group3],
-                        description=[u'Test desc 3'],
-                        objectclass=fuzzy_set_ci(objectclasses.externalgroup),
-                        ipauniqueid=[fuzzy_uuid],
-                        dn=get_group_dn(group3),
-                    ),
-                ],
-            ),
-        ),
-
-
-        dict(
-            desc='Convert posix group %r to support external membership' % (group2),
-            command=(
-                'group_mod', [group2], dict(external=True)
-            ),
-            expected=errors.PosixGroupViolation(),
-        ),
-
-
-        dict(
-            desc='Convert external members group %r to posix' % (group3),
-            command=(
-                'group_mod', [group3], dict(posix=True)
-            ),
-            expected=errors.ExternalGroupViolation(),
-        ),
-
-
-        dict(
-            desc='Add external member %r to %r' % (external_sid1, group3),
-            command=(
-                'group_add_member', [group3], dict(ipaexternalmember=external_sid1)
-            ),
-            expected=lambda x, output: (type(x) == errors.ValidationError
-                                        or type(x) == errors.NotFound
-                                        or 'failed' in output),
-        ),
-
-
-        dict(
-            desc='Remove group %r with external membership' % (group3),
-            command=('group_del', [group3], {}),
-            expected=dict(
-                result=dict(failed=[]),
-                value=[group3],
-                summary=u'Deleted group "testgroup3"',
-            ),
-        ),
-
-
-        ###############
-        # member stuff:
-        dict(
-            desc='Add member %r to %r' % (group2, group1),
-            command=(
-                'group_add_member', [group1], dict(group=group2)
-            ),
-            expected=dict(
-                completed=1,
-                failed=dict(
-                    member=dict(
-                        group=tuple(),
-                        user=tuple(),
-                    ),
-                ),
-                result={
-                        'dn': get_group_dn(group1),
-                        'member_group': (group2,),
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [group1],
-                        'description': [u'New desc 1'],
+class TestGroup(XMLRPC_test):
+    def test_create(self, group):
+        """ Create a group """
+        group.create()
+
+    def test_create_duplicate(self, group):
+        """ Try to create a duplicate group """
+        group.ensure_exists()
+        command = group.make_create_command()
+
+        with raises_exact(errors.DuplicateEntry(
+                message=u'group with name "%s" already exists' % group.cn)):
+            command()
+
+    def test_retrieve(self, group):
+        """ Retrieve a group """
+        group.retrieve()
+
+    def test_update(self, group):
+        """ Update a group with new description
+        and perform retrieve command to verify the update """
+        group.update(dict(description=u'New desc'))
+        group.retrieve()
+
+    def test_rename(self, group):
+        """ Rename a group and than rename it back """
+        origname = group.cn
+
+        command = group.make_command('group_mod', *[group.cn],
+                                     **dict(setattr=u'cn=%s' % renamedgroup1))
+        result = command()
+        group.attrs.update(cn=[renamedgroup1])
+        group.check_update(result)
+        group.cn = renamedgroup1
+
+        command = group.make_command('group_mod', *[group.cn],
+                                     **dict(setattr=u'cn=%s' % origname))
+        result = command()
+        group.attrs.update(cn=[origname])
+        group.check_update(result)
+        group.cn = origname
+
+    def test_convert_posix_to_external(self, group):
+        """ Try to convert a posix group to external """
+        command = group.make_update_command(dict(external=True))
+        with raises_exact(errors.PosixGroupViolation(
+                reason=u"""This is already a posix group and cannot
+                        be converted to external one""")):
+            command()
+
+    def test_add_with_invalid_name(self, group):
+        """ Try to add group with an invalid name """
+        command = group.make_command(
+            'group_add', *[invalidgroup1], **dict(description=u'Test')
+        )
+        with raises_exact(errors.ValidationError(
+                name='group_name',
+                error=u'may only include letters, numbers, _, -, . and $')):
+            command()
+
+
+@pytest.mark.tier1
+class TestFindGroup(XMLRPC_test):
+    def test_search(self, group):
+        """ Search for a group """
+        group.ensure_exists()
+        group.find()
+
+    def test_search_for_all_groups(self, group, group2):
+        """ Search for all groups """
+        group.ensure_exists()
+        group2.create()
+        command = group.make_command('group_find')
+        result = command()
+        assert_deepequal(dict(
+            summary=u'6 groups matched',
+            count=6,
+            truncated=False,
+            result=[
+                {
+                    'dn': get_group_dn('admins'),
+                    'member_user': [u'admin'],
+                    'gidnumber': [fuzzy_digits],
+                    'cn': [u'admins'],
+                    'description': [u'Account administrators group'],
+                },
+                {
+                    'dn': get_group_dn('editors'),
+                    'gidnumber': [fuzzy_digits],
+                    'cn': [u'editors'],
+                    'description':
+                        [u'Limited admins who can edit other users'],
+                },
+                {
+                    'dn': get_group_dn('ipausers'),
+                    'cn': [u'ipausers'],
+                    'description': [u'Default group for all users'],
+                },
+                {
+                    'dn': get_group_dn(group.cn),
+                    'cn': [group.cn],
+                    'description': [u'Test desc1'],
+                    'gidnumber': [fuzzy_digits],
+                },
+                {
+                    'dn': get_group_dn(group2.cn),
+                    'cn': [group2.cn],
+                    'description': [u'Test desc2'],
+                    'gidnumber': [fuzzy_digits],
                 },
-            ),
-        ),
-
-        dict(
-            # FIXME: Shouldn't this raise a NotFound instead?
-            desc='Try to add non-existent member to %r' % group1,
-            command=(
-                'group_add_member', [group1], dict(group=u'notfound')
-            ),
-            expected=dict(
-                completed=0,
-                failed=dict(
-                    member=dict(
-                        group=[(u'notfound', u'no such entry')],
-                        user=tuple(),
-                    ),
-                ),
-                result={
-                        'dn': get_group_dn(group1),
-                        'member_group': (group2,),
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [group1],
-                        'description': [u'New desc 1'],
+                {
+                    'dn': get_group_dn('trust admins'),
+                    'member_user': [u'admin'],
+                    'cn': [u'trust admins'],
+                    'description': [u'Trusts administrators group'],
                 },
-            ),
-        ),
-
-        dict(
-            desc='Remove member %r from %r' % (group2, group1),
-            command=('group_remove_member',
-                [group1], dict(group=group2)
-            ),
-            expected=dict(
-                completed=1,
-                failed=dict(
-                    member=dict(
-                        group=tuple(),
-                        user=tuple(),
-                    ),
-                ),
-                result={
-                    'dn': get_group_dn(group1),
-                    'cn': [group1],
+            ]), result)
+
+    def test_search_for_all_posix(self, group, group2):
+        """ Search for all posix groups """
+        command = group.make_command(
+            'group_find', **dict(posix=True, all=True)
+        )
+        result = command()
+        assert_deepequal(dict(
+            summary=u'4 groups matched',
+            count=4,
+            truncated=False,
+            result=[
+                {
+                    'dn': get_group_dn('admins'),
+                    'member_user': [u'admin'],
                     'gidnumber': [fuzzy_digits],
-                    'description': [u'New desc 1'],
+                    'cn': [u'admins'],
+                    'description': [u'Account administrators group'],
+                    'objectclass': fuzzy_set_ci(add_oc(
+                        objectclasses.posixgroup, u'ipantgroupattrs')),
+                    'ipauniqueid': [fuzzy_uuid],
                 },
-            ),
-        ),
-
-        dict(
-            # FIXME: Shouldn't this raise a NotFound instead?
-            desc='Try to remove non-existent member from %r' % group1,
-            command=('group_remove_member',
-                [group1], dict(group=u'notfound')
-            ),
-            expected=dict(
-                completed=0,
-                failed=dict(
-                    member=dict(
-                        group=[(u'notfound', u'This entry is not a member')],
-                        user=tuple(),
-                    ),
-                ),
-                result={
-                    'dn': get_group_dn(group1),
-                    'cn': [group1],
+                {
+                    'dn': get_group_dn('editors'),
                     'gidnumber': [fuzzy_digits],
-                    'description': [u'New desc 1'],
+                    'cn': [u'editors'],
+                    'description':
+                        [u'Limited admins who can edit other users'],
+                    'objectclass': fuzzy_set_ci(add_oc(
+                        objectclasses.posixgroup, u'ipantgroupattrs')),
+                    'ipauniqueid': [fuzzy_uuid],
                 },
-            ),
-        ),
-
-
-        dict(
-            desc='Rename %r' % group1,
-            command=('group_mod', [group1], dict(setattr=u'cn=%s' % renamedgroup1)),
-            expected=dict(
-                value=group1,
-                result=dict(
-                    cn=[renamedgroup1],
-                    description=[u'New desc 1'],
-                    gidnumber=[fuzzy_digits],
-                ),
-                summary=u'Modified group "%s"' % group1
-            )
-        ),
-
-
-        dict(
-            desc='Rename %r back' % renamedgroup1,
-            command=('group_mod', [renamedgroup1], dict(setattr=u'cn=%s' % group1)),
-            expected=dict(
-                value=renamedgroup1,
-                result=dict(
-                    cn=[group1],
-                    description=[u'New desc 1'],
-                    gidnumber=[fuzzy_digits],
-                ),
-                summary=u'Modified group "%s"' % renamedgroup1
-            )
-        ),
-
-
-
-        ################
-        # delete group1:
-        dict(
-            desc='Delete %r' % group1,
-            command=('group_del', [group1], {}),
-            expected=dict(
-                result=dict(failed=[]),
-                value=[group1],
-                summary=u'Deleted group "testgroup1"',
-            )
-        ),
-
-
-        dict(
-            desc='Try to delete non-existent %r' % group1,
-            command=('group_del', [group1], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group1),
-        ),
-
-
-        dict(
-            desc='Try to retrieve non-existent %r' % group1,
-            command=('group_show', [group1], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group1),
-        ),
-
-
-        dict(
-            desc='Try to update non-existent %r' % group1,
-            command=('group_mod', [group1], dict(description=u'Foo')),
-            expected=errors.NotFound(reason=u'%s: group not found' % group1),
-        ),
-
-
-
-        ################
-        # delete group2:
-        dict(
-            desc='Delete %r' % group2,
-            command=('group_del', [group2], {}),
-            expected=dict(
-                result=dict(failed=[]),
-                value=[group2],
-                summary=u'Deleted group "testgroup2"',
-            )
-        ),
-
-
-        dict(
-            desc='Try to delete non-existent %r' % group2,
-            command=('group_del', [group2], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group2),
-        ),
-
-
-        dict(
-            desc='Try to retrieve non-existent %r' % group2,
-            command=('group_show', [group2], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % group2),
-        ),
-
-
-        dict(
-            desc='Try to update non-existent %r' % group2,
-            command=('group_mod', [group2], dict(description=u'Foo')),
-            expected=errors.NotFound(reason=u'%s: group not found' % group2),
-        ),
-
-        dict(
-            desc='Test an invalid group name %r' % invalidgroup1,
-            command=('group_add', [invalidgroup1], dict(description=u'Test')),
-            expected=errors.ValidationError(name='group_name',
-                error=u'may only include letters, numbers, _, -, . and $'),
-        ),
-
-        # The assumption on these next 4 tests is that if we don't get a
-        # validation error then the request was processed normally.
-        dict(
-            desc='Test that validation is disabled on mods',
-            command=('group_mod', [invalidgroup1], {}),
-            expected=errors.NotFound(
-                reason=u'%s: group not found' % invalidgroup1),
-        ),
-
-
-        dict(
-            desc='Test that validation is disabled on deletes',
-            command=('group_del', [invalidgroup1], {}),
-            expected=errors.NotFound(
-                reason=u'%s: group not found' % invalidgroup1),
-        ),
-
-
-        dict(
-            desc='Test that validation is disabled on show',
-            command=('group_show', [invalidgroup1], {}),
-            expected=errors.NotFound(
-                reason=u'%s: group not found' % invalidgroup1),
-        ),
-
-
-        ##### managed entry tests
-        dict(
-            desc='Create %r' % user1,
-            command=(
-                'user_add', [], dict(givenname=u'Test', sn=u'User1')
-            ),
-            expected=dict(
-                value=user1,
-                summary=u'Added user "%s"' % user1,
-                result=get_user_result(user1, u'Test', u'User1', 'add'),
-            ),
-        ),
-
-
-        dict(
-            desc='Verify the managed group %r was created' % user1,
-            command=('group_show', [user1], {}),
-            expected=dict(
-                value=user1,
-                summary=None,
-                result=dict(
-                    cn=[user1],
-                    description=[u'User private group for %s' % user1],
-                    gidnumber=[fuzzy_digits],
-                    dn=get_group_dn(user1),
-                ),
-            ),
-        ),
-
-
-        dict(
-            desc='Verify that managed group %r can be found' % user1,
-            command=('group_find', [], {'cn': user1, 'private': True}),
-            expected=dict(
-                count=1,
-                truncated=False,
-                result=[
-                    dict(
-                        dn=get_group_dn(user1),
-                        cn=[user1],
-                        description=[u'User private group for %s' % user1],
-                        gidnumber=[fuzzy_digits],
-                    ),
-                ],
-                summary=u'1 group matched',
-            ),
-        ),
-
-
-        dict(
-            desc='Try to delete a managed group %r' % user1,
-            command=('group_del', [user1], {}),
-            expected=errors.ManagedGroupError(),
-        ),
-
-
-        dict(
-            desc='Detach managed group %r' % user1,
-            command=('group_detach', [user1], {}),
-            expected=dict(
-                result=True,
-                value=user1,
-                summary=u'Detached group "%s" from user "%s"' % (user1, user1),
-            ),
-        ),
-
-
-        dict(
-            desc='Now delete the unmanaged group %r' % user1,
-            command=('group_del', [user1], {}),
-            expected=dict(
-                result=dict(failed=[]),
-                value=[user1],
-                summary=u'Deleted group "%s"' % user1,
-            )
-        ),
-
-        dict(
-            desc='Verify that %r is really gone' % user1,
-            command=('group_show', [user1], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % user1),
-        ),
-
-        dict(
-            desc='Delete %r' % user1,
-            command=('user_del', [user1], {}),
-            expected=dict(
-                result=dict(failed=[]),
-                summary=u'Deleted user "tuser1"',
-                value=[user1],
-            ),
-        ),
-
-        dict(
-            desc='Create %r without User Private Group' % user1,
-            command=(
-                'user_add', [user1], dict(givenname=u'Test', sn=u'User1', noprivate=True, gidnumber=1000)
-            ),
-            expected=dict(
-                value=user1,
-                summary=u'Added user "tuser1"',
-                result=get_user_result(
-                    user1, u'Test', u'User1', 'add',
-                    description=[],
-                    objectclass=add_oc(objectclasses.user_base,
-                                       u'ipantuserattrs'),
-                    gidnumber=[u'1000'],
-                    omit=['mepmanagedentry'],
-                ),
-            ),
-        ),
-
-        dict(
-            desc='Verify the managed group %r was not created' % user1,
-            command=('group_show', [user1], {}),
-            expected=errors.NotFound(reason=u'%s: group not found' % user1),
-        ),
-
-        dict(
-            desc='Try to remove the admin user from the admins group',
-            command=('group_remove_member', [u'admins'], dict(user=[u'admin'])),
-            expected=errors.LastMemberError(key=u'admin', label=u'group',
-                container='admins'),
-        ),
-
-        dict(
-            desc='Add %r to the admins group' % user1,
-            command=('group_add_member', [u'admins'], dict(user=user1)),
-            expected=dict(
-                completed=1,
-                failed=dict(
-                    member=dict(
-                        group=tuple(),
-                        user=tuple(),
-                    ),
-                ),
-                result={
-                        'dn': get_group_dn('admins'),
-                        'member_user': [u'admin', user1],
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [u'admins'],
-                        'description': [u'Account administrators group'],
+                {
+                    'dn': get_group_dn(group.cn),
+                    'cn': [group.cn],
+                    'description': [u'Test desc1'],
+                    'gidnumber': [fuzzy_digits],
+                    'objectclass': fuzzy_set_ci(add_oc(
+                        objectclasses.posixgroup, u'ipantgroupattrs')),
+                    'ipauniqueid': [fuzzy_uuid],
                 },
-            ),
-        ),
-
-        dict(
-            desc='Try to remove admin and %r from the admins group' % user1,
-            command=('group_remove_member', [u'admins'],
-                dict(user=[u'admin', user1])),
-            expected=errors.LastMemberError(key=u'admin', label=u'group',
-                container='admins'),
-        ),
-
-        dict(
-            desc='Try to delete the admins group',
-            command=('group_del', [u'admins'], {}),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='admins', reason='privileged group'),
-        ),
-
-
-        dict(
-            desc='Try to rename the admins group',
-            command=('group_mod', [u'admins'], dict(rename=u'loosers')),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='admins', reason='Cannot be renamed'),
-        ),
-
-        dict(
-            desc='Try to rename the admins group via setattr',
-            command=('group_mod', [u'admins'], {'setattr': u'cn=loosers'}),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='admins', reason='Cannot be renamed'),
-        ),
-
-        dict(
-            desc='Try to modify the admins group to support external membership',
-            command=('group_mod', [u'admins'], dict(external=True)),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='admins', reason='Cannot support external non-IPA members'),
-        ),
-
-        dict(
-            desc='Try to delete the trust admins group',
-            command=('group_del', [u'trust admins'], {}),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='trust admins', reason='privileged group'),
-        ),
-
-        dict(
-            desc='Try to rename the trust admins group',
-            command=('group_mod', [u'trust admins'], dict(rename=u'loosers')),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='trust admins', reason='Cannot be renamed'),
-        ),
-
-        dict(
-            desc='Try to rename the trust admins group via setattr',
-            command=('group_mod', [u'trust admins'], {'setattr': u'cn=loosers'}),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='trust admins', reason='Cannot be renamed'),
-        ),
-
-
-        dict(
-            desc='Try to modify the trust admins group to support external membership',
-            command=('group_mod', [u'trust admins'], dict(external=True)),
-            expected=errors.ProtectedEntryError(label=u'group',
-                key='trust admins', reason='Cannot support external non-IPA members'),
-        ),
-
-        dict(
-            desc='Delete %r' % user1,
-            command=('user_del', [user1], {}),
-            expected=dict(
-                result=dict(failed=[]),
-                summary=u'Deleted user "%s"' % user1,
-                value=[user1],
-            ),
-        ),
-    ]
+                {
+                    'dn': get_group_dn(group2.cn),
+                    'cn': [group2.cn],
+                    'description': [u'Test desc2'],
+                    'gidnumber': [fuzzy_digits],
+                    'objectclass': fuzzy_set_ci(add_oc(
+                        objectclasses.posixgroup, u'ipantgroupattrs')),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+            ]), result)
+
+
+@pytest.mark.tier1
+class TestNonexistentGroup(XMLRPC_test):
+    def test_retrieve_nonexistent(self, group):
+        """ Try to retrieve a non-existent group """
+        group.ensure_missing()
+        command = group.make_retrieve_command()
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % group.cn)):
+            command()
+
+    def test_update_nonexistent(self, group):
+        """ Try to update a non-existent group """
+        group.ensure_missing()
+        command = group.make_update_command(
+            updates=dict(description=u'hey'))
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % group.cn)):
+            command()
+
+    def test_delete_nonexistent(self, group):
+        """ Try to delete a non-existent user """
+        group.ensure_missing()
+        command = group.make_delete_command()
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % group.cn)):
+            command()
+
+    def test_rename_nonexistent(self, group):
+        """ Try to rename a non-existent user """
+        group.ensure_missing()
+        command = group.make_update_command(
+            updates=dict(setattr=u'cn=%s' % renamedgroup1))
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % group.cn)):
+            command()
+
+
+@pytest.mark.tier1
+class TestNonposixGroup(XMLRPC_test):
+    def test_create_nonposix(self, group):
+        """ Create a non-posix group """
+        group.track_create()
+        command = group.make_create_command(**dict(nonposix=True))
+        result = command()
+
+        del group.attrs['gidnumber']
+        group.attrs.update(objectclass=objectclasses.group)
+        group.check_create(result)
+
+    def test_create_duplicate_to_nonposix(self, group):
+        """ Try to create a duplicate non-posix group """
+        group.ensure_exists()
+        command = group.make_create_command()
+
+        with raises_exact(errors.DuplicateEntry(
+                message=u'group with name "%s" already exists' % group.cn)):
+            command()
+
+    def test_retrieve_nonposix(self, group):
+        """ Retrieve a non-posix group """
+        group.retrieve()
+
+    def test_update_nonposix(self, group):
+        """ Update a non-posix group with new description
+        and perform retrieve command to verify the update """
+        group.update(dict(description=u'New desc'))
+        group.retrieve()
+
+    def test_search_for_all_nonposix(self, group):
+        """ Perform a search for all non-posix groups """
+        command = group.make_command(
+            'group_find', **dict(nonposix=True, all=True)
+        )
+        result = command()
+        assert_deepequal(dict(
+            summary=u'3 groups matched',
+            count=3,
+            truncated=False,
+            result=[
+                {
+                    'dn': get_group_dn('ipausers'),
+                    'cn': [u'ipausers'],
+                    'description': [u'Default group for all users'],
+                    'objectclass': fuzzy_set_ci(objectclasses.group),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+                {
+                    'dn': get_group_dn(group.cn),
+                    'cn': [group.cn],
+                    'description': [u'New desc'],
+                    'objectclass': fuzzy_set_ci(objectclasses.group),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+                {
+                    'dn': get_group_dn('trust admins'),
+                    'member_user': [u'admin'],
+                    'cn': [u'trust admins'],
+                    'description': [u'Trusts administrators group'],
+                    'objectclass': fuzzy_set_ci(objectclasses.group),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+            ],
+        ), result)
+
+    def test_upgrade_nonposix_to_posix(self, group):
+        """ Update non-posix group to promote it to posix group """
+        group.attrs.update(gidnumber=[fuzzy_digits])
+        group.update(dict(posix=True), dict(posix=None))
+        group.retrieve()
+
+    def test_search_for_all_nonposix_with_criteria(self, group):
+        """ Search for all non-posix groups with additional
+        criteria filter """
+        command = group.make_command(
+            'group_find', *[u'users'], **dict(nonposix=True, all=True)
+        )
+        result = command()
+        assert_deepequal(dict(
+            summary=u'1 group matched',
+            count=1,
+            truncated=False,
+            result=[
+                {
+                    'dn': get_group_dn('ipausers'),
+                    'cn': [u'ipausers'],
+                    'description': [u'Default group for all users'],
+                    'objectclass': fuzzy_set_ci(objectclasses.group),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+            ],
+        ), result)
+
+
+@pytest.mark.tier1
+class TestExternalGroup(XMLRPC_test):
+    def test_create_external(self, group):
+        """ Create a non-posix group """
+        group.track_create()
+        del group.attrs['gidnumber']
+        group.attrs.update(objectclass=objectclasses.externalgroup)
+        command = group.make_create_command(**dict(external=True))
+        result = command()
+        group.check_create(result)
+
+    def test_search_for_external(self, group):
+        """ Search for all external groups """
+        command = group.make_command(
+            'group_find', **dict(external=True, all=True)
+        )
+        result = command()
+        group.check_find(result, all=True)
+
+    def test_convert_external_to_posix(self, group):
+        """ Try to convert an external group to posix """
+        command = group.make_update_command(dict(posix=True))
+        with raises_exact(errors.ExternalGroupViolation(
+                reason=u'This group cannot be posix because it is external')):
+            command()
+
+    def test_add_external_member_to_external(self, group):
+        """ Try to add an invalid external member to an external
+        group and check that proper exceptions are raised """
+        # When adding external SID member to a group we can't test
+        # it fully due to possibly missing Samba 4 python bindings
+        # and/or not configured AD trusts. Thus, we'll use incorrect
+        # SID value to merely test that proper exceptions are raised
+        command = group.make_command('group_add_member', *[group.cn],
+                                     **dict(ipaexternalmember=external_sid1))
+        try:
+            command()
+        except Exception as ex:
+            if type(ex) == errors.ValidationError:
+                pass
+            elif type(ex) == errors.NotFound:
+                pass
+            elif 'failed' in str(ex):
+                pass
+            else:
+                raise ex
+
+    def test_delete_external_group(self, group):
+        group.delete()
 
 
 @pytest.mark.tier1
-class test_group_remove_group_from_protected_group(Declarative):
-    cleanup_commands = [
-        ('group_del', [group1], {}),
-    ]
-    tests = [
+class TestGroupMember(XMLRPC_test):
+    def test_add_nonexistent_member(self, group):
+        """ Try to add non-existent member to a group """
+        group.create()
+        command = group.make_add_member_command(dict(group=notagroup))
+        result = command()
+        group.check_add_member_negative(result, dict(group=notagroup))
+
+    def test_remove_nonexistent_member(self, group):
+        """ Try to remove non-existent member from a group """
+        group.ensure_exists()
+        command = group.make_remove_member_command(dict(group=notagroup))
+        result = command()
+        group.check_remove_member_negative(result, dict(group=notagroup))
+
+    def test_add_member(self, group, group2):
+        """ Add member group to a group """
+        group.ensure_exists()
+        group2.ensure_exists()
+        group.add_member(dict(group=group2.cn))
+
+    def test_remove_member(self, group, group2):
+        """ Remove a group member """
+        group.ensure_exists()
+        group2.ensure_exists()
+        group.remove_member(dict(group=group2.cn))
+
+    def test_add_and_remove_group_from_admins(self, group, admins):
+        """ Add group to protected admins group and then remove it """
         # Test scenario from ticket #4448
         # https://fedorahosted.org/freeipa/ticket/4448
-        dict(
-            desc='Add group %s' % group1,
-            command=('group_add', [group1], dict(description=u'Test desc 1')),
-            expected=dict(
-                value=group1,
-                summary=u'Added group "%s"' % group1,
-                result=dict(
-                    cn=[group1],
-                    description=[u'Test desc 1'],
-                    objectclass=objectclasses.posixgroup,
-                    gidnumber=[fuzzy_digits],
-                    ipauniqueid=[fuzzy_uuid],
-                    dn=get_group_dn(group1),
-                ),
-            ),
-        ),
-
-        dict(
-            desc='Add %s group to admins group' % group1,
-            command=('group_add_member', [u'admins'], dict(group=group1)),
-            expected=dict(
-                completed=1,
-                failed=dict(
-                    member=dict(
-                        group=tuple(),
-                        user=tuple(),
-                    ),
-                ),
-                result=dict(
-                        dn=get_group_dn('admins'),
-                        member_user=[u'admin'],
-                        member_group=[group1],
-                        gidnumber=[fuzzy_digits],
-                        cn=[u'admins'],
-                        description=[u'Account administrators group'],
-                ),
-            ),
-        ),
-
-        dict(
-            desc='Remove %s group from admins group' % group1,
-            command=('group_remove_member', [u'admins'], dict(group=group1)),
-            expected=dict(
-                completed=1,
-                failed=dict(
-                    member=dict(
-                        group=tuple(),
-                        user=tuple(),
-                    ),
-                ),
-                result=dict(
-                    dn=get_group_dn(u'admins'),
-                    cn=[u'admins'],
-                    gidnumber=[fuzzy_digits],
-                    member_user=[u'admin'],
-                    description=[u'Account administrators group'],
-                ),
-            ),
-        ),
-    ]
+        group.ensure_exists()
+        admins.add_member(dict(group=group.cn))
+        admins.remove_member(dict(group=group.cn))
 
 
 @pytest.mark.tier1
-class test_group_full_set_of_objectclass_not_available_post_detach(Declarative):
-    # https://fedorahosted.org/freeipa/ticket/4909#comment:1
-    cleanup_commands = [
-        ('group_del', [user1], {}),
-        ('user_del', [user1], {}),
-    ]
-
-    tests = [
-        dict(
-            desc='Create %r' % user1,
-            command=(
-                'user_add', [], dict(givenname=u'Test', sn=u'User1')
-            ),
-            expected=dict(
-                value=user1,
-                summary=u'Added user "%s"' % user1,
-                result=get_user_result(user1, u'Test', u'User1', 'add'),
-            ),
-        ),
-
-        dict(
-            desc='Detach managed group %r' % user1,
-            command=('group_detach', [user1], {}),
-            expected=dict(
-                result=True,
-                value=user1,
-                summary=u'Detached group "%s" from user "%s"' % (user1, user1),
-            ),
-        ),
-
-        dict(
-            desc='Show group - check objectclass',
-            command=('group_show', [user1], dict(all=True)),
-            expected=dict(
-                value=user1,
-                result={
-                    'cn':[user1],
-                    'description': [u'User private group for tuser1'],
-                    'gidnumber': [fuzzy_digits],
-                    'dn': get_group_dn('tuser1'),
-                    'ipauniqueid': [fuzzy_uuid],
-                    'objectclass': objectclasses.posixgroup,
-                },
-                summary=None,
-            ),
-        ),
-
-        dict(
-            desc='Add member back to the detached group',
-            command=('group_add_member', [user1], dict(user=user1)),
-            expected=dict(
-                completed=1,
-                failed=dict(
-                    member=dict(
-                        group=tuple(),
-                        user=tuple(),
-                    ),
-                ),
-                result={
-                        'dn': get_group_dn('tuser1'),
-                        'member_user': [user1],
-                        'gidnumber': [fuzzy_digits],
-                        'cn': [user1],
-                        'description': [u'User private group for tuser1'],
-                },
-            ),
-        ),
-    ]
+class TestValidation(XMLRPC_test):
+    # The assumption for this class of tests is that if we don't
+    # get a validation error then the request was processed normally.
+
+    def test_validation_disabled_on_delete(self, group):
+        """ Test that validation is disabled on group deletes """
+        command = group.make_command('group_del', invalidgroup1)
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % invalidgroup1)):
+            command()
+
+    def test_validation_disabled_on_show(self, group):
+        """ Test that validation is disabled on group retrieves """
+        command = group.make_command('group_show', invalidgroup1)
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % invalidgroup1)):
+            command()
+
+    def test_validation_disabled_on_mod(self, group):
+        """ Test that validation is disabled on group mods """
+        command = group.make_command('group_mod', invalidgroup1)
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % invalidgroup1)):
+            command()
+
+
+@pytest.mark.tier1
+class TestManagedGroups(XMLRPC_test):
+    def test_verify_managed_created(self, managed_group):
+        """ Verify that managed group is created with new user """
+        managed_group.retrieve()
+
+    def test_verify_managed_findable(self, managed_group):
+        """ Verify that managed group can be found """
+        command = managed_group.make_find_command(
+            **dict(cn=managed_group.cn, private=True)
+        )
+        result = command()
+        managed_group.check_find(result)
+
+    def test_delete_managed(self, managed_group):
+        """ Try to delete managed group """
+        command = managed_group.make_delete_command()
+        with raises_exact(errors.ManagedGroupError()):
+            command()
+
+    def test_detach_managed(self, managed_group):
+        """ Detach managed group from a user """
+        command = managed_group.make_detach_command()
+        result = command()
+        managed_group.check_detach(result)
+
+    def test_delete_detached_managed(self, managed_group, user):
+        """ Delete a previously managed group that is now detached
+        and verify it's really gone """
+        managed_group.delete()
+        command = managed_group.make_retrieve_command()
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % managed_group.cn)):
+            command()
+        user.ensure_missing()
+
+    def test_verify_managed_missing_for_user_without_upg(self, user_npg2):
+        """ Create a user without user private group and
+        verify private group wasn't created """
+        user_npg2.attrs.update(memberof_group=[u'ipausers'])
+        command = user_npg2.make_create_command()
+        result = command()
+        user_npg2.check_create(result, [u'description', u'memberof_group'])
+        command = user_npg2.make_command('group_show', *[user_npg2.uid])
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % user_npg2.uid)):
+            command()
+
+
+@pytest.mark.tier1
+class TestManagedGroupObjectclasses(XMLRPC_test):
+    def test_check_objectclasses_after_detach(self, user, managed_group):
+        """ Check objectclasses after user was detached from managed group """
+        # https://fedorahosted.org/freeipa/ticket/4909#comment:1
+        user.create()
+        user.run_command('group_detach', *[user.uid])
+        managed_group.retrieve(all=True)
+        managed_group.add_member(dict(user=user.uid))
+        managed_group.ensure_missing()
+        user.ensure_missing()
+
+
+@pytest.mark.tier1
+class TestAdminGroup(XMLRPC_test):
+    def test_remove_admin_from_admins(self, admins):
+        """ Remove the original admin from admins group """
+        command = admins.make_remove_member_command(
+            dict(user=u'admin')
+        )
+        with raises_exact(errors.LastMemberError(
+                key=u'admin', label=u'group', container=admins.cn)):
+            command()
+
+    def test_add_another_admin(self, admins, user):
+        """ Add second member to the admins group """
+        user.ensure_exists()
+        admins.add_member(dict(user=user.uid))
+
+    def test_remove_all_admins_from_admins(self, admins, user):
+        """ Try to remove both original and our admin from admins group """
+        command = admins.make_remove_member_command(
+            dict(user=[u'admin', user.uid])
+        )
+        with raises_exact(errors.LastMemberError(
+                key=u'admin', label=u'group', container=admins.cn)):
+            command()
+
+    def test_delete_admins(self, admins):
+        """ Try to delete the protected admins group """
+        command = admins.make_delete_command()
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=admins.cn, reason='privileged group')):
+            command()
+
+    def test_rename_admins(self, admins):
+        """ Try to rename the protected admins group """
+        command = admins.make_command('group_mod', *[admins.cn],
+                                      **dict(rename=renamedgroup1))
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=admins.cn, reason='Cannot be renamed')):
+            command()
+
+    def test_rename_admins_using_setattr(self, admins):
+        """ Try to rename the protected admins group using setattr """
+        command = admins.make_command('group_mod', *[admins.cn],
+                                      **dict(setattr=u'cn=%s' % renamedgroup1))
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=admins.cn, reason='Cannot be renamed')):
+            command()
+
+    def test_update_admins_to_support_external_membership(self, admins):
+        """ Try to modify the admins group to support external membership """
+        command = admins.make_command('group_mod', *[admins.cn],
+                                      **dict(external=True))
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=admins.cn,
+                          reason='Cannot support external non-IPA members')):
+            command()
+
+
+@pytest.mark.tier1
+class TestTrustAdminGroup(XMLRPC_test):
+    def test_delete_trust_admins(self, trustadmins):
+        """ Try to delete the protected 'trust admins' group """
+        command = trustadmins.make_delete_command()
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=trustadmins.cn, reason='privileged group')):
+            command()
+
+    def test_rename_trust_admins(self, trustadmins):
+        """ Try to rename the protected 'trust admins' group """
+        command = trustadmins.make_command('group_mod', *[trustadmins.cn],
+                                           **dict(rename=renamedgroup1))
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=trustadmins.cn, reason='Cannot be renamed')):
+            command()
+
+    def test_rename_trust_admins_using_setattr(self, trustadmins):
+        """ Try to rename the protected 'trust admins' group using setattr """
+        command = trustadmins.make_command(
+            'group_mod', *[trustadmins.cn],
+            **dict(setattr=u'cn=%s' % renamedgroup1)
+        )
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=trustadmins.cn, reason='Cannot be renamed')):
+            command()
+
+    def test_update_trust_admins_to_support_external_membership(
+            self, trustadmins
+    ):
+        """ Try to modify the 'trust admins' group to
+            support external membership """
+        command = trustadmins.make_command(
+            'group_mod', *[trustadmins.cn],
+            **dict(external=True)
+        )
+        with raises_exact(errors.ProtectedEntryError(label=u'group',
+                          key=trustadmins.cn,
+                          reason='Cannot support external non-IPA members')):
+            command()
diff --git a/ipatests/test_xmlrpc/test_stageuser_plugin.py b/ipatests/test_xmlrpc/test_stageuser_plugin.py
index 4eb968451f926ca0ee8fa5aeae1a96770f56eb45..bfe89c856deb7798a893c3086a32d3fe11acc80b 100644
--- a/ipatests/test_xmlrpc/test_stageuser_plugin.py
+++ b/ipatests/test_xmlrpc/test_stageuser_plugin.py
@@ -633,9 +633,7 @@ class TestGroups(XMLRPC_test):
     def test_remove_preserved_from_group(self, user, group):
         user.ensure_exists()
         group.ensure_exists()
-        command = group.make_add_member_command(options={u'user': user.uid})
-        result = command()
-        group.check_add_member(result)
+        command = group.add_member(options={u'user': user.uid})
 
         command = group.make_retrieve_command()
         result = command()
diff --git a/ipatests/test_xmlrpc/tracker/group_plugin.py b/ipatests/test_xmlrpc/tracker/group_plugin.py
index c47ce8ecf185f6491847b8fd7894abc249381004..cadc2706bd7a37000eb3f320bf63dcc86a6dc0d9 100644
--- a/ipatests/test_xmlrpc/tracker/group_plugin.py
+++ b/ipatests/test_xmlrpc/tracker/group_plugin.py
@@ -8,11 +8,14 @@ from ipatests.test_xmlrpc.xmlrpc_test import fuzzy_digits, fuzzy_uuid
 from ipatests.test_xmlrpc.tracker.base import Tracker
 from ipatests.util import assert_deepequal, get_group_dn
 
+from ipalib import api
+from ipapython.dn import DN
+
 
 class GroupTracker(Tracker):
     """ Class for host plugin like tests """
     retrieve_keys = {u'dn', u'cn', u'gidnumber', u'member_user',
-                     u'member_group'}
+                     u'member_group', u'description'}
     retrieve_all_keys = retrieve_keys | {u'ipauniqueid', u'objectclass'}
 
     create_keys = retrieve_all_keys
@@ -20,16 +23,19 @@ class GroupTracker(Tracker):
 
     add_member_keys = retrieve_keys | {u'description'}
 
-    def __init__(self, name):
+    def __init__(self, name, description=u'Group desc'):
         super(GroupTracker, self).__init__(default_version=None)
         self.cn = name
-        self.dn = get_group_dn(name)
+        self.description = description
+        self.dn = get_group_dn(self.cn)
 
     def make_create_command(self, nonposix=False, external=False,
-                            force=True):
+                            force=True, *args, **kwargs):
         """ Make function that creates a group using 'group-add' """
         return self.make_command('group_add', self.cn,
-                                 nonposix=nonposix, external=external)
+                                 description=self.description,
+                                 nonposix=nonposix, external=external,
+                                 *args, **kwargs)
 
     def make_delete_command(self):
         """ Make function that deletes a group using 'group-del' """
@@ -48,23 +54,12 @@ class GroupTracker(Tracker):
         return self.make_command('group_mod', self.cn, **updates)
 
     def make_add_member_command(self, options={}):
-        """ Make function that adds a member to a group
-        Attention: only works for one user OR group! """
-        if u'user' in options:
-            self.attrs[u'member_user'] = [options[u'user']]
-        elif u'group' in options:
-            self.attrs[u'member_group'] = [options[u'group']]
+        """ Make function that adds a member to a group """
         self.adds = options
-
         return self.make_command('group_add_member', self.cn, **options)
 
     def make_remove_member_command(self, options={}):
-        """ Make function that removes a member from a group
-        Attention: only works for one user OR group! """
-        if u'user' in options:
-            del self.attrs[u'member_user']
-        elif u'group' in options:
-            del self.attrs[u'member_group']
+        """ Make function that removes a member from a group """
         return self.make_command('group_remove_member', self.cn, **options)
 
     def make_detach_command(self):
@@ -78,12 +73,85 @@ class GroupTracker(Tracker):
         self.attrs = dict(
             dn=get_group_dn(self.cn),
             cn=[self.cn],
+            description=[self.description],
             gidnumber=[fuzzy_digits],
             ipauniqueid=[fuzzy_uuid],
             objectclass=objectclasses.posixgroup,
             )
         self.exists = True
 
+    def update(self, updates, expected_updates=None):
+        """Helper function to update this user and check the result
+
+        Overriding Tracker method for setting self.attrs correctly;
+         * most attributes stores its value in list
+         * the rest can be overridden by expected_updates
+         * allow deleting parametrs if update value is None
+        """
+        if expected_updates is None:
+            expected_updates = {}
+
+        self.ensure_exists()
+        command = self.make_update_command(updates)
+        result = command()
+
+        for key, value in updates.iteritems():
+            if value is None:
+                del self.attrs[key]
+            else:
+                self.attrs[key] = [value]
+        for key, value in expected_updates.iteritems():
+            if value is None:
+                del self.attrs[key]
+            else:
+                self.attrs[key] = value
+
+        self.check_update(
+            result,
+            extra_keys=set(updates.keys()) | set(expected_updates.keys())
+        )
+
+    def add_member(self, options):
+        """ Add a member (group OR user) and performs check """
+        if u'user' in options:
+            try:
+                self.attrs[u'member_user'] =\
+                    self.attrs[u'member_user'] + [options[u'user']]
+            except KeyError as ex:
+                self.attrs[u'member_user'] = [options[u'user']]
+        elif u'group' in options:
+            try:
+                self.attrs[u'member_group'] =\
+                    self.attrs[u'member_group'] + [options[u'group']]
+            except KeyError as ex:
+                self.attrs[u'member_group'] = [options[u'group']]
+
+        command = self.make_add_member_command(options)
+        result = command()
+        self.check_add_member(result)
+
+    def remove_member(self, options):
+        """ Remove a member (group OR user) and performs check """
+        if u'user' in options:
+            self.attrs[u'member_user'].remove(options[u'user'])
+        elif u'group' in options:
+            self.attrs[u'member_group'].remove(options[u'group'])
+
+        try:
+            if not self.attrs[u'member_user']:
+                del self.attrs[u'member_user']
+        except KeyError as ex:
+            pass
+        try:
+            if not self.attrs[u'member_group']:
+                del self.attrs[u'member_group']
+        except KeyError as ex:
+            pass
+
+        command = self.make_remove_member_command(options)
+        result = command()
+        self.check_remove_member(result)
+
     def check_create(self, result):
         """ Checks 'group_add' command result """
         assert_deepequal(dict(
@@ -143,35 +211,48 @@ class GroupTracker(Tracker):
             result=self.filter_attrs(self.add_member_keys)
         ), result)
 
-    def check_add_member_negative(self, result):
-        """ Checks 'group_add_member' command result when expected result
-        is failure of the operation"""
-        if u'member_user' in self.attrs:
-            del self.attrs[u'member_user']
-        elif u'member_group' in self.attrs:
-            del self.attrs[u'member_group']
+    def check_add_member_negative(self, result, options={}):
+        """ Checks 'group_add_member' command result
+        when expected result is failure of the operation"""
+        expected = dict(
+            completed=0,
+            failed={u'member': {u'group': (), u'user': ()}},
+            result=self.filter_attrs(self.add_member_keys)
+        )
+        if not options:
+            try:
+                options = self.adds
+            except NameError:
+                pass
+        if u'user' in options:
+            expected[u'failed'][u'member'][u'user'] = [(
+                options[u'user'], u'no such entry')]
+        elif u'group' in options:
+            expected[u'failed'][u'member'][u'group'] = [(
+                options[u'group'], u'no such entry')]
+
+        assert_deepequal(expected, result)
 
+    def check_remove_member_negative(self, result, options):
+        """ Checks 'group_remove_member' command result
+        when expected result is failure of the operation"""
         expected = dict(
             completed=0,
             failed={u'member': {u'group': (), u'user': ()}},
             result=self.filter_attrs(self.add_member_keys)
         )
-        if u'user' in self.adds:
+        if u'user' in options:
             expected[u'failed'][u'member'][u'user'] = [(
-                self.adds[u'user'], u'no such entry')]
-        elif u'group' in self.adds:
+                options[u'user'], u'This entry is not a member')]
+        elif u'group' in options:
             expected[u'failed'][u'member'][u'group'] = [(
-                self.adds[u'group'], u'no such entry')]
+                options[u'group'], u'This entry is not a member')]
 
         assert_deepequal(expected, result)
 
     def check_remove_member(self, result):
         """ Checks 'group_remove_member' command result """
-        assert_deepequal(dict(
-            completed=1,
-            failed={u'member': {u'group': (), u'user': ()}},
-            result=self.filter_attrs(self.add_member_keys)
-        ), result)
+        self.check_add_member(result)
 
     def check_detach(self, result):
         """ Checks 'group_detach' command result """
-- 
2.4.6

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to