URL: https://github.com/freeipa/freeipa/pull/310
Author: mirielka
 Title: #310: WIP: CLI testing
Action: opened

PR body:
"""
Here is basic part of CLI testing for you to take a look at and provide 
feedback, before it's all done and polished up.

How it works: so far it's only tuned to run stageuser tests, so use 
`./make-tests ipatests/test_xmlrpc/test_stageuser_plugin.py --cli` to run in 
CLI mode, or of course without the --cli option to run original API tests. Note 
that last three tests are expected to fail in CLI mode as group tracker has not 
been modified for CLI testing yet.

What gets changed: 
- '--cli' option is added
- base tracker: changes that should ensure the basic functionality to run 
existing tests in CLI mode, this includes way how to execute commands and 
mapping of defined API options and output values to the CLI style (the idea of 
the mapping was to convert the text output from CLI to API style output so that 
more extensive methods to compare CLI output with expected values do not have 
to be created)
- specific trackers: changes are needed because CLI calls have different output 
than API calls (some messages are different, some items of API output are not 
present in CLI). So far I've modified stageuser tracker fully (i.e. that one 
should be final) and user tracker partly (just so that stageuser tests that use 
user tracker work properly, but user tests will not work in CLI mode yet).
- xmlrpc_test.py: unfortunately I failed to find a way how to raise the 
exception from subprocess so that it would fit the exception comparison using 
raises_exact method, so I chose to raise ExecutionError when it occurs in 
subprocess and compare just the error message in stderr. Since I expect the CLI 
tests to be run along with API tests, any change in type of raised error should 
be cought by API tests.

So far I observed:
- positives: no tests need to be changed, all the necessary changes are in 
trackers (which was the intention)
- negatives: only works for tracker based tests, i.e. not for declarative tests 
or tests that use just api.Command style; tests run longer that API tests

So please, if you have any comment and suggestions, I'll be glad to hear them 
so that I can polish this and finish the modifications.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/310/head:pr310
git checkout pr310
From bf7cdbd506f6bda731aa68f44bd08d84c8136938 Mon Sep 17 00:00:00 2001
From: Lenka Doudova <ldoud...@redhat.com>
Date: Tue, 6 Dec 2016 09:31:20 +0100
Subject: [PATCH 1/3] WIP: CLI testing - new option to run tests

Add --cli option to ipa-run-tests/make-test command
---
 ipatests/pytest.ini                |  1 +
 ipatests/pytest_plugins/tracker.py | 12 ++++++++++++
 2 files changed, 13 insertions(+)
 create mode 100644 ipatests/pytest_plugins/tracker.py

diff --git a/ipatests/pytest.ini b/ipatests/pytest.ini
index b2497cb..b60c092 100644
--- a/ipatests/pytest.ini
+++ b/ipatests/pytest.ini
@@ -13,6 +13,7 @@ addopts = --doctest-modules
           -p ipatests.pytest_plugins.integration
           -p ipatests.pytest_plugins.beakerlib
           -p ipatests.pytest_plugins.additional_config
+          -p ipatests.pytest_plugins.tracker
             # Ignore files for doc tests.
             # TODO: ideally, these should all use __name__=='__main__' guards
           --ignore=ipasetup.py
diff --git a/ipatests/pytest_plugins/tracker.py b/ipatests/pytest_plugins/tracker.py
new file mode 100644
index 0000000..5b9e7e1
--- /dev/null
+++ b/ipatests/pytest_plugins/tracker.py
@@ -0,0 +1,12 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+import pytest
+
+
+def pytest_addoption(parser):
+    group = parser.getgroup("IPA tracker tests")
+
+    group.addoption('--cli', dest='cli', action='store_true', default=False,
+                    help="Run tests as CLI instead of API.")

From f2dc1a3bcee2133c36ad07beb54695cafac2263c Mon Sep 17 00:00:00 2001
From: Lenka Doudova <ldoud...@redhat.com>
Date: Tue, 6 Dec 2016 09:32:32 +0100
Subject: [PATCH 2/3] WIP: CLI testing - base tracker

Extend base tracker to have functionality to run existing tests in CLI mode if --cli option is specified.
---
 ipatests/test_xmlrpc/tracker/base.py | 129 +++++++++++++++++++++++++++++++----
 ipatests/test_xmlrpc/xmlrpc_test.py  |   8 ++-
 2 files changed, 123 insertions(+), 14 deletions(-)

diff --git a/ipatests/test_xmlrpc/tracker/base.py b/ipatests/test_xmlrpc/tracker/base.py
index aa88e6b..c055b36 100644
--- a/ipatests/test_xmlrpc/tracker/base.py
+++ b/ipatests/test_xmlrpc/tracker/base.py
@@ -14,6 +14,12 @@
 from ipapython.version import API_VERSION
 from ipatests.util import Fuzzy
 
+import subprocess
+import os
+import re
+import pytest
+from ipalib.cli import to_cli
+
 
 class Tracker(object):
     """Wraps and tracks modifications to a plugin LDAP entry object
@@ -70,6 +76,22 @@ class Tracker(object):
     create_keys = None
     update_keys = None
 
+    # Mapping of API style options and output to CLI style options and output
+    mapping_options = {
+        'addattr': 'addattr',
+        'all': 'all',
+        'setattr': 'setattr',
+        'raw': 'raw',
+        'version': 'version',
+        }
+    mapping_output = {}
+
+    # Indicator for skipping errors that are not handled during CLI testing
+    skip_error = False
+
+    # List of options that do not take value
+    novalue = ['all', 'raw']
+
     _override_me_msg = "This method needs to be overridden in a subclass"
 
     def __init__(self, default_version=None):
@@ -111,26 +133,105 @@ def filter_attrs(self, keys):
             raise RuntimeError('The tracker instance has no attributes.')
         return {k: v for k, v in self.attrs.items() if k in keys}
 
+    def cli_command(self, name, *args, **options):
+        """
+        Prepare CLI command for testing
+        """
+        cmd = "ipa {}".format(to_cli(name))
+        for item in args:
+            cmd += " {}".format(item)
+        for key in options:
+            if key in self.novalue and options[key]:
+                cmd += " --{0}".format(self.mapping_options[key])
+            elif key in self.novalue and not options[key]:
+                continue
+            else:
+                if type(options[key]) is unicode and ' ' in options[key]:
+                    options[key] = '"{}"'.format(options[key])
+                cmd += " --{0}={1}".format(
+                    self.mapping_options[key], options[key])
+        return cmd
+
+    def cli_output(self, result):
+        """
+        Reformats text output to dictionary to be compared with expected values
+        """
+        result = result.split("\n")
+        modresult = {'result': {}, 'summary': None}
+        for line in result:
+            if re.match("^-*$", line):
+                continue
+            elif ':' in line:
+                key = self.mapping_output[line.split(':')[0].strip()]
+                value = line.split(':')[1].strip()
+                if value == 'True':
+                    value = True
+                elif value == 'False':
+                    value = False
+                else:
+                    value = unicode(value)
+
+                if key in modresult['result']:
+                    modresult['result'][key].append(value)
+                elif value is True or value is False:
+                    modresult['result'][key] = value
+                else:
+                    modresult['result'][key] = [value]
+            else:
+                modresult['summary'] = unicode(line)
+
+        return modresult
+
+    def cli_mode(self):
+        """ Returns True if CLI testing mode is on, False otherwise """
+        return pytest.config.getoption('cli')
+
     def run_command(self, name, *args, **options):
         """Run the given IPA command
 
         Logs the command using print for easier debugging
-        """
-        cmd = self.api.Command[name]
 
-        options.setdefault('version', self.default_version)
+        Run in CLI testing mode if executed with --cli option,
+        run in API mode otherwise.
+        """
+        if self.cli_mode():
+            cmd = self.cli_command(name, *args, **options)
+            subprocess_env = os.environ.copy()
+            del subprocess_env["IPA_UNIT_TEST_MODE"]
+
+            cmd = subprocess.Popen(cmd, shell=True,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE,
+                                   env=subprocess_env)
+            output = cmd.communicate()
+            result, error = output
+
+            if error and not self.skip_error:
+                # if stderr is not empty and the error is not supposed to be
+                # ignored, raise error
+                raise(errors.ExecutionError(message=unicode(error[12:-1])))
+            elif self.skip_error:
+                self.skip_error = False
+
+            result = self.cli_output(result)
 
-        args_repr = ', '.join(
-            [repr(a) for a in args] +
-            ['%s=%r' % item for item in list(options.items())])
-        try:
-            result = cmd(*args, **options)
-        except Exception as e:
-            print('Ran command: %s(%s): %s: %s' % (cmd, args_repr,
-                                                   type(e).__name__, e))
-            raise
         else:
-            print('Ran command: %s(%s): OK' % (cmd, args_repr))
+            cmd = self.api.Command[name]
+
+            options.setdefault('version', self.default_version)
+
+            args_repr = ', '.join(
+                [repr(a) for a in args] +
+                ['%s=%r' % item for item in list(options.items())])
+            try:
+                result = cmd(*args, **options)
+            except Exception as e:
+                print('Ran command: %s(%s): %s: %s' % (cmd, args_repr,
+                                                       type(e).__name__, e))
+                raise
+            else:
+                print('Ran command: %s(%s): OK' % (cmd, args_repr))
+
         return result
 
     def make_command(self, name, *args, **options):
@@ -145,6 +246,7 @@ def make_fixture(self, request):
         """
         del_command = self.make_delete_command()
         try:
+            self.skip_error = True
             del_command()
         except errors.NotFound:
             pass
@@ -152,6 +254,7 @@ def make_fixture(self, request):
         def cleanup():
             existed = self.exists
             try:
+                self.skip_error = True
                 del_command()
             except errors.NotFound:
                 if existed:
diff --git a/ipatests/test_xmlrpc/xmlrpc_test.py b/ipatests/test_xmlrpc/xmlrpc_test.py
index 0ce1245..069c7e3 100644
--- a/ipatests/test_xmlrpc/xmlrpc_test.py
+++ b/ipatests/test_xmlrpc/xmlrpc_test.py
@@ -28,6 +28,7 @@
 import nose
 import contextlib
 import six
+import pytest
 
 from ipatests.util import assert_deepequal, Fuzzy
 from ipalib import api, request, errors
@@ -390,10 +391,15 @@ def raises_exact(expected_exception):
     >>> with raises_exact(errors.ValidationError(name='x', error='y')):
     ...     raise errors.ValidationError(name='x', error='y')
     """
+
+
     try:
         yield
     except errors.PublicError as got_exception:
-        assert type(expected_exception) is type(got_exception)
+        if pytest.config.getoption('cli'):
+            assert type(got_exception) is errors.ExecutionError
+        else:
+            assert type(expected_exception) is type(got_exception)
         # FIXME: We should return error information in a structured way.
         # For now just compare the strings
         assert expected_exception.strerror == got_exception.strerror

From fbaf4115dff58a85ef77e26291914c5a5570eb05 Mon Sep 17 00:00:00 2001
From: Lenka Doudova <ldoud...@redhat.com>
Date: Tue, 6 Dec 2016 09:33:48 +0100
Subject: [PATCH 3/3] WIP: CLI testing - specific trackers

Modify specific (so far stageuser and user) trackers to properly handle CLI testing.
---
 ipatests/test_xmlrpc/tracker/stageuser_plugin.py | 333 +++++++++++++++++++----
 ipatests/test_xmlrpc/tracker/user_plugin.py      | 270 ++++++++++++++----
 2 files changed, 497 insertions(+), 106 deletions(-)

diff --git a/ipatests/test_xmlrpc/tracker/stageuser_plugin.py b/ipatests/test_xmlrpc/tracker/stageuser_plugin.py
index 82d7e06..df7f5c5 100644
--- a/ipatests/test_xmlrpc/tracker/stageuser_plugin.py
+++ b/ipatests/test_xmlrpc/tracker/stageuser_plugin.py
@@ -3,6 +3,7 @@
 #
 
 import six
+import re
 
 from ipalib import api, errors
 
@@ -48,11 +49,16 @@ class StageUserTracker(Tracker):
     retrieve_all_keys = retrieve_keys | {
         u'cn', u'ipauniqueid', u'objectclass', u'description',
         u'displayname', u'gecos', u'initials', u'manager'}
+    retrieve_cli_keys = retrieve_keys - {u'dn', u'nsaccountlock'}
 
     create_keys = retrieve_all_keys | {
         u'objectclass', u'ipauniqueid', u'randompassword',
         u'userpassword', u'krbextradata', u'krblastpwdchange',
         u'krbpasswordexpiration', u'krbprincipalkey'}
+    create_cli_keys = create_keys - {
+        u'description', u'dn', u'ipauniqueid', u'nsaccountlock',
+        u'objectclass', u'krbextradata', u'krblastpwdchange',
+        u'krbpasswordexpiration', u'krbprincipalkey'}
 
     update_keys = retrieve_keys - {u'dn', u'nsaccountlock'}
     activate_keys = retrieve_keys | {
@@ -60,6 +66,108 @@ class StageUserTracker(Tracker):
 
     find_keys = retrieve_keys - {u'has_keytab', u'has_password'}
     find_all_keys = retrieve_all_keys - {u'has_keytab', u'has_password'}
+    find_cli_keys = find_keys - {u'dn', u'nsaccountlock'}
+
+    mapping_options_stageuser = {
+        'carlicense': 'carlicense',
+        'cn': 'cn',
+        'continue': 'continue',
+        'departmentnumber': 'departmentnumber',
+        'displayname': 'displayname',
+        'employeenumber': 'employeenumber',
+        'employeetype': 'employeetype',
+        'facsimiletelephonenumber': 'fax',
+        'gecos': 'gecos',
+        'gidnumber': 'gidnumber',
+        'givenname': 'first',
+        'homedirectory': 'homedir',
+        'in_group': 'in-groups',
+        'in_hbacrule': 'in-hbacrules',
+        'in_netgroup': 'in-netgroups',
+        'in_role': 'in-roles',
+        'in_sudorule': 'in-sudorules',
+        'initials': 'initials',
+        'ipasshpubkey': 'sshpubkey',
+        'ipatokenradiusconfiglink': 'radius',
+        'ipatokenradiususername': 'radius_username',
+        'ipauserauthtype': 'user-auth-type',
+        'krbprincipalexpiration': 'principal-expiration',
+        'krbprincipalname': 'principal',
+        'l': 'city',
+        'loginshell': 'shell',
+        'mail': 'email',
+        'manager': 'manager',
+        'mobile': 'mobile',
+        'no_members': 'no-members',
+        'no_preserve': 'no-preserve',
+        'not_in_group': 'not-in-groups',
+        'not_in_hbacrule': 'not-in-hbacrules',
+        'not_in_netgroup': 'not-in-netgroups',
+        'not_in_role': 'not-in-roles',
+        'not_in_sudorule': 'not-in-sudorules',
+        'ou': 'orgunit',
+        'pager': 'pager',
+        'pkey_only': 'pkey-only',
+        'postalcode': 'postalcode',
+        'preferredlanguage': 'preferredlanguage',
+        'preserve': 'preserve',
+        'random': 'random',
+        'rename': 'rename',
+        'rights': 'rights',
+        'sizelimit': 'sizelimit',
+        'sn': 'last',
+        'st': 'state',
+        'street': 'street',
+        'telephonenumber': 'phone',
+        'timelimit': 'timelimit',
+        'title': 'title',
+        'uid': 'login',
+        'uidnumber': 'uid',
+        'user': 'users',
+        'usercertificate': 'certificate',
+        'userclass': 'class',
+        'userpassword': 'password',
+        }
+
+    mapping_output_stageuser = {
+        'Car License': 'carlicense',
+        'City': 'l',
+        'Display name': 'displayname',
+        'description': 'description',
+        'dn': 'dn',
+        'Email address': 'mail',
+        'Fax Number': 'facsimiletelephonenumber',
+        'First name': 'givenname',
+        'Full name': 'cn',
+        'GECOS': 'gecos',
+        'GID': 'gidnumber',
+        'Home directory': 'homedirectory',
+        'Initials': 'initials',
+        'ipauniqueid': 'ipauniqueid',
+        'Job Title': 'title',
+        'Kerberos keys available': 'has_keytab',
+        'Last name': 'sn',
+        'Login shell': 'loginshell',
+        'Manager': 'manager',
+        'Member of groups': 'memberof_group',
+        'Mobile Telephone Number': 'mobile',
+        'nsaccountlock': 'nsaccountlock',
+        'objectclass': 'objectclass',
+        'Org. Unit': 'ou',
+        'Pager Number': 'pager',
+        'Password': 'has_password',
+        'Principal alias': 'krbprincipalname',
+        'Principal name': 'krbcanonicalname',
+        'Random password': 'randompassword',
+        'SSH public key': 'ipasshpubkey',
+        'SSH public key fingerprint': 'sshpubkeyfp',
+        'State/Province': 'st',
+        'Street address': 'street',
+        'Telephone Number': 'telephonenumber',
+        'UID': 'uidnumber',
+        'User login': 'uid',
+        'ZIP': 'postalcode',
+        }
 
     def __init__(self, name, givenname, sn, **kwargs):
         super(StageUserTracker, self).__init__(default_version=None)
@@ -70,6 +178,66 @@ def __init__(self, name, givenname, sn, **kwargs):
             ('uid', self.uid), api.env.container_stageuser, api.env.basedn)
 
         self.kwargs = kwargs
+        self.mapping_options.update(self.mapping_options_stageuser)
+        self.mapping_output.update(self.mapping_output_stageuser)
+        self.novalue.extend(['preserve', 'no_preserve', 'random'])
+
+    def cli_command(self, name, *args, **options):
+        """ Modification of CLI command prepared using BaseTracker method
+        In CLI, inputting '--password=<string>' is invalid. If this is present,
+        this method changes the resulting command to format:
+            echo <string> | command
+        """
+        cmd = super(StageUserTracker, self).cli_command(name, *args, **options)
+        if '--password' in cmd:
+            cmd = re.sub("(?<=--password)={}".format(options['userpassword']),
+                         '', cmd)
+            cmd = "echo {} | {}".format(options['userpassword'], cmd)
+        return cmd
+
+    def cli_output(self, result):
+        """
+        Reformats text output to dictionary to be compared with expected values
+
+        Overrides BaseTracker method due to necessity of different handling
+        of SSH pubkey and password data.
+        """
+        result = result.split("\n")
+        modresult = {'result': {}, 'summary': None}
+        for line in result:
+            if re.match("^-*$", line):
+                continue
+            elif ':' in line:
+                key = self.mapping_output[line.split(':')[0].strip()]
+                if "SSH public key fingerprint" in line:
+                    value = unicode(line[30:])
+                else:
+                    value = line.split(':')[1].strip()
+                    if value == 'True':
+                        value = True
+                    elif value == 'False':
+                        value = False
+                    else:
+                        value = unicode(value)
+
+                if key in modresult['result']:
+                    modresult['result'][key].append(value)
+                elif value is True or value is False:
+                    modresult['result'][key] = value
+                else:
+                    modresult['result'][key] = [value]
+
+                if key is 'has_password':
+                    if type(value) is bool:
+                        modresult['result'][key] = value
+                    else:
+                        modresult['result']['userpassword'] = [str(value)]
+                elif key is 'randompassword':
+                    modresult['result'][key] = value[0]
+            else:
+                modresult['summary'] = unicode(line)
+
+        return modresult
 
     def make_create_command(self, options=None):
         """ Make function that creates a staged user using stageuser-add """
@@ -155,86 +323,140 @@ def track_create(self):
 
     def check_create(self, result):
         """ Check 'stageuser-add' command result """
-        assert_deepequal(dict(
-            value=self.uid,
-            summary=u'Added stage user "%s"' % self.uid,
-            result=self.filter_attrs(self.create_keys),
-        ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Added stage user "%s"' % self.uid,
+                result=self.filter_attrs(self.create_cli_keys),
+            ), result)
+        else:
+            assert_deepequal(dict(
+                value=self.uid,
+                summary=u'Added stage user "%s"' % self.uid,
+                result=self.filter_attrs(self.create_keys),
+            ), result)
 
     def check_delete(self, result):
         """ Check 'stageuser-del' command result """
-        assert_deepequal(dict(
-            value=[self.uid],
-            summary=u'Deleted stage user "%s"' % self.uid,
-            result=dict(failed=[]),
-            ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Deleted stage user "%s"' % self.uid,
+                result={},
+                ), result)
+        else:
+            assert_deepequal(dict(
+                value=[self.uid],
+                summary=u'Deleted stage user "%s"' % self.uid,
+                result=dict(failed=[]),
+                ), result)
+
+    def modify_expected(self, expected):
+        """ Small override because stageuser-find returns different
+        type of nsaccountlock value than DS, but overall the value
+        fits expected result """
+        if expected[u'nsaccountlock'] == [u'true']:
+            expected[u'nsaccountlock'] = True
+        elif expected[u'nsaccountlock'] == [u'false']:
+            expected[u'nsaccountlock'] = False
+        return expected
 
     def check_retrieve(self, result, all=False, raw=False):
         """ Check 'stageuser-show' command result """
         if all:
             expected = self.filter_attrs(self.retrieve_all_keys)
+            expected = self.modify_expected(expected)
+        elif self.cli_mode():
+            expected = self.filter_attrs(self.retrieve_cli_keys)
         else:
             expected = self.filter_attrs(self.retrieve_keys)
-
-        # small override because stageuser-find returns different
-        # type of nsaccountlock value than DS, but overall the value
-        # fits expected result
-        if expected[u'nsaccountlock'] == [u'true']:
-            expected[u'nsaccountlock'] = True
-        elif expected[u'nsaccountlock'] == [u'false']:
-            expected[u'nsaccountlock'] = False
-
-        assert_deepequal(dict(
-            value=self.uid,
-            summary=None,
-            result=expected,
-        ), result)
+            expected = self.modify_expected(expected)
+
+        if self.cli_mode():
+            if 'dn' in result['result']:
+                result['result']['dn'] = result['result']['dn'][0]
+            if 'objectclass' in result['result']:
+                result['result']['objectclass'] = result[
+                    'result']['objectclass'][0].split(", ")
+            assert_deepequal(dict(
+                summary=None,
+                result=expected,
+            ), result)
+        else:
+            assert_deepequal(dict(
+                value=self.uid,
+                summary=None,
+                result=expected,
+            ), result)
 
     def check_find(self, result, all=False, raw=False):
         """ Check 'stageuser-find' command result """
         if all:
             expected = self.filter_attrs(self.find_all_keys)
+            expected = self.modify_expected(expected)
+        elif self.cli_mode():
+            expected = self.filter_attrs(self.find_cli_keys)
         else:
             expected = self.filter_attrs(self.find_keys)
-
-        # small override because stageuser-find returns different
-        # type of nsaccountlock value than DS, but overall the value
-        # fits expected result
-        if expected[u'nsaccountlock'] == [u'true']:
-            expected[u'nsaccountlock'] = True
-        elif expected[u'nsaccountlock'] == [u'false']:
-            expected[u'nsaccountlock'] = False
-
-        assert_deepequal(dict(
-            count=1,
-            truncated=False,
-            summary=u'1 user matched',
-            result=[expected],
-        ), result)
+            expected = self.modify_expected(expected)
+
+        if self.cli_mode():
+            if 'dn' in result['result']:
+                result['result']['dn'] = result['result']['dn'][0]
+            if 'objectclass' in result['result']:
+                result['result']['objectclass'] = result[
+                    'result']['objectclass'][0].split(", ")
+            assert_deepequal(dict(
+                summary=u'Number of entries returned 1',
+                result=expected,
+            ), result)
+        else:
+            assert_deepequal(dict(
+                count=1,
+                truncated=False,
+                summary=u'1 user matched',
+                result=[expected],
+            ), result)
 
     def check_find_nomatch(self, result):
         """ Check 'stageuser-find' command result when no match is expected """
-        assert_deepequal(dict(
-            count=0,
-            truncated=False,
-            summary=u'0 users matched',
-            result=[],
-        ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Number of entries returned 0',
+                result={},
+            ), result)
+        else:
+            assert_deepequal(dict(
+                count=0,
+                truncated=False,
+                summary=u'0 users matched',
+                result=[],
+            ), result)
 
     def check_update(self, result, extra_keys=()):
         """ Check 'stageuser-mod' command result """
-        assert_deepequal(dict(
-            value=self.uid,
-            summary=u'Modified stage user "%s"' % self.uid,
-            result=self.filter_attrs(self.update_keys | set(extra_keys))
-        ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Modified stage user "%s"' % self.uid,
+                result=self.filter_attrs(self.update_keys | set(extra_keys))
+            ), result)
+        else:
+            assert_deepequal(dict(
+                value=self.uid,
+                summary=u'Modified stage user "%s"' % self.uid,
+                result=self.filter_attrs(self.update_keys | set(extra_keys))
+            ), result)
 
     def check_restore_preserved(self, result):
-        assert_deepequal(dict(
-            value=[self.uid],
-            summary=u'Staged user account "%s"' % self.uid,
-            result=dict(failed=[]),
-        ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Staged user account "%s"' % self.uid,
+                result={},
+            ), result)
+        else:
+            assert_deepequal(dict(
+                value=[self.uid],
+                summary=u'Staged user account "%s"' % self.uid,
+                result=dict(failed=[]),
+            ), result)
 
     def make_fixture_activate(self, request):
         """Make a pytest fixture for a staged user that is to be activated
@@ -247,6 +469,7 @@ def make_fixture_activate(self, request):
         """
         del_command = self.make_delete_command()
         try:
+            self.skip_error = True
             del_command()
         except errors.NotFound:
             pass
diff --git a/ipatests/test_xmlrpc/tracker/user_plugin.py b/ipatests/test_xmlrpc/tracker/user_plugin.py
index 4485fd9..ac94b71 100644
--- a/ipatests/test_xmlrpc/tracker/user_plugin.py
+++ b/ipatests/test_xmlrpc/tracker/user_plugin.py
@@ -39,18 +39,24 @@ class UserTracker(KerberosAliasMixin, Tracker):
         u'krbpasswordexpiration', u'pager', u'st', u'manager', u'cn',
         u'ipauniqueid', u'objectclass', u'mepmanagedentry',
         u'displayname', u'gecos', u'initials', u'preserved'}
+    retrieve_cli_keys = retrieve_keys - {'dn'}
 
     retrieve_preserved_keys = (retrieve_keys - {u'memberof_group'}) | {
         u'preserved'}
     retrieve_preserved_all_keys = retrieve_all_keys - {u'memberof_group'}
+    retrieve_preserved_cli_keys = retrieve_cli_keys | {u'preserved'}
 
     create_keys = retrieve_all_keys | {
         u'krbextradata', u'krbpasswordexpiration', u'krblastpwdchange',
         u'krbprincipalkey', u'userpassword', u'randompassword'}
     create_keys = create_keys - {u'nsaccountlock'}
+    create_cli_keys = create_keys - {
+        u'dn', u'ipauniqueid', u'mepmanagedentry',
+        u'objectclass'}
 
     update_keys = retrieve_keys - {u'dn'}
     activate_keys = retrieve_keys
+    activate_cli_keys = activate_keys - {u'dn', u'nsaccountlock'}
 
     find_keys = retrieve_keys - {
         u'mepmanagedentry', u'memberof_group', u'has_keytab', u'has_password',
@@ -59,9 +65,99 @@ class UserTracker(KerberosAliasMixin, Tracker):
     find_all_keys = retrieve_all_keys - {
         u'has_keytab', u'has_password'
     }
+    find_cli_keys = find_keys - {u'dn'}
 
     primary_keys = {u'uid', u'dn'}
 
+    mapping_options_user = {
+        'carlicense': 'carlicense',
+        'cn': 'cn',
+        'continue': 'continue',
+        'departmentnumber': 'departmentnumber',
+        'displayname': 'displayname',
+        'employeenumber': 'employeenumber',
+        'employeetype': 'employeetype',
+        'facsimiletelephonenumber': 'fax',
+        'gecos': 'gecos',
+        'gidnumber': 'gidnumber',
+        'givenname': 'first',
+        'homedirectory': 'homedir',
+        'in_group': 'in-groups',
+        'in_hbacrule': 'in-hbacrules',
+        'in_netgroup': 'in-netgroups',
+        'in_role': 'in-roles',
+        'in_sudorule': 'in-sudorules',
+        'initials': 'initials',
+        'ipasshpubkey': 'sshpubkey',
+        'ipatokenradiusconfiglink': 'radius',
+        'ipatokenradiususername': 'radius_username',
+        'ipauserauthtype': 'user-auth-type',
+        'krbprincipalexpiration': 'principal-expiration',
+        'krbprincipalname': 'principal',
+        'l': 'city',
+        'loginshell': 'shell',
+        'mail': 'email',
+        'manager': 'manager',
+        'mobile': 'mobile',
+        'no_members': 'no-members',
+        'no_preserve': 'no-preserve',
+        'not_in_group': 'not-in-groups',
+        'not_in_hbacrule': 'not-in-hbacrules',
+        'not_in_netgroup': 'not-in-netgroups',
+        'not_in_role': 'not-in-roles',
+        'not_in_sudorule': 'not-in-sudorules',
+        'ou': 'orgunit',
+        'pager': 'pager',
+        'pkey_only': 'pkey-only',
+        'postalcode': 'postalcode',
+        'preferredlanguage': 'preferredlanguage',
+        'preserve': 'preserve',
+        'preserved': 'preserved',
+        'random': 'random',
+        'rename': 'rename',
+        'rights': 'rights',
+        'sizelimit': 'sizelimit',
+        'sn': 'last',
+        'st': 'state',
+        'street': 'street',
+        'telephonenumber': 'phone',
+        'timelimit': 'timelimit',
+        'title': 'title',
+        'uid': 'login',
+        'uidnumber': 'uid',
+        'user': 'users',
+        'usercertificate': 'certificate',
+        'userclass': 'class',
+        'userpassword': 'password',
+        }
+
+    mapping_output_user = {
+        'Account disabled': 'nsaccountlock',
+        'Display name': 'displayname',
+        'dn': 'dn',
+        'Email address': 'mail',
+        'First name': 'givenname',
+        'Full name': 'cn',
+        'GECOS': 'gecos',
+        'GID': 'gidnumber',
+        'Home directory': 'homedirectory',
+        'Initials': 'initials',
+        'ipauniqueid': 'ipauniqueid',
+        'Kerberos keys available': 'has_keytab',
+        'Last name': 'sn',
+        'Login shell': 'loginshell',
+        'Member of groups': 'memberof_group',
+        'mepmanagedentry': 'mepmanagedentry',
+        'nsaccountlock': 'nsaccountlock',
+        'objectclass': 'objectclass',
+        'Password': 'has_password',
+        'Preserved user': 'preserved',
+        'Principal alias': 'krbprincipalname',
+        'Principal name': 'krbcanonicalname',
+        'UID': 'uidnumber',
+        'User login': 'uid',
+        }
+
     def __init__(self, name, givenname, sn, **kwargs):
         super(UserTracker, self).__init__(default_version=None)
         self.uid = name
@@ -70,6 +166,9 @@ def __init__(self, name, givenname, sn, **kwargs):
         self.dn = DN(('uid', self.uid), api.env.container_user, api.env.basedn)
 
         self.kwargs = kwargs
+        self.mapping_options.update(self.mapping_options_user)
+        self.mapping_output.update(self.mapping_output_user)
+        self.novalue.extend(['preserve', 'no_preserve'])
 
     def make_create_command(self):
         """ Make function that crates a user using user-add """
@@ -224,31 +323,48 @@ def update(self, updates, expected_updates=None):
 
     def check_create(self, result, extra_keys=()):
         """ Check 'user-add' command result """
-        expected = self.filter_attrs(self.create_keys | set(extra_keys))
-        assert_deepequal(dict(
-            value=self.uid,
-            summary=u'Added user "%s"' % self.uid,
-            result=self.filter_attrs(expected),
-            ), result)
+        if self.cli_mode():
+            expected = self.filter_attrs(
+                self.create_cli_keys | set(extra_keys))
+            assert_deepequal(dict(
+                summary=u'Added user "%s"' % self.uid,
+                result=self.filter_attrs(expected),
+                ), result)
+        else:
+            expected = self.filter_attrs(self.create_keys | set(extra_keys))
+            assert_deepequal(dict(
+                value=self.uid,
+                summary=u'Added user "%s"' % self.uid,
+                result=self.filter_attrs(expected),
+                ), result)
 
     def check_delete(self, result):
         """ Check 'user-del' command result """
-        assert_deepequal(dict(
-            value=[self.uid],
-            summary=u'Deleted user "%s"' % self.uid,
-            result=dict(failed=[]),
-            ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Deleted user "%s"' % self.uid,
+                result={},
+                ), result)
+        else:
+            assert_deepequal(dict(
+                value=[self.uid],
+                summary=u'Deleted user "%s"' % self.uid,
+                result=dict(failed=[]),
+                ), result)
 
     def check_retrieve(self, result, all=False, raw=False):
         """ Check 'user-show' command result """
         if u'preserved' in self.attrs and self.attrs[u'preserved']:
             self.retrieve_all_keys = self.retrieve_preserved_all_keys
             self.retrieve_keys = self.retrieve_preserved_keys
+            self.retrieve_cli_keys = self.retrieve_preserved_cli_keys
         elif u'preserved' not in self.attrs and all:
             self.attrs[u'preserved'] = False
 
         if all:
             expected = self.filter_attrs(self.retrieve_all_keys)
+        elif self.cli_mode():
+            expected = self.filter_attrs(self.retrieve_cli_keys)
         else:
             expected = self.filter_attrs(self.retrieve_keys)
 
@@ -261,11 +377,22 @@ def check_retrieve(self, result, all=False, raw=False):
             elif expected[u'nsaccountlock'] == [u'false']:
                 expected[u'nsaccountlock'] = False
 
-        assert_deepequal(dict(
-            value=self.uid,
-            summary=None,
-            result=expected,
-        ), result)
+        if self.cli_mode():
+            if 'dn' in result['result']:
+                result['result']['dn'] = result['result']['dn'][0]
+            if 'objectclass' in result['result']:
+                result['result']['objectclass'] = result[
+                    'result']['objectclass'][0].split(", ")
+            assert_deepequal(dict(
+                summary=None,
+                result=expected,
+            ), result)
+        else:
+            assert_deepequal(dict(
+                value=self.uid,
+                summary=None,
+                result=expected,
+            ), result)
 
     def check_find(self, result, all=False, pkey_only=False, raw=False,
                    expected_override=None):
@@ -276,6 +403,8 @@ def check_find(self, result, all=False, pkey_only=False, raw=False,
             expected = self.filter_attrs(self.find_all_keys)
         elif pkey_only:
             expected = self.filter_attrs(self.primary_keys)
+        elif self.cli_mode():
+            expected = self.filter_attrs(self.find_cli_keys)
         else:
             expected = self.filter_attrs(self.find_keys)
 
@@ -292,21 +421,38 @@ def check_find(self, result, all=False, pkey_only=False, raw=False,
             assert isinstance(expected_override, dict)
             expected.update(expected_override)
 
-        assert_deepequal(dict(
-            count=1,
-            truncated=False,
-            summary=u'1 user matched',
-            result=[expected],
-        ), result)
+        if self.cli_mode():
+            if 'dn' in result['result']:
+                result['result']['dn'] = result['result']['dn'][0]
+            if 'objectclass' in result['result']:
+                result['result']['objectclass'] = result[
+                    'result']['objectclass'][0].split(", ")
+            assert_deepequal(dict(
+                summary=u'Number of entries returned 1',
+                result=expected,
+            ), result)
+        else:
+            assert_deepequal(dict(
+                count=1,
+                truncated=False,
+                summary=u'1 user matched',
+                result=[expected],
+            ), result)
 
     def check_find_nomatch(self, result):
         """ Check 'user-find' command result when no user should be found """
-        assert_deepequal(dict(
-            count=0,
-            truncated=False,
-            summary=u'0 users matched',
-            result=[],
-        ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Number of entries returned 0',
+                result={},
+            ), result)
+        else:
+            assert_deepequal(dict(
+                count=0,
+                truncated=False,
+                summary=u'0 users matched',
+                result=[],
+            ), result)
 
     def check_update(self, result, extra_keys=()):
         """ Check 'user-mod' command result """
@@ -316,11 +462,17 @@ def check_update(self, result, extra_keys=()):
         elif expected[u'nsaccountlock'] == [u'false']:
             expected[u'nsaccountlock'] = False
 
-        assert_deepequal(dict(
-            value=self.uid,
-            summary=u'Modified user "%s"' % self.uid,
-            result=expected
-        ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Modified user "%s"' % self.uid,
+                result=expected
+            ), result)
+        else:
+            assert_deepequal(dict(
+                value=self.uid,
+                summary=u'Modified user "%s"' % self.uid,
+                result=expected
+            ), result)
 
     def check_enable(self, result):
         """ Check result of enable user operation """
@@ -367,18 +519,23 @@ def create_from_staged(self, stageduser):
 
     def check_activate(self, result):
         """ Check 'stageuser-activate' command result """
-        expected = dict(
-            value=self.uid,
-            summary=u'Stage user %s activated' % self.uid,
-            result=self.filter_attrs(self.activate_keys))
-
-        # small override because stageuser-find returns different
-        # type of nsaccountlock value than DS, but overall the value
-        # fits expected result
-        if expected['result'][u'nsaccountlock'] == [u'true']:
-            expected['result'][u'nsaccountlock'] = True
-        elif expected['result'][u'nsaccountlock'] == [u'false']:
-            expected['result'][u'nsaccountlock'] = False
+        if self.cli_mode():
+            expected = dict(
+                summary=u'Stage user %s activated' % self.uid,
+                result=self.filter_attrs(self.activate_cli_keys))
+        else:
+            expected = dict(
+                value=self.uid,
+                summary=u'Stage user %s activated' % self.uid,
+                result=self.filter_attrs(self.activate_keys))
+
+            # small override because stageuser-find returns different
+            # type of nsaccountlock value than DS, but overall the value
+            # fits expected result
+            if expected['result'][u'nsaccountlock'] == [u'true']:
+                expected['result'][u'nsaccountlock'] = True
+            elif expected['result'][u'nsaccountlock'] == [u'false']:
+                expected['result'][u'nsaccountlock'] = False
 
         assert_deepequal(expected, result)
 
@@ -386,11 +543,17 @@ def check_activate(self, result):
 
     def check_undel(self, result):
         """ Check 'user-undel' command result """
-        assert_deepequal(dict(
-            value=self.uid,
-            summary=u'Undeleted user account "%s"' % self.uid,
-            result=True
-            ), result)
+        if self.cli_mode():
+            assert_deepequal(dict(
+                summary=u'Undeleted user account "%s"' % self.uid,
+                result={}
+                ), result)
+        else:
+            assert_deepequal(dict(
+                value=self.uid,
+                summary=u'Undeleted user account "%s"' % self.uid,
+                result=True
+                ), result)
 
     def enable(self):
         """ Enable user account if it was disabled """
@@ -441,8 +604,12 @@ def check_attr_preservation(self, expected):
             gidnumber=result[u'result'][u'gidnumber']
             ), expected)
 
+        if self.cli_mode():
+            group = [u'ipausers']
+        else:
+            group = (u'ipausers',)
         if (u'memberof_group' not in result[u'result'] or
-                result[u'result'][u'memberof_group'] != (u'ipausers',)):
+                result[u'result'][u'memberof_group'] != group):
             assert False
 
     def make_fixture_restore(self, request):
@@ -457,6 +624,7 @@ def make_fixture_restore(self, request):
         """
         del_command = self.make_delete_command()
         try:
+            self.skip_error = True
             del_command()
         except errors.NotFound:
             pass
-- 
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