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.
> 
> 

>From d6e30ee42ea427e9a2d5a85a787eddffd557eeba 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    | 1881 +++++++++-----------------
 ipatests/test_xmlrpc/tracker/__init__.py     |   22 +
 ipatests/test_xmlrpc/tracker/group_plugin.py |  303 +++++
 3 files changed, 927 insertions(+), 1279 deletions(-)
 create mode 100644 ipatests/test_xmlrpc/tracker/__init__.py
 create mode 100644 ipatests/test_xmlrpc/tracker/group_plugin.py

diff --git a/ipatests/test_xmlrpc/test_group_plugin.py b/ipatests/test_xmlrpc/test_group_plugin.py
index ed38c696e643a394510b6cbf7988f8c17520daf4..4350f128bad6306ac37492a8f5fdbb74b64478ab 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,1325 +27,647 @@ import pytest
 
 from ipalib import api, errors
 from ipatests.test_xmlrpc import objectclasses
-from 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.ldaptracker import Tracker
-from ipatests.test_xmlrpc.test_user_plugin import UserTracker
+from ipatests.test_xmlrpc.tracker.group_plugin import GroupTracker
+from ipatests.test_xmlrpc.test_user_plugin import UserTracker, user, user_npg
 from ipatests.util import assert_deepequal
 
 
-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'
 
 def get_group_dn(cn):
     return DN(('cn', cn), api.env.container_group, api.env.basedn)
 
 
-@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',
-            ),
-        ),
-
+@pytest.fixture(scope='class')
+def group(request):
+    tracker = GroupTracker(name=u'testgroup1', description=u'Test desc1')
+    return tracker.make_fixture(request)
+
+
+@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
 
-        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"',
-            ),
-        ),
+@pytest.mark.tier1
+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()
 
 
-        ###############
-        # 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'],
+@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': group.get_group_dn('admins'),
+                    'member_user': [u'admin'],
+                    'gidnumber': [fuzzy_digits],
+                    'cn': [u'admins'],
+                    'description': [u'Account administrators group'],
                 },
-            ),
-        ),
-
-        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': group.get_group_dn('editors'),
+                    'gidnumber': [fuzzy_digits],
+                    'cn': [u'editors'],
+                    'description':
+                        [u'Limited admins who can edit other users'],
                 },
-            ),
-        ),
-
-        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],
+                {
+                    'dn': group.get_group_dn('ipausers'),
+                    'cn': [u'ipausers'],
+                    'description': [u'Default group for all users'],
+                },
+                {
+                    'dn': group.get_group_dn(),
+                    'cn': [group.cn],
+                    'description': [u'Test desc1'],
                     'gidnumber': [fuzzy_digits],
-                    'description': [u'New desc 1'],
                 },
-            ),
-        ),
-
-        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': group2.get_group_dn(),
+                    'cn': [group2.cn],
+                    'description': [u'Test desc2'],
                     'gidnumber': [fuzzy_digits],
-                    'description': [u'New desc 1'],
                 },
-            ),
-        ),
-
-
-        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': group.get_group_dn('trust admins'),
+                    'member_user': [u'admin'],
+                    'cn': [u'trust admins'],
+                    'description': [u'Trusts administrators group'],
                 },
-            ),
-        ),
-
-        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],
-            ),
-        ),
-    ]
-
-
-@pytest.mark.tier1
-class test_group_remove_group_from_protected_group(Declarative):
-    cleanup_commands = [
-        ('group_del', [group1], {}),
-    ]
-    tests = [
-        # 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'],
-                ),
-            ),
-        ),
-    ]
-
-
-@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'],
+            ]), 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': group.get_group_dn('admins'),
+                    'member_user': [u'admin'],
                     'gidnumber': [fuzzy_digits],
-                    'dn': get_group_dn('tuser1'),
+                    'cn': [u'admins'],
+                    'description': [u'Account administrators group'],
+                    'objectclass': fuzzy_set_ci(add_oc(
+                        objectclasses.posixgroup, u'ipantgroupattrs')),
                     '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'],
+                {
+                    'dn': group.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')),
+                    'ipauniqueid': [fuzzy_uuid],
                 },
-            ),
-        ),
-    ]
-
-
-class GroupTracker(Tracker):
-    """ Class for host plugin like tests """
-    retrieve_keys = {u'dn', u'cn', u'gidnumber', u'member_user',
-                     u'member_group'}
-    retrieve_all_keys = retrieve_keys | {u'ipauniqueid', u'objectclass'}
-
-    create_keys = retrieve_all_keys
-    update_keys = retrieve_keys - {u'dn'}
-
-    add_member_keys = retrieve_keys | {u'description'}
-
-    def __init__(self, name):
-        super(GroupTracker, self).__init__(default_version=None)
-        self.cn = name
-        self.dn = get_group_dn(name)
-
-    def make_create_command(self, nonposix=False, external=False,
-                            force=True):
-        """ Make function that creates a group using 'group-add' """
-        return self.make_command('group_add', self.cn,
-                                 nonposix=nonposix, external=external)
-
-    def make_delete_command(self):
-        """ Make function that deletes a group using 'group-del' """
-        return self.make_command('group_del', self.cn)
-
-    def make_retrieve_command(self, all=False, raw=False):
-        """ Make function that retrieves a group using 'group-show' """
-        return self.make_command('group_show', self.cn, all=all)
-
-    def make_find_command(self, *args, **kwargs):
-        """ Make function that searches for a group using 'group-find' """
-        return self.make_command('group_find', *args, **kwargs)
-
-    def make_update_command(self, updates):
-        """ Make function that updates a group using 'group-mod' """
-        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']]
-        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']
-        return self.make_command('group_remove_member', self.cn, **options)
-
-    def make_detach_command(self):
-        """ Make function that detaches a managed group using
-        'group-detach' """
-        self.exists = True
-        return self.make_command('group_detach', self.cn)
-
-    def track_create(self):
-        """ Updates expected state for group creation"""
-        self.attrs = dict(
-            dn=get_group_dn(self.cn),
-            cn=[self.cn],
-            gidnumber=[fuzzy_digits],
-            ipauniqueid=[fuzzy_uuid],
-            objectclass=objectclasses.posixgroup,
-            )
-        self.exists = True
-
-    def check_create(self, result):
-        """ Checks 'group_add' command result """
-        assert_deepequal(dict(
-            value=self.cn,
-            summary=u'Added group "%s"' % self.cn,
-            result=self.filter_attrs(self.create_keys)
-            ), result)
-
-    def check_delete(self, result):
-        """ Checks 'group_del' command result """
-        assert_deepequal(dict(
-            value=[self.cn],
-            summary=u'Deleted group "%s"' % self.cn,
-            result=dict(failed=[]),
-            ), result)
+                {
+                    'dn': group.get_group_dn(),
+                    'cn': [group.cn],
+                    'description': [u'Test desc1'],
+                    'gidnumber': [fuzzy_digits],
+                    'objectclass': fuzzy_set_ci(add_oc(
+                        objectclasses.posixgroup, u'ipantgroupattrs')),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+                {
+                    'dn': group2.get_group_dn(),
+                    'cn': [group2.cn],
+                    'description': [u'Test desc2'],
+                    'gidnumber': [fuzzy_digits],
+                    'objectclass': fuzzy_set_ci(add_oc(
+                        objectclasses.posixgroup, u'ipantgroupattrs')),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+            ]), result)
 
-    def check_retrieve(self, result, all=False, raw=False):
-        """ Checks 'group_show' command result """
-        if all:
-            expected = self.filter_attrs(self.retrieve_all_keys)
-        else:
-            expected = self.filter_attrs(self.retrieve_keys)
 
-        assert_deepequal(dict(
-            value=self.cn,
-            summary=None,
-            result=expected
-            ), 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()
 
-    def check_find(self, result, all=False, raw=False):
-        """ Checks 'group_find' command result """
-        if all:
-            expected = self.filter_attrs(self.retrieve_all_keys)
-        else:
-            expected = self.filter_attrs(self.retrieve_keys)
 
+@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(
-            count=1,
+            summary=u'3 groups matched',
+            count=3,
             truncated=False,
+            result=[
+                {
+                    'dn': group.get_group_dn('ipausers'),
+                    'cn': [u'ipausers'],
+                    'description': [u'Default group for all users'],
+                    'objectclass': fuzzy_set_ci(objectclasses.group),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+                {
+                    'dn': group.get_group_dn(group.cn),
+                    'cn': [group.cn],
+                    'description': [u'New desc'],
+                    'objectclass': fuzzy_set_ci(objectclasses.group),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+                {
+                    'dn': group.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',
-            result=[expected],
+            count=1,
+            truncated=False,
+            result=[
+                {
+                    'dn': group.get_group_dn('ipausers'),
+                    'cn': [u'ipausers'],
+                    'description': [u'Default group for all users'],
+                    'objectclass': fuzzy_set_ci(objectclasses.group),
+                    'ipauniqueid': [fuzzy_uuid],
+                },
+            ],
         ), result)
 
-    def check_update(self, result, extra_keys={}):
-        """ Checks 'group_mod' command result """
-        assert_deepequal(dict(
-            value=self.cn,
-            summary=u'Modified group "%s"' % self.cn,
-            result=self.filter_attrs(self.update_keys | set(extra_keys))
-        ), result)
 
-    def check_add_member(self, result):
-        """ Checks 'group_add_member' command result """
-        assert_deepequal(dict(
-            completed=1,
-            failed={u'member': {u'group': (), u'user': ()}},
-            result=self.filter_attrs(self.add_member_keys)
-        ), 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 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']
-
-        expected = dict(
-            completed=0,
-            failed={u'member': {u'group': (), u'user': ()}},
-            result=self.filter_attrs(self.add_member_keys)
+    def test_search_for_external(self, group):
+        """ Search for all external groups """
+        command = group.make_command(
+            'group_find', **dict(external=True, all=True)
         )
-        if u'user' in self.adds:
-            expected[u'failed'][u'member'][u'user'] = [(
-                self.adds[u'user'], u'no such entry')]
-        elif u'group' in self.adds:
-            expected[u'failed'][u'member'][u'group'] = [(
-                self.adds[u'group'], u'no such entry')]
-
-        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)
+        result = command()
+        group.check_find(result, all=True)
 
-    def check_detach(self, result):
-        """ Checks 'group_detach' command result """
-        assert_deepequal(dict(
-            value=self.cn,
-            summary=u'Detached group "%s" from user "%s"' % (
-                self.cn, self.cn),
-            result=True
-        ), result)
+    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 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
+        group.ensure_exists()
+        admins.add_member(dict(group=group.cn))
+        admins.remove_member(dict(group=group.cn))
+
+
+@pytest.mark.tier1
+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_npg):
+        """ Create a user without user private group and
+        verify private group wasn't created """
+        user_npg.ensure_exists()
+        command = user_npg.make_command('group_show', *[user_npg.uid])
+        with raises_exact(errors.NotFound(
+                reason=u'%s: group not found' % user_npg.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 make_fixture_detach(self, request):
-        """Make a pytest fixture for this tracker
+    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))
 
-        The fixture ensures the plugin entry does not exist before
-        and after the tests that use itself.
-        """
-        def cleanup():
-            pass
+    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()
 
-        request.addfinalizer(cleanup)
 
-        return self
+@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/tracker/__init__.py b/ipatests/test_xmlrpc/tracker/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a8ecf1c263bbdbd5e8e28fc38c1b3688154ab6a
--- /dev/null
+++ b/ipatests/test_xmlrpc/tracker/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+#   Jason Gerard DeRose <jder...@redhat.com>
+#
+# Copyright (C) 2008  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Sub-package containing unit tests for `xmlrpc` package.
+"""
diff --git a/ipatests/test_xmlrpc/tracker/group_plugin.py b/ipatests/test_xmlrpc/tracker/group_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..72cd7e274f506d8af3818dc0331a508847c68355
--- /dev/null
+++ b/ipatests/test_xmlrpc/tracker/group_plugin.py
@@ -0,0 +1,303 @@
+# 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
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import api
+from ipapython.dn import DN
+from ipatests.test_xmlrpc import objectclasses
+from ipatests.test_xmlrpc.xmlrpc_test import fuzzy_digits, fuzzy_uuid
+
+from ipatests.test_xmlrpc.ldaptracker import Tracker
+from ipatests.util import assert_deepequal
+
+
+class GroupTracker(Tracker):
+    """ Class for host plugin like tests """
+    retrieve_keys = {u'dn', u'cn', u'gidnumber', u'member_user',
+                     u'member_group', u'description'}
+    retrieve_all_keys = retrieve_keys | {u'ipauniqueid', u'objectclass'}
+
+    create_keys = retrieve_all_keys
+    update_keys = retrieve_keys - {u'dn'}
+
+    add_member_keys = retrieve_keys | {u'description'}
+
+    def __init__(self, name, description=u'Group desc'):
+        super(GroupTracker, self).__init__(default_version=None)
+        self.cn = name
+        self.description = description
+        self.dn = self.get_group_dn()
+
+    def make_create_command(self, nonposix=False, external=False,
+                            force=True, *args, **kwargs):
+        """ Make function that creates a group using 'group-add' """
+        return self.make_command('group_add', self.cn,
+                                 description=self.description,
+                                 nonposix=nonposix, external=external,
+                                 *args, **kwargs)
+
+    def make_delete_command(self):
+        """ Make function that deletes a group using 'group-del' """
+        return self.make_command('group_del', self.cn)
+
+    def make_retrieve_command(self, all=False, raw=False):
+        """ Make function that retrieves a group using 'group-show' """
+        return self.make_command('group_show', self.cn, all=all)
+
+    def make_find_command(self, *args, **kwargs):
+        """ Make function that searches for a group using 'group-find' """
+        return self.make_command('group_find', *args, **kwargs)
+
+    def make_update_command(self, updates):
+        """ Make function that updates a group using 'group-mod' """
+        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 """
+        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 """
+        return self.make_command('group_remove_member', self.cn, **options)
+
+    def make_detach_command(self):
+        """ Make function that detaches a managed group using
+        'group-detach' """
+        self.exists = True
+        return self.make_command('group_detach', self.cn)
+
+    def track_create(self):
+        """ Updates expected state for group creation"""
+        self.attrs = dict(
+            dn=self.get_group_dn(),
+            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(
+            value=self.cn,
+            summary=u'Added group "%s"' % self.cn,
+            result=self.filter_attrs(self.create_keys)
+            ), result)
+
+    def check_delete(self, result):
+        """ Checks 'group_del' command result """
+        assert_deepequal(dict(
+            value=[self.cn],
+            summary=u'Deleted group "%s"' % self.cn,
+            result=dict(failed=[]),
+            ), result)
+
+    def check_retrieve(self, result, all=False, raw=False):
+        """ Checks 'group_show' command result """
+        if all:
+            expected = self.filter_attrs(self.retrieve_all_keys)
+        else:
+            expected = self.filter_attrs(self.retrieve_keys)
+
+        assert_deepequal(dict(
+            value=self.cn,
+            summary=None,
+            result=expected
+            ), result)
+
+    def check_find(self, result, all=False, raw=False):
+        """ Checks 'group_find' command result """
+        if all:
+            expected = self.filter_attrs(self.retrieve_all_keys)
+        else:
+            expected = self.filter_attrs(self.retrieve_keys)
+
+        assert_deepequal(dict(
+            count=1,
+            truncated=False,
+            summary=u'1 group matched',
+            result=[expected],
+        ), result)
+
+    def check_update(self, result, extra_keys={}):
+        """ Checks 'group_mod' command result """
+        assert_deepequal(dict(
+            value=self.cn,
+            summary=u'Modified group "%s"' % self.cn,
+            result=self.filter_attrs(self.update_keys | set(extra_keys))
+        ), result)
+
+    def check_add_member(self, result):
+        """ Checks 'group_add_member' command result """
+        assert_deepequal(dict(
+            completed=1,
+            failed={u'member': {u'group': (), u'user': ()}},
+            result=self.filter_attrs(self.add_member_keys)
+        ), result)
+
+    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 options:
+            expected[u'failed'][u'member'][u'user'] = [(
+                options[u'user'], u'This entry is not a member')]
+        elif u'group' in options:
+            expected[u'failed'][u'member'][u'group'] = [(
+                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 """
+        self.check_add_member(result)
+
+    def check_remove_member(self, result):
+        """ Checks 'group_remove_member' command result """
+        self.check_add_member(result)
+
+    def check_detach(self, result):
+        """ Checks 'group_detach' command result """
+        assert_deepequal(dict(
+            value=self.cn,
+            summary=u'Detached group "%s" from user "%s"' % (
+                self.cn, self.cn),
+            result=True
+        ), result)
+
+    def make_fixture_detach(self, request):
+        """Make a pytest fixture for this tracker
+
+        The fixture ensures the plugin entry does not exist before
+        and after the tests that use itself.
+        """
+        def cleanup():
+            pass
+
+        request.addfinalizer(cleanup)
+
+        return self
+
+    def get_group_dn(self, cn=None):
+        if not cn:
+            cn = self.cn
+
+        return DN(('cn', cn), api.env.container_group, api.env.basedn)
-- 
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