summaryrefslogtreecommitdiffstats
path: root/ipatests
diff options
context:
space:
mode:
Diffstat (limited to 'ipatests')
-rw-r--r--ipatests/__init__.py22
-rw-r--r--ipatests/data.py38
-rwxr-xr-xipatests/i18n.py838
-rw-r--r--ipatests/setup.py.in87
-rw-r--r--ipatests/test_cmdline/cmdline.py70
-rw-r--r--ipatests/test_cmdline/test_cli.py327
-rw-r--r--ipatests/test_cmdline/test_help.py141
-rw-r--r--ipatests/test_cmdline/test_ipagetkeytab.py152
-rw-r--r--ipatests/test_install/0_reset.update5
-rw-r--r--ipatests/test_install/1_add.update22
-rw-r--r--ipatests/test_install/2_update.update3
-rw-r--r--ipatests/test_install/3_update.update3
-rw-r--r--ipatests/test_install/4_update.update4
-rw-r--r--ipatests/test_install/5_update.update3
-rw-r--r--ipatests/test_install/6_update.update3
-rw-r--r--ipatests/test_install/8_badsyntax.update3
-rw-r--r--ipatests/test_install/9_badsyntax.update3
-rw-r--r--ipatests/test_install/__init__.py22
-rw-r--r--ipatests/test_install/test_updates.py337
-rw-r--r--ipatests/test_ipalib/__init__.py22
-rw-r--r--ipatests/test_ipalib/test_backend.py272
-rw-r--r--ipatests/test_ipalib/test_base.py352
-rw-r--r--ipatests/test_ipalib/test_capabilities.py33
-rw-r--r--ipatests/test_ipalib/test_cli.py116
-rw-r--r--ipatests/test_ipalib/test_config.py609
-rw-r--r--ipatests/test_ipalib/test_crud.py240
-rw-r--r--ipatests/test_ipalib/test_errors.py374
-rw-r--r--ipatests/test_ipalib/test_frontend.py1188
-rw-r--r--ipatests/test_ipalib/test_messages.py89
-rw-r--r--ipatests/test_ipalib/test_output.py89
-rw-r--r--ipatests/test_ipalib/test_parameters.py1533
-rw-r--r--ipatests/test_ipalib/test_plugable.py516
-rw-r--r--ipatests/test_ipalib/test_rpc.py244
-rw-r--r--ipatests/test_ipalib/test_text.py334
-rw-r--r--ipatests/test_ipalib/test_util.py26
-rw-r--r--ipatests/test_ipalib/test_x509.py139
-rw-r--r--ipatests/test_ipapython/__init__.py22
-rw-r--r--ipatests/test_ipapython/test_cookie.py478
-rw-r--r--ipatests/test_ipapython/test_dn.py1937
-rw-r--r--ipatests/test_ipapython/test_ipautil.py69
-rw-r--r--ipatests/test_ipapython/test_keyring.py147
-rw-r--r--ipatests/test_ipapython/test_ssh.py76
-rw-r--r--ipatests/test_ipaserver/__init__.py22
-rw-r--r--ipatests/test_ipaserver/httptest.py52
-rwxr-xr-xipatests/test_ipaserver/install/test_adtrustinstance.py59
-rw-r--r--ipatests/test_ipaserver/test_changepw.py107
-rw-r--r--ipatests/test_ipaserver/test_ldap.py259
-rw-r--r--ipatests/test_ipaserver/test_rpcserver.py247
-rw-r--r--ipatests/test_pkcs10/__init__.py22
-rw-r--r--ipatests/test_pkcs10/test0.csr12
-rw-r--r--ipatests/test_pkcs10/test1.csr13
-rw-r--r--ipatests/test_pkcs10/test2.csr15
-rw-r--r--ipatests/test_pkcs10/test3.csr3
-rw-r--r--ipatests/test_pkcs10/test4.csr4
-rw-r--r--ipatests/test_pkcs10/test5.csr20
-rw-r--r--ipatests/test_pkcs10/test_pkcs10.py124
-rw-r--r--ipatests/test_util.py367
-rw-r--r--ipatests/test_xmlrpc/__init__.py22
-rw-r--r--ipatests/test_xmlrpc/objectclasses.py163
-rw-r--r--ipatests/test_xmlrpc/test_attr.py562
-rw-r--r--ipatests/test_xmlrpc/test_automember_plugin.py1095
-rw-r--r--ipatests/test_xmlrpc/test_automount_plugin.py582
-rw-r--r--ipatests/test_xmlrpc/test_baseldap_plugin.py159
-rw-r--r--ipatests/test_xmlrpc/test_batch_plugin.py232
-rw-r--r--ipatests/test_xmlrpc/test_cert_plugin.py454
-rw-r--r--ipatests/test_xmlrpc/test_config_plugin.py121
-rw-r--r--ipatests/test_xmlrpc/test_delegation_plugin.py300
-rw-r--r--ipatests/test_xmlrpc/test_dns_plugin.py1511
-rw-r--r--ipatests/test_xmlrpc/test_dns_realmdomains_integration.py168
-rw-r--r--ipatests/test_xmlrpc/test_external_members.py160
-rw-r--r--ipatests/test_xmlrpc/test_group_plugin.py1046
-rw-r--r--ipatests/test_xmlrpc/test_hbac_plugin.py497
-rw-r--r--ipatests/test_xmlrpc/test_hbacsvcgroup_plugin.py256
-rw-r--r--ipatests/test_xmlrpc/test_hbactest_plugin.py217
-rw-r--r--ipatests/test_xmlrpc/test_host_plugin.py939
-rw-r--r--ipatests/test_xmlrpc/test_hostgroup_plugin.py313
-rw-r--r--ipatests/test_xmlrpc/test_krbtpolicy.py150
-rw-r--r--ipatests/test_xmlrpc/test_nesting.py797
-rw-r--r--ipatests/test_xmlrpc/test_netgroup_plugin.py1362
-rw-r--r--ipatests/test_xmlrpc/test_passwd_plugin.py69
-rw-r--r--ipatests/test_xmlrpc/test_permission_plugin.py972
-rw-r--r--ipatests/test_xmlrpc/test_ping_plugin.py53
-rw-r--r--ipatests/test_xmlrpc/test_privilege_plugin.py412
-rw-r--r--ipatests/test_xmlrpc/test_pwpolicy_plugin.py244
-rw-r--r--ipatests/test_xmlrpc/test_range_plugin.py536
-rw-r--r--ipatests/test_xmlrpc/test_realmdomains_plugin.py164
-rw-r--r--ipatests/test_xmlrpc/test_replace.py236
-rw-r--r--ipatests/test_xmlrpc/test_role_plugin.py625
-rw-r--r--ipatests/test_xmlrpc/test_selfservice_plugin.py290
-rw-r--r--ipatests/test_xmlrpc/test_selinuxusermap_plugin.py934
-rw-r--r--ipatests/test_xmlrpc/test_service_plugin.py632
-rw-r--r--ipatests/test_xmlrpc/test_sudocmd_plugin.py327
-rw-r--r--ipatests/test_xmlrpc/test_sudocmdgroup_plugin.py693
-rw-r--r--ipatests/test_xmlrpc/test_sudorule_plugin.py781
-rw-r--r--ipatests/test_xmlrpc/test_trust_plugin.py159
-rw-r--r--ipatests/test_xmlrpc/test_user_plugin.py1837
-rw-r--r--ipatests/test_xmlrpc/xmlrpc_test.py329
-rw-r--r--ipatests/util.py637
98 files changed, 33350 insertions, 0 deletions
diff --git a/ipatests/__init__.py b/ipatests/__init__.py
new file mode 100644
index 000000000..3dc405f01
--- /dev/null
+++ b/ipatests/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Package containing all unit tests.
+"""
diff --git a/ipatests/data.py b/ipatests/data.py
new file mode 100644
index 000000000..9332a53a5
--- /dev/null
+++ b/ipatests/data.py
@@ -0,0 +1,38 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Data frequently used in the unit tests, especially Unicode related tests.
+"""
+
+import struct
+
+
+# A string that should have bytes 'x\00' through '\xff':
+binary_bytes = ''.join(struct.pack('B', d) for d in xrange(256))
+assert '\x00' in binary_bytes and '\xff' in binary_bytes
+assert type(binary_bytes) is str and len(binary_bytes) == 256
+
+# A UTF-8 encoded str:
+utf8_bytes = '\xd0\x9f\xd0\xb0\xd0\xb2\xd0\xb5\xd0\xbb'
+
+# The same UTF-8 data decoded (a unicode instance):
+unicode_str = u'\u041f\u0430\u0432\u0435\u043b'
+assert utf8_bytes.decode('UTF-8') == unicode_str
+assert unicode_str.encode('UTF-8') == utf8_bytes
diff --git a/ipatests/i18n.py b/ipatests/i18n.py
new file mode 100755
index 000000000..9c8479bb0
--- /dev/null
+++ b/ipatests/i18n.py
@@ -0,0 +1,838 @@
+#!/usr/bin/python
+# Authors:
+# John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+#
+
+# WARNING: Do not import ipa modules, this is also used as a
+# stand-alone script (invoked from install/po Makefile).
+import optparse
+import sys
+import gettext
+import locale
+import re
+import os
+import traceback
+import polib
+from collections import namedtuple
+
+'''
+We test our translations by taking the original untranslated string
+(e.g. msgid) and prepend a prefix character and then append a suffix
+character. The test consists of asserting that the first character in the
+translated string is the prefix, the last character in the translated string
+is the suffix and the everything between the first and last character exactly
+matches the original msgid.
+
+We use unicode characters not in the ascii character set for the prefix and
+suffix to enhance the test. To make reading the translated string easier the
+prefix is the unicode right pointing arrow and the suffix left pointing arrow,
+thus the translated string looks like the original string enclosed in
+arrows. In ASCII art the string "foo" would render as:
+-->foo<--
+'''
+
+#-------------------------------------------------------------------------------
+
+verbose = False
+print_traceback = False
+pedantic = False
+show_strings = True
+
+# Unicode right pointing arrow
+prefix = u'\u2192' # utf-8 == '\xe2\x86\x92'
+# Unicode left pointing arrow
+suffix = u'\u2190' # utf-8 == '\xe2\x86\x90'
+
+page_width = 80
+section_seperator = '=' * page_width
+entry_seperator = '-' * page_width
+
+#-------------------------------------------------------------------------------
+# For efficiency compile these regexps just once
+_substitution_regexps = [re.compile(r'%[srduoxf]\b'), # e.g. %s
+ re.compile(r'%\(\w+\)[srduoxf]\b'), # e.g. %(foo)s
+ re.compile(r'\$\w+'), # e.g. $foo
+ re.compile(r'\${\w+}'), # e.g. ${foo}
+ re.compile(r'\$\(\w+\)') # e.g. $(foo)
+ ]
+# Python style substitution, e.g. %(foo)s
+# where foo is the key and s is the format char
+# group 1: whitespace between % and (
+# group 2: whitespace between ( and key
+# group 3: whitespace between key and )
+# group 4: whitespace between ) and format char
+# group 5: format char
+_python_substitution_regexp = re.compile(r'%(\s*)\((\s*)\w+(\s*)\)(\s*)([srduoxf]\b)?')
+
+# Shell style substitution, e.g. $foo $(foo) ${foo}
+# where foo is the variable
+_shell_substitution_regexp = re.compile(r'\$(\s*)([({]?)(\s*)\w+(\s*)([)}]?)')
+# group 1: whitespace between $ and delimiter
+# group 2: begining delimiter
+# group 3: whitespace between beginning delmiter and variable
+# group 4: whitespace between variable and ending delimiter
+# group 5: ending delimiter
+
+printf_fmt_re = re.compile(
+ r"%" # start
+ "(\d+\$)?" # fmt_arg (group 1)
+ "(([#0 +'I]|-(?!\d))*)" # flags (group 2)
+ "(([+-]?([1-9][0-9]*)?)|(\*|\*\d+\$))?" # width (group 4)
+ "(\.((-?\d*)|(\*|)|(\*\d+\$)))?" # precision (group 8)
+ "(h|hh|l|ll|L|j|z|t)?" # length (group 13)
+ "([diouxXeEfFgGaAcspnm%])") # conversion (group 14)
+
+#-------------------------------------------------------------------------------
+
+def get_prog_langs(entry):
+ '''
+ Given an entry in a pot or po file return a set of the
+ programming languges it was found in. It needs to be a set
+ because the same msgid may appear in more than one file which may
+ be in different programming languages.
+
+ Note: One might think you could use the c-format etc. flags to
+ attached to entry to make this determination, but you can't. Those
+ flags refer to the style of the string not the programming
+ language it came from. Also the flags are often omitted and/or are
+ inaccurate.
+
+ For now we just look at the file extension. If we knew the path to
+ the file we could use other heuristics such as looking for the
+ shbang interpreter string.
+
+ The set of possible language types witch might be returned are:
+
+ * c
+ * python
+
+ '''
+ result = set()
+
+ for location in entry.occurrences:
+ filename = location[0]
+ ext = os.path.splitext(filename)[1]
+
+ if ext in ('.c', '.h', '.cxx', '.cpp', '.hxx'):
+ result.add('c')
+ elif ext in ('.py'):
+ result.add('python')
+
+ return result
+
+def parse_printf_fmt(s):
+ '''
+ Parse a printf style format string and return a list of format
+ conversions found in the string.
+
+ Each conversion specification is introduced by the character %, and
+ ends with a conversion specifier. In between there may be (in this
+ order) zero or more flags, an optional minimum field width, an
+ optional precision and an optional length modifier. See "man 3
+ printf" for details.
+
+ Each item in the returned list is a dict whose keys are the
+ sub-parts of a conversion specification. The key and values are:
+
+ fmt
+ The entire format conversion specification
+ fmt_arg
+ The positional index of the matching argument in the argument
+ list, e.g. %1$ indicates the first argument in the argument
+ will be read for this conversion, excludes the leading % but
+ includes the trailing $, 1$ is the fmt_arg in %1$.
+ flags
+ The flag characaters, e.g. 0 is the flag in %08d
+ width
+ The width field, e.g. 20 is the width in %20s
+ precision
+ The precisioin field, e.g. .2 is the precision in %8.2f
+ length
+ The length modifier field, e.g. l is the length modifier in %ld
+ conversion
+ The conversion specifier character, e.g. d is the conversion
+ specification character in %ld
+
+ If the part is not found in the format it's value will be None.
+ '''
+
+ result = []
+
+ # get list of all matches, but skip escaped %
+ matches = [x for x in printf_fmt_re.finditer(s) if x.group(0) != "%%"]
+
+ # build dict of each sub-part of the format, append to result
+ for match in matches:
+ parts = {}
+ parts['fmt'] = match.group(0)
+ parts['fmt_arg'] = match.group(1)
+ parts['flags'] = match.group(2) or None
+ parts['width'] = match.group(4) or None
+ parts['precision'] = match.group(8)
+ parts['length'] = match.group(13)
+ parts['conversion'] = match.group(14)
+
+ result.append(parts)
+
+ return result
+
+def validate_substitutions_match(s1, s2, s1_name='string1', s2_name='string2'):
+ '''
+ Validate both s1 and s2 have the same number of substitution strings.
+ A substitution string would be something that looked like this:
+
+ * %(foo)s
+ * $foo
+ * ${foo}
+ * $(foo)
+
+ The substitutions may appear in any order in s1 and s2, however their
+ format must match exactly and the exact same number of each must exist
+ in both s1 and s2.
+
+ A list of error diagnostics is returned explaining how s1 and s2 failed
+ the validation check. If the returned error list is empty then the
+ validation succeeded.
+
+ :param s1: First string to validate
+ :param s2: First string to validate
+ :param s1_name: In diagnostic messages the name for s1
+ :param s2_name: In diagnostic messages the name for s2
+ :return: List of diagnostic error messages, if empty then success
+ '''
+ errors = []
+
+ def get_subs(s):
+ '''
+ Return a dict whoses keys are each unique substitution and whose
+ value is the count of how many times that substitution appeared.
+ '''
+ subs = {}
+ for regexp in _substitution_regexps:
+ for match in regexp.finditer(s):
+ matched = match.group(0)
+ subs[matched] = subs.get(matched, 0) + 1
+ return subs
+
+ # Get the substitutions and their occurance counts
+ subs1 = get_subs(s1)
+ subs2 = get_subs(s2)
+
+ # Form a set for each strings substitutions and
+ # do set subtraction and interesection
+ set1 = set(subs1.keys())
+ set2 = set(subs2.keys())
+
+ missing1 = set2 - set1
+ missing2 = set1 - set2
+ common = set1 & set2
+
+ # Test for substitutions which are absent in either string
+ if missing1:
+ errors.append("The following substitutions are absent in %s: %s" %
+ (s1_name, ' '.join(missing1)))
+
+ if missing2:
+ errors.append("The following substitutions are absent in %s: %s" %
+ (s2_name, ' '.join(missing2)))
+
+ if pedantic:
+ # For the substitutions which are shared assure they occur an equal number of times
+ for sub in common:
+ if subs1[sub] != subs2[sub]:
+ errors.append("unequal occurances of '%s', %s has %d occurances, %s has %d occurances" %
+ (sub, s1_name, subs1[sub], s2_name, subs2[sub]))
+
+ if errors:
+ if show_strings:
+ errors.append('>>> %s <<<' % s1_name)
+ errors.append(s1.rstrip())
+
+ errors.append('>>> %s <<<' % s2_name)
+ errors.append(s2.rstrip())
+ return errors
+
+
+def validate_substitution_syntax(s, s_name='string'):
+ '''
+ If s has one or more substitution variables then validate they
+ are syntactically correct.
+ A substitution string would be something that looked like this:
+
+ * %(foo)s
+ * $foo
+ * ${foo}
+ * $(foo)
+
+ A list of error diagnostics is returned explaining how s1 and s2 failed
+ the validation check. If the returned error list is empty then the
+ validation succeeded.
+
+ :param s: String to validate
+ :param s_name: In diagnostic messages the name for s
+ :return: List of diagnostic error messages, if empty then success
+ '''
+ errors = []
+
+ # Look for Python style substitutions, e.g. %(foo)s
+ for match in _python_substitution_regexp.finditer(s):
+ if match.group(1):
+ errors.append("%s has whitespace between %% and key in '%s'" %
+ (s_name, match.group(0)))
+ if match.group(2) or match.group(3):
+ errors.append("%s has whitespace next to key in '%s'" %
+ (s_name, match.group(0)))
+ if match.group(4):
+ errors.append("%s has whitespace between key and format character in '%s'" %
+ (s_name, match.group(0)))
+ if not match.group(5):
+ errors.append("%s has no format character in '%s'" %
+ (s_name, match.group(0)))
+
+ # Look for shell style substitutions, e.g. $foo $(foo) ${foo}
+ for match in _shell_substitution_regexp.finditer(s):
+ if match.group(1):
+ errors.append("%s has whitespace between $ and variable in '%s'" %
+ (s_name, match.group(0)))
+ if match.group(3) or (match.group(4) and match.group(5)):
+ errors.append("%s has whitespace next to variable in '%s'" %
+ (s_name, match.group(0)))
+
+ beg_delimiter = match.group(2)
+ end_delimiter = match.group(5)
+ matched_delimiters = {'': '', '(': ')', '{': '}'}
+ if beg_delimiter is not None or end_delimiter is not None:
+ if matched_delimiters[beg_delimiter] != end_delimiter:
+ errors.append("%s variable delimiters do not match in '%s', begin delimiter='%s' end delimiter='%s'" %
+ (s_name, match.group(0), beg_delimiter, end_delimiter))
+
+ if errors:
+ if show_strings:
+ errors.append('>>> %s <<<' % s_name)
+ errors.append(s.rstrip())
+
+ return errors
+
+
+def validate_positional_substitutions(s, prog_langs, s_name='string'):
+ '''
+ We do not permit multiple positional substitutions in translation
+ strings (e.g. '%s') because they do not allow translators to reorder the
+ wording. Instead keyword substitutions should be used when there are
+ more than one.
+ '''
+ errors = []
+
+ fmts = parse_printf_fmt(s)
+ n_fmts = len(fmts)
+
+ errors = []
+ if n_fmts > 1:
+ for i, fmt_parts in enumerate(fmts):
+ fmt = fmt_parts['fmt']
+ fmt_arg = fmt_parts['fmt_arg']
+ width = fmt_parts['width']
+
+ if width == '*':
+ errors.append("Error: * width arg in format '%s should be indexed" % fmt)
+
+ if fmt_arg is None:
+ if 'c' in prog_langs:
+ errors.append("%s format '%s' is positional, should use indexed argument" %
+ (s_name, fmt))
+ else:
+ errors.append("%s format '%s' is positional, should use keyword substitution" %
+ (s_name, fmt))
+
+ if errors:
+ if show_strings:
+ errors.append('>>> %s <<<' % s_name)
+ errors.append(s.rstrip())
+
+ return errors
+
+def validate_file(file_path, validation_mode, reference_pot=None):
+ '''
+ Given a pot or po file scan all it's entries looking for problems
+ with variable substitutions. See the following functions for
+ details on how the validation is performed.
+
+ * validate_substitutions_match()
+ * validate_substitution_syntax()
+ * validate_positional_substitutions()
+
+ Returns the number of entries with errors.
+
+ For po files, ``reference_pot`` gives a pot file to merge with (to recover
+ comments and file locations)
+ '''
+
+ def emit_messages():
+ if n_warnings:
+ warning_lines.insert(0, section_seperator)
+ warning_lines.insert(1, "%d validation warnings in %s" % (n_warnings, file_path))
+ print '\n'.join(warning_lines)
+
+ if n_errors:
+ error_lines.insert(0, section_seperator)
+ error_lines.insert(1, "%d validation errors in %s" % (n_errors, file_path))
+ print '\n'.join(error_lines)
+
+ Result = namedtuple('ValidateFileResult', ['n_entries', 'n_msgids', 'n_msgstrs', 'n_warnings', 'n_errors'])
+
+ warning_lines = []
+ error_lines = []
+ n_entries = 0
+ n_msgids = 0
+ n_msgstrs = 0
+ n_entries = 0
+ n_warnings = 0
+ n_errors = 0
+ n_plural_forms = 0
+
+ if not os.path.isfile(file_path):
+ error_lines.append(entry_seperator)
+ error_lines.append('file does not exist "%s"' % (file_path))
+ n_errors += 1
+ emit_messages()
+ return Result(n_entries=n_entries, n_msgids=n_msgids, n_msgstrs=n_msgstrs, n_warnings=n_warnings, n_errors=n_errors)
+
+ try:
+ po = polib.pofile(file_path)
+ except Exception, e:
+ error_lines.append(entry_seperator)
+ error_lines.append('Unable to parse file "%s": %s' % (file_path, e))
+ n_errors += 1
+ emit_messages()
+ return Result(n_entries=n_entries, n_msgids=n_msgids, n_msgstrs=n_msgstrs, n_warnings=n_warnings, n_errors=n_errors)
+
+ if validation_mode == 'po' and reference_pot:
+ # Merge the .pot file for comments and file locations
+ po.merge(reference_pot)
+
+ if validation_mode == 'po':
+ plural_forms = po.metadata.get('Plural-Forms')
+ if not plural_forms:
+ error_lines.append(entry_seperator)
+ error_lines.append("%s: does not have Plural-Forms header" % file_path)
+ n_errors += 1
+ match = re.search(r'\bnplurals\s*=\s*(\d+)', plural_forms)
+ if match:
+ n_plural_forms = int(match.group(1))
+ else:
+ error_lines.append(entry_seperator)
+ error_lines.append("%s: does not specify integer nplurals in Plural-Forms header" % file_path)
+ n_errors += 1
+
+ n_entries = len(po)
+ for entry in po:
+ entry_warnings = []
+ entry_errors = []
+ have_msgid = entry.msgid.strip() != ''
+ have_msgid_plural = entry.msgid_plural.strip() != ''
+ have_msgstr = entry.msgstr.strip() != ''
+
+ if have_msgid:
+ n_msgids += 1
+ if have_msgid_plural:
+ n_msgids += 1
+ if have_msgstr:
+ n_msgstrs += 1
+
+ if validation_mode == 'pot':
+ prog_langs = get_prog_langs(entry)
+ if have_msgid:
+ errors = validate_positional_substitutions(entry.msgid, prog_langs, 'msgid')
+ entry_errors.extend(errors)
+ if have_msgid_plural:
+ errors = validate_positional_substitutions(entry.msgid_plural, prog_langs, 'msgid_plural')
+ entry_errors.extend(errors)
+ elif validation_mode == 'po':
+ if have_msgid:
+ if have_msgstr:
+ errors = validate_substitutions_match(entry.msgid, entry.msgstr, 'msgid', 'msgstr')
+ entry_errors.extend(errors)
+
+ if have_msgid_plural and have_msgstr:
+ n_plurals = 0
+ for index, msgstr in entry.msgstr_plural.items():
+ have_msgstr_plural = msgstr.strip() != ''
+ if have_msgstr_plural:
+ n_plurals += 1
+ errors = validate_substitutions_match(entry.msgid_plural, msgstr, 'msgid_plural', 'msgstr_plural[%s]' % index)
+ entry_errors.extend(errors)
+ else:
+ entry_errors.append('msgstr_plural[%s] is empty' % (index))
+ if n_plural_forms != n_plurals:
+ entry_errors.append('%d plural forms specified, but this entry has %d plurals' % (n_plural_forms, n_plurals))
+
+ if pedantic:
+ if have_msgid:
+ errors = validate_substitution_syntax(entry.msgid, 'msgid')
+ entry_warnings.extend(errors)
+
+ if have_msgid_plural:
+ errors = validate_substitution_syntax(entry.msgid_plural, 'msgid_plural')
+ entry_warnings.extend(errors)
+
+ errors = validate_substitutions_match(entry.msgid, entry.msgid_plural, 'msgid', 'msgid_plural')
+ entry_warnings.extend(errors)
+
+ for index, msgstr in entry.msgstr_plural.items():
+ have_msgstr_plural = msgstr.strip() != ''
+ if have_msgstr_plural:
+ errors = validate_substitution_syntax(msgstr, 'msgstr_plural[%s]' % index)
+ entry_warnings.extend(errors)
+
+ if have_msgstr:
+ errors = validate_substitution_syntax(entry.msgstr, 'msgstr')
+ entry_warnings.extend(errors)
+
+ if entry_warnings:
+ warning_lines.append(entry_seperator)
+ warning_lines.append('locations: %s' % (', '.join(["%s:%d" % (x[0], int(x[1])) for x in entry.occurrences])))
+ warning_lines.extend(entry_warnings)
+ n_warnings += 1
+
+ if entry_errors:
+ error_lines.append(entry_seperator)
+ error_lines.append('locations: %s' % (', '.join(["%s:%d" % (x[0], int(x[1])) for x in entry.occurrences])))
+ error_lines.extend(entry_errors)
+ n_errors += 1
+
+ emit_messages()
+ return Result(n_entries=n_entries, n_msgids=n_msgids, n_msgstrs=n_msgstrs, n_warnings=n_warnings, n_errors=n_errors)
+
+
+#----------------------------------------------------------------------
+def create_po(pot_file, po_file, mo_file):
+
+ if not os.path.isfile(pot_file):
+ print >>sys.stderr, 'file does not exist "%s"' % (pot_file)
+ return 1
+ try:
+ po = polib.pofile(pot_file)
+ except Exception, e:
+ print >>sys.stderr, 'Unable to parse file "%s": %s' % (pot_file, e)
+ return 1
+
+ # Update the metadata in the po file header
+ # It's case insensitive so search the keys in a case insensitive manner
+ #
+ # We need to update the Plural-Forms otherwise gettext.py will raise the
+ # following error:
+ #
+ # raise ValueError, 'plural forms expression could be dangerous'
+ #
+ # It is demanding the rhs of plural= only contains the identifer 'n'
+
+ for k,v in po.metadata.items():
+ if k.lower() == 'plural-forms':
+ po.metadata[k] = 'nplurals=2; plural=(n != 1)'
+ break
+
+
+ # Iterate over all msgid's and form a the msgstr by prepending
+ # the prefix and appending the suffix
+ for entry in po:
+ if entry.msgid_plural:
+ entry.msgstr_plural = {0: prefix + entry.msgid + suffix,
+ 1: prefix + entry.msgid_plural + suffix}
+ else:
+ entry.msgstr = prefix + entry.msgid + suffix
+
+ # Write out the po and mo files
+ po.save(po_file)
+ print "Wrote: %s" % (po_file)
+
+ po.save_as_mofile(mo_file)
+ print "Wrote: %s" % (mo_file)
+
+ return 0
+
+#----------------------------------------------------------------------
+
+def validate_unicode_edit(msgid, msgstr):
+ # Verify the first character is the test prefix
+ if msgstr[0] != prefix:
+ raise ValueError('First char in translated string "%s" not equal to prefix "%s"' %
+ (msgstr.encode('utf-8'), prefix.encode('utf-8')))
+
+ # Verify the last character is the test suffix
+ if msgstr[-1] != suffix:
+ raise ValueError('Last char in translated string "%s" not equal to suffix "%s"' %
+ (msgstr.encode('utf-8'), suffix.encode('utf-8')))
+
+ # Verify everything between the first and last character is the
+ # original untranslated string
+ if msgstr[1:-1] != msgid:
+ raise ValueError('Translated string "%s" minus the first & last character is not equal to msgid "%s"' %
+ (msgstr.encode('utf-8'), msgid))
+
+ if verbose:
+ msg = 'Success: message string "%s" maps to translated string "%s"' % (msgid, msgstr)
+ print msg.encode('utf-8')
+
+
+def test_translations(po_file, lang, domain, locale_dir):
+ # The test installs the test message catalog under the xh_ZA
+ # (e.g. Zambia Xhosa) language by default. It would be nice to
+ # use a dummy language not associated with any real language,
+ # but the setlocale function demands the locale be a valid
+ # known locale, Zambia Xhosa is a reasonable choice :)
+
+ os.environ['LANG'] = lang
+
+ # Create a gettext translation object specifying our domain as
+ # 'ipa' and the locale_dir as 'test_locale' (i.e. where to
+ # look for the message catalog). Then use that translation
+ # object to obtain the translation functions.
+
+ t = gettext.translation(domain, locale_dir)
+
+ get_msgstr = t.ugettext
+ get_msgstr_plural = t.ungettext
+
+ return po_file_iterate(po_file, get_msgstr, get_msgstr_plural)
+
+def po_file_iterate(po_file, get_msgstr, get_msgstr_plural):
+ try:
+ # Iterate over the msgid's
+ if not os.path.isfile(po_file):
+ print >>sys.stderr, 'file does not exist "%s"' % (po_file)
+ return 1
+ try:
+ po = polib.pofile(po_file)
+ except Exception, e:
+ print >>sys.stderr, 'Unable to parse file "%s": %s' % (po_file, e)
+ return 1
+
+ n_entries = 0
+ n_translations = 0
+ n_valid = 0
+ n_fail = 0
+ for entry in po:
+ if entry.msgid_plural:
+ msgid = entry.msgid
+ msgid_plural = entry.msgid_plural
+ msgstr = get_msgstr_plural(msgid, msgid_plural, 1)
+ msgstr_plural = get_msgstr_plural(msgid, msgid_plural, 2)
+
+ try:
+ n_translations += 1
+ validate_unicode_edit(msgid, msgstr)
+ n_valid += 1
+ except Exception, e:
+ n_fail += 1
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+
+ try:
+ n_translations += 1
+ validate_unicode_edit(msgid_plural, msgstr_plural)
+ n_valid += 1
+ except Exception, e:
+ n_fail += 1
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+
+
+ else:
+ msgid = entry.msgid
+ msgstr = get_msgstr(msgid)
+
+ try:
+ n_translations += 1
+ validate_unicode_edit(msgid, msgstr)
+ n_valid += 1
+ except Exception, e:
+ n_fail += 1
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+
+ n_entries += 1
+
+ except Exception, e:
+ if print_traceback:
+ traceback.print_exc()
+ print >> sys.stderr, "ERROR: %s" % e
+ return 1
+
+ if not n_entries:
+ print >> sys.stderr, "ERROR: no translations found in %s" % (po_filename)
+ return 1
+
+ if n_fail:
+ print >> sys.stderr, "ERROR: %d failures out of %d translations" % (n_fail, n_entries)
+ return 1
+
+ print "%d translations in %d messages successfully tested" % (n_translations, n_entries)
+ return 0
+
+#----------------------------------------------------------------------
+
+usage ='''
+
+%prog --test-gettext
+%prog --create-test
+%prog --validate-pot [pot_file1, ...]
+%prog --validate-po po_file1 [po_file2, ...]
+'''
+
+def main():
+ global verbose, print_traceback, pedantic, show_strings
+
+ parser = optparse.OptionParser(usage=usage)
+
+ mode_group = optparse.OptionGroup(parser, 'Operational Mode',
+ 'You must select one these modes to run in')
+
+ mode_group.add_option('-g', '--test-gettext', action='store_const', const='test_gettext', dest='mode',
+ help='create the test translation file(s) and exercise them')
+ mode_group.add_option('-c', '--create-test', action='store_const', const='create_test', dest='mode',
+ help='create the test translation file(s)')
+ mode_group.add_option('-P', '--validate-pot', action='store_const', const='validate_pot', dest='mode',
+ help='validate pot file(s)')
+ mode_group.add_option('-p', '--validate-po', action='store_const', const='validate_po', dest='mode',
+ help='validate po file(s)')
+
+ parser.add_option_group(mode_group)
+ parser.set_defaults(mode='')
+
+ parser.add_option('-s', '--show-strings', action='store_true', dest='show_strings', default=False,
+ help='show the offending string when an error is detected')
+ parser.add_option('--pedantic', action='store_true', dest='pedantic', default=False,
+ help='be aggressive when validating')
+ parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False,
+ help='be informative')
+ parser.add_option('--traceback', action='store_true', dest='print_traceback', default=False,
+ help='print the traceback when an exception occurs')
+
+ param_group = optparse.OptionGroup(parser, 'Run Time Parameters',
+ 'These may be used to modify the run time defaults')
+
+ param_group.add_option('--test-lang', action='store', dest='test_lang', default='test',
+ help="test po file uses this as it's basename (default=test)")
+ param_group.add_option('--lang', action='store', dest='lang', default='xh_ZA',
+ help='lang used for locale, MUST be a valid lang (default=xh_ZA)')
+ param_group.add_option('--domain', action='store', dest='domain', default='ipa',
+ help='translation domain used during test (default=ipa)')
+ param_group.add_option('--locale-dir', action='store', dest='locale_dir', default='test_locale',
+ help='locale directory used during test (default=test_locale)')
+ param_group.add_option('--pot-file', action='store', dest='pot_file', default='ipa.pot',
+ help='default pot file, used when validating pot file or generating test po and mo files (default=ipa.pot)')
+
+ parser.add_option_group(param_group)
+
+ options, args = parser.parse_args()
+
+ verbose = options.verbose
+ print_traceback = options.print_traceback
+ pedantic = options.pedantic
+ show_strings = options.show_strings
+
+ if not options.mode:
+ print >> sys.stderr, 'ERROR: no mode specified'
+ return 1
+
+ if options.mode == 'validate_pot' or options.mode == 'validate_po':
+ if options.mode == 'validate_pot':
+ files = args
+ if not files:
+ files = [options.pot_file]
+ validation_mode = 'pot'
+ reference_pot = None
+ elif options.mode == 'validate_po':
+ files = args
+ if not files:
+ print >> sys.stderr, 'ERROR: no po files specified'
+ return 1
+ validation_mode = 'po'
+ reference_pot = polib.pofile(options.pot_file)
+ else:
+ print >> sys.stderr, 'ERROR: unknown validation mode "%s"' % (options.mode)
+ return 1
+
+ total_entries = 0
+ total_msgids = 0
+ total_msgstrs = 0
+ total_warnings = 0
+ total_errors = 0
+
+ for f in files:
+ result = validate_file(f, validation_mode, reference_pot)
+ total_entries += result.n_entries
+ total_msgids += result.n_msgids
+ total_msgstrs += result.n_msgstrs
+ total_warnings += result.n_warnings
+ total_errors += result.n_errors
+ print "%s: %d entries, %d msgid, %d msgstr, %d warnings %d errors" % \
+ (f, result.n_entries, result.n_msgids, result.n_msgstrs, result.n_warnings, result.n_errors)
+ if total_errors:
+ print section_seperator
+ print "%d errors in %d files" % (total_errors, len(files))
+ return 1
+ else:
+ return 0
+
+ elif options.mode == 'create_test' or 'test_gettext':
+ po_file = '%s.po' % options.test_lang
+ pot_file = options.pot_file
+
+ msg_dir = os.path.join(options.locale_dir, options.lang, 'LC_MESSAGES')
+ if not os.path.exists(msg_dir):
+ os.makedirs(msg_dir)
+
+ mo_basename = '%s.mo' % options.domain
+ mo_file = os.path.join(msg_dir, mo_basename)
+
+ result = create_po(pot_file, po_file, mo_file)
+ if result:
+ return result
+
+ if options.mode == 'create_test':
+ return result
+
+ # The test installs the test message catalog under the xh_ZA
+ # (e.g. Zambia Xhosa) language by default. It would be nice to
+ # use a dummy language not associated with any real language,
+ # but the setlocale function demands the locale be a valid
+ # known locale, Zambia Xhosa is a reasonable choice :)
+
+ lang = options.lang
+
+ # Create a gettext translation object specifying our domain as
+ # 'ipa' and the locale_dir as 'test_locale' (i.e. where to
+ # look for the message catalog). Then use that translation
+ # object to obtain the translation functions.
+
+ domain = options.domain
+ locale_dir = options.locale_dir
+
+ return test_translations(po_file, lang, domain, locale_dir)
+
+ else:
+ print >> sys.stderr, 'ERROR: unknown mode "%s"' % (options.mode)
+ return 1
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ipatests/setup.py.in b/ipatests/setup.py.in
new file mode 100644
index 000000000..2517651db
--- /dev/null
+++ b/ipatests/setup.py.in
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+# Copyright (C) 2007 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/>.
+#
+
+"""FreeIPA tests
+
+FreeIPA is a server for identity, policy, and audit.
+"""
+
+DOCLINES = __doc__.split("\n")
+
+import os
+import sys
+import distutils.sysconfig
+
+CLASSIFIERS = """\
+Development Status :: 4 - Beta
+Intended Audience :: System Environment/Base
+License :: GPL
+Programming Language :: Python
+Operating System :: POSIX
+Operating System :: Unix
+"""
+
+# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
+# update it when the contents of directories change.
+if os.path.exists('MANIFEST'):
+ os.remove('MANIFEST')
+
+def setup_package():
+
+ from distutils.core import setup
+
+ old_path = os.getcwd()
+ local_path = os.path.dirname(os.path.abspath(sys.argv[0]))
+ os.chdir(local_path)
+ sys.path.insert(0, local_path)
+
+ try:
+ setup(
+ name = "ipatests",
+ version = "__VERSION__",
+ license = "GPL",
+ author = "FreeIPA Developers",
+ author_email = "freeipa-devel@redhat.com",
+ maintainer = "FreeIPA Developers",
+ maintainer_email = "freeipa-devel@redhat.com",
+ url = "http://www.freeipa.org/",
+ description = DOCLINES[0],
+ long_description = "\n".join(DOCLINES[2:]),
+ download_url = "http://www.freeipa.org/page/Downloads",
+ classifiers=filter(None, CLASSIFIERS.split('\n')),
+ package_dir = {'ipatests': ''},
+ packages = ["ipatests",
+ "ipatests.test_cmdline",
+ "ipatests.test_install",
+ "ipatests.test_ipalib",
+ "ipatests.test_ipapython",
+ "ipatests.test_ipaserver",
+ "ipatests.test_ipaserver.install",
+ "ipatests.test_pkcs10",
+ "ipatests.test_xmlrpc"],
+ package_data = {
+ 'ipatests.test_install': ['*.update'],
+ 'ipatests.test_pkcs10': ['*.csr']}
+ )
+ finally:
+ del sys.path[0]
+ os.chdir(old_path)
+ return
+
+if __name__ == '__main__':
+ setup_package()
diff --git a/ipatests/test_cmdline/cmdline.py b/ipatests/test_cmdline/cmdline.py
new file mode 100644
index 000000000..6f3541d27
--- /dev/null
+++ b/ipatests/test_cmdline/cmdline.py
@@ -0,0 +1,70 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Base class for all cmdline tests
+"""
+
+import nose
+import krbV
+
+from ipalib import api
+from ipalib import errors
+from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test
+from ipaserver.plugins.ldap2 import ldap2
+from ipapython import ipautil
+
+# See if our LDAP server is up and we can talk to it over GSSAPI
+ccache = krbV.default_context().default_ccache()
+
+try:
+ conn = ldap2(shared_instance=False, ldap_uri=api.env.ldap_uri, base_dn=api.env.basedn)
+ conn.connect(ccache=ccache)
+ conn.disconnect()
+ server_available = True
+except errors.DatabaseError:
+ server_available = False
+except Exception, e:
+ server_available = False
+
+class cmdline_test(XMLRPC_test):
+ """
+ Base class for all command-line tests
+ """
+ # some reasonable default command
+ command = '/bin/ls'
+
+ def setUp(self):
+ # raise an error if the command is missing even if the remote
+ # server is not available.
+ if not ipautil.file_exists(self.command):
+ raise AssertionError(
+ 'Command %r not available' % self.command
+ )
+ super(cmdline_test, self).setUp()
+ if not server_available:
+ raise nose.SkipTest(
+ 'Server not available: %r' % api.env.xmlrpc_uri
+ )
+
+ def tearDown(self):
+ """
+ nose tear-down fixture.
+ """
+ super(cmdline_test, self).tearDown()
diff --git a/ipatests/test_cmdline/test_cli.py b/ipatests/test_cmdline/test_cli.py
new file mode 100644
index 000000000..fe411b703
--- /dev/null
+++ b/ipatests/test_cmdline/test_cli.py
@@ -0,0 +1,327 @@
+import shlex
+import sys
+import contextlib
+import StringIO
+
+import nose
+
+from ipatests import util
+from ipalib import api, errors
+from ipapython.version import API_VERSION
+
+
+class TestCLIParsing(object):
+ """Tests that commandlines are correctly parsed to Command keyword args
+ """
+ def check_command(self, commandline, expected_command_name, **kw_expected):
+ argv = shlex.split(commandline)
+ executioner = api.Backend.cli
+
+ cmd = executioner.get_command(argv)
+ kw_got = executioner.parse(cmd, argv[1:])
+ kw_got = executioner.process_keyword_arguments(cmd, kw_got)
+ util.assert_deepequal(expected_command_name, cmd.name, 'Command name')
+ util.assert_deepequal(kw_expected, kw_got)
+
+ def run_command(self, command_name, **kw):
+ """Run a command on the server"""
+ if not api.Backend.xmlclient.isconnected():
+ api.Backend.xmlclient.connect(fallback=False)
+ try:
+ api.Command[command_name](**kw)
+ except errors.NetworkError:
+ raise nose.SkipTest('%r: Server not available: %r' %
+ (self.__module__, api.env.xmlrpc_uri))
+
+ @contextlib.contextmanager
+ def fake_stdin(self, string_in):
+ """Context manager that temporarily replaces stdin to read a string"""
+ old_stdin = sys.stdin
+ sys.stdin = StringIO.StringIO(string_in)
+ yield
+ sys.stdin = old_stdin
+
+ def test_ping(self):
+ self.check_command('ping', 'ping',
+ version=API_VERSION)
+
+ def test_user_show(self):
+ self.check_command('user-show admin', 'user_show',
+ uid=u'admin',
+ rights=False,
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_user_show_underscore(self):
+ self.check_command('user_show admin', 'user_show',
+ uid=u'admin',
+ rights=False,
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_group_add(self):
+ self.check_command('group-add tgroup1 --desc="Test group"',
+ 'group_add',
+ cn=u'tgroup1',
+ description=u'Test group',
+ nonposix=False,
+ external=False,
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_sudocmdgroup_add_member(self):
+ # Test CSV splitting is not done
+ self.check_command(
+ # The following is as it would appear on the command line:
+ r'sudocmdgroup-add-member tcmdgroup1 --sudocmds=ab,c --sudocmds=d',
+ 'sudocmdgroup_add_member',
+ cn=u'tcmdgroup1',
+ sudocmd=[u'ab,c', u'd'],
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_group_add_nonposix(self):
+ self.check_command('group-add tgroup1 --desc="Test group" --nonposix',
+ 'group_add',
+ cn=u'tgroup1',
+ description=u'Test group',
+ nonposix=True,
+ external=False,
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_group_add_gid(self):
+ self.check_command('group-add tgroup1 --desc="Test group" --gid=1234',
+ 'group_add',
+ cn=u'tgroup1',
+ description=u'Test group',
+ gidnumber=u'1234',
+ nonposix=False,
+ external=False,
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_group_add_interactive(self):
+ with self.fake_stdin('Test group\n'):
+ self.check_command('group-add tgroup1', 'group_add',
+ cn=u'tgroup1',
+ description=u'Test group',
+ nonposix=False,
+ external=False,
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_dnsrecord_add(self):
+ self.check_command('dnsrecord-add test-example.com ns --a-rec=1.2.3.4',
+ 'dnsrecord_add',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'ns',
+ arecord=u'1.2.3.4',
+ structured=False,
+ force=False,
+ raw=False,
+ all=False,
+ version=API_VERSION)
+
+ def test_dnsrecord_del_all(self):
+ try:
+ self.run_command('dnszone_add', idnsname=u'test-example.com',
+ idnssoamname=u'ns.test-example.com', force=True)
+ except errors.NotFound:
+ raise nose.SkipTest('DNS is not configured')
+ try:
+ self.run_command('dnsrecord_add',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'ns', arecord=u'1.2.3.4')
+ with self.fake_stdin('yes\n'):
+ self.check_command('dnsrecord_del test-example.com ns',
+ 'dnsrecord_del',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'ns',
+ del_all=True,
+ structured=False,
+ version=API_VERSION)
+ with self.fake_stdin('YeS\n'):
+ self.check_command('dnsrecord_del test-example.com ns',
+ 'dnsrecord_del',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'ns',
+ del_all=True,
+ structured=False,
+ version=API_VERSION)
+ finally:
+ self.run_command('dnszone_del', idnsname=u'test-example.com')
+
+ def test_dnsrecord_del_one_by_one(self):
+ try:
+ self.run_command('dnszone_add', idnsname=u'test-example.com',
+ idnssoamname=u'ns.test-example.com', force=True)
+ except errors.NotFound:
+ raise nose.SkipTest('DNS is not configured')
+ try:
+ records = (u'1 1 E3B72BA346B90570EED94BE9334E34AA795CED23',
+ u'2 1 FD2693C1EFFC11A8D2BE57229212A04B45663791')
+ for record in records:
+ self.run_command('dnsrecord_add',
+ dnszoneidnsname=u'test-example.com', idnsname=u'ns',
+ sshfprecord=record)
+ with self.fake_stdin('no\nyes\nyes\n'):
+ self.check_command('dnsrecord_del test-example.com ns',
+ 'dnsrecord_del',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'ns',
+ del_all=False,
+ sshfprecord=records,
+ structured=False,
+ version=API_VERSION)
+ finally:
+ self.run_command('dnszone_del', idnsname=u'test-example.com')
+
+ def test_dnsrecord_add_ask_for_missing_fields(self):
+ sshfp_parts = (1, 1, u'E3B72BA346B90570EED94BE9334E34AA795CED23')
+
+ with self.fake_stdin('SSHFP\n%d\n%d\n%s' % sshfp_parts):
+ self.check_command('dnsrecord-add test-example.com sshfp',
+ 'dnsrecord_add',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'sshfp',
+ sshfp_part_fp_type=sshfp_parts[0],
+ sshfp_part_algorithm=sshfp_parts[1],
+ sshfp_part_fingerprint=sshfp_parts[2],
+ structured=False,
+ raw=False,
+ all=False,
+ force=False,
+ version=API_VERSION)
+
+ # NOTE: when a DNS record part is passed via command line, it is not
+ # converted to its base type when transfered via wire
+ with self.fake_stdin('%d\n%s' % (sshfp_parts[1], sshfp_parts[2])):
+ self.check_command('dnsrecord-add test-example.com sshfp ' \
+ '--sshfp-algorithm=%d' % sshfp_parts[0],
+ 'dnsrecord_add',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'sshfp',
+ sshfp_part_fp_type=sshfp_parts[0],
+ sshfp_part_algorithm=unicode(sshfp_parts[1]), # passed via cmdline
+ sshfp_part_fingerprint=sshfp_parts[2],
+ structured=False,
+ raw=False,
+ all=False,
+ force=False,
+ version=API_VERSION)
+
+ with self.fake_stdin(sshfp_parts[2]):
+ self.check_command('dnsrecord-add test-example.com sshfp ' \
+ '--sshfp-algorithm=%d --sshfp-fp-type=%d' % (sshfp_parts[0], sshfp_parts[1]),
+ 'dnsrecord_add',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'sshfp',
+ sshfp_part_fp_type=unicode(sshfp_parts[0]), # passed via cmdline
+ sshfp_part_algorithm=unicode(sshfp_parts[1]), # passed via cmdline
+ sshfp_part_fingerprint=sshfp_parts[2],
+ structured=False,
+ raw=False,
+ all=False,
+ force=False,
+ version=API_VERSION)
+
+ def test_dnsrecord_del_comma(self):
+ try:
+ self.run_command(
+ 'dnszone_add', idnsname=u'test-example.com',
+ idnssoamname=u'ns.test-example.com', force=True)
+ except errors.NotFound:
+ raise nose.SkipTest('DNS is not configured')
+ try:
+ self.run_command(
+ 'dnsrecord_add',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'test',
+ txtrecord=u'"A pretty little problem," said Holmes.')
+ with self.fake_stdin('no\nyes\n'):
+ self.check_command(
+ 'dnsrecord_del test-example.com test',
+ 'dnsrecord_del',
+ dnszoneidnsname=u'test-example.com',
+ idnsname=u'test',
+ del_all=False,
+ txtrecord=[u'"A pretty little problem," said Holmes.'],
+ structured=False,
+ version=API_VERSION)
+ finally:
+ self.run_command('dnszone_del', idnsname=u'test-example.com')
+
+ def test_dnszone_add(self):
+ """
+ Test dnszone-add with nameserver IP passed interatively
+ """
+ # Pass IP of nameserver interactively for nameserver in zone
+ # (absolute name)
+ with self.fake_stdin('1.1.1.1\n'):
+ self.check_command(
+ 'dnszone_add example.com --name-server=ns.example.com. '
+ '--admin-email=admin@example.com',
+ 'dnszone_add',
+ idnsname=u'example.com',
+ idnssoamname=u'ns.example.com.',
+ idnssoarname=u'admin@example.com',
+ ip_address=u'1.1.1.1',
+ idnssoaexpire=util.Fuzzy(type=int),
+ idnssoaserial=util.Fuzzy(type=int),
+ idnssoaretry=util.Fuzzy(type=int),
+ idnssoaminimum=util.Fuzzy(type=int),
+ idnssoarefresh=util.Fuzzy(type=int),
+ all=False,
+ raw=False,
+ force=False,
+ version=API_VERSION
+ )
+
+ # Pass IP of nameserver interactively for nameserver in zone
+ # (relative name)
+ with self.fake_stdin('1.1.1.1\n'):
+ self.check_command(
+ 'dnszone_add example.com --name-server=ns '
+ '--admin-email=admin@example.com',
+ 'dnszone_add',
+ idnsname=u'example.com',
+ idnssoamname=u'ns',
+ idnssoarname=u'admin@example.com',
+ ip_address=u'1.1.1.1',
+ idnssoaexpire=util.Fuzzy(type=int),
+ idnssoaserial=util.Fuzzy(type=int),
+ idnssoaretry=util.Fuzzy(type=int),
+ idnssoaminimum=util.Fuzzy(type=int),
+ idnssoarefresh=util.Fuzzy(type=int),
+ all=False,
+ raw=False,
+ force=False,
+ version=API_VERSION
+ )
+
+ # Nameserver is outside the zone - no need to pass the IP
+ self.check_command(
+ 'dnszone_add example.com --name-server=ns.example.net. '
+ '--admin-email=admin@example.com',
+ 'dnszone_add',
+ idnsname=u'example.com',
+ idnssoamname=u'ns.example.net.',
+ idnssoarname=u'admin@example.com',
+ idnssoaexpire=util.Fuzzy(type=int),
+ idnssoaserial=util.Fuzzy(type=int),
+ idnssoaretry=util.Fuzzy(type=int),
+ idnssoaminimum=util.Fuzzy(type=int),
+ idnssoarefresh=util.Fuzzy(type=int),
+ all=False,
+ raw=False,
+ force=False,
+ version=API_VERSION
+ )
diff --git a/ipatests/test_cmdline/test_help.py b/ipatests/test_cmdline/test_help.py
new file mode 100644
index 000000000..4cf633683
--- /dev/null
+++ b/ipatests/test_cmdline/test_help.py
@@ -0,0 +1,141 @@
+# Authors: Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+#
+
+import sys
+import contextlib
+import StringIO
+
+from nose.tools import assert_raises # pylint: disable=E0611
+
+from ipalib import api, errors
+from ipalib.plugins.user import user_add
+
+
+class CLITestContext(object):
+ """Context manager that replaces stdout & stderr, and catches SystemExit
+
+ Whatever was printed to the streams is available in ``stdout`` and
+ ``stderr`` attrributes once the with statement finishes.
+
+ When exception is given, asserts that exception is raised. The exception
+ will be available in the ``exception`` attribute.
+ """
+ def __init__(self, exception=None):
+ self.exception = exception
+
+ def __enter__(self):
+ self.old_streams = sys.stdout, sys.stderr
+ self.stdout_fileobj = sys.stdout = StringIO.StringIO()
+ self.stderr_fileobj = sys.stderr = StringIO.StringIO()
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ sys.stdout, sys.stderr = self.old_streams
+ self.stdout = self.stdout_fileobj.getvalue()
+ self.stderr = self.stderr_fileobj.getvalue()
+ self.stdout_fileobj.close()
+ self.stderr_fileobj.close()
+ if self.exception:
+ assert isinstance(exc_value, self.exception), exc_value
+ self.exception = exc_value
+ return True
+
+
+def test_ipa_help():
+ """Test that `ipa help` only writes to stdout"""
+ with CLITestContext() as ctx:
+ return_value = api.Backend.cli.run(['help'])
+ assert return_value == 0
+ assert ctx.stderr == ''
+
+
+def test_ipa_without_arguments():
+ """Test that `ipa` errors out, and prints the help to stderr"""
+ with CLITestContext(exception=SystemExit) as ctx:
+ api.Backend.cli.run([])
+ assert ctx.exception.code == 2
+ assert ctx.stdout == ''
+ assert 'Error: Command not specified' in ctx.stderr
+
+ with CLITestContext() as help_ctx:
+ api.Backend.cli.run(['help'])
+ assert help_ctx.stdout in ctx.stderr
+
+
+def test_bare_topic():
+ """Test that `ipa user` errors out, and prints the help to stderr
+
+ This is because `user` is a topic, not a command, so `ipa user` doesn't
+ match our usage string. The help should be accessed using `ipa help user`.
+ """
+ with CLITestContext(exception=errors.CommandError) as ctx:
+ api.Backend.cli.run(['user'])
+ assert ctx.exception.name == 'user'
+ assert ctx.stdout == ''
+
+ with CLITestContext() as help_ctx:
+ return_value = api.Backend.cli.run(['help', 'user'])
+ assert return_value == 0
+ assert help_ctx.stdout in ctx.stderr
+
+
+def test_command_help():
+ """Test that `help user-add` & `user-add -h` are equivalent and contain doc
+ """
+ with CLITestContext() as help_ctx:
+ return_value = api.Backend.cli.run(['help', 'user-add'])
+ assert return_value == 0
+ assert help_ctx.stderr == ''
+
+ with CLITestContext(exception=SystemExit) as h_ctx:
+ api.Backend.cli.run(['user-add', '-h'])
+ assert h_ctx.exception.code == 0
+ assert h_ctx.stderr == ''
+
+ assert h_ctx.stdout == help_ctx.stdout
+ assert unicode(user_add.__doc__) in help_ctx.stdout
+
+
+def test_ambiguous_command_or_topic():
+ """Test that `help ping` & `ping -h` are NOT equivalent
+
+ One is a topic, the other is a command
+ """
+ with CLITestContext() as help_ctx:
+ return_value = api.Backend.cli.run(['help', 'ping'])
+ assert return_value == 0
+ assert help_ctx.stderr == ''
+
+ with CLITestContext(exception=SystemExit) as h_ctx:
+ api.Backend.cli.run(['ping', '-h'])
+ assert h_ctx.exception.code == 0
+ assert h_ctx.stderr == ''
+
+ assert h_ctx.stdout != help_ctx.stdout
+
+def test_multiline_description():
+ """Test that all of a multi-line command description appears in output
+ """
+ # This assumes trust_add has multiline doc. Ensure it is so.
+ assert '\n\n' in unicode(api.Command.trust_add.doc).strip()
+
+ with CLITestContext(exception=SystemExit) as help_ctx:
+ return_value = api.Backend.cli.run(['trust-add', '-h'])
+
+ assert unicode(api.Command.trust_add.doc).strip() in help_ctx.stdout
diff --git a/ipatests/test_cmdline/test_ipagetkeytab.py b/ipatests/test_cmdline/test_ipagetkeytab.py
new file mode 100644
index 000000000..cb46fd23b
--- /dev/null
+++ b/ipatests/test_cmdline/test_ipagetkeytab.py
@@ -0,0 +1,152 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+"""
+Test `ipa-getkeytab`
+"""
+
+import os
+import shutil
+from cmdline import cmdline_test
+from ipalib import api
+from ipalib import errors
+import tempfile
+from ipapython import ipautil
+import nose
+import tempfile
+import krbV
+from ipaserver.plugins.ldap2 import ldap2
+from ipapython.dn import DN
+
+def use_keytab(principal, keytab):
+ try:
+ tmpdir = tempfile.mkdtemp(prefix = "tmp-")
+ ccache_file = 'FILE:%s/ccache' % tmpdir
+ krbcontext = krbV.default_context()
+ principal = str(principal)
+ keytab = krbV.Keytab(name=keytab, context=krbcontext)
+ principal = krbV.Principal(name=principal, context=krbcontext)
+ os.environ['KRB5CCNAME'] = ccache_file
+ ccache = krbV.CCache(name=ccache_file, context=krbcontext, primary_principal=principal)
+ ccache.init(principal)
+ ccache.init_creds_keytab(keytab=keytab, principal=principal)
+ conn = ldap2(shared_instance=False, ldap_uri=api.env.ldap_uri, base_dn=api.env.basedn)
+ conn.connect(ccache=ccache)
+ conn.disconnect()
+ except krbV.Krb5Error, e:
+ raise StandardError('Unable to bind to LDAP. Error initializing principal %s in %s: %s' % (principal.name, keytab, str(e)))
+ finally:
+ del os.environ['KRB5CCNAME']
+ if tmpdir:
+ shutil.rmtree(tmpdir)
+
+class test_ipagetkeytab(cmdline_test):
+ """
+ Test `ipa-getkeytab`.
+ """
+ command = "ipa-client/ipa-getkeytab"
+ host_fqdn = u'ipatest.%s' % api.env.domain
+ service_princ = u'test/%s@%s' % (host_fqdn, api.env.realm)
+ [keytabfd, keytabname] = tempfile.mkstemp()
+ os.close(keytabfd)
+
+ def test_0_setup(self):
+ """
+ Create a host to test against.
+ """
+ # Create the service
+ try:
+ api.Command['host_add'](self.host_fqdn, force=True)
+ except errors.DuplicateEntry:
+ # it already exists, no problem
+ pass
+
+ def test_1_run(self):
+ """
+ Create a keytab with `ipa-getkeytab` for a non-existent service.
+ """
+ new_args = [self.command,
+ "-s", api.env.host,
+ "-p", "test/notfound.example.com",
+ "-k", self.keytabname,
+ ]
+ (out, err, rc) = ipautil.run(new_args, stdin=None, raiseonerr=False)
+ assert err == 'Operation failed! PrincipalName not found.\n\n'
+
+ def test_2_run(self):
+ """
+ Create a keytab with `ipa-getkeytab` for an existing service.
+ """
+ # Create the service
+ try:
+ api.Command['service_add'](self.service_princ, force=True)
+ except errors.DuplicateEntry:
+ # it already exists, no problem
+ pass
+
+ os.unlink(self.keytabname)
+ new_args = [self.command,
+ "-s", api.env.host,
+ "-p", self.service_princ,
+ "-k", self.keytabname,
+ ]
+ try:
+ (out, err, rc) = ipautil.run(new_args, None)
+ expected = 'Keytab successfully retrieved and stored in: %s\n' % (
+ self.keytabname)
+ assert expected in err, 'Success message not in output:\n%s' % err
+ except ipautil.CalledProcessError, e:
+ assert (False)
+
+ def test_3_use(self):
+ """
+ Try to use the service keytab.
+ """
+ use_keytab(self.service_princ, self.keytabname)
+
+ def test_4_disable(self):
+ """
+ Disable a kerberos principal
+ """
+ # Verify that it has a principal key
+ entry = api.Command['service_show'](self.service_princ)['result']
+ assert(entry['has_keytab'] == True)
+
+ # Disable it
+ api.Command['service_disable'](self.service_princ)
+
+ # Verify that it looks disabled
+ entry = api.Command['service_show'](self.service_princ)['result']
+ assert(entry['has_keytab'] == False)
+
+ def test_5_use_disabled(self):
+ """
+ Try to use the disabled keytab
+ """
+ try:
+ use_keytab(self.service_princ, self.keytabname)
+ except StandardError, errmsg:
+ assert('Unable to bind to LDAP. Error initializing principal' in str(errmsg))
+
+ def test_9_cleanup(self):
+ """
+ Clean up test data
+ """
+ # First create the host that will use this policy
+ os.unlink(self.keytabname)
+ api.Command['host_del'](self.host_fqdn)
diff --git a/ipatests/test_install/0_reset.update b/ipatests/test_install/0_reset.update
new file mode 100644
index 000000000..bd6ee636d
--- /dev/null
+++ b/ipatests/test_install/0_reset.update
@@ -0,0 +1,5 @@
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+deleteentry:
+
+dn: cn=test, cn=accounts, $SUFFIX
+deleteentry: reset: nada
diff --git a/ipatests/test_install/1_add.update b/ipatests/test_install/1_add.update
new file mode 100644
index 000000000..2543a71f2
--- /dev/null
+++ b/ipatests/test_install/1_add.update
@@ -0,0 +1,22 @@
+# Add in a new place in the DIT for our test cases
+
+dn: cn=test, cn=accounts, $SUFFIX
+add:objectClass: top
+add:objectClass: nsContainer
+add:cn: test
+
+# Add a test user
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+add:objectclass: top
+add:objectclass: person
+add:objectclass: posixaccount
+add:objectclass: krbprincipalaux
+add:objectclass: inetuser
+add:homedirectory: /home/tuser
+add:loginshell: /bin/bash
+add:sn: User
+add:uid: tuser
+add:uidnumber: -1
+add:gidnumber: -1
+add:cn: Test User
+
diff --git a/ipatests/test_install/2_update.update b/ipatests/test_install/2_update.update
new file mode 100644
index 000000000..0d4d38e0b
--- /dev/null
+++ b/ipatests/test_install/2_update.update
@@ -0,0 +1,3 @@
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+add:gecos: Test User
+
diff --git a/ipatests/test_install/3_update.update b/ipatests/test_install/3_update.update
new file mode 100644
index 000000000..c938b43a3
--- /dev/null
+++ b/ipatests/test_install/3_update.update
@@ -0,0 +1,3 @@
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+only:gecos: Test User New
+
diff --git a/ipatests/test_install/4_update.update b/ipatests/test_install/4_update.update
new file mode 100644
index 000000000..0aef7494f
--- /dev/null
+++ b/ipatests/test_install/4_update.update
@@ -0,0 +1,4 @@
+# Replace the existing SINGLE-VALUE gecos with a new value thru add
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+add:gecos: Test User New2
+
diff --git a/ipatests/test_install/5_update.update b/ipatests/test_install/5_update.update
new file mode 100644
index 000000000..b0a7190ea
--- /dev/null
+++ b/ipatests/test_install/5_update.update
@@ -0,0 +1,3 @@
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+add:cn: Test User New
+
diff --git a/ipatests/test_install/6_update.update b/ipatests/test_install/6_update.update
new file mode 100644
index 000000000..d398c1d4d
--- /dev/null
+++ b/ipatests/test_install/6_update.update
@@ -0,0 +1,3 @@
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+remove:cn: Test User New
+
diff --git a/ipatests/test_install/8_badsyntax.update b/ipatests/test_install/8_badsyntax.update
new file mode 100644
index 000000000..1e878964c
--- /dev/null
+++ b/ipatests/test_install/8_badsyntax.update
@@ -0,0 +1,3 @@
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+bogus:cn: Test User New
+
diff --git a/ipatests/test_install/9_badsyntax.update b/ipatests/test_install/9_badsyntax.update
new file mode 100644
index 000000000..fd6d4580b
--- /dev/null
+++ b/ipatests/test_install/9_badsyntax.update
@@ -0,0 +1,3 @@
+dn: uid=tuser, cn=test, cn=accounts, $SUFFIX
+add:cn
+
diff --git a/ipatests/test_install/__init__.py b/ipatests/test_install/__init__.py
new file mode 100644
index 000000000..0ca31f1e3
--- /dev/null
+++ b/ipatests/test_install/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Package containing LDAP updates unit tests.
+"""
diff --git a/ipatests/test_install/test_updates.py b/ipatests/test_install/test_updates.py
new file mode 100644
index 000000000..c25d74a8a
--- /dev/null
+++ b/ipatests/test_install/test_updates.py
@@ -0,0 +1,337 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+"""
+Test the `ipaserver/install/ldapupdate.py` module.
+"""
+
+import unittest
+import os
+
+import nose
+
+from ipalib import api
+from ipalib import errors
+from ipaserver.install.ldapupdate import LDAPUpdate, BadSyntax
+from ipaserver.install import installutils
+from ipapython import ipautil, ipaldap
+from ipapython.dn import DN
+
+"""
+The updater works through files only so this is just a thin-wrapper controlling
+which file we test at any given point.
+
+IMPORTANT NOTE: It is easy for these tests to get out of sync. Any changes
+made to the update files may require changes to the test cases as well.
+Some cases pull records from LDAP and do comparisons to ensure that updates
+have occurred as expected.
+
+The DM password needs to be set in ~/.ipa/.dmpw
+"""
+
+class test_update(unittest.TestCase):
+ """
+ Test the LDAP updater.
+ """
+
+ def setUp(self):
+ fqdn = installutils.get_fqdn()
+ pwfile = api.env.dot_ipa + os.sep + ".dmpw"
+ if ipautil.file_exists(pwfile):
+ fp = open(pwfile, "r")
+ self.dm_password = fp.read().rstrip()
+ fp.close()
+ else:
+ raise nose.SkipTest("No directory manager password")
+ self.updater = LDAPUpdate(dm_password=self.dm_password, sub_dict={}, live_run=True)
+ self.ld = ipaldap.IPAdmin(fqdn)
+ self.ld.do_simple_bind(bindpw=self.dm_password)
+ if ipautil.file_exists("0_reset.update"):
+ self.testdir="./"
+ elif ipautil.file_exists("ipatests/test_install/0_reset.update"):
+ self.testdir= "./ipatests/test_install/"
+ else:
+ raise nose.SkipTest("Unable to find test update files")
+
+ self.container_dn = DN(self.updater._template_str('cn=test, cn=accounts, $SUFFIX'))
+ self.user_dn = DN(self.updater._template_str('uid=tuser, cn=test, cn=accounts, $SUFFIX'))
+
+ def tearDown(self):
+ if self.ld:
+ self.ld.unbind()
+
+ def test_0_reset(self):
+ """
+ Reset the updater test data to a known initial state (test_0_reset)
+ """
+ try:
+ modified = self.updater.update([self.testdir + "0_reset.update"])
+ except errors.NotFound:
+ # Just means the entry doesn't exist yet
+ modified = True
+
+ self.assertTrue(modified)
+
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.container_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+
+ def test_1_add(self):
+ """
+ Test the updater with an add directive (test_1_add)
+ """
+ modified = self.updater.update([self.testdir + "1_add.update"])
+
+ self.assertTrue(modified)
+
+ entries = self.ld.get_entries(
+ self.container_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+
+ objectclasses = entry.get('objectclass')
+ for item in ('top', 'nsContainer'):
+ self.assertTrue(item in objectclasses)
+
+ self.assertEqual(entry.single_value('cn'), 'test')
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+
+ objectclasses = entry.get('objectclass')
+ for item in ('top', 'person', 'posixaccount', 'krbprincipalaux', 'inetuser'):
+ self.assertTrue(item in objectclasses)
+
+ self.assertEqual(entry.single_value('loginshell'), '/bin/bash')
+ self.assertEqual(entry.single_value('sn'), 'User')
+ self.assertEqual(entry.single_value('uid'), 'tuser')
+ self.assertEqual(entry.single_value('cn'), 'Test User')
+
+
+ def test_2_update(self):
+ """
+ Test the updater when adding an attribute to an existing entry (test_2_update)
+ """
+ modified = self.updater.update([self.testdir + "2_update.update"])
+ self.assertTrue(modified)
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+ self.assertEqual(entry.single_value('gecos'), 'Test User')
+
+ def test_3_update(self):
+ """
+ Test the updater forcing an attribute to a given value (test_3_update)
+ """
+ modified = self.updater.update([self.testdir + "3_update.update"])
+ self.assertTrue(modified)
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+ self.assertEqual(entry.single_value('gecos'), 'Test User New')
+
+ def test_4_update(self):
+ """
+ Test the updater adding a new value to a single-valued attribute (test_4_update)
+ """
+ modified = self.updater.update([self.testdir + "4_update.update"])
+ self.assertTrue(modified)
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+ self.assertEqual(entry.single_value('gecos'), 'Test User New2')
+
+ def test_5_update(self):
+ """
+ Test the updater adding a new value to a multi-valued attribute (test_5_update)
+ """
+ modified = self.updater.update([self.testdir + "5_update.update"])
+ self.assertTrue(modified)
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+ self.assertEqual(sorted(entry.get('cn')), sorted(['Test User', 'Test User New']))
+
+ def test_6_update(self):
+ """
+ Test the updater removing a value from a multi-valued attribute (test_6_update)
+ """
+ modified = self.updater.update([self.testdir + "6_update.update"])
+ self.assertTrue(modified)
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+ self.assertEqual(sorted(entry.get('cn')), sorted(['Test User']))
+
+ def test_6_update_1(self):
+ """
+ Test the updater removing a non-existent value from a multi-valued attribute (test_6_update_1)
+ """
+ modified = self.updater.update([self.testdir + "6_update.update"])
+ self.assertFalse(modified)
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+ self.assertEqual(sorted(entry.get('cn')), sorted(['Test User']))
+
+ def test_7_cleanup(self):
+ """
+ Reset the test data to a known initial state (test_7_cleanup)
+ """
+ try:
+ modified = self.updater.update([self.testdir + "0_reset.update"])
+ except errors.NotFound:
+ # Just means the entry doesn't exist yet
+ modified = True
+
+ self.assertTrue(modified)
+
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.container_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+
+ def test_8_badsyntax(self):
+ """
+ Test the updater with an unknown keyword (test_8_badsyntax)
+ """
+ with self.assertRaises(BadSyntax):
+ modified = self.updater.update([self.testdir + "8_badsyntax.update"])
+
+ def test_9_badsyntax(self):
+ """
+ Test the updater with an incomplete line (test_9_badsyntax)
+ """
+ with self.assertRaises(BadSyntax):
+ modified = self.updater.update([self.testdir + "9_badsyntax.update"])
+
+ def test_from_dict(self):
+ """
+ Test updating from a dict.
+
+ This replicates what was done in test 1.
+ """
+
+ # First make sure we're clean
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.container_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+
+
+ update = {
+ self.container_dn:
+ {'dn': self.container_dn,
+ 'updates': ['add:objectClass: top',
+ 'add:objectClass: nsContainer',
+ 'add:cn: test'
+ ],
+ },
+ self.user_dn:
+ {'dn': self.user_dn,
+ 'updates': ['add:objectclass: top',
+ 'add:objectclass: person',
+ 'add:objectclass: posixaccount',
+ 'add:objectclass: krbprincipalaux',
+ 'add:objectclass: inetuser',
+ 'add:homedirectory: /home/tuser',
+ 'add:loginshell: /bin/bash',
+ 'add:sn: User',
+ 'add:uid: tuser',
+ 'add:uidnumber: 999',
+ 'add:gidnumber: 999',
+ 'add:cn: Test User',
+ ],
+ },
+ }
+
+ modified = self.updater.update_from_dict(update)
+ self.assertTrue(modified)
+
+ entries = self.ld.get_entries(
+ self.container_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+
+ objectclasses = entry.get('objectclass')
+ for item in ('top', 'nsContainer'):
+ self.assertTrue(item in objectclasses)
+
+ self.assertEqual(entry.single_value('cn'), 'test')
+
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+ self.assertEqual(len(entries), 1)
+ entry = entries[0]
+
+ objectclasses = entry.get('objectclass')
+ for item in ('top', 'person', 'posixaccount', 'krbprincipalaux', 'inetuser'):
+ self.assertTrue(item in objectclasses)
+
+ self.assertEqual(entry.single_value('loginshell'), '/bin/bash')
+ self.assertEqual(entry.single_value('sn'), 'User')
+ self.assertEqual(entry.single_value('uid'), 'tuser')
+ self.assertEqual(entry.single_value('cn'), 'Test User')
+
+ # Now delete
+
+ update = {
+ self.container_dn:
+ {'dn': self.container_dn,
+ 'deleteentry': None,
+ },
+ self.user_dn:
+ {'dn': self.user_dn,
+ 'deleteentry': 'deleteentry: reset: nada',
+ },
+ }
+
+ modified = self.updater.update_from_dict(update)
+ self.assertTrue(modified)
+
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.container_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
+
+ with self.assertRaises(errors.NotFound):
+ entries = self.ld.get_entries(
+ self.user_dn, self.ld.SCOPE_BASE, 'objectclass=*', ['*'])
diff --git a/ipatests/test_ipalib/__init__.py b/ipatests/test_ipalib/__init__.py
new file mode 100644
index 000000000..4e4c605cd
--- /dev/null
+++ b/ipatests/test_ipalib/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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 `ipalib` package.
+"""
diff --git a/ipatests/test_ipalib/test_backend.py b/ipatests/test_ipalib/test_backend.py
new file mode 100644
index 000000000..3ebed4bba
--- /dev/null
+++ b/ipatests/test_ipalib/test_backend.py
@@ -0,0 +1,272 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.backend` module.
+"""
+
+import threading
+from ipatests.util import ClassChecker, raises, create_test_api
+from ipatests.data import unicode_str
+from ipalib.request import context, Connection
+from ipalib.frontend import Command
+from ipalib import backend, plugable, errors, base
+from ipapython.version import API_VERSION
+
+
+
+class test_Backend(ClassChecker):
+ """
+ Test the `ipalib.backend.Backend` class.
+ """
+
+ _cls = backend.Backend
+
+ def test_class(self):
+ assert self.cls.__bases__ == (plugable.Plugin,)
+
+
+class Disconnect(object):
+ called = False
+
+ def __init__(self, id=None):
+ self.id = id
+
+ def __call__(self):
+ assert self.called is False
+ self.called = True
+ if self.id is not None:
+ delattr(context, self.id)
+
+
+class test_Connectible(ClassChecker):
+ """
+ Test the `ipalib.backend.Connectible` class.
+ """
+
+ _cls = backend.Connectible
+
+ def test_connect(self):
+ """
+ Test the `ipalib.backend.Connectible.connect` method.
+ """
+ # Test that connection is created:
+ class example(self.cls):
+ def create_connection(self, *args, **kw):
+ object.__setattr__(self, 'args', args)
+ object.__setattr__(self, 'kw', kw)
+ return 'The connection.'
+ o = example()
+ args = ('Arg1', 'Arg2', 'Arg3')
+ kw = dict(key1='Val1', key2='Val2', key3='Val3')
+ assert not hasattr(context, 'example')
+ assert o.connect(*args, **kw) is None
+ conn = context.example
+ assert type(conn) is Connection
+ assert o.args == args
+ assert o.kw == kw
+ assert conn.conn == 'The connection.'
+ assert conn.disconnect == o.disconnect
+
+ # Test that StandardError is raised if already connected:
+ m = "connect: 'context.%s' already exists in thread %r"
+ e = raises(StandardError, o.connect, *args, **kw)
+ assert str(e) == m % ('example', threading.currentThread().getName())
+
+ # Double check that it works after deleting context.example:
+ del context.example
+ assert o.connect(*args, **kw) is None
+
+ def test_create_connection(self):
+ """
+ Test the `ipalib.backend.Connectible.create_connection` method.
+ """
+ class example(self.cls):
+ pass
+ for klass in (self.cls, example):
+ o = klass()
+ e = raises(NotImplementedError, o.create_connection)
+ assert str(e) == '%s.create_connection()' % klass.__name__
+
+ def test_disconnect(self):
+ """
+ Test the `ipalib.backend.Connectible.disconnect` method.
+ """
+ class example(self.cls):
+ destroy_connection = Disconnect()
+ o = example()
+
+ m = "disconnect: 'context.%s' does not exist in thread %r"
+ e = raises(StandardError, o.disconnect)
+ assert str(e) == m % ('example', threading.currentThread().getName())
+
+ context.example = 'The connection.'
+ assert o.disconnect() is None
+ assert example.destroy_connection.called is True
+
+ def test_destroy_connection(self):
+ """
+ Test the `ipalib.backend.Connectible.destroy_connection` method.
+ """
+ class example(self.cls):
+ pass
+ for klass in (self.cls, example):
+ o = klass()
+ e = raises(NotImplementedError, o.destroy_connection)
+ assert str(e) == '%s.destroy_connection()' % klass.__name__
+
+ def test_isconnected(self):
+ """
+ Test the `ipalib.backend.Connectible.isconnected` method.
+ """
+ class example(self.cls):
+ pass
+ for klass in (self.cls, example):
+ o = klass()
+ assert o.isconnected() is False
+ conn = 'whatever'
+ setattr(context, klass.__name__, conn)
+ assert o.isconnected() is True
+ delattr(context, klass.__name__)
+
+ def test_conn(self):
+ """
+ Test the `ipalib.backend.Connectible.conn` property.
+ """
+ msg = 'no context.%s in thread %r'
+ class example(self.cls):
+ pass
+ for klass in (self.cls, example):
+ o = klass()
+ e = raises(AttributeError, getattr, o, 'conn')
+ assert str(e) == msg % (
+ klass.__name__, threading.currentThread().getName()
+ )
+ conn = Connection('The connection.', Disconnect())
+ setattr(context, klass.__name__, conn)
+ assert o.conn is conn.conn
+ delattr(context, klass.__name__)
+
+
+class test_Executioner(ClassChecker):
+ """
+ Test the `ipalib.backend.Executioner` class.
+ """
+ _cls = backend.Executioner
+
+ def test_execute(self):
+ """
+ Test the `ipalib.backend.Executioner.execute` method.
+ """
+ (api, home) = create_test_api(in_server=True)
+
+ class echo(Command):
+ takes_args = ('arg1', 'arg2+')
+ takes_options = ('option1?', 'option2?')
+ def execute(self, *args, **options):
+ assert type(args[1]) is tuple
+ return dict(result=args + (options,))
+ api.register(echo)
+
+ class good(Command):
+ def execute(self, **options):
+ raise errors.ValidationError(
+ name='nurse',
+ error=u'Not naughty!',
+ )
+ api.register(good)
+
+ class bad(Command):
+ def execute(self, **options):
+ raise ValueError('This is private.')
+ api.register(bad)
+
+ class with_name(Command):
+ """
+ Test that a kwarg named 'name' can be used.
+ """
+ takes_options = 'name'
+ def execute(self, **options):
+ return dict(result=options['name'].upper())
+ api.register(with_name)
+
+ api.finalize()
+ o = self.cls()
+ o.set_api(api)
+ o.finalize()
+
+ # Test that CommandError is raised:
+ conn = Connection('The connection.', Disconnect('someconn'))
+ context.someconn = conn
+ print str(context.__dict__.keys())
+ e = raises(errors.CommandError, o.execute, 'nope')
+ assert e.name == 'nope'
+ assert conn.disconnect.called is True # Make sure destroy_context() was called
+ print str(context.__dict__.keys())
+ assert context.__dict__.keys() == []
+
+ # Test with echo command:
+ arg1 = unicode_str
+ arg2 = (u'Hello', unicode_str, u'world!')
+ args = (arg1,) + arg2
+ options = dict(option1=u'How are you?', option2=unicode_str,
+ version=API_VERSION)
+
+ conn = Connection('The connection.', Disconnect('someconn'))
+ context.someconn = conn
+ print o.execute('echo', arg1, arg2, **options)
+ print dict(
+ result=(arg1, arg2, options)
+ )
+ assert o.execute('echo', arg1, arg2, **options) == dict(
+ result=(arg1, arg2, options)
+ )
+ assert conn.disconnect.called is True # Make sure destroy_context() was called
+ assert context.__dict__.keys() == []
+
+ conn = Connection('The connection.', Disconnect('someconn'))
+ context.someconn = conn
+ assert o.execute('echo', *args, **options) == dict(
+ result=(arg1, arg2, options)
+ )
+ assert conn.disconnect.called is True # Make sure destroy_context() was called
+ assert context.__dict__.keys() == []
+
+ # Test with good command:
+ conn = Connection('The connection.', Disconnect('someconn'))
+ context.someconn = conn
+ e = raises(errors.ValidationError, o.execute, 'good')
+ assert e.name == 'nurse'
+ assert e.error == u'Not naughty!'
+ assert conn.disconnect.called is True # Make sure destroy_context() was called
+ assert context.__dict__.keys() == []
+
+ # Test with bad command:
+ conn = Connection('The connection.', Disconnect('someconn'))
+ context.someconn = conn
+ e = raises(errors.InternalError, o.execute, 'bad')
+ assert conn.disconnect.called is True # Make sure destroy_context() was called
+ assert context.__dict__.keys() == []
+
+ # Test with option 'name':
+ conn = Connection('The connection.', Disconnect('someconn'))
+ context.someconn = conn
+ expected = dict(result=u'TEST')
+ assert expected == o.execute('with_name', name=u'test',
+ version=API_VERSION)
diff --git a/ipatests/test_ipalib/test_base.py b/ipatests/test_ipalib/test_base.py
new file mode 100644
index 000000000..ef6c180c7
--- /dev/null
+++ b/ipatests/test_ipalib/test_base.py
@@ -0,0 +1,352 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.base` module.
+"""
+
+from ipatests.util import ClassChecker, raises
+from ipalib.constants import NAME_REGEX, NAME_ERROR
+from ipalib.constants import TYPE_ERROR, SET_ERROR, DEL_ERROR, OVERRIDE_ERROR
+from ipalib import base
+
+
+class test_ReadOnly(ClassChecker):
+ """
+ Test the `ipalib.base.ReadOnly` class
+ """
+ _cls = base.ReadOnly
+
+ def test_lock(self):
+ """
+ Test the `ipalib.base.ReadOnly.__lock__` method.
+ """
+ o = self.cls()
+ assert o._ReadOnly__locked is False
+ o.__lock__()
+ assert o._ReadOnly__locked is True
+ e = raises(AssertionError, o.__lock__) # Can only be locked once
+ assert str(e) == '__lock__() can only be called once'
+ assert o._ReadOnly__locked is True # This should still be True
+
+ def test_islocked(self):
+ """
+ Test the `ipalib.base.ReadOnly.__islocked__` method.
+ """
+ o = self.cls()
+ assert o.__islocked__() is False
+ o.__lock__()
+ assert o.__islocked__() is True
+
+ def test_setattr(self):
+ """
+ Test the `ipalib.base.ReadOnly.__setattr__` method.
+ """
+ o = self.cls()
+ o.attr1 = 'Hello, world!'
+ assert o.attr1 == 'Hello, world!'
+ o.__lock__()
+ for name in ('attr1', 'attr2'):
+ e = raises(AttributeError, setattr, o, name, 'whatever')
+ assert str(e) == SET_ERROR % ('ReadOnly', name, 'whatever')
+ assert o.attr1 == 'Hello, world!'
+
+ def test_delattr(self):
+ """
+ Test the `ipalib.base.ReadOnly.__delattr__` method.
+ """
+ o = self.cls()
+ o.attr1 = 'Hello, world!'
+ o.attr2 = 'How are you?'
+ assert o.attr1 == 'Hello, world!'
+ assert o.attr2 == 'How are you?'
+ del o.attr1
+ assert not hasattr(o, 'attr1')
+ o.__lock__()
+ e = raises(AttributeError, delattr, o, 'attr2')
+ assert str(e) == DEL_ERROR % ('ReadOnly', 'attr2')
+ assert o.attr2 == 'How are you?'
+
+
+def test_lock():
+ """
+ Test the `ipalib.base.lock` function
+ """
+ f = base.lock
+
+ # Test with ReadOnly instance:
+ o = base.ReadOnly()
+ assert o.__islocked__() is False
+ assert f(o) is o
+ assert o.__islocked__() is True
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'already locked: %r' % o
+
+ # Test with another class implemented locking protocol:
+ class Lockable(object):
+ __locked = False
+ def __lock__(self):
+ self.__locked = True
+ def __islocked__(self):
+ return self.__locked
+ o = Lockable()
+ assert o.__islocked__() is False
+ assert f(o) is o
+ assert o.__islocked__() is True
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'already locked: %r' % o
+
+ # Test with a class incorrectly implementing the locking protocol:
+ class Broken(object):
+ def __lock__(self):
+ pass
+ def __islocked__(self):
+ return False
+ o = Broken()
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'failed to lock: %r' % o
+
+
+def test_islocked():
+ """
+ Test the `ipalib.base.islocked` function.
+ """
+ f = base.islocked
+
+ # Test with ReadOnly instance:
+ o = base.ReadOnly()
+ assert f(o) is False
+ o.__lock__()
+ assert f(o) is True
+
+ # Test with another class implemented locking protocol:
+ class Lockable(object):
+ __locked = False
+ def __lock__(self):
+ self.__locked = True
+ def __islocked__(self):
+ return self.__locked
+ o = Lockable()
+ assert f(o) is False
+ o.__lock__()
+ assert f(o) is True
+
+ # Test with a class incorrectly implementing the locking protocol:
+ class Broken(object):
+ __lock__ = False
+ def __islocked__(self):
+ return False
+ o = Broken()
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'no __lock__() method: %r' % o
+
+
+def test_check_name():
+ """
+ Test the `ipalib.base.check_name` function.
+ """
+ f = base.check_name
+ okay = [
+ 'user_add',
+ 'stuff2junk',
+ 'sixty9',
+ ]
+ nope = [
+ '_user_add',
+ '__user_add',
+ 'user_add_',
+ 'user_add__',
+ '_user_add_',
+ '__user_add__',
+ '60nine',
+ ]
+ for name in okay:
+ assert name is f(name)
+ e = raises(TypeError, f, unicode(name))
+ assert str(e) == TYPE_ERROR % ('name', str, unicode(name), unicode)
+ for name in nope:
+ e = raises(ValueError, f, name)
+ assert str(e) == NAME_ERROR % (NAME_REGEX, name)
+ for name in okay:
+ e = raises(ValueError, f, name.upper())
+ assert str(e) == NAME_ERROR % (NAME_REGEX, name.upper())
+
+
+def membername(i):
+ return 'member%03d' % i
+
+
+class DummyMember(object):
+ def __init__(self, i):
+ self.i = i
+ self.name = membername(i)
+
+
+def gen_members(*indexes):
+ return tuple(DummyMember(i) for i in indexes)
+
+
+class test_NameSpace(ClassChecker):
+ """
+ Test the `ipalib.base.NameSpace` class.
+ """
+ _cls = base.NameSpace
+
+ def new(self, count, sort=True):
+ members = tuple(DummyMember(i) for i in xrange(count, 0, -1))
+ assert len(members) == count
+ o = self.cls(members, sort=sort)
+ return (o, members)
+
+ def test_init(self):
+ """
+ Test the `ipalib.base.NameSpace.__init__` method.
+ """
+ o = self.cls([])
+ assert len(o) == 0
+ assert list(o) == []
+ assert list(o()) == []
+
+ # Test members as attribute and item:
+ for cnt in (3, 42):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ assert len(members) == cnt
+ for m in members:
+ assert getattr(o, m.name) is m
+ assert o[m.name] is m
+
+ # Test that TypeError is raised if sort is not a bool:
+ e = raises(TypeError, self.cls, [], sort=None)
+ assert str(e) == TYPE_ERROR % ('sort', bool, None, type(None))
+
+ # Test that AttributeError is raised with duplicate member name:
+ members = gen_members(0, 1, 2, 1, 3)
+ e = raises(AttributeError, self.cls, members)
+ assert str(e) == OVERRIDE_ERROR % (
+ 'NameSpace', membername(1), members[1], members[3]
+ )
+
+ def test_len(self):
+ """
+ Test the `ipalib.base.NameSpace.__len__` method.
+ """
+ for count in (5, 18, 127):
+ (o, members) = self.new(count)
+ assert len(o) == count
+ (o, members) = self.new(count, sort=False)
+ assert len(o) == count
+
+ def test_iter(self):
+ """
+ Test the `ipalib.base.NameSpace.__iter__` method.
+ """
+ (o, members) = self.new(25)
+ assert list(o) == sorted(m.name for m in members)
+ (o, members) = self.new(25, sort=False)
+ assert list(o) == list(m.name for m in members)
+
+ def test_call(self):
+ """
+ Test the `ipalib.base.NameSpace.__call__` method.
+ """
+ (o, members) = self.new(25)
+ assert list(o()) == sorted(members, key=lambda m: m.name)
+ (o, members) = self.new(25, sort=False)
+ assert tuple(o()) == members
+
+ def test_contains(self):
+ """
+ Test the `ipalib.base.NameSpace.__contains__` method.
+ """
+ yes = (99, 3, 777)
+ no = (9, 333, 77)
+ for sort in (True, False):
+ members = gen_members(*yes)
+ o = self.cls(members, sort=sort)
+ for i in yes:
+ assert membername(i) in o
+ assert membername(i).upper() not in o
+ for i in no:
+ assert membername(i) not in o
+
+ def test_getitem(self):
+ """
+ Test the `ipalib.base.NameSpace.__getitem__` method.
+ """
+ cnt = 17
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ assert len(members) == cnt
+ if sort is True:
+ members = tuple(sorted(members, key=lambda m: m.name))
+
+ # Test str keys:
+ for m in members:
+ assert o[m.name] is m
+ e = raises(KeyError, o.__getitem__, 'nope')
+
+ # Test int indexes:
+ for i in xrange(cnt):
+ assert o[i] is members[i]
+ e = raises(IndexError, o.__getitem__, cnt)
+
+ # Test negative int indexes:
+ for i in xrange(1, cnt + 1):
+ assert o[-i] is members[-i]
+ e = raises(IndexError, o.__getitem__, -(cnt + 1))
+
+ # Test slicing:
+ assert o[3:] == members[3:]
+ assert o[:10] == members[:10]
+ assert o[3:10] == members[3:10]
+ assert o[-9:] == members[-9:]
+ assert o[:-4] == members[:-4]
+ assert o[-9:-4] == members[-9:-4]
+
+ # Test that TypeError is raised with wrong type
+ e = raises(TypeError, o.__getitem__, 3.0)
+ assert str(e) == TYPE_ERROR % ('key', (str, int, slice), 3.0, float)
+
+ def test_repr(self):
+ """
+ Test the `ipalib.base.NameSpace.__repr__` method.
+ """
+ for cnt in (0, 1, 2):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ if cnt == 1:
+ assert repr(o) == \
+ 'NameSpace(<%d member>, sort=%r)' % (cnt, sort)
+ else:
+ assert repr(o) == \
+ 'NameSpace(<%d members>, sort=%r)' % (cnt, sort)
+
+ def test_todict(self):
+ """
+ Test the `ipalib.base.NameSpace.__todict__` method.
+ """
+ for cnt in (3, 101):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ d = o.__todict__()
+ assert d == dict((m.name, m) for m in members)
+
+ # Test that a copy is returned:
+ assert o.__todict__() is not d
diff --git a/ipatests/test_ipalib/test_capabilities.py b/ipatests/test_ipalib/test_capabilities.py
new file mode 100644
index 000000000..21e53c2dc
--- /dev/null
+++ b/ipatests/test_ipalib/test_capabilities.py
@@ -0,0 +1,33 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+
+"""
+Test the `ipalib.errors` module.
+"""
+
+from ipalib.capabilities import capabilities, client_has_capability
+
+
+def test_client_has_capability():
+ assert capabilities['messages'] == u'2.52'
+ assert client_has_capability(u'2.52', 'messages')
+ assert client_has_capability(u'2.60', 'messages')
+ assert client_has_capability(u'3.0', 'messages')
+ assert not client_has_capability(u'2.11', 'messages')
+ assert not client_has_capability(u'0.1', 'messages')
diff --git a/ipatests/test_ipalib/test_cli.py b/ipatests/test_ipalib/test_cli.py
new file mode 100644
index 000000000..07935c5ba
--- /dev/null
+++ b/ipatests/test_ipalib/test_cli.py
@@ -0,0 +1,116 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.cli` module.
+"""
+
+from ipatests.util import raises, get_api, ClassChecker
+from ipalib import cli, plugable, frontend, backend
+
+
+class test_textui(ClassChecker):
+ _cls = cli.textui
+
+ def test_max_col_width(self):
+ """
+ Test the `ipalib.cli.textui.max_col_width` method.
+ """
+ o = self.cls()
+ e = raises(TypeError, o.max_col_width, 'hello')
+ assert str(e) == 'rows: need %r or %r; got %r' % (list, tuple, 'hello')
+ rows = [
+ 'hello',
+ 'naughty',
+ 'nurse',
+ ]
+ assert o.max_col_width(rows) == len('naughty')
+ rows = (
+ ( 'a', 'bbb', 'ccccc'),
+ ('aa', 'bbbb', 'cccccc'),
+ )
+ assert o.max_col_width(rows, col=0) == 2
+ assert o.max_col_width(rows, col=1) == 4
+ assert o.max_col_width(rows, col=2) == 6
+
+
+def test_to_cli():
+ """
+ Test the `ipalib.cli.to_cli` function.
+ """
+ f = cli.to_cli
+ assert f('initialize') == 'initialize'
+ assert f('user_add') == 'user-add'
+
+
+def test_from_cli():
+ """
+ Test the `ipalib.cli.from_cli` function.
+ """
+ f = cli.from_cli
+ assert f('initialize') == 'initialize'
+ assert f('user-add') == 'user_add'
+
+
+def get_cmd_name(i):
+ return 'cmd_%d' % i
+
+
+class DummyCommand(object):
+ def __init__(self, name):
+ self.__name = name
+
+ def __get_name(self):
+ return self.__name
+ name = property(__get_name)
+
+
+class DummyAPI(object):
+ def __init__(self, cnt):
+ self.__cmd = plugable.NameSpace(self.__cmd_iter(cnt))
+
+ def __get_cmd(self):
+ return self.__cmd
+ Command = property(__get_cmd)
+
+ def __cmd_iter(self, cnt):
+ for i in xrange(cnt):
+ yield DummyCommand(get_cmd_name(i))
+
+ def finalize(self):
+ pass
+
+ def register(self, *args, **kw):
+ pass
+
+
+config_cli = """
+[global]
+
+from_cli_conf = set in cli.conf
+"""
+
+config_default = """
+[global]
+
+from_default_conf = set in default.conf
+
+# Make sure cli.conf is loaded first:
+from_cli_conf = overridden in default.conf
+"""
diff --git a/ipatests/test_ipalib/test_config.py b/ipatests/test_ipalib/test_config.py
new file mode 100644
index 000000000..f896b8936
--- /dev/null
+++ b/ipatests/test_ipalib/test_config.py
@@ -0,0 +1,609 @@
+# Authors:
+# Martin Nagy <mnagy@redhat.com>
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.config` module.
+"""
+
+import os
+from os import path
+import sys
+import socket
+from ipatests.util import raises, setitem, delitem, ClassChecker
+from ipatests.util import getitem, setitem, delitem
+from ipatests.util import TempDir, TempHome
+from ipalib.constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR
+from ipalib.constants import NAME_REGEX, NAME_ERROR
+from ipalib import config, constants, base
+
+
+# Valid environment variables in (key, raw, value) tuples:
+# key: the name of the environment variable
+# raw: the value being set (possibly a string repr)
+# value: the expected value after the lightweight conversion
+good_vars = (
+ ('a_string', u'Hello world!', u'Hello world!'),
+ ('trailing_whitespace', u' value ', u'value'),
+ ('an_int', 42, 42),
+ ('int_repr', ' 42 ', 42),
+ ('a_float', 3.14, 3.14),
+ ('float_repr', ' 3.14 ', 3.14),
+ ('true', True, True),
+ ('true_repr', ' True ', True),
+ ('false', False, False),
+ ('false_repr', ' False ', False),
+ ('none', None, None),
+ ('none_repr', ' None ', None),
+ ('empty', '', None),
+
+ # These verify that the implied conversion is case-sensitive:
+ ('not_true', u' true ', u'true'),
+ ('not_false', u' false ', u'false'),
+ ('not_none', u' none ', u'none'),
+)
+
+
+bad_names = (
+ ('CamelCase', u'value'),
+ ('_leading_underscore', u'value'),
+ ('trailing_underscore_', u'value'),
+)
+
+
+# Random base64-encoded data to simulate a misbehaving config file.
+config_bad = """
+/9j/4AAQSkZJRgABAQEAlgCWAAD//gAIT2xpdmVy/9sAQwAQCwwODAoQDg0OEhEQExgoGhgWFhgx
+IyUdKDozPTw5Mzg3QEhcTkBEV0U3OFBtUVdfYmdoZz5NcXlwZHhcZWdj/8AACwgAlgB0AQERAP/E
+ABsAAAEFAQEAAAAAAAAAAAAAAAQAAQIDBQYH/8QAMhAAAgICAAUDBAIABAcAAAAAAQIAAwQRBRIh
+MUEGE1EiMmFxFIEVI0LBFjNSYnKRof/aAAgBAQAAPwDCtzmNRr1o/MEP1D6f7kdkRakgBsAtoQhk
+xls/y3Z113I11mhiUc1ewCf1Oq4anJgINdhLhQoextfedmYrenfcvdzaFQnYAE08XhONTWEK8+js
+Fpo1oqAKoAA8CWjoJJTHM8kJ5jsiOiszAKD1+IV/hmW76rosbfnlh1Pp3Mah2srCnXQE9YXiel/c
+p5r7uVj2CwxPTuFjjmdLbteNwmrLwsYe3TjsD8cmjKV43ycy+3o76D4llFuXmuCoZEPczXVOSsLv
+f5lgGpNZLxJL2jnvMar0/wAOp6jHDH/uO4RViY9f/KpRdfC6k3R9fRyj+pRZVkWKqF10e+hCKaFq
+XlH/ALlmhK7Met/uUGZ5ow8XL57lU8/Yt4lx4jUOJphLobTe/wDaHeZLxHXtJEya9o5lFzCqpmPY
+CUYoPtDfc9TLj0G5jZvHaMFirAs++oEHq9U4rbNiMp8a6wO/1Zbzn2alC+Nx8P1JfdeBboA+AILx
+rin8pfbA1ynvKuFUXZOXXkLbzOp2R56andL2G45MmO0RPWWLEe8GzaffoKb/ADI44Pt9ZXxAuuFa
+axtgp0BOSPCcviNX8n3Aw8KTNHB4FiY9StkobLWHVSeghq8M4bkAhKKyV6Hl8RV8MwMZG1Uuz3Jn
+IcUQJlMFGlJ6D4hfpymy7iChHKqvVtefxO7Ai1txLBIn7pcojN3jGVhQO0ZgCNfM5ZHycTLycSkr
+yhtqD4Bmrfw5cuqsm6xHXyp1seRLcHCp4dQy1bOzslj1MzeJ5dVFnuMVdgOiHxOWzrmyMg2Nrbde
+k3vR2OTddcd6A5R8GdZqOo67k4wXrLAQPMRKnzImMZEzm+P1nFz6cxQeVujagWR6jsYiqivlH/Ux
+1M+7jWY30i7QHx1gF11tjGyxiSfmVc+503pPidVROHYNNY21b/adVZZySo3uOo1qIZQYd9RCzfYm
+TUk/qW71LjGkTA+IYiZmM1T9N9j8Gee5+McXJem0/Wp8GUK6KOi7b5MgzFjsxpJHZGDKSCOxE3cD
+OvsxbbLc9lsT7Vc73KX4ln3q1ZyVrPx2J/uAjLyan37z7B+Zp4vqPJqKi0K4EvzvUt1qBMdfb+T5
+gycfzkXXuc35InfE6nO8Y9SjFc1Yqh2Hdj2mH/xFxR26XgD/AMRJf45mWMqW5bBD3KqAZlZtb++7
+kEqTsHe//sG1CcTBvy7OWpD+Sewhz8CyKCTYAQPiGV0LVWPdxqQNADQ6zL4nWq2gopU6+ofmA8x3
+1MlvfeIGbnBeCHitRt94IFbRGus2U9H08v13sT+BNHjeX/D4bY4OmP0rPPbHLMWJ2Yy2EDQjVsos
+BdeYDx8wo5L5KpSdLWPAE1+G8NrFtBKgOAXPTf6mzViql5ZBoE87eJZkKbOQ8m+Yjf5EBzcO621y
+GCqD0H41Obzq7U6vzM577HTXgzPPeOIvM1eB59nD8xXVj7bHTr8iej1MtlauvUMNgzi/V2ctliYy
+HYTq37nMExpZXRZYpZVJUdzNjg+FXYwZgdhv6nVVUJU/uH7iNf1CARrtF0IB113M7jTNVjFl2xJA
+5ROey88OrVOugOy67TDs+89NRKdSYILdRC8ZQVJ+PHyJs4fqe3EoFPLzBexPxOdusa2xndiWY7JM
+qMUNrzOTAfHC9XO9/E3vT9blVJB0o2Zu3MAoYrsL13Ii0Muw3XvJG9KkDOeqjf6gWcw5A33XN9nX
+tOeyMRFWy3Jch+bX7mXmCsW/5RBXUoHaOIRi2asAJ0IRbjqzll3o/EAaRiltDojgv2E1aePmhEWq
+rsNHZ7wir1K/8Y1vUCSCAd+IXiZ9b1gLYvN07trXTUD4rxN2TkUgEts8p2NDtD0t5MVGchr2Xe99
+hMPNvD1LX5J2TuZhGyYwBijjfiHU5bJXrnYfqBRtRtSbIBWG3+xI6HiLUWz8xA9RuaVNrMAPfB5x
+r6v9MLr4S1il7LaxyjY69Jl5eG+Kyhiv1jYIMGYMO8etGscKoJJ8Cbp4bVg4ivaq22t3G/tmRYo5
+zyjQ+JRFFET01GB0Yid9YiYh1l9KgEHqT8Tco/hewA/NzgdQdwTNGNTY3uU2crL9HN00ZlovNzfV
+oCanBrBRk1rpCHPUkQjjYoW4GtwAw30MDpuxvbAvpJceR5mXFFEY0W4o4mpg0XNXutQxPUHxLb8q
+7mRDyszLr6esz8u++9wL2LcvQb8RXCkhBV3A6mR5rEVSrdFPT8SBLMdsdmWe6P8AUAx+TB4oooxi
+i1Jmt0+5dfuOLbANB2H6MjzNzc2zv5ji1g2+5/MYnbb+Yh+T0kubUY940UUbUWtRpJN8w1CfebkK
+WfUu+/mDOAGOjsRo0UkIo+pPl6Rckl7ehuR1INGAj9u0kW2nXvK45YlQp1odukaICSAjgSQWf//Z
+"""
+
+
+# A config file that tries to override some standard vars:
+config_override = """
+[global]
+
+key0 = var0
+home = /home/sweet/home
+key1 = var1
+site_packages = planet
+key2 = var2
+key3 = var3
+"""
+
+
+# A config file that tests the automatic type conversion
+config_good = """
+[global]
+
+string = Hello world!
+null = None
+yes = True
+no = False
+number = 42
+floating = 3.14
+"""
+
+
+# A default config file to make sure it does not overwrite the explicit one
+config_default = """
+[global]
+
+yes = Hello
+not_in_other = foo_bar
+"""
+
+
+class test_Env(ClassChecker):
+ """
+ Test the `ipalib.config.Env` class.
+ """
+
+ _cls = config.Env
+
+ def test_init(self):
+ """
+ Test the `ipalib.config.Env.__init__` method.
+ """
+ o = self.cls()
+ assert list(o) == []
+ assert len(o) == 0
+ assert o.__islocked__() is False
+
+ def test_lock(self):
+ """
+ Test the `ipalib.config.Env.__lock__` method.
+ """
+ o = self.cls()
+ assert o.__islocked__() is False
+ o.__lock__()
+ assert o.__islocked__() is True
+ e = raises(StandardError, o.__lock__)
+ assert str(e) == 'Env.__lock__() already called'
+
+ # Also test with base.lock() function:
+ o = self.cls()
+ assert o.__islocked__() is False
+ assert base.lock(o) is o
+ assert o.__islocked__() is True
+ e = raises(AssertionError, base.lock, o)
+ assert str(e) == 'already locked: %r' % o
+
+ def test_islocked(self):
+ """
+ Test the `ipalib.config.Env.__islocked__` method.
+ """
+ o = self.cls()
+ assert o.__islocked__() is False
+ assert base.islocked(o) is False
+ o.__lock__()
+ assert o.__islocked__() is True
+ assert base.islocked(o) is True
+
+ def test_setattr(self):
+ """
+ Test the `ipalib.config.Env.__setattr__` method.
+ """
+ o = self.cls()
+ for (name, raw, value) in good_vars:
+ # Test setting the value:
+ setattr(o, name, raw)
+ result = getattr(o, name)
+ assert type(result) is type(value)
+ assert result == value
+ assert result is o[name]
+
+ # Test that value cannot be overridden once set:
+ e = raises(AttributeError, setattr, o, name, raw)
+ assert str(e) == OVERRIDE_ERROR % ('Env', name, value, raw)
+
+ # Test that values cannot be set once locked:
+ o = self.cls()
+ o.__lock__()
+ for (name, raw, value) in good_vars:
+ e = raises(AttributeError, setattr, o, name, raw)
+ assert str(e) == SET_ERROR % ('Env', name, raw)
+
+ # Test that name is tested with check_name():
+ o = self.cls()
+ for (name, value) in bad_names:
+ e = raises(ValueError, setattr, o, name, value)
+ assert str(e) == NAME_ERROR % (NAME_REGEX, name)
+
+ def test_setitem(self):
+ """
+ Test the `ipalib.config.Env.__setitem__` method.
+ """
+ o = self.cls()
+ for (key, raw, value) in good_vars:
+ # Test setting the value:
+ o[key] = raw
+ result = o[key]
+ assert type(result) is type(value)
+ assert result == value
+ assert result is getattr(o, key)
+
+ # Test that value cannot be overridden once set:
+ e = raises(AttributeError, o.__setitem__, key, raw)
+ assert str(e) == OVERRIDE_ERROR % ('Env', key, value, raw)
+
+ # Test that values cannot be set once locked:
+ o = self.cls()
+ o.__lock__()
+ for (key, raw, value) in good_vars:
+ e = raises(AttributeError, o.__setitem__, key, raw)
+ assert str(e) == SET_ERROR % ('Env', key, raw)
+
+ # Test that name is tested with check_name():
+ o = self.cls()
+ for (key, value) in bad_names:
+ e = raises(ValueError, o.__setitem__, key, value)
+ assert str(e) == NAME_ERROR % (NAME_REGEX, key)
+
+ def test_getitem(self):
+ """
+ Test the `ipalib.config.Env.__getitem__` method.
+ """
+ o = self.cls()
+ value = u'some value'
+ o.key = value
+ assert o.key is value
+ assert o['key'] is value
+ for name in ('one', 'two'):
+ e = raises(KeyError, getitem, o, name)
+ assert str(e) == repr(name)
+
+ def test_delattr(self):
+ """
+ Test the `ipalib.config.Env.__delattr__` method.
+
+ This also tests that ``__delitem__`` is not implemented.
+ """
+ o = self.cls()
+ o.one = 1
+ assert o.one == 1
+ for key in ('one', 'two'):
+ e = raises(AttributeError, delattr, o, key)
+ assert str(e) == DEL_ERROR % ('Env', key)
+ e = raises(AttributeError, delitem, o, key)
+ assert str(e) == '__delitem__'
+
+ def test_contains(self):
+ """
+ Test the `ipalib.config.Env.__contains__` method.
+ """
+ o = self.cls()
+ items = [
+ ('one', 1),
+ ('two', 2),
+ ('three', 3),
+ ('four', 4),
+ ]
+ for (key, value) in items:
+ assert key not in o
+ o[key] = value
+ assert key in o
+
+ def test_len(self):
+ """
+ Test the `ipalib.config.Env.__len__` method.
+ """
+ o = self.cls()
+ assert len(o) == 0
+ for i in xrange(1, 11):
+ key = 'key%d' % i
+ value = u'value %d' % i
+ o[key] = value
+ assert o[key] is value
+ assert len(o) == i
+
+ def test_iter(self):
+ """
+ Test the `ipalib.config.Env.__iter__` method.
+ """
+ o = self.cls()
+ default_keys = tuple(o)
+ keys = ('one', 'two', 'three', 'four', 'five')
+ for key in keys:
+ o[key] = 'the value'
+ assert list(o) == sorted(keys + default_keys)
+
+ def test_merge(self):
+ """
+ Test the `ipalib.config.Env._merge` method.
+ """
+ group1 = (
+ ('key1', u'value 1'),
+ ('key2', u'value 2'),
+ ('key3', u'value 3'),
+ ('key4', u'value 4'),
+ )
+ group2 = (
+ ('key0', u'Value 0'),
+ ('key2', u'Value 2'),
+ ('key4', u'Value 4'),
+ ('key5', u'Value 5'),
+ )
+ o = self.cls()
+ assert o._merge(**dict(group1)) == (4, 4)
+ assert len(o) == 4
+ assert list(o) == list(key for (key, value) in group1)
+ for (key, value) in group1:
+ assert getattr(o, key) is value
+ assert o[key] is value
+ assert o._merge(**dict(group2)) == (2, 4)
+ assert len(o) == 6
+ expected = dict(group2)
+ expected.update(dict(group1))
+ assert list(o) == sorted(expected)
+ assert expected['key2'] == 'value 2' # And not 'Value 2'
+ for (key, value) in expected.iteritems():
+ assert getattr(o, key) is value
+ assert o[key] is value
+ assert o._merge(**expected) == (0, 6)
+ assert len(o) == 6
+ assert list(o) == sorted(expected)
+
+ def test_merge_from_file(self):
+ """
+ Test the `ipalib.config.Env._merge_from_file` method.
+ """
+ tmp = TempDir()
+ assert callable(tmp.join)
+
+ # Test a config file that doesn't exist
+ no_exist = tmp.join('no_exist.conf')
+ assert not path.exists(no_exist)
+ o = self.cls()
+ o._bootstrap()
+ keys = tuple(o)
+ orig = dict((k, o[k]) for k in o)
+ assert o._merge_from_file(no_exist) is None
+ assert tuple(o) == keys
+
+ # Test an empty config file
+ empty = tmp.touch('empty.conf')
+ assert path.isfile(empty)
+ assert o._merge_from_file(empty) == (0, 0)
+ assert tuple(o) == keys
+
+ # Test a mal-formed config file:
+ bad = tmp.join('bad.conf')
+ open(bad, 'w').write(config_bad)
+ assert path.isfile(bad)
+ assert o._merge_from_file(bad) is None
+ assert tuple(o) == keys
+
+ # Test a valid config file that tries to override
+ override = tmp.join('override.conf')
+ open(override, 'w').write(config_override)
+ assert path.isfile(override)
+ assert o._merge_from_file(override) == (4, 6)
+ for (k, v) in orig.items():
+ assert o[k] is v
+ assert list(o) == sorted(keys + ('key0', 'key1', 'key2', 'key3', 'config_loaded'))
+ for i in xrange(4):
+ assert o['key%d' % i] == ('var%d' % i)
+ keys = tuple(o)
+
+ # Test a valid config file with type conversion
+ good = tmp.join('good.conf')
+ open(good, 'w').write(config_good)
+ assert path.isfile(good)
+ assert o._merge_from_file(good) == (6, 6)
+ added = ('string', 'null', 'yes', 'no', 'number', 'floating')
+ assert list(o) == sorted(keys + added)
+ assert o.string == 'Hello world!'
+ assert o.null is None
+ assert o.yes is True
+ assert o.no is False
+ assert o.number == 42
+ assert o.floating == 3.14
+
+ def new(self, in_tree=False):
+ """
+ Set os.environ['HOME'] to a tempdir.
+
+ Returns tuple with new Env instance and the TempHome instance. This
+ helper method is used in testing the bootstrap related methods below.
+ """
+ home = TempHome()
+ o = self.cls()
+ if in_tree:
+ o.in_tree = True
+ return (o, home)
+
+ def bootstrap(self, **overrides):
+ """
+ Helper method used in testing bootstrap related methods below.
+ """
+ (o, home) = self.new()
+ assert o._isdone('_bootstrap') is False
+ o._bootstrap(**overrides)
+ assert o._isdone('_bootstrap') is True
+ e = raises(StandardError, o._bootstrap)
+ assert str(e) == 'Env._bootstrap() already called'
+ return (o, home)
+
+ def test_bootstrap(self):
+ """
+ Test the `ipalib.config.Env._bootstrap` method.
+ """
+ # Test defaults created by _bootstrap():
+ (o, home) = self.new()
+ o._bootstrap()
+ ipalib = path.dirname(path.abspath(config.__file__))
+ assert o.ipalib == ipalib
+ assert o.site_packages == path.dirname(ipalib)
+ assert o.script == path.abspath(sys.argv[0])
+ assert o.bin == path.dirname(path.abspath(sys.argv[0]))
+ assert o.home == home.path
+ assert o.dot_ipa == home.join('.ipa')
+ assert o.in_tree is False
+ assert o.context == 'default'
+ assert o.confdir == '/etc/ipa'
+ assert o.conf == '/etc/ipa/default.conf'
+ assert o.conf_default == o.conf
+
+ # Test overriding values created by _bootstrap()
+ (o, home) = self.bootstrap(in_tree='True', context='server')
+ assert o.in_tree is True
+ assert o.context == 'server'
+ assert o.conf == home.join('.ipa', 'server.conf')
+ (o, home) = self.bootstrap(conf='/my/wacky/whatever.conf')
+ assert o.in_tree is False
+ assert o.context == 'default'
+ assert o.conf == '/my/wacky/whatever.conf'
+ assert o.conf_default == '/etc/ipa/default.conf'
+ (o, home) = self.bootstrap(conf_default='/my/wacky/default.conf')
+ assert o.in_tree is False
+ assert o.context == 'default'
+ assert o.conf == '/etc/ipa/default.conf'
+ assert o.conf_default == '/my/wacky/default.conf'
+
+ # Test various overrides and types conversion
+ kw = dict(
+ yes=True,
+ no=False,
+ num=42,
+ msg='Hello, world!',
+ )
+ override = dict(
+ (k, u' %s ' % v) for (k, v) in kw.items()
+ )
+ (o, home) = self.new()
+ for key in kw:
+ assert key not in o
+ o._bootstrap(**override)
+ for (key, value) in kw.items():
+ assert getattr(o, key) == value
+ assert o[key] == value
+
+ def finalize_core(self, ctx, **defaults):
+ """
+ Helper method used in testing `Env._finalize_core`.
+ """
+ # We must force in_tree=True so we don't load possible config files in
+ # /etc/ipa/, whose contents could break this test:
+ (o, home) = self.new(in_tree=True)
+ if ctx:
+ o.context = ctx
+
+ # Check that calls cascade down the chain:
+ set_here = ('in_server', 'logdir', 'log')
+ assert o._isdone('_bootstrap') is False
+ assert o._isdone('_finalize_core') is False
+ assert o._isdone('_finalize') is False
+ for key in set_here:
+ assert key not in o
+ o._finalize_core(**defaults)
+ assert o._isdone('_bootstrap') is True
+ assert o._isdone('_finalize_core') is True
+ assert o._isdone('_finalize') is False # Should not cascade
+ for key in set_here:
+ assert key in o
+
+ # Check that it can't be called twice:
+ e = raises(StandardError, o._finalize_core)
+ assert str(e) == 'Env._finalize_core() already called'
+
+ return (o, home)
+
+ def test_finalize_core(self):
+ """
+ Test the `ipalib.config.Env._finalize_core` method.
+ """
+ # Test that correct defaults are generated:
+ (o, home) = self.finalize_core(None)
+ assert o.in_server is False
+ assert o.logdir == home.join('.ipa', 'log')
+ assert o.log == home.join('.ipa', 'log', 'default.log')
+
+ # Test with context='server'
+ (o, home) = self.finalize_core('server')
+ assert o.in_server is True
+ assert o.logdir == home.join('.ipa', 'log')
+ assert o.log == home.join('.ipa', 'log', 'server.log')
+
+ # Test that **defaults can't set in_server, logdir, nor log:
+ (o, home) = self.finalize_core(None,
+ in_server='IN_SERVER',
+ logdir='LOGDIR',
+ log='LOG',
+ )
+ assert o.in_server is False
+ assert o.logdir == home.join('.ipa', 'log')
+ assert o.log == home.join('.ipa', 'log', 'default.log')
+
+ # Test loading config file, plus test some in-tree stuff
+ (o, home) = self.bootstrap(in_tree=True, context='server')
+ for key in ('yes', 'no', 'number'):
+ assert key not in o
+ home.write(config_good, '.ipa', 'server.conf')
+ home.write(config_default, '.ipa', 'default.conf')
+ o._finalize_core()
+ assert o.in_tree is True
+ assert o.context == 'server'
+ assert o.in_server is True
+ assert o.logdir == home.join('.ipa', 'log')
+ assert o.log == home.join('.ipa', 'log', 'server.log')
+ assert o.yes is True
+ assert o.no is False
+ assert o.number == 42
+ assert o.not_in_other == 'foo_bar'
+
+ # Test using DEFAULT_CONFIG:
+ defaults = dict(constants.DEFAULT_CONFIG)
+ (o, home) = self.finalize_core(None, **defaults)
+ assert list(o) == sorted(defaults)
+ for (key, value) in defaults.items():
+ if value is object:
+ continue
+ if key == 'mode':
+ continue
+ assert o[key] == value, '%r is %r; should be %r' % (key, o[key], value)
+
+ def test_finalize(self):
+ """
+ Test the `ipalib.config.Env._finalize` method.
+ """
+ # Check that calls cascade up the chain:
+ (o, home) = self.new(in_tree=True)
+ assert o._isdone('_bootstrap') is False
+ assert o._isdone('_finalize_core') is False
+ assert o._isdone('_finalize') is False
+ o._finalize()
+ assert o._isdone('_bootstrap') is True
+ assert o._isdone('_finalize_core') is True
+ assert o._isdone('_finalize') is True
+
+ # Check that it can't be called twice:
+ e = raises(StandardError, o._finalize)
+ assert str(e) == 'Env._finalize() already called'
+
+ # Check that _finalize() calls __lock__()
+ (o, home) = self.new(in_tree=True)
+ assert o.__islocked__() is False
+ o._finalize()
+ assert o.__islocked__() is True
+ e = raises(StandardError, o.__lock__)
+ assert str(e) == 'Env.__lock__() already called'
+
+ # Check that **lastchance works
+ (o, home) = self.finalize_core(None)
+ key = 'just_one_more_key'
+ value = u'with one more value'
+ lastchance = {key: value}
+ assert key not in o
+ assert o._isdone('_finalize') is False
+ o._finalize(**lastchance)
+ assert key in o
+ assert o[key] is value
diff --git a/ipatests/test_ipalib/test_crud.py b/ipatests/test_ipalib/test_crud.py
new file mode 100644
index 000000000..602f99f24
--- /dev/null
+++ b/ipatests/test_ipalib/test_crud.py
@@ -0,0 +1,240 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.crud` module.
+"""
+
+from ipatests.util import read_only, raises, get_api, ClassChecker
+from ipalib import crud, frontend, plugable, config
+from ipalib.parameters import Str
+
+
+class CrudChecker(ClassChecker):
+ """
+ Class for testing base classes in `ipalib.crud`.
+ """
+
+ def get_api(self, args=tuple(), options=tuple()):
+ """
+ Return a finalized `ipalib.plugable.API` instance.
+ """
+ (api, home) = get_api()
+ class user(frontend.Object):
+ takes_params = (
+ 'givenname',
+ Str('sn', flags='no_update'),
+ Str('uid', primary_key=True),
+ 'initials',
+ Str('uidnumber', flags=['no_create', 'no_search'])
+ )
+ class user_verb(self.cls):
+ takes_args = args
+ takes_options = options
+ api.register(user)
+ api.register(user_verb)
+ api.finalize()
+ return api
+
+
+class test_Create(CrudChecker):
+ """
+ Test the `ipalib.crud.Create` class.
+ """
+
+ _cls = crud.Create
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Create.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Create.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'sn', 'initials', 'all', 'raw', 'version']
+ for param in api.Method.user_verb.options():
+ if param.name != 'version':
+ assert param.required is True
+ api = self.get_api(options=('extra?',))
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'sn', 'initials', 'extra', 'all', 'raw', 'version']
+ assert api.Method.user_verb.options.extra.required is False
+
+
+class test_Update(CrudChecker):
+ """
+ Test the `ipalib.crud.Update` class.
+ """
+
+ _cls = crud.Update
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Update.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Update.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'initials', 'uidnumber', 'all', 'raw', 'version']
+ for param in api.Method.user_verb.options():
+ if param.name in ['all', 'raw']:
+ assert param.required is True
+ else:
+ assert param.required is False
+
+
+class test_Retrieve(CrudChecker):
+ """
+ Test the `ipalib.crud.Retrieve` class.
+ """
+
+ _cls = crud.Retrieve
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Retrieve.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Retrieve.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == ['all', 'raw', 'version']
+
+
+class test_Delete(CrudChecker):
+ """
+ Test the `ipalib.crud.Delete` class.
+ """
+
+ _cls = crud.Delete
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Delete.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Delete.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == ['version']
+ assert len(api.Method.user_verb.options) == 1
+
+
+class test_Search(CrudChecker):
+ """
+ Test the `ipalib.crud.Search` class.
+ """
+
+ _cls = crud.Search
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Search.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['criteria']
+ assert api.Method.user_verb.args.criteria.required is False
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Search.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'sn', 'uid', 'initials', 'all', 'raw', 'version']
+ for param in api.Method.user_verb.options():
+ if param.name in ['all', 'raw']:
+ assert param.required is True
+ else:
+ assert param.required is False
+
+
+class test_CrudBackend(ClassChecker):
+ """
+ Test the `ipalib.crud.CrudBackend` class.
+ """
+
+ _cls = crud.CrudBackend
+
+ def get_subcls(self):
+ class ldap(self.cls):
+ pass
+ return ldap
+
+ def check_method(self, name, *args):
+ o = self.cls()
+ e = raises(NotImplementedError, getattr(o, name), *args)
+ assert str(e) == 'CrudBackend.%s()' % name
+ sub = self.subcls()
+ e = raises(NotImplementedError, getattr(sub, name), *args)
+ assert str(e) == 'ldap.%s()' % name
+
+ def test_create(self):
+ """
+ Test the `ipalib.crud.CrudBackend.create` method.
+ """
+ self.check_method('create')
+
+ def test_retrieve(self):
+ """
+ Test the `ipalib.crud.CrudBackend.retrieve` method.
+ """
+ self.check_method('retrieve', 'primary key', 'attribute')
+
+ def test_update(self):
+ """
+ Test the `ipalib.crud.CrudBackend.update` method.
+ """
+ self.check_method('update', 'primary key')
+
+ def test_delete(self):
+ """
+ Test the `ipalib.crud.CrudBackend.delete` method.
+ """
+ self.check_method('delete', 'primary key')
+
+ def test_search(self):
+ """
+ Test the `ipalib.crud.CrudBackend.search` method.
+ """
+ self.check_method('search')
diff --git a/ipatests/test_ipalib/test_errors.py b/ipatests/test_ipalib/test_errors.py
new file mode 100644
index 000000000..258af3b3f
--- /dev/null
+++ b/ipatests/test_ipalib/test_errors.py
@@ -0,0 +1,374 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.errors` module.
+"""
+
+import re
+import inspect
+
+from ipatests.util import assert_equal, raises
+from ipalib import errors, text
+from ipalib.constants import TYPE_ERROR
+
+
+class PrivateExceptionTester(object):
+ _klass = None
+ __klass = None
+
+ def __get_klass(self):
+ if self.__klass is None:
+ self.__klass = self._klass
+ assert issubclass(self.__klass, StandardError)
+ assert issubclass(self.__klass, errors.PrivateError)
+ assert not issubclass(self.__klass, errors.PublicError)
+ return self.__klass
+ klass = property(__get_klass)
+
+ def new(self, **kw):
+ for (key, value) in kw.iteritems():
+ assert not hasattr(self.klass, key), key
+ inst = self.klass(**kw)
+ assert isinstance(inst, StandardError)
+ assert isinstance(inst, errors.PrivateError)
+ assert isinstance(inst, self.klass)
+ assert not isinstance(inst, errors.PublicError)
+ for (key, value) in kw.iteritems():
+ assert getattr(inst, key) is value
+ assert str(inst) == self.klass.format % kw
+ assert inst.message == str(inst)
+ return inst
+
+
+class test_PrivateError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors.PrivateError` exception.
+ """
+ _klass = errors.PrivateError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.PrivateError.__init__` method.
+ """
+ inst = self.klass(key1='Value 1', key2='Value 2')
+ assert inst.key1 == 'Value 1'
+ assert inst.key2 == 'Value 2'
+ assert str(inst) == ''
+
+ # Test subclass and use of format:
+ class subclass(self.klass):
+ format = '%(true)r %(text)r %(number)r'
+
+ kw = dict(true=True, text='Hello!', number=18)
+ inst = subclass(**kw)
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+ assert str(inst) == subclass.format % kw
+
+ # Test via PrivateExceptionTester.new()
+ inst = self.new(**kw)
+ assert isinstance(inst, self.klass)
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+
+
+class test_SubprocessError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors.SubprocessError` exception.
+ """
+
+ _klass = errors.SubprocessError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.SubprocessError.__init__` method.
+ """
+ inst = self.new(returncode=1, argv=('/bin/false',))
+ assert inst.returncode == 1
+ assert inst.argv == ('/bin/false',)
+ assert str(inst) == "return code 1 from ('/bin/false',)"
+ assert inst.message == str(inst)
+
+
+class test_PluginSubclassError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors.PluginSubclassError` exception.
+ """
+
+ _klass = errors.PluginSubclassError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.PluginSubclassError.__init__` method.
+ """
+ inst = self.new(plugin='bad', bases=('base1', 'base2'))
+ assert inst.plugin == 'bad'
+ assert inst.bases == ('base1', 'base2')
+ assert str(inst) == \
+ "'bad' not subclass of any base in ('base1', 'base2')"
+ assert inst.message == str(inst)
+
+
+class test_PluginDuplicateError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors.PluginDuplicateError` exception.
+ """
+
+ _klass = errors.PluginDuplicateError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.PluginDuplicateError.__init__` method.
+ """
+ inst = self.new(plugin='my_plugin')
+ assert inst.plugin == 'my_plugin'
+ assert str(inst) == "'my_plugin' was already registered"
+ assert inst.message == str(inst)
+
+
+class test_PluginOverrideError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors.PluginOverrideError` exception.
+ """
+
+ _klass = errors.PluginOverrideError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.PluginOverrideError.__init__` method.
+ """
+ inst = self.new(base='Base', name='cmd', plugin='my_cmd')
+ assert inst.base == 'Base'
+ assert inst.name == 'cmd'
+ assert inst.plugin == 'my_cmd'
+ assert str(inst) == "unexpected override of Base.cmd with 'my_cmd'"
+ assert inst.message == str(inst)
+
+
+class test_PluginMissingOverrideError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors.PluginMissingOverrideError` exception.
+ """
+
+ _klass = errors.PluginMissingOverrideError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.PluginMissingOverrideError.__init__` method.
+ """
+ inst = self.new(base='Base', name='cmd', plugin='my_cmd')
+ assert inst.base == 'Base'
+ assert inst.name == 'cmd'
+ assert inst.plugin == 'my_cmd'
+ assert str(inst) == "Base.cmd not registered, cannot override with 'my_cmd'"
+ assert inst.message == str(inst)
+
+
+
+##############################################################################
+# Unit tests for public errors:
+
+class PublicExceptionTester(object):
+ _klass = None
+ __klass = None
+
+ def __get_klass(self):
+ if self.__klass is None:
+ self.__klass = self._klass
+ assert issubclass(self.__klass, StandardError)
+ assert issubclass(self.__klass, errors.PublicError)
+ assert not issubclass(self.__klass, errors.PrivateError)
+ assert type(self.__klass.errno) is int
+ assert 900 <= self.__klass.errno <= 5999
+ return self.__klass
+ klass = property(__get_klass)
+
+ def new(self, format=None, message=None, **kw):
+ # Test that TypeError is raised if message isn't unicode:
+ e = raises(TypeError, self.klass, message='The message')
+ assert str(e) == TYPE_ERROR % ('message', unicode, 'The message', str)
+
+ # Test the instance:
+ for (key, value) in kw.iteritems():
+ assert not hasattr(self.klass, key), key
+ inst = self.klass(format=format, message=message, **kw)
+ for required_class in self.required_classes:
+ assert isinstance(inst, required_class)
+ assert isinstance(inst, self.klass)
+ assert not isinstance(inst, errors.PrivateError)
+ for (key, value) in kw.iteritems():
+ assert getattr(inst, key) is value
+ return inst
+
+
+class test_PublicError(PublicExceptionTester):
+ """
+ Test the `ipalib.errors.PublicError` exception.
+ """
+ _klass = errors.PublicError
+ required_classes = StandardError, errors.PublicError
+
+ def test_init(self):
+ message = u'The translated, interpolated message'
+ format = 'key=%(key1)r and key2=%(key2)r'
+ uformat = u'Translated key=%(key1)r and key2=%(key2)r'
+ val1 = 'Value 1'
+ val2 = 'Value 2'
+ kw = dict(key1=val1, key2=val2)
+
+ # Test with format=str, message=None
+ inst = self.klass(format, **kw)
+ assert inst.format is format
+ assert_equal(inst.message, format % kw)
+ assert inst.forwarded is False
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+ # Test with format=None, message=unicode
+ inst = self.klass(message=message, **kw)
+ assert inst.format is None
+ assert inst.message is message
+ assert inst.strerror is message
+ assert inst.forwarded is True
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+ # Test with format=None, message=str
+ e = raises(TypeError, self.klass, message='the message', **kw)
+ assert str(e) == TYPE_ERROR % ('message', unicode, 'the message', str)
+
+ # Test with format=None, message=None
+ e = raises(ValueError, self.klass, **kw)
+ assert (str(e) == '%s.format is None yet format=None, message=None' %
+ self.klass.__name__)
+
+
+ ######################################
+ # Test via PublicExceptionTester.new()
+
+ # Test with format=str, message=None
+ inst = self.new(format, **kw)
+ assert isinstance(inst, self.klass)
+ assert inst.format is format
+ assert_equal(inst.message, format % kw)
+ assert inst.forwarded is False
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+ # Test with format=None, message=unicode
+ inst = self.new(message=message, **kw)
+ assert isinstance(inst, self.klass)
+ assert inst.format is None
+ assert inst.message is message
+ assert inst.strerror is message
+ assert inst.forwarded is True
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+
+ ##################
+ # Test a subclass:
+ class subclass(self.klass):
+ format = '%(true)r %(text)r %(number)r'
+
+ uformat = u'Translated %(true)r %(text)r %(number)r'
+ kw = dict(true=True, text='Hello!', number=18)
+
+ # Test with format=str, message=None
+ e = raises(ValueError, subclass, format, **kw)
+ assert str(e) == 'non-generic %r needs format=None; got format=%r' % (
+ 'subclass', format)
+
+ # Test with format=None, message=None:
+ inst = subclass(**kw)
+ assert inst.format is subclass.format
+ assert_equal(inst.message, subclass.format % kw)
+ assert inst.forwarded is False
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+
+ # Test with format=None, message=unicode:
+ inst = subclass(message=message, **kw)
+ assert inst.format is subclass.format
+ assert inst.message is message
+ assert inst.strerror is message
+ assert inst.forwarded is True
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+
+ # Test with instructions:
+ # first build up "instructions", then get error and search for
+ # lines of instructions appended to the end of the strerror
+ # despite the parameter 'instructions' not existing in the format
+ instructions = u"The quick brown fox jumps over the lazy dog".split()
+ # this expression checks if each word of instructions
+ # exists in a string as a separate line, with right order
+ regexp = re.compile('(?ims).*' +
+ ''.join(map(lambda x: '(%s).*' % (x),
+ instructions)) +
+ '$')
+ inst = subclass(instructions=instructions, **kw)
+ assert inst.format is subclass.format
+ assert_equal(inst.instructions, instructions)
+ inst_match = regexp.match(inst.strerror).groups()
+ assert_equal(list(inst_match),list(instructions))
+
+
+class BaseMessagesTest(object):
+ """Generic test for all of a module's errors or messages
+ """
+ def test_public_messages(self):
+ i = 0
+ for klass in self.message_list:
+ for required_class in self.required_classes:
+ assert issubclass(klass, required_class)
+ assert type(klass.errno) is int
+ assert klass.errno in self.errno_range
+ doc = inspect.getdoc(klass)
+ assert doc is not None, 'need class docstring for %s' % klass.__name__
+ m = re.match(r'^\*{2}(\d+)\*{2} ', doc)
+ assert m is not None, "need '**ERRNO**' in %s docstring" % klass.__name__
+ errno = int(m.group(1))
+ assert errno == klass.errno, (
+ 'docstring=%r but errno=%r in %s' % (errno, klass.errno, klass.__name__)
+ )
+ self.extratest(klass)
+
+ # Test format
+ if klass.format is not None:
+ assert klass.format is self.texts[i]
+ i += 1
+
+ def extratest(self, cls):
+ pass
+
+
+class test_PublicErrors(object):
+ message_list = errors.public_errors
+ errno_range = xrange(900, 5999)
+ required_classes = (StandardError, errors.PublicError)
+ texts = errors._texts
+
+ def extratest(self, cls):
+ assert not issubclass(cls, errors.PrivateError)
diff --git a/ipatests/test_ipalib/test_frontend.py b/ipatests/test_ipalib/test_frontend.py
new file mode 100644
index 000000000..310d7a53d
--- /dev/null
+++ b/ipatests/test_ipalib/test_frontend.py
@@ -0,0 +1,1188 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.frontend` module.
+"""
+
+from ipatests.util import raises, getitem, no_set, no_del, read_only
+from ipatests.util import check_TypeError, ClassChecker, create_test_api
+from ipatests.util import assert_equal
+from ipalib.constants import TYPE_ERROR
+from ipalib.base import NameSpace
+from ipalib import frontend, backend, plugable, errors, parameters, config
+from ipalib import output, messages
+from ipalib.parameters import Str
+from ipapython.version import API_VERSION
+
+def test_RULE_FLAG():
+ assert frontend.RULE_FLAG == 'validation_rule'
+
+
+def test_rule():
+ """
+ Test the `ipalib.frontend.rule` function.
+ """
+ flag = frontend.RULE_FLAG
+ rule = frontend.rule
+ def my_func():
+ pass
+ assert not hasattr(my_func, flag)
+ rule(my_func)
+ assert getattr(my_func, flag) is True
+ @rule
+ def my_func2():
+ pass
+ assert getattr(my_func2, flag) is True
+
+
+def test_is_rule():
+ """
+ Test the `ipalib.frontend.is_rule` function.
+ """
+ is_rule = frontend.is_rule
+ flag = frontend.RULE_FLAG
+
+ class no_call(object):
+ def __init__(self, value):
+ if value is not None:
+ assert value in (True, False)
+ setattr(self, flag, value)
+
+ class call(no_call):
+ def __call__(self):
+ pass
+
+ assert is_rule(call(True))
+ assert not is_rule(no_call(True))
+ assert not is_rule(call(False))
+ assert not is_rule(call(None))
+
+
+class test_HasParam(ClassChecker):
+ """
+ Test the `ipalib.frontend.Command` class.
+ """
+
+ _cls = frontend.HasParam
+
+ def test_get_param_iterable(self):
+ """
+ Test the `ipalib.frontend.HasParam._get_param_iterable` method.
+ """
+ class WithTuple(self.cls):
+ takes_stuff = ('one', 'two')
+ o = WithTuple()
+ assert o._get_param_iterable('stuff') is WithTuple.takes_stuff
+
+ junk = ('three', 'four')
+ class WithCallable(self.cls):
+ def takes_stuff(self):
+ return junk
+ o = WithCallable()
+ assert o._get_param_iterable('stuff') is junk
+
+ class WithParam(self.cls):
+ takes_stuff = parameters.Str('five')
+ o = WithParam()
+ assert o._get_param_iterable('stuff') == (WithParam.takes_stuff,)
+
+ class WithStr(self.cls):
+ takes_stuff = 'six'
+ o = WithStr()
+ assert o._get_param_iterable('stuff') == ('six',)
+
+ class Wrong(self.cls):
+ takes_stuff = ['seven', 'eight']
+ o = Wrong()
+ e = raises(TypeError, o._get_param_iterable, 'stuff')
+ assert str(e) == '%s.%s must be a tuple, callable, or spec; got %r' % (
+ 'Wrong', 'takes_stuff', Wrong.takes_stuff
+ )
+
+ def test_filter_param_by_context(self):
+ """
+ Test the `ipalib.frontend.HasParam._filter_param_by_context` method.
+ """
+ class Example(self.cls):
+ def get_stuff(self):
+ return (
+ 'one', # Make sure create_param() is called for each spec
+ 'two',
+ parameters.Str('three', include='cli'),
+ parameters.Str('four', exclude='server'),
+ parameters.Str('five', exclude=['whatever', 'cli']),
+ )
+ o = Example()
+
+ # Test when env is None:
+ params = list(o._filter_param_by_context('stuff'))
+ assert list(p.name for p in params) == [
+ 'one', 'two', 'three', 'four', 'five'
+ ]
+ for p in params:
+ assert type(p) is parameters.Str
+
+ # Test when env.context == 'cli':
+ cli = config.Env(context='cli')
+ assert cli.context == 'cli'
+ params = list(o._filter_param_by_context('stuff', cli))
+ assert list(p.name for p in params) == ['one', 'two', 'three', 'four']
+ for p in params:
+ assert type(p) is parameters.Str
+
+ # Test when env.context == 'server'
+ server = config.Env(context='server')
+ assert server.context == 'server'
+ params = list(o._filter_param_by_context('stuff', server))
+ assert list(p.name for p in params) == ['one', 'two', 'five']
+ for p in params:
+ assert type(p) is parameters.Str
+
+ # Test with no get_stuff:
+ class Missing(self.cls):
+ pass
+ o = Missing()
+ gen = o._filter_param_by_context('stuff')
+ e = raises(NotImplementedError, list, gen)
+ assert str(e) == 'Missing.get_stuff()'
+
+ # Test when get_stuff is not callable:
+ class NotCallable(self.cls):
+ get_stuff = ('one', 'two')
+ o = NotCallable()
+ gen = o._filter_param_by_context('stuff')
+ e = raises(TypeError, list, gen)
+ assert str(e) == '%s.%s must be a callable; got %r' % (
+ 'NotCallable', 'get_stuff', NotCallable.get_stuff
+ )
+
+
+class test_Command(ClassChecker):
+ """
+ Test the `ipalib.frontend.Command` class.
+ """
+
+ _cls = frontend.Command
+
+ def get_subcls(self):
+ """
+ Return a standard subclass of `ipalib.frontend.Command`.
+ """
+ class Rule(object):
+ def __init__(self, name):
+ self.name = name
+
+ def __call__(self, _, value):
+ if value != self.name:
+ return _('must equal %r') % self.name
+
+ default_from = parameters.DefaultFrom(
+ lambda arg: arg,
+ 'default_from'
+ )
+ normalizer = lambda value: value.lower()
+
+ class example(self.cls):
+ takes_options = (
+ parameters.Str('option0', Rule('option0'),
+ normalizer=normalizer,
+ default_from=default_from,
+ ),
+ parameters.Str('option1', Rule('option1'),
+ normalizer=normalizer,
+ default_from=default_from,
+ ),
+ )
+ return example
+
+ def get_instance(self, args=tuple(), options=tuple()):
+ """
+ Helper method used to test args and options.
+ """
+ class example(self.cls):
+ takes_args = args
+ takes_options = options
+ o = example()
+ o.finalize()
+ return o
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Command` class.
+ """
+ assert self.cls.takes_options == tuple()
+ assert self.cls.takes_args == tuple()
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.frontend.Command.get_args` method.
+ """
+ assert list(self.cls().get_args()) == []
+ args = ('login', 'stuff')
+ o = self.get_instance(args=args)
+ assert tuple(o.get_args()) == args
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.frontend.Command.get_options` method.
+ """
+ options = list(self.cls().get_options())
+ assert len(options) == 1
+ assert options[0].name == 'version'
+ options = ('verbose', 'debug')
+ o = self.get_instance(options=options)
+ assert len(tuple(o.get_options())) == 3
+ assert 'verbose' in tuple(o.get_options())
+ assert 'debug' in tuple(o.get_options())
+
+ def test_args(self):
+ """
+ Test the ``ipalib.frontend.Command.args`` instance attribute.
+ """
+ assert self.cls().args is None
+ o = self.cls()
+ o.finalize()
+ assert type(o.args) is plugable.NameSpace
+ assert len(o.args) == 0
+ args = ('destination', 'source?')
+ ns = self.get_instance(args=args).args
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == len(args)
+ assert list(ns) == ['destination', 'source']
+ assert type(ns.destination) is parameters.Str
+ assert type(ns.source) is parameters.Str
+ assert ns.destination.required is True
+ assert ns.destination.multivalue is False
+ assert ns.source.required is False
+ assert ns.source.multivalue is False
+
+ # Test TypeError:
+ e = raises(TypeError, self.get_instance, args=(u'whatever',))
+ assert str(e) == TYPE_ERROR % (
+ 'spec', (str, parameters.Param), u'whatever', unicode)
+
+ # Test ValueError, required after optional:
+ e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2'))
+ assert str(e) == 'arg2: required argument after optional'
+
+ # Test ValueError, scalar after multivalue:
+ e = raises(ValueError, self.get_instance, args=('arg1+', 'arg2'))
+ assert str(e) == 'arg2: only final argument can be multivalue'
+
+ def test_max_args(self):
+ """
+ Test the ``ipalib.frontend.Command.max_args`` instance attribute.
+ """
+ o = self.get_instance()
+ assert o.max_args == 0
+ o = self.get_instance(args=('one?',))
+ assert o.max_args == 1
+ o = self.get_instance(args=('one', 'two?'))
+ assert o.max_args == 2
+ o = self.get_instance(args=('one', 'multi+',))
+ assert o.max_args is None
+ o = self.get_instance(args=('one', 'multi*',))
+ assert o.max_args is None
+
+ def test_options(self):
+ """
+ Test the ``ipalib.frontend.Command.options`` instance attribute.
+ """
+ assert self.cls().options is None
+ o = self.cls()
+ o.finalize()
+ assert type(o.options) is plugable.NameSpace
+ assert len(o.options) == 1
+ options = ('target', 'files*')
+ ns = self.get_instance(options=options).options
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == len(options) + 1
+ assert list(ns) == ['target', 'files', 'version']
+ assert type(ns.target) is parameters.Str
+ assert type(ns.files) is parameters.Str
+ assert ns.target.required is True
+ assert ns.target.multivalue is False
+ assert ns.files.required is False
+ assert ns.files.multivalue is True
+
+ def test_output(self):
+ """
+ Test the ``ipalib.frontend.Command.output`` instance attribute.
+ """
+ inst = self.cls()
+ assert inst.output is None
+ inst.finalize()
+ assert type(inst.output) is plugable.NameSpace
+ assert list(inst.output) == ['result']
+ assert type(inst.output.result) is output.Output
+
+ def test_iter_output(self):
+ """
+ Test the ``ipalib.frontend.Command._iter_output`` instance attribute.
+ """
+ class Example(self.cls):
+ pass
+ inst = Example()
+
+ inst.has_output = tuple()
+ assert list(inst._iter_output()) == []
+
+ wrong = ['hello', 'world']
+ inst.has_output = wrong
+ e = raises(TypeError, list, inst._iter_output())
+ assert str(e) == 'Example.has_output: need a %r; got a %r: %r' % (
+ tuple, list, wrong
+ )
+
+ wrong = ('hello', 17)
+ inst.has_output = wrong
+ e = raises(TypeError, list, inst._iter_output())
+ assert str(e) == 'Example.has_output[1]: need a %r; got a %r: %r' % (
+ (str, output.Output), int, 17
+ )
+
+ okay = ('foo', output.Output('bar'), 'baz')
+ inst.has_output = okay
+ items = list(inst._iter_output())
+ assert len(items) == 3
+ assert list(o.name for o in items) == ['foo', 'bar', 'baz']
+ for o in items:
+ assert type(o) is output.Output
+
+ def test_soft_validate(self):
+ """
+ Test the `ipalib.frontend.Command.soft_validate` method.
+ """
+ class user_add(frontend.Command):
+ takes_args = parameters.Str('uid',
+ normalizer=lambda value: value.lower(),
+ default_from=lambda givenname, sn: givenname[0] + sn,
+ )
+
+ takes_options = ('givenname', 'sn')
+
+ cmd = user_add()
+ cmd.env = config.Env(context='cli')
+ cmd.finalize()
+ assert list(cmd.params) == ['givenname', 'sn', 'uid', 'version']
+ ret = cmd.soft_validate({})
+ assert sorted(ret['values']) == ['version']
+ assert sorted(ret['errors']) == ['givenname', 'sn', 'uid']
+ assert cmd.soft_validate(dict(givenname=u'First', sn=u'Last')) == dict(
+ values=dict(givenname=u'First', sn=u'Last', uid=u'flast',
+ version=None),
+ errors=dict(),
+ )
+
+ def test_convert(self):
+ """
+ Test the `ipalib.frontend.Command.convert` method.
+ """
+ kw = dict(
+ option0=u'1.5',
+ option1=u'7',
+ )
+ o = self.subcls()
+ o.finalize()
+ for (key, value) in o.convert(**kw).iteritems():
+ assert_equal(unicode(kw[key]), value)
+
+ def test_normalize(self):
+ """
+ Test the `ipalib.frontend.Command.normalize` method.
+ """
+ kw = dict(
+ option0=u'OPTION0',
+ option1=u'OPTION1',
+ )
+ norm = dict((k, v.lower()) for (k, v) in kw.items())
+ sub = self.subcls()
+ sub.finalize()
+ assert sub.normalize(**kw) == norm
+
+ def test_get_default(self):
+ """
+ Test the `ipalib.frontend.Command.get_default` method.
+ """
+ # FIXME: Add an updated unit tests for get_default()
+
+ def test_default_from_chaining(self):
+ """
+ Test chaining of parameters through default_from.
+ """
+ class my_cmd(self.cls):
+ takes_options = (
+ Str('option0'),
+ Str('option1', default_from=lambda option0: option0),
+ Str('option2', default_from=lambda option1: option1),
+ )
+
+ def run(self, *args, **options):
+ return dict(result=options)
+
+ kw = dict(option0=u'some value')
+
+ (api, home) = create_test_api()
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ o.finalize()
+ e = o(**kw)
+ assert type(e) is dict
+ assert 'result' in e
+ assert 'option2' in e['result']
+ assert e['result']['option2'] == u'some value'
+
+ def test_validate(self):
+ """
+ Test the `ipalib.frontend.Command.validate` method.
+ """
+
+ sub = self.subcls()
+ sub.env = config.Env(context='cli')
+ sub.finalize()
+
+ # Check with valid values
+ okay = dict(
+ option0=u'option0',
+ option1=u'option1',
+ another_option='some value',
+ version=API_VERSION,
+ )
+ sub.validate(**okay)
+
+ # Check with an invalid value
+ fail = dict(okay)
+ fail['option0'] = u'whatever'
+ e = raises(errors.ValidationError, sub.validate, **fail)
+ assert_equal(e.name, 'option0')
+ assert_equal(e.value, u'whatever')
+ assert_equal(e.error, u"must equal 'option0'")
+ assert e.rule.__class__.__name__ == 'Rule'
+ assert e.index is None
+
+ # Check with a missing required arg
+ fail = dict(okay)
+ fail.pop('option1')
+ e = raises(errors.RequirementError, sub.validate, **fail)
+ assert e.name == 'option1'
+
+ def test_execute(self):
+ """
+ Test the `ipalib.frontend.Command.execute` method.
+ """
+ o = self.cls()
+ e = raises(NotImplementedError, o.execute)
+ assert str(e) == 'Command.execute()'
+
+ def test_args_options_2_params(self):
+ """
+ Test the `ipalib.frontend.Command.args_options_2_params` method.
+ """
+
+ # Test that ZeroArgumentError is raised:
+ o = self.get_instance()
+ e = raises(errors.ZeroArgumentError, o.args_options_2_params, 1)
+ assert e.name == 'example'
+
+ # Test that MaxArgumentError is raised (count=1)
+ o = self.get_instance(args=('one?',))
+ e = raises(errors.MaxArgumentError, o.args_options_2_params, 1, 2)
+ assert e.name == 'example'
+ assert e.count == 1
+ assert str(e) == "command 'example' takes at most 1 argument"
+
+ # Test that MaxArgumentError is raised (count=2)
+ o = self.get_instance(args=('one', 'two?'))
+ e = raises(errors.MaxArgumentError, o.args_options_2_params, 1, 2, 3)
+ assert e.name == 'example'
+ assert e.count == 2
+ assert str(e) == "command 'example' takes at most 2 arguments"
+
+ # Test that OptionError is raised when an extra option is given:
+ o = self.get_instance()
+ e = raises(errors.OptionError, o.args_options_2_params, bad_option=True)
+ assert e.option == 'bad_option'
+
+ # Test that OverlapError is raised:
+ o = self.get_instance(args=('one', 'two'), options=('three', 'four'))
+ e = raises(errors.OverlapError, o.args_options_2_params,
+ 1, 2, three=3, two=2, four=4, one=1)
+ assert e.names == ['one', 'two']
+
+ # Test the permutations:
+ o = self.get_instance(args=('one', 'two*'), options=('three', 'four'))
+ mthd = o.args_options_2_params
+ assert mthd() == dict()
+ assert mthd(1) == dict(one=1)
+ assert mthd(1, 2) == dict(one=1, two=(2,))
+ assert mthd(1, 21, 22, 23) == dict(one=1, two=(21, 22, 23))
+ assert mthd(1, (21, 22, 23)) == dict(one=1, two=(21, 22, 23))
+ assert mthd(three=3, four=4) == dict(three=3, four=4)
+ assert mthd(three=3, four=4, one=1, two=2) == \
+ dict(one=1, two=2, three=3, four=4)
+ assert mthd(1, 21, 22, 23, three=3, four=4) == \
+ dict(one=1, two=(21, 22, 23), three=3, four=4)
+ assert mthd(1, (21, 22, 23), three=3, four=4) == \
+ dict(one=1, two=(21, 22, 23), three=3, four=4)
+
+ def test_args_options_2_entry(self):
+ """
+ Test `ipalib.frontend.Command.args_options_2_entry` method.
+ """
+ class my_cmd(self.cls):
+ takes_args = (
+ parameters.Str('one', attribute=True),
+ parameters.Str('two', attribute=False),
+ )
+ takes_options = (
+ parameters.Str('three', attribute=True, multivalue=True),
+ parameters.Str('four', attribute=True, multivalue=False),
+ )
+
+ def run(self, *args, **kw):
+ return self.args_options_2_entry(*args, **kw)
+
+ args = ('one', 'two')
+ kw = dict(three=('three1', 'three2'), four='four')
+
+ (api, home) = create_test_api()
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ o.finalize()
+ e = o.run(*args, **kw)
+ assert type(e) is dict
+ assert 'one' in e
+ assert 'two' not in e
+ assert 'three' in e
+ assert 'four' in e
+ assert e['one'] == 'one'
+ assert e['three'] == ['three1', 'three2']
+ assert e['four'] == 'four'
+
+ def test_params_2_args_options(self):
+ """
+ Test the `ipalib.frontend.Command.params_2_args_options` method.
+ """
+ o = self.get_instance(args='one', options='two')
+ assert o.params_2_args_options() == ((None,), {})
+ assert o.params_2_args_options(one=1) == ((1,), {})
+ assert o.params_2_args_options(two=2) == ((None,), dict(two=2))
+ assert o.params_2_args_options(two=2, one=1) == ((1,), dict(two=2))
+
+ def test_run(self):
+ """
+ Test the `ipalib.frontend.Command.run` method.
+ """
+ class my_cmd(self.cls):
+ def execute(self, *args, **kw):
+ return ('execute', args, kw)
+
+ def forward(self, *args, **kw):
+ return ('forward', args, kw)
+
+ args = ('Hello,', 'world,')
+ kw = dict(how_are='you', on_this='fine day?', version=API_VERSION)
+
+ # Test in server context:
+ (api, home) = create_test_api(in_server=True)
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ assert o.run.im_func is self.cls.run.im_func
+ out = o.run(*args, **kw)
+ assert ('execute', args, kw) == out
+
+ # Test in non-server context
+ (api, home) = create_test_api(in_server=False)
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ assert o.run.im_func is self.cls.run.im_func
+ assert ('forward', args, kw) == o.run(*args, **kw)
+
+ def test_messages(self):
+ """
+ Test correct handling of messages
+ """
+ class TestMessage(messages.PublicMessage):
+ type = 'info'
+ format = 'This is a message.'
+ errno = 1234
+
+ class my_cmd(self.cls):
+ def execute(self, *args, **kw):
+ result = {'name': 'execute'}
+ messages.add_message(kw['version'], result, TestMessage())
+ return result
+
+ def forward(self, *args, **kw):
+ result = {'name': 'forward'}
+ messages.add_message(kw['version'], result, TestMessage())
+ return result
+
+ args = ('Hello,', 'world,')
+ kw = dict(how_are='you', on_this='fine day?', version=API_VERSION)
+
+ expected = [TestMessage().to_dict()]
+
+ # Test in server context:
+ (api, home) = create_test_api(in_server=True)
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ assert o.run.im_func is self.cls.run.im_func
+ assert {'name': 'execute', 'messages': expected} == o.run(*args, **kw)
+
+ # Test in non-server context
+ (api, home) = create_test_api(in_server=False)
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ assert o.run.im_func is self.cls.run.im_func
+ assert {'name': 'forward', 'messages': expected} == o.run(*args, **kw)
+
+ def test_validate_output_basic(self):
+ """
+ Test the `ipalib.frontend.Command.validate_output` method.
+ """
+ class Example(self.cls):
+ has_output = ('foo', 'bar', 'baz')
+
+ inst = Example()
+ inst.finalize()
+
+ # Test with wrong type:
+ wrong = ('foo', 'bar', 'baz')
+ e = raises(TypeError, inst.validate_output, wrong)
+ assert str(e) == '%s.validate_output(): need a %r; got a %r: %r' % (
+ 'Example', dict, tuple, wrong
+ )
+
+ # Test with a missing keys:
+ wrong = dict(bar='hello')
+ e = raises(ValueError, inst.validate_output, wrong)
+ assert str(e) == '%s.validate_output(): missing keys %r in %r' % (
+ 'Example', ['baz', 'foo'], wrong
+ )
+
+ # Test with extra keys:
+ wrong = dict(foo=1, bar=2, baz=3, fee=4, azz=5)
+ e = raises(ValueError, inst.validate_output, wrong)
+ assert str(e) == '%s.validate_output(): unexpected keys %r in %r' % (
+ 'Example', ['azz', 'fee'], wrong
+ )
+
+ # Test with different keys:
+ wrong = dict(baz=1, xyzzy=2, quux=3)
+ e = raises(ValueError, inst.validate_output, wrong)
+ assert str(e) == '%s.validate_output(): missing keys %r in %r' % (
+ 'Example', ['bar', 'foo'], wrong
+ ), str(e)
+
+ def test_validate_output_per_type(self):
+ """
+ Test `ipalib.frontend.Command.validate_output` per-type validation.
+ """
+
+ class Complex(self.cls):
+ has_output = (
+ output.Output('foo', int),
+ output.Output('bar', list),
+ )
+ inst = Complex()
+ inst.finalize()
+
+ wrong = dict(foo=17.9, bar=[18])
+ e = raises(TypeError, inst.validate_output, wrong)
+ assert str(e) == '%s:\n output[%r]: need %r; got %r: %r' % (
+ 'Complex.validate_output()', 'foo', int, float, 17.9
+ )
+
+ wrong = dict(foo=18, bar=17)
+ e = raises(TypeError, inst.validate_output, wrong)
+ assert str(e) == '%s:\n output[%r]: need %r; got %r: %r' % (
+ 'Complex.validate_output()', 'bar', list, int, 17
+ )
+
+ def test_validate_output_nested(self):
+ """
+ Test `ipalib.frontend.Command.validate_output` nested validation.
+ """
+
+ class Subclass(output.ListOfEntries):
+ pass
+
+ # Test nested validation:
+ class nested(self.cls):
+ has_output = (
+ output.Output('hello', int),
+ Subclass('world'),
+ )
+ inst = nested()
+ inst.finalize()
+ okay = dict(foo='bar')
+ nope = ('aye', 'bee')
+
+ wrong = dict(hello=18, world=[okay, nope, okay])
+ e = raises(TypeError, inst.validate_output, wrong)
+ assert str(e) == output.emsg % (
+ 'nested', 'Subclass', 'world', 1, dict, tuple, nope
+ )
+
+ wrong = dict(hello=18, world=[okay, okay, okay, okay, nope])
+ e = raises(TypeError, inst.validate_output, wrong)
+ assert str(e) == output.emsg % (
+ 'nested', 'Subclass', 'world', 4, dict, tuple, nope
+ )
+
+ def test_get_output_params(self):
+ """
+ Test the `ipalib.frontend.Command.get_output_params` method.
+ """
+ class example(self.cls):
+ has_output_params = (
+ 'one',
+ 'two',
+ 'three',
+ )
+ takes_args = (
+ 'foo',
+ )
+ takes_options = (
+ Str('bar', flags='no_output'),
+ 'baz',
+ )
+
+ inst = example()
+ assert list(inst.get_output_params()) == ['one', 'two', 'three']
+ inst.finalize()
+ assert list(inst.get_output_params()) == [
+ 'one', 'two', 'three', inst.params.foo, inst.params.baz
+ ]
+ assert list(inst.output_params) == ['one', 'two', 'three', 'foo', 'baz']
+
+
+class test_LocalOrRemote(ClassChecker):
+ """
+ Test the `ipalib.frontend.LocalOrRemote` class.
+ """
+ _cls = frontend.LocalOrRemote
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.LocalOrRemote.__init__` method.
+ """
+ o = self.cls()
+ o.finalize()
+ assert list(o.args) == []
+ assert list(o.options) == ['server', 'version']
+ op = o.options.server
+ assert op.required is False
+ assert op.default is False
+
+ def test_run(self):
+ """
+ Test the `ipalib.frontend.LocalOrRemote.run` method.
+ """
+ class example(self.cls):
+ takes_args = 'key?'
+
+ def forward(self, *args, **options):
+ return dict(result=('forward', args, options))
+
+ def execute(self, *args, **options):
+ return dict(result=('execute', args, options))
+
+ # Test when in_server=False:
+ (api, home) = create_test_api(in_server=False)
+ api.register(example)
+ api.finalize()
+ cmd = api.Command.example
+ assert cmd(version=u'2.47') == dict(
+ result=('execute', (None,), dict(version=u'2.47', server=False))
+ )
+ assert cmd(u'var', version=u'2.47') == dict(
+ result=('execute', (u'var',), dict(version=u'2.47', server=False))
+ )
+ assert cmd(server=True, version=u'2.47') == dict(
+ result=('forward', (None,), dict(version=u'2.47', server=True))
+ )
+ assert cmd(u'var', server=True, version=u'2.47') == dict(
+ result=('forward', (u'var',), dict(version=u'2.47', server=True))
+ )
+
+ # Test when in_server=True (should always call execute):
+ (api, home) = create_test_api(in_server=True)
+ api.register(example)
+ api.finalize()
+ cmd = api.Command.example
+ assert cmd(version=u'2.47') == dict(
+ result=('execute', (None,), dict(version=u'2.47', server=False))
+ )
+ assert cmd(u'var', version=u'2.47') == dict(
+ result=('execute', (u'var',), dict(version=u'2.47', server=False))
+ )
+ assert cmd(server=True, version=u'2.47') == dict(
+ result=('execute', (None,), dict(version=u'2.47', server=True))
+ )
+ assert cmd(u'var', server=True, version=u'2.47') == dict(
+ result=('execute', (u'var',), dict(version=u'2.47', server=True))
+ )
+
+
+class test_Object(ClassChecker):
+ """
+ Test the `ipalib.frontend.Object` class.
+ """
+ _cls = frontend.Object
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Object` class.
+ """
+ assert self.cls.backend is None
+ assert self.cls.methods is None
+ assert self.cls.properties is None
+ assert self.cls.params is None
+ assert self.cls.params_minus_pk is None
+ assert self.cls.takes_params == tuple()
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Object.__init__` method.
+ """
+ o = self.cls()
+ assert o.backend is None
+ assert o.methods is None
+ assert o.properties is None
+ assert o.params is None
+ assert o.params_minus_pk is None
+ assert o.properties is None
+
+ def test_set_api(self):
+ """
+ Test the `ipalib.frontend.Object.set_api` method.
+ """
+ # Setup for test:
+ class DummyAttribute(object):
+ def __init__(self, obj_name, attr_name, name=None):
+ self.obj_name = obj_name
+ self.attr_name = attr_name
+ if name is None:
+ self.name = '%s_%s' % (obj_name, attr_name)
+ else:
+ self.name = name
+ self.param = frontend.create_param(attr_name)
+
+ def __clone__(self, attr_name):
+ return self.__class__(
+ self.obj_name,
+ self.attr_name,
+ getattr(self, attr_name)
+ )
+
+ def get_attributes(cnt, format):
+ for name in ['other', 'user', 'another']:
+ for i in xrange(cnt):
+ yield DummyAttribute(name, format % i)
+
+ cnt = 10
+ formats = dict(
+ methods='method_%d',
+ properties='property_%d',
+ )
+
+
+ _d = dict(
+ Method=plugable.NameSpace(
+ get_attributes(cnt, formats['methods'])
+ ),
+ Property=plugable.NameSpace(
+ get_attributes(cnt, formats['properties'])
+ ),
+ )
+ api = plugable.MagicDict(_d)
+ assert len(api.Method) == cnt * 3
+ assert len(api.Property) == cnt * 3
+
+ class user(self.cls):
+ pass
+
+ # Actually perform test:
+ o = user()
+ o.set_api(api)
+ assert read_only(o, 'api') is api
+ for name in ['methods', 'properties']:
+ namespace = getattr(o, name)
+ assert isinstance(namespace, plugable.NameSpace)
+ assert len(namespace) == cnt
+ f = formats[name]
+ for i in xrange(cnt):
+ attr_name = f % i
+ attr = namespace[attr_name]
+ assert isinstance(attr, DummyAttribute)
+ assert attr is getattr(namespace, attr_name)
+ assert attr.obj_name == 'user'
+ assert attr.attr_name == attr_name
+ assert attr.name == '%s_%s' % ('user', attr_name)
+
+ # Test params instance attribute
+ o = self.cls()
+ o.set_api(api)
+ ns = o.params
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == 0
+ class example(self.cls):
+ takes_params = ('banana', 'apple')
+ o = example()
+ o.set_api(api)
+ ns = o.params
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == 2, repr(ns)
+ assert list(ns) == ['banana', 'apple']
+ for p in ns():
+ assert type(p) is parameters.Str
+ assert p.required is True
+ assert p.multivalue is False
+
+ def test_primary_key(self):
+ """
+ Test the `ipalib.frontend.Object.primary_key` attribute.
+ """
+ (api, home) = create_test_api()
+ api.finalize()
+
+ # Test with no primary keys:
+ class example1(self.cls):
+ takes_params = (
+ 'one',
+ 'two',
+ )
+ o = example1()
+ o.set_api(api)
+ assert o.primary_key is None
+
+ # Test with 1 primary key:
+ class example2(self.cls):
+ takes_params = (
+ 'one',
+ 'two',
+ parameters.Str('three', primary_key=True),
+ 'four',
+ )
+ o = example2()
+ o.set_api(api)
+ pk = o.primary_key
+ assert type(pk) is parameters.Str
+ assert pk.name == 'three'
+ assert pk.primary_key is True
+ assert o.params[2] is o.primary_key
+ assert isinstance(o.params_minus_pk, plugable.NameSpace)
+ assert list(o.params_minus_pk) == ['one', 'two', 'four']
+
+ # Test with multiple primary_key:
+ class example3(self.cls):
+ takes_params = (
+ parameters.Str('one', primary_key=True),
+ parameters.Str('two', primary_key=True),
+ 'three',
+ parameters.Str('four', primary_key=True),
+ )
+ o = example3()
+ o.set_api(api)
+ e = raises(ValueError, o.finalize)
+ assert str(e) == \
+ 'example3 (Object) has multiple primary keys: one, two, four'
+
+ def test_backend(self):
+ """
+ Test the `ipalib.frontend.Object.backend` attribute.
+ """
+ (api, home) = create_test_api()
+ class ldap(backend.Backend):
+ whatever = 'It worked!'
+ api.register(ldap)
+ class user(frontend.Object):
+ backend_name = 'ldap'
+ api.register(user)
+ api.finalize()
+ b = api.Object.user.backend
+ assert isinstance(b, ldap)
+ assert b.whatever == 'It worked!'
+
+ def test_get_dn(self):
+ """
+ Test the `ipalib.frontend.Object.get_dn` method.
+ """
+ o = self.cls()
+ e = raises(NotImplementedError, o.get_dn, 'primary key')
+ assert str(e) == 'Object.get_dn()'
+ class user(self.cls):
+ pass
+ o = user()
+ e = raises(NotImplementedError, o.get_dn, 'primary key')
+ assert str(e) == 'user.get_dn()'
+
+ def test_params_minus(self):
+ """
+ Test the `ipalib.frontend.Object.params_minus` method.
+ """
+ class example(self.cls):
+ takes_params = ('one', 'two', 'three', 'four')
+ o = example()
+ (api, home) = create_test_api()
+ o.set_api(api)
+ p = o.params
+ assert tuple(o.params_minus()) == tuple(p())
+ assert tuple(o.params_minus([])) == tuple(p())
+ assert tuple(o.params_minus('two', 'three')) == (p.one, p.four)
+ assert tuple(o.params_minus(['two', 'three'])) == (p.one, p.four)
+ assert tuple(o.params_minus(p.two, p.three)) == (p.one, p.four)
+ assert tuple(o.params_minus([p.two, p.three])) == (p.one, p.four)
+ ns = NameSpace([p.two, p.three])
+ assert tuple(o.params_minus(ns)) == (p.one, p.four)
+
+
+class test_Attribute(ClassChecker):
+ """
+ Test the `ipalib.frontend.Attribute` class.
+ """
+ _cls = frontend.Attribute
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Attribute` class.
+ """
+ assert self.cls.__bases__ == (plugable.Plugin,)
+ assert type(self.cls.obj) is property
+ assert type(self.cls.obj_name) is property
+ assert type(self.cls.attr_name) is property
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Attribute.__init__` method.
+ """
+ class user_add(self.cls):
+ pass
+ o = user_add()
+ assert read_only(o, 'obj') is None
+ assert read_only(o, 'obj_name') == 'user'
+ assert read_only(o, 'attr_name') == 'add'
+
+ def test_set_api(self):
+ """
+ Test the `ipalib.frontend.Attribute.set_api` method.
+ """
+ user_obj = 'The user frontend.Object instance'
+ class api(object):
+ Object = dict(user=user_obj)
+ class user_add(self.cls):
+ pass
+ o = user_add()
+ assert read_only(o, 'api') is None
+ assert read_only(o, 'obj') is None
+ o.set_api(api)
+ assert read_only(o, 'api') is api
+ assert read_only(o, 'obj') is user_obj
+
+
+class test_Method(ClassChecker):
+ """
+ Test the `ipalib.frontend.Method` class.
+ """
+ _cls = frontend.Method
+
+ def get_api(self, args=tuple(), options=tuple()):
+ """
+ Return a finalized `ipalib.plugable.API` instance.
+ """
+ (api, home) = create_test_api()
+ class user(frontend.Object):
+ takes_params = (
+ 'givenname',
+ 'sn',
+ frontend.Param('uid', primary_key=True),
+ 'initials',
+ )
+ class user_verb(self.cls):
+ takes_args = args
+ takes_options = options
+ api.register(user)
+ api.register(user_verb)
+ api.finalize()
+ return api
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Method` class.
+ """
+ assert self.cls.__bases__ == (frontend.Attribute, frontend.Command)
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Method.__init__` method.
+ """
+ class user_add(self.cls):
+ pass
+ o = user_add()
+ assert o.name == 'user_add'
+ assert o.obj_name == 'user'
+ assert o.attr_name == 'add'
+
+
+class test_Property(ClassChecker):
+ """
+ Test the `ipalib.frontend.Property` class.
+ """
+ _cls = frontend.Property
+
+ def get_subcls(self):
+ """
+ Return a standard subclass of `ipalib.frontend.Property`.
+ """
+ class user_givenname(self.cls):
+ 'User first name'
+
+ @frontend.rule
+ def rule0_lowercase(self, value):
+ if not value.islower():
+ return 'Must be lowercase'
+ return user_givenname
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Property` class.
+ """
+ assert self.cls.__bases__ == (frontend.Attribute,)
+ assert self.cls.klass is parameters.Str
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Property.__init__` method.
+ """
+ o = self.subcls()
+ assert len(o.rules) == 1
+ assert o.rules[0].__name__ == 'rule0_lowercase'
+ param = o.param
+ assert isinstance(param, parameters.Str)
+ assert param.name == 'givenname'
+ assert unicode(param.doc) == u'User first name'
diff --git a/ipatests/test_ipalib/test_messages.py b/ipatests/test_ipalib/test_messages.py
new file mode 100644
index 000000000..686bf8dd5
--- /dev/null
+++ b/ipatests/test_ipalib/test_messages.py
@@ -0,0 +1,89 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 1012 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/>.
+
+"""
+Test the `ipalib.messages` module.
+"""
+
+from ipalib import messages
+from ipalib.capabilities import capabilities
+from ipatests.test_ipalib import test_errors
+
+
+class HelloMessage(messages.PublicMessage):
+ type = 'info'
+ format = '%(greeting)s, %(object)s!'
+ errno = 1234
+
+
+class test_PublicMessage(test_errors.test_PublicError):
+ """Test public messages"""
+ # The messages are a lot like public errors; defer testing to that.
+ klass = messages.PublicMessage
+ required_classes = (UserWarning, messages.PublicMessage)
+
+
+class test_PublicMessages(test_errors.BaseMessagesTest):
+ message_list = messages.public_messages
+ errno_range = xrange(10000, 19999)
+ required_classes = (UserWarning, messages.PublicMessage)
+ texts = messages._texts
+
+ def extratest(self, cls):
+ if cls is not messages.PublicMessage:
+ assert cls.type in ('debug', 'info', 'warning', 'error')
+
+
+def test_to_dict():
+ expected = dict(
+ name='HelloMessage',
+ type='info',
+ message='Hello, world!',
+ code=1234,
+ )
+
+ assert HelloMessage(greeting='Hello', object='world').to_dict() == expected
+
+
+def test_add_message():
+ result = {}
+
+ assert capabilities['messages'] == u'2.52'
+
+ messages.add_message(u'2.52', result,
+ HelloMessage(greeting='Hello', object='world'))
+ messages.add_message(u'2.1', result,
+ HelloMessage(greeting="'Lo", object='version'))
+ messages.add_message(u'2.60', result,
+ HelloMessage(greeting='Hi', object='version'))
+
+ assert result == {'messages': [
+ dict(
+ name='HelloMessage',
+ type='info',
+ message='Hello, world!',
+ code=1234,
+ ),
+ dict(
+ name='HelloMessage',
+ type='info',
+ message='Hi, version!',
+ code=1234,
+ )
+ ]}
diff --git a/ipatests/test_ipalib/test_output.py b/ipatests/test_ipalib/test_output.py
new file mode 100644
index 000000000..15ef11e10
--- /dev/null
+++ b/ipatests/test_ipalib/test_output.py
@@ -0,0 +1,89 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+
+"""
+Test the `ipalib.output` module.
+"""
+
+from ipatests.util import raises, ClassChecker
+from ipalib import output
+from ipalib.frontend import Command
+from ipalib import _
+
+class test_Output(ClassChecker):
+ """
+ Test the `ipalib.output.Output` class.
+ """
+
+ _cls = output.Output
+
+ def test_init(self):
+ """
+ Test the `ipalib.output.Output.__init__` method.
+ """
+ o = self.cls('result')
+ assert o.name == 'result'
+ assert o.type is None
+ assert o.doc is None
+
+ def test_repr(self):
+ """
+ Test the `ipalib.output.Output.__repr__` method.
+ """
+ o = self.cls('aye')
+ assert repr(o) == "Output('aye', None, None)"
+ o = self.cls('aye', type=int, doc='An A, aye?')
+ assert repr(o) == "Output('aye', %r, 'An A, aye?')" % int
+
+ class Entry(self.cls):
+ pass
+ o = Entry('aye')
+ assert repr(o) == "Entry('aye', None, None)"
+ o = Entry('aye', type=int, doc='An A, aye?')
+ assert repr(o) == "Entry('aye', %r, 'An A, aye?')" % int
+
+
+class test_ListOfEntries(ClassChecker):
+ """
+ Test the `ipalib.output.ListOfEntries` class.
+ """
+
+ _cls = output.ListOfEntries
+
+ def test_validate(self):
+ """
+ Test the `ipalib.output.ListOfEntries.validate` method.
+ """
+ class example(Command):
+ pass
+ cmd = example()
+ inst = self.cls('stuff')
+
+ okay = dict(foo='bar')
+ nope = ('aye', 'bee')
+
+ e = raises(TypeError, inst.validate, cmd, [okay, okay, nope])
+ assert str(e) == output.emsg % (
+ 'example', 'ListOfEntries', 'stuff', 2, dict, tuple, nope
+ )
+
+ e = raises(TypeError, inst.validate, cmd, [nope, okay, nope])
+ assert str(e) == output.emsg % (
+ 'example', 'ListOfEntries', 'stuff', 0, dict, tuple, nope
+ )
diff --git a/ipatests/test_ipalib/test_parameters.py b/ipatests/test_ipalib/test_parameters.py
new file mode 100644
index 000000000..71acfce71
--- /dev/null
+++ b/ipatests/test_ipalib/test_parameters.py
@@ -0,0 +1,1533 @@
+# -*- coding: utf-8 -*-
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.parameters` module.
+"""
+
+import re
+import sys
+from types import NoneType
+from decimal import Decimal
+from inspect import isclass
+from ipatests.util import raises, ClassChecker, read_only
+from ipatests.util import dummy_ugettext, assert_equal
+from ipatests.data import binary_bytes, utf8_bytes, unicode_str
+from ipalib import parameters, text, errors, config
+from ipalib.constants import TYPE_ERROR, CALLABLE_ERROR, NULLS
+from ipalib.errors import ValidationError, ConversionError
+from ipalib import _
+from xmlrpclib import MAXINT, MININT
+
+class test_DefaultFrom(ClassChecker):
+ """
+ Test the `ipalib.parameters.DefaultFrom` class.
+ """
+ _cls = parameters.DefaultFrom
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.DefaultFrom.__init__` method.
+ """
+ def callback(*args):
+ return args
+ keys = ('givenname', 'sn')
+ o = self.cls(callback, *keys)
+ assert read_only(o, 'callback') is callback
+ assert read_only(o, 'keys') == keys
+ lam = lambda first, last: first[0] + last
+ o = self.cls(lam)
+ assert read_only(o, 'keys') == ('first', 'last')
+
+ # Test that TypeError is raised when callback isn't callable:
+ e = raises(TypeError, self.cls, 'whatever')
+ assert str(e) == CALLABLE_ERROR % ('callback', 'whatever', str)
+
+ # Test that TypeError is raised when a key isn't an str:
+ e = raises(TypeError, self.cls, callback, 'givenname', 17)
+ assert str(e) == TYPE_ERROR % ('keys', str, 17, int)
+
+ # Test that ValueError is raised when inferring keys from a callback
+ # which has *args:
+ e = raises(ValueError, self.cls, lambda foo, *args: None)
+ assert str(e) == "callback: variable-length argument list not allowed"
+
+ # Test that ValueError is raised when inferring keys from a callback
+ # which has **kwargs:
+ e = raises(ValueError, self.cls, lambda foo, **kwargs: None)
+ assert str(e) == "callback: variable-length argument list not allowed"
+
+ def test_repr(self):
+ """
+ Test the `ipalib.parameters.DefaultFrom.__repr__` method.
+ """
+ def stuff(one, two):
+ pass
+
+ o = self.cls(stuff)
+ assert repr(o) == "DefaultFrom(stuff, 'one', 'two')"
+
+ o = self.cls(stuff, 'aye', 'bee', 'see')
+ assert repr(o) == "DefaultFrom(stuff, 'aye', 'bee', 'see')"
+
+ cb = lambda first, last: first[0] + last
+
+ o = self.cls(cb)
+ assert repr(o) == "DefaultFrom(<lambda>, 'first', 'last')"
+
+ o = self.cls(cb, 'aye', 'bee', 'see')
+ assert repr(o) == "DefaultFrom(<lambda>, 'aye', 'bee', 'see')"
+
+ def test_call(self):
+ """
+ Test the `ipalib.parameters.DefaultFrom.__call__` method.
+ """
+ def callback(givenname, sn):
+ return givenname[0] + sn[0]
+ keys = ('givenname', 'sn')
+ o = self.cls(callback, *keys)
+ kw = dict(
+ givenname='John',
+ sn='Public',
+ hello='world',
+ )
+ assert o(**kw) == 'JP'
+ assert o() is None
+ for key in ('givenname', 'sn'):
+ kw_copy = dict(kw)
+ del kw_copy[key]
+ assert o(**kw_copy) is None
+
+ # Test using implied keys:
+ o = self.cls(lambda first, last: first[0] + last)
+ assert o(first='john', last='doe') == 'jdoe'
+ assert o(first='', last='doe') is None
+ assert o(one='john', two='doe') is None
+
+ # Test that co_varnames slice is used:
+ def callback2(first, last):
+ letter = first[0]
+ return letter + last
+ o = self.cls(callback2)
+ assert o.keys == ('first', 'last')
+ assert o(first='john', last='doe') == 'jdoe'
+
+
+def test_parse_param_spec():
+ """
+ Test the `ipalib.parameters.parse_param_spec` function.
+ """
+ f = parameters.parse_param_spec
+ assert f('name') == ('name', dict(required=True, multivalue=False))
+ assert f('name?') == ('name', dict(required=False, multivalue=False))
+ assert f('name*') == ('name', dict(required=False, multivalue=True))
+ assert f('name+') == ('name', dict(required=True, multivalue=True))
+
+ # Make sure other "funny" endings are *not* treated special:
+ assert f('name^') == ('name^', dict(required=True, multivalue=False))
+
+ # Test that TypeError is raised if spec isn't an str:
+ e = raises(TypeError, f, u'name?')
+ assert str(e) == TYPE_ERROR % ('spec', str, u'name?', unicode)
+
+
+class DummyRule(object):
+ def __init__(self, error=None):
+ assert error is None or type(error) is unicode
+ self.error = error
+ self.reset()
+
+ def __call__(self, *args):
+ self.calls.append(args)
+ return self.error
+
+ def reset(self):
+ self.calls = []
+
+
+class test_Param(ClassChecker):
+ """
+ Test the `ipalib.parameters.Param` class.
+ """
+ _cls = parameters.Param
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Param.__init__` method.
+ """
+ name = 'my_param'
+ o = self.cls(name)
+ assert o.param_spec is name
+ assert o.name is name
+ assert o.nice == "Param('my_param')"
+ assert o.password is False
+ assert o.__islocked__() is True
+
+ # Test default rules:
+ assert o.rules == tuple()
+ assert o.class_rules == tuple()
+ assert o.all_rules == tuple()
+
+ # Test default kwarg values:
+ assert o.cli_name is name
+ assert o.label.msg == 'my_param'
+ assert o.doc.msg == 'my_param'
+ assert o.required is True
+ assert o.multivalue is False
+ assert o.primary_key is False
+ assert o.normalizer is None
+ assert o.default is None
+ assert o.default_from is None
+ assert o.autofill is False
+ assert o.query is False
+ assert o.attribute is False
+ assert o.include is None
+ assert o.exclude is None
+ assert o.flags == frozenset()
+ assert o.sortorder == 2
+ assert o.csv is False
+
+ # Test that doc defaults from label:
+ o = self.cls('my_param', doc=_('Hello world'))
+ assert o.label.msg == 'my_param'
+ assert o.doc.msg == 'Hello world'
+
+ o = self.cls('my_param', label='My Param')
+ assert o.label == 'My Param'
+ assert o.doc == 'My Param'
+
+
+ # Test that ValueError is raised when a kwarg from a subclass
+ # conflicts with an attribute:
+ class Subclass(self.cls):
+ kwargs = self.cls.kwargs + (
+ ('convert', callable, None),
+ )
+ e = raises(ValueError, Subclass, name)
+ assert str(e) == "kwarg 'convert' conflicts with attribute on Subclass"
+
+ # Test type validation of keyword arguments:
+ class Subclass(self.cls):
+ kwargs = self.cls.kwargs + (
+ ('extra1', bool, True),
+ ('extra2', str, 'Hello'),
+ ('extra3', (int, float), 42),
+ ('extra4', callable, lambda whatever: whatever + 7),
+ )
+ o = Subclass('my_param') # Test with no **kw:
+ for (key, kind, default) in o.kwargs:
+ # Test with a type invalid for all:
+ value = object()
+ kw = {key: value}
+ e = raises(TypeError, Subclass, 'my_param', **kw)
+ if kind is callable:
+ assert str(e) == CALLABLE_ERROR % (key, value, type(value))
+ else:
+ assert str(e) == TYPE_ERROR % (key, kind, value, type(value))
+ # Test with None:
+ kw = {key: None}
+ Subclass('my_param', **kw)
+
+ # Test when using unknown kwargs:
+ e = raises(TypeError, self.cls, 'my_param',
+ flags=['hello', 'world'],
+ whatever=u'Hooray!',
+ )
+ assert str(e) == \
+ "Param('my_param'): takes no such kwargs: 'whatever'"
+ e = raises(TypeError, self.cls, 'my_param', great='Yes', ape='he is!')
+ assert str(e) == \
+ "Param('my_param'): takes no such kwargs: 'ape', 'great'"
+
+ # Test that ValueError is raised if you provide both include and
+ # exclude:
+ e = raises(ValueError, self.cls, 'my_param',
+ include=['server', 'foo'],
+ exclude=['client', 'bar'],
+ )
+ assert str(e) == '%s: cannot have both %s=%r and %s=%r' % (
+ "Param('my_param')",
+ 'include', frozenset(['server', 'foo']),
+ 'exclude', frozenset(['client', 'bar']),
+ )
+
+ # Test that ValueError is raised if csv is set and multivalue is not set:
+ e = raises(ValueError, self.cls, 'my_param', csv=True)
+ assert str(e) == '%s: cannot have csv without multivalue' % "Param('my_param')"
+
+ # Test that default_from gets set:
+ call = lambda first, last: first[0] + last
+ o = self.cls('my_param', default_from=call)
+ assert type(o.default_from) is parameters.DefaultFrom
+ assert o.default_from.callback is call
+
+ def test_repr(self):
+ """
+ Test the `ipalib.parameters.Param.__repr__` method.
+ """
+ for name in ['name', 'name?', 'name*', 'name+']:
+ o = self.cls(name)
+ assert repr(o) == 'Param(%r)' % name
+ o = self.cls('name', required=False)
+ assert repr(o) == "Param('name', required=False)"
+ o = self.cls('name', multivalue=True)
+ assert repr(o) == "Param('name', multivalue=True)"
+
+ def test_use_in_context(self):
+ """
+ Test the `ipalib.parameters.Param.use_in_context` method.
+ """
+ set1 = ('one', 'two', 'three')
+ set2 = ('four', 'five', 'six')
+ param1 = self.cls('param1')
+ param2 = self.cls('param2', include=set1)
+ param3 = self.cls('param3', exclude=set2)
+ for context in set1:
+ env = config.Env()
+ env.context = context
+ assert param1.use_in_context(env) is True, context
+ assert param2.use_in_context(env) is True, context
+ assert param3.use_in_context(env) is True, context
+ for context in set2:
+ env = config.Env()
+ env.context = context
+ assert param1.use_in_context(env) is True, context
+ assert param2.use_in_context(env) is False, context
+ assert param3.use_in_context(env) is False, context
+
+ def test_safe_value(self):
+ """
+ Test the `ipalib.parameters.Param.safe_value` method.
+ """
+ values = (unicode_str, binary_bytes, utf8_bytes)
+ o = self.cls('my_param')
+ for value in values:
+ assert o.safe_value(value) is value
+ assert o.safe_value(None) is None
+ p = parameters.Password('my_passwd')
+ for value in values:
+ assert_equal(p.safe_value(value), u'********')
+ assert p.safe_value(None) is None
+
+ def test_clone(self):
+ """
+ Test the `ipalib.parameters.Param.clone` method.
+ """
+ # Test with the defaults
+ orig = self.cls('my_param')
+ clone = orig.clone()
+ assert clone is not orig
+ assert type(clone) is self.cls
+ assert clone.name is orig.name
+ for (key, kind, default) in self.cls.kwargs:
+ assert getattr(clone, key) is getattr(orig, key)
+
+ # Test with a param spec:
+ orig = self.cls('my_param*')
+ assert orig.param_spec == 'my_param*'
+ clone = orig.clone()
+ assert clone.param_spec == 'my_param'
+ assert clone is not orig
+ assert type(clone) is self.cls
+ for (key, kind, default) in self.cls.kwargs:
+ assert getattr(clone, key) is getattr(orig, key)
+
+ # Test with overrides:
+ orig = self.cls('my_param*')
+ assert orig.required is False
+ assert orig.multivalue is True
+ clone = orig.clone(required=True)
+ assert clone is not orig
+ assert type(clone) is self.cls
+ assert clone.required is True
+ assert clone.multivalue is True
+ assert clone.param_spec == 'my_param'
+ assert clone.name == 'my_param'
+
+ def test_clone_rename(self):
+ """
+ Test the `ipalib.parameters.Param.clone` method.
+ """
+ new_name = 'my_new_param'
+
+ # Test with the defaults
+ orig = self.cls('my_param')
+ clone = orig.clone_rename(new_name)
+ assert clone is not orig
+ assert type(clone) is self.cls
+ assert clone.name == new_name
+ for (key, kind, default) in self.cls.kwargs:
+ assert getattr(clone, key) is getattr(orig, key)
+
+ # Test with overrides:
+ orig = self.cls('my_param*')
+ assert orig.required is False
+ assert orig.multivalue is True
+ clone = orig.clone_rename(new_name, required=True)
+ assert clone is not orig
+ assert type(clone) is self.cls
+ assert clone.required is True
+ assert clone.multivalue is True
+ assert clone.param_spec == new_name
+ assert clone.name == new_name
+
+
+ def test_convert(self):
+ """
+ Test the `ipalib.parameters.Param.convert` method.
+ """
+ okay = ('Hello', u'Hello', 0, 4.2, True, False, unicode_str)
+ class Subclass(self.cls):
+ def _convert_scalar(self, value, index=None):
+ return value
+
+ # Test when multivalue=False:
+ o = Subclass('my_param')
+ for value in NULLS:
+ assert o.convert(value) is None
+ assert o.convert(None) is None
+ for value in okay:
+ assert o.convert(value) is value
+
+ # Test when multivalue=True:
+ o = Subclass('my_param', multivalue=True)
+ for value in NULLS:
+ assert o.convert(value) is None
+ assert o.convert(okay) == okay
+ assert o.convert(NULLS) is None
+ assert o.convert(okay + NULLS) == okay
+ assert o.convert(NULLS + okay) == okay
+ for value in okay:
+ assert o.convert(value) == (value,)
+ assert o.convert([None, value]) == (value,)
+ assert o.convert([value, None]) == (value,)
+
+ def test_convert_scalar(self):
+ """
+ Test the `ipalib.parameters.Param._convert_scalar` method.
+ """
+ dummy = dummy_ugettext()
+
+ # Test with correct type:
+ o = self.cls('my_param')
+ assert o._convert_scalar(None) is None
+ assert dummy.called() is False
+ # Test with incorrect type
+ e = raises(errors.ConversionError, o._convert_scalar, 'hello', index=17)
+
+ def test_validate(self):
+ """
+ Test the `ipalib.parameters.Param.validate` method.
+ """
+
+ # Test in default state (with no rules, no kwarg):
+ o = self.cls('my_param')
+ e = raises(errors.RequirementError, o.validate, None, 'cli')
+ assert e.name == 'my_param'
+
+ # Test in default state that cli_name gets returned in the exception
+ # when context == 'cli'
+ o = self.cls('my_param', cli_name='short')
+ e = raises(errors.RequirementError, o.validate, None, 'cli')
+ assert e.name == 'short'
+
+ # Test with required=False
+ o = self.cls('my_param', required=False)
+ assert o.required is False
+ assert o.validate(None, 'cli') is None
+
+ # Test with query=True:
+ o = self.cls('my_param', query=True)
+ assert o.query is True
+ e = raises(errors.RequirementError, o.validate, None, 'cli')
+ assert_equal(e.name, 'my_param')
+
+ # Test with multivalue=True:
+ o = self.cls('my_param', multivalue=True)
+ e = raises(TypeError, o.validate, [], 'cli')
+ assert str(e) == TYPE_ERROR % ('value', tuple, [], list)
+ e = raises(ValueError, o.validate, tuple(), 'cli')
+ assert str(e) == 'value: empty tuple must be converted to None'
+
+ # Test with wrong (scalar) type:
+ e = raises(TypeError, o.validate, (None, None, 42, None), 'cli')
+ assert str(e) == TYPE_ERROR % ('my_param', NoneType, 42, int)
+ o = self.cls('my_param')
+ e = raises(TypeError, o.validate, 'Hello', 'cli')
+ assert str(e) == TYPE_ERROR % ('my_param', NoneType, 'Hello', str)
+
+ class Example(self.cls):
+ type = int
+
+ # Test with some rules and multivalue=False
+ pass1 = DummyRule()
+ pass2 = DummyRule()
+ fail = DummyRule(u'no good')
+ o = Example('example', pass1, pass2)
+ assert o.multivalue is False
+ assert o.validate(11, 'cli') is None
+ assert pass1.calls == [(text.ugettext, 11)]
+ assert pass2.calls == [(text.ugettext, 11)]
+ pass1.reset()
+ pass2.reset()
+ o = Example('example', pass1, pass2, fail)
+ e = raises(errors.ValidationError, o.validate, 42, 'cli')
+ assert e.name == 'example'
+ assert e.error == u'no good'
+ assert e.index is None
+ assert pass1.calls == [(text.ugettext, 42)]
+ assert pass2.calls == [(text.ugettext, 42)]
+ assert fail.calls == [(text.ugettext, 42)]
+
+ # Test with some rules and multivalue=True
+ pass1 = DummyRule()
+ pass2 = DummyRule()
+ fail = DummyRule(u'this one is not good')
+ o = Example('example', pass1, pass2, multivalue=True)
+ assert o.multivalue is True
+ assert o.validate((3, 9), 'cli') is None
+ assert pass1.calls == [
+ (text.ugettext, 3),
+ (text.ugettext, 9),
+ ]
+ assert pass2.calls == [
+ (text.ugettext, 3),
+ (text.ugettext, 9),
+ ]
+ pass1.reset()
+ pass2.reset()
+ o = Example('multi_example', pass1, pass2, fail, multivalue=True)
+ assert o.multivalue is True
+ e = raises(errors.ValidationError, o.validate, (3, 9), 'cli')
+ assert e.name == 'multi_example'
+ assert e.error == u'this one is not good'
+ assert e.index == 0
+ assert pass1.calls == [(text.ugettext, 3)]
+ assert pass2.calls == [(text.ugettext, 3)]
+ assert fail.calls == [(text.ugettext, 3)]
+
+ def test_validate_scalar(self):
+ """
+ Test the `ipalib.parameters.Param._validate_scalar` method.
+ """
+ class MyParam(self.cls):
+ type = bool
+ okay = DummyRule()
+ o = MyParam('my_param', okay)
+
+ # Test that TypeError is appropriately raised:
+ e = raises(TypeError, o._validate_scalar, 0)
+ assert str(e) == TYPE_ERROR % ('my_param', bool, 0, int)
+ e = raises(TypeError, o._validate_scalar, 'Hi', index=4)
+ assert str(e) == TYPE_ERROR % ('my_param', bool, 'Hi', str)
+ e = raises(TypeError, o._validate_scalar, True, index=3.0)
+ assert str(e) == TYPE_ERROR % ('index', int, 3.0, float)
+
+ # Test with passing rule:
+ assert o._validate_scalar(True, index=None) is None
+ assert o._validate_scalar(False, index=None) is None
+ assert okay.calls == [
+ (text.ugettext, True),
+ (text.ugettext, False),
+ ]
+
+ # Test with a failing rule:
+ okay = DummyRule()
+ fail = DummyRule(u'this describes the error')
+ o = MyParam('my_param', okay, fail)
+ e = raises(errors.ValidationError, o._validate_scalar, True)
+ assert e.name == 'my_param'
+ assert e.error == u'this describes the error'
+ assert e.index is None
+ e = raises(errors.ValidationError, o._validate_scalar, False, index=2)
+ assert e.name == 'my_param'
+ assert e.error == u'this describes the error'
+ assert e.index == 2
+ assert okay.calls == [
+ (text.ugettext, True),
+ (text.ugettext, False),
+ ]
+ assert fail.calls == [
+ (text.ugettext, True),
+ (text.ugettext, False),
+ ]
+
+ def test_get_default(self):
+ """
+ Test the `ipalib.parameters.Param.get_default` method.
+ """
+ class PassThrough(object):
+ value = None
+
+ def __call__(self, value):
+ assert self.value is None
+ assert value is not None
+ self.value = value
+ return value
+
+ def reset(self):
+ assert self.value is not None
+ self.value = None
+
+ class Str(self.cls):
+ type = unicode
+
+ def __init__(self, name, **kw):
+ self._convert_scalar = PassThrough()
+ super(Str, self).__init__(name, **kw)
+
+ # Test with only a static default:
+ o = Str('my_str',
+ normalizer=PassThrough(),
+ default=u'Static Default',
+ )
+ assert_equal(o.get_default(), u'Static Default')
+ assert o._convert_scalar.value is None
+ assert o.normalizer.value is None
+
+ # Test with default_from:
+ o = Str('my_str',
+ normalizer=PassThrough(),
+ default=u'Static Default',
+ default_from=lambda first, last: first[0] + last,
+ )
+ assert_equal(o.get_default(), u'Static Default')
+ assert o._convert_scalar.value is None
+ assert o.normalizer.value is None
+ default = o.get_default(first=u'john', last='doe')
+ assert_equal(default, u'jdoe')
+ assert o._convert_scalar.value is default
+ assert o.normalizer.value is default
+
+
+class test_Flag(ClassChecker):
+ """
+ Test the `ipalib.parameters.Flag` class.
+ """
+ _cls = parameters.Flag
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Flag.__init__` method.
+ """
+ # Test with no kwargs:
+ o = self.cls('my_flag')
+ assert o.type is bool
+ assert isinstance(o, parameters.Bool)
+ assert o.autofill is True
+ assert o.default is False
+
+ # Test that TypeError is raise if default is not a bool:
+ e = raises(TypeError, self.cls, 'my_flag', default=None)
+ assert str(e) == TYPE_ERROR % ('default', bool, None, NoneType)
+
+ # Test with autofill=False, default=True
+ o = self.cls('my_flag', autofill=False, default=True)
+ assert o.autofill is True
+ assert o.default is True
+
+ # Test when cloning:
+ orig = self.cls('my_flag')
+ for clone in [orig.clone(), orig.clone(autofill=False)]:
+ assert clone.autofill is True
+ assert clone.default is False
+ assert clone is not orig
+ assert type(clone) is self.cls
+
+ # Test when cloning with default=True/False
+ orig = self.cls('my_flag')
+ assert orig.clone().default is False
+ assert orig.clone(default=True).default is True
+ orig = self.cls('my_flag', default=True)
+ assert orig.clone().default is True
+ assert orig.clone(default=False).default is False
+
+
+class test_Data(ClassChecker):
+ """
+ Test the `ipalib.parameters.Data` class.
+ """
+ _cls = parameters.Data
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Data.__init__` method.
+ """
+ o = self.cls('my_data')
+ assert o.type is NoneType
+ assert o.password is False
+ assert o.rules == tuple()
+ assert o.class_rules == tuple()
+ assert o.all_rules == tuple()
+ assert o.minlength is None
+ assert o.maxlength is None
+ assert o.length is None
+ assert o.pattern is None
+
+ # Test mixing length with minlength or maxlength:
+ o = self.cls('my_data', length=5)
+ assert o.length == 5
+ permutations = [
+ dict(minlength=3),
+ dict(maxlength=7),
+ dict(minlength=3, maxlength=7),
+ ]
+ for kw in permutations:
+ o = self.cls('my_data', **kw)
+ for (key, value) in kw.iteritems():
+ assert getattr(o, key) == value
+ e = raises(ValueError, self.cls, 'my_data', length=5, **kw)
+ assert str(e) == \
+ "Data('my_data'): cannot mix length with minlength or maxlength"
+
+ # Test when minlength or maxlength are less than 1:
+ e = raises(ValueError, self.cls, 'my_data', minlength=0)
+ assert str(e) == "Data('my_data'): minlength must be >= 1; got 0"
+ e = raises(ValueError, self.cls, 'my_data', maxlength=0)
+ assert str(e) == "Data('my_data'): maxlength must be >= 1; got 0"
+
+ # Test when minlength > maxlength:
+ e = raises(ValueError, self.cls, 'my_data', minlength=22, maxlength=15)
+ assert str(e) == \
+ "Data('my_data'): minlength > maxlength (minlength=22, maxlength=15)"
+
+ # Test when minlength == maxlength
+ e = raises(ValueError, self.cls, 'my_data', minlength=7, maxlength=7)
+ assert str(e) == \
+ "Data('my_data'): minlength == maxlength; use length=7 instead"
+
+
+class test_Bytes(ClassChecker):
+ """
+ Test the `ipalib.parameters.Bytes` class.
+ """
+ _cls = parameters.Bytes
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Bytes.__init__` method.
+ """
+ o = self.cls('my_bytes')
+ assert o.type is str
+ assert o.password is False
+ assert o.rules == tuple()
+ assert o.class_rules == tuple()
+ assert o.all_rules == tuple()
+ assert o.minlength is None
+ assert o.maxlength is None
+ assert o.length is None
+ assert o.pattern is None
+ assert o.re is None
+
+ # Test mixing length with minlength or maxlength:
+ o = self.cls('my_bytes', length=5)
+ assert o.length == 5
+ assert len(o.class_rules) == 1
+ assert len(o.rules) == 0
+ assert len(o.all_rules) == 1
+ permutations = [
+ dict(minlength=3),
+ dict(maxlength=7),
+ dict(minlength=3, maxlength=7),
+ ]
+ for kw in permutations:
+ o = self.cls('my_bytes', **kw)
+ assert len(o.class_rules) == len(kw)
+ assert len(o.rules) == 0
+ assert len(o.all_rules) == len(kw)
+ for (key, value) in kw.iteritems():
+ assert getattr(o, key) == value
+ e = raises(ValueError, self.cls, 'my_bytes', length=5, **kw)
+ assert str(e) == \
+ "Bytes('my_bytes'): cannot mix length with minlength or maxlength"
+
+ # Test when minlength or maxlength are less than 1:
+ e = raises(ValueError, self.cls, 'my_bytes', minlength=0)
+ assert str(e) == "Bytes('my_bytes'): minlength must be >= 1; got 0"
+ e = raises(ValueError, self.cls, 'my_bytes', maxlength=0)
+ assert str(e) == "Bytes('my_bytes'): maxlength must be >= 1; got 0"
+
+ # Test when minlength > maxlength:
+ e = raises(ValueError, self.cls, 'my_bytes', minlength=22, maxlength=15)
+ assert str(e) == \
+ "Bytes('my_bytes'): minlength > maxlength (minlength=22, maxlength=15)"
+
+ # Test when minlength == maxlength
+ e = raises(ValueError, self.cls, 'my_bytes', minlength=7, maxlength=7)
+ assert str(e) == \
+ "Bytes('my_bytes'): minlength == maxlength; use length=7 instead"
+
+ def test_rule_minlength(self):
+ """
+ Test the `ipalib.parameters.Bytes._rule_minlength` method.
+ """
+ o = self.cls('my_bytes', minlength=3)
+ assert o.minlength == 3
+ rule = o._rule_minlength
+ translation = u'minlength=%(minlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in ('abc', 'four', '12345'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in ('', 'a', '12'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(minlength=3)
+ )
+ assert dummy.message == 'must be at least %(minlength)d bytes'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_maxlength(self):
+ """
+ Test the `ipalib.parameters.Bytes._rule_maxlength` method.
+ """
+ o = self.cls('my_bytes', maxlength=4)
+ assert o.maxlength == 4
+ rule = o._rule_maxlength
+ translation = u'maxlength=%(maxlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in ('ab', '123', 'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in ('12345', 'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(maxlength=4)
+ )
+ assert dummy.message == 'can be at most %(maxlength)d bytes'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_length(self):
+ """
+ Test the `ipalib.parameters.Bytes._rule_length` method.
+ """
+ o = self.cls('my_bytes', length=4)
+ assert o.length == 4
+ rule = o._rule_length
+ translation = u'length=%(length)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in ('1234', 'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in ('ab', '123', '12345', 'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(length=4),
+ )
+ assert dummy.message == 'must be exactly %(length)d bytes'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_pattern(self):
+ """
+ Test the `ipalib.parameters.Bytes._rule_pattern` method.
+ """
+ # Test our assumptions about Python re module and Unicode:
+ pat = '\w+$'
+ r = re.compile(pat)
+ assert r.match('Hello_World') is not None
+ assert r.match(utf8_bytes) is None
+ assert r.match(binary_bytes) is None
+
+ # Create instance:
+ o = self.cls('my_bytes', pattern=pat)
+ assert o.pattern is pat
+ rule = o._rule_pattern
+ translation = u'pattern=%(pattern)r'
+ dummy = dummy_ugettext(translation)
+
+ # Test with passing values:
+ for value in ('HELLO', 'hello', 'Hello_World'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in ('Hello!', 'Hello World', utf8_bytes, binary_bytes):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(pattern=pat),
+ )
+ assert_equal(dummy.message, 'must match pattern "%(pattern)s"')
+ assert dummy.called() is True
+ dummy.reset()
+
+
+class test_Str(ClassChecker):
+ """
+ Test the `ipalib.parameters.Str` class.
+ """
+ _cls = parameters.Str
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Str.__init__` method.
+ """
+ o = self.cls('my_str')
+ assert o.type is unicode
+ assert o.password is False
+ assert o.minlength is None
+ assert o.maxlength is None
+ assert o.length is None
+ assert o.pattern is None
+
+ def test_convert_scalar(self):
+ """
+ Test the `ipalib.parameters.Str._convert_scalar` method.
+ """
+ o = self.cls('my_str')
+ mthd = o._convert_scalar
+ for value in (u'Hello', 42, 1.2, unicode_str):
+ assert mthd(value) == unicode(value)
+ bad = [True, 'Hello', dict(one=1), utf8_bytes]
+ for value in bad:
+ e = raises(errors.ConversionError, mthd, value)
+ assert e.name == 'my_str'
+ assert e.index is None
+ assert_equal(unicode(e.error), u'must be Unicode text')
+ e = raises(errors.ConversionError, mthd, value, index=18)
+ assert e.name == 'my_str'
+ assert e.index == 18
+ assert_equal(unicode(e.error), u'must be Unicode text')
+ bad = [(u'Hello',), [42.3]]
+ for value in bad:
+ e = raises(errors.ConversionError, mthd, value)
+ assert e.name == 'my_str'
+ assert e.index is None
+ assert_equal(unicode(e.error), u'Only one value is allowed')
+ assert o.convert(None) is None
+
+ def test_rule_minlength(self):
+ """
+ Test the `ipalib.parameters.Str._rule_minlength` method.
+ """
+ o = self.cls('my_str', minlength=3)
+ assert o.minlength == 3
+ rule = o._rule_minlength
+ translation = u'minlength=%(minlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (u'abc', u'four', u'12345'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (u'', u'a', u'12'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(minlength=3)
+ )
+ assert dummy.message == 'must be at least %(minlength)d characters'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_maxlength(self):
+ """
+ Test the `ipalib.parameters.Str._rule_maxlength` method.
+ """
+ o = self.cls('my_str', maxlength=4)
+ assert o.maxlength == 4
+ rule = o._rule_maxlength
+ translation = u'maxlength=%(maxlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (u'ab', u'123', u'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (u'12345', u'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(maxlength=4)
+ )
+ assert dummy.message == 'can be at most %(maxlength)d characters'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_length(self):
+ """
+ Test the `ipalib.parameters.Str._rule_length` method.
+ """
+ o = self.cls('my_str', length=4)
+ assert o.length == 4
+ rule = o._rule_length
+ translation = u'length=%(length)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (u'1234', u'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (u'ab', u'123', u'12345', u'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(length=4),
+ )
+ assert dummy.message == 'must be exactly %(length)d characters'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_pattern(self):
+ """
+ Test the `ipalib.parameters.Str._rule_pattern` method.
+ """
+ # Test our assumptions about Python re module and Unicode:
+ pat = '\w{5}$'
+ r1 = re.compile(pat)
+ r2 = re.compile(pat, re.UNICODE)
+ assert r1.match(unicode_str) is None
+ assert r2.match(unicode_str) is not None
+
+ # Create instance:
+ o = self.cls('my_str', pattern=pat)
+ assert o.pattern is pat
+ rule = o._rule_pattern
+ translation = u'pattern=%(pattern)r'
+ dummy = dummy_ugettext(translation)
+
+ # Test with passing values:
+ for value in (u'HELLO', u'hello', unicode_str):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (u'H LLO', u'***lo', unicode_str + unicode_str):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(pattern=pat),
+ )
+ assert_equal(dummy.message, 'must match pattern "%(pattern)s"')
+ assert dummy.called() is True
+ dummy.reset()
+
+
+class test_Password(ClassChecker):
+ """
+ Test the `ipalib.parameters.Password` class.
+ """
+ _cls = parameters.Password
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Password.__init__` method.
+ """
+ o = self.cls('my_password')
+ assert o.type is unicode
+ assert o.minlength is None
+ assert o.maxlength is None
+ assert o.length is None
+ assert o.pattern is None
+ assert o.password is True
+
+ def test_convert_scalar(self):
+ """
+ Test the `ipalib.parameters.Password._convert_scalar` method.
+ """
+ o = self.cls('my_password')
+ e = raises(errors.PasswordMismatch, o._convert_scalar, [u'one', u'two'])
+ assert e.name == 'my_password'
+ assert e.index is None
+ assert o._convert_scalar([u'one', u'one']) == u'one'
+ assert o._convert_scalar(u'one') == u'one'
+
+
+class test_StrEnum(ClassChecker):
+ """
+ Test the `ipalib.parameters.StrEnum` class.
+ """
+ _cls = parameters.StrEnum
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.StrEnum.__init__` method.
+ """
+ values = (u'Hello', u'naughty', u'nurse!')
+ o = self.cls('my_strenum', values=values)
+ assert o.type is unicode
+ assert o.values is values
+ assert o.class_rules == (o._rule_values,)
+ assert o.rules == tuple()
+ assert o.all_rules == (o._rule_values,)
+
+ badvalues = (u'Hello', 'naughty', u'nurse!')
+ e = raises(TypeError, self.cls, 'my_enum', values=badvalues)
+ assert str(e) == TYPE_ERROR % (
+ "StrEnum('my_enum') values[1]", unicode, 'naughty', str
+ )
+
+ # Test that ValueError is raised when list of values is empty
+ badvalues = tuple()
+ e = raises(ValueError, self.cls, 'empty_enum', values=badvalues)
+ assert_equal(str(e), "StrEnum('empty_enum'): list of values must not "
+ "be empty")
+
+ def test_rules_values(self):
+ """
+ Test the `ipalib.parameters.StrEnum._rule_values` method.
+ """
+ values = (u'Hello', u'naughty', u'nurse!')
+ o = self.cls('my_enum', values=values)
+ rule = o._rule_values
+ translation = u"values='Hello', 'naughty', 'nurse!'"
+ dummy = dummy_ugettext(translation)
+
+ # Test with passing values:
+ for v in values:
+ assert rule(dummy, v) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for val in (u'Howdy', u'quiet', u'library!'):
+ assert_equal(
+ rule(dummy, val),
+ translation % dict(values=values),
+ )
+ assert_equal(dummy.message, "must be one of %(values)s")
+ dummy.reset()
+
+ # test a special case when we have just one allowed value
+ values = (u'Hello', )
+ o = self.cls('my_enum', values=values)
+ rule = o._rule_values
+ translation = u"value='Hello'"
+ dummy = dummy_ugettext(translation)
+
+ for val in (u'Howdy', u'quiet', u'library!'):
+ assert_equal(
+ rule(dummy, val),
+ translation % dict(values=values),
+ )
+ assert_equal(dummy.message, "must be '%(value)s'")
+ dummy.reset()
+
+
+class test_Number(ClassChecker):
+ """
+ Test the `ipalib.parameters.Number` class.
+ """
+ _cls = parameters.Number
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Number.__init__` method.
+ """
+ o = self.cls('my_number')
+ assert o.type is NoneType
+ assert o.password is False
+ assert o.rules == tuple()
+ assert o.class_rules == tuple()
+ assert o.all_rules == tuple()
+
+
+
+class test_Int(ClassChecker):
+ """
+ Test the `ipalib.parameters.Int` class.
+ """
+ _cls = parameters.Int
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Int.__init__` method.
+ """
+ # Test with no kwargs:
+ o = self.cls('my_number')
+ assert o.type is int
+ assert isinstance(o, parameters.Int)
+ assert o.minvalue == int(MININT)
+ assert o.maxvalue == int(MAXINT)
+
+ # Test when min > max:
+ e = raises(ValueError, self.cls, 'my_number', minvalue=22, maxvalue=15)
+ assert str(e) == \
+ "Int('my_number'): minvalue > maxvalue (minvalue=22, maxvalue=15)"
+
+ def test_rule_minvalue(self):
+ """
+ Test the `ipalib.parameters.Int._rule_minvalue` method.
+ """
+ o = self.cls('my_number', minvalue=3)
+ assert o.minvalue == 3
+ rule = o._rule_minvalue
+ translation = u'minvalue=%(minvalue)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (4, 99, 1001):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (-1, 0, 2):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(minvalue=3)
+ )
+ assert dummy.message == 'must be at least %(minvalue)d'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_maxvalue(self):
+ """
+ Test the `ipalib.parameters.Int._rule_maxvalue` method.
+ """
+ o = self.cls('my_number', maxvalue=4)
+ assert o.maxvalue == 4
+ rule = o._rule_maxvalue
+ translation = u'maxvalue=%(maxvalue)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (-1, 0, 4):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (5, 99, 1009):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(maxvalue=4)
+ )
+ assert dummy.message == 'can be at most %(maxvalue)d'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_convert_scalar(self):
+ """
+ Test the `ipalib.parameters.Int._convert_scalar` method.
+ Assure radix prefixes work, str objects fail,
+ floats (native & string) are truncated,
+ large magnitude values are promoted to long,
+ empty strings & invalid numerical representations fail
+ """
+ o = self.cls('my_number')
+ # Assure invalid inputs raise error
+ for bad in ['hello', u'hello', True, None, u'', u'.']:
+ e = raises(errors.ConversionError, o._convert_scalar, bad)
+ assert e.name == 'my_number'
+ assert e.index is None
+ # Assure large magnatude values are handled correctly
+ assert type(o._convert_scalar(sys.maxint*2)) == long
+ assert o._convert_scalar(sys.maxint*2) == sys.maxint*2
+ assert o._convert_scalar(unicode(sys.maxint*2)) == sys.maxint*2
+ assert o._convert_scalar(long(16)) == 16
+ # Assure normal conversions produce expected result
+ assert o._convert_scalar(u'16.99') == 16
+ assert o._convert_scalar(16.99) == 16
+ assert o._convert_scalar(u'16') == 16
+ assert o._convert_scalar(u'0x10') == 16
+ assert o._convert_scalar(u'020') == 16
+
+class test_Decimal(ClassChecker):
+ """
+ Test the `ipalib.parameters.Decimal` class.
+ """
+ _cls = parameters.Decimal
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Decimal.__init__` method.
+ """
+ # Test with no kwargs:
+ o = self.cls('my_number')
+ assert o.type is Decimal
+ assert isinstance(o, parameters.Decimal)
+ assert o.minvalue is None
+ assert o.maxvalue is None
+
+ # Test when min > max:
+ e = raises(ValueError, self.cls, 'my_number', minvalue=Decimal('22.5'), maxvalue=Decimal('15.1'))
+ assert str(e) == \
+ "Decimal('my_number'): minvalue > maxvalue (minvalue=22.5, maxvalue=15.1)"
+
+ def test_rule_minvalue(self):
+ """
+ Test the `ipalib.parameters.Decimal._rule_minvalue` method.
+ """
+ o = self.cls('my_number', minvalue='3.1')
+ assert o.minvalue == Decimal('3.1')
+ rule = o._rule_minvalue
+ translation = u'minvalue=%(minvalue)s'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (Decimal('3.2'), Decimal('99.0')):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (Decimal('-1.2'), Decimal('0.0'), Decimal('3.0')):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(minvalue=Decimal('3.1'))
+ )
+ assert dummy.message == 'must be at least %(minvalue)s'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_maxvalue(self):
+ """
+ Test the `ipalib.parameters.Decimal._rule_maxvalue` method.
+ """
+ o = self.cls('my_number', maxvalue='4.7')
+ assert o.maxvalue == Decimal('4.7')
+ rule = o._rule_maxvalue
+ translation = u'maxvalue=%(maxvalue)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (Decimal('-1.0'), Decimal('0.1'), Decimal('4.2')):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (Decimal('5.3'), Decimal('99.9')):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(maxvalue=Decimal('4.7'))
+ )
+ assert dummy.message == 'can be at most %(maxvalue)s'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_precision(self):
+ """
+ Test the `ipalib.parameters.Decimal` precision attribute
+ """
+ # precission is None
+ param = self.cls('my_number')
+
+ for value in (Decimal('0'), Decimal('4.4'), Decimal('4.67')):
+ assert_equal(
+ param(value),
+ value)
+
+ # precision is 0
+ param = self.cls('my_number', precision=0)
+ for original,expected in ((Decimal('0'), '0'),
+ (Decimal('1.1'), '1'),
+ (Decimal('4.67'), '5')):
+ assert_equal(
+ str(param(original)),
+ expected)
+
+ # precision is 1
+ param = self.cls('my_number', precision=1)
+ for original,expected in ((Decimal('0'), '0.0'),
+ (Decimal('1.1'), '1.1'),
+ (Decimal('4.67'), '4.7')):
+ assert_equal(
+ str(param(original)),
+ expected)
+
+ # value has too many digits
+ param = self.cls('my_number', precision=1)
+ e = raises(ConversionError, param, '123456789012345678901234567890')
+
+ assert str(e) == \
+ "invalid 'my_number': quantize result has too many digits for current context"
+
+ def test_exponential(self):
+ """
+ Test the `ipalib.parameters.Decimal` exponential attribute
+ """
+ param = self.cls('my_number', exponential=True)
+ for original,expected in ((Decimal('0'), '0'),
+ (Decimal('1E3'), '1E+3'),
+ (Decimal('3.4E2'), '3.4E+2')):
+ assert_equal(
+ str(param(original)),
+ expected)
+
+
+ param = self.cls('my_number', exponential=False)
+ for original,expected in ((Decimal('0'), '0'),
+ (Decimal('1E3'), '1000'),
+ (Decimal('3.4E2'), '340')):
+ assert_equal(
+ str(param(original)),
+ expected)
+
+ def test_numberclass(self):
+ """
+ Test the `ipalib.parameters.Decimal` numberclass attribute
+ """
+ # test default value: '-Normal', '+Zero', '+Normal'
+ param = self.cls('my_number')
+ for value,raises_verror in ((Decimal('0'), False),
+ (Decimal('-0'), True),
+ (Decimal('1E8'), False),
+ (Decimal('-1.1'), False),
+ (Decimal('-Infinity'), True),
+ (Decimal('+Infinity'), True),
+ (Decimal('NaN'), True)):
+ if raises_verror:
+ raises(ValidationError, param, value)
+ else:
+ param(value)
+
+
+ param = self.cls('my_number', exponential=True,
+ numberclass=('-Normal', '+Zero', '+Infinity'))
+ for value,raises_verror in ((Decimal('0'), False),
+ (Decimal('-0'), True),
+ (Decimal('1E8'), True),
+ (Decimal('-1.1'), False),
+ (Decimal('-Infinity'), True),
+ (Decimal('+Infinity'), False),
+ (Decimal('NaN'), True)):
+ if raises_verror:
+ raises(ValidationError, param, value)
+ else:
+ param(value)
+
+class test_AccessTime(ClassChecker):
+ """
+ Test the `ipalib.parameters.AccessTime` class.
+ """
+ _cls = parameters.AccessTime
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.AccessTime.__init__` method.
+ """
+ # Test with no kwargs:
+ o = self.cls('my_time')
+ assert o.type is unicode
+ assert isinstance(o, parameters.AccessTime)
+ assert o.multivalue is False
+ translation = u'length=%(length)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+ rule = o._rule_required
+
+ # Check some good rules
+ for value in (u'absolute 201012161032 ~ 201012161033',
+ u'periodic monthly week 2 day Sat,Sun 0900-1300',
+ u'periodic yearly month 4 day 1-31 0800-1400',
+ u'periodic weekly day 7 0800-1400',
+ u'periodic daily 0800-1400',
+ ):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # And some bad ones
+ for value in (u'absolute 201012161032 - 201012161033',
+ u'absolute 201012161032 ~',
+ u'periodic monthly day Sat,Sun 0900-1300',
+ u'periodical yearly month 4 day 1-31 0800-1400',
+ u'periodic weekly day 8 0800-1400',
+ ):
+ e = raises(ValidationError, o._rule_required, None, value)
+
+def test_create_param():
+ """
+ Test the `ipalib.parameters.create_param` function.
+ """
+ f = parameters.create_param
+
+ # Test that Param instances are returned unchanged:
+ params = (
+ parameters.Param('one?'),
+ parameters.Int('two+'),
+ parameters.Str('three*'),
+ parameters.Bytes('four'),
+ )
+ for p in params:
+ assert f(p) is p
+
+ # Test that the spec creates an Str instance:
+ for spec in ('one?', 'two+', 'three*', 'four'):
+ (name, kw) = parameters.parse_param_spec(spec)
+ p = f(spec)
+ assert p.param_spec is spec
+ assert p.name == name
+ assert p.required is kw['required']
+ assert p.multivalue is kw['multivalue']
+
+ # Test that TypeError is raised when spec is neither a Param nor a str:
+ for spec in (u'one', 42, parameters.Param, parameters.Str):
+ e = raises(TypeError, f, spec)
+ assert str(e) == \
+ TYPE_ERROR % ('spec', (str, parameters.Param), spec, type(spec))
+
+
+def test_messages():
+ """
+ Test module level message in `ipalib.parameters`.
+ """
+ for name in dir(parameters):
+ if name.startswith('_'):
+ continue
+ attr = getattr(parameters, name)
+ if not (isclass(attr) and issubclass(attr, parameters.Param)):
+ continue
+ assert type(attr.type_error) is str
+ assert attr.type_error in parameters.__messages
+
+
+class test_IA5Str(ClassChecker):
+ """
+ Test the `ipalib.parameters.IA5Str` class.
+ """
+ _cls = parameters.IA5Str
+
+ def test_convert_scalar(self):
+ """
+ Test the `ipalib.parameters.IA5Str._convert_scalar` method.
+ """
+ o = self.cls('my_str')
+ mthd = o._convert_scalar
+ for value in (u'Hello', 42, 1.2):
+ assert mthd(value) == unicode(value)
+ bad = ['Helloá']
+ for value in bad:
+ e = raises(errors.ConversionError, mthd, value)
+ assert e.name == 'my_str'
+ assert e.index is None
+ assert_equal(e.error, "The character '\\xc3' is not allowed.")
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
new file mode 100644
index 000000000..c495e74dc
--- /dev/null
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -0,0 +1,516 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.plugable` module.
+"""
+
+import inspect
+from ipatests.util import raises, no_set, no_del, read_only
+from ipatests.util import getitem, setitem, delitem
+from ipatests.util import ClassChecker, create_test_api
+from ipalib import plugable, errors, text
+
+
+class test_SetProxy(ClassChecker):
+ """
+ Test the `ipalib.plugable.SetProxy` class.
+ """
+ _cls = plugable.SetProxy
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.SetProxy` class.
+ """
+ assert self.cls.__bases__ == (plugable.ReadOnly,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.plugable.SetProxy.__init__` method.
+ """
+ okay = (set, frozenset, dict)
+ fail = (list, tuple)
+ for t in okay:
+ self.cls(t())
+ raises(TypeError, self.cls, t)
+ for t in fail:
+ raises(TypeError, self.cls, t())
+ raises(TypeError, self.cls, t)
+
+ def test_SetProxy(self):
+ """
+ Test container emulation of `ipalib.plugable.SetProxy` class.
+ """
+ def get_key(i):
+ return 'key_%d' % i
+
+ cnt = 10
+ target = set()
+ proxy = self.cls(target)
+ for i in xrange(cnt):
+ key = get_key(i)
+
+ # Check initial state
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert key not in proxy
+ assert key not in target
+
+ # Add and test again
+ target.add(key)
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert key in proxy
+ assert key in target
+
+
+class test_DictProxy(ClassChecker):
+ """
+ Test the `ipalib.plugable.DictProxy` class.
+ """
+ _cls = plugable.DictProxy
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.DictProxy` class.
+ """
+ assert self.cls.__bases__ == (plugable.SetProxy,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.plugable.DictProxy.__init__` method.
+ """
+ self.cls(dict())
+ raises(TypeError, self.cls, dict)
+ fail = (set, frozenset, list, tuple)
+ for t in fail:
+ raises(TypeError, self.cls, t())
+ raises(TypeError, self.cls, t)
+
+ def test_DictProxy(self):
+ """
+ Test container emulation of `ipalib.plugable.DictProxy` class.
+ """
+ def get_kv(i):
+ return (
+ 'key_%d' % i,
+ 'val_%d' % i,
+ )
+ cnt = 10
+ target = dict()
+ proxy = self.cls(target)
+ for i in xrange(cnt):
+ (key, val) = get_kv(i)
+
+ # Check initial state
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert list(proxy()) == [target[k] for k in sorted(target)]
+ assert key not in proxy
+ raises(KeyError, getitem, proxy, key)
+
+ # Add and test again
+ target[key] = val
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert list(proxy()) == [target[k] for k in sorted(target)]
+
+ # Verify TypeError is raised trying to set/del via proxy
+ raises(TypeError, setitem, proxy, key, val)
+ raises(TypeError, delitem, proxy, key)
+
+
+class test_MagicDict(ClassChecker):
+ """
+ Test the `ipalib.plugable.MagicDict` class.
+ """
+ _cls = plugable.MagicDict
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.MagicDict` class.
+ """
+ assert self.cls.__bases__ == (plugable.DictProxy,)
+ for non_dict in ('hello', 69, object):
+ raises(TypeError, self.cls, non_dict)
+
+ def test_MagicDict(self):
+ """
+ Test container emulation of `ipalib.plugable.MagicDict` class.
+ """
+ cnt = 10
+ keys = []
+ d = dict()
+ dictproxy = self.cls(d)
+ for i in xrange(cnt):
+ key = 'key_%d' % i
+ val = 'val_%d' % i
+ keys.append(key)
+
+ # Test thet key does not yet exist
+ assert len(dictproxy) == i
+ assert key not in dictproxy
+ assert not hasattr(dictproxy, key)
+ raises(KeyError, getitem, dictproxy, key)
+ raises(AttributeError, getattr, dictproxy, key)
+
+ # Test that items/attributes cannot be set on dictproxy:
+ raises(TypeError, setitem, dictproxy, key, val)
+ raises(AttributeError, setattr, dictproxy, key, val)
+
+ # Test that additions in d are reflected in dictproxy:
+ d[key] = val
+ assert len(dictproxy) == i + 1
+ assert key in dictproxy
+ assert hasattr(dictproxy, key)
+ assert dictproxy[key] is val
+ assert read_only(dictproxy, key) is val
+
+ # Test __iter__
+ assert list(dictproxy) == keys
+
+ for key in keys:
+ # Test that items cannot be deleted through dictproxy:
+ raises(TypeError, delitem, dictproxy, key)
+ raises(AttributeError, delattr, dictproxy, key)
+
+ # Test that deletions in d are reflected in dictproxy
+ del d[key]
+ assert len(dictproxy) == len(d)
+ assert key not in dictproxy
+ raises(KeyError, getitem, dictproxy, key)
+ raises(AttributeError, getattr, dictproxy, key)
+
+
+class test_Plugin(ClassChecker):
+ """
+ Test the `ipalib.plugable.Plugin` class.
+ """
+ _cls = plugable.Plugin
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.Plugin` class.
+ """
+ assert self.cls.__bases__ == (plugable.ReadOnly,)
+ assert type(self.cls.api) is property
+
+ def test_init(self):
+ """
+ Test the `ipalib.plugable.Plugin.__init__` method.
+ """
+ o = self.cls()
+ assert o.name == 'Plugin'
+ assert o.module == 'ipalib.plugable'
+ assert o.fullname == 'ipalib.plugable.Plugin'
+ assert isinstance(o.doc, text.Gettext)
+ class some_subclass(self.cls):
+ """
+ Do sub-classy things.
+
+ Although it doesn't know how to comport itself and is not for mixed
+ company, this class *is* useful as we all need a little sub-class
+ now and then.
+
+ One more paragraph.
+ """
+ o = some_subclass()
+ assert o.name == 'some_subclass'
+ assert o.module == __name__
+ assert o.fullname == '%s.some_subclass' % __name__
+ assert o.summary == 'Do sub-classy things.'
+ assert isinstance(o.doc, text.Gettext)
+ class another_subclass(self.cls):
+ pass
+ o = another_subclass()
+ assert o.summary == '<%s>' % o.fullname
+
+ # Test that Plugin makes sure the subclass hasn't defined attributes
+ # whose names conflict with the logger methods set in Plugin.__init__():
+ class check(self.cls):
+ info = 'whatever'
+ e = raises(StandardError, check)
+ assert str(e) == \
+ "info is already bound to ipatests.test_ipalib.test_plugable.check()"
+
+ def test_set_api(self):
+ """
+ Test the `ipalib.plugable.Plugin.set_api` method.
+ """
+ api = 'the api instance'
+ o = self.cls()
+ assert o.api is None
+ e = raises(AssertionError, o.set_api, None)
+ assert str(e) == 'set_api() argument cannot be None'
+ o.set_api(api)
+ assert o.api is api
+ e = raises(AssertionError, o.set_api, api)
+ assert str(e) == 'set_api() can only be called once'
+
+ def test_finalize(self):
+ """
+ Test the `ipalib.plugable.Plugin.finalize` method.
+ """
+ o = self.cls()
+ assert not o.__islocked__()
+ o.finalize()
+ assert o.__islocked__()
+
+ def test_call(self):
+ """
+ Test the `ipalib.plugable.Plugin.call` method.
+ """
+ o = self.cls()
+ o.call('/bin/true') is None
+ e = raises(errors.SubprocessError, o.call, '/bin/false')
+ assert e.returncode == 1
+ assert e.argv == ('/bin/false',)
+
+
+def test_Registrar():
+ """
+ Test the `ipalib.plugable.Registrar` class
+ """
+ class Base1(object):
+ pass
+ class Base2(object):
+ pass
+ class Base3(object):
+ pass
+ class plugin1(Base1):
+ pass
+ class plugin2(Base2):
+ pass
+ class plugin3(Base3):
+ pass
+
+ # Test creation of Registrar:
+ r = plugable.Registrar(Base1, Base2)
+
+ # Test __iter__:
+ assert list(r) == ['Base1', 'Base2']
+
+ # Test __hasitem__, __getitem__:
+ for base in [Base1, Base2]:
+ name = base.__name__
+ assert name in r
+ assert r[name] is base
+ magic = getattr(r, name)
+ assert type(magic) is plugable.MagicDict
+ assert len(magic) == 0
+
+ # Check that TypeError is raised trying to register something that isn't
+ # a class:
+ p = plugin1()
+ e = raises(TypeError, r, p)
+ assert str(e) == 'plugin must be a class; got %r' % p
+
+ # Check that SubclassError is raised trying to register a class that is
+ # not a subclass of an allowed base:
+ e = raises(errors.PluginSubclassError, r, plugin3)
+ assert e.plugin is plugin3
+
+ # Check that registration works
+ r(plugin1)
+ assert len(r.Base1) == 1
+ assert r.Base1['plugin1'] is plugin1
+ assert r.Base1.plugin1 is plugin1
+
+ # Check that DuplicateError is raised trying to register exact class
+ # again:
+ e = raises(errors.PluginDuplicateError, r, plugin1)
+ assert e.plugin is plugin1
+
+ # Check that OverrideError is raised trying to register class with same
+ # name and same base:
+ orig1 = plugin1
+ class base1_extended(Base1):
+ pass
+ class plugin1(base1_extended):
+ pass
+ e = raises(errors.PluginOverrideError, r, plugin1)
+ assert e.base == 'Base1'
+ assert e.name == 'plugin1'
+ assert e.plugin is plugin1
+
+ # Check that overriding works
+ r(plugin1, override=True)
+ assert len(r.Base1) == 1
+ assert r.Base1.plugin1 is plugin1
+ assert r.Base1.plugin1 is not orig1
+
+ # Check that MissingOverrideError is raised trying to override a name
+ # not yet registerd:
+ e = raises(errors.PluginMissingOverrideError, r, plugin2, override=True)
+ assert e.base == 'Base2'
+ assert e.name == 'plugin2'
+ assert e.plugin is plugin2
+
+ # Test that another plugin can be registered:
+ assert len(r.Base2) == 0
+ r(plugin2)
+ assert len(r.Base2) == 1
+ assert r.Base2.plugin2 is plugin2
+
+ # Setup to test more registration:
+ class plugin1a(Base1):
+ pass
+ r(plugin1a)
+
+ class plugin1b(Base1):
+ pass
+ r(plugin1b)
+
+ class plugin2a(Base2):
+ pass
+ r(plugin2a)
+
+ class plugin2b(Base2):
+ pass
+ r(plugin2b)
+
+ # Again test __hasitem__, __getitem__:
+ for base in [Base1, Base2]:
+ name = base.__name__
+ assert name in r
+ assert r[name] is base
+ magic = getattr(r, name)
+ assert len(magic) == 3
+ for key in magic:
+ klass = magic[key]
+ assert getattr(magic, key) is klass
+ assert issubclass(klass, base)
+
+
+class test_API(ClassChecker):
+ """
+ Test the `ipalib.plugable.API` class.
+ """
+
+ _cls = plugable.API
+
+ def test_API(self):
+ """
+ Test the `ipalib.plugable.API` class.
+ """
+ assert issubclass(plugable.API, plugable.ReadOnly)
+
+ # Setup the test bases, create the API:
+ class base0(plugable.Plugin):
+ def method(self, n):
+ return n
+
+ class base1(plugable.Plugin):
+ def method(self, n):
+ return n + 1
+
+ api = plugable.API(base0, base1)
+ api.env.mode = 'unit_test'
+ api.env.in_tree = True
+ r = api.register
+ assert isinstance(r, plugable.Registrar)
+ assert read_only(api, 'register') is r
+
+ class base0_plugin0(base0):
+ pass
+ r(base0_plugin0)
+
+ class base0_plugin1(base0):
+ pass
+ r(base0_plugin1)
+
+ class base0_plugin2(base0):
+ pass
+ r(base0_plugin2)
+
+ class base1_plugin0(base1):
+ pass
+ r(base1_plugin0)
+
+ class base1_plugin1(base1):
+ pass
+ r(base1_plugin1)
+
+ class base1_plugin2(base1):
+ pass
+ r(base1_plugin2)
+
+ # Test API instance:
+ assert api.isdone('bootstrap') is False
+ assert api.isdone('finalize') is False
+ api.finalize()
+ assert api.isdone('bootstrap') is True
+ assert api.isdone('finalize') is True
+
+ def get_base_name(b):
+ return 'base%d' % b
+
+
+ def get_plugin_name(b, p):
+ return 'base%d_plugin%d' % (b, p)
+
+ for b in xrange(2):
+ base_name = get_base_name(b)
+ base = locals()[base_name]
+ ns = getattr(api, base_name)
+ assert isinstance(ns, plugable.NameSpace)
+ assert read_only(api, base_name) is ns
+ assert len(ns) == 3
+ for p in xrange(3):
+ plugin_name = get_plugin_name(b, p)
+ plugin = locals()[plugin_name]
+ inst = ns[plugin_name]
+ assert isinstance(inst, base)
+ assert isinstance(inst, plugin)
+ assert inst.name == plugin_name
+ assert read_only(ns, plugin_name) is inst
+ assert inst.method(7) == 7 + b
+
+ # Test that calling finilize again raises AssertionError:
+ e = raises(StandardError, api.finalize)
+ assert str(e) == 'API.finalize() already called', str(e)
+
+ def test_bootstrap(self):
+ """
+ Test the `ipalib.plugable.API.bootstrap` method.
+ """
+ (o, home) = create_test_api()
+ assert o.env._isdone('_bootstrap') is False
+ assert o.env._isdone('_finalize_core') is False
+ assert o.isdone('bootstrap') is False
+ o.bootstrap(my_test_override='Hello, world!')
+ assert o.isdone('bootstrap') is True
+ assert o.env._isdone('_bootstrap') is True
+ assert o.env._isdone('_finalize_core') is True
+ assert o.env.my_test_override == 'Hello, world!'
+ e = raises(StandardError, o.bootstrap)
+ assert str(e) == 'API.bootstrap() already called'
+
+ def test_load_plugins(self):
+ """
+ Test the `ipalib.plugable.API.load_plugins` method.
+ """
+ (o, home) = create_test_api()
+ assert o.isdone('bootstrap') is False
+ assert o.isdone('load_plugins') is False
+ o.load_plugins()
+ assert o.isdone('bootstrap') is True
+ assert o.isdone('load_plugins') is True
+ e = raises(StandardError, o.load_plugins)
+ assert str(e) == 'API.load_plugins() already called'
diff --git a/ipatests/test_ipalib/test_rpc.py b/ipatests/test_ipalib/test_rpc.py
new file mode 100644
index 000000000..56b8184cf
--- /dev/null
+++ b/ipatests/test_ipalib/test_rpc.py
@@ -0,0 +1,244 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.rpc` module.
+"""
+
+import threading
+from xmlrpclib import Binary, Fault, dumps, loads, ServerProxy
+from ipatests.util import raises, assert_equal, PluginTester, DummyClass
+from ipatests.data import binary_bytes, utf8_bytes, unicode_str
+from ipalib.frontend import Command
+from ipalib.request import context, Connection
+from ipalib import rpc, errors
+
+
+std_compound = (binary_bytes, utf8_bytes, unicode_str)
+
+
+def dump_n_load(value):
+ (param, method) = loads(
+ dumps((value,), allow_none=True)
+ )
+ return param[0]
+
+
+def round_trip(value):
+ return rpc.xml_unwrap(
+ dump_n_load(rpc.xml_wrap(value))
+ )
+
+
+def test_round_trip():
+ """
+ Test `ipalib.rpc.xml_wrap` and `ipalib.rpc.xml_unwrap`.
+
+ This tests the two functions together with ``xmlrpclib.dumps()`` and
+ ``xmlrpclib.loads()`` in a full wrap/dumps/loads/unwrap round trip.
+ """
+ # We first test that our assumptions about xmlrpclib module in the Python
+ # standard library are correct:
+ assert_equal(dump_n_load(utf8_bytes), unicode_str)
+ assert_equal(dump_n_load(unicode_str), unicode_str)
+ assert_equal(dump_n_load(Binary(binary_bytes)).data, binary_bytes)
+ assert isinstance(dump_n_load(Binary(binary_bytes)), Binary)
+ assert type(dump_n_load('hello')) is str
+ assert type(dump_n_load(u'hello')) is str
+ assert_equal(dump_n_load(''), '')
+ assert_equal(dump_n_load(u''), '')
+ assert dump_n_load(None) is None
+
+ # Now we test our wrap and unwrap methods in combination with dumps, loads:
+ # All str should come back str (because they get wrapped in
+ # xmlrpclib.Binary(). All unicode should come back unicode because str
+ # explicity get decoded by rpc.xml_unwrap() if they weren't already
+ # decoded by xmlrpclib.loads().
+ assert_equal(round_trip(utf8_bytes), utf8_bytes)
+ assert_equal(round_trip(unicode_str), unicode_str)
+ assert_equal(round_trip(binary_bytes), binary_bytes)
+ assert type(round_trip('hello')) is str
+ assert type(round_trip(u'hello')) is unicode
+ assert_equal(round_trip(''), '')
+ assert_equal(round_trip(u''), u'')
+ assert round_trip(None) is None
+ compound = [utf8_bytes, None, binary_bytes, (None, unicode_str),
+ dict(utf8=utf8_bytes, chars=unicode_str, data=binary_bytes)
+ ]
+ assert round_trip(compound) == tuple(compound)
+
+
+def test_xml_wrap():
+ """
+ Test the `ipalib.rpc.xml_wrap` function.
+ """
+ f = rpc.xml_wrap
+ assert f([]) == tuple()
+ assert f({}) == dict()
+ b = f('hello')
+ assert isinstance(b, Binary)
+ assert b.data == 'hello'
+ u = f(u'hello')
+ assert type(u) is unicode
+ assert u == u'hello'
+ value = f([dict(one=False, two=u'hello'), None, 'hello'])
+
+
+def test_xml_unwrap():
+ """
+ Test the `ipalib.rpc.xml_unwrap` function.
+ """
+ f = rpc.xml_unwrap
+ assert f([]) == tuple()
+ assert f({}) == dict()
+ value = f(Binary(utf8_bytes))
+ assert type(value) is str
+ assert value == utf8_bytes
+ assert f(utf8_bytes) == unicode_str
+ assert f(unicode_str) == unicode_str
+ value = f([True, Binary('hello'), dict(one=1, two=utf8_bytes, three=None)])
+ assert value == (True, 'hello', dict(one=1, two=unicode_str, three=None))
+ assert type(value[1]) is str
+ assert type(value[2]['two']) is unicode
+
+
+def test_xml_dumps():
+ """
+ Test the `ipalib.rpc.xml_dumps` function.
+ """
+ f = rpc.xml_dumps
+ params = (binary_bytes, utf8_bytes, unicode_str, None)
+
+ # Test serializing an RPC request:
+ data = f(params, 'the_method')
+ (p, m) = loads(data)
+ assert_equal(m, u'the_method')
+ assert type(p) is tuple
+ assert rpc.xml_unwrap(p) == params
+
+ # Test serializing an RPC response:
+ data = f((params,), methodresponse=True)
+ (tup, m) = loads(data)
+ assert m is None
+ assert len(tup) == 1
+ assert type(tup) is tuple
+ assert rpc.xml_unwrap(tup[0]) == params
+
+ # Test serializing an RPC response containing a Fault:
+ fault = Fault(69, unicode_str)
+ data = f(fault, methodresponse=True)
+ e = raises(Fault, loads, data)
+ assert e.faultCode == 69
+ assert_equal(e.faultString, unicode_str)
+
+
+def test_xml_loads():
+ """
+ Test the `ipalib.rpc.xml_loads` function.
+ """
+ f = rpc.xml_loads
+ params = (binary_bytes, utf8_bytes, unicode_str, None)
+ wrapped = rpc.xml_wrap(params)
+
+ # Test un-serializing an RPC request:
+ data = dumps(wrapped, 'the_method', allow_none=True)
+ (p, m) = f(data)
+ assert_equal(m, u'the_method')
+ assert_equal(p, params)
+
+ # Test un-serializing an RPC response:
+ data = dumps((wrapped,), methodresponse=True, allow_none=True)
+ (tup, m) = f(data)
+ assert m is None
+ assert len(tup) == 1
+ assert type(tup) is tuple
+ assert_equal(tup[0], params)
+
+ # Test un-serializing an RPC response containing a Fault:
+ for error in (unicode_str, u'hello'):
+ fault = Fault(69, error)
+ data = dumps(fault, methodresponse=True, allow_none=True, encoding='UTF-8')
+ e = raises(Fault, f, data)
+ assert e.faultCode == 69
+ assert_equal(e.faultString, error)
+ assert type(e.faultString) is unicode
+
+
+class test_xmlclient(PluginTester):
+ """
+ Test the `ipalib.rpc.xmlclient` plugin.
+ """
+ _plugin = rpc.xmlclient
+
+ def test_forward(self):
+ """
+ Test the `ipalib.rpc.xmlclient.forward` method.
+ """
+ class user_add(Command):
+ pass
+
+ # Test that ValueError is raised when forwarding a command that is not
+ # in api.Command:
+ (o, api, home) = self.instance('Backend', in_server=False)
+ e = raises(ValueError, o.forward, 'user_add')
+ assert str(e) == '%s.forward(): %r not in api.Command' % (
+ 'xmlclient', 'user_add'
+ )
+
+ (o, api, home) = self.instance('Backend', user_add, in_server=False)
+ args = (binary_bytes, utf8_bytes, unicode_str)
+ kw = dict(one=binary_bytes, two=utf8_bytes, three=unicode_str)
+ params = [args, kw]
+ result = (unicode_str, binary_bytes, utf8_bytes)
+ conn = DummyClass(
+ (
+ 'user_add',
+ rpc.xml_wrap(params),
+ {},
+ rpc.xml_wrap(result),
+ ),
+ (
+ 'user_add',
+ rpc.xml_wrap(params),
+ {},
+ Fault(3007, u"'four' is required"), # RequirementError
+ ),
+ (
+ 'user_add',
+ rpc.xml_wrap(params),
+ {},
+ Fault(700, u'no such error'), # There is no error 700
+ ),
+
+ )
+ context.xmlclient = Connection(conn, lambda: None)
+
+ # Test with a successful return value:
+ assert o.forward('user_add', *args, **kw) == result
+
+ # Test with an errno the client knows:
+ e = raises(errors.RequirementError, o.forward, 'user_add', *args, **kw)
+ assert_equal(e.args[0], u"'four' is required")
+
+ # Test with an errno the client doesn't know
+ e = raises(errors.UnknownError, o.forward, 'user_add', *args, **kw)
+ assert_equal(e.code, 700)
+ assert_equal(e.error, u'no such error')
+
+ assert context.xmlclient.conn._calledall() is True
diff --git a/ipatests/test_ipalib/test_text.py b/ipatests/test_ipalib/test_text.py
new file mode 100644
index 000000000..2a5ff7a36
--- /dev/null
+++ b/ipatests/test_ipalib/test_text.py
@@ -0,0 +1,334 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2009 Red Hat
+# see file 'COPYING' for use and warranty contextrmation
+#
+# 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/>.
+
+"""
+Test the `ipalib.text` module.
+"""
+
+import os
+import shutil
+import tempfile
+import re
+import nose
+import locale
+from ipatests.util import raises, assert_equal
+from ipatests.i18n import create_po, po_file_iterate
+from ipalib.request import context
+from ipalib import request
+from ipalib import text
+from ipapython.ipautil import file_exists
+
+singular = '%(count)d goose makes a %(dish)s'
+plural = '%(count)d geese make a %(dish)s'
+
+
+def test_create_translation():
+ f = text.create_translation
+ key = ('foo', None)
+ t = f(key)
+ assert context.__dict__[key] is t
+
+
+class test_TestLang(object):
+ def setUp(self):
+ self.tmp_dir = None
+ self.saved_lang = None
+
+ self.lang = 'xh_ZA'
+ self.domain = 'ipa'
+
+ self.ipa_i18n_dir = os.path.join(os.path.dirname(__file__), '../../install/po')
+
+ self.pot_basename = '%s.pot' % self.domain
+ self.po_basename = '%s.po' % self.lang
+ self.mo_basename = '%s.mo' % self.domain
+
+ self.tmp_dir = tempfile.mkdtemp()
+ self.saved_lang = os.environ['LANG']
+
+ self.locale_dir = os.path.join(self.tmp_dir, 'test_locale')
+ self.msg_dir = os.path.join(self.locale_dir, self.lang, 'LC_MESSAGES')
+
+ if not os.path.exists(self.msg_dir):
+ os.makedirs(self.msg_dir)
+
+ self.pot_file = os.path.join(self.ipa_i18n_dir, self.pot_basename)
+ self.mo_file = os.path.join(self.msg_dir, self.mo_basename)
+ self.po_file = os.path.join(self.tmp_dir, self.po_basename)
+
+ result = create_po(self.pot_file, self.po_file, self.mo_file)
+ if result:
+ raise nose.SkipTest('Unable to create po file "%s" & mo file "%s" from pot file "%s"' %
+ (self.po_file, self.mo_file, self.pot_file))
+
+ if not file_exists(self.po_file):
+ raise nose.SkipTest('Test po file unavailable, run "make test" in install/po')
+
+ if not file_exists(self.mo_file):
+ raise nose.SkipTest('Test mo file unavailable, run "make test" in install/po')
+
+ self.po_file_iterate = po_file_iterate
+
+ def tearDown(self):
+ if self.saved_lang is not None:
+ os.environ['LANG'] = self.saved_lang
+
+ if self.tmp_dir is not None:
+ shutil.rmtree(self.tmp_dir)
+
+ def test_test_lang(self):
+ print "test_test_lang"
+ # The test installs the test message catalog under the xh_ZA
+ # (e.g. Zambia Xhosa) language by default. It would be nice to
+ # use a dummy language not associated with any real language,
+ # but the setlocale function demands the locale be a valid
+ # known locale, Zambia Xhosa is a reasonable choice :)
+
+ os.environ['LANG'] = self.lang
+
+ # Create a gettext translation object specifying our domain as
+ # 'ipa' and the locale_dir as 'test_locale' (i.e. where to
+ # look for the message catalog). Then use that translation
+ # object to obtain the translation functions.
+
+ def get_msgstr(msg):
+ gt = text.GettextFactory(localedir=self.locale_dir)(msg)
+ return unicode(gt)
+
+ def get_msgstr_plural(singular, plural, count):
+ ng = text.NGettextFactory(localedir=self.locale_dir)(singular, plural, count)
+ return ng(count)
+
+ result = self.po_file_iterate(self.po_file, get_msgstr, get_msgstr_plural)
+ assert result == 0
+
+class test_LazyText(object):
+
+ klass = text.LazyText
+
+ def test_init(self):
+ inst = self.klass('foo', 'bar')
+ assert inst.domain == 'foo'
+ assert inst.localedir == 'bar'
+ assert inst.key == ('foo', 'bar')
+
+
+class test_FixMe(object):
+ klass = text.FixMe
+
+ def test_init(self):
+ inst = self.klass('user.label')
+ assert inst.msg == 'user.label'
+ assert inst.domain is None
+ assert inst.localedir is None
+
+ def test_repr(self):
+ inst = self.klass('user.label')
+ assert repr(inst) == "FixMe('user.label')"
+
+ def test_unicode(self):
+ inst = self.klass('user.label')
+ assert unicode(inst) == u'<user.label>'
+ assert type(unicode(inst)) is unicode
+
+
+class test_Gettext(object):
+
+ klass = text.Gettext
+
+ def test_init(self):
+ inst = self.klass('what up?', 'foo', 'bar')
+ assert inst.domain == 'foo'
+ assert inst.localedir == 'bar'
+ assert inst.msg is 'what up?'
+ assert inst.args == ('what up?', 'foo', 'bar')
+
+ def test_repr(self):
+ inst = self.klass('foo', 'bar', 'baz')
+ assert repr(inst) == "Gettext('foo', domain='bar', localedir='baz')"
+
+ def test_unicode(self):
+ inst = self.klass('what up?', 'foo', 'bar')
+ assert unicode(inst) == u'what up?'
+
+ def test_mod(self):
+ inst = self.klass('hello %(adj)s nurse', 'foo', 'bar')
+ assert inst % dict(adj='naughty', stuff='junk') == 'hello naughty nurse'
+
+ def test_eq(self):
+ inst1 = self.klass('what up?', 'foo', 'bar')
+ inst2 = self.klass('what up?', 'foo', 'bar')
+ inst3 = self.klass('Hello world', 'foo', 'bar')
+ inst4 = self.klass('what up?', 'foo', 'baz')
+
+ assert (inst1 == inst1) is True
+ assert (inst1 == inst2) is True
+ assert (inst1 == inst3) is False
+ assert (inst1 == inst4) is False
+
+ # Test with args flipped
+ assert (inst2 == inst1) is True
+ assert (inst3 == inst1) is False
+ assert (inst4 == inst1) is False
+
+ def test_ne(self):
+ inst1 = self.klass('what up?', 'foo', 'bar')
+ inst2 = self.klass('what up?', 'foo', 'bar')
+ inst3 = self.klass('Hello world', 'foo', 'bar')
+ inst4 = self.klass('what up?', 'foo', 'baz')
+
+ assert (inst1 != inst2) is False
+ assert (inst1 != inst2) is False
+ assert (inst1 != inst3) is True
+ assert (inst1 != inst4) is True
+
+ # Test with args flipped
+ assert (inst2 != inst1) is False
+ assert (inst3 != inst1) is True
+ assert (inst4 != inst1) is True
+
+
+class test_NGettext(object):
+
+ klass = text.NGettext
+
+ def test_init(self):
+ inst = self.klass(singular, plural, 'foo', 'bar')
+ assert inst.singular is singular
+ assert inst.plural is plural
+ assert inst.domain == 'foo'
+ assert inst.localedir == 'bar'
+ assert inst.args == (singular, plural, 'foo', 'bar')
+
+ def test_repr(self):
+ inst = self.klass('sig', 'plu', 'foo', 'bar')
+ assert repr(inst) == \
+ "NGettext('sig', 'plu', domain='foo', localedir='bar')"
+
+ def test_call(self):
+ inst = self.klass(singular, plural, 'foo', 'bar')
+ assert inst(0) == plural
+ assert inst(1) == singular
+ assert inst(2) == plural
+ assert inst(3) == plural
+
+ def test_mod(self):
+ inst = self.klass(singular, plural, 'foo', 'bar')
+ assert inst % dict(count=0, dish='frown') == '0 geese make a frown'
+ assert inst % dict(count=1, dish='stew') == '1 goose makes a stew'
+ assert inst % dict(count=2, dish='pie') == '2 geese make a pie'
+
+ def test_eq(self):
+ inst1 = self.klass(singular, plural, 'foo', 'bar')
+ inst2 = self.klass(singular, plural, 'foo', 'bar')
+ inst3 = self.klass(singular, '%(count)d thingies', 'foo', 'bar')
+ inst4 = self.klass(singular, plural, 'foo', 'baz')
+
+ assert (inst1 == inst1) is True
+ assert (inst1 == inst2) is True
+ assert (inst1 == inst3) is False
+ assert (inst1 == inst4) is False
+
+ # Test with args flipped
+ assert (inst2 == inst1) is True
+ assert (inst3 == inst1) is False
+ assert (inst4 == inst1) is False
+
+ def test_ne(self):
+ inst1 = self.klass(singular, plural, 'foo', 'bar')
+ inst2 = self.klass(singular, plural, 'foo', 'bar')
+ inst3 = self.klass(singular, '%(count)d thingies', 'foo', 'bar')
+ inst4 = self.klass(singular, plural, 'foo', 'baz')
+
+ assert (inst1 != inst2) is False
+ assert (inst1 != inst2) is False
+ assert (inst1 != inst3) is True
+ assert (inst1 != inst4) is True
+
+ # Test with args flipped
+ assert (inst2 != inst1) is False
+ assert (inst3 != inst1) is True
+ assert (inst4 != inst1) is True
+
+
+class test_GettextFactory(object):
+
+ klass = text.GettextFactory
+
+ def test_init(self):
+ # Test with defaults:
+ inst = self.klass()
+ assert inst.domain == 'ipa'
+ assert inst.localedir is None
+
+ # Test with overrides:
+ inst = self.klass('foo', 'bar')
+ assert inst.domain == 'foo'
+ assert inst.localedir == 'bar'
+
+ def test_repr(self):
+ # Test with defaults:
+ inst = self.klass()
+ assert repr(inst) == "GettextFactory(domain='ipa', localedir=None)"
+
+ # Test with overrides:
+ inst = self.klass('foo', 'bar')
+ assert repr(inst) == "GettextFactory(domain='foo', localedir='bar')"
+
+ def test_call(self):
+ inst = self.klass('foo', 'bar')
+ g = inst('what up?')
+ assert type(g) is text.Gettext
+ assert g.msg is 'what up?'
+ assert g.domain == 'foo'
+ assert g.localedir == 'bar'
+
+
+class test_NGettextFactory(object):
+
+ klass = text.NGettextFactory
+
+ def test_init(self):
+ # Test with defaults:
+ inst = self.klass()
+ assert inst.domain == 'ipa'
+ assert inst.localedir is None
+
+ # Test with overrides:
+ inst = self.klass('foo', 'bar')
+ assert inst.domain == 'foo'
+ assert inst.localedir == 'bar'
+
+ def test_repr(self):
+ # Test with defaults:
+ inst = self.klass()
+ assert repr(inst) == "NGettextFactory(domain='ipa', localedir=None)"
+
+ # Test with overrides:
+ inst = self.klass('foo', 'bar')
+ assert repr(inst) == "NGettextFactory(domain='foo', localedir='bar')"
+
+ def test_call(self):
+ inst = self.klass('foo', 'bar')
+ ng = inst(singular, plural, 7)
+ assert type(ng) is text.NGettext
+ assert ng.singular is singular
+ assert ng.plural is plural
+ assert ng.domain == 'foo'
+ assert ng.localedir == 'bar'
diff --git a/ipatests/test_ipalib/test_util.py b/ipatests/test_ipalib/test_util.py
new file mode 100644
index 000000000..9d19dfb2c
--- /dev/null
+++ b/ipatests/test_ipalib/test_util.py
@@ -0,0 +1,26 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipalib.util` module.
+"""
+
+from ipalib import util
+
+
diff --git a/ipatests/test_ipalib/test_x509.py b/ipatests/test_ipalib/test_x509.py
new file mode 100644
index 000000000..c7fafbbd9
--- /dev/null
+++ b/ipatests/test_ipalib/test_x509.py
@@ -0,0 +1,139 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib.x509` module.
+"""
+
+import os
+from os import path
+import sys
+from ipatests.util import raises, setitem, delitem, ClassChecker
+from ipatests.util import getitem, setitem, delitem
+from ipatests.util import TempDir, TempHome
+from ipalib.constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR
+from ipalib.constants import NAME_REGEX, NAME_ERROR
+import base64
+from ipalib import x509
+from nss.error import NSPRError
+from ipapython.dn import DN
+
+# certutil -
+
+# certificate for CN=ipa.example.com,O=IPA
+goodcert = 'MIICAjCCAWugAwIBAgICBEUwDQYJKoZIhvcNAQEFBQAwKTEnMCUGA1UEAxMeSVBBIFRlc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTEwMDYyNTEzMDA0MloXDTE1MDYyNTEzMDA0MlowKDEMMAoGA1UEChMDSVBBMRgwFgYDVQQDEw9pcGEuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJcZ+H6+cQaN/BlzR8OYkVeJgaU5tCaV9FF1m7Ws/ftPtTJUaSL1ncp6603rjA4tH1aa/B8i8xdC46+ZbY2au8b9ryGcOsx2uaRpNLEQ2Fy//q1kQC8oM+iD8Nd6osF0a2wnugsgnJHPuJzhViaWxYgzk5DRdP81debokF3f3FX/AgMBAAGjOjA4MBEGCWCGSAGG+EIBAQQEAwIGQDATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADgYEALD6X9V9w381AzzQPcHsjIjiX3B/AF9RCGocKZUDXkdDhsD9NZ3PLPEf1AMjkraKG963HPB8scyiBbbSuSh6m7TCp0eDgRpo77zNuvd3U4Qpm0Qk+KEjtHQDjNNG6N4ZnCQPmjFPScElvc/GgW7XMbywJy2euF+3/Uip8cnPgSH4='
+
+# The base64-encoded string 'bad cert'
+badcert = 'YmFkIGNlcnQ='
+
+class test_x509(object):
+ """
+ Test `ipalib.x509`
+
+ I created the contents of this certificate with a self-signed CA with:
+ % certutil -R -s "CN=ipa.example.com,O=IPA" -d . -a -o example.csr
+ % ./ipa host-add ipa.example.com
+ % ./ipa cert-request --add --principal=test/ipa.example.com example.csr
+ """
+
+ def test_1_load_base64_cert(self):
+ """
+ Test loading a base64-encoded certificate.
+ """
+
+ # Load a good cert
+ cert = x509.load_certificate(goodcert)
+
+ # Load a good cert with headers
+ newcert = '-----BEGIN CERTIFICATE-----' + goodcert + '-----END CERTIFICATE-----'
+ cert = x509.load_certificate(newcert)
+
+ # Load a good cert with bad headers
+ newcert = '-----BEGIN CERTIFICATE-----' + goodcert
+ try:
+ cert = x509.load_certificate(newcert)
+ except TypeError:
+ pass
+
+ # Load a bad cert
+ try:
+ cert = x509.load_certificate(badcert)
+ except NSPRError:
+ pass
+
+ def test_1_load_der_cert(self):
+ """
+ Test loading a DER certificate.
+ """
+
+ der = base64.b64decode(goodcert)
+
+ # Load a good cert
+ cert = x509.load_certificate(der, x509.DER)
+
+ def test_2_get_subject(self):
+ """
+ Test retrieving the subject
+ """
+ subject = x509.get_subject(goodcert)
+ assert DN(str(subject)) == DN(('CN','ipa.example.com'),('O','IPA'))
+
+ der = base64.b64decode(goodcert)
+ subject = x509.get_subject(der, x509.DER)
+ assert DN(str(subject)) == DN(('CN','ipa.example.com'),('O','IPA'))
+
+ # We should be able to pass in a tuple/list of certs too
+ subject = x509.get_subject((goodcert))
+ assert DN(str(subject)) == DN(('CN','ipa.example.com'),('O','IPA'))
+
+ subject = x509.get_subject([goodcert])
+ assert DN(str(subject)) == DN(('CN','ipa.example.com'),('O','IPA'))
+
+ def test_2_get_serial_number(self):
+ """
+ Test retrieving the serial number
+ """
+ serial = x509.get_serial_number(goodcert)
+ assert serial == 1093
+
+ der = base64.b64decode(goodcert)
+ serial = x509.get_serial_number(der, x509.DER)
+ assert serial == 1093
+
+ # We should be able to pass in a tuple/list of certs too
+ serial = x509.get_serial_number((goodcert))
+ assert serial == 1093
+
+ serial = x509.get_serial_number([goodcert])
+ assert serial == 1093
+
+ def test_3_cert_contents(self):
+ """
+ Test the contents of a certificate
+ """
+ # Verify certificate contents. This exercises python-nss more than
+ # anything but confirms our usage of it.
+
+ cert = x509.load_certificate(goodcert)
+
+ assert DN(str(cert.subject)) == DN(('CN','ipa.example.com'),('O','IPA'))
+ assert DN(str(cert.issuer)) == DN(('CN','IPA Test Certificate Authority'))
+ assert cert.serial_number == 1093
+ assert cert.valid_not_before_str == 'Fri Jun 25 13:00:42 2010 UTC'
+ assert cert.valid_not_after_str == 'Thu Jun 25 13:00:42 2015 UTC'
diff --git a/ipatests/test_ipapython/__init__.py b/ipatests/test_ipapython/__init__.py
new file mode 100644
index 000000000..fa0e44bb5
--- /dev/null
+++ b/ipatests/test_ipapython/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+# Jan Cholasta <jcholast@redhat.com>
+#
+# Copyright (C) 2011 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 `ipapython` package.
+"""
diff --git a/ipatests/test_ipapython/test_cookie.py b/ipatests/test_ipapython/test_cookie.py
new file mode 100644
index 000000000..b8a2d36da
--- /dev/null
+++ b/ipatests/test_ipapython/test_cookie.py
@@ -0,0 +1,478 @@
+# Authors:
+# John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+
+import unittest
+import time
+import datetime
+import email.utils
+import calendar
+from ipapython.cookie import Cookie
+
+class TestParse(unittest.TestCase):
+
+ def test_parse(self):
+ # Empty string
+ s = ''
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 0)
+
+ # Invalid single token
+ s = 'color'
+ with self.assertRaises(ValueError):
+ cookies = Cookie.parse(s)
+
+ # Invalid single token that's keyword
+ s = 'HttpOnly'
+ with self.assertRaises(ValueError):
+ cookies = Cookie.parse(s)
+
+ # Invalid key/value pair whose key is a keyword
+ s = 'domain=example.com'
+ with self.assertRaises(ValueError):
+ cookies = Cookie.parse(s)
+
+ # 1 cookie with name/value
+ s = 'color=blue'
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 1)
+ cookie = cookies[0]
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue")
+ self.assertEqual(cookie.http_cookie(), "color=blue;")
+
+ # 1 cookie with whose value is quoted
+ # Use "get by name" utility to extract specific cookie
+ s = 'color="blue"'
+ cookie = Cookie.get_named_cookie_from_string(s, 'color')
+ self.assertIsNotNone(cookie)
+ self.assertIsNotNone(cookie, Cookie)
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue")
+ self.assertEqual(cookie.http_cookie(), "color=blue;")
+
+ # 1 cookie with name/value and domain, path attributes.
+ # Change up the whitespace a bit.
+ s = 'color =blue; domain= example.com ; path = /toplevel '
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 1)
+ cookie = cookies[0]
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, 'example.com')
+ self.assertEqual(cookie.path, '/toplevel')
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue; Domain=example.com; Path=/toplevel")
+ self.assertEqual(cookie.http_cookie(), "color=blue;")
+
+ # 2 cookies, various attributes
+ s = 'color=blue; Max-Age=3600; temperature=hot; HttpOnly'
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 2)
+ cookie = cookies[0]
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, 3600)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue; Max-Age=3600")
+ self.assertEqual(cookie.http_cookie(), "color=blue;")
+ cookie = cookies[1]
+ self.assertEqual(cookie.key, 'temperature')
+ self.assertEqual(cookie.value, 'hot')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, True)
+ self.assertEqual(str(cookie), "temperature=hot; HttpOnly")
+ self.assertEqual(cookie.http_cookie(), "temperature=hot;")
+
+class TestExpires(unittest.TestCase):
+
+ def setUp(self):
+ # Force microseconds to zero because cookie timestamps only have second resolution
+ self.now = datetime.datetime.utcnow().replace(microsecond=0)
+ self.now_timestamp = calendar.timegm(self.now.utctimetuple())
+ self.now_string = email.utils.formatdate(self.now_timestamp, usegmt=True)
+
+ self.max_age = 3600 # 1 hour
+ self.age_expiration = self.now + datetime.timedelta(seconds=self.max_age)
+ self.age_timestamp = calendar.timegm(self.age_expiration.utctimetuple())
+ self.age_string = email.utils.formatdate(self.age_timestamp, usegmt=True)
+
+ self.expires = self.now + datetime.timedelta(days=1) # 1 day
+ self.expires_timestamp = calendar.timegm(self.expires.utctimetuple())
+ self.expires_string = email.utils.formatdate(self.expires_timestamp, usegmt=True)
+
+ def test_expires(self):
+ # 1 cookie with name/value and no Max-Age and no Expires
+ s = 'color=blue;'
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 1)
+ cookie = cookies[0]
+ # Force timestamp to known value
+ cookie.timestamp = self.now
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue")
+ self.assertEqual(cookie.get_expiration(), None)
+ # Normalize
+ self.assertEqual(cookie.normalize_expiration(), None)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(str(cookie), "color=blue")
+
+ # 1 cookie with name/value and Max-Age
+ s = 'color=blue; max-age=%d' % (self.max_age)
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 1)
+ cookie = cookies[0]
+ # Force timestamp to known value
+ cookie.timestamp = self.now
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, self.max_age)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue; Max-Age=%d" % (self.max_age))
+ self.assertEqual(cookie.get_expiration(), self.age_expiration)
+ # Normalize
+ self.assertEqual(cookie.normalize_expiration(), self.age_expiration)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, self.age_expiration)
+ self.assertEqual(str(cookie), "color=blue; Expires=%s" % (self.age_string))
+
+
+ # 1 cookie with name/value and Expires
+ s = 'color=blue; Expires=%s' % (self.expires_string)
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 1)
+ cookie = cookies[0]
+ # Force timestamp to known value
+ cookie.timestamp = self.now
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, self.expires)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue; Expires=%s" % (self.expires_string))
+ self.assertEqual(cookie.get_expiration(), self.expires)
+ # Normalize
+ self.assertEqual(cookie.normalize_expiration(), self.expires)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, self.expires)
+ self.assertEqual(str(cookie), "color=blue; Expires=%s" % (self.expires_string))
+
+ # 1 cookie with name/value witht both Max-Age and Expires, Max-Age takes precedence
+ s = 'color=blue; Expires=%s; max-age=%d' % (self.expires_string, self.max_age)
+ cookies = Cookie.parse(s)
+ self.assertEqual(len(cookies), 1)
+ cookie = cookies[0]
+ # Force timestamp to known value
+ cookie.timestamp = self.now
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, self.max_age)
+ self.assertEqual(cookie.expires, self.expires)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue; Max-Age=%d; Expires=%s" % (self.max_age, self.expires_string))
+ self.assertEqual(cookie.get_expiration(), self.age_expiration)
+ # Normalize
+ self.assertEqual(cookie.normalize_expiration(), self.age_expiration)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, self.age_expiration)
+ self.assertEqual(str(cookie), "color=blue; Expires=%s" % (self.age_string))
+
+ # Verify different types can be assigned to the timestamp and
+ # expires attribute.
+
+ cookie = Cookie('color', 'blue')
+ cookie.timestamp = self.now
+ self.assertEqual(cookie.timestamp, self.now)
+ cookie.timestamp = self.now_timestamp
+ self.assertEqual(cookie.timestamp, self.now)
+ cookie.timestamp = self.now_string
+ self.assertEqual(cookie.timestamp, self.now)
+
+ self.assertEqual(cookie.expires, None)
+
+ cookie.expires = self.expires
+ self.assertEqual(cookie.expires, self.expires)
+ cookie.expires = self.expires_timestamp
+ self.assertEqual(cookie.expires, self.expires)
+ cookie.expires = self.expires_string
+ self.assertEqual(cookie.expires, self.expires)
+
+class TestInvalidAttributes(unittest.TestCase):
+ def test_invalid(self):
+ # Invalid Max-Age
+ s = 'color=blue; Max-Age=over-the-hill'
+ with self.assertRaises(ValueError):
+ cookies = Cookie.parse(s)
+
+ cookie = Cookie('color', 'blue')
+ with self.assertRaises(ValueError):
+ cookie.max_age = 'over-the-hill'
+
+ # Invalid Expires
+ s = 'color=blue; Expires=Sun, 06 Xxx 1994 08:49:37 GMT'
+ with self.assertRaises(ValueError):
+ cookies = Cookie.parse(s)
+
+ cookie = Cookie('color', 'blue')
+ with self.assertRaises(ValueError):
+ cookie.expires = 'Sun, 06 Xxx 1994 08:49:37 GMT'
+
+
+class TestAttributes(unittest.TestCase):
+ def test_attributes(self):
+ cookie = Cookie('color', 'blue')
+ self.assertEqual(cookie.key, 'color')
+ self.assertEqual(cookie.value, 'blue')
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+ self.assertEqual(cookie.max_age, None)
+ self.assertEqual(cookie.expires, None)
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(cookie.httponly, None)
+
+ cookie.domain = 'example.com'
+ self.assertEqual(cookie.domain, 'example.com')
+ cookie.domain = None
+ self.assertEqual(cookie.domain, None)
+
+ cookie.path = '/toplevel'
+ self.assertEqual(cookie.path, '/toplevel')
+ cookie.path = None
+ self.assertEqual(cookie.path, None)
+
+ cookie.max_age = 400
+ self.assertEqual(cookie.max_age, 400)
+ cookie.max_age = None
+ self.assertEqual(cookie.max_age, None)
+
+ cookie.expires = 'Sun, 06 Nov 1994 08:49:37 GMT'
+ self.assertEqual(cookie.expires, datetime.datetime(1994, 11, 6, 8, 49, 37))
+ cookie.expires = None
+ self.assertEqual(cookie.expires, None)
+
+ cookie.secure = True
+ self.assertEqual(cookie.secure, True)
+ self.assertEqual(str(cookie), "color=blue; Secure")
+ cookie.secure = False
+ self.assertEqual(cookie.secure, False)
+ self.assertEqual(str(cookie), "color=blue")
+ cookie.secure = None
+ self.assertEqual(cookie.secure, None)
+ self.assertEqual(str(cookie), "color=blue")
+
+ cookie.httponly = True
+ self.assertEqual(cookie.httponly, True)
+ self.assertEqual(str(cookie), "color=blue; HttpOnly")
+ cookie.httponly = False
+ self.assertEqual(cookie.httponly, False)
+ self.assertEqual(str(cookie), "color=blue")
+ cookie.httponly = None
+ self.assertEqual(cookie.httponly, None)
+ self.assertEqual(str(cookie), "color=blue")
+
+
+class TestHTTPReturn(unittest.TestCase):
+ def setUp(self):
+ self.url = 'http://www.foo.bar.com/one/two'
+
+ def test_no_attributes(self):
+ cookie = Cookie('color', 'blue')
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ def test_domain(self):
+ cookie = Cookie('color', 'blue', domain='www.foo.bar.com')
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', domain='.foo.bar.com')
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', domain='.bar.com')
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', domain='bar.com')
+ with self.assertRaises(Cookie.URLMismatch):
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', domain='bogus.com')
+ with self.assertRaises(Cookie.URLMismatch):
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', domain='www.foo.bar.com')
+ with self.assertRaises(Cookie.URLMismatch):
+ self.assertTrue(cookie.http_return_ok('http://192.168.1.1/one/two'))
+
+ def test_path(self):
+ cookie = Cookie('color', 'blue')
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', path='/')
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', path='/one')
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ cookie = Cookie('color', 'blue', path='/oneX')
+ with self.assertRaises(Cookie.URLMismatch):
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ def test_expires(self):
+ now = datetime.datetime.utcnow().replace(microsecond=0)
+
+ # expires 1 day from now
+ expires = now + datetime.timedelta(days=1)
+
+ cookie = Cookie('color', 'blue', expires=expires)
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+ # expired 1 day ago
+ expires = now + datetime.timedelta(days=-1)
+ cookie = Cookie('color', 'blue', expires=expires)
+ with self.assertRaises(Cookie.Expired):
+ self.assertTrue(cookie.http_return_ok(self.url))
+
+
+ def test_httponly(self):
+ cookie = Cookie('color', 'blue', httponly=True)
+ self.assertTrue(cookie.http_return_ok('http://example.com'))
+ self.assertTrue(cookie.http_return_ok('https://example.com'))
+
+ with self.assertRaises(Cookie.URLMismatch):
+ self.assertTrue(cookie.http_return_ok('ftp://example.com'))
+
+ def test_secure(self):
+ cookie = Cookie('color', 'blue', secure=True)
+ self.assertTrue(cookie.http_return_ok('https://Xexample.com'))
+
+ with self.assertRaises(Cookie.URLMismatch):
+ self.assertTrue(cookie.http_return_ok('http://Xexample.com'))
+
+class TestNormalization(unittest.TestCase):
+ def setUp(self):
+ # Force microseconds to zero because cookie timestamps only have second resolution
+ self.now = datetime.datetime.utcnow().replace(microsecond=0)
+ self.now_timestamp = calendar.timegm(self.now.utctimetuple())
+ self.now_string = email.utils.formatdate(self.now_timestamp, usegmt=True)
+
+ self.max_age = 3600 # 1 hour
+ self.age_expiration = self.now + datetime.timedelta(seconds=self.max_age)
+ self.age_timestamp = calendar.timegm(self.age_expiration.utctimetuple())
+ self.age_string = email.utils.formatdate(self.age_timestamp, usegmt=True)
+
+ self.expires = self.now + datetime.timedelta(days=1) # 1 day
+ self.expires_timestamp = calendar.timegm(self.expires.utctimetuple())
+ self.expires_string = email.utils.formatdate(self.expires_timestamp, usegmt=True)
+
+ def test_path_normalization(self):
+ self.assertEqual(Cookie.normalize_url_path(''), '/')
+ self.assertEqual(Cookie.normalize_url_path('foo'), '/')
+ self.assertEqual(Cookie.normalize_url_path('foo/'), '/')
+ self.assertEqual(Cookie.normalize_url_path('/foo'), '/')
+ self.assertEqual(Cookie.normalize_url_path('/foo/'), '/foo')
+ self.assertEqual(Cookie.normalize_url_path('/Foo/bar'), '/foo')
+ self.assertEqual(Cookie.normalize_url_path('/foo/baR/'), '/foo/bar')
+
+ def test_normalization(self):
+ cookie = Cookie('color', 'blue', expires=self.expires)
+ cookie.timestamp = self.now_timestamp
+
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+
+ url = 'http://example.COM/foo'
+ cookie.normalize(url)
+ self.assertEqual(cookie.domain, 'example.com')
+ self.assertEqual(cookie.path, '/')
+ self.assertEqual(cookie.expires, self.expires)
+
+ cookie = Cookie('color', 'blue', max_age=self.max_age)
+ cookie.timestamp = self.now_timestamp
+
+ self.assertEqual(cookie.domain, None)
+ self.assertEqual(cookie.path, None)
+
+ url = 'http://example.com/foo/'
+ cookie.normalize(url)
+ self.assertEqual(cookie.domain, 'example.com')
+ self.assertEqual(cookie.path, '/foo')
+ self.assertEqual(cookie.expires, self.age_expiration)
+
+ cookie = Cookie('color', 'blue')
+ url = 'http://example.com/foo'
+ cookie.normalize(url)
+ self.assertEqual(cookie.domain, 'example.com')
+ self.assertEqual(cookie.path, '/')
+
+ cookie = Cookie('color', 'blue')
+ url = 'http://example.com/foo/bar'
+ cookie.normalize(url)
+ self.assertEqual(cookie.domain, 'example.com')
+ self.assertEqual(cookie.path, '/foo')
+
+ cookie = Cookie('color', 'blue')
+ url = 'http://example.com/foo/bar/'
+ cookie.normalize(url)
+ self.assertEqual(cookie.domain, 'example.com')
+ self.assertEqual(cookie.path, '/foo/bar')
+
+
+#-------------------------------------------------------------------------------
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ipatests/test_ipapython/test_dn.py b/ipatests/test_ipapython/test_dn.py
new file mode 100644
index 000000000..cdeab9374
--- /dev/null
+++ b/ipatests/test_ipapython/test_dn.py
@@ -0,0 +1,1937 @@
+#!/usr/bin/python
+
+import unittest
+from ipapython.dn import *
+
+def default_rdn_attr_arg(i):
+ return 'a%d' % i
+
+def default_rdn_value_arg(i):
+ return str(i)
+
+def alt_rdn_attr_arg(i):
+ return 'b%d' % i
+
+def alt_rdn_value_arg(i):
+ return str(i*10)
+
+def make_rdn_args(low, high, kind, attr=None, value=None):
+ result=[]
+ for i in range(low, high):
+ if attr is None:
+ new_attr = default_rdn_attr_arg(i)
+ elif callable(attr):
+ new_attr = attr(i)
+ else:
+ new_attr = attr
+
+ if value is None:
+ new_value = default_rdn_value_arg(i)
+ elif callable(value):
+ new_value = value(i)
+ else:
+ new_value = value
+
+ if kind == 'tuple':
+ result.append((new_attr, new_value))
+ elif kind == 'list':
+ result.append([new_attr, new_value])
+ elif kind == 'RDN':
+ result.append(RDN((new_attr, new_value)))
+ else:
+ raise ValueError("Unknown kind = %s" % kind)
+
+ return result
+
+def expected_class(klass, component):
+ if klass is AVA:
+ if component == 'self':
+ return AVA
+
+ elif klass is EditableAVA:
+ if component == 'self':
+ return EditableAVA
+
+ elif klass is RDN:
+ if component == 'self':
+ return RDN
+ elif component == 'AVA':
+ return AVA
+
+ elif klass is EditableRDN:
+ if component == 'self':
+ return EditableRDN
+ elif component == 'AVA':
+ return EditableAVA
+
+ elif klass is DN:
+ if component == 'self':
+ return DN
+ elif component == 'AVA':
+ return AVA
+ elif component == 'RDN':
+ return RDN
+
+ elif klass is EditableDN:
+ if component == 'self':
+ return EditableDN
+ elif component == 'AVA':
+ return EditableAVA
+ elif component == 'RDN':
+ return EditableRDN
+
+ raise ValueError("class %s with component '%s' unknown" % (klass.__name__, component))
+
+
+class TestAVA(unittest.TestCase):
+ def setUp(self):
+ self.attr1 = 'cn'
+ self.value1 = 'Bob'
+ self.str_ava1 = '%s=%s' % (self.attr1, self.value1)
+ self.ava1 = AVA(self.attr1, self.value1)
+
+ self.attr2 = 'ou'
+ self.value2 = 'People'
+ self.str_ava2 = '%s=%s' % (self.attr2, self.value2)
+ self.ava2 = AVA(self.attr2, self.value2)
+
+ self.attr3 = 'c'
+ self.value3 = 'US'
+ self.str_ava3 = '%s=%s' % (self.attr3, self.value3)
+ self.ava3 = AVA(self.attr3, self.value3)
+
+ def assertExpectedClass(self, klass, obj, component):
+ self.assertIs(obj.__class__, expected_class(klass, component))
+
+ def test_create(self):
+ for AVA_class in (AVA, EditableAVA):
+ # Create with attr,value pair
+ ava1 = AVA_class(self.attr1, self.value1)
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1, self.ava1)
+
+ # Create with "attr=value" string
+ ava1 = AVA_class(self.str_ava1)
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1, self.ava1)
+
+ # Create with tuple (attr, value)
+ ava1 = AVA_class((self.attr1, self.value1))
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1, self.ava1)
+
+ # Create with list [attr, value]
+ ava1 = AVA_class([self.attr1, self.value1])
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1, self.ava1)
+
+ # Create with no args should fail
+ with self.assertRaises(TypeError):
+ AVA_class()
+
+ # Create with more than 2 args should fail
+ with self.assertRaises(TypeError):
+ AVA_class(self.attr1, self.value1, self.attr1)
+
+ # Create with 1 arg which is not string should fail
+ with self.assertRaises(TypeError):
+ AVA_class(1)
+
+ # Create with malformed AVA_class string should fail
+ with self.assertRaises(ValueError):
+ AVA_class("cn")
+
+ # Create with non-string parameters, should convert
+ ava1 = AVA_class(1, self.value1)
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1.attr, u'1')
+
+ ava1 = AVA_class((1, self.value1))
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1.attr, u'1')
+
+ ava1 = AVA_class(self.attr1, 1)
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1.value, u'1')
+
+ ava1 = AVA_class((self.attr1, 1))
+ self.assertExpectedClass(AVA_class, ava1, 'self')
+ self.assertEqual(ava1.value, u'1')
+
+ def test_indexing(self):
+ for AVA_class in (AVA, EditableAVA):
+ ava1 = AVA_class(self.ava1)
+
+ self.assertEqual(ava1[self.attr1], self.value1)
+
+ with self.assertRaises(KeyError):
+ ava1['foo']
+
+ with self.assertRaises(TypeError):
+ ava1[0]
+
+ def test_properties(self):
+ for AVA_class in (AVA, EditableAVA):
+ ava1 = AVA_class(self.ava1)
+
+ self.assertEqual(ava1.attr, self.attr1)
+ self.assertIsInstance(ava1.attr, unicode)
+
+ self.assertEqual(ava1.value, self.value1)
+ self.assertIsInstance(ava1.value, unicode)
+
+ def test_str(self):
+ for AVA_class in (AVA, EditableAVA):
+ ava1 = AVA_class(self.ava1)
+
+ self.assertEqual(str(ava1), self.str_ava1)
+ self.assertIsInstance(str(ava1), str)
+
+ def test_cmp(self):
+ for AVA_class in (AVA, EditableAVA):
+ # Equality
+ ava1 = AVA_class(self.attr1, self.value1)
+
+ self.assertTrue(ava1 == self.ava1)
+ self.assertFalse(ava1 != self.ava1)
+
+ self.assertTrue(ava1 == self.str_ava1)
+ self.assertFalse(ava1 != self.str_ava1)
+
+ result = cmp(ava1, self.ava1)
+ self.assertEqual(result, 0)
+
+ # Upper case attr should still be equal
+ ava1 = AVA_class(self.attr1.upper(), self.value1)
+
+ self.assertFalse(ava1.attr == self.attr1)
+ self.assertTrue(ava1.value == self.value1)
+ self.assertTrue(ava1 == self.ava1)
+ self.assertFalse(ava1 != self.ava1)
+
+ result = cmp(ava1, self.ava1)
+ self.assertEqual(result, 0)
+
+ # Upper case value should still be equal
+ ava1 = AVA_class(self.attr1, self.value1.upper())
+
+ self.assertTrue(ava1.attr == self.attr1)
+ self.assertFalse(ava1.value == self.value1)
+ self.assertTrue(ava1 == self.ava1)
+ self.assertFalse(ava1 != self.ava1)
+
+ result = cmp(ava1, self.ava1)
+ self.assertEqual(result, 0)
+
+ # Make ava1's attr greater
+ if AVA_class.is_mutable:
+ ava1.attr = self.attr1 + "1"
+ else:
+ with self.assertRaises(AttributeError):
+ ava1.attr = self.attr1 + "1"
+ ava1 = AVA_class(self.attr1 + "1", self.value1.upper())
+
+ self.assertFalse(ava1 == self.ava1)
+ self.assertTrue(ava1 != self.ava1)
+
+ result = cmp(ava1, self.ava1)
+ self.assertEqual(result, 1)
+
+ result = cmp(self.ava1, ava1)
+ self.assertEqual(result, -1)
+
+ # Reset ava1's attr, should be equal again
+ if AVA_class.is_mutable:
+ ava1.attr = self.attr1
+ else:
+ with self.assertRaises(AttributeError):
+ ava1.attr = self.attr1
+ ava1 = AVA_class(self.attr1, self.value1.upper())
+
+ result = cmp(ava1, self.ava1)
+ self.assertEqual(result, 0)
+
+ # Make ava1's value greater
+ # attr will be equal, this tests secondary comparision component
+ if AVA_class.is_mutable:
+ ava1.value = self.value1 + "1"
+ else:
+ with self.assertRaises(AttributeError):
+ ava1.value = self.value1 + "1"
+ ava1 = AVA_class(self.attr1, self.value1 + "1")
+
+ result = cmp(ava1, self.ava1)
+ self.assertEqual(result, 1)
+
+ result = cmp(self.ava1, ava1)
+ self.assertEqual(result, -1)
+
+ def test_hashing(self):
+ # create AVA's that are equal but differ in case
+ immutable_ava1 = AVA((self.attr1.lower(), self.value1.upper()))
+ immutable_ava2 = AVA((self.attr1.upper(), self.value1.lower()))
+
+ mutable_ava1 = EditableAVA((self.attr1.lower(), self.value1.upper()))
+ mutable_ava2 = EditableAVA((self.attr1.upper(), self.value1.lower()))
+
+ # Immutable AVA's that are equal should hash to the same value.
+ # Mutable AVA's should not be hashable.
+
+ self.assertEqual(immutable_ava1, immutable_ava2)
+ self.assertEqual(immutable_ava1, mutable_ava1)
+ self.assertEqual(immutable_ava1, mutable_ava2)
+ self.assertEqual(mutable_ava1, immutable_ava2)
+
+ # Good, everyone's equal, now verify their hash values
+
+ self.assertEqual(hash(immutable_ava1), hash(immutable_ava2))
+ with self.assertRaises(TypeError):
+ hash(mutable_ava1)
+ with self.assertRaises(TypeError):
+ hash(mutable_ava2)
+
+ # Different immutable AVA objects with the same value should
+ # map to 1 common key and 1 member in a set. The key and
+ # member are based on the object's value.
+ #
+ # Mutable AVA objects should be unhashable.
+
+ for AVA_class in (AVA, EditableAVA):
+ ava1_a = AVA_class(self.ava1)
+ ava1_b = AVA_class(self.ava1)
+
+ ava2_a = AVA_class(self.ava2)
+ ava2_b = AVA_class(self.ava2)
+
+ ava3_a = AVA_class(self.ava3)
+ ava3_b = AVA_class(self.ava3)
+
+ self.assertEqual(ava1_a, ava1_b)
+ self.assertEqual(ava2_a, ava2_b)
+ self.assertEqual(ava3_a, ava3_b)
+
+ d = dict()
+ s = set()
+
+ if AVA_class.is_mutable:
+ with self.assertRaises(TypeError):
+ d[ava1_a] = str(ava1_a)
+ with self.assertRaises(TypeError):
+ d[ava1_b] = str(ava1_b)
+ with self.assertRaises(TypeError):
+ d[ava2_a] = str(ava2_a)
+ with self.assertRaises(TypeError):
+ d[ava2_b] = str(ava2_b)
+
+ with self.assertRaises(TypeError):
+ s.add(ava1_a)
+ with self.assertRaises(TypeError):
+ s.add(ava1_b)
+ with self.assertRaises(TypeError):
+ s.add(ava2_a)
+ with self.assertRaises(TypeError):
+ s.add(ava2_b)
+ else:
+ d[ava1_a] = str(ava1_a)
+ d[ava1_b] = str(ava1_b)
+ d[ava2_a] = str(ava2_a)
+ d[ava2_b] = str(ava2_b)
+
+ s.add(ava1_a)
+ s.add(ava1_b)
+ s.add(ava2_a)
+ s.add(ava2_b)
+
+ self.assertEqual(len(d), 2)
+ self.assertEqual(len(s), 2)
+ self.assertEqual(sorted(d.keys()), sorted([ava1_a, ava2_a]))
+ self.assertEqual(sorted(s), sorted([ava1_a, ava2_a]))
+
+ self.assertTrue(ava1_a in d)
+ self.assertTrue(ava1_b in d)
+ self.assertTrue(ava2_a in d)
+ self.assertTrue(ava2_b in d)
+ self.assertFalse(ava3_a in d)
+ self.assertFalse(ava3_b in d)
+
+ self.assertTrue(d.has_key(ava1_a))
+ self.assertTrue(d.has_key(ava1_b))
+ self.assertTrue(d.has_key(ava2_a))
+ self.assertTrue(d.has_key(ava2_b))
+ self.assertFalse(d.has_key(ava3_a))
+ self.assertFalse(d.has_key(ava3_b))
+
+ self.assertTrue(ava1_a in s)
+ self.assertTrue(ava1_b in s)
+ self.assertTrue(ava2_a in s)
+ self.assertTrue(ava2_b in s)
+ self.assertFalse(ava3_a in s)
+ self.assertFalse(ava3_b in s)
+
+ def test_coerce(self):
+ # Coerce an immutable to a mutable
+ immutable_ava1 = AVA(self.ava1)
+ mutable_ava1 = EditableAVA(immutable_ava1)
+ self.assertEqual(mutable_ava1, self.ava1)
+ self.assertEqual(mutable_ava1, immutable_ava1)
+
+ # Coerce a mutable to an immutable
+ mutable_ava1 = EditableAVA(self.ava1)
+ immutable_ava1 = AVA(mutable_ava1)
+ self.assertEqual(immutable_ava1, self.ava1)
+ self.assertEqual(immutable_ava1, mutable_ava1)
+
+class TestRDN(unittest.TestCase):
+ def setUp(self):
+ # ava1 must sort before ava2
+ self.attr1 = 'cn'
+ self.value1 = 'Bob'
+ self.str_ava1 = '%s=%s' % (self.attr1, self.value1)
+ self.ava1 = AVA(self.attr1, self.value1)
+
+ self.str_rdn1 = '%s=%s' % (self.attr1, self.value1)
+ self.rdn1 = RDN((self.attr1, self.value1))
+
+ self.attr2 = 'ou'
+ self.value2 = 'people'
+ self.str_ava2 = '%s=%s' % (self.attr2, self.value2)
+ self.ava2 = AVA(self.attr2, self.value2)
+
+ self.str_rdn2 = '%s=%s' % (self.attr2, self.value2)
+ self.rdn2 = RDN((self.attr2, self.value2))
+
+ self.str_ava3 = '%s=%s+%s=%s' % (self.attr1, self.value1, self.attr2, self.value2)
+
+ self.str_rdn3 = '%s=%s+%s=%s' % (self.attr1, self.value1, self.attr2, self.value2)
+ self.rdn3 = RDN(self.ava1, self.ava2)
+
+ def assertExpectedClass(self, klass, obj, component):
+ self.assertIs(obj.__class__, expected_class(klass, component))
+
+ def test_create(self):
+ for RDN_class in (RDN, EditableRDN):
+ # Create with single attr,value pair
+ rdn1 = RDN_class((self.attr1, self.value1))
+
+
+ self.assertEqual(len(rdn1), 1)
+ self.assertEqual(rdn1, self.rdn1)
+ self.assertExpectedClass(RDN_class, rdn1, 'self')
+ for i in range(0, len(rdn1)):
+ self.assertExpectedClass(RDN_class, rdn1[i], 'AVA')
+ self.assertEqual(rdn1[0], self.ava1)
+
+ # Create with multiple attr,value pairs
+ rdn3 = RDN_class((self.attr1, self.value1), (self.attr2, self.value2))
+ self.assertEqual(len(rdn3), 2)
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+ self.assertEqual(rdn3[0], self.ava1)
+ self.assertEqual(rdn3[1], self.ava2)
+
+ # Create with multiple attr,value pairs passed as lists
+ rdn3 = RDN_class([self.attr1, self.value1], [self.attr2, self.value2])
+ self.assertEqual(len(rdn3), 2)
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+ self.assertEqual(rdn3[0], self.ava1)
+ self.assertEqual(rdn3[1], self.ava2)
+
+ # Create with multiple attr,value pairs but reverse
+ # constructor parameter ordering. RDN canonical ordering
+ # should remain the same
+ rdn3 = RDN_class((self.attr2, self.value2), (self.attr1, self.value1))
+ self.assertEqual(len(rdn3), 2)
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+ self.assertEqual(rdn3[0], self.ava1)
+ self.assertEqual(rdn3[1], self.ava2)
+
+ # Create with single AVA object
+ rdn1 = RDN_class(self.ava1)
+ self.assertEqual(len(rdn1), 1)
+ self.assertEqual(rdn1, self.rdn1)
+ self.assertExpectedClass(RDN_class, rdn1, 'self')
+ for i in range(0, len(rdn1)):
+ self.assertExpectedClass(RDN_class, rdn1[i], 'AVA')
+ self.assertEqual(rdn1[0], self.ava1)
+
+ # Create with multiple AVA objects
+ rdn3 = RDN_class(self.ava1, self.ava2)
+ self.assertEqual(len(rdn3), 2)
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+ self.assertEqual(rdn3[0], self.ava1)
+ self.assertEqual(rdn3[1], self.ava2)
+
+
+ # Create with multiple AVA objects but reverse constructor
+ # parameter ordering. RDN canonical ordering should remain
+ # the same
+ rdn3 = RDN_class(self.ava2, self.ava1)
+ self.assertEqual(len(rdn3), 2)
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+ self.assertEqual(rdn3[0], self.ava1)
+ self.assertEqual(rdn3[1], self.ava2)
+
+ # Create with single string with 1 AVA
+ rdn1 = RDN_class(self.str_rdn1)
+ self.assertEqual(len(rdn1), 1)
+ self.assertEqual(rdn1, self.rdn1)
+ self.assertExpectedClass(RDN_class, rdn1, 'self')
+ for i in range(0, len(rdn1)):
+ self.assertExpectedClass(RDN_class, rdn1[i], 'AVA')
+ self.assertEqual(rdn1[0], self.ava1)
+
+ # Create with single string with 2 AVA's
+ rdn3 = RDN_class(self.str_rdn3)
+ self.assertEqual(len(rdn3), 2)
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+ self.assertEqual(rdn3[0], self.ava1)
+ self.assertEqual(rdn3[1], self.ava2)
+
+ def test_properties(self):
+ for RDN_class in (RDN, EditableRDN):
+ rdn1 = RDN_class(self.rdn1)
+ rdn2 = RDN_class(self.rdn2)
+ rdn3 = RDN_class(self.rdn3)
+
+ self.assertEqual(rdn1.attr, self.attr1)
+ self.assertIsInstance(rdn1.attr, unicode)
+
+ self.assertEqual(rdn1.value, self.value1)
+ self.assertIsInstance(rdn1.value, unicode)
+
+ self.assertEqual(rdn2.attr, self.attr2)
+ self.assertIsInstance(rdn2.attr, unicode)
+
+ self.assertEqual(rdn2.value, self.value2)
+ self.assertIsInstance(rdn2.value, unicode)
+
+ self.assertEqual(rdn3.attr, self.attr1)
+ self.assertIsInstance(rdn3.attr, unicode)
+
+ self.assertEqual(rdn3.value, self.value1)
+ self.assertIsInstance(rdn3.value, unicode)
+
+ def test_str(self):
+ for RDN_class in (RDN, EditableRDN):
+ rdn1 = RDN_class(self.rdn1)
+ rdn2 = RDN_class(self.rdn2)
+ rdn3 = RDN_class(self.rdn3)
+
+ self.assertEqual(str(rdn1), self.str_rdn1)
+ self.assertIsInstance(str(rdn1), str)
+
+ self.assertEqual(str(rdn2), self.str_rdn2)
+ self.assertIsInstance(str(rdn2), str)
+
+ self.assertEqual(str(rdn3), self.str_rdn3)
+ self.assertIsInstance(str(rdn3), str)
+
+ def test_cmp(self):
+ for RDN_class in (RDN, EditableRDN):
+ # Equality
+ rdn1 = RDN_class((self.attr1, self.value1))
+
+ self.assertTrue(rdn1 == self.rdn1)
+ self.assertFalse(rdn1 != self.rdn1)
+
+ self.assertTrue(rdn1 == self.str_rdn1)
+ self.assertFalse(rdn1 != self.str_rdn1)
+
+ result = cmp(rdn1, self.rdn1)
+ self.assertEqual(result, 0)
+
+ # Make rdn1's attr greater
+ if RDN_class.is_mutable:
+ rdn1.attr = self.attr1 + "1"
+ else:
+ rdn1 = RDN_class((self.attr1 + "1", self.value1))
+
+ self.assertFalse(rdn1 == self.rdn1)
+ self.assertTrue(rdn1 != self.rdn1)
+
+ result = cmp(rdn1, self.rdn1)
+ self.assertEqual(result, 1)
+
+ result = cmp(self.rdn1, rdn1)
+ self.assertEqual(result, -1)
+
+ # Reset rdn1's attr, should be equal again
+ if RDN_class.is_mutable:
+ rdn1.attr = self.attr1
+ else:
+ rdn1 = RDN_class((self.attr1, self.value1))
+
+ result = cmp(rdn1, self.rdn1)
+ self.assertEqual(result, 0)
+
+ # Make rdn1's value greater
+ # attr will be equal, this tests secondary comparision component
+ if RDN_class.is_mutable:
+ rdn1.value = self.value1 + "1"
+ else:
+ rdn1 = RDN_class((self.attr1, self.value1 + "1"))
+
+ result = cmp(rdn1, self.rdn1)
+ self.assertEqual(result, 1)
+
+ result = cmp(self.rdn1, rdn1)
+ self.assertEqual(result, -1)
+
+ # Make sure rdn's with more ava's are greater
+ result = cmp(self.rdn1, self.rdn3)
+ self.assertEqual(result, -1)
+ result = cmp(self.rdn3, self.rdn1)
+ self.assertEqual(result, 1)
+
+ def test_indexing(self):
+ for RDN_class in (RDN, EditableRDN):
+ rdn1 = RDN_class(self.rdn1)
+ rdn2 = RDN_class(self.rdn2)
+ rdn3 = RDN_class(self.rdn3)
+
+ self.assertEqual(rdn1[0], self.ava1)
+ self.assertEqual(rdn1[self.ava1.attr], self.ava1.value)
+ with self.assertRaises(KeyError):
+ rdn1['foo']
+
+ self.assertEqual(rdn2[0], self.ava2)
+ self.assertEqual(rdn2[self.ava2.attr], self.ava2.value)
+ with self.assertRaises(KeyError):
+ rdn2['foo']
+
+ self.assertEqual(rdn3[0], self.ava1)
+ self.assertEqual(rdn3[self.ava1.attr], self.ava1.value)
+ self.assertEqual(rdn3[1], self.ava2)
+ self.assertEqual(rdn3[self.ava2.attr], self.ava2.value)
+ with self.assertRaises(KeyError):
+ rdn3['foo']
+
+ self.assertEqual(rdn1.attr, self.attr1)
+ self.assertEqual(rdn1.value, self.value1)
+
+ with self.assertRaises(TypeError):
+ rdn3[1.0]
+
+ # Slices
+ self.assertEqual(rdn3[0:1], [self.ava1])
+ self.assertEqual(rdn3[:], [self.ava1, self.ava2])
+
+ def test_assignments(self):
+ for RDN_class in (RDN, EditableRDN):
+ rdn = RDN_class((self.attr1, self.value1))
+ if RDN_class.is_mutable:
+ rdn[0] = self.ava2
+ self.assertEqual(rdn, self.rdn2)
+ else:
+ with self.assertRaises(TypeError):
+ rdn[0] = self.ava2
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ rdn = RDN_class((self.attr1, self.value1))
+ if RDN_class.is_mutable:
+ rdn[0] = (self.attr2, self.value2)
+ self.assertEqual(rdn, self.rdn2)
+ else:
+ with self.assertRaises(TypeError):
+ rdn[0] = (self.attr2, self.value2)
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ rdn = RDN_class((self.attr1, self.value1))
+ if RDN_class.is_mutable:
+ rdn[self.attr1] = self.str_ava2
+ self.assertEqual(rdn[0], self.ava2)
+ else:
+ with self.assertRaises(TypeError):
+ rdn[self.attr1] = self.str_ava2
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ # Can't assign multiples to single entry
+ rdn = RDN_class((self.attr1, self.value1))
+ with self.assertRaises(TypeError):
+ rdn[self.attr1] = self.str_ava3
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ rdn = RDN_class((self.attr1, self.value1))
+ with self.assertRaises(TypeError):
+ rdn[self.attr1] = (self.attr1, self.value1, self.attr2, self.value2)
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ rdn = RDN_class((self.attr1, self.value1))
+ with self.assertRaises(TypeError):
+ rdn[self.attr1] = [(self.attr1, self.value1), (self.attr2, self.value2)]
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ # Slices
+ rdn = RDN_class((self.attr1, self.value1))
+ self.assertEqual(rdn, self.rdn1)
+ if RDN_class.is_mutable:
+ rdn[0:1] = [self.ava2]
+ self.assertEqual(rdn, self.rdn2)
+ else:
+ with self.assertRaises(TypeError):
+ rdn[0:1] = [self.ava2]
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ rdn = RDN_class((self.attr1, self.value1))
+ self.assertEqual(rdn, self.rdn1)
+ if RDN_class.is_mutable:
+ rdn[:] = [(self.attr2, self.value2)]
+ self.assertEqual(rdn, self.rdn2)
+ else:
+ with self.assertRaises(TypeError):
+ rdn[:] = [(self.attr2, self.value2)]
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ rdn = RDN_class((self.attr1, self.value1))
+ self.assertEqual(rdn, self.rdn1)
+ if RDN_class.is_mutable:
+ rdn[:] = [(self.attr1, self.value1),(self.attr2, self.value2)]
+ self.assertEqual(rdn, self.rdn3)
+ else:
+ with self.assertRaises(TypeError):
+ rdn[:] = [(self.attr1, self.value1),(self.attr2, self.value2)]
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+ rdn = RDN_class((self.attr1, self.value1))
+ self.assertEqual(rdn, self.rdn1)
+ if RDN_class.is_mutable:
+ rdn[0:1] = [(self.attr1, self.value1), (self.attr2, self.value2)]
+ self.assertEqual(rdn, self.rdn3)
+ else:
+ with self.assertRaises(TypeError):
+ rdn[0:1] = [(self.attr1, self.value1), (self.attr2, self.value2)]
+ self.assertExpectedClass(RDN_class, rdn, 'self')
+ for i in range(0, len(rdn)):
+ self.assertExpectedClass(RDN_class, rdn[i], 'AVA')
+
+
+ def test_iter(self):
+ for RDN_class in (RDN, EditableRDN):
+ rdn1 = RDN_class(self.rdn1)
+ rdn2 = RDN_class(self.rdn2)
+ rdn3 = RDN_class(self.rdn3)
+
+ self.assertEqual(len(rdn1), 1)
+ self.assertEqual(rdn1[:], [self.ava1])
+ for i, ava in enumerate(rdn1):
+ if i == 0:
+ self.assertEqual(ava, self.ava1)
+ else:
+ self.fail("got iteration index %d, but len=%d" % (i, len(rdn1)))
+
+ self.assertEqual(len(rdn2), 1)
+ self.assertEqual(rdn2[:], [self.ava2])
+ for i, ava in enumerate(rdn2):
+ if i == 0:
+ self.assertEqual(ava, self.ava2)
+ else:
+ self.fail("got iteration index %d, but len=%d" % (i, len(rdn2)))
+
+ self.assertEqual(len(rdn3), 2)
+ self.assertEqual(rdn3[:], [self.ava1, self.ava2])
+ for i, ava in enumerate(rdn3):
+ if i == 0:
+ self.assertEqual(ava, self.ava1)
+ elif i == 1:
+ self.assertEqual(ava, self.ava2)
+ else:
+ self.fail("got iteration index %d, but len=%d" % (i, len(rdn3)))
+
+
+ def test_concat(self):
+ for RDN_class in (RDN, EditableRDN):
+ rdn1 = RDN_class((self.attr1, self.value1))
+ rdn2 = RDN_class((self.attr2, self.value2))
+
+ # in-place addtion
+
+ # Note: If __iadd__ is not available Python will emulate += by
+ # replacing the lhs object with the result of __add__ (if available).
+ # Thus += works for both immutable and mutable RDN,DN object, the only
+ # difference is an immutable without __iadd__ will have a different object
+ # on the lhs after the operator evaluates.
+
+ rdn1 += rdn2
+ self.assertEqual(rdn1, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn1, 'self')
+ for i in range(0, len(rdn1)):
+ self.assertExpectedClass(RDN_class, rdn1[i], 'AVA')
+
+ rdn1 = RDN_class((self.attr1, self.value1))
+ rdn1 += self.ava2
+ self.assertEqual(rdn1, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn1, 'self')
+ for i in range(0, len(rdn1)):
+ self.assertExpectedClass(RDN_class, rdn1[i], 'AVA')
+
+ rdn1 = RDN_class((self.attr1, self.value1))
+ rdn1 += self.str_ava2
+ self.assertEqual(rdn1, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn1, 'self')
+ for i in range(0, len(rdn1)):
+ self.assertExpectedClass(RDN_class, rdn1[i], 'AVA')
+
+ # concatenation
+ rdn1 = RDN_class((self.attr1, self.value1))
+ rdn3 = rdn1 + rdn2
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+
+ rdn3 = rdn1 + self.ava2
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+
+ rdn3 = rdn1 + self.str_ava2
+ self.assertEqual(rdn3, self.rdn3)
+ self.assertExpectedClass(RDN_class, rdn3, 'self')
+ for i in range(0, len(rdn3)):
+ self.assertExpectedClass(RDN_class, rdn3[i], 'AVA')
+
+
+ def test_hashing(self):
+ # create RDN's that are equal but differ in case
+ immutable_rdn1 = RDN((self.attr1.lower(), self.value1.upper()))
+ immutable_rdn2 = RDN((self.attr1.upper(), self.value1.lower()))
+
+ mutable_rdn1 = EditableRDN((self.attr1.lower(), self.value1.upper()))
+ mutable_rdn2 = EditableRDN((self.attr1.upper(), self.value1.lower()))
+
+ # Immutable RDN's that are equal should hash to the same value.
+ # Mutable RDN's should not be hashable.
+
+ self.assertEqual(immutable_rdn1, immutable_rdn2)
+ self.assertEqual(immutable_rdn1, mutable_rdn1)
+ self.assertEqual(immutable_rdn1, mutable_rdn2)
+ self.assertEqual(mutable_rdn1, immutable_rdn2)
+
+ # Good, everyone's equal, now verify their hash values
+
+ self.assertEqual(hash(immutable_rdn1), hash(immutable_rdn2))
+ with self.assertRaises(TypeError):
+ hash(mutable_rdn1)
+ with self.assertRaises(TypeError):
+ hash(mutable_rdn2)
+
+ def test_coerce(self):
+ # Coerce an immutable to a mutable
+ immutable_rdn3 = RDN(self.rdn3)
+ mutable_rdn3 = EditableRDN(immutable_rdn3)
+ self.assertEqual(mutable_rdn3, self.rdn3)
+ self.assertEqual(mutable_rdn3, immutable_rdn3)
+
+ # Coerce a mutable to an immutable
+ mutable_rdn3 = EditableRDN(self.rdn3)
+ immutable_rdn3 = RDN(mutable_rdn3)
+ self.assertEqual(immutable_rdn3, self.rdn3)
+ self.assertEqual(immutable_rdn3, mutable_rdn3)
+
+class TestDN(unittest.TestCase):
+ def setUp(self):
+ # ava1 must sort before ava2
+ self.attr1 = 'cn'
+ self.value1 = 'Bob'
+ self.str_ava1 = '%s=%s' % (self.attr1, self.value1)
+ self.ava1 = AVA(self.attr1, self.value1)
+
+ self.str_rdn1 = '%s=%s' % (self.attr1, self.value1)
+ self.rdn1 = RDN((self.attr1, self.value1))
+
+ self.attr2 = 'ou'
+ self.value2 = 'people'
+ self.str_ava2 = '%s=%s' % (self.attr2, self.value2)
+ self.ava2 = AVA(self.attr2, self.value2)
+
+ self.str_rdn2 = '%s=%s' % (self.attr2, self.value2)
+ self.rdn2 = RDN((self.attr2, self.value2))
+
+ self.str_dn1 = self.str_rdn1
+ self.dn1 = DN(self.rdn1)
+
+ self.str_dn2 = self.str_rdn2
+ self.dn2 = DN(self.rdn2)
+
+ self.str_dn3 = '%s,%s' % (self.str_rdn1, self.str_rdn2)
+ self.dn3 = DN(self.rdn1, self.rdn2)
+
+ self.base_rdn1 = RDN(('dc', 'redhat'))
+ self.base_rdn2 = RDN(('dc', 'com'))
+ self.base_dn = DN(self.base_rdn1, self.base_rdn2)
+
+ self.container_rdn1 = RDN(('cn', 'sudorules'))
+ self.container_rdn2 = RDN(('cn', 'sudo'))
+ self.container_dn = DN(self.container_rdn1, self.container_rdn2)
+
+ self.base_container_dn = DN((self.attr1, self.value1),
+ self.container_dn, self.base_dn)
+
+
+ def assertExpectedClass(self, klass, obj, component):
+ self.assertIs(obj.__class__, expected_class(klass, component))
+
+ def test_create(self):
+ for DN_class in (DN, EditableDN):
+ # Create with single attr,value pair
+ dn1 = DN_class((self.attr1, self.value1))
+ self.assertEqual(len(dn1), 1)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[0].attr, unicode)
+ self.assertIsInstance(dn1[0].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+
+ # Create with single attr,value pair passed as a tuple
+ dn1 = DN_class((self.attr1, self.value1))
+ self.assertEqual(len(dn1), 1)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+
+ # Creation with multiple attr,value string pairs should fail
+ with self.assertRaises(ValueError):
+ dn1 = DN_class(self.attr1, self.value1, self.attr2, self.value2)
+
+ # Create with multiple attr,value pairs passed as tuples & lists
+ dn1 = DN_class((self.attr1, self.value1), [self.attr2, self.value2])
+ self.assertEqual(len(dn1), 2)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+ self.assertEqual(dn1[1], self.rdn2)
+
+ # Create with multiple attr,value pairs passed as tuple and RDN
+ dn1 = DN_class((self.attr1, self.value1), RDN((self.attr2, self.value2)))
+ self.assertEqual(len(dn1), 2)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+ self.assertEqual(dn1[1], self.rdn2)
+
+ # Create with multiple attr,value pairs but reverse
+ # constructor parameter ordering. RDN ordering should also be
+ # reversed because DN's are a ordered sequence of RDN's
+ dn1 = DN_class((self.attr2, self.value2), (self.attr1, self.value1))
+ self.assertEqual(len(dn1), 2)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn2)
+ self.assertEqual(dn1[1], self.rdn1)
+
+ # Create with single RDN object
+ dn1 = DN_class(self.rdn1)
+ self.assertEqual(len(dn1), 1)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+
+ # Create with multiple RDN objects, assure ordering is preserved.
+ dn1 = DN_class(self.rdn1, self.rdn2)
+ self.assertEqual(len(dn1), 2)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+ self.assertEqual(dn1[1], self.rdn2)
+
+ # Create with multiple RDN objects in different order, assure
+ # ordering is preserved.
+ dn1 = DN_class(self.rdn2, self.rdn1)
+ self.assertEqual(len(dn1), 2)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn2)
+ self.assertEqual(dn1[1], self.rdn1)
+
+ # Create with single string with 1 RDN
+ dn1 = DN_class(self.str_rdn1)
+ self.assertEqual(len(dn1), 1)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+
+ # Create with single string with 2 RDN's
+ dn1 = DN_class(self.str_dn3)
+ self.assertEqual(len(dn1), 2)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+ self.assertIsInstance(dn1[i].attr, unicode)
+ self.assertIsInstance(dn1[i].value, unicode)
+ self.assertEqual(dn1[0], self.rdn1)
+ self.assertEqual(dn1[1], self.rdn2)
+
+ # Create with RDN, and 2 DN's (e.g. attr + container + base)
+ dn1 = DN_class((self.attr1, self.value1), self.container_dn, self.base_dn)
+ self.assertEqual(len(dn1), 5)
+ dn_str = ','.join([str(self.rdn1),
+ str(self.container_rdn1), str(self.container_rdn2),
+ str(self.base_rdn1), str(self.base_rdn2)])
+ self.assertEqual(str(dn1), dn_str)
+
+ def test_str(self):
+ for DN_class in (DN, EditableDN):
+ dn1 = DN_class(self.dn1)
+ dn2 = DN_class(self.dn2)
+ dn3 = DN_class(self.dn3)
+
+ self.assertEqual(str(dn1), self.str_dn1)
+ self.assertIsInstance(str(dn1), str)
+
+ self.assertEqual(str(dn2), self.str_dn2)
+ self.assertIsInstance(str(dn2), str)
+
+ self.assertEqual(str(dn3), self.str_dn3)
+ self.assertIsInstance(str(dn3), str)
+
+ def test_cmp(self):
+ for DN_class in (DN, EditableDN):
+ # Equality
+ dn1 = DN_class((self.attr1, self.value1))
+
+ self.assertTrue(dn1 == self.dn1)
+ self.assertFalse(dn1 != self.dn1)
+
+ self.assertTrue(dn1 == self.str_dn1)
+ self.assertFalse(dn1 != self.str_dn1)
+
+ result = cmp(dn1, self.dn1)
+ self.assertEqual(result, 0)
+
+ # Make dn1's attr greater
+ if DN_class.is_mutable:
+ dn1[0].attr = self.attr1 + "1"
+ else:
+ with self.assertRaises(AttributeError):
+ dn1[0].attr = self.attr1 + "1"
+ dn1 = DN_class((self.attr1 + "1", self.value1))
+
+ self.assertFalse(dn1 == self.dn1)
+ self.assertTrue(dn1 != self.dn1)
+
+ result = cmp(dn1, self.dn1)
+ self.assertEqual(result, 1)
+
+ result = cmp(self.dn1, dn1)
+ self.assertEqual(result, -1)
+
+ # Reset dn1's attr, should be equal again
+ if DN_class.is_mutable:
+ dn1[0].attr = self.attr1
+ else:
+ with self.assertRaises(AttributeError):
+ dn1[0].attr = self.attr1
+ dn1 = DN_class((self.attr1, self.value1))
+
+ result = cmp(dn1, self.dn1)
+ self.assertEqual(result, 0)
+
+ # Make dn1's value greater
+ # attr will be equal, this tests secondary comparision component
+ if DN_class.is_mutable:
+ dn1[0].value = self.value1 + "1"
+ else:
+ with self.assertRaises(AttributeError):
+ dn1[0].value = self.value1 + "1"
+ dn1 = DN_class((self.attr1, self.value1 + "1"))
+
+ result = cmp(dn1, self.dn1)
+ self.assertEqual(result, 1)
+
+ result = cmp(self.dn1, dn1)
+ self.assertEqual(result, -1)
+
+ # Make sure dn's with more rdn's are greater
+ result = cmp(self.dn1, self.dn3)
+ self.assertEqual(result, -1)
+ result = cmp(self.dn3, self.dn1)
+ self.assertEqual(result, 1)
+
+
+ # Test startswith, endswith
+ container_dn = DN_class(self.container_dn)
+ base_container_dn = DN_class(self.base_container_dn)
+
+ self.assertTrue(base_container_dn.startswith(self.rdn1))
+ self.assertTrue(base_container_dn.startswith(self.dn1))
+ self.assertTrue(base_container_dn.startswith(self.dn1 + container_dn))
+ self.assertFalse(base_container_dn.startswith(self.dn2))
+ self.assertFalse(base_container_dn.startswith(self.rdn2))
+ self.assertTrue(base_container_dn.startswith((self.dn1)))
+ self.assertTrue(base_container_dn.startswith((self.rdn1)))
+ self.assertFalse(base_container_dn.startswith((self.rdn2)))
+ self.assertTrue(base_container_dn.startswith((self.rdn2, self.rdn1)))
+ self.assertTrue(base_container_dn.startswith((self.dn1, self.dn2)))
+
+ self.assertTrue(base_container_dn.endswith(self.base_dn))
+ self.assertTrue(base_container_dn.endswith(container_dn + self.base_dn))
+ self.assertFalse(base_container_dn.endswith(DN(self.base_rdn1)))
+ self.assertTrue(base_container_dn.endswith(DN(self.base_rdn2)))
+ self.assertTrue(base_container_dn.endswith((DN(self.base_rdn1), DN(self.base_rdn2))))
+
+ # Test "in" membership
+ self.assertTrue(self.container_rdn1 in container_dn)
+ self.assertTrue(container_dn in container_dn)
+ self.assertFalse(self.base_rdn1 in container_dn)
+
+ self.assertTrue(self.container_rdn1 in base_container_dn)
+ self.assertTrue(container_dn in base_container_dn)
+ self.assertTrue(container_dn + self.base_dn in
+ base_container_dn)
+ self.assertTrue(self.dn1 + container_dn + self.base_dn in
+ base_container_dn)
+ self.assertTrue(self.dn1 + container_dn + self.base_dn ==
+ base_container_dn)
+
+ self.assertFalse(self.container_rdn1 in self.base_dn)
+
+ def test_indexing(self):
+ for DN_class in (DN, EditableDN):
+ dn1 = DN_class(self.dn1)
+ dn2 = DN_class(self.dn2)
+ dn3 = DN_class(self.dn3)
+
+ self.assertEqual(dn1[0], self.rdn1)
+ self.assertEqual(dn1[self.rdn1.attr], self.rdn1.value)
+ with self.assertRaises(KeyError):
+ dn1['foo']
+
+ self.assertEqual(dn2[0], self.rdn2)
+ self.assertEqual(dn2[self.rdn2.attr], self.rdn2.value)
+ with self.assertRaises(KeyError):
+ dn2['foo']
+
+ self.assertEqual(dn3[0], self.rdn1)
+ self.assertEqual(dn3[self.rdn1.attr], self.rdn1.value)
+ self.assertEqual(dn3[1], self.rdn2)
+ self.assertEqual(dn3[self.rdn2.attr], self.rdn2.value)
+ with self.assertRaises(KeyError):
+ dn3['foo']
+
+ with self.assertRaises(TypeError):
+ dn3[1.0]
+
+ def test_assignments(self):
+ for DN_class in (DN, EditableDN):
+ dn_low = 0
+ dn_high = 6
+
+ rdn_args = make_rdn_args(dn_low, dn_high, 'tuple',
+ default_rdn_attr_arg, default_rdn_value_arg)
+ dn1 = DN_class(*rdn_args)
+
+ rdn_args = make_rdn_args(dn_low, dn_high, 'list',
+ default_rdn_attr_arg, default_rdn_value_arg)
+ dn2 = DN_class(*rdn_args)
+
+ rdn_args = make_rdn_args(dn_low, dn_high, 'RDN',
+ default_rdn_attr_arg, default_rdn_value_arg)
+ dn3 = DN_class(*rdn_args)
+
+ self.assertEqual(dn1, dn2)
+ self.assertEqual(dn1, dn3)
+
+ for i in range(dn_low, dn_high):
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
+
+ self.assertEqual(dn1[i].attr, attr)
+ self.assertEqual(dn1[i].value, value)
+ self.assertEqual(dn1[attr], value)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+
+ self.assertEqual(dn2[i].attr, attr)
+ self.assertEqual(dn2[i].value, value)
+ self.assertEqual(dn2[attr], value)
+ self.assertExpectedClass(DN_class, dn2, 'self')
+ self.assertExpectedClass(DN_class, dn2[i], 'RDN')
+ for j in range(0, len(dn2[i])):
+ self.assertExpectedClass(DN_class, dn2[i][j], 'AVA')
+
+ self.assertEqual(dn3[i].attr, attr)
+ self.assertEqual(dn3[i].value, value)
+ self.assertEqual(dn3[attr], value)
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ self.assertExpectedClass(DN_class, dn3[i], 'RDN')
+ for j in range(0, len(dn3[i])):
+ self.assertExpectedClass(DN_class, dn3[i][j], 'AVA')
+
+
+ for i in range(dn_low, dn_high):
+ if i % 2:
+ orig_attr = default_rdn_attr_arg(i)
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
+
+ if DN_class.is_mutable:
+ dn1[i] = attr, value
+ else:
+ with self.assertRaises(TypeError):
+ dn1[i] = attr, value
+
+ if DN_class.is_mutable:
+ dn2[orig_attr] = (attr, value)
+ else:
+ with self.assertRaises(TypeError):
+ dn2[orig_attr] = (attr, value)
+
+ if DN_class.is_mutable:
+ dn3[i] = RDN((attr, value))
+ else:
+ with self.assertRaises(TypeError):
+ dn3[i] = RDN((attr, value))
+
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+
+ self.assertExpectedClass(DN_class, dn2, 'self')
+ for i in range(0, len(dn2)):
+ self.assertExpectedClass(DN_class, dn2[i], 'RDN')
+ for j in range(0, len(dn2[i])):
+ self.assertExpectedClass(DN_class, dn2[i][j], 'AVA')
+
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ for i in range(0, len(dn3)):
+ self.assertExpectedClass(DN_class, dn3[i], 'RDN')
+ for j in range(0, len(dn3[i])):
+ self.assertExpectedClass(DN_class, dn3[i][j], 'AVA')
+
+
+ if DN_class.is_mutable:
+ self.assertEqual(dn1, dn2)
+ self.assertEqual(dn1, dn3)
+
+ for i in range(dn_low, dn_high):
+ if i % 2:
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
+ else:
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
+ self.assertEqual(dn1[i].attr, attr)
+ self.assertEqual(dn1[i].value, value)
+ self.assertEqual(dn1[attr], value)
+
+ # Slices
+ slice_low = 2
+ slice_high = 4
+ slice_interval = range(slice_low, slice_high)
+
+ # Slices
+ # Assign via tuple
+ rdn_args = make_rdn_args(dn_low, dn_high, 'tuple',
+ default_rdn_attr_arg, default_rdn_value_arg)
+ dn = DN_class(*rdn_args)
+
+ dn_slice = make_rdn_args(slice_low, slice_high, 'tuple',
+ alt_rdn_attr_arg, alt_rdn_value_arg)
+
+ if DN_class.is_mutable:
+ dn[slice_low:slice_high] = dn_slice
+ for i in range(dn_low, dn_high):
+ if i in slice_interval:
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
+ else:
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
+ self.assertEqual(dn[i].attr, attr)
+ self.assertEqual(dn[i].value, value)
+ self.assertEqual(dn[attr], value)
+
+ query_slice = dn[slice_low:slice_high]
+ for i, query_rdn in enumerate(query_slice):
+ slice_rdn = RDN(dn_slice[i])
+ self.assertEqual(slice_rdn, query_rdn)
+ else:
+ with self.assertRaises(TypeError):
+ dn[slice_low:slice_high] = dn_slice
+
+
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ # insert
+ dn = DN_class(self.rdn2)
+
+ if DN_class.is_mutable:
+ dn.insert(0, self.rdn1)
+ self.assertEqual(dn, self.dn3)
+ else:
+ with self.assertRaises(AttributeError):
+ dn.insert(0, self.rdn1)
+
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+ dn = DN_class(self.rdn1)
+
+ if DN_class.is_mutable:
+ dn.insert(1, (self.attr2, self.value2))
+ self.assertEqual(dn, self.dn3)
+ else:
+ with self.assertRaises(AttributeError):
+ dn.insert(1, (self.attr2, self.value2))
+
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ # Slices
+ # Assign via RDN
+ rdn_args = make_rdn_args(dn_low, dn_high, 'tuple',
+ default_rdn_attr_arg, default_rdn_value_arg)
+ dn = DN_class(*rdn_args)
+
+ dn_slice = make_rdn_args(slice_low, slice_high, 'RDN',
+ alt_rdn_attr_arg, alt_rdn_value_arg)
+
+ if DN_class.is_mutable:
+ dn[slice_low:slice_high] = dn_slice
+ for i in range(dn_low, dn_high):
+ if i in slice_interval:
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
+ else:
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
+ self.assertEqual(dn[i].value, value)
+ self.assertEqual(dn[attr], value)
+
+ query_slice = dn[slice_low:slice_high]
+ for i, query_rdn in enumerate(query_slice):
+ slice_rdn = dn_slice[i]
+ self.assertEqual(slice_rdn, query_rdn)
+ else:
+ with self.assertRaises(TypeError):
+ dn[slice_low:slice_high] = dn_slice
+
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ def test_iter(self):
+ for DN_class in (DN, EditableDN):
+ dn1 = DN_class(self.dn1)
+ dn2 = DN_class(self.dn2)
+ dn3 = DN_class(self.dn3)
+
+ self.assertEqual(len(dn1), 1)
+ self.assertEqual(dn1[:], [self.rdn1])
+ for i, ava in enumerate(dn1):
+ if i == 0:
+ self.assertEqual(ava, self.rdn1)
+ else:
+ self.fail("got iteration index %d, but len=%d" % (i, len(self.rdn1)))
+
+ self.assertEqual(len(dn2), 1)
+ self.assertEqual(dn2[:], [self.rdn2])
+ for i, ava in enumerate(dn2):
+ if i == 0:
+ self.assertEqual(ava, self.rdn2)
+ else:
+ self.fail("got iteration index %d, but len=%d" % (i, len(self.rdn2)))
+
+ self.assertEqual(len(dn3), 2)
+ self.assertEqual(dn3[:], [self.rdn1, self.rdn2])
+ for i, ava in enumerate(dn3):
+ if i == 0:
+ self.assertEqual(ava, self.rdn1)
+ elif i == 1:
+ self.assertEqual(ava, self.rdn2)
+ else:
+ self.fail("got iteration index %d, but len=%d" % (i, len(dn3)))
+
+
+ def test_concat(self):
+ for DN_class in (DN, EditableDN):
+ dn1 = DN_class((self.attr1, self.value1))
+ dn2 = DN_class([self.attr2, self.value2])
+
+ # in-place addtion
+
+ # Note: If __iadd__ is not available Python will emulate += by
+ # replacing the lhs object with the result of __add__ (if available).
+ # Thus += works for both immutable and mutable RDN,DN object, the only
+ # difference is an immutable without __iadd__ will have a different object
+ # on the lhs after the operator evaluates.
+
+ dn1 += dn2
+ self.assertEqual(dn1, self.dn3)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+
+
+ dn1 = DN_class((self.attr1, self.value1))
+ dn1 += self.rdn2
+ self.assertEqual(dn1, self.dn3)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+
+
+ dn1 = DN_class((self.attr1, self.value1))
+ dn1 += self.dn2
+ self.assertEqual(dn1, self.dn3)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+
+
+ dn1 = DN_class((self.attr1, self.value1))
+ dn1 += self.str_dn2
+ self.assertEqual(dn1, self.dn3)
+ self.assertExpectedClass(DN_class, dn1, 'self')
+ for i in range(0, len(dn1)):
+ self.assertExpectedClass(DN_class, dn1[i], 'RDN')
+ for j in range(0, len(dn1[i])):
+ self.assertExpectedClass(DN_class, dn1[i][j], 'AVA')
+
+
+ # concatenation
+ dn1 = DN_class((self.attr1, self.value1))
+ dn3 = dn1 + dn2
+ self.assertEqual(dn3, self.dn3)
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ for i in range(0, len(dn3)):
+ self.assertExpectedClass(DN_class, dn3[i], 'RDN')
+ for j in range(0, len(dn3[i])):
+ self.assertExpectedClass(DN_class, dn3[i][j], 'AVA')
+
+
+ dn1 = DN_class((self.attr1, self.value1))
+ dn3 = dn1 + self.rdn2
+ self.assertEqual(dn3, self.dn3)
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ for i in range(0, len(dn3)):
+ self.assertExpectedClass(DN_class, dn3[i], 'RDN')
+ for j in range(0, len(dn3[i])):
+ self.assertExpectedClass(DN_class, dn3[i][j], 'AVA')
+
+ dn3 = dn1 + self.str_rdn2
+ self.assertEqual(dn3, self.dn3)
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ for i in range(0, len(dn3)):
+ self.assertExpectedClass(DN_class, dn3[i], 'RDN')
+ self.assertExpectedClass(DN_class, dn3[i][0], 'AVA')
+
+ dn3 = dn1 + self.str_dn2
+ self.assertEqual(dn3, self.dn3)
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ for i in range(0, len(dn3)):
+ self.assertExpectedClass(DN_class, dn3[i], 'RDN')
+ for j in range(0, len(dn3[i])):
+ self.assertExpectedClass(DN_class, dn3[i][j], 'AVA')
+
+ dn3 = dn1 + self.dn2
+ self.assertEqual(dn3, self.dn3)
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ self.assertExpectedClass(DN_class, dn3, 'self')
+ for i in range(0, len(dn3)):
+ self.assertExpectedClass(DN_class, dn3[i], 'RDN')
+ for j in range(0, len(dn3[i])):
+ self.assertExpectedClass(DN_class, dn3[i][j], 'AVA')
+
+ def test_find(self):
+ for DN_class in (DN, EditableDN):
+ # -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
+ dn = DN_class('t=0,t=1,cn=bob,t=3,t=4,t=5,cn=bob,t=7,t=8,t=9')
+ pat = DN_class('cn=bob')
+
+ # forward
+ self.assertEqual(dn.find(pat), 2)
+ self.assertEqual(dn.find(pat, 1), 2)
+ self.assertEqual(dn.find(pat, 1, 3), 2)
+ self.assertEqual(dn.find(pat, 2, 3), 2)
+ self.assertEqual(dn.find(pat, 6), 6)
+
+ self.assertEqual(dn.find(pat, 7), -1)
+ self.assertEqual(dn.find(pat, 1, 2), -1)
+
+ with self.assertRaises(ValueError):
+ self.assertEqual(dn.index(pat, 7), -1)
+ with self.assertRaises(ValueError):
+ self.assertEqual(dn.index(pat, 1, 2), -1)
+
+ # reverse
+ self.assertEqual(dn.rfind(pat), 6)
+ self.assertEqual(dn.rfind(pat, -4), 6)
+ self.assertEqual(dn.rfind(pat, 6), 6)
+ self.assertEqual(dn.rfind(pat, 6, 8), 6)
+ self.assertEqual(dn.rfind(pat, 6, 8), 6)
+ self.assertEqual(dn.rfind(pat, -8), 6)
+ self.assertEqual(dn.rfind(pat, -8, -4), 6)
+ self.assertEqual(dn.rfind(pat, -8, -5), 2)
+
+ self.assertEqual(dn.rfind(pat, 7), -1)
+ self.assertEqual(dn.rfind(pat, -3), -1)
+
+ with self.assertRaises(ValueError):
+ self.assertEqual(dn.rindex(pat, 7), -1)
+ with self.assertRaises(ValueError):
+ self.assertEqual(dn.rindex(pat, -3), -1)
+
+
+ def test_replace(self):
+ for DN_class in (DN, EditableDN):
+ dn = DN_class('t=0,t=1,t=2,t=3,t=4,t=5,t=6,t=7,t=8,t=9')
+ pat = DN('cn=bob')
+ replacement = DN('cn=bob')
+
+ if DN_class.is_mutable:
+ n_replaced = dn.replace(pat, replacement)
+ self.assertEqual(n_replaced, 0)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ pat = DN('t=2')
+ if DN_class.is_mutable:
+ expected_dn = DN('t=0,t=1,cn=bob,t=3,t=4,t=5,t=6,t=7,t=8,t=9')
+ n_replaced = dn.replace(pat, replacement)
+ self.assertEqual(n_replaced, 1)
+ self.assertEqual(dn, expected_dn)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ dn = DN_class('t=0,t=1,t=2,t=3,t=4,t=5,t=6,t=7,t=2,t=9')
+ if DN_class.is_mutable:
+ expected_dn = DN('t=0,t=1,cn=bob,t=3,t=4,t=5,t=6,t=7,t=2,t=9')
+ n_replaced = dn.replace(pat, replacement, 1)
+ self.assertEqual(n_replaced, 1)
+ self.assertEqual(dn, expected_dn)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement, 1)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ dn = DN_class('t=0,t=1,t=2,t=3,t=4,t=5,t=6,t=7,t=2,t=9')
+ if DN_class.is_mutable:
+ expected_dn = DN('t=0,t=1,cn=bob,t=3,t=4,t=5,t=6,t=7,t=2,t=9')
+ n_replaced = dn.replace(pat, replacement, 1)
+ self.assertEqual(n_replaced, 1)
+ self.assertEqual(dn, expected_dn)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement, 1)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ replacement = DN('cn=bob,ou=people')
+
+ dn = DN_class('t=0,t=1,t=2,t=3,t=4,t=5,t=6,t=7,t=2,t=9')
+ if DN_class.is_mutable:
+ expected_dn = DN('t=0,t=1,cn=bob,ou=people,t=3,t=4,t=5,t=6,t=7,t=2,t=9')
+ n_replaced = dn.replace(pat, replacement, 1)
+ self.assertEqual(n_replaced, 1)
+ self.assertEqual(dn, expected_dn)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement, 1)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ dn = DN_class('t=0,t=1,t=2,t=3,t=4,t=5,t=6,t=7,t=2,t=9')
+ if DN_class.is_mutable:
+ expected_dn = DN('t=0,t=1,cn=bob,ou=people,t=3,t=4,t=5,t=6,t=7,cn=bob,ou=people,t=9')
+ n_replaced = dn.replace(pat, replacement)
+ self.assertEqual(n_replaced, 2)
+ self.assertEqual(dn, expected_dn)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ pat = DN('t=3,t=4')
+ replacement = DN('cn=bob')
+ dn = DN_class('t=0,t=1,t=2,t=3,t=4,t=5,t=6,t=7,t=8,t=9')
+ if DN_class.is_mutable:
+ expected_dn = DN('t=0,t=1,t=2,cn=bob,t=5,t=6,t=7,t=8,t=9')
+ n_replaced = dn.replace(pat, replacement)
+ self.assertEqual(n_replaced, 1)
+ self.assertEqual(dn, expected_dn)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ pat = DN('t=3,t=4')
+ replacement = DN('cn=bob,ou=people')
+ dn = DN_class('t=0,t=1,t=2,t=3,t=4,t=5,t=6,t=7,t=8,t=9')
+ if DN_class.is_mutable:
+ expected_dn = DN('t=0,t=1,t=2,cn=bob,ou=people,t=5,t=6,t=7,t=8,t=9')
+ n_replaced = dn.replace(pat, replacement)
+ self.assertEqual(n_replaced, 1)
+ self.assertEqual(dn, expected_dn)
+ else:
+ with self.assertRaises(AttributeError):
+ n_replaced = dn.replace(pat, replacement)
+ self.assertExpectedClass(DN_class, dn, 'self')
+ for i in range(0, len(dn)):
+ self.assertExpectedClass(DN_class, dn[i], 'RDN')
+ for j in range(0, len(dn[i])):
+ self.assertExpectedClass(DN_class, dn[i][j], 'AVA')
+
+ def test_hashing(self):
+ # create DN's that are equal but differ in case
+ immutable_dn1 = DN((self.attr1.lower(), self.value1.upper()))
+ immutable_dn2 = DN((self.attr1.upper(), self.value1.lower()))
+
+ mutable_dn1 = EditableDN((self.attr1.lower(), self.value1.upper()))
+ mutable_dn2 = EditableDN((self.attr1.upper(), self.value1.lower()))
+
+ # Immutable DN's that are equal should hash to the same value.
+ # Mutable DN's should not be hashable.
+
+ self.assertEqual(immutable_dn1, immutable_dn2)
+ self.assertEqual(immutable_dn1, mutable_dn1)
+ self.assertEqual(immutable_dn1, mutable_dn2)
+ self.assertEqual(mutable_dn1, immutable_dn2)
+
+ # Good, everyone's equal, now verify their hash values
+
+ self.assertEqual(hash(immutable_dn1), hash(immutable_dn2))
+ with self.assertRaises(TypeError):
+ hash(mutable_dn1)
+ with self.assertRaises(TypeError):
+ hash(mutable_dn2)
+
+ # Different immutable DN objects with the same value should
+ # map to 1 common key and 1 member in a set. The key and
+ # member are based on the object's value.
+ #
+ # Mutable DN objects should be unhashable.
+
+ for DN_class in (DN, EditableDN):
+ dn1_a = DN_class(self.dn1)
+ dn1_b = DN_class(self.dn1)
+
+ dn2_a = DN_class(self.dn2)
+ dn2_b = DN_class(self.dn2)
+
+ dn3_a = DN_class(self.dn3)
+ dn3_b = DN_class(self.dn3)
+
+ self.assertEqual(dn1_a, dn1_b)
+ self.assertEqual(dn2_a, dn2_b)
+ self.assertEqual(dn3_a, dn3_b)
+
+ d = dict()
+ s = set()
+
+ if DN_class.is_mutable:
+ with self.assertRaises(TypeError):
+ d[dn1_a] = str(dn1_a)
+ with self.assertRaises(TypeError):
+ d[dn1_b] = str(dn1_b)
+ with self.assertRaises(TypeError):
+ d[dn2_a] = str(dn2_a)
+ with self.assertRaises(TypeError):
+ d[dn2_b] = str(dn2_b)
+
+ with self.assertRaises(TypeError):
+ s.add(dn1_a)
+ with self.assertRaises(TypeError):
+ s.add(dn1_b)
+ with self.assertRaises(TypeError):
+ s.add(dn2_a)
+ with self.assertRaises(TypeError):
+ s.add(dn2_b)
+ else:
+ d[dn1_a] = str(dn1_a)
+ d[dn1_b] = str(dn1_b)
+ d[dn2_a] = str(dn2_a)
+ d[dn2_b] = str(dn2_b)
+
+ s.add(dn1_a)
+ s.add(dn1_b)
+ s.add(dn2_a)
+ s.add(dn2_b)
+
+ self.assertEqual(len(d), 2)
+ self.assertEqual(len(s), 2)
+ self.assertEqual(sorted(d.keys()), sorted([dn1_a, dn2_a]))
+ self.assertEqual(sorted(s), sorted([dn1_a, dn2_a]))
+
+ self.assertTrue(dn1_a in d)
+ self.assertTrue(dn1_b in d)
+ self.assertTrue(dn2_a in d)
+ self.assertTrue(dn2_b in d)
+ self.assertFalse(dn3_a in d)
+ self.assertFalse(dn3_b in d)
+
+ self.assertTrue(d.has_key(dn1_a))
+ self.assertTrue(d.has_key(dn1_b))
+ self.assertTrue(d.has_key(dn2_a))
+ self.assertTrue(d.has_key(dn2_b))
+ self.assertFalse(d.has_key(dn3_a))
+ self.assertFalse(d.has_key(dn3_b))
+
+ self.assertTrue(dn1_a in s)
+ self.assertTrue(dn1_b in s)
+ self.assertTrue(dn2_a in s)
+ self.assertTrue(dn2_b in s)
+ self.assertFalse(dn3_a in s)
+ self.assertFalse(dn3_b in s)
+
+ def test_coerce(self):
+ # Coerce an immutable to a mutable
+ immutable_dn3 = DN(self.dn3)
+ mutable_dn3 = EditableDN(immutable_dn3)
+ self.assertEqual(mutable_dn3, self.dn3)
+ self.assertEqual(mutable_dn3, immutable_dn3)
+
+ # Coerce a mutable to an immutable
+ mutable_dn3 = EditableDN(self.dn3)
+ immutable_dn3 = DN(mutable_dn3)
+ self.assertEqual(immutable_dn3, self.dn3)
+ self.assertEqual(immutable_dn3, mutable_dn3)
+
+class TestEscapes(unittest.TestCase):
+ def setUp(self):
+ self.privilege = 'R,W privilege'
+ self.dn_str_hex_escape = 'cn=R\\2cW privilege,cn=privileges,cn=pbac,dc=idm,dc=lab,dc=bos,dc=redhat,dc=com'
+ self.dn_str_backslash_escape = 'cn=R\\,W privilege,cn=privileges,cn=pbac,dc=idm,dc=lab,dc=bos,dc=redhat,dc=com'
+
+ def test_escape(self):
+ for DN_class in (DN, EditableDN):
+ dn = DN_class(self.dn_str_hex_escape)
+ self.assertEqual(dn['cn'], self.privilege)
+ self.assertEqual(dn[0].value, self.privilege)
+
+ dn = DN_class(self.dn_str_backslash_escape)
+ self.assertEqual(dn['cn'], self.privilege)
+ self.assertEqual(dn[0].value, self.privilege)
+
+class TestInternationalization(unittest.TestCase):
+ def setUp(self):
+ # Hello in Arabic
+ self.arabic_hello_utf8 = '\xd9\x85\xd9\x83\xd9\x8a\xd9\x84' + \
+ '\xd8\xb9\x20\xd9\x85\xd8\xa7\xd9' + \
+ '\x84\xd9\x91\xd8\xb3\xd9\x84\xd8\xa7'
+
+ self.arabic_hello_unicode = self.arabic_hello_utf8.decode('utf-8')
+
+ def test_i18n(self):
+ self.assertEqual(self.arabic_hello_utf8,
+ self.arabic_hello_unicode.encode('utf-8'))
+
+ # AVA's
+ # test attr i18n
+ for AVA_class in (AVA, EditableAVA):
+ ava1 = AVA_class(self.arabic_hello_unicode, 'foo')
+ self.assertIsInstance(ava1.attr, unicode)
+ self.assertIsInstance(ava1.value, unicode)
+ self.assertEqual(ava1.attr, self.arabic_hello_unicode)
+ self.assertEqual(str(ava1), self.arabic_hello_utf8+'=foo')
+
+ ava1 = AVA_class(self.arabic_hello_utf8, 'foo')
+ self.assertIsInstance(ava1.attr, unicode)
+ self.assertIsInstance(ava1.value, unicode)
+ self.assertEqual(ava1.attr, self.arabic_hello_unicode)
+ self.assertEqual(str(ava1), self.arabic_hello_utf8+'=foo')
+
+ # test value i18n
+ ava1 = AVA_class('cn', self.arabic_hello_unicode)
+ self.assertIsInstance(ava1.attr, unicode)
+ self.assertIsInstance(ava1.value, unicode)
+ self.assertEqual(ava1.value, self.arabic_hello_unicode)
+ self.assertEqual(str(ava1), 'cn='+self.arabic_hello_utf8)
+
+ ava1 = AVA_class('cn', self.arabic_hello_utf8)
+ self.assertIsInstance(ava1.attr, unicode)
+ self.assertIsInstance(ava1.value, unicode)
+ self.assertEqual(ava1.value, self.arabic_hello_unicode)
+ self.assertEqual(str(ava1), 'cn='+self.arabic_hello_utf8)
+
+ # RDN's
+ # test attr i18n
+ for RDN_class in (RDN, EditableRDN):
+ rdn1 = RDN_class((self.arabic_hello_unicode, 'foo'))
+ self.assertIsInstance(rdn1.attr, unicode)
+ self.assertIsInstance(rdn1.value, unicode)
+ self.assertEqual(rdn1.attr, self.arabic_hello_unicode)
+ self.assertEqual(str(rdn1), self.arabic_hello_utf8+'=foo')
+
+ rdn1 = RDN_class((self.arabic_hello_utf8, 'foo'))
+ self.assertIsInstance(rdn1.attr, unicode)
+ self.assertIsInstance(rdn1.value, unicode)
+ self.assertEqual(rdn1.attr, self.arabic_hello_unicode)
+ self.assertEqual(str(rdn1), self.arabic_hello_utf8+'=foo')
+
+ # test value i18n
+ rdn1 = RDN_class(('cn', self.arabic_hello_unicode))
+ self.assertIsInstance(rdn1.attr, unicode)
+ self.assertIsInstance(rdn1.value, unicode)
+ self.assertEqual(rdn1.value, self.arabic_hello_unicode)
+ self.assertEqual(str(rdn1), 'cn='+self.arabic_hello_utf8)
+
+ rdn1 = RDN_class(('cn', self.arabic_hello_utf8))
+ self.assertIsInstance(rdn1.attr, unicode)
+ self.assertIsInstance(rdn1.value, unicode)
+ self.assertEqual(rdn1.value, self.arabic_hello_unicode)
+ self.assertEqual(str(rdn1), 'cn='+self.arabic_hello_utf8)
+
+ # DN's
+ # test attr i18n
+ for DN_class in (DN, EditableDN):
+ dn1 = DN_class((self.arabic_hello_unicode, 'foo'))
+ self.assertIsInstance(dn1[0].attr, unicode)
+ self.assertIsInstance(dn1[0].value, unicode)
+ self.assertEqual(dn1[0].attr, self.arabic_hello_unicode)
+ self.assertEqual(str(dn1), self.arabic_hello_utf8+'=foo')
+
+ dn1 = DN_class((self.arabic_hello_utf8, 'foo'))
+ self.assertIsInstance(dn1[0].attr, unicode)
+ self.assertIsInstance(dn1[0].value, unicode)
+ self.assertEqual(dn1[0].attr, self.arabic_hello_unicode)
+ self.assertEqual(str(dn1), self.arabic_hello_utf8+'=foo')
+
+ # test value i18n
+ dn1 = DN_class(('cn', self.arabic_hello_unicode))
+ self.assertIsInstance(dn1[0].attr, unicode)
+ self.assertIsInstance(dn1[0].value, unicode)
+ self.assertEqual(dn1[0].value, self.arabic_hello_unicode)
+ self.assertEqual(str(dn1), 'cn='+self.arabic_hello_utf8)
+
+ dn1 = DN_class(('cn', self.arabic_hello_utf8))
+ self.assertIsInstance(dn1[0].attr, unicode)
+ self.assertIsInstance(dn1[0].value, unicode)
+ self.assertEqual(dn1[0].value, self.arabic_hello_unicode)
+ self.assertEqual(str(dn1), 'cn='+self.arabic_hello_utf8)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ipatests/test_ipapython/test_ipautil.py b/ipatests/test_ipapython/test_ipautil.py
new file mode 100644
index 000000000..650e1ce95
--- /dev/null
+++ b/ipatests/test_ipapython/test_ipautil.py
@@ -0,0 +1,69 @@
+# Authors:
+# Jan Cholasta <jcholast@redhat.com>
+#
+# Copyright (C) 2011 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/>.
+"""
+Test the `ipapython/ipautil.py` module.
+"""
+
+import nose
+
+from ipapython import ipautil
+
+class CheckIPAddress:
+ def __init__(self, addr):
+ self.description = "Test IP address parsing and verification (%s)" % addr
+
+ def __call__(self, addr, words=None, prefixlen=None):
+ try:
+ ip = ipautil.CheckedIPAddress(addr, match_local=False)
+ assert ip.words == words and ip.prefixlen == prefixlen
+ except:
+ assert words is None and prefixlen is None
+
+def test_ip_address():
+ addrs = [
+ ('10.11.12.13', (10, 11, 12, 13), 8),
+ ('10.11.12.13/14', (10, 11, 12, 13), 14),
+ ('10.11.12.13%zoneid',),
+ ('10.11.12.13%zoneid/14',),
+ ('10.11.12.1337',),
+ ('10.11.12.13/33',),
+ ('127.0.0.1',),
+ ('241.1.2.3',),
+ ('169.254.1.2',),
+ ('10.11.12.0/24',),
+ ('224.5.6.7',),
+ ('10.11.12.255/24',),
+
+ ('2001::1', (0x2001, 0, 0, 0, 0, 0, 0, 1), 64),
+ ('2001::1/72', (0x2001, 0, 0, 0, 0, 0, 0, 1), 72),
+ ('2001::1%zoneid', (0x2001, 0, 0, 0, 0, 0, 0, 1), 64),
+ ('2001::1%zoneid/72',),
+ ('2001::1beef',),
+ ('2001::1/129',),
+ ('::1',),
+ ('6789::1',),
+ ('fe89::1',),
+ ('2001::/64',),
+ ('ff01::1',),
+
+ ('junk',)
+ ]
+
+ for addr in addrs:
+ yield (CheckIPAddress(addr[0]),) + addr
diff --git a/ipatests/test_ipapython/test_keyring.py b/ipatests/test_ipapython/test_keyring.py
new file mode 100644
index 000000000..568fd5ee1
--- /dev/null
+++ b/ipatests/test_ipapython/test_keyring.py
@@ -0,0 +1,147 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+"""
+Test the `kernel_keyring.py` module.
+"""
+
+from nose.tools import raises, assert_raises # pylint: disable=E0611
+from ipapython import kernel_keyring
+
+TEST_KEY = 'ipa_test'
+TEST_VALUE = 'abc123'
+UPDATE_VALUE = '123abc'
+
+SIZE_256 = 'abcdefgh' * 32
+SIZE_512 = 'abcdefgh' * 64
+SIZE_1024 = 'abcdefgh' * 128
+
+class test_keyring(object):
+ """
+ Test the kernel keyring interface
+ """
+
+ def setUp(self):
+ try:
+ kernel_keyring.del_key(TEST_KEY)
+ except ValueError:
+ pass
+ try:
+ kernel_keyring.del_key(SIZE_256)
+ except ValueError:
+ pass
+
+ def test_01(self):
+ """
+ Add a new key and value, then remove it
+ """
+ kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+ result = kernel_keyring.read_key(TEST_KEY)
+ assert(result == TEST_VALUE)
+
+ kernel_keyring.del_key(TEST_KEY)
+
+ # Make sure it is gone
+ try:
+ result = kernel_keyring.read_key(TEST_KEY)
+ except ValueError, e:
+ assert e.message == 'key %s not found' % TEST_KEY
+
+ def test_02(self):
+ """
+ Delete a non_existent key
+ """
+ try:
+ kernel_keyring.del_key(TEST_KEY)
+ raise AssertionError('key should not have been deleted')
+ except ValueError:
+ pass
+
+ @raises(ValueError)
+ def test_03(self):
+ """
+ Add a duplicate key
+ """
+ kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+ kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+
+ def test_04(self):
+ """
+ Update the value in a key
+ """
+ kernel_keyring.update_key(TEST_KEY, UPDATE_VALUE)
+ result = kernel_keyring.read_key(TEST_KEY)
+ assert(result == UPDATE_VALUE)
+
+ # Now update it 10 times
+ for i in xrange(10):
+ kernel_keyring.update_key(TEST_KEY, 'test %d' % i)
+ result = kernel_keyring.read_key(TEST_KEY)
+ assert(result == 'test %d' % i)
+
+ kernel_keyring.del_key(TEST_KEY)
+
+ @raises(ValueError)
+ def test_05(self):
+ """
+ Read a non-existent key
+ """
+ result = kernel_keyring.read_key(TEST_KEY)
+
+ def test_06(self):
+ """
+ See if a key is available
+ """
+ kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+
+ result = kernel_keyring.has_key(TEST_KEY)
+ assert(result == True)
+ kernel_keyring.del_key(TEST_KEY)
+
+ result = kernel_keyring.has_key(TEST_KEY)
+ assert(result == False)
+
+ def test_07(self):
+ """
+ Test a 256-byte key
+ """
+ kernel_keyring.add_key(SIZE_256, TEST_VALUE)
+ result = kernel_keyring.read_key(SIZE_256)
+ assert(result == TEST_VALUE)
+
+ kernel_keyring.del_key(SIZE_256)
+
+ def test_08(self):
+ """
+ Test 512-bytes of data
+ """
+ kernel_keyring.add_key(TEST_KEY, SIZE_512)
+ result = kernel_keyring.read_key(TEST_KEY)
+ assert(result == SIZE_512)
+
+ kernel_keyring.del_key(TEST_KEY)
+
+ def test_09(self):
+ """
+ Test 1k bytes of data
+ """
+ kernel_keyring.add_key(TEST_KEY, SIZE_1024)
+ result = kernel_keyring.read_key(TEST_KEY)
+ assert(result == SIZE_1024)
+
+ kernel_keyring.del_key(TEST_KEY)
diff --git a/ipatests/test_ipapython/test_ssh.py b/ipatests/test_ipapython/test_ssh.py
new file mode 100644
index 000000000..2640af50d
--- /dev/null
+++ b/ipatests/test_ipapython/test_ssh.py
@@ -0,0 +1,76 @@
+# Authors:
+# Jan Cholasta <jcholast@redhat.com>
+#
+# Copyright (C) 2011 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/>.
+"""
+Test the `ipapython/ssh.py` module.
+"""
+
+import base64
+import nose
+
+from ipapython import ssh
+
+class CheckPublicKey:
+ def __init__(self, pk):
+ self.description = "Test SSH public key parsing (%s)" % repr(pk)
+
+ def __call__(self, pk, out):
+ try:
+ parsed = ssh.SSHPublicKey(pk)
+ assert parsed.openssh() == out
+ except Exception, e:
+ assert type(e) is out
+
+def test_public_key_parsing():
+ b64 = 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDGAX3xAeLeaJggwTqMjxNwa6XHBUAikXPGMzEpVrlLDCZtv00djsFTBi38PkgxBJVkgRWMrcBsr/35lq7P6w8KGIwA8GI48Z0qBS2NBMJ2u9WQ2hjLN6GdMlo77O0uJY3251p12pCVIS/bHRSq8kHO2No8g7KA9fGGcagPfQH+ee3t7HUkpbQkFTmbPPN++r3V8oVUk5LxbryB3UIIVzNmcSIn3JrXynlvui4MixvrtX6zx+O/bBo68o8/eZD26QrahVbA09fivrn/4h3TM019Eu/c2jOdckfU3cHUV/3Tno5d6JicibyaoDDK7S/yjdn5jhaz8MSEayQvFkZkiF0L'
+ raw = base64.b64decode(b64)
+ openssh = 'ssh-rsa %s' % b64
+
+ pks = [
+ ('\xff', UnicodeDecodeError),
+
+ (raw, openssh),
+ ('\0\0\0\x04none', u'none AAAABG5vbmU='),
+ ('\0\0\0', ValueError),
+ ('\0\0\0\0', ValueError),
+ ('\0\0\0\x01', ValueError),
+ ('\0\0\0\x01\xff', ValueError),
+
+ (b64, openssh),
+ (unicode(b64), openssh),
+ (u'\n%s\n\n' % b64, openssh),
+ (u'AAAABG5vbmU=', u'none AAAABG5vbmU='),
+ (u'AAAAB', ValueError),
+
+ (openssh, openssh),
+ (unicode(openssh), openssh),
+ (u'none AAAABG5vbmU=', u'none AAAABG5vbmU='),
+ (u'\t \t ssh-rsa \t \t%s\t \tthis is a comment\t \t ' % b64,
+ u'%s this is a comment' % openssh),
+ (u'opt3,opt2="\tx ",opt1,opt2="\\"x " %s comment ' % openssh,
+ u'opt1,opt2="\\"x ",opt3 %s comment' % openssh),
+ (u'ssh-rsa\n%s' % b64, ValueError),
+ (u'ssh-rsa\t%s' % b64, ValueError),
+ (u'vanitas %s' % b64, ValueError),
+ (u'@opt %s' % openssh, ValueError),
+ (u'opt=val %s' % openssh, ValueError),
+ (u'opt, %s' % openssh, ValueError),
+ ]
+
+ for pk in pks:
+ yield (CheckPublicKey(pk[0]),) + pk
diff --git a/ipatests/test_ipaserver/__init__.py b/ipatests/test_ipaserver/__init__.py
new file mode 100644
index 000000000..2192cc291
--- /dev/null
+++ b/ipatests/test_ipaserver/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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 `ipaserver` package.
+"""
diff --git a/ipatests/test_ipaserver/httptest.py b/ipatests/test_ipaserver/httptest.py
new file mode 100644
index 000000000..7f1b5b136
--- /dev/null
+++ b/ipatests/test_ipaserver/httptest.py
@@ -0,0 +1,52 @@
+# Authors:
+# Martin Kosek <mkosek@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+"""
+Base class for HTTP request tests
+"""
+
+import urllib
+import httplib
+
+from ipalib import api
+
+class Unauthorized_HTTP_test(object):
+ """
+ Base class for simple HTTP request tests executed against URI
+ with no required authorization
+ """
+ app_uri = ''
+ host = api.env.host
+ content_type = 'application/x-www-form-urlencoded'
+
+ def send_request(self, method='POST', params=None):
+ """
+ Send a request to HTTP server
+
+ :param key When not None, overrides default app_uri
+ """
+ if params is not None:
+ params = urllib.urlencode(params, True)
+ url = 'https://' + self.host + self.app_uri
+
+ headers = {'Content-Type' : self.content_type,
+ 'Referer' : url}
+
+ conn = httplib.HTTPSConnection(self.host)
+ conn.request(method, self.app_uri, params, headers)
+ return conn.getresponse()
diff --git a/ipatests/test_ipaserver/install/test_adtrustinstance.py b/ipatests/test_ipaserver/install/test_adtrustinstance.py
new file mode 100755
index 000000000..9a62f87ce
--- /dev/null
+++ b/ipatests/test_ipaserver/install/test_adtrustinstance.py
@@ -0,0 +1,59 @@
+# Authors:
+# Sumit Bose <sbose@redhat.com>
+#
+# Copyright (C) 2011 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/>.
+"""
+Test `adtrustinstance`
+"""
+
+import os
+import nose
+
+from ipaserver.install import adtrustinstance
+
+class test_adtrustinstance:
+ """
+ Test `adtrustinstance`.
+ """
+
+ def test_make_netbios_name(self):
+ s = adtrustinstance.make_netbios_name("ABCDEF")
+ assert s == 'ABCDEF' and isinstance(s, str)
+ s = adtrustinstance.make_netbios_name(U"ABCDEF")
+ assert s == 'ABCDEF' and isinstance(s, unicode)
+ s = adtrustinstance.make_netbios_name("abcdef")
+ assert s == 'ABCDEF'
+ s = adtrustinstance.make_netbios_name("abc.def")
+ assert s == 'ABC'
+ s = adtrustinstance.make_netbios_name("abcdefghijklmnopqr.def")
+ assert s == 'ABCDEFGHIJKLMNO'
+ s = adtrustinstance.make_netbios_name("A!$%B&/()C=?+*D")
+ assert s == 'ABCD'
+ s = adtrustinstance.make_netbios_name("!$%&/()=?+*")
+ assert not s
+
+ def test_check_netbios_name(self):
+ assert adtrustinstance.check_netbios_name("ABCDEF")
+ assert not adtrustinstance.check_netbios_name("abcdef")
+ assert adtrustinstance.check_netbios_name("ABCDE12345ABCDE")
+ assert not adtrustinstance.check_netbios_name("ABCDE12345ABCDE1")
+ assert not adtrustinstance.check_netbios_name("")
+
+ assert adtrustinstance.check_netbios_name(U"ABCDEF")
+ assert not adtrustinstance.check_netbios_name(U"abcdef")
+ assert adtrustinstance.check_netbios_name(U"ABCDE12345ABCDE")
+ assert not adtrustinstance.check_netbios_name(U"ABCDE12345ABCDE1")
diff --git a/ipatests/test_ipaserver/test_changepw.py b/ipatests/test_ipaserver/test_changepw.py
new file mode 100644
index 000000000..040c9cd36
--- /dev/null
+++ b/ipatests/test_ipaserver/test_changepw.py
@@ -0,0 +1,107 @@
+# Authors:
+# Martin Kosek <mkosek@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+
+import nose
+
+from httptest import Unauthorized_HTTP_test
+from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test
+from ipatests.util import assert_equal, assert_not_equal
+from ipalib import api, errors
+from ipapython.dn import DN
+import ldap
+
+testuser = u'tuser'
+old_password = u'old_password'
+new_password = u'new_password'
+
+class test_changepw(XMLRPC_test, Unauthorized_HTTP_test):
+ app_uri = '/ipa/session/change_password'
+
+ def setUp(self):
+ super(test_changepw, self).setUp()
+ try:
+ api.Command['user_add'](uid=testuser, givenname=u'Test', sn=u'User')
+ api.Command['passwd'](testuser, password=u'old_password')
+ except errors.ExecutionError, e:
+ raise nose.SkipTest(
+ 'Cannot set up test user: %s' % e
+ )
+
+ def tearDown(self):
+ try:
+ api.Command['user_del']([testuser])
+ except errors.NotFound:
+ pass
+ super(test_changepw, self).tearDown()
+
+ def _changepw(self, user, old_password, new_password):
+ return self.send_request(params={'user': str(user),
+ 'old_password' : str(old_password),
+ 'new_password' : str(new_password)},
+ )
+
+ def _checkpw(self, user, password):
+ dn = str(DN(('uid', user), api.env.container_user, api.env.basedn))
+ conn = ldap.initialize(api.env.ldap_uri)
+ try:
+ conn.simple_bind_s(dn, password)
+ finally:
+ conn.unbind_s()
+
+ def test_bad_options(self):
+ for params in (None, # no params
+ {'user': 'foo'}, # missing options
+ {'user': 'foo',
+ 'old_password' : 'old'}, # missing option
+ {'user': 'foo',
+ 'old_password' : 'old',
+ 'new_password' : ''}, # empty option
+ ):
+ response = self.send_request(params=params)
+ assert_equal(response.status, 400)
+ assert_equal(response.reason, 'Bad Request')
+
+ def test_invalid_auth(self):
+ response = self._changepw(testuser, 'wrongpassword', 'new_password')
+
+ assert_equal(response.status, 200)
+ assert_equal(response.getheader('X-IPA-Pwchange-Result'), 'invalid-password')
+
+ # make sure that password is NOT changed
+ self._checkpw(testuser, old_password)
+
+ def test_pwpolicy_error(self):
+ response = self._changepw(testuser, old_password, '1')
+
+ assert_equal(response.status, 200)
+ assert_equal(response.getheader('X-IPA-Pwchange-Result'), 'policy-error')
+ assert_equal(response.getheader('X-IPA-Pwchange-Policy-Error'),
+ 'Constraint violation: Password is too short')
+
+ # make sure that password is NOT changed
+ self._checkpw(testuser, old_password)
+
+ def test_pwpolicy_success(self):
+ response = self._changepw(testuser, old_password, new_password)
+
+ assert_equal(response.status, 200)
+ assert_equal(response.getheader('X-IPA-Pwchange-Result'), 'ok')
+
+ # make sure that password IS changed
+ self._checkpw(testuser, new_password)
diff --git a/ipatests/test_ipaserver/test_ldap.py b/ipatests/test_ipaserver/test_ldap.py
new file mode 100644
index 000000000..21363f2ef
--- /dev/null
+++ b/ipatests/test_ipaserver/test_ldap.py
@@ -0,0 +1,259 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+# Test some simple LDAP requests using the ldap2 backend
+
+# This fetches a certificate from a host principal so we can ensure that the
+# schema is working properly. We know this because the schema will tell the
+# encoder not to utf-8 encode binary attributes.
+
+# The DM password needs to be set in ~/.ipa/.dmpw
+
+import os
+
+import nose
+from nose.tools import assert_raises # pylint: disable=E0611
+import nss.nss as nss
+
+from ipaserver.plugins.ldap2 import ldap2
+from ipalib.plugins.service import service, service_show
+from ipalib.plugins.host import host
+from ipalib import api, x509, create_api, errors
+from ipapython import ipautil
+from ipapython.dn import DN
+
+class test_ldap(object):
+ """
+ Test various LDAP client bind methods.
+ """
+
+ def setUp(self):
+ self.conn = None
+ self.ldapuri = 'ldap://%s' % ipautil.format_netloc(api.env.host)
+ self.ccache = '/tmp/krb5cc_%d' % os.getuid()
+ nss.nss_init_nodb()
+ self.dn = DN(('krbprincipalname','ldap/%s@%s' % (api.env.host, api.env.realm)),
+ ('cn','services'),('cn','accounts'),api.env.basedn)
+
+ def tearDown(self):
+ if self.conn and self.conn.isconnected():
+ self.conn.disconnect()
+
+ def test_anonymous(self):
+ """
+ Test an anonymous LDAP bind using ldap2
+ """
+ self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+ self.conn.connect()
+ (dn, entry_attrs) = self.conn.get_entry(self.dn, ['usercertificate'])
+ cert = entry_attrs.get('usercertificate')
+ cert = cert[0]
+ serial = unicode(x509.get_serial_number(cert, x509.DER))
+ assert serial is not None
+
+ def test_GSSAPI(self):
+ """
+ Test a GSSAPI LDAP bind using ldap2
+ """
+ if not ipautil.file_exists(self.ccache):
+ raise nose.SkipTest('Missing ccache %s' % self.ccache)
+ self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+ self.conn.connect(ccache='FILE:%s' % self.ccache)
+ (dn, entry_attrs) = self.conn.get_entry(self.dn, ['usercertificate'])
+ cert = entry_attrs.get('usercertificate')
+ cert = cert[0]
+ serial = unicode(x509.get_serial_number(cert, x509.DER))
+ assert serial is not None
+
+ def test_simple(self):
+ """
+ Test a simple LDAP bind using ldap2
+ """
+ pwfile = api.env.dot_ipa + os.sep + ".dmpw"
+ if ipautil.file_exists(pwfile):
+ fp = open(pwfile, "r")
+ dm_password = fp.read().rstrip()
+ fp.close()
+ else:
+ raise nose.SkipTest("No directory manager password in %s" % pwfile)
+ self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+ self.conn.connect(bind_dn=DN(('cn', 'directory manager')), bind_pw=dm_password)
+ (dn, entry_attrs) = self.conn.get_entry(self.dn, ['usercertificate'])
+ cert = entry_attrs.get('usercertificate')
+ cert = cert[0]
+ serial = unicode(x509.get_serial_number(cert, x509.DER))
+ assert serial is not None
+
+ def test_Backend(self):
+ """
+ Test using the ldap2 Backend directly (ala ipa-server-install)
+ """
+
+ # Create our own api because the one generated for the tests is
+ # a client-only api. Then we register in the commands and objects
+ # we need for the test.
+ myapi = create_api(mode=None)
+ myapi.bootstrap(context='cli', in_server=True, in_tree=True)
+ myapi.register(ldap2)
+ myapi.register(host)
+ myapi.register(service)
+ myapi.register(service_show)
+ myapi.finalize()
+
+ pwfile = api.env.dot_ipa + os.sep + ".dmpw"
+ if ipautil.file_exists(pwfile):
+ fp = open(pwfile, "r")
+ dm_password = fp.read().rstrip()
+ fp.close()
+ else:
+ raise nose.SkipTest("No directory manager password in %s" % pwfile)
+ myapi.Backend.ldap2.connect(bind_dn=DN(('cn', 'Directory Manager')), bind_pw=dm_password)
+
+ result = myapi.Command['service_show']('ldap/%s@%s' % (api.env.host, api.env.realm,))
+ entry_attrs = result['result']
+ cert = entry_attrs.get('usercertificate')
+ cert = cert[0]
+ serial = unicode(x509.get_serial_number(cert, x509.DER))
+ assert serial is not None
+
+ def test_autobind(self):
+ """
+ Test an autobind LDAP bind using ldap2
+ """
+ ldapuri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % api.env.realm.replace('.','-')
+ self.conn = ldap2(shared_instance=False, ldap_uri=ldapuri)
+ try:
+ self.conn.connect(autobind=True)
+ except errors.ACIError:
+ raise nose.SkipTest("Only executed as root")
+ (dn, entry_attrs) = self.conn.get_entry(self.dn, ['usercertificate'])
+ cert = entry_attrs.get('usercertificate')
+ cert = cert[0]
+ serial = unicode(x509.get_serial_number(cert, x509.DER))
+ assert serial is not None
+
+
+class test_LDAPEntry(object):
+ """
+ Test the LDAPEntry class
+ """
+ cn1 = [u'test1']
+ cn2 = [u'test2']
+ dn1 = DN(('cn', cn1[0]))
+ dn2 = DN(('cn', cn2[0]))
+
+ def setUp(self):
+ self.ldapuri = 'ldap://%s' % ipautil.format_netloc(api.env.host)
+ self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+ self.conn.connect()
+
+ self.entry = self.conn.make_entry(self.dn1, cn=self.cn1)
+
+ def tearDown(self):
+ if self.conn and self.conn.isconnected():
+ self.conn.disconnect()
+
+ def test_entry(self):
+ e = self.entry
+ assert e.dn is self.dn1
+ assert u'cn' in e
+ assert u'cn' in e.keys()
+ assert 'CN' in e
+ assert 'CN' not in e.keys()
+ assert 'commonName' in e
+ assert 'commonName' not in e.keys()
+ assert e['CN'] is self.cn1
+ assert e['CN'] is e[u'cn']
+
+ e.dn = self.dn2
+ assert e.dn is self.dn2
+
+ def test_set_attr(self):
+ e = self.entry
+ e['commonName'] = self.cn2
+ assert u'cn' in e
+ assert u'cn' not in e.keys()
+ assert 'CN' in e
+ assert 'CN' not in e.keys()
+ assert 'commonName' in e
+ assert 'commonName' in e.keys()
+ assert e['CN'] is self.cn2
+ assert e['CN'] is e[u'cn']
+
+ def test_del_attr(self):
+ e = self.entry
+ del e['CN']
+ assert 'CN' not in e
+ assert 'CN' not in e.keys()
+ assert u'cn' not in e
+ assert u'cn' not in e.keys()
+ assert 'commonName' not in e
+ assert 'commonName' not in e.keys()
+
+ def test_popitem(self):
+ e = self.entry
+ assert e.popitem() == ('cn', self.cn1)
+ e.keys() == []
+
+ def test_setdefault(self):
+ e = self.entry
+ assert e.setdefault('cn', self.cn2) == self.cn1
+ assert e['cn'] == self.cn1
+ assert e.setdefault('xyz', self.cn2) == self.cn2
+ assert e['xyz'] == self.cn2
+
+ def test_update(self):
+ e = self.entry
+ e.update({'cn': self.cn2}, xyz=self.cn2)
+ assert e['cn'] == self.cn2
+ assert e['xyz'] == self.cn2
+
+ def test_pop(self):
+ e = self.entry
+ assert e.pop('cn') == self.cn1
+ assert 'cn' not in e
+ assert e.pop('cn', 'default') is 'default'
+ with assert_raises(KeyError):
+ e.pop('cn')
+
+ def test_clear(self):
+ e = self.entry
+ e.clear()
+ assert not e
+ assert 'cn' not in e
+
+ def test_has_key(self):
+ e = self.entry
+ assert not e.has_key('xyz')
+ assert e.has_key('cn')
+ assert e.has_key('COMMONNAME')
+
+ def test_get(self):
+ e = self.entry
+ assert e.get('cn') == self.cn1
+ assert e.get('commonname') == self.cn1
+ assert e.get('COMMONNAME', 'default') == self.cn1
+ assert e.get('bad key', 'default') == 'default'
+
+ def test_single_value(self):
+ e = self.entry
+ assert e.single_value('cn') == self.cn1[0]
+ assert e.single_value('commonname') == self.cn1[0]
+ assert e.single_value('COMMONNAME', 'default') == self.cn1[0]
+ assert e.single_value('bad key', 'default') == 'default'
diff --git a/ipatests/test_ipaserver/test_rpcserver.py b/ipatests/test_ipaserver/test_rpcserver.py
new file mode 100644
index 000000000..bd5673844
--- /dev/null
+++ b/ipatests/test_ipaserver/test_rpcserver.py
@@ -0,0 +1,247 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `ipaserver.rpc` module.
+"""
+
+import json
+
+from ipatests.util import create_test_api, assert_equal, raises, PluginTester
+from ipatests.data import unicode_str
+from ipalib import errors, Command
+from ipaserver import rpcserver
+
+
+class StartResponse(object):
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.status = None
+ self.headers = None
+
+ def __call__(self, status, headers):
+ assert self.status is None
+ assert self.headers is None
+ assert isinstance(status, str)
+ assert isinstance(headers, list)
+ self.status = status
+ self.headers = headers
+
+
+def test_not_found():
+ f = rpcserver.HTTP_Status()
+ t = rpcserver._not_found_template
+ s = StartResponse()
+
+ # Test with an innocent URL:
+ url = '/ipa/foo/stuff'
+ assert_equal(
+ f.not_found(None, s, url, None),
+ [t % dict(url='/ipa/foo/stuff')]
+ )
+ assert s.status == '404 Not Found'
+ assert s.headers == [('Content-Type', 'text/html; charset=utf-8')]
+
+ # Test when URL contains any of '<>&'
+ s.reset()
+ url ='&nbsp;' + '<script>do_bad_stuff();</script>'
+ assert_equal(
+ f.not_found(None, s, url, None),
+ [t % dict(url='&amp;nbsp;&lt;script&gt;do_bad_stuff();&lt;/script&gt;')]
+ )
+ assert s.status == '404 Not Found'
+ assert s.headers == [('Content-Type', 'text/html; charset=utf-8')]
+
+
+def test_bad_request():
+ f = rpcserver.HTTP_Status()
+ t = rpcserver._bad_request_template
+ s = StartResponse()
+
+ assert_equal(
+ f.bad_request(None, s, 'illegal request'),
+ [t % dict(message='illegal request')]
+ )
+ assert s.status == '400 Bad Request'
+ assert s.headers == [('Content-Type', 'text/html; charset=utf-8')]
+
+
+def test_internal_error():
+ f = rpcserver.HTTP_Status()
+ t = rpcserver._internal_error_template
+ s = StartResponse()
+
+ assert_equal(
+ f.internal_error(None, s, 'request failed'),
+ [t % dict(message='request failed')]
+ )
+ assert s.status == '500 Internal Server Error'
+ assert s.headers == [('Content-Type', 'text/html; charset=utf-8')]
+
+
+def test_unauthorized_error():
+ f = rpcserver.HTTP_Status()
+ t = rpcserver._unauthorized_template
+ s = StartResponse()
+
+ assert_equal(
+ f.unauthorized(None, s, 'unauthorized', 'password-expired'),
+ [t % dict(message='unauthorized')]
+ )
+ assert s.status == '401 Unauthorized'
+ assert s.headers == [('Content-Type', 'text/html; charset=utf-8'),
+ ('X-IPA-Rejection-Reason', 'password-expired')]
+
+
+def test_params_2_args_options():
+ """
+ Test the `ipaserver.rpcserver.params_2_args_options` function.
+ """
+ f = rpcserver.params_2_args_options
+ args = ('Hello', u'world!')
+ options = dict(one=1, two=u'Two', three='Three')
+ assert f(tuple()) == (tuple(), dict())
+ assert f([args]) == (args, dict())
+ assert f([args, options]) == (args, options)
+
+
+class test_session(object):
+ klass = rpcserver.wsgi_dispatch
+
+ def test_route(self):
+ def app1(environ, start_response):
+ return (
+ 'from 1',
+ [environ[k] for k in ('SCRIPT_NAME', 'PATH_INFO')]
+ )
+
+ def app2(environ, start_response):
+ return (
+ 'from 2',
+ [environ[k] for k in ('SCRIPT_NAME', 'PATH_INFO')]
+ )
+
+ inst = self.klass()
+ inst.mount(app1, '/foo/stuff')
+ inst.mount(app2, '/bar')
+
+ d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/foo/stuff')
+ assert inst.route(d, None) == ('from 1', ['/ipa', '/foo/stuff'])
+
+ d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/bar')
+ assert inst.route(d, None) == ('from 2', ['/ipa', '/bar'])
+
+ def test_mount(self):
+ def app1(environ, start_response):
+ pass
+
+ def app2(environ, start_response):
+ pass
+
+ # Test that mount works:
+ inst = self.klass()
+ inst.mount(app1, 'foo')
+ assert inst['foo'] is app1
+ assert list(inst) == ['foo']
+
+ # Test that StandardError is raise if trying override a mount:
+ e = raises(StandardError, inst.mount, app2, 'foo')
+ assert str(e) == '%s.mount(): cannot replace %r with %r at %r' % (
+ 'wsgi_dispatch', app1, app2, 'foo'
+ )
+
+ # Test mounting a second app:
+ inst.mount(app2, 'bar')
+ assert inst['bar'] is app2
+ assert list(inst) == ['bar', 'foo']
+
+
+class test_xmlserver(PluginTester):
+ """
+ Test the `ipaserver.rpcserver.xmlserver` plugin.
+ """
+
+ _plugin = rpcserver.xmlserver
+
+ def test_marshaled_dispatch(self): # FIXME
+ (o, api, home) = self.instance('Backend', in_server=True)
+
+
+class test_jsonserver(PluginTester):
+ """
+ Test the `ipaserver.rpcserver.jsonserver` plugin.
+ """
+
+ _plugin = rpcserver.jsonserver
+
+ def test_unmarshal(self):
+ """
+ Test the `ipaserver.rpcserver.jsonserver.unmarshal` method.
+ """
+ (o, api, home) = self.instance('Backend', in_server=True)
+
+ # Test with invalid JSON-data:
+ e = raises(errors.JSONError, o.unmarshal, 'this wont work')
+ assert isinstance(e.error, ValueError)
+ assert unicode(e.error) == 'No JSON object could be decoded'
+
+ # Test with non-dict type:
+ e = raises(errors.JSONError, o.unmarshal, json.dumps([1, 2, 3]))
+ assert unicode(e.error) == 'Request must be a dict'
+
+ params = [[1, 2], dict(three=3, four=4)]
+ # Test with missing method:
+ d = dict(params=params, id=18)
+ e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
+ assert unicode(e.error) == 'Request is missing "method"'
+
+ # Test with missing params:
+ d = dict(method='echo', id=18)
+ e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
+ assert unicode(e.error) == 'Request is missing "params"'
+
+ # Test with non-list params:
+ for p in ('hello', dict(args=tuple(), options=dict())):
+ d = dict(method='echo', id=18, params=p)
+ e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
+ assert unicode(e.error) == 'params must be a list'
+
+ # Test with other than 2 params:
+ for p in ([], [tuple()], [None, dict(), tuple()]):
+ d = dict(method='echo', id=18, params=p)
+ e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
+ assert unicode(e.error) == 'params must contain [args, options]'
+
+ # Test when args is not a list:
+ d = dict(method='echo', id=18, params=['args', dict()])
+ e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
+ assert unicode(e.error) == 'params[0] (aka args) must be a list'
+
+ # Test when options is not a dict:
+ d = dict(method='echo', id=18, params=[('hello', 'world'), 'options'])
+ e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
+ assert unicode(e.error) == 'params[1] (aka options) must be a dict'
+
+ # Test with valid values:
+ args = [u'jdoe']
+ options = dict(givenname=u'John', sn='Doe')
+ d = dict(method=u'user_add', params=[args, options], id=18)
+ assert o.unmarshal(json.dumps(d)) == (u'user_add', args, options, 18)
diff --git a/ipatests/test_pkcs10/__init__.py b/ipatests/test_pkcs10/__init__.py
new file mode 100644
index 000000000..cd03658cf
--- /dev/null
+++ b/ipatests/test_pkcs10/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2009 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 `pkcs10` package.
+"""
diff --git a/ipatests/test_pkcs10/test0.csr b/ipatests/test_pkcs10/test0.csr
new file mode 100644
index 000000000..eadfb70b4
--- /dev/null
+++ b/ipatests/test_pkcs10/test0.csr
@@ -0,0 +1,12 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIIBjjCB+AIBADBPMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEQ
+MA4GA1UEChMHRXhhbXBsZTEZMBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTCBnzAN
+BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyxsN5dmvyKiw+5nyrcO3a61sivZRg+ja
+kyNIyUo+tIUiYwTdpPESAHTWRlk0XhydauAkWfOIN7pR3a5Z+kQw8W7F+DuZze2M
+6wRNmN+NTrTlqnKOiMHBXhIM0Qxrx68GDctYqtnKTVT94FvvLl9XYVdUEi2ePTc2
+Nyfr1z66+W0CAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBAIf3r+Y6WHrFnttUqDow
+9/UCHtCeQlQoJqjjxi5wcjbkGwTgHbx/BPOd/8OVaHElboMXLGaZx+L/eFO6E9Yg
+mDOYv3OsibDFGaEhJrU8EnfuFZKnbrGeSC9Hkqrq+3OjqacaPla5N7MHKbfLY377
+ddbOHKzR0sURZ+ro4z3fATW2
+-----END NEW CERTIFICATE REQUEST-----
+
diff --git a/ipatests/test_pkcs10/test1.csr b/ipatests/test_pkcs10/test1.csr
new file mode 100644
index 000000000..0dad3ae1e
--- /dev/null
+++ b/ipatests/test_pkcs10/test1.csr
@@ -0,0 +1,13 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIIBwDCCASkCAQAwTzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+EDAOBgNVBAoTB0V4YW1wbGUxGTAXBgNVBAMTEHRlc3QuZXhhbXBsZS5jb20wgZ8w
+DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMK+3uy1CGwek8jutw4UO62YTpkmStlw
+cKPEjTER7Ra1a1wyWJTo1mMnPhVia0GODeq8ERPgcIckCVogBu8+gL6g8NevaBNv
+ij1XWU08BEQqmoqAkrFiI8EdDckKYrSoXo2cg1fiTGzlG8AWtr5eT0op5jBBo0J6
+qXX5Sf6e+n+nAgMBAAGgMTAvBgkqhkiG9w0BCQ4xIjAgMB4GA1UdEQQXMBWCE3Rl
+c3Rsb3cuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADgYEAwRDa7ZOaym9mAUH7
+hudbvsRkqXHehgf51uMUq0OC9hQ6vPLWqUMAod05lxn3Tnvq6a/fVK0ybgCH5Ld7
+qpAcUruYdj7YxkFfuBc1dpAK6h94rVsJXFCWIMEZm9Fe7n5RERjhO6h2IRSXBHFz
+QIszvqBamm/W1ONKdQSM2g+M4BQ=
+-----END NEW CERTIFICATE REQUEST-----
+
diff --git a/ipatests/test_pkcs10/test2.csr b/ipatests/test_pkcs10/test2.csr
new file mode 100644
index 000000000..ccc47f890
--- /dev/null
+++ b/ipatests/test_pkcs10/test2.csr
@@ -0,0 +1,15 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIICETCCAXoCAQAwTzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx
+EDAOBgNVBAoTB0V4YW1wbGUxGTAXBgNVBAMTEHRlc3QuZXhhbXBsZS5jb20wgZ8w
+DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOXfP8LeiU7g6wLCclgkT1lVskK+Lxm1
+6ijE4LmEQBk5nn2P46im+E/UOgTddbDo5cdJlkoCnqXkO4RkqJckXYDxfI34KL3C
+CRFPvOa5Sg02m1x5Rg3boZfS6NciP62lRp0SI+0TCt3F16wYZxMahVIOXjbJ6Lu5
+mGjNn7XaWJhFAgMBAAGggYEwfwYJKoZIhvcNAQkOMXIwcDAeBgNVHREEFzAVghN0
+ZXN0bG93LmV4YW1wbGUuY29tME4GA1UdHwRHMEUwQ6BBoD+GHGh0dHA6Ly9jYS5l
+eGFtcGxlLmNvbS9teS5jcmyGH2h0dHA6Ly9vdGhlci5leGFtcGxlLmNvbS9teS5j
+cmwwDQYJKoZIhvcNAQEFBQADgYEAkv8pppcgGhX7erJmvg9r2UHrRriuKaOYgKZQ
+lf/eBt2N0L2mV4QvCY82H7HWuE+7T3mra9ikfvz0nYkPJQe2gntjZzECE0Jt5LWR
+UZOFwX8N6wrX11U2xu0NlvsbjU6siWd6OZjZ1p5/V330lzut/q3CNzaAcW1Fx3wL
+sV5SXSw=
+-----END NEW CERTIFICATE REQUEST-----
+
diff --git a/ipatests/test_pkcs10/test3.csr b/ipatests/test_pkcs10/test3.csr
new file mode 100644
index 000000000..82c84d154
--- /dev/null
+++ b/ipatests/test_pkcs10/test3.csr
@@ -0,0 +1,3 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+VGhpcyBpcyBhbiBpbnZhbGlkIENTUg==
+-----END NEW CERTIFICATE REQUEST-----
diff --git a/ipatests/test_pkcs10/test4.csr b/ipatests/test_pkcs10/test4.csr
new file mode 100644
index 000000000..9f08b802b
--- /dev/null
+++ b/ipatests/test_pkcs10/test4.csr
@@ -0,0 +1,4 @@
+-----BEGIN NEW CERTIFICATE REQUEST-----
+Invalidate data
+-----END NEW CERTIFICATE REQUEST-----
+
diff --git a/ipatests/test_pkcs10/test5.csr b/ipatests/test_pkcs10/test5.csr
new file mode 100644
index 000000000..41c3c1f3d
--- /dev/null
+++ b/ipatests/test_pkcs10/test5.csr
@@ -0,0 +1,20 @@
+
+Certificate request generated by Netscape certutil
+Phone: (not specified)
+
+Common Name: test.example.com
+Email: (not specified)
+Organization: IPA
+State: (not specified)
+Country: (not specified)
+
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIIBaDCB0gIBADApMQwwCgYDVQQKEwNJUEExGTAXBgNVBAMTEHRlc3QuZXhhbXBs
+ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPnSCLwl7IytP2HC7+zv
+nI2fe6oRCE/J8K1jIoiqS9engx3Yfe4kaXWWzcwmuUV57VhUmWDEQIbSREPdrVSi
+tWC55ilGmPOAEw+mP4qg6Ctb+d8Egmy1JVrpIYCLNXvEd3dAaimB0J+K3hKFRyHI
+2MzrIuFqqohRijkDLwB8oVVdAgMBAAGgADANBgkqhkiG9w0BAQUFAAOBgQACt37K
+j+RMEbqG8s0Uxs3FhcfiAx8Do99CDizY/b7hZEgMyG4dLmm+vSCBbxBrG5oMlxJD
+dxnpk0PQSknNkJVrCS/J1OTpOPRTi4VKATT3tHJAfDbWZTwcSelUCLQ4lREiuT3D
+WP4vKrLIxDJDb+/mwuV7WWo34E6MD9iTB1xINg==
+-----END NEW CERTIFICATE REQUEST-----
diff --git a/ipatests/test_pkcs10/test_pkcs10.py b/ipatests/test_pkcs10/test_pkcs10.py
new file mode 100644
index 000000000..6b3534b33
--- /dev/null
+++ b/ipatests/test_pkcs10/test_pkcs10.py
@@ -0,0 +1,124 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+"""
+Test the `pkcs10.py` module.
+"""
+
+import os
+import sys
+import nose
+from ipatests.util import raises, PluginTester
+from ipalib import pkcs10
+from ipapython import ipautil
+import nss.nss as nss
+from nss.error import NSPRError
+
+class test_update(object):
+ """
+ Test the PKCS#10 Parser.
+ """
+
+ def setUp(self):
+ nss.nss_init_nodb()
+ if ipautil.file_exists("test0.csr"):
+ self.testdir="./"
+ elif ipautil.file_exists("ipatests/test_pkcs10/test0.csr"):
+ self.testdir= "./ipatests/test_pkcs10/"
+ else:
+ raise nose.SkipTest("Unable to find test update files")
+
+ def read_file(self, filename):
+ fp = open(self.testdir + filename, "r")
+ data = fp.read()
+ fp.close()
+ return data
+
+ def test_0(self):
+ """
+ Test simple CSR with no attributes
+ """
+ csr = self.read_file("test0.csr")
+ request = pkcs10.load_certificate_request(csr)
+
+ subject = pkcs10.get_subject(request)
+
+ assert(subject.common_name == 'test.example.com')
+ assert(subject.state_name == 'California')
+ assert(subject.country_name == 'US')
+
+ def test_1(self):
+ """
+ Test CSR with subject alt name
+ """
+ csr = self.read_file("test1.csr")
+ request = pkcs10.load_certificate_request(csr)
+
+ subject = pkcs10.get_subject(request)
+
+ assert(subject.common_name == 'test.example.com')
+ assert(subject.state_name == 'California')
+ assert(subject.country_name == 'US')
+
+ for extension in request.extensions:
+ if extension.oid_tag == nss.SEC_OID_X509_SUBJECT_ALT_NAME:
+ assert nss.x509_alt_name(extension.value)[0] == 'testlow.example.com'
+
+ def test_2(self):
+ """
+ Test CSR with subject alt name and a list of CRL distribution points
+ """
+ csr = self.read_file("test2.csr")
+ request = pkcs10.load_certificate_request(csr)
+
+ subject = pkcs10.get_subject(request)
+
+ assert(subject.common_name == 'test.example.com')
+ assert(subject.state_name == 'California')
+ assert(subject.country_name == 'US')
+
+ for extension in request.extensions:
+ if extension.oid_tag == nss.SEC_OID_X509_SUBJECT_ALT_NAME:
+ assert nss.x509_alt_name(extension.value)[0] == 'testlow.example.com'
+ if extension.oid_tag == nss.SEC_OID_X509_CRL_DIST_POINTS:
+ pts = nss.CRLDistributionPts(extension.value)
+ urls = pts[0].get_general_names()
+ assert('http://ca.example.com/my.crl' in urls)
+ assert('http://other.example.com/my.crl' in urls)
+
+ def test_3(self):
+ """
+ Test CSR with base64-encoded bogus data
+ """
+ csr = self.read_file("test3.csr")
+
+ try:
+ request = pkcs10.load_certificate_request(csr)
+ except NSPRError, nsprerr:
+ # (SEC_ERROR_BAD_DER) security library: improperly formatted DER-encoded message.
+ assert(nsprerr. errno== -8183)
+
+ def test_4(self):
+ """
+ Test CSR with badly formatted base64-encoded data
+ """
+ csr = self.read_file("test4.csr")
+ try:
+ request = pkcs10.load_certificate_request(csr)
+ except TypeError, typeerr:
+ assert(str(typeerr) == 'Incorrect padding')
diff --git a/ipatests/test_util.py b/ipatests/test_util.py
new file mode 100644
index 000000000..f87822a22
--- /dev/null
+++ b/ipatests/test_util.py
@@ -0,0 +1,367 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Test the `tests.util` module.
+"""
+
+import re
+import util
+from util import raises, TYPE, VALUE, LEN, KEYS
+
+
+class Prop(object):
+ def __init__(self, *ops):
+ self.__ops = frozenset(ops)
+ self.__prop = 'prop value'
+
+ def __get_prop(self):
+ if 'get' not in self.__ops:
+ raise AttributeError('get prop')
+ return self.__prop
+
+ def __set_prop(self, value):
+ if 'set' not in self.__ops:
+ raise AttributeError('set prop')
+ self.__prop = value
+
+ def __del_prop(self):
+ if 'del' not in self.__ops:
+ raise AttributeError('del prop')
+ self.__prop = None
+
+ prop = property(__get_prop, __set_prop, __del_prop)
+
+
+class test_Fuzzy(object):
+ klass = util.Fuzzy
+
+ def test_init(self):
+ inst = self.klass()
+ assert inst.regex is None
+ assert inst.type is None
+ assert inst.test is None
+ assert inst.re is None
+
+ inst = self.klass('(foo|bar)')
+ assert inst.regex == '(foo|bar)'
+ assert inst.type is unicode
+ assert inst.test is None
+ assert isinstance(inst.re, re._pattern_type)
+
+ inst = self.klass('(foo|bar)', type=str)
+ assert inst.regex == '(foo|bar)'
+ assert inst.type is str
+ assert inst.test is None
+ assert isinstance(inst.re, re._pattern_type)
+
+ t = lambda other: other > 500
+
+ inst = self.klass(test=t)
+ assert inst.regex is None
+ assert inst.type is None
+ assert inst.test is t
+ assert inst.re is None
+
+ inst = self.klass(type=(int, float), test=t)
+ assert inst.regex is None
+ assert inst.type == (int, float)
+ assert inst.test is t
+ assert inst.re is None
+
+ def test_repr(self):
+ s = 'Fuzzy(%r, %r, %r)'
+ t = lambda other: 0.0 <= other <= 1.0
+
+ inst = self.klass()
+ assert repr(inst) == s % (None, None, None)
+
+ inst = self.klass('foo')
+ assert repr(inst) == s % ('foo', unicode, None)
+
+ inst = self.klass(type=(int, float))
+ assert repr(inst) == s % (None, (int, float), None)
+
+ inst = self.klass(type=(int, float), test=t)
+ assert repr(inst) == s % (None, (int, float), t)
+
+ inst = self.klass(test=t)
+ assert repr(inst) == s % (None, None, t)
+
+ def test_eq(self):
+ assert (self.klass('bar') == u'foobar') is True
+ assert (self.klass('^bar') == u'foobar') is False
+ assert (self.klass('bar', type=str) == u'foobar') is False
+
+ assert ('18' == self.klass()) is True
+ assert ('18' == self.klass(type=int)) is False
+ assert (18 == self.klass(type=int)) is True
+ assert ('18' == self.klass(type=(int, str))) is True
+
+ assert (self.klass() == '18') is True
+ assert (self.klass(type=int) == '18') is False
+ assert (self.klass(type=int) == 18) is True
+ assert (self.klass(type=(int, str)) == '18') is True
+
+ t = lambda other: other.endswith('bar')
+ assert (self.klass(test=t) == 'foobar') is True
+ assert (self.klass(test=t, type=unicode) == 'foobar') is False
+ assert (self.klass(test=t) == 'barfoo') is False
+
+ assert (False == self.klass()) is True
+ assert (True == self.klass()) is True
+ assert (None == self.klass()) is True
+
+
+def test_assert_deepequal():
+ f = util.assert_deepequal
+
+ # Test with good scalar values:
+ f(u'hello', u'hello')
+ f(util.Fuzzy(), u'hello')
+ f(util.Fuzzy(type=unicode), u'hello')
+ f(util.Fuzzy('ell'), u'hello')
+ f(util.Fuzzy(test=lambda other: other.endswith('llo')), u'hello')
+ f(18, 18)
+ f(util.Fuzzy(), 18)
+ f(util.Fuzzy(type=int), 18)
+ f(util.Fuzzy(type=(int, float), test=lambda other: other > 17.9), 18)
+
+ # Test with bad scalar values:
+ e = raises(AssertionError, f, u'hello', u'world', 'foo')
+ assert str(e) == VALUE % (
+ 'foo', u'hello', u'world', tuple()
+ )
+
+ e = raises(AssertionError, f, 'hello', u'hello', 'foo')
+ assert str(e) == TYPE % (
+ 'foo', str, unicode, 'hello', u'hello', tuple()
+ )
+
+ e = raises(AssertionError, f, 18, 18.0, 'foo')
+ assert str(e) == TYPE % (
+ 'foo', int, float, 18, 18.0, tuple()
+ )
+
+ # Test with good compound values:
+ a = [
+ u'hello',
+ dict(naughty=u'nurse'),
+ 18,
+ ]
+ b = [
+ u'hello',
+ dict(naughty=u'nurse'),
+ 18,
+ ]
+ f(a, b)
+
+ # Test with bad compound values:
+ b = [
+ 'hello',
+ dict(naughty=u'nurse'),
+ 18,
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == TYPE % (
+ 'foo', unicode, str, u'hello', 'hello', (2,)
+ )
+
+ b = [
+ u'hello',
+ dict(naughty='nurse'),
+ 18,
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == TYPE % (
+ 'foo', unicode, str, u'nurse', 'nurse', (1, 'naughty')
+ )
+
+ b = [
+ u'hello',
+ dict(naughty=u'nurse'),
+ 18.0,
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == TYPE % (
+ 'foo', int, float, 18, 18.0, (0,)
+ )
+
+ # List length mismatch
+ b = [
+ u'hello',
+ dict(naughty=u'nurse'),
+ 18,
+ 19
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == LEN % (
+ 'foo', 3, 4, a, b, tuple()
+ )
+
+ b = [
+ dict(naughty=u'nurse'),
+ 18,
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == LEN % (
+ 'foo', 3, 2, a, b, tuple()
+ )
+
+ # Dict keys mismatch:
+
+ # Missing
+ b = [
+ u'hello',
+ dict(),
+ 18,
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == KEYS % ('foo',
+ ['naughty'], [],
+ dict(naughty=u'nurse'), dict(),
+ (1,)
+ )
+
+ # Extra
+ b = [
+ u'hello',
+ dict(naughty=u'nurse', barely=u'legal'),
+ 18,
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == KEYS % ('foo',
+ [], ['barely'],
+ dict(naughty=u'nurse'), dict(naughty=u'nurse', barely=u'legal'),
+ (1,)
+ )
+
+ # Missing + Extra
+ b = [
+ u'hello',
+ dict(barely=u'legal'),
+ 18,
+ ]
+ e = raises(AssertionError, f, a, b, 'foo')
+ assert str(e) == KEYS % ('foo',
+ ['naughty'], ['barely'],
+ dict(naughty=u'nurse'), dict(barely=u'legal'),
+ (1,)
+ )
+
+
+def test_yes_raised():
+ f = util.raises
+
+ class SomeError(Exception):
+ pass
+
+ class AnotherError(Exception):
+ pass
+
+ def callback1():
+ 'raises correct exception'
+ raise SomeError()
+
+ def callback2():
+ 'raises wrong exception'
+ raise AnotherError()
+
+ def callback3():
+ 'raises no exception'
+
+ f(SomeError, callback1)
+
+ raised = False
+ try:
+ f(SomeError, callback2)
+ except AnotherError:
+ raised = True
+ assert raised
+
+ raised = False
+ try:
+ f(SomeError, callback3)
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+
+def test_no_set():
+ # Tests that it works when prop cannot be set:
+ util.no_set(Prop('get', 'del'), 'prop')
+
+ # Tests that ExceptionNotRaised is raised when prop *can* be set:
+ raised = False
+ try:
+ util.no_set(Prop('set'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+
+def test_no_del():
+ # Tests that it works when prop cannot be deleted:
+ util.no_del(Prop('get', 'set'), 'prop')
+
+ # Tests that ExceptionNotRaised is raised when prop *can* be set:
+ raised = False
+ try:
+ util.no_del(Prop('del'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+
+def test_read_only():
+ # Test that it works when prop is read only:
+ assert util.read_only(Prop('get'), 'prop') == 'prop value'
+
+ # Test that ExceptionNotRaised is raised when prop can be set:
+ raised = False
+ try:
+ util.read_only(Prop('get', 'set'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+ # Test that ExceptionNotRaised is raised when prop can be deleted:
+ raised = False
+ try:
+ util.read_only(Prop('get', 'del'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+ # Test that ExceptionNotRaised is raised when prop can be both set and
+ # deleted:
+ raised = False
+ try:
+ util.read_only(Prop('get', 'del'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+ # Test that AttributeError is raised when prop can't be read:
+ raised = False
+ try:
+ util.read_only(Prop(), 'prop')
+ except AttributeError:
+ raised = True
+ assert raised
diff --git a/ipatests/test_xmlrpc/__init__.py b/ipatests/test_xmlrpc/__init__.py
new file mode 100644
index 000000000..1a8ecf1c2
--- /dev/null
+++ b/ipatests/test_xmlrpc/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/objectclasses.py b/ipatests/test_xmlrpc/objectclasses.py
new file mode 100644
index 000000000..75ac3eb17
--- /dev/null
+++ b/ipatests/test_xmlrpc/objectclasses.py
@@ -0,0 +1,163 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Defines the expected objectclass for various entries.
+"""
+
+user_base = [
+ u'top',
+ u'person',
+ u'organizationalperson',
+ u'inetorgperson',
+ u'inetuser',
+ u'posixaccount',
+ u'krbprincipalaux',
+ u'krbticketpolicyaux',
+ u'ipaobject',
+ u'ipasshuser',
+ u'ipaSshGroupOfPubKeys',
+]
+
+user = user_base + [u'mepOriginEntry']
+
+group = [
+ u'top',
+ u'groupofnames',
+ u'nestedgroup',
+ u'ipausergroup',
+ u'ipaobject',
+]
+
+externalgroup = group + [u'ipaexternalgroup']
+posixgroup = group + [u'posixgroup']
+
+host = [
+ u'ipasshhost',
+ u'ipaSshGroupOfPubKeys',
+ u'ieee802device',
+ u'ipaobject',
+ u'nshost',
+ u'ipahost',
+ u'pkiuser',
+ u'ipaservice',
+ u'krbprincipalaux',
+ u'krbprincipal',
+ u'top',
+]
+
+hostgroup = [
+ u'ipaobject',
+ u'ipahostgroup',
+ u'nestedGroup',
+ u'groupOfNames',
+ u'top',
+ u'mepOriginEntry',
+]
+
+role = [
+ u'groupofnames',
+ u'nestedgroup',
+ u'top',
+]
+
+permission = [
+ u'groupofnames',
+ u'ipapermission',
+ u'top'
+]
+
+privilege = [
+ u'nestedgroup',
+ u'groupofnames',
+ u'top'
+]
+
+service = [
+ u'krbprincipal',
+ u'krbprincipalaux',
+ u'krbticketpolicyaux',
+ u'ipaobject',
+ u'ipaservice',
+ u'pkiuser',
+ u'ipakrbprincipal',
+ u'top',
+]
+
+hbacsvc = [
+ u'ipaobject',
+ u'ipahbacservice',
+]
+
+hbacsvcgroup = [
+ u'ipaobject',
+ u'ipahbacservicegroup',
+ u'groupOfNames',
+ u'top',
+]
+
+sudocmd = [
+ u'ipaobject',
+ u'ipasudocmd',
+]
+
+sudocmdgroup = [
+ u'ipaobject',
+ u'ipasudocmdgrp',
+ u'groupOfNames',
+ u'top',
+]
+
+netgroup = [
+ u'ipaobject',
+ u'ipaassociation',
+ u'ipanisnetgroup',
+]
+
+automember = [
+ u'top',
+ u'automemberregexrule',
+]
+
+selinuxusermap = [
+ u'ipaassociation',
+ u'ipaselinuxusermap',
+]
+
+hbacrule = [
+ u'ipaassociation',
+ u'ipahbacrule',
+]
+
+dnszone = [
+ u'top',
+ u'idnsrecord',
+ u'idnszone',
+]
+
+dnsrecord = [
+ u'top',
+ u'idnsrecord',
+]
+
+realmdomains = [
+ u'top',
+ u'nsContainer',
+ u'domainRelatedObject',
+]
diff --git a/ipatests/test_xmlrpc/test_attr.py b/ipatests/test_xmlrpc/test_attr.py
new file mode 100644
index 000000000..ef5b882c5
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_attr.py
@@ -0,0 +1,562 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test --setattr and --addattr and other attribute-specific issues
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+user1=u'tuser1'
+
+class test_attr(Declarative):
+
+ cleanup_commands = [
+ ('user_del', [user1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to add user %r with single-value attribute set via '
+ 'option and --addattr' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ addattr=u'sn=User2')
+ ),
+ expected=errors.OnlyOneValueAllowed(attr='sn'),
+ ),
+
+ dict(
+ desc='Create %r' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ setattr=None)
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "tuser1"',
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user1),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ dn=DN(('uid','tuser1'),('cn','users'),('cn','accounts'),
+ api.env.basedn),
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Change givenname, add mail %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=(u'givenname=Finkle', u'mail=test@example.com'))
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com'],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Add another mail %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(addattr=u'mail=test2@example.com')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Add two phone numbers at once %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=u'telephoneNumber=410-555-1212', addattr=u'telephoneNumber=301-555-1212')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'410-555-1212', u'301-555-1212'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Go from two phone numbers to one %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=u'telephoneNumber=301-555-1212')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'301-555-1212'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Add two more phone numbers %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(addattr=(u'telephoneNumber=703-555-1212', u'telephoneNumber=202-888-9833'))
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'301-555-1212', u'202-888-9833', u'703-555-1212'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Delete one phone number for %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(delattr=u'telephoneNumber=301-555-1212')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'202-888-9833', u'703-555-1212'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Try deleting the number again for %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(delattr=u'telephoneNumber=301-555-1212')
+ ),
+ expected=errors.AttrValueNotFound(attr=u'telephonenumber',
+ value=u'301-555-1212')
+ ),
+
+
+ dict(
+ desc='Add and delete one phone number for %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(addattr=u'telephoneNumber=301-555-1212',
+ delattr=u'telephoneNumber=202-888-9833')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'301-555-1212', u'703-555-1212'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Add and delete the same phone number for %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(addattr=(u'telephoneNumber=301-555-1212',
+ u'telephoneNumber=202-888-9833'),
+ delattr=u'telephoneNumber=301-555-1212')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'703-555-1212', u'301-555-1212', u'202-888-9833'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Set and delete a phone number for %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=(u'telephoneNumber=301-555-1212',
+ u'telephoneNumber=202-888-9833'),
+ delattr=u'telephoneNumber=301-555-1212')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'202-888-9833'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Try setting givenname to None with setattr in %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=(u'givenname='))
+ ),
+ expected=errors.RequirementError(name='givenname'),
+ ),
+
+
+ dict(
+ desc='Try setting givenname to None with option in %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(givenname=None)
+ ),
+ expected=errors.RequirementError(name='first'),
+ ),
+
+
+ dict(
+ desc='Make sure setting givenname works with option in %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(givenname=u'Fred')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Fred'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'202-888-9833'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Make sure setting givenname works with setattr in %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=u'givenname=Finkle')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'202-888-9833'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Lock %r using setattr' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=u'nsaccountlock=TrUe')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'202-888-9833'],
+ nsaccountlock=True,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Unlock %r using addattr&delattr' % user1,
+ command=(
+ 'user_mod', [user1], dict(
+ addattr=u'nsaccountlock=FaLsE',
+ delattr=u'nsaccountlock=TRUE')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test@example.com', u'test2@example.com'],
+ memberof_group=[u'ipausers'],
+ telephonenumber=[u'202-888-9833'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Try adding a new group search fields config entry',
+ command=(
+ 'config_mod', [], dict(addattr=u'ipagroupsearchfields=newattr')
+ ),
+ expected=errors.OnlyOneValueAllowed(attr='ipagroupsearchfields'),
+ ),
+
+ dict(
+ desc='Try adding a new cert subject base config entry',
+ command=(
+ 'config_mod', [], dict(addattr=u'ipacertificatesubjectbase=0=DOMAIN.COM')
+ ),
+ expected=errors.ValidationError(name='ipacertificatesubjectbase',
+ error='attribute is not configurable'),
+ ),
+
+ dict(
+ desc='Try deleting a required config entry',
+ command=(
+ 'config_mod', [], dict(delattr=u'ipasearchrecordslimit=100')
+ ),
+ expected=errors.RequirementError(name='ipasearchrecordslimit'),
+ ),
+
+ dict(
+ desc='Try setting nonexistent attribute',
+ command=('config_mod', [], dict(setattr=u'invalid_attr=false')),
+ expected=errors.ObjectclassViolation(
+ info='attribute "invalid_attr" not allowed'),
+ ),
+
+ dict(
+ desc='Try setting out-of-range krbpwdmaxfailure',
+ command=('pwpolicy_mod', [], dict(setattr=u'krbpwdmaxfailure=-1')),
+ expected=errors.ValidationError(name='krbpwdmaxfailure',
+ error='must be at least 0'),
+ ),
+
+ dict(
+ desc='Try setting out-of-range maxfail',
+ command=('pwpolicy_mod', [], dict(krbpwdmaxfailure=u'-1')),
+ expected=errors.ValidationError(name='maxfail',
+ error='must be at least 0'),
+ ),
+
+ dict(
+ desc='Try setting non-numeric krbpwdmaxfailure',
+ command=('pwpolicy_mod', [], dict(setattr=u'krbpwdmaxfailure=abc')),
+ expected=errors.ConversionError(name='krbpwdmaxfailure',
+ error='must be an integer'),
+ ),
+
+ dict(
+ desc='Try setting non-numeric maxfail',
+ command=('pwpolicy_mod', [], dict(krbpwdmaxfailure=u'abc')),
+ expected=errors.ConversionError(name='maxfail',
+ error='must be an integer'),
+ ),
+
+ dict(
+ desc='Try deleting bogus attribute',
+ command=('config_mod', [], dict(delattr=u'bogusattribute=xyz')),
+ expected=errors.ValidationError(name='bogusattribute',
+ error='No such attribute on this entry'),
+ ),
+
+ dict(
+ desc='Try deleting empty attribute',
+ command=('config_mod', [],
+ dict(delattr=u'ipaCustomFields=See Also,seealso,false')),
+ expected=errors.ValidationError(name='ipacustomfields',
+ error='No such attribute on this entry'),
+ ),
+
+ dict(
+ desc='Set and delete one value, plus try deleting a missing one',
+ command=('config_mod', [], dict(
+ delattr=[u'ipaCustomFields=See Also,seealso,false',
+ u'ipaCustomFields=Country,c,false'],
+ addattr=u'ipaCustomFields=See Also,seealso,false')),
+ expected=errors.AttrValueNotFound(attr='ipacustomfields',
+ value='Country,c,false'),
+ ),
+
+ dict(
+ desc='Try to delete an operational attribute with --delattr',
+ command=('config_mod', [], dict(
+ delattr=u'creatorsName=cn=directory manager')),
+ expected=errors.DatabaseError(
+ desc='Server is unwilling to perform', info=''),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_automember_plugin.py b/ipatests/test_xmlrpc/test_automember_plugin.py
new file mode 100644
index 000000000..a50860e66
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_automember_plugin.py
@@ -0,0 +1,1095 @@
+# Authors:
+# Jr Aquino <jr.aquino@citrix.com>
+#
+# Copyright (C) 2011 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/>.
+
+"""
+Test the `ipalib/plugins/automember.py` module.
+"""
+
+from ipalib import api, errors
+from ipapython.dn import DN
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+
+
+user1=u'tuser1'
+manager1=u'mscott'
+fqdn1 = u'web1.%s' % api.env.domain
+short1 = u'web1'
+fqdn2 = u'dev1.%s' % api.env.domain
+short2 = u'dev1'
+fqdn3 = u'web5.%s' % api.env.domain
+short3 = u'web5'
+fqdn4 = u'www5.%s' % api.env.domain
+short4 = u'www5'
+fqdn5 = u'webserver5.%s' % api.env.domain
+short5 = u'webserver5'
+
+group1=u'group1'
+defaultgroup1=u'defaultgroup1'
+hostgroup1=u'hostgroup1'
+hostgroup2=u'hostgroup2'
+hostgroup3=u'hostgroup3'
+hostgroup4=u'hostgroup4'
+defaulthostgroup1=u'defaulthostgroup1'
+
+group_include_regex = u'mscott'
+hostgroup_include_regex = u'^web[1-9]'
+hostgroup_include_regex2 = u'^www[1-9]'
+hostgroup_include_regex3 = u'webserver[1-9]'
+hostgroup_exclude_regex = u'^web5'
+hostgroup_exclude_regex2 = u'^www5'
+hostgroup_exclude_regex3 = u'^webserver5'
+
+
+class test_automember(Declarative):
+
+ cleanup_commands = [
+ ('user_del', [user1, manager1], {}),
+ ('group_del', [group1, defaultgroup1], {}),
+ ('host_del', [fqdn1, fqdn2, fqdn3, fqdn4, fqdn5], {}),
+ ('hostgroup_del', [hostgroup1, hostgroup2, hostgroup3, hostgroup4, defaulthostgroup1], {}),
+ ('automember_del', [group1], {'type': u'group'}),
+ ('automember_del', [hostgroup1], {'type': u'hostgroup'}),
+ ('automember_del', [hostgroup2], {'type': u'hostgroup'}),
+ ('automember_del', [hostgroup3], {'type': u'hostgroup'}),
+ ('automember_del', [hostgroup4], {'type': u'hostgroup'}),
+ ('automember_default_group_remove', [], {'type': u'hostgroup'}),
+ ('automember_default_group_remove', [], {'type': u'group'}),
+
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent group rule %r' % group1,
+ command=('automember_add', [group1],
+ dict(description=u'Test desc', type=u'group')),
+ expected=errors.NotFound(reason=u'Group: %s not found!' % group1),
+ ),
+
+ dict(
+ desc='Try to update non-existent group rule %r' % group1,
+ command=('automember_add', [group1], dict(type=u'group')),
+ expected=errors.NotFound(reason=u'Group: %s not found!' % group1),
+ ),
+
+ dict(
+ desc='Try to delete non-existent group rule %r' % group1,
+ command=('automember_del', [group1], dict(type=u'group')),
+ expected=errors.NotFound(reason=u': auto_member_rule not found'),
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent hostgroup rule %r' % hostgroup1,
+ command=('automember_add', [hostgroup1],
+ dict(description=u'Test desc', type=u'hostgroup')),
+ expected=errors.NotFound(
+ reason=u'Group: %s not found!' % hostgroup1),
+ ),
+
+ dict(
+ desc='Try to update non-existent hostgroup rule %r' % hostgroup1,
+ command=('automember_add', [hostgroup1], dict(type=u'hostgroup')),
+ expected=errors.NotFound(
+ reason=u'Group: %s not found!' % hostgroup1),
+ ),
+
+ dict(
+ desc='Try to delete non-existent hostgroup rule %r' % hostgroup1,
+ command=('automember_del', [hostgroup1], dict(type=u'hostgroup')),
+ expected=errors.NotFound(reason=u': auto_member_rule not found'),
+ ),
+
+
+
+ dict(
+ desc='Create %r' % group1,
+ command=(
+ 'group_add', [group1], dict(description=u'Test desc')
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added group "%s"' % group1,
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn', group1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup1,
+ command=(
+ 'hostgroup_add', [hostgroup1], dict(description=u'Test desc')
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added hostgroup "%s"' % hostgroup1,
+ result=dict(
+ cn=[hostgroup1],
+ description=[u'Test desc'],
+ objectclass=objectclasses.hostgroup,
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn', hostgroup1), ('cn', 'ng'), ('cn', 'alt'), api.env.basedn)],
+ dn=DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup2,
+ command=(
+ 'hostgroup_add', [hostgroup2], dict(description=u'Test desc')
+ ),
+ expected=dict(
+ value=hostgroup2,
+ summary=u'Added hostgroup "%s"' % hostgroup2,
+ result=dict(
+ cn=[hostgroup2],
+ description=[u'Test desc'],
+ objectclass=objectclasses.hostgroup,
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn', hostgroup2), ('cn', 'ng'), ('cn', 'alt'), api.env.basedn)],
+ dn=DN(('cn', hostgroup2), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup3,
+ command=(
+ 'hostgroup_add', [hostgroup3], dict(description=u'Test desc')
+ ),
+ expected=dict(
+ value=hostgroup3,
+ summary=u'Added hostgroup "%s"' % hostgroup3,
+ result=dict(
+ cn=[hostgroup3],
+ description=[u'Test desc'],
+ objectclass=objectclasses.hostgroup,
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn', hostgroup3), ('cn', 'ng'), ('cn', 'alt'), api.env.basedn)],
+ dn=DN(('cn', hostgroup3), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup4,
+ command=(
+ 'hostgroup_add', [hostgroup4], dict(description=u'Test desc')
+ ),
+ expected=dict(
+ value=hostgroup4,
+ summary=u'Added hostgroup "%s"' % hostgroup4,
+ result=dict(
+ cn=[hostgroup4],
+ description=[u'Test desc'],
+ objectclass=objectclasses.hostgroup,
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn', hostgroup4), ('cn', 'ng'), ('cn', 'alt'), api.env.basedn)],
+ dn=DN(('cn', hostgroup4), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % defaultgroup1,
+ command=(
+ 'group_add', [defaultgroup1], dict(description=u'Default test desc')
+ ),
+ expected=dict(
+ value=defaultgroup1,
+ summary=u'Added group "%s"' % defaultgroup1,
+ result=dict(
+ cn=[defaultgroup1],
+ description=[u'Default test desc'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn', defaultgroup1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % defaulthostgroup1,
+ command=(
+ 'hostgroup_add', [defaulthostgroup1], dict(description=u'Default test desc')
+ ),
+ expected=dict(
+ value=defaulthostgroup1,
+ summary=u'Added hostgroup "%s"' % defaulthostgroup1,
+ result=dict(
+ cn=[defaulthostgroup1],
+ description=[u'Default test desc'],
+ objectclass=objectclasses.hostgroup,
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn', defaulthostgroup1), ('cn', 'ng'), ('cn', 'alt'), api.env.basedn)],
+ dn=DN(('cn', defaulthostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember %r' % group1,
+ command=(
+ 'automember_add', [group1], dict(description=u'Test desc', type=u'group')
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added automember rule "%s"' % group1,
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', group1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ objectclass=objectclasses.automember,
+ dn=DN(('cn', group1), ('cn', 'group'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember condition %r' % group1,
+ command=(
+ 'automember_add_condition', [group1], dict(
+ key=u'manager', type=u'group',
+ automemberinclusiveregex=[group_include_regex],
+ )
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added condition(s) to "%s"' % group1,
+ completed=1,
+ failed=dict(
+ failed = dict(
+ automemberinclusiveregex=tuple(),
+ automemberexclusiveregex=tuple(),
+ )
+ ),
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc'],
+ automemberinclusiveregex=[u'manager=%s' % group_include_regex],
+ automembertargetgroup=[DN(('cn', group1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember %r' % hostgroup1,
+ command=(
+ 'automember_add', [hostgroup1], dict(
+ description=u'Test desc', type=u'hostgroup',
+ )
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added automember rule "%s"' % hostgroup1,
+ result=dict(
+ cn=[hostgroup1],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ objectclass=objectclasses.automember,
+ dn=DN(('cn', hostgroup1), ('cn', 'hostgroup'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember condition %r' % hostgroup1,
+ command=(
+ 'automember_add_condition', [hostgroup1], dict(
+ key=u'fqdn', type=u'hostgroup',
+ automemberinclusiveregex=[hostgroup_include_regex],
+ )
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added condition(s) to "%s"' % hostgroup1,
+ completed=1,
+ failed=dict(
+ failed = dict(
+ automemberinclusiveregex=tuple(),
+ automemberexclusiveregex=tuple(),
+ )
+ ),
+ result=dict(
+ cn=[hostgroup1],
+ description=[u'Test desc'],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_include_regex],
+ automembertargetgroup=[DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create duplicate automember condition %r' % hostgroup1,
+ command=(
+ 'automember_add_condition', [hostgroup1], dict(
+ key=u'fqdn', type=u'hostgroup',
+ automemberinclusiveregex=[hostgroup_include_regex],
+ )
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added condition(s) to "%s"' % hostgroup1,
+ completed=0,
+ failed=dict(
+ failed = dict(
+ automemberinclusiveregex=tuple(),
+ automemberexclusiveregex=tuple(),
+ )
+ ),
+ result=dict(
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_include_regex],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create additional automember conditions %r' % hostgroup1,
+ command=(
+ 'automember_add_condition', [hostgroup1], dict(
+ key=u'fqdn', type=u'hostgroup',
+ automemberinclusiveregex=[hostgroup_include_regex2, hostgroup_include_regex3],
+ automemberexclusiveregex=[hostgroup_exclude_regex, hostgroup_exclude_regex2, hostgroup_exclude_regex3],
+ )
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added condition(s) to "%s"' % hostgroup1,
+ completed=5,
+ failed=dict(
+ failed = dict(
+ automemberinclusiveregex=tuple(),
+ automemberexclusiveregex=tuple(),
+ )
+ ),
+ result=dict(
+ cn=[hostgroup1],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_include_regex,
+ u'fqdn=%s' % hostgroup_include_regex3,
+ u'fqdn=%s' % hostgroup_include_regex2,
+ ],
+ automemberexclusiveregex=[u'fqdn=%s' % hostgroup_exclude_regex2,
+ u'fqdn=%s' % hostgroup_exclude_regex3,
+ u'fqdn=%s' % hostgroup_exclude_regex,
+ ],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember %r' % hostgroup2,
+ command=(
+ 'automember_add', [hostgroup2], dict(
+ description=u'Test desc', type=u'hostgroup',
+ )
+ ),
+ expected=dict(
+ value=hostgroup2,
+ summary=u'Added automember rule "%s"' % hostgroup2,
+ result=dict(
+ cn=[hostgroup2],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', hostgroup2), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ objectclass=objectclasses.automember,
+ dn=DN(('cn', hostgroup2), ('cn', 'hostgroup'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember condition %r' % hostgroup2,
+ command=(
+ 'automember_add_condition', [hostgroup2], dict(
+ key=u'fqdn', type=u'hostgroup',
+ automemberinclusiveregex=[hostgroup_exclude_regex],
+ )
+ ),
+ expected=dict(
+ value=hostgroup2,
+ summary=u'Added condition(s) to "%s"' % hostgroup2,
+ completed=1,
+ failed=dict(
+ failed = dict(
+ automemberinclusiveregex=tuple(),
+ automemberexclusiveregex=tuple(),
+ )
+ ),
+ result=dict(
+ cn=[hostgroup2],
+ description=[u'Test desc'],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_exclude_regex],
+ automembertargetgroup=[DN(('cn', hostgroup2), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember %r' % hostgroup3,
+ command=(
+ 'automember_add', [hostgroup3], dict(
+ description=u'Test desc', type=u'hostgroup',
+ )
+ ),
+ expected=dict(
+ value=hostgroup3,
+ summary=u'Added automember rule "%s"' % hostgroup3,
+ result=dict(
+ cn=[hostgroup3],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', hostgroup3), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ objectclass=objectclasses.automember,
+ dn=DN(('cn', hostgroup3), ('cn', 'hostgroup'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember condition %r' % hostgroup3,
+ command=(
+ 'automember_add_condition', [hostgroup3], dict(
+ key=u'fqdn', type=u'hostgroup',
+ automemberinclusiveregex=[hostgroup_exclude_regex2],
+ )
+ ),
+ expected=dict(
+ value=hostgroup3,
+ summary=u'Added condition(s) to "%s"' % hostgroup3,
+ completed=1,
+ failed=dict(
+ failed = dict(
+ automemberinclusiveregex=tuple(),
+ automemberexclusiveregex=tuple(),
+ )
+ ),
+ result=dict(
+ cn=[hostgroup3],
+ description=[u'Test desc'],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_exclude_regex2],
+ automembertargetgroup=[DN(('cn', hostgroup3), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember %r' % hostgroup4,
+ command=(
+ 'automember_add', [hostgroup4], dict(
+ description=u'Test desc', type=u'hostgroup',
+ )
+ ),
+ expected=dict(
+ value=hostgroup4,
+ summary=u'Added automember rule "%s"' % hostgroup4,
+ result=dict(
+ cn=[hostgroup4],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', hostgroup4), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ objectclass=objectclasses.automember,
+ dn=DN(('cn', hostgroup4), ('cn', 'hostgroup'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create automember condition %r' % hostgroup4,
+ command=(
+ 'automember_add_condition', [hostgroup4], dict(
+ key=u'fqdn', type=u'hostgroup',
+ automemberinclusiveregex=[hostgroup_exclude_regex3],
+ )
+ ),
+ expected=dict(
+ value=hostgroup4,
+ summary=u'Added condition(s) to "%s"' % hostgroup4,
+ completed=1,
+ failed=dict(
+ failed = dict(
+ automemberinclusiveregex=tuple(),
+ automemberexclusiveregex=tuple(),
+ )
+ ),
+ result=dict(
+ cn=[hostgroup4],
+ description=[u'Test desc'],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_exclude_regex3],
+ automembertargetgroup=[DN(('cn', hostgroup4), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc="Retrieve automember rule for group %s" % group1,
+ command=('automember_show', [group1], dict(
+ type=u'group',
+ )
+ ),
+ expected=dict(
+ value=group1,
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc'],
+ automemberinclusiveregex=[u'manager=%s' % group_include_regex],
+ automembertargetgroup=[DN(('cn', group1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ dn=DN(('cn', group1), ('cn', 'group'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % group1,
+ command=('automember_find', [group1], dict(
+ type=u'group'
+ )
+ ),
+ expected=dict(
+ count=1,
+ truncated=False,
+ result=[
+ dict(
+ cn=[group1],
+ description=[u'Test desc'],
+ automemberinclusiveregex=[u'manager=%s' % group_include_regex],
+ automembertargetgroup=[DN(('cn', group1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ dn=DN(('cn', group1), ('cn', 'group'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ ],
+ summary=u'1 rules matched',
+ ),
+ ),
+
+
+ dict(
+ desc='Updated automember rule %r' % group1,
+ command=(
+ 'automember_mod', [group1], dict(
+ type=u'group',
+ description=u'New desc 1',
+ )
+ ),
+ expected=dict(
+ result=dict(
+ cn=[group1],
+ description=[u'New desc 1'],
+ automemberinclusiveregex=[u'manager=%s' % group_include_regex],
+ automembertargetgroup=[DN(('cn', group1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ summary=u'Modified automember rule "%s"' % group1,
+ value=group1,
+ ),
+ ),
+
+
+ dict(
+ desc="Retrieve automember rule for hostgroup %s" % hostgroup1,
+ command=('automember_show', [hostgroup1], dict(
+ type=u'hostgroup',
+ )
+ ),
+ expected=dict(
+ value=hostgroup1,
+ result=dict(
+ cn=[hostgroup1],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_include_regex,
+ u'fqdn=%s' % hostgroup_include_regex3,
+ u'fqdn=%s' % hostgroup_include_regex2,
+ ],
+ automemberexclusiveregex=[u'fqdn=%s' % hostgroup_exclude_regex2,
+ u'fqdn=%s' % hostgroup_exclude_regex3,
+ u'fqdn=%s' % hostgroup_exclude_regex,
+ ],
+ dn=DN(('cn', hostgroup1), ('cn', 'hostgroup'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % hostgroup1,
+ command=('automember_find', [hostgroup1], dict(
+ type=u'hostgroup'
+ )
+ ),
+ expected=dict(
+ count=1,
+ truncated=False,
+ result=[
+ dict(
+ cn=[hostgroup1],
+ description=[u'Test desc'],
+ automembertargetgroup=[DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_include_regex,
+ u'fqdn=%s' % hostgroup_include_regex3,
+ u'fqdn=%s' % hostgroup_include_regex2,
+ ],
+ automemberexclusiveregex=[u'fqdn=%s' % hostgroup_exclude_regex2,
+ u'fqdn=%s' % hostgroup_exclude_regex3,
+ u'fqdn=%s' % hostgroup_exclude_regex,
+ ],
+ dn=DN(('cn', hostgroup1), ('cn', 'hostgroup'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ ),
+ ],
+ summary=u'1 rules matched',
+ ),
+ ),
+
+
+ dict(
+ desc='Updated automember rule %r' % hostgroup1,
+ command=(
+ 'automember_mod', [hostgroup1], dict(
+ type=u'hostgroup',
+ description=u'New desc 1',
+ )
+ ),
+ expected=dict(
+ result=dict(
+ cn=[hostgroup1],
+ description=[u'New desc 1'],
+ automembertargetgroup=[DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ automemberinclusiveregex=[u'fqdn=%s' % hostgroup_include_regex,
+ u'fqdn=%s' % hostgroup_include_regex3,
+ u'fqdn=%s' % hostgroup_include_regex2,
+ ],
+ automemberexclusiveregex=[u'fqdn=%s' % hostgroup_exclude_regex2,
+ u'fqdn=%s' % hostgroup_exclude_regex3,
+ u'fqdn=%s' % hostgroup_exclude_regex,
+ ],
+ ),
+ summary=u'Modified automember rule "%s"' % hostgroup1,
+ value=hostgroup1,
+ ),
+ ),
+
+
+ dict(
+ desc='Set default automember group for groups',
+ command=(
+ 'automember_default_group_set', [], dict(
+ type=u'group',
+ automemberdefaultgroup=defaultgroup1
+ )
+ ),
+ expected=dict(
+ result=dict(
+ cn=[u'Group'],
+ automemberdefaultgroup=[DN(('cn', defaultgroup1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ value=u'group',
+ summary=u'Set default (fallback) group for automember "group"',
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve default automember group for groups',
+ command=(
+ 'automember_default_group_show', [], dict(type=u'group')
+ ),
+ expected=dict(
+ result=dict(
+ dn=DN(('cn', 'group'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ cn=[u'Group'],
+ automemberdefaultgroup=[DN(('cn', defaultgroup1), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ value=u'group',
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Set default (fallback) automember group for hostgroups',
+ command=(
+ 'automember_default_group_set', [], dict(
+ type=u'hostgroup',
+ automemberdefaultgroup=defaulthostgroup1,
+ )
+ ),
+ expected=dict(
+ result=dict(
+ cn=[u'Hostgroup'],
+ automemberdefaultgroup=[DN(('cn', defaulthostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ value=u'hostgroup',
+ summary=u'Set default (fallback) group for automember "hostgroup"',
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve default automember group for hostgroups',
+ command=(
+ 'automember_default_group_show', [], dict(
+ type=u'hostgroup',
+ )
+ ),
+ expected=dict(
+ result=dict(
+ dn=DN(('cn', 'hostgroup'), ('cn', 'automember'), ('cn', 'etc'), api.env.basedn),
+ cn=[u'Hostgroup'],
+ automemberdefaultgroup=[DN(('cn', defaulthostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ value=u'hostgroup',
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % manager1,
+ command=(
+ 'user_add', [manager1], dict(givenname=u'Michael', sn=u'Scott')
+ ),
+ expected=dict(
+ value=manager1,
+ summary=u'Added user "mscott"',
+ result=dict(
+ gecos=[u'Michael Scott'],
+ givenname=[u'Michael'],
+ homedirectory=[u'/home/mscott'],
+ krbprincipalname=[u'mscott@' + api.env.realm],
+ has_keytab=False,
+ has_password=False,
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'Scott'],
+ uid=[manager1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (manager1, api.env.domain)],
+ displayname=[u'Michael Scott'],
+ cn=[u'Michael Scott'],
+ initials=[u'MS'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn', 'global_policy'), ('cn', api.env.realm), ('cn', 'kerberos'),
+ api.env.basedn)],
+ mepmanagedentry=[DN(('cn', manager1), ('cn', 'groups'), ('cn', 'accounts'),
+ api.env.basedn)],
+ memberof_group=[u'defaultgroup1', u'ipausers'],
+ dn=DN(('uid', 'mscott'), ('cn', 'users'), ('cn', 'accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1', manager=manager1)
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "tuser1"',
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ has_keytab=False,
+ has_password=False,
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ manager=[DN(('uid', 'mscott'), ('cn', 'users'), ('cn', 'accounts'), api.env.basedn)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn', 'global_policy'), ('cn', api.env.realm), ('cn', 'kerberos'),
+ api.env.basedn)],
+ mepmanagedentry=[DN(('cn', user1), ('cn', 'groups'), ('cn', 'accounts'),
+ api.env.basedn)],
+ memberof_group=[u'group1', u'ipausers'],
+ dn=DN(('uid', 'tuser1'), ('cn', 'users'), ('cn', 'accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn1,
+ command=('host_add', [fqdn1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Added host "%s"' % fqdn1,
+ result=dict(
+ dn=DN(('fqdn', fqdn1), ('cn', 'computers'), ('cn', 'accounts'), api.env.basedn),
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn1],
+ memberof_hostgroup=[hostgroup1],
+ memberofindirect_netgroup=[hostgroup1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn2,
+ command=('host_add', [fqdn2],
+ dict(
+ description=u'Test host 2',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn2,
+ summary=u'Added host "%s"' % fqdn2,
+ result=dict(
+ dn=DN(('fqdn', fqdn2), ('cn', 'computers'), ('cn', 'accounts'), api.env.basedn),
+ fqdn=[fqdn2],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn2, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn2],
+ memberof_hostgroup=[defaulthostgroup1],
+ memberofindirect_netgroup=[defaulthostgroup1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn3,
+ command=('host_add', [fqdn3],
+ dict(
+ description=u'Test host 3',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn3,
+ summary=u'Added host "%s"' % fqdn3,
+ result=dict(
+ dn=DN(('fqdn', fqdn3), ('cn', 'computers'), ('cn', 'accounts'), api.env.basedn),
+ fqdn=[fqdn3],
+ description=[u'Test host 3'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn3, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn3],
+ memberof_hostgroup=[hostgroup2],
+ memberofindirect_netgroup=[hostgroup2],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn4,
+ command=('host_add', [fqdn4],
+ dict(
+ description=u'Test host 4',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn4,
+ summary=u'Added host "%s"' % fqdn4,
+ result=dict(
+ dn=DN(('fqdn', fqdn4), ('cn', 'computers'), ('cn', 'accounts'), api.env.basedn),
+ fqdn=[fqdn4],
+ description=[u'Test host 4'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn4, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn4],
+ memberof_hostgroup=[hostgroup3],
+ memberofindirect_netgroup=[hostgroup3],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn5,
+ command=('host_add', [fqdn5],
+ dict(
+ description=u'Test host 5',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn5,
+ summary=u'Added host "%s"' % fqdn5,
+ result=dict(
+ dn=DN(('fqdn', fqdn5), ('cn', 'computers'), ('cn', 'accounts'), api.env.basedn),
+ fqdn=[fqdn5],
+ description=[u'Test host 5'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn5, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn5],
+ memberof_hostgroup=[hostgroup4],
+ memberofindirect_netgroup=[hostgroup4],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % hostgroup1,
+ command=('hostgroup_show', [hostgroup1], {}),
+ expected=dict(
+ value=hostgroup1,
+ summary=None,
+ result={
+ 'dn': DN(('cn', hostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ 'member_host': [u'%s' % fqdn1],
+ 'cn': [hostgroup1],
+ 'description': [u'Test desc'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % defaulthostgroup1,
+ command=('hostgroup_show', [defaulthostgroup1], {}),
+ expected=dict(
+ value=defaulthostgroup1,
+ summary=None,
+ result={
+ 'dn': DN(('cn', defaulthostgroup1), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ 'member_host': [u'%s' % fqdn2],
+ 'cn': [defaulthostgroup1],
+ 'description': [u'Default test desc'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % hostgroup2,
+ command=('hostgroup_show', [hostgroup2], {}),
+ expected=dict(
+ value=hostgroup2,
+ summary=None,
+ result={
+ 'dn': DN(('cn', hostgroup2), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ 'member_host': [u'%s' % fqdn3],
+ 'cn': [hostgroup2],
+ 'description': [u'Test desc'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % hostgroup3,
+ command=('hostgroup_show', [hostgroup3], {}),
+ expected=dict(
+ value=hostgroup3,
+ summary=None,
+ result={
+ 'dn': DN(('cn', hostgroup3), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ 'member_host': [u'%s' % fqdn4],
+ 'cn': [hostgroup3],
+ 'description': [u'Test desc'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % hostgroup4,
+ command=('hostgroup_show', [hostgroup4], {}),
+ expected=dict(
+ value=hostgroup4,
+ summary=None,
+ result={
+ 'dn': DN(('cn', hostgroup4), ('cn', 'hostgroups'), ('cn', 'accounts'), api.env.basedn),
+ 'member_host': [u'%s' % fqdn5],
+ 'cn': [hostgroup4],
+ 'description': [u'Test desc'],
+ },
+ ),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_automount_plugin.py b/ipatests/test_xmlrpc/test_automount_plugin.py
new file mode 100644
index 000000000..e1af651c8
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_automount_plugin.py
@@ -0,0 +1,582 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@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/>.
+"""
+Test the `ipalib/plugins/automount.py' module.
+"""
+
+import sys
+import textwrap
+import tempfile
+import shutil
+
+from ipalib import api
+from ipalib import errors
+from ipapython.dn import DN
+
+from nose.tools import raises, assert_raises # pylint: disable=E0611
+from xmlrpc_test import XMLRPC_test, assert_attr_equal
+from ipatests.util import assert_deepequal
+
+
+class MockTextui(list):
+ """Collects output lines"""
+ # Extend the mock object if other textui methods are called
+ def print_plain(self, line):
+ self.append(unicode(line))
+
+
+class AutomountTest(XMLRPC_test):
+ """Provides common functionality for automount tests"""
+ def check_tofiles(self):
+ """Check automountlocation_tofiles output against self.tofiles_output
+ """
+ res = api.Command['automountlocation_tofiles'](self.locname)
+
+ mock_ui = MockTextui()
+ command = api.Command['automountlocation_tofiles']
+ command.output_for_cli(mock_ui, res, self.locname)
+ expected_output = self.tofiles_output
+ assert_deepequal(expected_output, u'\n'.join(mock_ui))
+
+ def check_import_roundtrip(self):
+ """Check automountlocation_tofiles/automountlocation_import roundtrip
+
+ Loads self.tofiles_output (which should correspond to
+ automountlocation_tofiles output), then checks the resulting map
+ against tofiles_output again.
+ Do not use this if the test creates maps that aren't connected to
+ auto.master -- these can't be imported successfully.
+ """
+ conf_directory = tempfile.mkdtemp()
+
+ # Parse the tofiles_output into individual files, replace /etc/ by
+ # our temporary directory name
+ current_file = None
+ for line in self.tofiles_output.splitlines():
+ line = line.replace('/etc/', '%s/' % conf_directory)
+ if line.startswith(conf_directory) and line.endswith(':'):
+ current_file = open(line.rstrip(':'), 'w')
+ elif '--------' in line:
+ current_file.close()
+ elif line.startswith('maps not connected to '):
+ break
+ else:
+ current_file.write(line + '\n')
+ current_file.close()
+
+ self.failsafe_add(api.Object.automountlocation, self.locname)
+
+ try:
+ # Feed the files to automountlocation_import & check
+ master_file = u'%s/auto.master' % conf_directory
+ automountlocation_import = api.Command['automountlocation_import']
+ res = automountlocation_import(self.locname, master_file)
+ assert_deepequal(dict(
+ result=dict(
+ keys=lambda k: k,
+ maps=lambda m: m,
+ skipped=(),
+ duplicatemaps=(),
+ duplicatekeys=(),
+ )), res)
+ self.check_tofiles()
+ finally:
+ res = api.Command['automountlocation_del'](self.locname)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Success; delete the temporary directory
+ shutil.rmtree(conf_directory)
+
+class test_automount(AutomountTest):
+ """
+ Test the `automount` plugin.
+ """
+ locname = u'testlocation'
+ mapname = u'testmap'
+ keyname = u'testkey'
+ keyname_rename = u'testkey_rename'
+ keyname2 = u'testkey2'
+ description = u'description of map'
+ info = u'ro'
+ newinfo = u'rw'
+ map_kw = {'automountmapname': mapname, 'description': description, 'raw': True}
+ key_kw = {'automountkey': keyname, 'automountinformation': info, 'raw': True}
+ key_kw2 = {'automountkey': keyname2, 'automountinformation': info, 'raw': True}
+
+ tofiles_output = textwrap.dedent(u"""
+ /etc/auto.master:
+ /-\t/etc/auto.direct
+ ---------------------------
+ /etc/auto.direct:
+
+ maps not connected to /etc/auto.master:
+ ---------------------------
+ /etc/testmap:
+ testkey2\tro
+ """).strip()
+
+ def test_0_automountlocation_add(self):
+ """
+ Test adding a location `xmlrpc.automountlocation_add` method.
+ """
+ ret = self.failsafe_add(
+ api.Object.automountlocation, self.locname
+ )
+ entry = ret['result']
+ assert_attr_equal(entry, 'cn', self.locname)
+
+ def test_1_automountmap_add(self):
+ """
+ Test adding a map `xmlrpc.automountmap_add` method.
+ """
+ res = api.Command['automountmap_add'](self.locname, **self.map_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountmapname', self.mapname)
+
+ def test_2_automountkey_add(self):
+ """
+ Test adding a key using `xmlrpc.automountkey_add` method.
+ """
+ res = api.Command['automountkey_add'](self.locname, self.mapname, **self.key_kw2)['result']
+ assert res
+ assert_attr_equal(res, 'automountkey', self.keyname2)
+
+ def test_3_automountkey_add(self):
+ """
+ Test adding a key using `xmlrpc.automountkey_add` method.
+ """
+ res = api.Command['automountkey_add'](self.locname, self.mapname, **self.key_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountkey', self.keyname)
+
+ @raises(errors.DuplicateEntry)
+ def test_4_automountkey_add(self):
+ """
+ Test adding a duplicate key using `xmlrpc.automountkey_add` method.
+ """
+ res = api.Command['automountkey_add'](self.locname, self.mapname, **self.key_kw)
+
+ def test_5_automountmap_show(self):
+ """
+ Test the `xmlrpc.automountmap_show` method.
+ """
+ res = api.Command['automountmap_show'](self.locname, self.mapname, raw=True)['result']
+ assert res
+ assert_attr_equal(res, 'automountmapname', self.mapname)
+
+ def test_6_automountmap_find(self):
+ """
+ Test the `xmlrpc.automountmap_find` method.
+ """
+ res = api.Command['automountmap_find'](self.locname, self.mapname, raw=True)['result']
+ assert_attr_equal(res[0], 'automountmapname', self.mapname)
+
+ def test_7_automountkey_show(self):
+ """
+ Test the `xmlrpc.automountkey_show` method.
+ """
+ showkey_kw={'automountkey': self.keyname, 'automountinformation' : self.info, 'raw': True}
+ res = api.Command['automountkey_show'](self.locname, self.mapname, **showkey_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountkey', self.keyname)
+ assert_attr_equal(res, 'automountinformation', self.info)
+
+ def test_8_automountkey_find(self):
+ """
+ Test the `xmlrpc.automountkey_find` method.
+ """
+ res = api.Command['automountkey_find'](self.locname, self.mapname, raw=True)['result']
+ assert res
+ assert len(res) == 2
+ assert_attr_equal(res[0], 'automountkey', self.keyname)
+ assert_attr_equal(res[0], 'automountinformation', self.info)
+
+ def test_9_automountkey_mod(self):
+ """
+ Test the `xmlrpc.automountkey_mod` method.
+ """
+ self.key_kw['newautomountinformation'] = self.newinfo
+ self.key_kw['rename'] = self.keyname_rename
+ res = api.Command['automountkey_mod'](self.locname, self.mapname, **self.key_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountinformation', self.newinfo)
+ assert_attr_equal(res, 'automountkey', self.keyname_rename)
+
+ def test_a_automountmap_mod(self):
+ """
+ Test the `xmlrpc.automountmap_mod` method.
+ """
+ mod_kw = {'description': u'new description'}
+ res = api.Command['automountmap_mod'](self.locname, self.mapname, **mod_kw)['result']
+ assert res
+ assert_attr_equal(res, 'description', 'new description')
+
+ def test_a2_automountmap_tofiles(self):
+ """
+ Test the `automountlocation_tofiles` command.
+ """
+ res = api.Command['automountlocation_tofiles'](self.locname)
+ assert_deepequal(dict(
+ result=dict(
+ keys={'auto.direct': ()},
+ orphanmaps=(dict(
+ dn=DN(('automountmapname', self.mapname),
+ ('cn', self.locname),
+ ('cn', 'automount'), api.env.basedn),
+ description=(u'description of map',),
+ automountmapname=(u'testmap',)),),
+ orphankeys=[(
+ dict(
+ dn=DN(('description', self.keyname2),
+ ('automountmapname', 'testmap'),
+ ('cn', self.locname),
+ ('cn', 'automount'), api.env.basedn),
+ automountkey=(self.keyname2,),
+ description=(self.keyname2,),
+ automountinformation=(u'ro',),
+ ),
+ dict(
+ dn=DN(('description', self.keyname_rename),
+ ('automountmapname', 'testmap'),
+ ('cn', self.locname),
+ ('cn', 'automount'), api.env.basedn),
+ automountkey=(self.keyname_rename,),
+ description=(self.keyname_rename,),
+ automountinformation=(u'rw',),
+ ))],
+ maps=(
+ dict(
+ dn=DN(('description', '/- auto.direct'),
+ ('automountmapname', 'auto.master'),
+ ('cn', self.locname),
+ ('cn', 'automount'), api.env.basedn),
+ automountkey=(u'/-',),
+ description=(u'/- auto.direct',),
+ automountinformation=(u'auto.direct',)
+ ),
+ ))), res)
+
+ # Also check the CLI output
+
+ self.check_tofiles()
+
+ def test_b_automountkey_del(self):
+ """
+ Test the `xmlrpc.automountkey_del` method.
+ """
+ delkey_kw={'automountkey': self.keyname_rename, 'automountinformation' : self.newinfo}
+ res = api.Command['automountkey_del'](self.locname, self.mapname, **delkey_kw)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verify that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountkey_show'](self.locname, self.mapname, **delkey_kw)
+
+ def test_c_automountlocation_del(self):
+ """
+ Test the `xmlrpc.automountlocation_del` method.
+ """
+ res = api.Command['automountlocation_del'](self.locname)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verify that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountlocation_show'](self.locname)
+
+ def test_d_automountmap_del(self):
+ """
+ Test that the `xmlrpc.automountlocation_del` method removes all maps and keys
+ """
+ # Verify that the second key we added is gone
+ key_kw = {'automountkey': self.keyname2, 'automountinformation': self.info, 'raw': True}
+ with assert_raises(errors.NotFound):
+ api.Command['automountkey_show'](self.locname, self.mapname, **key_kw)
+
+
+class test_automount_direct(AutomountTest):
+ """
+ Test the `automount` plugin indirect map functionality.
+ """
+ locname = u'testlocation'
+ mapname = u'auto.direct2'
+ keyname = u'/-'
+ direct_kw = { 'key' : keyname }
+
+ tofiles_output = textwrap.dedent(u"""
+ /etc/auto.master:
+ /-\t/etc/auto.direct
+ /-\t/etc/auto.direct2
+ ---------------------------
+ /etc/auto.direct:
+ ---------------------------
+ /etc/auto.direct2:
+
+ maps not connected to /etc/auto.master:
+ """).strip()
+
+ def test_0_automountlocation_add(self):
+ """
+ Test adding a location.
+ """
+ res = api.Command['automountlocation_add'](self.locname, raw=True)['result']
+ assert res
+ assert_attr_equal(res, 'cn', self.locname)
+
+ def test_1_automountmap_add_direct(self):
+ """
+ Test adding a second direct map with a different info
+ """
+ res = api.Command['automountmap_add_indirect'](self.locname, self.mapname, **self.direct_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountmapname', self.mapname)
+
+ @raises(errors.DuplicateEntry)
+ def test_2_automountmap_add_duplicate(self):
+ """
+ Test adding a duplicate direct map.
+ """
+ res = api.Command['automountmap_add_indirect'](self.locname, self.mapname, **self.direct_kw)['result']
+
+ def test_2a_automountmap_tofiles(self):
+ """Test the `automountmap_tofiles` command"""
+ self.check_tofiles()
+
+ def test_3_automountlocation_del(self):
+ """
+ Remove the location.
+ """
+ res = api.Command['automountlocation_del'](self.locname)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verity that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountlocation_show'](self.locname)
+
+ def test_z_import_roundtrip(self):
+ """Check automountlocation_tofiles/automountlocation_import roundtrip
+ """
+ self.check_import_roundtrip()
+
+
+class test_automount_indirect(AutomountTest):
+ """
+ Test the `automount` plugin indirect map functionality.
+ """
+ locname = u'testlocation'
+ mapname = u'auto.home'
+ keyname = u'/home'
+ parentmap = u'auto.master'
+ map_kw = {'key': keyname, 'parentmap': parentmap, 'raw': True}
+ key_kw = {'automountkey': keyname, 'automountinformation': mapname}
+
+ tofiles_output = textwrap.dedent(u"""
+ /etc/auto.master:
+ /-\t/etc/auto.direct
+ /home\t/etc/auto.home
+ ---------------------------
+ /etc/auto.direct:
+ ---------------------------
+ /etc/auto.home:
+
+ maps not connected to /etc/auto.master:
+ """).strip()
+
+ def test_0_automountlocation_add(self):
+ """
+ Test adding a location.
+ """
+ res = api.Command['automountlocation_add'](self.locname, raw=True)['result']
+ assert res
+ assert_attr_equal(res, 'cn', self.locname)
+
+ def test_1_automountmap_add_indirect(self):
+ """
+ Test adding an indirect map.
+ """
+ res = api.Command['automountmap_add_indirect'](self.locname, self.mapname, **self.map_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountmapname', self.mapname)
+
+ @raises(errors.DuplicateEntry)
+ def test_1a_automountmap_add_indirect(self):
+ """
+ Test adding a duplicate indirect map.
+ """
+ api.Command['automountmap_add_indirect'](self.locname, self.mapname, **self.map_kw)['result']
+
+ def test_2_automountmap_show(self):
+ """
+ Test the `xmlrpc.automountmap_show` method.
+ """
+ res = api.Command['automountmap_show'](self.locname, self.mapname, raw=True)['result']
+ assert res
+ assert_attr_equal(res, 'automountmapname', self.mapname)
+
+ def test_2a_automountmap_tofiles(self):
+ """Test the `automountmap_tofiles` command"""
+ self.check_tofiles()
+
+ def test_3_automountkey_del(self):
+ """
+ Remove the indirect key /home.
+ """
+ res = api.Command['automountkey_del'](self.locname, self.parentmap, **self.key_kw)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verify that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountkey_show'](self.locname, self.parentmap, **self.key_kw)
+
+ def test_4_automountmap_del(self):
+ """
+ Remove the indirect map for auto.home.
+ """
+ res = api.Command['automountmap_del'](self.locname, self.mapname)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verify that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountmap_show'](self.locname, self.mapname)
+
+ def test_5_automountlocation_del(self):
+ """
+ Remove the location.
+ """
+ res = api.Command['automountlocation_del'](self.locname)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verity that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountlocation_show'](self.locname)
+
+ def test_z_import_roundtrip(self):
+ """Check automountlocation_tofiles/automountlocation_import roundtrip
+ """
+ self.check_import_roundtrip()
+
+class test_automount_indirect_no_parent(AutomountTest):
+ """
+ Test the `automount` plugin Indirect map function.
+ """
+ locname = u'testlocation'
+ mapname = u'auto.home'
+ keyname = u'/home'
+ mapname2 = u'auto.direct2'
+ keyname2 = u'direct2'
+ parentmap = u'auto.master'
+ map_kw = {'key': keyname, 'raw': True}
+ map_kw2 = {'key': keyname2, 'raw': True}
+
+ tofiles_output = textwrap.dedent(u"""
+ /etc/auto.master:
+ /-\t/etc/auto.direct
+ /home\t/etc/auto.home
+ ---------------------------
+ /etc/auto.direct:
+ ---------------------------
+ /etc/auto.home:
+ direct2\t-fstype=autofs ldap:auto.direct2
+
+ maps not connected to /etc/auto.master:
+ ---------------------------
+ /etc/auto.direct2:
+ """).strip()
+
+ def test_0_automountlocation_add(self):
+ """
+ Test adding a location.
+ """
+ res = api.Command['automountlocation_add'](self.locname, raw=True)['result']
+ assert res
+ assert_attr_equal(res, 'cn', self.locname)
+
+ def test_1_automountmap_add_indirect(self):
+ """
+ Test adding an indirect map with default parent.
+ """
+ res = api.Command['automountmap_add_indirect'](self.locname, self.mapname, **self.map_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountmapname', self.mapname)
+
+ def test_2_automountkey_show(self):
+ """
+ Test the `xmlrpc.automountkey_show` method with default parent.
+ """
+ showkey_kw = {'automountkey': self.keyname, 'automountinformation': self.mapname, 'raw': True}
+ res = api.Command['automountkey_show'](self.locname, self.parentmap, **showkey_kw)['result']
+ assert res
+ assert_attr_equal(res, 'automountkey', self.keyname)
+
+ def test_2a_automountmap_add_indirect(self):
+ """
+ Test adding an indirect map with default parent.
+ """
+ res = api.Command['automountmap_add_indirect'](self.locname,
+ u'auto.direct2', parentmap=self.mapname, **self.map_kw2)['result']
+ assert res
+ assert_attr_equal(res, 'automountmapname', self.mapname2)
+
+ def test_2b_automountmap_tofiles(self):
+ """Test the `automountmap_tofiles` command"""
+ self.check_tofiles()
+
+ def test_3_automountkey_del(self):
+ """
+ Remove the indirect key /home.
+ """
+ delkey_kw={'automountkey': self.keyname, 'automountinformation': self.mapname}
+ res = api.Command['automountkey_del'](self.locname, self.parentmap, **delkey_kw)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verify that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountkey_show'](self.locname, self.parentmap, **delkey_kw)
+
+ def test_4_automountmap_del(self):
+ """
+ Remove the indirect map for auto.home.
+ """
+ res = api.Command['automountmap_del'](self.locname, self.mapname)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verify that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountmap_show'](self.locname, self.mapname)
+
+ def test_5_automountlocation_del(self):
+ """
+ Remove the location.
+ """
+ res = api.Command['automountlocation_del'](self.locname)['result']
+ assert res
+ assert_attr_equal(res, 'failed', '')
+
+ # Verity that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['automountlocation_show'](self.locname)
diff --git a/ipatests/test_xmlrpc/test_baseldap_plugin.py b/ipatests/test_xmlrpc/test_baseldap_plugin.py
new file mode 100644
index 000000000..6a8501f76
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_baseldap_plugin.py
@@ -0,0 +1,159 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+
+"""
+Test the `ipalib.plugins.baseldap` module.
+"""
+
+from ipalib import errors
+from ipalib.plugins import baseldap
+
+
+def test_exc_wrapper():
+ """Test the CallbackInterface._exc_wrapper helper method"""
+ handled_exceptions = []
+
+ class test_callback(baseldap.BaseLDAPCommand):
+ """Fake IPA method"""
+ def test_fail(self):
+ self._exc_wrapper([], {}, self.fail)(1, 2, a=1, b=2)
+
+ def fail(self, *args, **kwargs):
+ assert args == (1, 2)
+ assert kwargs == dict(a=1, b=2)
+ raise errors.ExecutionError('failure')
+
+ instance = test_callback()
+
+ # Test with one callback first
+
+ @test_callback.register_exc_callback
+ def handle_exception(self, keys, options, e, call_func, *args, **kwargs):
+ assert args == (1, 2)
+ assert kwargs == dict(a=1, b=2)
+ handled_exceptions.append(type(e))
+
+ instance.test_fail()
+ assert handled_exceptions == [errors.ExecutionError]
+
+ # Test with another callback added
+
+ handled_exceptions = []
+
+ def dont_handle(self, keys, options, e, call_func, *args, **kwargs):
+ assert args == (1, 2)
+ assert kwargs == dict(a=1, b=2)
+ handled_exceptions.append(None)
+ raise e
+ test_callback.register_exc_callback(dont_handle, first=True)
+
+ instance.test_fail()
+ assert handled_exceptions == [None, errors.ExecutionError]
+
+
+def test_callback_registration():
+ class callbacktest_base(baseldap.CallbackInterface):
+ _callback_registry = dict(test={})
+
+ def test_callback(self, param):
+ messages.append(('Base test_callback', param))
+
+ def registered_callback(self, param):
+ messages.append(('Base registered callback', param))
+ callbacktest_base.register_callback('test', registered_callback)
+
+ class SomeClass(object):
+ def registered_callback(self, command, param):
+ messages.append(('Registered callback from another class', param))
+ callbacktest_base.register_callback('test', SomeClass().registered_callback)
+
+ class callbacktest_subclass(callbacktest_base):
+ pass
+
+ def subclass_callback(self, param):
+ messages.append(('Subclass registered callback', param))
+ callbacktest_subclass.register_callback('test', subclass_callback)
+
+
+ messages = []
+ instance = callbacktest_base()
+ for callback in instance.get_callbacks('test'):
+ callback(instance, 42)
+ assert messages == [
+ ('Base test_callback', 42),
+ ('Base registered callback', 42),
+ ('Registered callback from another class', 42)]
+
+ messages = []
+ instance = callbacktest_subclass()
+ for callback in instance.get_callbacks('test'):
+ callback(instance, 42)
+ assert messages == [
+ ('Base test_callback', 42),
+ ('Subclass registered callback', 42)]
+
+
+def test_exc_callback_registration():
+ messages = []
+ class callbacktest_base(baseldap.BaseLDAPCommand):
+ """A method superclass with an exception callback"""
+ def exc_callback(self, keys, options, exc, call_func, *args, **kwargs):
+ """Let the world know we saw the error, but don't handle it"""
+ messages.append('Base exc_callback')
+ raise exc
+
+ def test_fail(self):
+ """Raise a handled exception"""
+ try:
+ self._exc_wrapper([], {}, self.fail)(1, 2, a=1, b=2)
+ except Exception:
+ pass
+
+ def fail(self, *args, **kwargs):
+ """Raise an error"""
+ raise errors.ExecutionError('failure')
+
+ base_instance = callbacktest_base()
+
+ class callbacktest_subclass(callbacktest_base):
+ pass
+
+ @callbacktest_subclass.register_exc_callback
+ def exc_callback(self, keys, options, exc, call_func, *args, **kwargs):
+ """Subclass's private exception callback"""
+ messages.append('Subclass registered callback')
+ raise exc
+
+ subclass_instance = callbacktest_subclass()
+
+ # Make sure exception in base class is only handled by the base class
+ base_instance.test_fail()
+ assert messages == ['Base exc_callback']
+
+
+ @callbacktest_base.register_exc_callback
+ def exc_callback(self, keys, options, exc, call_func, *args, **kwargs):
+ """Callback on super class; doesn't affect the subclass"""
+ messages.append('Superclass registered callback')
+ raise exc
+
+ # Make sure exception in subclass is only handled by both
+ messages = []
+ subclass_instance.test_fail()
+ assert messages == ['Base exc_callback', 'Subclass registered callback']
diff --git a/ipatests/test_xmlrpc/test_batch_plugin.py b/ipatests/test_xmlrpc/test_batch_plugin.py
new file mode 100644
index 000000000..2b056c93f
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_batch_plugin.py
@@ -0,0 +1,232 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+
+"""
+Test the `ipalib/plugins/batch.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from ipatests.util import assert_equal, Fuzzy, assert_deepequal
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+group1 = u'testgroup1'
+
+
+def deepequal_list(*expected):
+ """Factory for a function that checks a list
+
+ The created function asserts items of a list are "deepequal" to the given
+ argument. Unlike using assert_deepequal directly, the order matters.
+ """
+ def checker(got):
+ if len(expected) != len(got):
+ raise AssertionError('Expected %s entries, got %s\n\n%s\n%s' %
+ (len(expected), len(got), expected, got))
+ for e, g in zip(expected, got):
+ assert_deepequal(e, g)
+ return True
+ return checker
+
+
+class test_batch(Declarative):
+
+ cleanup_commands = [
+ ('group_del', [group1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Batch ping',
+ command=('batch', [dict(method='ping', params=([], {}))], {}),
+ expected=dict(
+ count=1,
+ results=[
+ dict(summary=Fuzzy('IPA server version .*'), error=None),
+ ]
+ ),
+ ),
+
+ dict(
+ desc='Batch two pings',
+ command=('batch', [dict(method='ping', params=([], {}))] * 2, {}),
+ expected=dict(
+ count=2,
+ results=[
+ dict(summary=Fuzzy('IPA server version .*'), error=None),
+ dict(summary=Fuzzy('IPA server version .*'), error=None),
+ ]
+ ),
+ ),
+
+ dict(
+ desc='Create and deleting a group',
+ command=('batch', [
+ dict(method='group_add',
+ params=([group1], dict(description=u'Test desc 1'))),
+ dict(method='group_del', params=([group1], dict())),
+ ], {}),
+ expected=dict(
+ count=2,
+ results=deepequal_list(
+ dict(
+ value=group1,
+ summary=u'Added group "testgroup1"',
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc 1'],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ gidnumber=[fuzzy_digits],
+ dn=DN(('cn', 'testgroup1'),
+ ('cn', 'groups'),
+ ('cn', 'accounts'),
+ api.env.basedn),
+ ),
+ error=None),
+ dict(
+ summary=u'Deleted group "%s"' % group1,
+ result=dict(failed=u''),
+ value=group1,
+ error=None),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Try to delete nonexistent group twice',
+ command=('batch', [
+ dict(method='group_del', params=([group1], dict())),
+ dict(method='group_del', params=([group1], dict())),
+ ], {}),
+ expected=dict(
+ count=2,
+ results=[
+ dict(
+ error=u'%s: group not found' % group1,
+ error_name=u'NotFound',
+ error_code=4001,
+ ),
+ dict(
+ error=u'%s: group not found' % group1,
+ error_name=u'NotFound',
+ error_code=4001,
+ ),
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Try to delete non-existent group first, then create it',
+ command=('batch', [
+ dict(method='group_del', params=([group1], dict())),
+ dict(method='group_add',
+ params=([group1], dict(description=u'Test desc 1'))),
+ ], {}),
+ expected=dict(
+ count=2,
+ results=deepequal_list(
+ dict(
+ error=u'%s: group not found' % group1,
+ error_name=u'NotFound',
+ error_code=4001,
+ ),
+ dict(
+ value=group1,
+ summary=u'Added group "testgroup1"',
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc 1'],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ gidnumber=[fuzzy_digits],
+ dn=DN(('cn', 'testgroup1'),
+ ('cn', 'groups'),
+ ('cn', 'accounts'),
+ api.env.basedn),
+ ),
+ error=None),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Try bad command invocations',
+ command=('batch', [
+ # bad command name
+ dict(method='nonexistent_ipa_command', params=([], dict())),
+ # dash, not underscore, in command name
+ dict(method='user-del', params=([], dict())),
+ # missing command name
+ dict(params=([group1], dict())),
+ # missing params
+ dict(method='user_del'),
+ # missing required argument
+ dict(method='user_add', params=([], dict())),
+ # missing required option
+ dict(method='group_add', params=([group1], dict())),
+ # bad type
+ dict(method='group_add', params=([group1], dict(
+ description=u't', gidnumber=u'bad'))),
+ ], {}),
+ expected=dict(
+ count=7,
+ results=deepequal_list(
+ dict(
+ error=u"unknown command 'nonexistent_ipa_command'",
+ error_name=u'CommandError',
+ error_code=905,
+ ),
+ dict(
+ error=u"unknown command 'user-del'",
+ error_name=u'CommandError',
+ error_code=905,
+ ),
+ dict(
+ error=u"'method' is required",
+ error_name=u'RequirementError',
+ error_code=3007,
+ ),
+ dict(
+ error=u"'params' is required",
+ error_name=u'RequirementError',
+ error_code=3007,
+ ),
+ dict(
+ error=u"'givenname' is required",
+ error_name=u'RequirementError',
+ error_code=3007,
+ ),
+ dict(
+ error=u"'description' is required",
+ error_name=u'RequirementError',
+ error_code=3007,
+ ),
+ dict(
+ error=Fuzzy(u"invalid 'gid'.*"),
+ error_name=u'ConversionError',
+ error_code=3008,
+ ),
+ ),
+ ),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_cert_plugin.py b/ipatests/test_xmlrpc/test_cert_plugin.py
new file mode 100644
index 000000000..508e9141a
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_cert_plugin.py
@@ -0,0 +1,454 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2009,2013 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/>.
+"""
+Test the `ipalib/plugins/cert.py` module against a RA.
+"""
+
+import sys
+import os
+import shutil
+from nose.tools import raises, assert_raises # pylint: disable=E0611
+
+from xmlrpc_test import XMLRPC_test, assert_attr_equal
+from ipalib import api
+from ipalib import errors
+from ipalib import x509
+import tempfile
+from ipapython import ipautil
+import nose
+import base64
+from ipapython.dn import DN
+
+# So we can save the cert from issuance and compare it later
+cert = None
+newcert = None
+
+def is_db_configured():
+ """
+ Raise an exception if we are testing against lite-server and the
+ developer cert database is configured.
+ """
+ aliasdir = api.env.dot_ipa + os.sep + 'alias' + os.sep + '.pwd'
+
+ if (api.env.xmlrpc_uri == u'http://localhost:8888/ipa/xml' and
+ not ipautil.file_exists(aliasdir)):
+ raise nose.SkipTest('developer CA not configured in %s' % aliasdir)
+
+# Test setup
+#
+# This test needs a configured CA behind it in order to work properly
+#
+# To test against Apache directly then no changes are required. Just be
+# sure the xmlrpc_uri in ~/.ipa/default.conf points to Apache.
+#
+# To test against Dogtag CA in the lite-server:
+#
+# - Copy the 3 NSS db files from /etc/httpd/alias to ~/.ipa/alias
+# - Copy /etc/httpd/alias/pwdfile.txt to ~/.ipa/alias/.pwd.
+# - Change ownership of these files to be readable by you.
+#
+# The API tested depends on the value of ~/.ipa/default/ra_plugin when
+# running as the lite-server.
+
+class test_cert(XMLRPC_test):
+
+ @classmethod
+ def setUpClass(cls):
+ super(test_cert, cls).setUpClass()
+
+ if 'cert_request' not in api.Command:
+ raise nose.SkipTest('cert_request not registered')
+
+ is_db_configured()
+
+ def run_certutil(self, args, stdin=None):
+ new_args = ["/usr/bin/certutil", "-d", self.reqdir]
+ new_args = new_args + args
+ return ipautil.run(new_args, stdin)
+
+ def setUp(self):
+ super(test_cert, self).setUp()
+ self.reqdir = tempfile.mkdtemp(prefix = "tmp-")
+ self.reqfile = self.reqdir + "/test.csr"
+ self.pwname = self.reqdir + "/pwd"
+
+ # Create an empty password file
+ fp = open(self.pwname, "w")
+ fp.write("\n")
+ fp.close()
+
+ # Create our temporary NSS database
+ self.run_certutil(["-N", "-f", self.pwname])
+
+ self.subject = DN(('CN', self.host_fqdn), x509.subject_base())
+
+ def tearDown(self):
+ super(test_cert, self).tearDown()
+ shutil.rmtree(self.reqdir, ignore_errors=True)
+
+ def generateCSR(self, subject):
+ self.run_certutil(["-R", "-s", subject,
+ "-o", self.reqfile,
+ "-z", "/etc/group",
+ "-f", self.pwname,
+ "-a",
+ ])
+ fp = open(self.reqfile, "r")
+ data = fp.read()
+ fp.close()
+ return data
+
+ """
+ Test the `cert` plugin.
+ """
+ host_fqdn = u'ipatestcert.%s' % api.env.domain
+ service_princ = u'test/%s@%s' % (host_fqdn, api.env.realm)
+
+ def test_0001_cert_add(self):
+ """
+ Test the `xmlrpc.cert_request` method without --add.
+
+ This should fail because the service principal doesn't exist
+ """
+ # First create the host that will use this policy
+ res = api.Command['host_add'](self.host_fqdn, force= True)['result']
+
+ csr = unicode(self.generateCSR(str(self.subject)))
+ with assert_raises(errors.NotFound):
+ res = api.Command['cert_request'](csr, principal=self.service_princ)
+
+ def test_0002_cert_add(self):
+ """
+ Test the `xmlrpc.cert_request` method with --add.
+ """
+ # Our host should exist from previous test
+ global cert
+
+ csr = unicode(self.generateCSR(str(self.subject)))
+ res = api.Command['cert_request'](csr, principal=self.service_princ, add=True)['result']
+ assert DN(res['subject']) == self.subject
+ # save the cert for the service_show/find tests
+ cert = res['certificate']
+
+ def test_0003_service_show(self):
+ """
+ Verify that service-show has the right certificate using service-show.
+ """
+ global cert
+
+ res = api.Command['service_show'](self.service_princ)['result']
+ assert base64.b64encode(res['usercertificate'][0]) == cert
+
+ def test_0004_service_find(self):
+ """
+ Verify that service-find has the right certificate using service-find.
+ """
+ global cert
+
+ # Assume there is only one service
+ res = api.Command['service_find'](self.service_princ)['result']
+ assert base64.b64encode(res[0]['usercertificate'][0]) == cert
+
+ def test_0005_cert_renew(self):
+ """
+ Issue a new certificate for a service
+ """
+ global newcert
+
+ csr = unicode(self.generateCSR(str(self.subject)))
+ res = api.Command['cert_request'](csr, principal=self.service_princ)['result']
+ assert DN(res['subject']) == self.subject
+ # save the cert for the service_show/find tests
+ newcert = res['certificate']
+
+ def test_0006_service_show(self):
+ """
+ Verify the new certificate with service-show.
+ """
+ global cert, newcert
+
+ res = api.Command['service_show'](self.service_princ)['result']
+ # It should no longer match our old cert
+ assert base64.b64encode(res['usercertificate'][0]) != cert
+ # And it should match the new one
+ assert base64.b64encode(res['usercertificate'][0]) == newcert
+
+ def test_0007_cleanup(self):
+ """
+ Clean up cert test data
+ """
+ # Now clean things up
+ api.Command['host_del'](self.host_fqdn)
+
+ # Verify that the service is gone
+ res = api.Command['service_find'](self.service_princ)
+ assert res['count'] == 0
+
+class test_cert_find(XMLRPC_test):
+
+ @classmethod
+ def setUpClass(cls):
+ super(test_cert_find, cls).setUpClass()
+
+ if 'cert_find' not in api.Command:
+ raise nose.SkipTest('cert_find not registered')
+
+ if api.env.ra_plugin != 'dogtag':
+ raise nose.SkipTest('cert_find for dogtag CA only')
+
+ is_db_configured()
+
+ """
+ Test the `cert-find` command.
+ """
+ short = api.env.host.replace('.' + api.env.domain, '')
+
+ def test_0001_find_all(self):
+ """
+ Search for all certificates.
+
+ We don't know how many we'll get but there should be at least 10
+ by default.
+ """
+ res = api.Command['cert_find']()
+ assert 'count' in res and res['count'] >= 10
+
+ def test_0002_find_CA(self):
+ """
+ Search for the CA certificate.
+ """
+ res = api.Command['cert_find'](subject=u'Certificate Authority')
+ assert 'count' in res and res['count'] == 1
+
+ def test_0003_find_OCSP(self):
+ """
+ Search for the OCSP certificate.
+ """
+ res = api.Command['cert_find'](subject=u'OCSP Subsystem')
+
+ def test_0004_find_this_host(self):
+ """
+ Find all certificates for this IPA server
+ """
+ res = api.Command['cert_find'](subject=api.env.host)
+ assert 'count' in res and res['count'] > 1
+
+ def test_0005_find_this_host_exact(self):
+ """
+ Find all certificates for this IPA server (exact)
+ """
+ res = api.Command['cert_find'](subject=api.env.host, exactly=True)
+ assert 'count' in res and res['count'] > 1
+
+ def test_0006_find_this_short_host_exact(self):
+ """
+ Find all certificates for this IPA server short name (exact)
+ """
+ res = api.Command['cert_find'](subject=self.short, exactly=True)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0007_find_revocation_reason_0(self):
+ """
+ Find all certificates with revocation reason 0
+ """
+ res = api.Command['cert_find'](revocation_reason=0)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0008_find_revocation_reason_1(self):
+ """
+ Find all certificates with revocation reason 1
+ """
+ res = api.Command['cert_find'](revocation_reason=1)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0009_find_revocation_reason_2(self):
+ """
+ Find all certificates with revocation reason 2
+ """
+ res = api.Command['cert_find'](revocation_reason=2)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0010_find_revocation_reason_3(self):
+ """
+ Find all certificates with revocation reason 3
+ """
+ res = api.Command['cert_find'](revocation_reason=3)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0011_find_revocation_reason_4(self):
+ """
+ Find all certificates with revocation reason 4
+
+ There is no way to know in advance how many revoked certificates
+ we'll have but in the context of make-test we'll have at least one.
+ """
+ res = api.Command['cert_find'](revocation_reason=4)
+ assert 'count' in res and res['count'] >= 1
+
+ def test_0012_find_revocation_reason_5(self):
+ """
+ Find all certificates with revocation reason 5
+ """
+ res = api.Command['cert_find'](revocation_reason=5)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0013_find_revocation_reason_6(self):
+ """
+ Find all certificates with revocation reason 6
+ """
+ res = api.Command['cert_find'](revocation_reason=6)
+ assert 'count' in res and res['count'] == 0
+
+ # There is no revocation reason #7
+
+ def test_0014_find_revocation_reason_8(self):
+ """
+ Find all certificates with revocation reason 8
+ """
+ res = api.Command['cert_find'](revocation_reason=8)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0015_find_revocation_reason_9(self):
+ """
+ Find all certificates with revocation reason 9
+ """
+ res = api.Command['cert_find'](revocation_reason=9)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0016_find_revocation_reason_10(self):
+ """
+ Find all certificates with revocation reason 10
+ """
+ res = api.Command['cert_find'](revocation_reason=10)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0017_find_by_issuedon(self):
+ """
+ Find all certificates issued since 2008
+ """
+ res = api.Command['cert_find'](issuedon_from=u'2008-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 10
+
+ def test_0018_find_through_issuedon(self):
+ """
+ Find all certificates issued through 2008
+ """
+ res = api.Command['cert_find'](issuedon_to=u'2008-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0019_find_notvalid_before(self):
+ """
+ Find all certificates valid not before 2008
+ """
+ res = api.Command['cert_find'](validnotbefore_from=u'2008-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 10
+
+ def test_0020_find_notvalid_before(self):
+ """
+ Find all certificates valid not before to 2100
+ """
+ res = api.Command['cert_find'](validnotbefore_to=u'2100-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 10
+
+ def test_0021_find_notvalid_before(self):
+ """
+ Find all certificates valid not before 2100
+ """
+ res = api.Command['cert_find'](validnotbefore_from=u'2100-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0022_find_notvalid_before(self):
+ """
+ Find all certificates valid not before to 2008
+ """
+ res = api.Command['cert_find'](validnotbefore_to=u'2008-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0023_find_notvalid_after(self):
+ """
+ Find all certificates valid not after 2008
+ """
+ res = api.Command['cert_find'](validnotafter_from=u'2008-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 10
+
+ def test_0024_find_notvalid_after(self):
+ """
+ Find all certificates valid not after to 2100
+ """
+ res = api.Command['cert_find'](validnotafter_to=u'2100-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 10
+
+ def test_0025_find_notvalid_after(self):
+ """
+ Find all certificates valid not after 2100
+ """
+ res = api.Command['cert_find'](validnotafter_from=u'2100-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0026_find_notvalid_after(self):
+ """
+ Find all certificates valid not after to 2008
+ """
+ res = api.Command['cert_find'](validnotafter_to=u'2008-01-01',
+ sizelimit=10)
+ assert 'count' in res and res['count'] == 0
+
+ def test_0027_sizelimit_zero(self):
+ """
+ Search with a sizelimit of 0
+ """
+ res = api.Command['cert_find'](sizelimit=0)
+ assert 'count' in res and res['count'] == 0
+
+ @raises(errors.ValidationError)
+ def test_0028_find_negative_size(self):
+ """
+ Search with a negative sizelimit
+ """
+ res = api.Command['cert_find'](sizelimit=-100)
+
+ def test_0029_search_for_notfound(self):
+ """
+ Search for a host that isn't there.
+ """
+ res = api.Command['cert_find'](subject=u'notfound')
+ assert 'count' in res and res['count'] == 0
+
+ def test_0030_search_for_testcerts(self):
+ """
+ Search for certs created in other tests
+ """
+ res = api.Command['cert_find'](subject=u'ipatestcert.%s' % api.env.domain)
+ assert 'count' in res and res['count'] >= 1
+
+ @raises(errors.ValidationError)
+ def test_0031_search_on_invalid_date(self):
+ """
+ Search using invalid date format
+ """
+ res = api.Command['cert_find'](issuedon_from=u'xyz')
diff --git a/ipatests/test_xmlrpc/test_config_plugin.py b/ipatests/test_xmlrpc/test_config_plugin.py
new file mode 100644
index 000000000..3d9a31daf
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_config_plugin.py
@@ -0,0 +1,121 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib/plugins/config.py` module.
+"""
+
+from ipalib import errors
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+
+class test_config(Declarative):
+
+ cleanup_commands = [
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to add an unrelated objectclass to ipauserobjectclasses',
+ command=('config_mod', [],
+ dict(addattr=u'ipauserobjectclasses=ipahost')),
+ expected=dict(
+ result=lambda d: 'ipahost' in d['ipauserobjectclasses'],
+ value=u'',
+ summary=None,
+ ),
+ ),
+
+ dict(
+ desc='Remove the unrelated objectclass from ipauserobjectclasses',
+ command=('config_mod', [],
+ dict(delattr=u'ipauserobjectclasses=ipahost')),
+ expected=dict(
+ result=lambda d: 'ipahost' not in d['ipauserobjectclasses'],
+ value=u'',
+ summary=None,
+ ),
+ ),
+
+ dict(
+ desc='Try to remove ipausersearchfields',
+ command=('config_mod', [],
+ dict(delattr=u'ipausersearchfields=uid,givenname,sn,telephonenumber,ou,title')),
+ expected=errors.RequirementError(name='ipausersearchfields'),
+ ),
+
+ dict(
+ desc='Try to set ipaselinuxusermapdefault not in selinux order list',
+ command=('config_mod', [],
+ dict(ipaselinuxusermapdefault=u'unknown_u:s0')),
+ expected=errors.ValidationError(name='ipaselinuxusermapdefault',
+ error='SELinux user map default user not in order list'),
+ ),
+
+ dict(
+ desc='Try to set invalid ipaselinuxusermapdefault',
+ command=('config_mod', [],
+ dict(ipaselinuxusermapdefault=u'foo')),
+ expected=errors.ValidationError(name='ipaselinuxusermapdefault',
+ error='Invalid MLS value, must match s[0-15](-s[0-15])'),
+ ),
+
+ dict(
+ desc='Try to set invalid ipaselinuxusermapdefault with setattr',
+ command=('config_mod', [],
+ dict(setattr=u'ipaselinuxusermapdefault=unknown_u:s0')),
+ expected=errors.ValidationError(name='ipaselinuxusermapdefault',
+ error='SELinux user map default user not in order list'),
+ ),
+
+ dict(
+ desc='Try to set ipaselinuxusermaporder without ipaselinuxusermapdefault out of it',
+ command=('config_mod', [],
+ dict(ipaselinuxusermaporder=u'notfound_u:s0')),
+ expected=errors.ValidationError(name='ipaselinuxusermaporder',
+ error='SELinux user map default user not in order list'),
+ ),
+
+ dict(
+ desc='Try to set invalid ipaselinuxusermaporder',
+ command=('config_mod', [],
+ dict(ipaselinuxusermaporder=u'$')),
+ expected=errors.ValidationError(name='ipaselinuxusermaporder',
+ error='A list of SELinux users delimited by $ expected'),
+ ),
+
+ dict(
+ desc='Try to set invalid selinux user in ipaselinuxusermaporder',
+ command=('config_mod', [],
+ dict(ipaselinuxusermaporder=u'unconfined_u:s0-s0:c0.c1023$baduser$guest_u:s0')),
+ expected=errors.ValidationError(name='ipaselinuxusermaporder',
+ error='SELinux user \'baduser\' is not valid: Invalid MLS '
+ 'value, must match s[0-15](-s[0-15])'),
+ ),
+
+ dict(
+ desc='Try to set new selinux order and invalid default user',
+ command=('config_mod', [],
+ dict(ipaselinuxusermaporder=u'xguest_u:s0$guest_u:s0$user_u:s0-s0:c0.c1023$staff_u:s0-s0:c0.c1023$unconfined_u:s0-s0:c0.c1023',
+ ipaselinuxusermapdefault=u'unknown_u:s0')),
+ expected=errors.ValidationError(name='ipaselinuxusermapdefault',
+ error='SELinux user map default user not in order list'),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_delegation_plugin.py b/ipatests/test_xmlrpc/test_delegation_plugin.py
new file mode 100644
index 000000000..f2cfc8302
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_delegation_plugin.py
@@ -0,0 +1,300 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib/plugins/delegation.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+delegation1 = u'testdelegation'
+member1 = u'admins'
+
+class test_delegation(Declarative):
+
+ cleanup_commands = [
+ ('delegation_del', [delegation1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % delegation1,
+ command=('delegation_show', [delegation1], {}),
+ expected=errors.NotFound(
+ reason=u'ACI with name "%s" not found' % delegation1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % delegation1,
+ command=('delegation_mod', [delegation1], dict(group=u'admins')),
+ expected=errors.NotFound(
+ reason=u'ACI with name "%s" not found' % delegation1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % delegation1,
+ command=('delegation_del', [delegation1], {}),
+ expected=errors.NotFound(
+ reason=u'ACI with name "%s" not found' % delegation1),
+ ),
+
+
+ dict(
+ desc='Search for non-existent %r' % delegation1,
+ command=('delegation_find', [delegation1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 delegations matched',
+ result=[],
+ ),
+ ),
+
+ dict(
+ desc='Try to create %r for non-existing member group' % delegation1,
+ command=(
+ 'delegation_add', [delegation1], dict(
+ attrs=u'street,c,l,st,postalCode',
+ permissions=u'write',
+ group=u'editors',
+ memberof=u'nonexisting',
+ ),
+ ),
+ expected=errors.NotFound(reason=u'nonexisting: group not found'),
+ ),
+
+ # Note that we add postalCode but expect postalcode. This tests
+ # the attrs normalizer.
+ dict(
+ desc='Create %r' % delegation1,
+ command=(
+ 'delegation_add', [delegation1], dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalCode'],
+ permissions=u'write',
+ group=u'editors',
+ memberof=u'admins',
+ )
+ ),
+ expected=dict(
+ value=delegation1,
+ summary=u'Added delegation "%s"' % delegation1,
+ result=dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalcode'],
+ permissions=[u'write'],
+ aciname=delegation1,
+ group=u'editors',
+ memberof=member1,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % delegation1,
+ command=(
+ 'delegation_add', [delegation1], dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalCode'],
+ permissions=u'write',
+ group=u'editors',
+ memberof=u'admins',
+ ),
+ ),
+ expected=errors.DuplicateEntry(),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % delegation1,
+ command=('delegation_show', [delegation1], {}),
+ expected=dict(
+ value=delegation1,
+ summary=None,
+ result={
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'write'],
+ 'aciname': delegation1,
+ 'group': u'editors',
+ 'memberof': member1,
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r with --raw' % delegation1,
+ command=('delegation_show', [delegation1], {'raw' : True}),
+ expected=dict(
+ value=delegation1,
+ summary=None,
+ result={
+ 'aci': u'(targetattr = "street || c || l || st || postalcode")(targetfilter = "(memberOf=%s)")(version 3.0;acl "delegation:testdelegation";allow (write) groupdn = "ldap:///%s";)' % \
+ (DN(('cn', 'admins'), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn),
+ DN(('cn', 'editors'), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn))
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % delegation1,
+ command=('delegation_find', [delegation1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 delegation matched',
+ result=[
+ {
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'write'],
+ 'aciname': delegation1,
+ 'group': u'editors',
+ 'memberof': member1,
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r using --group filter' % delegation1,
+ command=('delegation_find', [delegation1], {'group': u'editors'}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 delegation matched',
+ result=[
+ {
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'write'],
+ 'aciname': delegation1,
+ 'group': u'editors',
+ 'memberof': member1,
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r using --membergroup filter' % delegation1,
+ command=('delegation_find', [delegation1], {'memberof': member1}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 delegation matched',
+ result=[
+ {
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'write'],
+ 'aciname': delegation1,
+ 'group': u'editors',
+ 'memberof': member1,
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with --pkey-only' % delegation1,
+ command=('delegation_find', [delegation1], {'pkey_only' : True}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 delegation matched',
+ result=[
+ {
+ 'aciname': delegation1,
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with --raw' % delegation1,
+ command=('delegation_find', [delegation1], {'raw' : True}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 delegation matched',
+ result=[
+ {
+ 'aci': u'(targetattr = "street || c || l || st || postalcode")(targetfilter = "(memberOf=%s)")(version 3.0;acl "delegation:testdelegation";allow (write) groupdn = "ldap:///%s";)' % \
+ (DN(('cn', 'admins'), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn),
+ DN(('cn', 'editors'), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)),
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % delegation1,
+ command=(
+ 'delegation_mod', [delegation1], dict(permissions=u'read')
+ ),
+ expected=dict(
+ value=delegation1,
+ summary=u'Modified delegation "%s"' % delegation1,
+ result=dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalcode'],
+ permissions=[u'read'],
+ aciname=delegation1,
+ group=u'editors',
+ memberof=member1,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % delegation1,
+ command=('delegation_show', [delegation1], {}),
+ expected=dict(
+ value=delegation1,
+ summary=None,
+ result={
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'read'],
+ 'aciname': delegation1,
+ 'group': u'editors',
+ 'memberof': member1,
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % delegation1,
+ command=('delegation_del', [delegation1], {}),
+ expected=dict(
+ result=True,
+ value=delegation1,
+ summary=u'Deleted delegation "%s"' % delegation1,
+ )
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py
new file mode 100644
index 000000000..ea9b70e36
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_dns_plugin.py
@@ -0,0 +1,1511 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+"""
+Test the `ipalib/plugins/dns.py` module.
+"""
+
+import nose
+from ipalib import api, errors
+from ipapython.dn import DN
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+
+dnszone1 = u'dnszone.test'
+dnszone1_dn = DN(('idnsname',dnszone1), api.env.container_dns, api.env.basedn)
+dnszone1_mname = u'ns1.%s.' % dnszone1
+dnszone1_mname_dn = DN(('idnsname','ns1'), dnszone1_dn)
+dnszone1_rname = u'root.%s.' % dnszone1
+dnszone1_permission = u'Manage DNS zone %s' % dnszone1
+dnszone1_permission_dn = DN(('cn',dnszone1_permission),
+ api.env.container_permission,api.env.basedn)
+dnszone1_txtrec_dn = DN(('idnsname', '_kerberos'), dnszone1_dn)
+dnszone2 = u'dnszone2.test'
+dnszone2_dn = DN(('idnsname', dnszone2), api.env.container_dns, api.env.basedn)
+dnszone2_mname = u'ns1.%s.' % dnszone2
+dnszone2_rname = u'root.%s.' % dnszone2
+revdnszone1 = u'15.142.80.in-addr.arpa.'
+revdnszone1_ip = u'80.142.15.0/24'
+revdnszone1_dn = DN(('idnsname', revdnszone1), api.env.container_dns, api.env.basedn)
+revdnszone2 = u'16.142.80.in-addr.arpa.'
+revdnszone2_ip = u'80.142.16.0'
+revdnszone2_dn = DN(('idnsname',revdnszone2), api.env.container_dns, api.env.basedn)
+dnsres1 = u'testdnsres'
+dnsres1_dn = DN(('idnsname',dnsres1), dnszone1_dn)
+dnsres1_renamed = u'testdnsres-renamed'
+dnsrev1 = u'80'
+dnsrev1_dn = DN(('idnsname',dnsrev1), revdnszone1_dn)
+dnsrev2 = u'81'
+dnsrev2_dn = DN(('idnsname',dnsrev2), revdnszone1_dn)
+dnsrescname = u'testcnamerec'
+dnsrescname_dn = DN(('idnsname',dnsrescname), dnszone1_dn)
+dnsresdname = u'testdns-dname'
+dnsresdname_dn = DN(('idnsname',dnsresdname), dnszone1_dn)
+
+class test_dns(Declarative):
+
+ @classmethod
+ def setUpClass(cls):
+ super(test_dns, cls).setUpClass()
+
+ if not api.Backend.xmlclient.isconnected():
+ api.Backend.xmlclient.connect(fallback=False)
+ try:
+ api.Command['dnszone_add'](dnszone1,
+ idnssoamname = dnszone1_mname,
+ idnssoarname = dnszone1_rname,
+ force = True,
+ )
+ api.Command['dnszone_del'](dnszone1)
+ except errors.NotFound:
+ raise nose.SkipTest('DNS is not configured')
+ except errors.DuplicateEntry:
+ pass
+
+ cleanup_commands = [
+ ('dnszone_del', [dnszone1, dnszone2, revdnszone1, revdnszone2],
+ {'continue': True}),
+ ('dnsconfig_mod', [], {'idnsforwarders' : None,
+ 'idnsforwardpolicy' : None,
+ 'idnsallowsyncptr' : None,
+ 'idnszonerefresh' : None,
+ }),
+ ('permission_del', [dnszone1_permission], {'force': True}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent zone %r' % dnszone1,
+ command=('dnszone_show', [dnszone1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: DNS zone not found' % dnszone1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent zone %r' % dnszone1,
+ command=('dnszone_mod', [dnszone1], {'idnssoaminimum': 3500}),
+ expected=errors.NotFound(
+ reason=u'%s: DNS zone not found' % dnszone1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent zone %r' % dnszone1,
+ command=('dnszone_del', [dnszone1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: DNS zone not found' % dnszone1),
+ ),
+
+
+ dict(
+ desc='Try to create zone with invalid name',
+ command=(
+ 'dnszone_add', [u'invalid zone'], {
+ 'idnssoamname': dnszone1_mname,
+ 'idnssoarname': dnszone1_rname,
+ 'ip_address' : u'1.2.3.4',
+ }
+ ),
+ expected=errors.ValidationError(name='name',
+ error=u'only letters, numbers, and - are allowed. ' +
+ u'DNS label may not start or end with -'),
+ ),
+
+
+ dict(
+ desc='Create zone %r' % dnszone1,
+ command=(
+ 'dnszone_add', [dnszone1], {
+ 'idnssoamname': dnszone1_mname,
+ 'idnssoarname': dnszone1_rname,
+ 'ip_address' : u'1.2.3.4',
+ }
+ ),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [dnszone1_mname],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; '
+ u'grant %(realm)s krb5-self * AAAA; '
+ u'grant %(realm)s krb5-self * SSHFP;'
+ % dict(realm=api.env.realm)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to create duplicate zone %r' % dnszone1,
+ command=(
+ 'dnszone_add', [dnszone1], {
+ 'idnssoamname': dnszone1_mname,
+ 'idnssoarname': dnszone1_rname,
+ 'ip_address' : u'1.2.3.4',
+ }
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'DNS zone with name "%s" already exists' % dnszone1),
+ ),
+
+ dict(
+ desc='Try to create a zone with nonexistent NS entry',
+ command=(
+ 'dnszone_add', [dnszone2], {
+ 'idnssoamname': dnszone2_mname,
+ 'idnssoarname': dnszone2_rname,
+ }
+ ),
+ expected=errors.NotFound(reason='Nameserver \'%s\' does not have a corresponding A/AAAA record' % (dnszone2_mname)),
+ ),
+
+ dict(
+ desc='Create a zone with nonexistent NS entry with --force',
+ command=(
+ 'dnszone_add', [dnszone2], {
+ 'idnssoamname': dnszone2_mname,
+ 'idnssoarname': dnszone2_rname,
+ 'force' : True,
+ }
+ ),
+ expected={
+ 'value': dnszone2,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone2_dn,
+ 'idnsname': [dnszone2],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [dnszone2_mname],
+ 'nsrecord': [dnszone2_mname],
+ 'idnssoarname': [dnszone2_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; '
+ u'grant %(realm)s krb5-self * AAAA; '
+ u'grant %(realm)s krb5-self * SSHFP;'
+ % dict(realm=api.env.realm)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Retrieve zone %r' % dnszone1,
+ command=('dnszone_show', [dnszone1], {}),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Update zone %r' % dnszone1,
+ command=('dnszone_mod', [dnszone1], {'idnssoarefresh': 5478}),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [u'5478'],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to create reverse zone %r with NS record in it' % revdnszone1,
+ command=(
+ 'dnszone_add', [revdnszone1], {
+ 'idnssoamname': u'ns',
+ 'idnssoarname': dnszone1_rname,
+ }
+ ),
+ expected=errors.ValidationError(name='name-server',
+ error=u"Nameserver for reverse zone cannot be a relative DNS name"),
+ ),
+
+
+ dict(
+ desc='Create reverse zone %r' % revdnszone1,
+ command=(
+ 'dnszone_add', [revdnszone1], {
+ 'idnssoamname': dnszone1_mname,
+ 'idnssoarname': dnszone1_rname,
+ }
+ ),
+ expected={
+ 'value': revdnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': revdnszone1_dn,
+ 'idnsname': [revdnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [dnszone1_mname],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-subdomain %(zone)s PTR;'
+ % dict(realm=api.env.realm, zone=revdnszone1)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Search for zones with name server %r' % (dnszone1_mname),
+ command=('dnszone_find', [], {'idnssoamname': dnszone1_mname}),
+ expected={
+ 'summary': None,
+ 'count': 2,
+ 'truncated': False,
+ 'result': [{
+ 'dn': revdnszone1_dn,
+ 'idnsname': [revdnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ },
+ {
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [u'5478'],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ }],
+ },
+ ),
+
+
+ dict(
+ desc='Search for zones with name server %r with --forward-only' % dnszone1_mname,
+ command=('dnszone_find', [], {'idnssoamname': dnszone1_mname, 'forward_only' : True}),
+ expected={
+ 'summary': None,
+ 'count': 1,
+ 'truncated': False,
+ 'result': [{
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [u'5478'],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ }],
+ },
+ ),
+
+
+ dict(
+ desc='Delete reverse zone %r' % revdnszone1,
+ command=('dnszone_del', [revdnszone1], {}),
+ expected={
+ 'value': revdnszone1,
+ 'summary': u'Deleted DNS zone "%s"' % revdnszone1,
+ 'result': {'failed': u''},
+ },
+ ),
+
+
+ dict(
+ desc='Disable zone %r' % dnszone1,
+ command=('dnszone_disable', [dnszone1], {}),
+ expected={
+ 'value': dnszone1,
+ 'summary': u'Disabled DNS zone "%s"' % dnszone1,
+ 'result': True,
+ },
+ ),
+
+
+ dict(
+ desc='Check if zone %r is really disabled' % dnszone1,
+ command=('dnszone_show', [dnszone1], {}),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'FALSE'],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Enable zone %r' % dnszone1,
+ command=('dnszone_enable', [dnszone1], {}),
+ expected={
+ 'value': dnszone1,
+ 'summary': u'Enabled DNS zone "%s"' % dnszone1,
+ 'result': True,
+ },
+ ),
+
+
+ dict(
+ desc='Check if zone %r is really enabled' % dnszone1,
+ command=('dnszone_show', [dnszone1], {}),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent record %r in zone %r' % (dnsres1, dnszone1),
+ command=('dnsrecord_show', [dnszone1, dnsres1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: DNS resource record not found' % dnsres1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent record %r in zone %r' % (dnsres1, dnszone1),
+ command=('dnsrecord_del', [dnszone1, dnsres1], {'del_all' : True}),
+ expected=errors.NotFound(
+ reason=u'%s: DNS resource record not found' % dnsres1),
+ ),
+
+
+ dict(
+ desc='Try to delete root zone record \'@\' in %r' % (dnszone1),
+ command=('dnsrecord_del', [dnszone1, u'@'], {'del_all' : True}),
+ expected=errors.ValidationError(name='del_all',
+ error=u"Zone record '@' cannot be deleted"),
+ ),
+
+
+ dict(
+ desc='Try to create record with invalid name in zone %r' % dnszone1,
+ command=('dnsrecord_add', [dnszone1, u'invalid record'], {'arecord': u'127.0.0.1'}),
+ expected=errors.ValidationError(name='name',
+ error=u'only letters, numbers, _, and - are allowed. ' +
+ u'DNS label may not start or end with -'),
+ ),
+
+
+ dict(
+ desc='Create record %r in zone %r' % (dnszone1, dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'arecord': u'127.0.0.1'}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'objectclass': objectclasses.dnsrecord,
+ 'arecord': [u'127.0.0.1'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Search for all records in zone %r' % dnszone1,
+ command=('dnsrecord_find', [dnszone1], {}),
+ expected={
+ 'summary': None,
+ 'count': 4,
+ 'truncated': False,
+ 'result': [
+ {
+ 'dn': dnszone1_dn,
+ 'nsrecord': (dnszone1_mname,),
+ 'idnsname': [u'@'],
+ },
+ {
+ 'dn': dnszone1_txtrec_dn,
+ 'txtrecord': [api.env.realm],
+ 'idnsname': [u'_kerberos'],
+ },
+ {
+ 'dn': dnszone1_mname_dn,
+ 'idnsname': [u'ns1'],
+ 'arecord': [u'1.2.3.4'],
+ },
+ {
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'arecord': [u'127.0.0.1'],
+ },
+ ],
+ },
+ ),
+
+
+ dict(
+ desc='Add A record to %r in zone %r' % (dnsres1, dnszone1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'arecord': u'10.10.0.1'}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'arecord': [u'127.0.0.1', u'10.10.0.1'],
+ 'objectclass': objectclasses.dnsrecord,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Remove A record from %r in zone %r' % (dnsres1, dnszone1),
+ command=('dnsrecord_del', [dnszone1, dnsres1], {'arecord': u'127.0.0.1'}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Add AAAA record to %r in zone %r using dnsrecord_mod' % (dnsres1, dnszone1),
+ command=('dnsrecord_mod', [dnszone1, dnsres1], {'aaaarecord': u'::1'}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ 'aaaarecord': [u'::1'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to modify nonexistent record in zone %r' % dnszone1,
+ command=('dnsrecord_mod',
+ [dnszone1, u'ghostname'],
+ {'aaaarecord': u'f001:baad::1'}),
+ expected=errors.NotFound(
+ reason=u'ghostname: DNS resource record not found'),
+ ),
+
+
+ dict(
+ desc='Modify AAAA record in %r in zone %r' % (dnsres1, dnszone1),
+ command=('dnsrecord_mod', [dnszone1, dnsres1], {'aaaarecord': u'ff02::1'}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ 'aaaarecord': [u'ff02::1'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Remove AAAA record from %r in zone %r using dnsrecord_mod' % (dnsres1, dnszone1),
+ command=('dnsrecord_mod', [dnszone1, dnsres1], {'aaaarecord': u''}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add invalid MX record to zone %r using dnsrecord_add' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'@'], {'mxrecord': dnszone1_mname }),
+ expected=errors.ValidationError(name='mx_rec',
+ error=u'format must be specified as "PREFERENCE EXCHANGER" ' +
+ u' (see RFC 1035 for details)'),
+ ),
+
+ dict(
+ desc='Add MX record to zone %r using dnsrecord_add' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'@'], {'mxrecord': u"0 %s" % dnszone1_mname }),
+ expected={
+ 'value': u'@',
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnszone,
+ 'dn': dnszone1_dn,
+ 'idnsname': [u'@'],
+ 'mxrecord': [u"0 %s" % dnszone1_mname],
+ 'nsrecord': [dnszone1_mname],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add invalid SRV record to zone %r using dnsrecord_add' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srvrecord': dnszone1_mname}),
+ expected=errors.ValidationError(name='srv_rec',
+ error=u'format must be specified as "PRIORITY WEIGHT PORT TARGET" ' +
+ u' (see RFC 2782 for details)'),
+ ),
+
+ dict(
+ desc='Try to add invalid SRV record via parts to zone %r using dnsrecord_add' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 0,
+ 'srv_part_weight' : 0,
+ 'srv_part_port' : 123,
+ 'srv_part_target' : u'foo bar'}),
+ expected=errors.ValidationError(name='srv_target',
+ error=u'invalid domain-name: only letters, numbers, _, and - ' +
+ u'are allowed. DNS label may not start or end with -'),
+ ),
+
+ dict(
+ desc='Try to add SRV record to zone %r both via parts and a raw value' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 0,
+ 'srv_part_weight' : 0,
+ 'srv_part_port' : 123,
+ 'srv_part_target' : u'foo.bar.',
+ 'srvrecord': [u"1 100 1234 %s" \
+ % dnszone1_mname]}),
+ expected=errors.ValidationError(name='srv_target',
+ error=u'Raw value of a DNS record was already set by ' +
+ u'"srv_rec" option'),
+ ),
+
+ dict(
+ desc='Add SRV record to zone %r using dnsrecord_add' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'_foo._tcp'], {'srvrecord': u"0 100 1234 %s" % dnszone1_mname}),
+ expected={
+ 'value': u'_foo._tcp',
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': DN(('idnsname', u'_foo._tcp'), dnszone1_dn),
+ 'idnsname': [u'_foo._tcp'],
+ 'srvrecord': [u"0 100 1234 %s" % dnszone1_mname],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to modify SRV record in zone %r without specifying modified value' % (dnszone1),
+ command=('dnsrecord_mod', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 1,}),
+ expected=errors.RequirementError(name='srvrecord'),
+ ),
+
+ dict(
+ desc='Try to modify SRV record in zone %r with non-existent modified value' % (dnszone1),
+ command=('dnsrecord_mod', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 1,
+ 'srvrecord' : [u"0 100 1234 does.not.exist."] }),
+ expected=errors.AttrValueNotFound(attr='SRV record',
+ value=u'0 100 1234 does.not.exist.'),
+ ),
+
+ dict(
+ desc='Try to modify SRV record in zone %r with invalid part value' % (dnszone1),
+ command=('dnsrecord_mod', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 100000,
+ 'srvrecord' : [u"0 100 1234 %s" % dnszone1_mname] }),
+ expected=errors.ValidationError(name='srv_priority', error=u'can be at most 65535'),
+ ),
+
+ dict(
+ desc='Modify SRV record in zone %r using parts' % (dnszone1),
+ command=('dnsrecord_mod', [dnszone1, u'_foo._tcp'], {'srv_part_priority': 1,
+ 'srvrecord' : [u"0 100 1234 %s" % dnszone1_mname] }),
+ expected={
+ 'value': u'_foo._tcp',
+ 'summary': None,
+ 'result': {
+ 'idnsname': [u'_foo._tcp'],
+ 'srvrecord': [u"1 100 1234 %s" % dnszone1_mname],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add invalid LOC record to zone %r using dnsrecord_add' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'@'], {'locrecord': u"91 11 42.4 N 16 36 29.6 E 227.64" }),
+ expected=errors.ValidationError(name='lat_deg',
+ error=u'can be at most 90'),
+ ),
+
+ dict(
+ desc='Add LOC record to zone %r using dnsrecord_add' % (dnszone1),
+ command=('dnsrecord_add', [dnszone1, u'@'], {'locrecord': u"49 11 42.4 N 16 36 29.6 E 227.64" }),
+ expected={
+ 'value': u'@',
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnszone,
+ 'dn': dnszone1_dn,
+ 'idnsname': [u'@'],
+ 'mxrecord': [u"0 %s" % dnszone1_mname],
+ 'nsrecord': [dnszone1_mname],
+ 'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add CNAME record to %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'cnamerecord': u'foo-1.example.com.'}),
+ expected=errors.ValidationError(name='cnamerecord',
+ error=u'CNAME record is not allowed to coexist with any other '
+ u'record (RFC 1034, section 3.6.2)'),
+ ),
+
+ dict(
+ desc='Try to add invalid CNAME record %r using dnsrecord_add' % (dnsrescname),
+ command=('dnsrecord_add', [dnszone1, dnsrescname], {'cnamerecord': u'-.example.com'}),
+ expected=errors.ValidationError(name='hostname',
+ error=u'invalid domain-name: only letters, numbers, _, and - ' +
+ u'are allowed. DNS label may not start or end with -'),
+ ),
+
+ dict(
+ desc='Try to add multiple CNAME record %r using dnsrecord_add' % (dnsrescname),
+ command=('dnsrecord_add', [dnszone1, dnsrescname], {'cnamerecord':
+ [u'1.example.com.', u'2.example.com.']}),
+ expected=errors.ValidationError(name='cnamerecord',
+ error=u'only one CNAME record is allowed per name (RFC 2136, section 1.1.5)'),
+ ),
+
+ dict(
+ desc='Add CNAME record to %r using dnsrecord_add' % (dnsrescname),
+ command=('dnsrecord_add', [dnszone1, dnsrescname], {'cnamerecord': u'foo-1.example.com.'}),
+ expected={
+ 'value': dnsrescname,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': dnsrescname_dn,
+ 'idnsname': [dnsrescname],
+ 'cnamerecord': [u'foo-1.example.com.'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add other record to CNAME record %r using dnsrecord_add' % (dnsrescname),
+ command=('dnsrecord_add', [dnszone1, dnsrescname], {'arecord': u'10.0.0.1'}),
+ expected=errors.ValidationError(name='cnamerecord',
+ error=u'CNAME record is not allowed to coexist with any other '
+ u'record (RFC 1034, section 3.6.2)'),
+ ),
+
+ dict(
+ desc='Try to add other record to CNAME record %r using dnsrecord_mod' % (dnsrescname),
+ command=('dnsrecord_mod', [dnszone1, dnsrescname], {'arecord': u'10.0.0.1'}),
+ expected=errors.ValidationError(name='cnamerecord',
+ error=u'CNAME record is not allowed to coexist with any other '
+ u'record (RFC 1034, section 3.6.2)'),
+ ),
+
+ dict(
+ desc='Add A record and delete CNAME record in %r with dnsrecord_mod' % (dnsrescname),
+ command=('dnsrecord_mod', [dnszone1, dnsrescname], {'arecord': u'10.0.0.1',
+ 'cnamerecord': None}),
+ expected={
+ 'value': dnsrescname,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnsrescname],
+ 'arecord': [u'10.0.0.1'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add multiple DNAME records to %r using dnsrecord_add' % (dnsresdname),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'dnamerecord':
+ [u'foo-1.example.com.', u'foo-2.example.com.']}),
+ expected=errors.ValidationError(name='dnamerecord',
+ error=u'only one DNAME record is allowed per name (RFC 6672, section 2.4)'),
+ ),
+
+ dict(
+ desc='Try to add invalid DNAME record %r using dnsrecord_add' % (dnsresdname),
+ command=('dnsrecord_add', [dnszone1, dnsresdname], {'dnamerecord': u'-.example.com.'}),
+ expected=errors.ValidationError(name='target',
+ error=u'invalid domain-name: only letters, numbers, _, and - ' +
+ u'are allowed. DNS label may not start or end with -'),
+ ),
+
+ dict(
+ desc='Add DNAME record to %r using dnsrecord_add' % (dnsresdname),
+ command=('dnsrecord_add', [dnszone1, dnsresdname],
+ {'dnamerecord': u'd.example.com.', 'arecord': u'10.0.0.1'}),
+ expected={
+ 'value': dnsresdname,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': dnsresdname_dn,
+ 'idnsname': [dnsresdname],
+ 'dnamerecord': [u'd.example.com.'],
+ 'arecord': [u'10.0.0.1'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add CNAME record to %r using dnsrecord_add' % (dnsresdname),
+ command=('dnsrecord_add', [dnszone1, dnsresdname], {'cnamerecord': u'foo-1.example.com.'}),
+ expected=errors.ValidationError(name='cnamerecord',
+ error=u'CNAME record is not allowed to coexist with any other '
+ u'record (RFC 1034, section 3.6.2)'),
+ ),
+
+ dict(
+ desc='Try to add NS record to %r using dnsrecord_add' % (dnsresdname),
+ command=('dnsrecord_add', [dnszone1, dnsresdname],
+ {'nsrecord': u'%s.%s.' % (dnsres1, dnszone1)}),
+ expected=errors.ValidationError(name='dnamerecord',
+ error=u'DNAME record is not allowed to coexist with an NS '
+ u'record except when located in a zone root record (RFC 6672, section 2.3)'),
+ ),
+
+ dict(
+ desc='Add NS+DNAME record to %r zone record using dnsrecord_add' % (dnszone2),
+ command=('dnsrecord_add', [dnszone2, u'@'],
+ {'dnamerecord': u'd.example.com.',
+ 'nsrecord': dnszone1_mname}),
+ expected = {
+ 'value': u'@',
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnszone,
+ 'dnamerecord': [u'd.example.com.'],
+ 'dn': dnszone2_dn,
+ 'nsrecord': [dnszone2_mname, dnszone1_mname],
+ 'idnsname': [u'@']
+ }
+ },
+ ),
+
+
+ dict(
+ desc='Delete zone %r' % dnszone2,
+ command=('dnszone_del', [dnszone2], {}),
+ expected={
+ 'value': dnszone2,
+ 'summary': u'Deleted DNS zone "%s"' % dnszone2,
+ 'result': {'failed': u''},
+ },
+ ),
+
+ dict(
+ desc='Try to add invalid KX record %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'kxrecord': u'foo-1.example.com' }),
+ expected=errors.ValidationError(name='kx_rec',
+ error=u'format must be specified as "PREFERENCE EXCHANGER" ' +
+ u' (see RFC 2230 for details)'),
+ ),
+
+ dict(
+ desc='Add KX record to %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'kxrecord': u'1 foo-1' }),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ 'kxrecord': [u'1 foo-1'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Add TXT record to %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'txtrecord': u'foo bar' }),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ 'kxrecord': [u'1 foo-1'],
+ 'txtrecord': [u'foo bar'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Add NSEC record to %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {
+ 'nsec_part_next': dnszone1,
+ 'nsec_part_types' : [u'TXT', u'A']}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ 'kxrecord': [u'1 foo-1'],
+ 'txtrecord': [u'foo bar'],
+ 'nsecrecord': [dnszone1 + u' TXT A'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to add unresolvable absolute NS record to %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'nsrecord': u'does.not.exist.'}),
+ expected=errors.NotFound(reason=u"Nameserver 'does.not.exist.' does not have a corresponding A/AAAA record"),
+ ),
+
+ dict(
+ desc='Try to add unresolvable relative NS record to %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'nsrecord': u'does.not.exist'}),
+ expected=errors.NotFound(reason=u"Nameserver 'does.not.exist.%s.' does not have a corresponding A/AAAA record" % dnszone1),
+ ),
+
+ dict(
+ desc='Add unresolvable NS record with --force to %r using dnsrecord_add' % (dnsres1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'nsrecord': u'does.not.exist.',
+ 'force' : True}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'arecord': [u'10.10.0.1'],
+ 'kxrecord': [u'1 foo-1'],
+ 'txtrecord': [u'foo bar'],
+ 'nsecrecord': [dnszone1 + u' TXT A'],
+ 'nsrecord': [u'does.not.exist.'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Try to to rename DNS zone %r root record' % (dnszone1),
+ command=('dnsrecord_mod', [dnszone1, u'@'], {'rename': dnsres1_renamed,}),
+ expected=errors.ValidationError(name='rename',
+ error=u'DNS zone root record cannot be renamed')
+ ),
+
+ dict(
+ desc='Rename DNS record %r to %r' % (dnsres1, dnsres1_renamed),
+ command=('dnsrecord_mod', [dnszone1, dnsres1], {'rename': dnsres1_renamed,}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnsres1_renamed],
+ 'arecord': [u'10.10.0.1'],
+ 'kxrecord': [u'1 foo-1'],
+ 'txtrecord': [u'foo bar'],
+ 'nsecrecord': [dnszone1 + u' TXT A'],
+ 'nsrecord': [u'does.not.exist.'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Delete record %r in zone %r' % (dnsres1_renamed, dnszone1),
+ command=('dnsrecord_del', [dnszone1, dnsres1_renamed], {'del_all': True }),
+ expected={
+ 'value': dnsres1_renamed,
+ 'summary': u'Deleted record "%s"' % dnsres1_renamed,
+ 'result': {'failed': u''},
+ },
+ ),
+
+
+ dict(
+ desc='Try to create a reverse zone from invalid IP',
+ command=(
+ 'dnszone_add', [], {
+ 'name_from_ip': u'foo',
+ 'idnssoamname': dnszone1_mname,
+ 'idnssoarname': dnszone1_rname,
+ }
+ ),
+ expected=errors.ValidationError(name='name_from_ip',
+ error=u'invalid IP network format'),
+ ),
+
+ dict(
+ desc='Create reverse zone from IP/netmask %r using name_from_ip option' % revdnszone1_ip,
+ command=(
+ 'dnszone_add', [], {
+ 'name_from_ip': revdnszone1_ip,
+ 'idnssoamname': dnszone1_mname,
+ 'idnssoarname': dnszone1_rname,
+ }
+ ),
+ expected={
+ 'value': revdnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': revdnszone1_dn,
+ 'idnsname': [revdnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [dnszone1_mname],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-subdomain %(zone)s PTR;'
+ % dict(realm=api.env.realm, zone=revdnszone1)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Create reverse zone from IP %r using name_from_ip option' % revdnszone2_ip,
+ command=(
+ 'dnszone_add', [], {
+ 'name_from_ip': revdnszone2_ip,
+ 'idnssoamname': dnszone1_mname,
+ 'idnssoarname': dnszone1_rname,
+ }
+ ),
+ expected={
+ 'value': revdnszone2,
+ 'summary': None,
+ 'result': {
+ 'dn': revdnszone2_dn,
+ 'idnsname': [revdnszone2],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [dnszone1_mname],
+ 'nsrecord': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-subdomain %(zone)s PTR;'
+ % dict(realm=api.env.realm, zone=revdnszone2)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to add invalid PTR %r to %r using dnsrecord_add' % (dnsrev1, revdnszone1),
+ command=('dnsrecord_add', [revdnszone1, dnsrev1], {'ptrrecord': u'-.example.com' }),
+ expected=errors.ValidationError(name='hostname',
+ error=u'invalid domain-name: only letters, numbers, and - ' +
+ u'are allowed. DNS label may not start or end with -'),
+ ),
+
+ dict(
+ desc='Add PTR record %r to %r using dnsrecord_add' % (dnsrev1, revdnszone1),
+ command=('dnsrecord_add', [revdnszone1, dnsrev1], {'ptrrecord': u'foo-1.example.com' }),
+ expected={
+ 'value': dnsrev1,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': dnsrev1_dn,
+ 'idnsname': [dnsrev1],
+ 'ptrrecord': [u'foo-1.example.com.'],
+ },
+ },
+ ),
+
+ dict(
+ desc='Show record %r in zone %r with --structured and --all options'\
+ % (dnsrev1, revdnszone1),
+ command=('dnsrecord_show', [revdnszone1, dnsrev1],
+ {'structured': True, 'all': True}),
+ expected={
+ 'value': dnsrev1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnsrev1_dn,
+ 'idnsname': [dnsrev1],
+ 'objectclass': objectclasses.dnsrecord,
+ 'dnsrecords': [
+ {
+ 'dnstype': u'PTR',
+ 'dnsdata': u'foo-1.example.com.',
+ 'ptr_part_hostname': u'foo-1.example.com.'
+ },
+ ],
+ },
+ },
+ ),
+
+ dict(
+ desc='Update global DNS settings',
+ command=('dnsconfig_mod', [], {'idnsforwarders' : [u'80.142.15.80'],}),
+ expected={
+ 'value': u'',
+ 'summary': None,
+ 'result': {
+ 'idnsforwarders': [u'80.142.15.80'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to add invalid allow-query to zone %r' % dnszone1,
+ command=('dnszone_mod', [dnszone1], {'idnsallowquery': u'foo'}),
+ expected=errors.ValidationError(name='allow_query',
+ error=u"failed to detect a valid IP address from 'foo'"),
+ ),
+
+ dict(
+ desc='Add allow-query ACL to zone %r' % dnszone1,
+ command=('dnszone_mod', [dnszone1], {'idnsallowquery': u'!10/8;any'}),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'mxrecord': [u'0 ns1.dnszone.test.'],
+ 'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [u'5478'],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowquery': [u'!10.0.0.0/8;any;'],
+ 'idnsallowtransfer': [u'none;'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to add invalid allow-transfer to zone %r' % dnszone1,
+ command=('dnszone_mod', [dnszone1], {'idnsallowtransfer': u'10.'}),
+ expected=errors.ValidationError(name='allow_transfer',
+ error=u"failed to detect a valid IP address from '10.'"),
+ ),
+
+ dict(
+ desc='Add allow-transer ACL to zone %r' % dnszone1,
+ command=('dnszone_mod', [dnszone1], {'idnsallowtransfer': u'80.142.15.80'}),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'mxrecord': [u'0 ns1.dnszone.test.'],
+ 'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [u'5478'],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowquery': [u'!10.0.0.0/8;any;'],
+ 'idnsallowtransfer': [u'80.142.15.80;'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Set SOA serial of zone %r to high number' % dnszone1,
+ command=('dnszone_mod', [dnszone1], {'idnssoaserial': 4294967295}),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'nsrecord': [dnszone1_mname],
+ 'mxrecord': [u'0 ns1.dnszone.test.'],
+ 'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"],
+ 'idnssoamname': [dnszone1_mname],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [u'4294967295'],
+ 'idnssoarefresh': [u'5478'],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowquery': [u'!10.0.0.0/8;any;'],
+ 'idnsallowtransfer': [u'80.142.15.80;'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to create duplicate PTR record for %r with --a-create-reverse' % dnsres1,
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'arecord': u'80.142.15.80',
+ 'a_extra_create_reverse' : True}),
+ expected=errors.DuplicateEntry(message=u'Reverse record for IP ' +
+ u'address 80.142.15.80 already exists in reverse zone ' +
+ u'15.142.80.in-addr.arpa..'),
+ ),
+
+
+ dict(
+ desc='Create A record %r in zone %r with --a-create-reverse' % (dnsres1, dnszone1),
+ command=('dnsrecord_add', [dnszone1, dnsres1], {'arecord': u'80.142.15.81',
+ 'a_extra_create_reverse' : True}),
+ expected={
+ 'value': dnsres1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnsres1_dn,
+ 'idnsname': [dnsres1],
+ 'objectclass': objectclasses.dnsrecord,
+ 'arecord': [u'80.142.15.81'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Check reverse record for %r created via --a-create-reverse' % dnsres1,
+ command=('dnsrecord_show', [revdnszone1, dnsrev2], {}),
+ expected={
+ 'value': dnsrev2,
+ 'summary': None,
+ 'result': {
+ 'dn': dnsrev2_dn,
+ 'idnsname': [dnsrev2],
+ 'ptrrecord': [dnsres1 + '.' + dnszone1 + '.'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Try to add per-zone permission for unknown zone',
+ command=('dnszone_add_permission', [u'does.not.exist'], {}),
+ expected=errors.NotFound(reason=u'does.not.exist: DNS zone not found')
+ ),
+
+
+ dict(
+ desc='Add per-zone permission for zone %r' % dnszone1,
+ command=(
+ 'dnszone_add_permission', [dnszone1], {}
+ ),
+ expected=dict(
+ result=True,
+ value=dnszone1_permission,
+ summary=u'Added system permission "%s"' % dnszone1_permission,
+ ),
+ ),
+
+
+ dict(
+ desc='Try to add duplicate per-zone permission for zone %r' % dnszone1,
+ command=(
+ 'dnszone_add_permission', [dnszone1], {}
+ ),
+ expected=errors.DuplicateEntry(message=u'permission with name '
+ '"%s" already exists' % dnszone1_permission)
+ ),
+
+
+ dict(
+ desc='Make sure the permission was created %r' % dnszone1,
+ command=(
+ 'permission_show', [dnszone1_permission], {}
+ ),
+ expected=dict(
+ value=dnszone1_permission,
+ summary=None,
+ result={
+ 'dn': dnszone1_permission_dn,
+ 'cn': [dnszone1_permission],
+ 'ipapermissiontype': [u'SYSTEM'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Try to remove per-zone permission for unknown zone',
+ command=('dnszone_remove_permission', [u'does.not.exist'], {}),
+ expected=errors.NotFound(reason=u'does.not.exist: DNS zone not found')
+ ),
+
+
+ dict(
+ desc='Remove per-zone permission for zone %r' % dnszone1,
+ command=(
+ 'dnszone_remove_permission', [dnszone1], {}
+ ),
+ expected=dict(
+ result=True,
+ value=dnszone1_permission,
+ summary=u'Removed system permission "%s"' % dnszone1_permission,
+ ),
+ ),
+
+
+ dict(
+ desc='Make sure the permission for zone %r was deleted' % dnszone1,
+ command=(
+ 'permission_show', [dnszone1_permission], {}
+ ),
+ expected=errors.NotFound(reason=u'%s: permission not found'
+ % dnszone1_permission)
+ ),
+
+
+ dict(
+ desc='Delete zone %r' % dnszone1,
+ command=('dnszone_del', [dnszone1], {}),
+ expected={
+ 'value': dnszone1,
+ 'summary': u'Deleted DNS zone "%s"' % dnszone1,
+ 'result': {'failed': u''},
+ },
+ ),
+
+
+ dict(
+ desc='Try to create zone %r nameserver not in it' % dnszone1,
+ command=(
+ 'dnszone_add', [dnszone1], {
+ 'idnssoamname': u'not.in.this.zone.',
+ 'idnssoarname': dnszone1_rname,
+ 'ip_address' : u'1.2.3.4',
+ }
+ ),
+ expected=errors.ValidationError(name='ip_address',
+ error=u"Nameserver DNS record is created only for nameservers"
+ u" in current zone"),
+ ),
+
+
+ dict(
+ desc='Create zone %r with relative nameserver' % dnszone1,
+ command=(
+ 'dnszone_add', [dnszone1], {
+ 'idnssoamname': u'ns',
+ 'idnssoarname': dnszone1_rname,
+ 'ip_address' : u'1.2.3.4',
+ }
+ ),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [u'ns'],
+ 'nsrecord': [u'ns'],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; '
+ u'grant %(realm)s krb5-self * AAAA; '
+ u'grant %(realm)s krb5-self * SSHFP;'
+ % dict(realm=api.env.realm)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Delete zone %r' % dnszone1,
+ command=('dnszone_del', [dnszone1], {}),
+ expected={
+ 'value': dnszone1,
+ 'summary': u'Deleted DNS zone "%s"' % dnszone1,
+ 'result': {'failed': u''},
+ },
+ ),
+
+
+ dict(
+ desc='Create zone %r with nameserver in the zone itself' % dnszone1,
+ command=(
+ 'dnszone_add', [dnszone1], {
+ 'idnssoamname': dnszone1 + u'.',
+ 'idnssoarname': dnszone1_rname,
+ 'ip_address' : u'1.2.3.4',
+ }
+ ),
+ expected={
+ 'value': dnszone1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone1_dn,
+ 'idnsname': [dnszone1],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [dnszone1 + u'.'],
+ 'nsrecord': [dnszone1 + u'.'],
+ 'idnssoarname': [dnszone1_rname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; '
+ u'grant %(realm)s krb5-self * AAAA; '
+ u'grant %(realm)s krb5-self * SSHFP;'
+ % dict(realm=api.env.realm)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+ },
+ },
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_dns_realmdomains_integration.py b/ipatests/test_xmlrpc/test_dns_realmdomains_integration.py
new file mode 100644
index 000000000..1e46d362e
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_dns_realmdomains_integration.py
@@ -0,0 +1,168 @@
+# Authors:
+# Ana Krivokapic <akrivoka@redhat.com>
+#
+# Copyright (C) 2013 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/>.
+"""
+Test integration of DNS and realmdomains.
+1. dnszone_{add,del} should create/delete appropriate entry in realmdomains.
+2. realmdomains_mod should add a _kerberos TXT record in the DNS zone.
+"""
+
+from ipalib import api, errors
+from ipapython.dn import DN
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits
+
+
+cn = u'Realm Domains'
+dn = DN(('cn', cn), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
+our_domain = api.env.domain
+dnszone_1 = u'dnszone.test'
+dnszone_1_dn = DN(('idnsname', dnszone_1), api.env.container_dns,
+ api.env.basedn)
+idnssoamname = u'ns1.%s.' % dnszone_1
+idnssoarname = u'root.%s.' % dnszone_1
+dnszone_2 = u'dnszone2.test'
+dnszone_2_dn = DN(('idnsname', dnszone_2), api.env.container_dns,
+ api.env.basedn)
+
+
+def assert_realmdomain_and_txt_record_present(response):
+ zone = response['value']
+
+ r = api.Command['realmdomains_show']()
+ assert zone in r['result']['associateddomain']
+
+ r = api.Command['dnsrecord_show'](zone, u'_kerberos')
+ assert api.env.realm in r['result']['txtrecord']
+
+ return True
+
+
+def assert_realmdomain_and_txt_record_not_present(response):
+ zone = response['value']
+
+ r = api.Command['realmdomains_show']()
+ assert zone not in r['result']['associateddomain']
+
+ try:
+ api.Command['dnsrecord_show'](zone, u'_kerberos')
+ except errors.NotFound:
+ return True
+
+
+class test_dns_realmdomains_integration(Declarative):
+ cleanup_commands = [
+ ('realmdomains_mod', [], {'associateddomain': [our_domain]}),
+ ('dnszone_del', [dnszone_1, dnszone_2], {'continue': True}),
+ ]
+
+ tests = [
+ dict(
+ desc='Check realmdomain and TXT record get created '
+ 'during dnszone_add',
+ command=(
+ 'dnszone_add', [dnszone_1], {
+ 'idnssoamname': idnssoamname,
+ 'idnssoarname': idnssoarname,
+ 'ip_address': u'1.2.3.4',
+ }
+ ),
+ expected={
+ 'value': dnszone_1,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone_1_dn,
+ 'idnsname': [dnszone_1],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [idnssoamname],
+ 'nsrecord': [idnssoamname],
+ 'idnssoarname': [idnssoarname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; '
+ u'grant %(realm)s krb5-self * AAAA; '
+ u'grant %(realm)s krb5-self * SSHFP;'
+ % dict(realm=api.env.realm)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+
+ },
+ },
+ extra_check=assert_realmdomain_and_txt_record_present,
+ ),
+
+ dict(
+ desc='Check realmdomain and TXT record do not get created '
+ 'during dnszone_add for forwarded zone',
+ command=(
+ 'dnszone_add', [dnszone_2], {
+ 'idnssoamname': idnssoamname,
+ 'idnssoarname': idnssoarname,
+ 'idnsforwarders': u'1.2.3.4',
+ 'idnsforwardpolicy': u'only',
+ 'force': True,
+ }
+ ),
+ expected={
+ 'value': dnszone_2,
+ 'summary': None,
+ 'result': {
+ 'dn': dnszone_2_dn,
+ 'idnsname': [dnszone_2],
+ 'idnszoneactive': [u'TRUE'],
+ 'idnssoamname': [idnssoamname],
+ 'idnsforwarders': [u'1.2.3.4'],
+ 'idnsforwardpolicy': [u'only'],
+ 'nsrecord': [idnssoamname],
+ 'idnssoarname': [idnssoarname],
+ 'idnssoaserial': [fuzzy_digits],
+ 'idnssoarefresh': [fuzzy_digits],
+ 'idnssoaretry': [fuzzy_digits],
+ 'idnssoaexpire': [fuzzy_digits],
+ 'idnssoaminimum': [fuzzy_digits],
+ 'idnsallowdynupdate': [u'FALSE'],
+ 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; '
+ u'grant %(realm)s krb5-self * AAAA; '
+ u'grant %(realm)s krb5-self * SSHFP;'
+ % dict(realm=api.env.realm)],
+ 'idnsallowtransfer': [u'none;'],
+ 'idnsallowquery': [u'any;'],
+ 'objectclass': objectclasses.dnszone,
+
+ },
+ },
+ extra_check=assert_realmdomain_and_txt_record_not_present,
+ ),
+
+ dict(
+ desc='Check realmdomain and TXT record get deleted '
+ 'during dnszone_del',
+ command=('dnszone_del', [dnszone_1], {}),
+ expected={
+ 'value': dnszone_1,
+ 'summary': u'Deleted DNS zone "%s"' % dnszone_1,
+ 'result': {'failed': u''},
+ },
+ extra_check=assert_realmdomain_and_txt_record_not_present,
+ ),
+ ]
diff --git a/ipatests/test_xmlrpc/test_external_members.py b/ipatests/test_xmlrpc/test_external_members.py
new file mode 100644
index 000000000..112470dcb
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_external_members.py
@@ -0,0 +1,160 @@
+# Authors:
+# Ana Krivokapic <akrivoka@redhat.com>
+#
+# Copyright (C) 2013 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/>.
+"""
+Test adding/removing external members (trusted domain objects) to IPA groups.
+These tests are skipped if trust is not established.
+"""
+
+import nose
+from ipalib import api
+from ipapython.dn import DN
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_uuid, fuzzy_user_or_group_sid
+
+group_name = u'external_group'
+group_desc = u'Test external group'
+group_dn = DN(('cn', group_name), api.env.container_group, api.env.basedn)
+
+
+def get_trusted_group_name():
+ trusts = api.Command['trust_find']()
+ if trusts['count'] == 0:
+ return None
+
+ ad_netbios = trusts['result'][0]['ipantflatname']
+ return u'%s\Domain Admins' % ad_netbios
+
+
+class test_external_members(Declarative):
+ @classmethod
+ def setUpClass(cls):
+ super(test_external_members, cls).setUpClass()
+ if not api.Backend.xmlclient.isconnected():
+ api.Backend.xmlclient.connect(fallback=False)
+
+ trusts = api.Command['trust_find']()
+ if trusts['count'] == 0:
+ raise nose.SkipTest('Trust is not established')
+
+ cleanup_commands = [
+ ('group_del', [group_name], {}),
+ ]
+
+ tests = [
+ dict(
+ desc='Create external group "%s"' % group_name,
+ command=(
+ 'group_add', [group_name], dict(description=group_desc, external=True)
+ ),
+ expected=dict(
+ value=group_name,
+ summary=u'Added group "%s"' % group_name,
+ result=dict(
+ cn=[group_name],
+ description=[group_desc],
+ objectclass=objectclasses.externalgroup,
+ ipauniqueid=[fuzzy_uuid],
+ dn=group_dn,
+ ),
+ ),
+ ),
+ dict(
+ desc='Add external member "%s" to group "%s"' % (get_trusted_group_name(), group_name),
+ command=(
+ 'group_add_member', [group_name], dict(ipaexternalmember=get_trusted_group_name())
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result=dict(
+ dn=group_dn,
+ ipaexternalmember=[fuzzy_user_or_group_sid],
+ cn=[group_name],
+ description=[group_desc],
+ ),
+ ),
+ ),
+ dict(
+ desc='Try to add duplicate external member "%s" to group "%s"' % (get_trusted_group_name(), group_name),
+ command=(
+ 'group_add_member', [group_name], dict(ipaexternalmember=get_trusted_group_name())
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ group=[(fuzzy_user_or_group_sid, u'This entry is already a member')],
+ user=tuple(),
+ ),
+ ),
+ result=dict(
+ dn=group_dn,
+ ipaexternalmember=[fuzzy_user_or_group_sid],
+ cn=[group_name],
+ description=[group_desc],
+ ),
+ ),
+ ),
+ dict(
+ desc='Remove external member "%s" from group "%s"' % (get_trusted_group_name(), group_name),
+ command=(
+ 'group_remove_member', [group_name], dict(ipaexternalmember=get_trusted_group_name())
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result=dict(
+ dn=group_dn,
+ cn=[group_name],
+ ipaexternalmember=[],
+ description=[group_desc],
+ ),
+ ),
+ ),
+ dict(
+ desc='Try to remove external entry "%s" which is not a member of group "%s" from group "%s"' % (get_trusted_group_name(), group_name, group_name),
+ command=(
+ 'group_remove_member', [group_name], dict(ipaexternalmember=get_trusted_group_name())
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ group=[(fuzzy_user_or_group_sid, u'This entry is not a member')],
+ user=tuple(),
+ ),
+ ),
+ result=dict(
+ dn=group_dn,
+ cn=[group_name],
+ description=[group_desc],
+ ),
+ ),
+ ),
+ ]
diff --git a/ipatests/test_xmlrpc/test_group_plugin.py b/ipatests/test_xmlrpc/test_group_plugin.py
new file mode 100644
index 000000000..1d0cfeb16
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_group_plugin.py
@@ -0,0 +1,1046 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@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/>.
+"""
+Test the `ipalib/plugins/group.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from ipatests.util import Fuzzy
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid, fuzzy_set_ci
+from ipapython.dn import DN
+
+group1 = u'testgroup1'
+group2 = u'testgroup2'
+group3 = u'testgroup3'
+renamedgroup1 = u'testgroup'
+user1 = u'tuser1'
+
+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)
+
+class test_group(Declarative):
+ cleanup_commands = [
+ ('group_del', [group1], {}),
+ ('group_del', [group2], {}),
+ ('group_del', [group3], {}),
+ ('group_del', [renamedgroup1], {}),
+ ('user_del', [user1], {}),
+ ]
+
+ tests = [
+
+ ################
+ # create group1:
+ dict(
+ desc='Try to retrieve non-existent %r' % group1,
+ command=('group_show', [group1], {}),
+ expected=errors.NotFound(reason=u'%s: group not found' % group1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % group1,
+ command=('group_mod', [group1], dict(description=u'Foo')),
+ expected=errors.NotFound(reason=u'%s: group not found' % group1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % group1,
+ command=('group_del', [group1], {}),
+ expected=errors.NotFound(reason=u'%s: group not found' % group1),
+ ),
+
+
+ dict(
+ desc='Try to rename non-existent %r' % group1,
+ command=('group_mod', [group1], dict(setattr=u'cn=%s' % renamedgroup1)),
+ expected=errors.NotFound(reason=u'%s: group not found' % group1),
+ ),
+
+
+ dict(
+ desc='Create non-POSIX %r' % group1,
+ command=(
+ 'group_add', [group1], dict(description=u'Test desc 1',nonposix=True)
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added group "testgroup1"',
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc 1'],
+ objectclass=objectclasses.group,
+ ipauniqueid=[fuzzy_uuid],
+ dn=get_group_dn('testgroup1'),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % group1,
+ command=(
+ 'group_add', [group1], dict(description=u'Test desc 1')
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'group with name "%s" already exists' % group1),
+ ),
+
+
+ dict(
+ desc='Retrieve non-POSIX %r' % group1,
+ command=('group_show', [group1], {}),
+ expected=dict(
+ value=group1,
+ summary=None,
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc 1'],
+ dn=get_group_dn('testgroup1'),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Updated non-POSIX %r' % group1,
+ command=(
+ 'group_mod', [group1], dict(description=u'New desc 1')
+ ),
+ expected=dict(
+ result=dict(
+ cn=[group1],
+ description=[u'New desc 1'],
+ ),
+ summary=u'Modified group "testgroup1"',
+ value=group1,
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % group1,
+ command=('group_show', [group1], {}),
+ expected=dict(
+ value=group1,
+ result=dict(
+ cn=[group1],
+ description=[u'New desc 1'],
+ dn=get_group_dn('testgroup1'),
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ # FIXME: The return value is totally different here than from the above
+ # group_mod() test. I think that for all *_mod() commands we should
+ # just return the entry exactly as *_show() does.
+ dict(
+ desc='Updated %r to promote it to a POSIX group' % group1,
+ command=('group_mod', [group1], dict(posix=True)),
+ expected=dict(
+ result=dict(
+ cn=[group1],
+ description=[u'New desc 1'],
+ gidnumber=[fuzzy_digits],
+ ),
+ value=group1,
+ summary=u'Modified group "testgroup1"',
+ ),
+ ),
+
+
+ dict(
+ desc="Retrieve %r to verify it's a POSIX group" % group1,
+ command=('group_show', [group1], {}),
+ expected=dict(
+ value=group1,
+ result=dict(
+ cn=[group1],
+ description=(u'New desc 1',),
+ dn=get_group_dn('testgroup1'),
+ gidnumber=[fuzzy_digits],
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % group1,
+ command=('group_find', [], dict(cn=group1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ result=[
+ dict(
+ dn=get_group_dn(group1),
+ cn=[group1],
+ description=[u'New desc 1'],
+ gidnumber=[fuzzy_digits],
+ ),
+ ],
+ summary=u'1 group matched',
+ ),
+ ),
+
+
+
+ ################
+ # create group2:
+ dict(
+ desc='Try to retrieve non-existent %r' % group2,
+ command=('group_show', [group2], {}),
+ expected=errors.NotFound(reason=u'%s: group not found' % group2),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % group2,
+ command=('group_mod', [group2], dict(description=u'Foo')),
+ expected=errors.NotFound(reason=u'%s: group not found' % group2),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % group2,
+ command=('group_del', [group2], {}),
+ expected=errors.NotFound(reason=u'%s: group not found' % group2),
+ ),
+
+
+ dict(
+ desc='Create %r' % group2,
+ command=(
+ 'group_add', [group2], dict(description=u'Test desc 2')
+ ),
+ expected=dict(
+ value=group2,
+ summary=u'Added group "testgroup2"',
+ result=dict(
+ cn=[group2],
+ description=[u'Test desc 2'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.posixgroup,
+ ipauniqueid=[fuzzy_uuid],
+ dn=get_group_dn('testgroup2'),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % group2,
+ command=(
+ 'group_add', [group2], dict(description=u'Test desc 2')
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'group with name "%s" already exists' % group2),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % group2,
+ command=('group_show', [group2], {}),
+ expected=dict(
+ value=group2,
+ summary=None,
+ result=dict(
+ cn=[group2],
+ description=[u'Test desc 2'],
+ gidnumber=[fuzzy_digits],
+ dn=get_group_dn('testgroup2'),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Updated %r' % group2,
+ command=(
+ 'group_mod', [group2], dict(description=u'New desc 2')
+ ),
+ expected=dict(
+ result=dict(
+ cn=[group2],
+ gidnumber=[fuzzy_digits],
+ description=[u'New desc 2'],
+ ),
+ summary=u'Modified group "testgroup2"',
+ value=group2,
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % group2,
+ command=('group_show', [group2], {}),
+ expected=dict(
+ value=group2,
+ result=dict(
+ cn=[group2],
+ description=[u'New desc 2'],
+ gidnumber=[fuzzy_digits],
+ dn=get_group_dn('testgroup2'),
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % group2,
+ command=('group_find', [], dict(cn=group2)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ result=[
+ dict(
+ dn=get_group_dn('testgroup2'),
+ cn=[group2],
+ description=[u'New desc 2'],
+ gidnumber=[fuzzy_digits],
+ ),
+ ],
+ summary=u'1 group matched',
+ ),
+ ),
+
+
+ dict(
+ desc='Search for all groups',
+ command=('group_find', [], {}),
+ expected=dict(
+ summary=u'6 groups matched',
+ count=6,
+ truncated=False,
+ result=[
+ {
+ 'dn': get_group_dn('admins'),
+ 'member_user': [u'admin'],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [u'admins'],
+ 'description': [u'Account administrators group'],
+ },
+ {
+ 'dn': get_group_dn('editors'),
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [u'editors'],
+ 'description': [u'Limited admins who can edit other users'],
+ },
+ {
+ 'dn': get_group_dn('ipausers'),
+ 'cn': [u'ipausers'],
+ 'description': [u'Default group for all users'],
+ },
+ dict(
+ dn=get_group_dn(group1),
+ cn=[group1],
+ description=[u'New desc 1'],
+ gidnumber=[fuzzy_digits],
+ ),
+ dict(
+ dn=get_group_dn(group2),
+ cn=[group2],
+ description=[u'New desc 2'],
+ gidnumber=[fuzzy_digits],
+ ),
+ {
+ 'dn': get_group_dn('trust admins'),
+ 'member_user': [u'admin'],
+ 'cn': [u'trust admins'],
+ 'description': [u'Trusts administrators group'],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Search for non-POSIX groups',
+ command=('group_find', [], dict(nonposix=True, all=True)),
+ expected=dict(
+ summary=u'2 groups matched',
+ count=2,
+ truncated=False,
+ result=[
+ {
+ 'dn': get_group_dn('ipausers'),
+ 'cn': [u'ipausers'],
+ 'description': [u'Default group for all users'],
+ 'objectclass': fuzzy_set_ci(objectclasses.group),
+ 'ipauniqueid': [fuzzy_uuid],
+ },
+ {
+ 'dn': get_group_dn('trust admins'),
+ 'member_user': [u'admin'],
+ 'cn': [u'trust admins'],
+ 'description': [u'Trusts administrators group'],
+ 'objectclass': fuzzy_set_ci(objectclasses.group),
+ 'ipauniqueid': [fuzzy_uuid],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Search for non-POSIX groups with criteria filter',
+ command=('group_find', [u'users'], dict(nonposix=True, all=True)),
+ expected=dict(
+ summary=u'1 group matched',
+ count=1,
+ truncated=False,
+ result=[
+ {
+ 'dn': get_group_dn('ipausers'),
+ 'cn': [u'ipausers'],
+ 'description': [u'Default group for all users'],
+ 'objectclass': fuzzy_set_ci(objectclasses.group),
+ 'ipauniqueid': [fuzzy_uuid],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Search for POSIX groups',
+ command=('group_find', [], dict(posix=True, all=True)),
+ expected=dict(
+ summary=u'4 groups matched',
+ count=4,
+ truncated=False,
+ result=[
+ {
+ 'dn': get_group_dn('admins'),
+ 'member_user': [u'admin'],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [u'admins'],
+ 'description': [u'Account administrators group'],
+ 'objectclass': fuzzy_set_ci(objectclasses.posixgroup),
+ 'ipauniqueid': [fuzzy_uuid],
+ },
+ {
+ 'dn': get_group_dn('editors'),
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [u'editors'],
+ 'description': [u'Limited admins who can edit other users'],
+ 'objectclass': fuzzy_set_ci(objectclasses.posixgroup),
+ 'ipauniqueid': [fuzzy_uuid],
+ },
+ 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],
+ ),
+ dict(
+ dn=get_group_dn(group2),
+ cn=[group2],
+ description=[u'New desc 2'],
+ gidnumber=[fuzzy_digits],
+ objectclass=fuzzy_set_ci(objectclasses.posixgroup),
+ 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,
+ ),
+
+
+ dict(
+ desc='Remove group %r with external membership' % (group3),
+ command=('group_del', [group3], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=group3,
+ summary=u'Deleted group "testgroup3"',
+ ),
+ ),
+
+
+ ###############
+ # member stuff:
+ dict(
+ desc='Add member %r to %r' % (group2, group1),
+ command=(
+ 'group_add_member', [group1], dict(group=group2)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': get_group_dn(group1),
+ 'member_group': (group2,),
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ 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'],
+ },
+ ),
+ ),
+
+ 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],
+ '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],
+ '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=u''),
+ 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=u''),
+ 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=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/%s' % user1],
+ krbprincipalname=[u'%s@%s' % (user1, api.env.realm)],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ dn=DN(('uid',user1),('cn','users'),('cn','accounts'),
+ api.env.basedn),
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ 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=u''),
+ 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=u''),
+ 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=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ description=[],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user_base,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[u'1000'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('uid','tuser1'),('cn','users'),('cn','accounts'),
+ api.env.basedn),
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+ 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'],
+ },
+ ),
+ ),
+
+ 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=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_hbac_plugin.py b/ipatests/test_xmlrpc/test_hbac_plugin.py
new file mode 100644
index 000000000..c0f8b5307
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_hbac_plugin.py
@@ -0,0 +1,497 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+"""
+Test the `ipalib/plugins/hbacrule.py` module.
+"""
+
+from nose.tools import raises, assert_raises # pylint: disable=E0611
+
+from xmlrpc_test import XMLRPC_test, assert_attr_equal
+from ipalib import api
+from ipalib import errors
+
+class test_hbac(XMLRPC_test):
+ """
+ Test the `hbacrule` plugin.
+ """
+ rule_name = u'testing_rule1234'
+ rule_type = u'allow'
+ rule_type_fail = u'value not allowed'
+ rule_service = u'ssh'
+ rule_time = u'absolute 20081010000000 ~ 20081015120000'
+ rule_time2 = u'absolute 20081010000000 ~ 20081016120000'
+ # wrong time, has 30th day in February in first date
+ rule_time_fail = u'absolute 20080230000000 ~ 20081015120000'
+ rule_desc = u'description'
+ rule_desc_mod = u'description modified'
+
+ test_user = u'hbacrule_test_user'
+ test_group = u'hbacrule_test_group'
+ test_host = u'hbacrule.testnetgroup'
+ test_hostgroup = u'hbacrule_test_hostgroup'
+ test_service = u'sshd'
+ test_host_external = u'notfound.example.com'
+
+ test_invalid_sourcehost = u'inv+alid#srchost.nonexist.com'
+
+ def test_0_hbacrule_add(self):
+ """
+ Test adding a new HBAC rule using `xmlrpc.hbacrule_add`.
+ """
+ ret = self.failsafe_add(api.Object.hbacrule,
+ self.rule_name,
+ accessruletype=self.rule_type,
+ description=self.rule_desc,
+ )
+ entry = ret['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert_attr_equal(entry, 'accessruletype', self.rule_type)
+ assert_attr_equal(entry, 'ipaenabledflag', 'TRUE')
+ assert_attr_equal(entry, 'description', self.rule_desc)
+
+ @raises(errors.DuplicateEntry)
+ def test_1_hbacrule_add(self):
+ """
+ Test adding an existing HBAC rule using `xmlrpc.hbacrule_add'.
+ """
+ api.Command['hbacrule_add'](
+ self.rule_name, accessruletype=self.rule_type
+ )
+
+ def test_2_hbacrule_show(self):
+ """
+ Test displaying a HBAC rule using `xmlrpc.hbacrule_show`.
+ """
+ entry = api.Command['hbacrule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert_attr_equal(entry, 'ipaenabledflag', 'TRUE')
+ assert_attr_equal(entry, 'description', self.rule_desc)
+
+ def test_3_hbacrule_mod(self):
+ """
+ Test modifying a HBAC rule using `xmlrpc.hbacrule_mod`.
+ """
+ ret = api.Command['hbacrule_mod'](
+ self.rule_name, description=self.rule_desc_mod
+ )
+ entry = ret['result']
+ assert_attr_equal(entry, 'description', self.rule_desc_mod)
+
+# def test_4_hbacrule_add_accesstime(self):
+# """
+# Test adding access time to HBAC rule using `xmlrpc.hbacrule_add_accesstime`.
+# """
+# return
+# ret = api.Command['hbacrule_add_accesstime'](
+# self.rule_name, accesstime=self.rule_time2
+# )
+# entry = ret['result']
+# assert_attr_equal(entry, 'accesstime', self.rule_time);
+# assert_attr_equal(entry, 'accesstime', self.rule_time2);
+
+# def test_5_hbacrule_add_accesstime(self):
+# """
+# Test adding invalid access time to HBAC rule using `xmlrpc.hbacrule_add_accesstime`.
+# """
+# try:
+# api.Command['hbacrule_add_accesstime'](
+# self.rule_name, accesstime=self.rule_time_fail
+# )
+# except errors.ValidationError:
+# pass
+# else:
+# assert False
+
+ def test_6_hbacrule_find(self):
+ """
+ Test searching for HBAC rules using `xmlrpc.hbacrule_find`.
+ """
+ ret = api.Command['hbacrule_find'](
+ cn=self.rule_name, accessruletype=self.rule_type,
+ description=self.rule_desc_mod
+ )
+ assert ret['truncated'] is False
+ entries = ret['result']
+ assert_attr_equal(entries[0], 'cn', self.rule_name)
+ assert_attr_equal(entries[0], 'accessruletype', self.rule_type)
+ assert_attr_equal(entries[0], 'description', self.rule_desc_mod)
+
+ def test_7_hbacrule_init_testing_data(self):
+ """
+ Initialize data for more HBAC plugin testing.
+ """
+ self.failsafe_add(api.Object.user,
+ self.test_user, givenname=u'first', sn=u'last'
+ )
+ self.failsafe_add(api.Object.group,
+ self.test_group, description=u'description'
+ )
+ self.failsafe_add(api.Object.host,
+ self.test_host, force=True
+ )
+ self.failsafe_add(api.Object.hostgroup,
+ self.test_hostgroup, description=u'description'
+ )
+ self.failsafe_add(api.Object.hbacsvc,
+ self.test_service, description=u'desc',
+ )
+
+ def test_8_hbacrule_add_user(self):
+ """
+ Test adding user and group to HBAC rule using `xmlrpc.hbacrule_add_user`.
+ """
+ ret = api.Command['hbacrule_add_user'](
+ self.rule_name, user=self.test_user, group=self.test_group
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberuser' in failed
+ assert 'user' in failed['memberuser']
+ assert not failed['memberuser']['user']
+ assert 'group' in failed['memberuser']
+ assert not failed['memberuser']['group']
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberuser_user', self.test_user)
+ assert_attr_equal(entry, 'memberuser_group', self.test_group)
+
+ def test_9_a_show_user(self):
+ """
+ Test showing a user to verify HBAC rule membership
+ `xmlrpc.user_show`.
+ """
+ ret = api.Command['user_show'](self.test_user, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_hbacrule', self.rule_name)
+
+ def test_9_b_show_group(self):
+ """
+ Test showing a group to verify HBAC rule membership
+ `xmlrpc.group_show`.
+ """
+ ret = api.Command['group_show'](self.test_group, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_hbacrule', self.rule_name)
+
+ def test_9_hbacrule_remove_user(self):
+ """
+ Test removing user and group from HBAC rule using `xmlrpc.hbacrule_remove_user'.
+ """
+ ret = api.Command['hbacrule_remove_user'](
+ self.rule_name, user=self.test_user, group=self.test_group
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberuser' in failed
+ assert 'user' in failed['memberuser']
+ assert not failed['memberuser']['user']
+ assert 'group' in failed['memberuser']
+ assert not failed['memberuser']['group']
+ entry = ret['result']
+ assert 'memberuser_user' not in entry
+ assert 'memberuser_group' not in entry
+
+ def test_a_hbacrule_add_host(self):
+ """
+ Test adding host and hostgroup to HBAC rule using `xmlrpc.hbacrule_add_host`.
+ """
+ ret = api.Command['hbacrule_add_host'](
+ self.rule_name, host=self.test_host, hostgroup=self.test_hostgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberhost' in failed
+ assert 'host' in failed['memberhost']
+ assert not failed['memberhost']['host']
+ assert 'hostgroup' in failed['memberhost']
+ assert not failed['memberhost']['hostgroup']
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberhost_host', self.test_host)
+ assert_attr_equal(entry, 'memberhost_hostgroup', self.test_hostgroup)
+
+ def test_a_hbacrule_show_host(self):
+ """
+ Test showing host to verify HBAC rule membership
+ `xmlrpc.host_show`.
+ """
+ ret = api.Command['host_show'](self.test_host, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_hbacrule', self.rule_name)
+
+ def test_a_hbacrule_show_hostgroup(self):
+ """
+ Test showing hostgroup to verify HBAC rule membership
+ `xmlrpc.hostgroup_show`.
+ """
+ ret = api.Command['hostgroup_show'](self.test_hostgroup, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_hbacrule', self.rule_name)
+
+ def test_b_hbacrule_remove_host(self):
+ """
+ Test removing host and hostgroup from HBAC rule using `xmlrpc.hbacrule_remove_host`.
+ """
+ ret = api.Command['hbacrule_remove_host'](
+ self.rule_name, host=self.test_host, hostgroup=self.test_hostgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberhost' in failed
+ assert 'host' in failed['memberhost']
+ assert not failed['memberhost']['host']
+ assert 'hostgroup' in failed['memberhost']
+ assert not failed['memberhost']['hostgroup']
+ entry = ret['result']
+ assert 'memberhost_host' not in entry
+ assert 'memberhost_hostgroup' not in entry
+
+ @raises(errors.DeprecationError)
+ def test_a_hbacrule_add_sourcehost_deprecated(self):
+ """
+ Test deprecated command hbacrule_add_sourcehost.
+ """
+ ret = api.Command['hbacrule_add_sourcehost'](
+ self.rule_name, host=self.test_host, hostgroup=self.test_hostgroup
+ )
+
+ def test_a_hbacrule_add_service(self):
+ """
+ Test adding service to HBAC rule using `xmlrpc.hbacrule_add_service`.
+ """
+ ret = api.Command['hbacrule_add_service'](
+ self.rule_name, hbacsvc=self.test_service
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ assert 'memberservice' in failed
+ assert 'hbacsvc' in failed['memberservice']
+ assert not failed['memberservice']['hbacsvc']
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberservice_hbacsvc', self.test_service)
+
+ def test_a_hbacrule_remove_service(self):
+ """
+ Test removing service to HBAC rule using `xmlrpc.hbacrule_remove_service`.
+ """
+ ret = api.Command['hbacrule_remove_service'](
+ self.rule_name, hbacsvc=self.test_service
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ assert 'memberservice' in failed
+ assert 'hbacsvc' in failed['memberservice']
+ assert not failed['memberservice']['hbacsvc']
+ entry = ret['result']
+ assert 'memberservice service' not in entry
+
+ @raises(errors.DeprecationError)
+ def test_b_hbacrule_remove_sourcehost_deprecated(self):
+ """
+ Test deprecated command hbacrule_remove_sourcehost.
+ """
+ ret = api.Command['hbacrule_remove_sourcehost'](
+ self.rule_name, host=self.test_host, hostgroup=self.test_hostgroup
+ )
+
+ @raises(errors.ValidationError)
+ def test_c_hbacrule_mod_invalid_external_setattr(self):
+ """
+ Test adding the same external host using `xmlrpc.hbacrule_add_host`.
+ """
+ ret = api.Command['hbacrule_mod'](
+ self.rule_name, setattr=self.test_invalid_sourcehost
+ )
+
+ def test_d_hbacrule_disable(self):
+ """
+ Test disabling HBAC rule using `xmlrpc.hbacrule_disable`.
+ """
+ assert api.Command['hbacrule_disable'](self.rule_name)['result'] is True
+ entry = api.Command['hbacrule_show'](self.rule_name)['result']
+ # FIXME: Should this be 'disabled' or 'FALSE'?
+ assert_attr_equal(entry, 'ipaenabledflag', 'FALSE')
+
+ def test_e_hbacrule_enabled(self):
+ """
+ Test enabling HBAC rule using `xmlrpc.hbacrule_enable`.
+ """
+ assert api.Command['hbacrule_enable'](self.rule_name)['result'] is True
+ # check it's really enabled
+ entry = api.Command['hbacrule_show'](self.rule_name)['result']
+ # FIXME: Should this be 'enabled' or 'TRUE'?
+ assert_attr_equal(entry, 'ipaenabledflag', 'TRUE')
+
+ def test_ea_hbacrule_disable_setattr(self):
+ """
+ Test disabling HBAC rule using setattr
+ """
+ command_result = api.Command['hbacrule_mod'](
+ self.rule_name, setattr=u'ipaenabledflag=false')
+ assert command_result['result']['ipaenabledflag'] == (u'FALSE',)
+ entry = api.Command['hbacrule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'ipaenabledflag', 'FALSE')
+
+ def test_eb_hbacrule_enable_setattr(self):
+ """
+ Test enabling HBAC rule using setattr
+ """
+ command_result = api.Command['hbacrule_mod'](
+ self.rule_name, setattr=u'ipaenabledflag=1')
+ assert command_result['result']['ipaenabledflag'] == (u'TRUE',)
+ # check it's really enabled
+ entry = api.Command['hbacrule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'ipaenabledflag', 'TRUE')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_f_hbacrule_exclusiveuser(self):
+ """
+ Test adding a user to an HBAC rule when usercat='all'
+ """
+ api.Command['hbacrule_mod'](self.rule_name, usercategory=u'all')
+ try:
+ api.Command['hbacrule_add_user'](self.rule_name, user=u'admin')
+ finally:
+ api.Command['hbacrule_mod'](self.rule_name, usercategory=u'')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_g_hbacrule_exclusiveuser(self):
+ """
+ Test setting usercat='all' in an HBAC rule when there are users
+ """
+ api.Command['hbacrule_add_user'](self.rule_name, user=u'admin')
+ try:
+ api.Command['hbacrule_mod'](self.rule_name, usercategory=u'all')
+ finally:
+ api.Command['hbacrule_remove_user'](self.rule_name, user=u'admin')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_h_hbacrule_exclusivehost(self):
+ """
+ Test adding a host to an HBAC rule when hostcat='all'
+ """
+ api.Command['hbacrule_mod'](self.rule_name, hostcategory=u'all')
+ try:
+ api.Command['hbacrule_add_host'](self.rule_name, host=self.test_host)
+ finally:
+ api.Command['hbacrule_mod'](self.rule_name, hostcategory=u'')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_i_hbacrule_exclusivehost(self):
+ """
+ Test setting hostcat='all' in an HBAC rule when there are hosts
+ """
+ api.Command['hbacrule_add_host'](self.rule_name, host=self.test_host)
+ try:
+ api.Command['hbacrule_mod'](self.rule_name, hostcategory=u'all')
+ finally:
+ api.Command['hbacrule_remove_host'](self.rule_name, host=self.test_host)
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_j_hbacrule_exclusiveservice(self):
+ """
+ Test adding a service to an HBAC rule when servicecat='all'
+ """
+ api.Command['hbacrule_mod'](self.rule_name, servicecategory=u'all')
+ try:
+ api.Command['hbacrule_add_service'](self.rule_name, hbacsvc=self.test_service)
+ finally:
+ api.Command['hbacrule_mod'](self.rule_name, servicecategory=u'')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_k_hbacrule_exclusiveservice(self):
+ """
+ Test setting servicecat='all' in an HBAC rule when there are services
+ """
+ api.Command['hbacrule_add_service'](self.rule_name, hbacsvc=self.test_service)
+ try:
+ api.Command['hbacrule_mod'](self.rule_name, servicecategory=u'all')
+ finally:
+ api.Command['hbacrule_remove_service'](self.rule_name, hbacsvc=self.test_service)
+
+ @raises(errors.ValidationError)
+ def test_l_hbacrule_add(self):
+ """
+ Test adding a new HBAC rule with a deny type.
+ """
+ api.Command['hbacrule_add'](
+ u'denyrule',
+ accessruletype=u'deny',
+ description=self.rule_desc,
+ )
+
+ @raises(errors.ValidationError)
+ def test_m_hbacrule_add(self):
+ """
+ Test changing an HBAC rule to the deny type
+ """
+ api.Command['hbacrule_mod'](
+ self.rule_name,
+ accessruletype=u'deny',
+ )
+
+ def test_n_hbacrule_links(self):
+ """
+ Test adding various links to HBAC rule
+ """
+ api.Command['hbacrule_add_service'](
+ self.rule_name, hbacsvc=self.test_service
+ )
+
+ entry = api.Command['hbacrule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert_attr_equal(entry, 'memberservice_hbacsvc', self.test_service)
+
+ def test_y_hbacrule_zap_testing_data(self):
+ """
+ Clear data for HBAC plugin testing.
+ """
+ api.Command['hbacrule_remove_host'](self.rule_name, host=self.test_host)
+ api.Command['hbacrule_remove_host'](self.rule_name, hostgroup=self.test_hostgroup)
+ api.Command['user_del'](self.test_user)
+ api.Command['group_del'](self.test_group)
+ api.Command['host_del'](self.test_host)
+ api.Command['hostgroup_del'](self.test_hostgroup)
+ api.Command['hbacsvc_del'](self.test_service)
+
+ def test_k_2_sudorule_referential_integrity(self):
+ """
+ Test that links in HBAC rule were removed by referential integrity plugin
+ """
+ entry = api.Command['hbacrule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert 'sourcehost_host' not in entry
+ assert 'sourcehost_hostgroup' not in entry
+ assert 'memberservice_hbacsvc' not in entry
+
+ def test_z_hbacrule_del(self):
+ """
+ Test deleting a HBAC rule using `xmlrpc.hbacrule_del`.
+ """
+ api.Command['hbacrule_del'](self.rule_name)
+ # verify that it's gone
+ with assert_raises(errors.NotFound):
+ api.Command['hbacrule_show'](self.rule_name)
+
+ @raises(errors.ValidationError)
+ def test_zz_hbacrule_add_with_deprecated_option(self):
+ """
+ Test using a deprecated command option 'sourcehostcategory' with 'hbacrule_add'.
+ """
+ api.Command['hbacrule_add'](
+ self.rule_name, sourcehostcategory=u'all'
+ )
diff --git a/ipatests/test_xmlrpc/test_hbacsvcgroup_plugin.py b/ipatests/test_xmlrpc/test_hbacsvcgroup_plugin.py
new file mode 100644
index 000000000..8140741d9
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_hbacsvcgroup_plugin.py
@@ -0,0 +1,256 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib.plugins.hbacsvcgroup` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid
+from ipatests.test_xmlrpc import objectclasses
+from ipapython.dn import DN
+
+hbacsvcgroup1 = u'testhbacsvcgroup1'
+dn1 = DN(('cn',hbacsvcgroup1),('cn','hbacservicegroups'),('cn','hbac'),
+ api.env.basedn)
+
+hbacsvc1 = u'sshd'
+hbacsvc_dn1 = DN(('cn',hbacsvc1),('cn','hbacservices'),('cn','hbac'),
+ api.env.basedn)
+
+
+class test_hbacsvcgroup(Declarative):
+
+ cleanup_commands = [
+ ('hbacsvcgroup_del', [hbacsvcgroup1], {}),
+ ('hbacsvc_del', [hbacsvc1], {}),
+ ]
+
+ tests=[
+
+ dict(
+ desc='Try to retrieve non-existent %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_show', [hbacsvcgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: HBAC service group not found' % hbacsvcgroup1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_mod', [hbacsvcgroup1],
+ dict(description=u'Updated hbacsvcgroup 1')
+ ),
+ expected=errors.NotFound(
+ reason=u'%s: HBAC service group not found' % hbacsvcgroup1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_del', [hbacsvcgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: HBAC service group not found' % hbacsvcgroup1),
+ ),
+
+
+ dict(
+ desc='Create %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_add', [hbacsvcgroup1],
+ dict(description=u'Test hbacsvcgroup 1')
+ ),
+ expected=dict(
+ value=hbacsvcgroup1,
+ summary=u'Added HBAC service group "testhbacsvcgroup1"',
+ result=dict(
+ dn=dn1,
+ cn=[hbacsvcgroup1],
+ objectclass=objectclasses.hbacsvcgroup,
+ description=[u'Test hbacsvcgroup 1'],
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_add', [hbacsvcgroup1],
+ dict(description=u'Test hbacsvcgroup 1')
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'HBAC service group with name "%s" already exists' %
+ hbacsvcgroup1),
+ ),
+
+
+ dict(
+ desc='Create service %r' % hbacsvc1,
+ command=('hbacsvc_add', [hbacsvc1],
+ dict(
+ description=u'Test service 1',
+ ),
+ ),
+ expected=dict(
+ value=hbacsvc1,
+ summary=u'Added HBAC service "%s"' % hbacsvc1,
+ result=dict(
+ dn=hbacsvc_dn1,
+ cn=[hbacsvc1],
+ description=[u'Test service 1'],
+ objectclass=objectclasses.hbacsvc,
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc=u'Add service %r to %r' % (hbacsvc1, hbacsvcgroup1),
+ command=(
+ 'hbacsvcgroup_add_member', [hbacsvcgroup1], dict(hbacsvc=hbacsvc1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ hbacsvc=tuple(),
+ ),
+ ),
+ result={
+ 'dn': dn1,
+ 'cn': [hbacsvcgroup1],
+ 'description': [u'Test hbacsvcgroup 1'],
+ 'member_hbacsvc': [hbacsvc1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_show', [hbacsvcgroup1], {}),
+ expected=dict(
+ value=hbacsvcgroup1,
+ summary=None,
+ result={
+ 'dn': dn1,
+ 'member_hbacsvc': [hbacsvc1],
+ 'cn': [hbacsvcgroup1],
+ 'description': [u'Test hbacsvcgroup 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_find', [], dict(cn=hbacsvcgroup1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 HBAC service group matched',
+ result=[
+ {
+ 'dn': dn1,
+ 'member_hbacsvc': [hbacsvc1],
+ 'cn': [hbacsvcgroup1],
+ 'description': [u'Test hbacsvcgroup 1'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_mod', [hbacsvcgroup1],
+ dict(description=u'Updated hbacsvcgroup 1')
+ ),
+ expected=dict(
+ value=hbacsvcgroup1,
+ summary=u'Modified HBAC service group "testhbacsvcgroup1"',
+ result=dict(
+ cn=[hbacsvcgroup1],
+ description=[u'Updated hbacsvcgroup 1'],
+ member_hbacsvc=[hbacsvc1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % hbacsvcgroup1,
+ command=('hbacsvcgroup_show', [hbacsvcgroup1], {}),
+ expected=dict(
+ value=hbacsvcgroup1,
+ summary=None,
+ result={
+ 'dn': dn1,
+ 'member_hbacsvc': [hbacsvc1],
+ 'cn': [hbacsvcgroup1],
+ 'description': [u'Updated hbacsvcgroup 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove service %r from %r' % (hbacsvc1, hbacsvcgroup1),
+ command=('hbacsvcgroup_remove_member', [hbacsvcgroup1],
+ dict(hbacsvc=hbacsvc1)
+ ),
+ expected=dict(
+ failed=dict(
+ member=dict(
+ hbacsvc=tuple(),
+ ),
+ ),
+ completed=1,
+ result={
+ 'dn': dn1,
+ 'cn': [hbacsvcgroup1],
+ 'description': [u'Updated hbacsvcgroup 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % hbacsvcgroup1,
+ command=('hbacsvcgroup_del', [hbacsvcgroup1], {}),
+ expected=dict(
+ value=hbacsvcgroup1,
+ summary=u'Deleted HBAC service group "testhbacsvcgroup1"',
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete service %r' % hbacsvc1,
+ command=('hbacsvc_del', [hbacsvc1], {}),
+ expected=dict(
+ value=hbacsvc1,
+ summary=u'Deleted HBAC service "%s"' % hbacsvc1,
+ result=dict(failed=u''),
+ ),
+ )
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_hbactest_plugin.py b/ipatests/test_xmlrpc/test_hbactest_plugin.py
new file mode 100644
index 000000000..520f20247
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_hbactest_plugin.py
@@ -0,0 +1,217 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+# Alexander Bokovoy <abokovoy@redhat.com>
+#
+# Copyright (C) 2009-2011 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/>.
+"""
+Test the `ipalib/plugins/hbactest.py` module.
+"""
+
+from xmlrpc_test import XMLRPC_test, assert_attr_equal
+from ipalib import api
+from ipalib import errors
+from types import NoneType
+from nose.tools import raises
+
+# Test strategy:
+# 1. Create few allow rules: with user categories, with explicit users, with user groups, with groups, with services
+# 2. Create users for test
+# 3. Run detailed and non-detailed tests for explicitly specified rules, check expected result
+#
+class test_hbactest(XMLRPC_test):
+ """
+ Test the `hbactest` plugin.
+ """
+ rule_names = [u'testing_rule1234_%d' % (d) for d in [1,2,3,4]]
+ rule_type = u'allow'
+ rule_service = u'ssh'
+ rule_descs = [u'description %d' % (d) for d in [1,2,3,4]]
+
+ test_user = u'hbacrule_test_user'
+ test_group = u'hbacrule_test_group'
+ test_host = u'hbacrule.testhost'
+ test_hostgroup = u'hbacrule_test_hostgroup'
+ test_sourcehost = u'hbacrule.testsrchost'
+ test_sourcehostgroup = u'hbacrule_test_src_hostgroup'
+ test_service = u'ssh'
+
+ # Auxiliary funcion for checking existence of warning for specified rule
+ def check_rule_presence(self,rule_name,warnings):
+ for warning in warnings:
+ if rule_name in warning:
+ return True
+ return False
+
+ def test_0_hbactest_addrules(self):
+ """
+ Prepare data by adding test HBAC rules using `xmlrpc.hbacrule_add'.
+ """
+
+ self.failsafe_add(api.Object.user,
+ self.test_user, givenname=u'first', sn=u'last'
+ )
+ self.failsafe_add(api.Object.group,
+ self.test_group, description=u'description'
+ )
+ self.failsafe_add(api.Object.host,
+ self.test_host, force=True
+ )
+ self.failsafe_add(api.Object.hostgroup,
+ self.test_hostgroup, description=u'description'
+ )
+ self.failsafe_add(api.Object.host,
+ self.test_sourcehost, force=True
+ )
+ self.failsafe_add(api.Object.hostgroup,
+ self.test_sourcehostgroup, description=u'desc'
+ )
+ self.failsafe_add(api.Object.hbacsvc,
+ self.test_service, description=u'desc'
+ )
+
+ for i in [0,1,2,3]:
+ api.Command['hbacrule_add'](
+ self.rule_names[i], accessruletype=self.rule_type, description=self.rule_descs[i],
+ )
+
+ ret = api.Command['hbacrule_add_user'](
+ self.rule_names[i], user=self.test_user, group=self.test_group
+ )
+
+ ret = api.Command['hbacrule_add_host'](
+ self.rule_names[i], host=self.test_host, hostgroup=self.test_hostgroup
+ )
+
+ ret = api.Command['hbacrule_add_service'](
+ self.rule_names[i], hbacsvc=self.test_service
+ )
+
+ if i & 1:
+ ret = api.Command['hbacrule_disable'](self.rule_names[i])
+
+ def test_a_hbactest_check_rules_detail(self):
+ """
+ Test 'ipa hbactest --rules' (explicit IPA rules, detailed output)
+ """
+ ret = api.Command['hbactest'](
+ user=self.test_user,
+ targethost=self.test_host,
+ service=self.test_service,
+ rules=self.rule_names
+ )
+ assert ret['value'] == True
+ assert type(ret['error']) == NoneType
+ for i in [0,1,2,3]:
+ assert self.rule_names[i] in ret['matched']
+
+ def test_b_hbactest_check_rules_nodetail(self):
+ """
+ Test 'ipa hbactest --rules --nodetail' (explicit IPA rules, no detailed output)
+ """
+ ret = api.Command['hbactest'](
+ user=self.test_user,
+ targethost=self.test_host,
+ service=self.test_service,
+ rules=self.rule_names,
+ nodetail=True
+ )
+ assert ret['value'] == True
+ assert ret['error'] == None
+ assert ret['matched'] == None
+ assert ret['notmatched'] == None
+
+ def test_c_hbactest_check_rules_enabled_detail(self):
+ """
+ Test 'ipa hbactest --enabled' (all enabled IPA rules, detailed output)
+ """
+ ret = api.Command['hbactest'](
+ user=self.test_user,
+ targethost=self.test_host,
+ service=self.test_service,
+ enabled=True
+ )
+ # --enabled will try to work with _all_ enabled rules in IPA database
+ # It means we could have matched something else (unlikely but possible)
+ # Thus, check that our two enabled rules are in matched, nothing more
+ for i in [0,2]:
+ assert self.rule_names[i] in ret['matched']
+
+ def test_d_hbactest_check_rules_disabled_detail(self):
+ """
+ Test 'ipa hbactest --disabled' (all disabled IPA rules, detailed output)
+ """
+ ret = api.Command['hbactest'](
+ user=self.test_user,
+ targethost=self.test_host,
+ service=self.test_service,
+ disabled=True
+ )
+ # --disabled will try to work with _all_ disabled rules in IPA database
+ # It means we could have matched something else (unlikely but possible)
+ # Thus, check that our two disabled rules are in matched, nothing more
+ for i in [1,3]:
+ assert self.rule_names[i] in ret['matched']
+
+ def test_e_hbactest_check_non_existing_rule_detail(self):
+ """
+ Test running 'ipa hbactest' with non-existing rule in --rules
+ """
+ ret = api.Command['hbactest'](
+ user=self.test_user,
+ targethost=self.test_host,
+ service=self.test_service,
+ rules=[u'%s_1x1' % (rule) for rule in self.rule_names],
+ nodetail=True
+ )
+
+ assert ret['value'] == False
+ assert ret['matched'] == None
+ assert ret['notmatched'] == None
+ for rule in self.rule_names:
+ assert u'%s_1x1' % (rule) in ret['error']
+
+ @raises(errors.ValidationError)
+ def test_f_hbactest_check_sourcehost_option_is_deprecated(self):
+ """
+ Test running 'ipa hbactest' with --srchost option raises ValidationError
+ """
+ api.Command['hbactest'](
+ user=self.test_user,
+ targethost=self.test_host,
+ sourcehost=self.test_sourcehost,
+ service=self.test_service,
+ rules=[u'%s_1x1' % rule for rule in self.rule_names],
+ nodetail=True
+ )
+
+ def test_g_hbactest_clear_testing_data(self):
+ """
+ Clear data for HBAC test plugin testing.
+ """
+ for i in [0,1,2,3]:
+ api.Command['hbacrule_remove_host'](self.rule_names[i], host=self.test_host)
+ api.Command['hbacrule_remove_host'](self.rule_names[i], hostgroup=self.test_hostgroup)
+ api.Command['hbacrule_del'](self.rule_names[i])
+
+ api.Command['user_del'](self.test_user)
+ api.Command['group_del'](self.test_group)
+ api.Command['host_del'](self.test_host)
+ api.Command['hostgroup_del'](self.test_hostgroup)
+ api.Command['host_del'](self.test_sourcehost)
+ api.Command['hostgroup_del'](self.test_sourcehostgroup)
+ api.Command['hbacsvc_del'](self.test_service)
+
diff --git a/ipatests/test_xmlrpc/test_host_plugin.py b/ipatests/test_xmlrpc/test_host_plugin.py
new file mode 100644
index 000000000..a23a34112
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_host_plugin.py
@@ -0,0 +1,939 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2008, 2009 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/>.
+
+"""
+Test the `ipalib.plugins.host` module.
+"""
+
+import os
+import tempfile
+from ipapython import ipautil
+from ipalib import api, errors, x509
+from ipapython.dn import DN
+from nose.tools import raises, assert_raises
+from nose.plugins.skip import Skip, SkipTest
+from ipatests.test_xmlrpc.xmlrpc_test import (Declarative, XMLRPC_test,
+ fuzzy_uuid, fuzzy_digits, fuzzy_hash, fuzzy_date, fuzzy_issuer,
+ fuzzy_hex)
+from ipatests.test_xmlrpc import objectclasses
+import base64
+
+
+fqdn1 = u'testhost1.%s' % api.env.domain
+short1 = u'testhost1'
+dn1 = DN(('fqdn',fqdn1),('cn','computers'),('cn','accounts'),
+ api.env.basedn)
+service1 = u'dns/%s@%s' % (fqdn1, api.env.realm)
+service1dn = DN(('krbprincipalname',service1.lower()),('cn','services'),
+ ('cn','accounts'),api.env.basedn)
+fqdn2 = u'shouldnotexist.%s' % api.env.domain
+dn2 = DN(('fqdn',fqdn2),('cn','computers'),('cn','accounts'),
+ api.env.basedn)
+fqdn3 = u'testhost2.%s' % api.env.domain
+short3 = u'testhost2'
+dn3 = DN(('fqdn',fqdn3),('cn','computers'),('cn','accounts'),
+ api.env.basedn)
+fqdn4 = u'testhost2.lab.%s' % api.env.domain
+dn4 = DN(('fqdn',fqdn4),('cn','computers'),('cn','accounts'),
+ api.env.basedn)
+invalidfqdn1 = u'foo_bar.lab.%s' % api.env.domain
+
+# We can use the same cert we generated for the service tests
+fd = open('ipatests/test_xmlrpc/service.crt', 'r')
+servercert = fd.readlines()
+servercert = ''.join(servercert)
+servercert = x509.strip_header(servercert)
+fd.close()
+
+sshpubkey = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGAX3xAeLeaJggwTqMjxNwa6XHBUAikXPGMzEpVrlLDCZtv00djsFTBi38PkgxBJVkgRWMrcBsr/35lq7P6w8KGIwA8GI48Z0qBS2NBMJ2u9WQ2hjLN6GdMlo77O0uJY3251p12pCVIS/bHRSq8kHO2No8g7KA9fGGcagPfQH+ee3t7HUkpbQkFTmbPPN++r3V8oVUk5LxbryB3UIIVzNmcSIn3JrXynlvui4MixvrtX6zx+O/bBo68o8/eZD26QrahVbA09fivrn/4h3TM019Eu/c2jOdckfU3cHUV/3Tno5d6JicibyaoDDK7S/yjdn5jhaz8MSEayQvFkZkiF0L public key test'
+sshpubkeyfp = u'13:67:6B:BF:4E:A2:05:8E:AE:25:8B:A1:31:DE:6F:1B public key test (ssh-rsa)'
+
+class test_host(Declarative):
+
+ cleanup_commands = [
+ ('host_del', [fqdn1], {}),
+ ('host_del', [fqdn2], {}),
+ ('host_del', [fqdn3], {}),
+ ('host_del', [fqdn4], {}),
+ ('service_del', [service1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % fqdn1,
+ command=('host_show', [fqdn1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: host not found' % fqdn1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(description=u'Nope')),
+ expected=errors.NotFound(
+ reason=u'%s: host not found' % fqdn1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % fqdn1,
+ command=('host_del', [fqdn1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: host not found' % fqdn1),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn1,
+ command=('host_add', [fqdn1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Added host "%s"' % fqdn1,
+ result=dict(
+ dn=dn1,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % fqdn1,
+ command=('host_add', [fqdn1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=errors.DuplicateEntry(message=u'host with name ' +
+ u'"%s" already exists' % fqdn1),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % fqdn1,
+ command=('host_show', [fqdn1], {}),
+ expected=dict(
+ value=fqdn1,
+ summary=None,
+ result=dict(
+ dn=dn1,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ managedby_host=[fqdn1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r with all=True' % fqdn1,
+ command=('host_show', [fqdn1], dict(all=True)),
+ expected=dict(
+ value=fqdn1,
+ summary=None,
+ result=dict(
+ dn=dn1,
+ cn=[fqdn1],
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ # FIXME: Why is 'localalityname' returned as 'l' with --all?
+ # It is intuitive for --all to return additional attributes,
+ # but not to return existing attributes under different
+ # names.
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ serverhostname=[u'testhost1'],
+ objectclass=objectclasses.host,
+ managedby_host=[fqdn1],
+ managing_host=[fqdn1],
+ ipauniqueid=[fuzzy_uuid],
+ has_keytab=False,
+ has_password=False,
+ ipakrbokasdelegate=False,
+ ipakrbrequirespreauth=True,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % fqdn1,
+ command=('host_find', [fqdn1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 host matched',
+ result=[
+ dict(
+ dn=dn1,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ managedby_host=[u'%s' % fqdn1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with all=True' % fqdn1,
+ command=('host_find', [fqdn1], dict(all=True)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 host matched',
+ result=[
+ dict(
+ dn=dn1,
+ cn=[fqdn1],
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ # FIXME: Why is 'localalityname' returned as 'l' with --all?
+ # It is intuitive for --all to return additional attributes,
+ # but not to return existing attributes under different
+ # names.
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ serverhostname=[u'testhost1'],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[u'%s' % fqdn1],
+ managing_host=[u'%s' % fqdn1],
+ has_keytab=False,
+ has_password=False,
+ ipakrbokasdelegate=False,
+ ipakrbrequirespreauth=True,
+ ),
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(description=u'Updated host 1',
+ usercertificate=servercert)),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Modified host "%s"' % fqdn1,
+ result=dict(
+ description=[u'Updated host 1'],
+ fqdn=[fqdn1],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ managedby_host=[u'%s' % fqdn1],
+ usercertificate=[base64.b64decode(servercert)],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % fqdn1,
+ command=('host_show', [fqdn1], {}),
+ expected=dict(
+ value=fqdn1,
+ summary=None,
+ result=dict(
+ dn=dn1,
+ fqdn=[fqdn1],
+ description=[u'Updated host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ managedby_host=[u'%s' % fqdn1],
+ usercertificate=[base64.b64decode(servercert)],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Create %r' % fqdn3,
+ command=('host_add', [fqdn3],
+ dict(
+ description=u'Test host 2',
+ l=u'Undisclosed location 2',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn3,
+ summary=u'Added host "%s"' % fqdn3,
+ result=dict(
+ dn=dn3,
+ fqdn=[fqdn3],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn3, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[u'%s' % fqdn3],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn4,
+ command=('host_add', [fqdn4],
+ dict(
+ description=u'Test host 4',
+ l=u'Undisclosed location 4',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn4,
+ summary=u'Added host "%s"' % fqdn4,
+ result=dict(
+ dn=dn4,
+ fqdn=[fqdn4],
+ description=[u'Test host 4'],
+ l=[u'Undisclosed location 4'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn4, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[u'%s' % fqdn4],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add managedby_host %r to %r' % (fqdn1, fqdn3),
+ command=('host_add_managedby', [fqdn3],
+ dict(
+ host=u'%s' % fqdn1,
+ ),
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ managedby = dict(
+ host=tuple(),
+ ),
+ ),
+ result=dict(
+ dn=dn3,
+ fqdn=[fqdn3],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn3, api.env.realm)],
+ managedby_host=[u'%s' % fqdn3, u'%s' % fqdn1],
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Retrieve %r' % fqdn3,
+ command=('host_show', [fqdn3], {}),
+ expected=dict(
+ value=fqdn3,
+ summary=None,
+ result=dict(
+ dn=dn3,
+ fqdn=[fqdn3],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn3, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ managedby_host=[u'%s' % fqdn3, u'%s' % fqdn1],
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Search for hosts with --man-hosts and --not-man-hosts',
+ command=('host_find', [], {'man_host' : fqdn3, 'not_man_host' : fqdn1}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 host matched',
+ result=[
+ dict(
+ dn=dn3,
+ fqdn=[fqdn3],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn3, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ managedby_host=[u'%s' % fqdn3, u'%s' % fqdn1],
+ ),
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Try to search for hosts with --man-hosts',
+ command=('host_find', [], {'man_host' : [fqdn3,fqdn4]}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 hosts matched',
+ result=[],
+ ),
+ ),
+
+ dict(
+ desc='Remove managedby_host %r from %r' % (fqdn1, fqdn3),
+ command=('host_remove_managedby', [fqdn3],
+ dict(
+ host=u'%s' % fqdn1,
+ ),
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ managedby = dict(
+ host=tuple(),
+ ),
+ ),
+ result=dict(
+ dn=dn3,
+ fqdn=[fqdn3],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn3, api.env.realm)],
+ managedby_host=[u'%s' % fqdn3],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Show a host with multiple matches %s' % short3,
+ command=('host_show', [short3], {}),
+ expected=errors.SingleMatchExpected(found=2),
+ ),
+
+
+ dict(
+ desc='Try to rename %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(setattr=u'fqdn=changed.example.com')),
+ expected=errors.NotAllowedOnRDN()
+ ),
+
+
+ dict(
+ desc='Add MAC address to %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(macaddress=u'00:50:56:30:F6:5F')),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Modified host "%s"' % fqdn1,
+ result=dict(
+ description=[u'Updated host 1'],
+ fqdn=[fqdn1],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ managedby_host=[u'%s' % fqdn1],
+ usercertificate=[base64.b64decode(servercert)],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ macaddress=[u'00:50:56:30:F6:5F'],
+ issuer=fuzzy_issuer,
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add another MAC address to %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(macaddress=[u'00:50:56:30:F6:5F', u'00:50:56:2C:8D:82'])),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Modified host "%s"' % fqdn1,
+ result=dict(
+ description=[u'Updated host 1'],
+ fqdn=[fqdn1],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ managedby_host=[u'%s' % fqdn1],
+ usercertificate=[base64.b64decode(servercert)],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ macaddress=[u'00:50:56:30:F6:5F', u'00:50:56:2C:8D:82'],
+ issuer=fuzzy_issuer,
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add an illegal MAC address to %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(macaddress=[u'xx'])),
+ expected=errors.ValidationError(name='macaddress',
+ error=u'Must be of the form HH:HH:HH:HH:HH:HH, where ' +
+ u'each H is a hexadecimal character.'),
+ ),
+
+
+ dict(
+ desc='Add SSH public key to %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(ipasshpubkey=[sshpubkey])),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Modified host "%s"' % fqdn1,
+ result=dict(
+ description=[u'Updated host 1'],
+ fqdn=[fqdn1],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ managedby_host=[u'%s' % fqdn1],
+ usercertificate=[base64.b64decode(servercert)],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ macaddress=[u'00:50:56:30:F6:5F', u'00:50:56:2C:8D:82'],
+ ipasshpubkey=[sshpubkey],
+ sshpubkeyfp=[sshpubkeyfp],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add an illegal SSH public key to %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(ipasshpubkey=[u'no-pty %s' % sshpubkey])),
+ expected=errors.ValidationError(name='sshpubkey',
+ error=u'options are not allowed'),
+ ),
+
+
+ dict(
+ desc='Delete %r' % fqdn1,
+ command=('host_del', [fqdn1], {}),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Deleted host "%s"' % fqdn1,
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent %r' % fqdn1,
+ command=('host_show', [fqdn1], {}),
+ expected=errors.NotFound(reason=u'%s: host not found' % fqdn1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % fqdn1,
+ command=('host_mod', [fqdn1], dict(description=u'Nope')),
+ expected=errors.NotFound(reason=u'%s: host not found' % fqdn1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % fqdn1,
+ command=('host_del', [fqdn1], {}),
+ expected=errors.NotFound(reason=u'%s: host not found' % fqdn1),
+ ),
+
+ # Test deletion using a non-fully-qualified hostname. Services
+ # associated with this host should also be removed.
+ dict(
+ desc='Re-create %r' % fqdn1,
+ command=('host_add', [fqdn1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Added host "%s"' % fqdn1,
+ result=dict(
+ dn=dn1,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[u'%s' % fqdn1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Add a service to host %r' % fqdn1,
+ command=('service_add', [service1], {'force': True}),
+ expected=dict(
+ value=service1,
+ summary=u'Added service "%s"' % service1,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ objectclass=objectclasses.service,
+ managedby_host=[fqdn1],
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Delete using host name %r' % short1,
+ command=('host_del', [short1], {}),
+ expected=dict(
+ value=short1,
+ summary=u'Deleted host "%s"' % short1,
+ result=dict(failed=u''),
+ ),
+ ),
+
+ dict(
+ desc='Search for services for %r' % fqdn1,
+ command=('service_find', [fqdn1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 services matched',
+ result=[
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Try to add host not in DNS %r without force' % fqdn2,
+ command=('host_add', [fqdn2], {}),
+ expected=errors.DNSNotARecordError(
+ reason=u'Host does not have corresponding DNS A record'),
+ ),
+
+
+ dict(
+ desc='Try to add host not in DNS %r with force' % fqdn2,
+ command=('host_add', [fqdn2],
+ dict(
+ description=u'Test host 2',
+ l=u'Undisclosed location 2',
+ userclass=[u'webserver', u'mailserver'],
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn2,
+ summary=u'Added host "%s"' % fqdn2,
+ result=dict(
+ dn=dn2,
+ fqdn=[fqdn2],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn2, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn2],
+ userclass=[u'webserver', u'mailserver'],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % fqdn2,
+ command=('host_show', [fqdn2], {}),
+ expected=dict(
+ value=fqdn2,
+ summary=None,
+ result=dict(
+ dn=dn2,
+ fqdn=[fqdn2],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn2, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ managedby_host=[fqdn2],
+ userclass=[u'webserver', u'mailserver'],
+ ),
+ ),
+ ),
+
+
+ # This test will only succeed when running against lite-server.py
+ # on same box as IPA install.
+ dict(
+ desc='Delete the current host (master?) %s should be caught' % api.env.host,
+ command=('host_del', [api.env.host], {}),
+ expected=errors.ValidationError(name='hostname',
+ error=u'An IPA master host cannot be deleted or disabled'),
+ ),
+
+
+ dict(
+ desc='Disable the current host (master?) %s should be caught' % api.env.host,
+ command=('host_disable', [api.env.host], {}),
+ expected=errors.ValidationError(name='hostname',
+ error=u'An IPA master host cannot be deleted or disabled'),
+ ),
+
+
+ dict(
+ desc='Test that validation is enabled on adds',
+ command=('host_add', [invalidfqdn1], {}),
+ expected=errors.ValidationError(name='hostname',
+ error=u'invalid domain-name: only letters, numbers, and - ' +
+ u'are allowed. DNS label may not start or end with -'),
+ ),
+
+
+ # 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=('host_mod', [invalidfqdn1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: host not found' % invalidfqdn1),
+ ),
+
+
+ dict(
+ desc='Test that validation is disabled on deletes',
+ command=('host_del', [invalidfqdn1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: host not found' % invalidfqdn1),
+ ),
+
+
+ dict(
+ desc='Test that validation is disabled on show',
+ command=('host_show', [invalidfqdn1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: host not found' % invalidfqdn1),
+ ),
+
+
+ dict(
+ desc='Test that validation is disabled on find',
+ command=('host_find', [invalidfqdn1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 hosts matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Add managedby_host %r to %r' % (fqdn3, fqdn4),
+ command=('host_add_managedby', [fqdn4], dict(host=fqdn3,),
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ managedby = dict(
+ host=tuple(),
+ ),
+ ),
+ result=dict(
+ dn=dn4,
+ fqdn=[fqdn4],
+ description=[u'Test host 4'],
+ l=[u'Undisclosed location 4'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn4, api.env.realm)],
+ managedby_host=[fqdn4, fqdn3],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % fqdn3,
+ command=('host_del', [fqdn3], {}),
+ expected=dict(
+ value=fqdn3,
+ summary=u'Deleted host "%s"' % fqdn3,
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify that %r is gone from managedBy' % (fqdn4, fqdn3),
+ command=('host_show', [fqdn4], {}),
+ expected=dict(
+ value=fqdn4,
+ summary=None,
+ result=dict(
+ dn=dn4,
+ fqdn=[fqdn4],
+ description=[u'Test host 4'],
+ l=[u'Undisclosed location 4'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn4, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ managedby_host=[fqdn4],
+ ),
+ ),
+ ),
+
+ ]
+
+class test_host_false_pwd_change(XMLRPC_test):
+
+ fqdn1 = u'testhost1.%s' % api.env.domain
+ short1 = u'testhost1'
+ new_pass = u'pass_123'
+ command = "ipa-client/ipa-join"
+
+ @classmethod
+ def setUpClass(cls):
+ [cls.keytabfd,cls.keytabname] = tempfile.mkstemp()
+ os.close(cls.keytabfd)
+
+ does_command_exist = os.path.isfile(cls.command)
+
+ if not does_command_exist:
+ raise SkipTest("Command '%s' not found" % cls.command)
+
+ # auxiliary function for checking whether the join operation has set
+ # correct attributes
+ def host_joined(self):
+ ret = api.Command['host_show'](self.fqdn1, all=True)
+ assert (ret['result']['has_keytab'] == True)
+ assert (ret['result']['has_password'] == False)
+
+ def test_a_join_host(self):
+ """
+ Create a test host and join him into IPA.
+ """
+
+ # create a test host with bulk enrollment password
+ random_pass = api.Command['host_add'](self.fqdn1, random=True, force=True)['result']['randompassword']
+
+ # joint the host with the bulk password
+ new_args = [self.command,
+ "-s", api.env.host,
+ "-h", self.fqdn1,
+ "-k", self.keytabname,
+ "-w", random_pass,
+ "-q",
+ ]
+ try:
+ # join operation may fail on 'adding key into keytab', but
+ # the keytab is not necessary for further tests
+ (out, err, rc) = ipautil.run(new_args, None)
+ except ipautil.CalledProcessError, e:
+ pass
+ finally:
+ self.host_joined()
+
+ @raises(errors.ValidationError)
+ def test_b_try_password(self):
+ """
+ Try to change the password of enrolled host with specified password
+ """
+ api.Command['host_mod'](self.fqdn1, userpassword=self.new_pass)
+
+ @raises(errors.ValidationError)
+ def test_c_try_random(self):
+ """
+ Try to change the password of enrolled host with random password
+ """
+ api.Command['host_mod'](self.fqdn1, random=True)
+
+ def test_d_cleanup(self):
+ """
+ Clean up test data
+ """
+ os.unlink(self.keytabname)
+ api.Command['host_del'](self.fqdn1)
+ # verify that it's gone
+ with assert_raises(errors.NotFound):
+ api.Command['host_show'](self.fqdn1)
diff --git a/ipatests/test_xmlrpc/test_hostgroup_plugin.py b/ipatests/test_xmlrpc/test_hostgroup_plugin.py
new file mode 100644
index 000000000..b610979ec
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_hostgroup_plugin.py
@@ -0,0 +1,313 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2008, 2009 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/>.
+
+"""
+Test the `ipalib.plugins.hostgroup` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid
+from ipatests.test_xmlrpc import objectclasses
+from ipapython.dn import DN
+
+hostgroup1 = u'testhostgroup1'
+dn1 = DN(('cn',hostgroup1),('cn','hostgroups'),('cn','accounts'),
+ api.env.basedn)
+
+hostgroup_single = u'a'
+dn_single = DN(('cn',hostgroup_single),('cn','hostgroups'),('cn','accounts'),
+ api.env.basedn)
+
+fqdn1 = u'testhost1.%s' % api.env.domain
+host_dn1 = DN(('fqdn',fqdn1),('cn','computers'),('cn','accounts'),
+ api.env.basedn)
+
+invalidhostgroup1 = u'@invalid'
+
+
+class test_hostgroup(Declarative):
+
+ cleanup_commands = [
+ ('hostgroup_del', [hostgroup1], {}),
+ ('host_del', [fqdn1], {}),
+ ]
+
+ tests=[
+
+ dict(
+ desc='Try to retrieve non-existent %r' % hostgroup1,
+ command=('hostgroup_show', [hostgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: host group not found' % hostgroup1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % hostgroup1,
+ command=('hostgroup_mod', [hostgroup1],
+ dict(description=u'Updated hostgroup 1')
+ ),
+ expected=errors.NotFound(
+ reason=u'%s: host group not found' % hostgroup1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % hostgroup1,
+ command=('hostgroup_del', [hostgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: host group not found' % hostgroup1),
+ ),
+
+
+ dict(
+ desc='Test an invalid hostgroup name %r' % invalidhostgroup1,
+ command=('hostgroup_add', [invalidhostgroup1], dict(description=u'Test')),
+ expected=errors.ValidationError(name='hostgroup_name',
+ error=u'may only include letters, numbers, _, -, and .'),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup1,
+ command=('hostgroup_add', [hostgroup1],
+ dict(description=u'Test hostgroup 1')
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added hostgroup "testhostgroup1"',
+ result=dict(
+ dn=dn1,
+ cn=[hostgroup1],
+ objectclass=objectclasses.hostgroup,
+ description=[u'Test hostgroup 1'],
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn',hostgroup1),('cn','ng'),('cn','alt'),
+ api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % hostgroup1,
+ command=('hostgroup_add', [hostgroup1],
+ dict(description=u'Test hostgroup 1')
+ ),
+ expected=errors.DuplicateEntry(message=
+ u'host group with name "%s" already exists' % hostgroup1),
+ ),
+
+
+ dict(
+ desc='Create host %r' % fqdn1,
+ command=('host_add', [fqdn1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Added host "%s"' % fqdn1,
+ result=dict(
+ dn=host_dn1,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc=u'Add host %r to %r' % (fqdn1, hostgroup1),
+ command=(
+ 'hostgroup_add_member', [hostgroup1], dict(host=fqdn1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ host=tuple(),
+ hostgroup=tuple(),
+ ),
+ ),
+ result={
+ 'dn': dn1,
+ 'cn': [hostgroup1],
+ 'description': [u'Test hostgroup 1'],
+ 'member_host': [fqdn1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % hostgroup1,
+ command=('hostgroup_show', [hostgroup1], {}),
+ expected=dict(
+ value=hostgroup1,
+ summary=None,
+ result={
+ 'dn': dn1,
+ 'member_host': [u'testhost1.%s' % api.env.domain],
+ 'cn': [hostgroup1],
+ 'description': [u'Test hostgroup 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % hostgroup1,
+ command=('hostgroup_find', [], dict(cn=hostgroup1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 hostgroup matched',
+ result=[
+ {
+ 'dn': dn1,
+ 'member_host': [u'testhost1.%s' % api.env.domain],
+ 'cn': [hostgroup1],
+ 'description': [u'Test hostgroup 1'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % hostgroup1,
+ command=('hostgroup_mod', [hostgroup1],
+ dict(description=u'Updated hostgroup 1')
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Modified hostgroup "testhostgroup1"',
+ result=dict(
+ cn=[hostgroup1],
+ description=[u'Updated hostgroup 1'],
+ member_host=[u'testhost1.%s' % api.env.domain],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % hostgroup1,
+ command=('hostgroup_show', [hostgroup1], {}),
+ expected=dict(
+ value=hostgroup1,
+ summary=None,
+ result={
+ 'dn': dn1,
+ 'member_host': [u'testhost1.%s' % api.env.domain],
+ 'cn': [hostgroup1],
+ 'description': [u'Updated hostgroup 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove host %r from %r' % (fqdn1, hostgroup1),
+ command=('hostgroup_remove_member', [hostgroup1],
+ dict(host=fqdn1)
+ ),
+ expected=dict(
+ failed=dict(
+ member=dict(
+ host=tuple(),
+ hostgroup=tuple(),
+ ),
+ ),
+ completed=1,
+ result={
+ 'dn': dn1,
+ 'cn': [hostgroup1],
+ 'description': [u'Updated hostgroup 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % hostgroup1,
+ command=('hostgroup_del', [hostgroup1], {}),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Deleted hostgroup "testhostgroup1"',
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Create hostgroup with name containing only one letter: %r' % hostgroup_single,
+ command=('hostgroup_add', [hostgroup_single],
+ dict(description=u'Test hostgroup with single letter in name')
+ ),
+ expected=dict(
+ value=hostgroup_single,
+ summary=u'Added hostgroup "a"',
+ result=dict(
+ dn=dn_single,
+ cn=[hostgroup_single],
+ objectclass=objectclasses.hostgroup,
+ description=[u'Test hostgroup with single letter in name'],
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn',hostgroup_single),('cn','ng'),('cn','alt'),
+ api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % hostgroup_single,
+ command=('hostgroup_del', [hostgroup_single], {}),
+ expected=dict(
+ value=hostgroup_single,
+ summary=u'Deleted hostgroup "a"',
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete host %r' % fqdn1,
+ command=('host_del', [fqdn1], {}),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Deleted host "%s"' % fqdn1,
+ result=dict(failed=u''),
+ ),
+ )
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_krbtpolicy.py b/ipatests/test_xmlrpc/test_krbtpolicy.py
new file mode 100644
index 000000000..b940c5e5d
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_krbtpolicy.py
@@ -0,0 +1,150 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2011 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/>.
+"""
+Test kerberos ticket policy
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+user1 = u'tuser1'
+
+class test_krbtpolicy(Declarative):
+ cleanup_commands = [
+ ('user_del', [user1], {}),
+ ('krbtpolicy_reset', [], {}),
+ ]
+
+ tests = [
+
+
+ dict(
+ desc='Reset global policy',
+ command=(
+ 'krbtpolicy_reset', [], {}
+ ),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ krbmaxticketlife=[u'86400'],
+ krbmaxrenewableage=[u'604800'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Show global policy',
+ command=(
+ 'krbtpolicy_show', [], {}
+ ),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ dn=DN(('cn',api.env.domain),('cn','kerberos'),
+ api.env.basedn),
+ krbmaxticketlife=[u'86400'],
+ krbmaxrenewableage=[u'604800'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Update global policy',
+ command=(
+ 'krbtpolicy_mod', [], dict(krbmaxticketlife=3600)
+ ),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ krbmaxticketlife=[u'3600'],
+ krbmaxrenewableage=[u'604800'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user1),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user1),('cn','users'),('cn','accounts'), api.env.basedn)
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Update user ticket policy',
+ command=(
+ 'krbtpolicy_mod', [user1], dict(krbmaxticketlife=3600)
+ ),
+ expected=dict(
+ value=user1,
+ summary=None,
+ result=dict(
+ krbmaxticketlife=[u'3600'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try updating other user attribute',
+ command=(
+ 'krbtpolicy_mod', [user1], dict(setattr=u'givenname=Pete')
+ ),
+ expected=errors.ObjectclassViolation(info='attribute "givenname" not allowed'),
+ ),
+
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_nesting.py b/ipatests/test_xmlrpc/test_nesting.py
new file mode 100644
index 000000000..5c093c93a
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_nesting.py
@@ -0,0 +1,797 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+"""
+Test group nexting an indirect members
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+group1 = u'testgroup1'
+group2 = u'testgroup2'
+group3 = u'testgroup3'
+group4 = u'testgroup4'
+user1 = u'tuser1'
+user2 = u'tuser2'
+user3 = u'tuser3'
+user4 = u'tuser4'
+
+hostgroup1 = u'testhostgroup1'
+hgdn1 = DN(('cn',hostgroup1),('cn','hostgroups'),('cn','accounts'),
+ api.env.basedn)
+hostgroup2 = u'testhostgroup2'
+hgdn2 = DN(('cn',hostgroup2),('cn','hostgroups'),('cn','accounts'),
+ api.env.basedn)
+
+fqdn1 = u'testhost1.%s' % api.env.domain
+host_dn1 = DN(('fqdn',fqdn1),('cn','computers'),('cn','accounts'),
+ api.env.basedn)
+
+
+class test_nesting(Declarative):
+ cleanup_commands = [
+ ('group_del', [group1], {}),
+ ('group_del', [group2], {}),
+ ('group_del', [group3], {}),
+ ('group_del', [group4], {}),
+ ('user_del', [user1], {}),
+ ('user_del', [user2], {}),
+ ('user_del', [user3], {}),
+ ('user_del', [user4], {}),
+ ('host_del', [fqdn1], {}),
+ ('hostgroup_del', [hostgroup1], {}),
+ ('hostgroup_del', [hostgroup2], {}),
+ ]
+
+ tests = [
+
+ ################
+ # create group1:
+
+ dict(
+ desc='Create %r' % group1,
+ command=(
+ 'group_add', [group1], dict(description=u'Test desc 1')
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added group "testgroup1"',
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc 1'],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ gidnumber=[fuzzy_digits],
+ dn=DN(('cn','testgroup1'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ ################
+ # create 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.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn','testgroup2'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % group3,
+ command=(
+ 'group_add', [group3], dict(description=u'Test desc 3')
+ ),
+ expected=dict(
+ value=group3,
+ summary=u'Added group "testgroup3"',
+ result=dict(
+ cn=[group3],
+ description=[u'Test desc 3'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn','testgroup3'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % group4,
+ command=(
+ 'group_add', [group4], dict(description=u'Test desc 4')
+ ),
+ expected=dict(
+ value=group4,
+ summary=u'Added group "testgroup4"',
+ result=dict(
+ cn=[group4],
+ description=[u'Test desc 4'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn','testgroup4'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user1),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user1),('cn','users'),('cn','accounts'),
+ api.env.basedn)
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % user2,
+ command=(
+ 'user_add', [user2], dict(givenname=u'Test', sn=u'User2')
+ ),
+ expected=dict(
+ value=user2,
+ summary=u'Added user "%s"' % user2,
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user2),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user2),('cn','users'),('cn','accounts'),
+ api.env.basedn)
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % user3,
+ command=(
+ 'user_add', [user3], dict(givenname=u'Test', sn=u'User3')
+ ),
+ expected=dict(
+ value=user3,
+ summary=u'Added user "%s"' % user3,
+ result=dict(
+ gecos=[u'Test User3'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser3'],
+ krbprincipalname=[u'tuser3@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User3'],
+ uid=[user3],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user3, api.env.domain)],
+ displayname=[u'Test User3'],
+ cn=[u'Test User3'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user3),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user3),('cn','users'),('cn','accounts'),
+ api.env.basedn)
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % user4,
+ command=(
+ 'user_add', [user4], dict(givenname=u'Test', sn=u'User4')
+ ),
+ expected=dict(
+ value=user4,
+ summary=u'Added user "%s"' % user4,
+ result=dict(
+ gecos=[u'Test User4'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser4'],
+ krbprincipalname=[u'tuser4@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User4'],
+ uid=[user4],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user4, api.env.domain)],
+ displayname=[u'Test User4'],
+ cn=[u'Test User4'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user4),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user4),('cn','users'),('cn','accounts'),
+ api.env.basedn)
+ ),
+ ),
+ ),
+
+
+ ###############
+ # member stuff
+ #
+ # Create 4 groups and 4 users and set the following membership:
+ #
+ # g1:
+ # no direct memberships
+ #
+ # g2:
+ # memberof: g1
+ # member: user1, user2
+ #
+ # g3:
+ # memberof: g1
+ # member: user3, g4
+ #
+ # g4:
+ # memberof: g3
+ # member: user1, user4
+ #
+ # So when we do a show it looks like:
+ #
+ # g1:
+ # member groups: g2, g3
+ # indirect member group: g4
+ # indirect member users: user1, user2, tuser3, tuser4
+ #
+ # g2:
+ # member of group: g1
+ # member users: tuser1, tuser2
+ #
+ # g3:
+ # member users: tuser3
+ # member groups: g4
+ # member of groups: g1
+ # indirect member users: tuser4
+ #
+ # g4:
+ # member users: tuser1, tuser4
+ # member of groups: g3
+ # indirect member of groups: g1
+ #
+ # Note that tuser1 is an indirect member of g1 both through
+ # g2 and g4. It should appear just once in the list.
+
+ dict(
+ desc='Add a group 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': DN(('cn',group1),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_group': (group2,),
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group1],
+ 'description': [u'Test desc 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add a group member %r to %r' % (group3, group1),
+ command=(
+ 'group_add_member', [group1], dict(group=group3)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group1),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_group': [group2, group3,],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group1],
+ 'description': [u'Test desc 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add a user member %r to %r' % (user1, group2),
+ command=(
+ 'group_add_member', [group2], dict(user=user1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group2),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_user': (u'tuser1',),
+ 'memberof_group': (u'testgroup1',),
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group2],
+ 'description': [u'Test desc 2'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add a user member %r to %r' % (user2, group2),
+ command=(
+ 'group_add_member', [group2], dict(user=user2)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group2),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_user': [user1, user2],
+ 'memberof_group': [group1],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group2],
+ 'description': [u'Test desc 2'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add a user member %r to %r' % (user3, group3),
+ command=(
+ 'group_add_member', [group3], dict(user=user3)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group3),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_user': [user3],
+ 'memberof_group': [group1],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group3],
+ 'description': [u'Test desc 3'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add a group member %r to %r' % (group4, group3),
+ command=(
+ 'group_add_member', [group3], dict(group=group4)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group3),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_user': [user3],
+ 'memberof_group': [group1],
+ 'member_group': [group4],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group3],
+ 'description': [u'Test desc 3'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add a user member %r to %r' % (user1, group4),
+ command=(
+ 'group_add_member', [group4], dict(user=user1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group4),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_user': [user1],
+ 'memberof_group': [group3],
+ 'memberofindirect_group': [group1],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group4],
+ 'description': [u'Test desc 4'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add a user member %r to %r' % (user4, group4),
+ command=(
+ 'group_add_member', [group4], dict(user=user4)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group4),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_user': [user1, user4],
+ 'memberof_group': [group3],
+ 'memberofindirect_group': [group1],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group4],
+ 'description': [u'Test desc 4'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve group %r' % group1,
+ command=('group_show', [group1], {}),
+ expected=dict(
+ value=group1,
+ summary=None,
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc 1'],
+ gidnumber= [fuzzy_digits],
+ memberindirect_group = [group4],
+ member_group = [group2, group3],
+ memberindirect_user = [user1, user2, user3, user4],
+ dn=DN(('cn','testgroup1'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve group %r' % group2,
+ command=('group_show', [group2], {}),
+ expected=dict(
+ value=group2,
+ summary=None,
+ result=dict(
+ cn=[group2],
+ description=[u'Test desc 2'],
+ gidnumber= [fuzzy_digits],
+ memberof_group = [group1],
+ member_user = [user1, user2],
+ dn=DN(('cn','testgroup2'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve group %r' % group3,
+ command=('group_show', [group3], {}),
+ expected=dict(
+ value=group3,
+ summary=None,
+ result=dict(
+ cn=[group3],
+ description=[u'Test desc 3'],
+ gidnumber= [fuzzy_digits],
+ memberof_group = [group1],
+ member_user = [user3],
+ member_group = [group4],
+ memberindirect_user = [user1, user4],
+ dn=DN(('cn','testgroup3'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve group %r' % group4,
+ command=('group_show', [group4], {}),
+ expected=dict(
+ value=group4,
+ summary=None,
+ result=dict(
+ cn=[group4],
+ description=[u'Test desc 4'],
+ gidnumber= [fuzzy_digits],
+ memberof_group = [group3],
+ member_user = [user1, user4],
+ memberofindirect_group = [group1],
+ dn=DN(('cn','testgroup4'),('cn','groups'),
+ ('cn','accounts'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ # Now do something similar with hosts and hostgroups
+ dict(
+ desc='Create host %r' % fqdn1,
+ command=('host_add', [fqdn1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Added host "%s"' % fqdn1,
+ result=dict(
+ dn=host_dn1,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup1,
+ command=('hostgroup_add', [hostgroup1],
+ dict(description=u'Test hostgroup 1')
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added hostgroup "testhostgroup1"',
+ result=dict(
+ dn=hgdn1,
+ cn=[hostgroup1],
+ objectclass=objectclasses.hostgroup,
+ description=[u'Test hostgroup 1'],
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn',hostgroup1),('cn','ng'),('cn','alt'),
+ api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup2,
+ command=('hostgroup_add', [hostgroup2],
+ dict(description=u'Test hostgroup 2')
+ ),
+ expected=dict(
+ value=hostgroup2,
+ summary=u'Added hostgroup "testhostgroup2"',
+ result=dict(
+ dn=hgdn2,
+ cn=[hostgroup2],
+ objectclass=objectclasses.hostgroup,
+ description=[u'Test hostgroup 2'],
+ ipauniqueid=[fuzzy_uuid],
+ mepmanagedentry=[DN(('cn',hostgroup2),('cn','ng'),('cn','alt'),
+ api.env.basedn)],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc=u'Add host %r to %r' % (fqdn1, hostgroup2),
+ command=(
+ 'hostgroup_add_member', [hostgroup2], dict(host=fqdn1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ host=tuple(),
+ hostgroup=tuple(),
+ ),
+ ),
+ result={
+ 'dn': hgdn2,
+ 'cn': [hostgroup2],
+ 'description': [u'Test hostgroup 2'],
+ 'member_host': [fqdn1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc=u'Add hostgroup %r to %r' % (hostgroup2, hostgroup1),
+ command=(
+ 'hostgroup_add_member', [hostgroup1], dict(hostgroup=hostgroup2)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ host=tuple(),
+ hostgroup=tuple(),
+ ),
+ ),
+ result={
+ 'dn': hgdn1,
+ 'cn': [hostgroup1],
+ 'description': [u'Test hostgroup 1'],
+ 'member_hostgroup': [hostgroup2],
+ 'memberindirect_host': [fqdn1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % hostgroup1,
+ command=('hostgroup_show', [hostgroup1], {}),
+ expected=dict(
+ value=hostgroup1,
+ summary=None,
+ result={
+ 'dn': hgdn1,
+ 'memberindirect_host': [u'testhost1.%s' % api.env.domain],
+ 'member_hostgroup': [hostgroup2],
+ 'cn': [hostgroup1],
+ 'description': [u'Test hostgroup 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % fqdn1,
+ command=('host_show', [fqdn1], {}),
+ expected=dict(
+ value=fqdn1,
+ summary=None,
+ result=dict(
+ dn=host_dn1,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ has_keytab=False,
+ has_password=False,
+ managedby_host=[fqdn1],
+ memberof_hostgroup = [u'testhostgroup2'],
+ memberofindirect_hostgroup = [u'testhostgroup1'],
+ ),
+ ),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_netgroup_plugin.py b/ipatests/test_xmlrpc/test_netgroup_plugin.py
new file mode 100644
index 000000000..3dccac1bd
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_netgroup_plugin.py
@@ -0,0 +1,1362 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+"""
+Test the `ipalib/plugins/netgroup.py` module.
+"""
+
+import nose
+import krbV
+from ipalib import api
+from ipalib import errors
+from ipaserver.plugins.ldap2 import ldap2
+from ipatests.test_xmlrpc.xmlrpc_test import (Declarative, fuzzy_digits,
+ fuzzy_uuid, fuzzy_netgroupdn)
+from ipatests.test_xmlrpc import objectclasses
+from ipapython.dn import DN
+
+# Global so we can save the value between tests
+netgroup_dn = None
+
+# See if our LDAP server is up and we can talk to it over GSSAPI
+ccache = krbV.default_context().default_ccache().name
+
+netgroup1 = u'netgroup1'
+netgroup2 = u'netgroup2'
+netgroup_single = u'a'
+
+host1 = u'ipatesthost.%s' % api.env.domain
+host_dn1 = DN(('fqdn',host1),('cn','computers'),('cn','accounts'),
+ api.env.basedn)
+
+unknown_host = u'unknown'
+
+unknown_host2 = u'unknown2'
+
+hostgroup1 = u'hg1'
+hostgroup_dn1 = DN(('cn',hostgroup1),('cn','hostgroups'),('cn','accounts'),
+ api.env.basedn)
+
+user1 = u'jexample'
+
+# user2 is a member of testgroup
+user2 = u'pexample'
+
+group1 = u'testgroup'
+
+invalidnetgroup1=u'+badnetgroup'
+invalidnisdomain1=u'domain1,domain2'
+invalidnisdomain2=u'+invalidnisdomain'
+invalidhost=u'+invalid&host'
+
+class test_netgroup(Declarative):
+ """
+ Test the `netgroup` plugin.
+ """
+
+ cleanup_commands = [
+ ('netgroup_del', [netgroup1], {}),
+ ('netgroup_del', [netgroup2], {}),
+ ('host_del', [host1], {}),
+ ('hostgroup_del', [hostgroup1], {}),
+ ('user_del', [user1], {}),
+ ('user_del', [user2], {}),
+ ('group_del', [group1], {}),
+ ]
+
+ tests=[
+
+ dict(
+ desc='Try to retrieve non-existent %r' % netgroup1,
+ command=('netgroup_show', [netgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: netgroup not found' % netgroup1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % netgroup1,
+ command=('netgroup_mod', [netgroup1],
+ dict(description=u'Updated hostgroup 1')
+ ),
+ expected=errors.NotFound(
+ reason=u'%s: netgroup not found' % netgroup1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % netgroup1,
+ command=('netgroup_del', [netgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: netgroup not found' % netgroup1),
+ ),
+
+
+ dict(
+ desc='Test an invalid netgroup name %r' % invalidnetgroup1,
+ command=('netgroup_add', [invalidnetgroup1], dict(description=u'Test')),
+ expected=errors.ValidationError(name='name',
+ error=u'may only include letters, numbers, _, -, and .'),
+ ),
+
+
+ dict(
+ desc='Test an invalid nisdomain1 name %r' % invalidnisdomain1,
+ command=('netgroup_add', [netgroup1],
+ dict(description=u'Test',nisdomainname=invalidnisdomain1)),
+ expected=errors.ValidationError(name='nisdomain',
+ error='may only include letters, numbers, _, -, and .'),
+ ),
+
+
+ dict(
+ desc='Test an invalid nisdomain2 name %r' % invalidnisdomain2,
+ command=('netgroup_add', [netgroup1],
+ dict(description=u'Test',nisdomainname=invalidnisdomain2)),
+ expected=errors.ValidationError(name='nisdomain',
+ error='may only include letters, numbers, _, -, and .'),
+ ),
+
+
+ dict(
+ desc='Create %r' % netgroup1,
+ command=('netgroup_add', [netgroup1],
+ dict(description=u'Test netgroup 1')
+ ),
+ expected=dict(
+ value=netgroup1,
+ summary=u'Added netgroup "%s"' % netgroup1,
+ result=dict(
+ dn=fuzzy_netgroupdn,
+ cn=[netgroup1],
+ objectclass=objectclasses.netgroup,
+ description=[u'Test netgroup 1'],
+ nisdomainname=['%s' % api.env.domain],
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % netgroup2,
+ command=('netgroup_add', [netgroup2],
+ dict(description=u'Test netgroup 2')
+ ),
+ expected=dict(
+ value=netgroup2,
+ summary=u'Added netgroup "%s"' % netgroup2,
+ result=dict(
+ dn=fuzzy_netgroupdn,
+ cn=[netgroup2],
+ objectclass=objectclasses.netgroup,
+ description=[u'Test netgroup 2'],
+ nisdomainname=['%s' % api.env.domain],
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create netgroup with name containing only one letter: %r' % netgroup_single,
+ command=('netgroup_add', [netgroup_single],
+ dict(description=u'Test netgroup_single')
+ ),
+ expected=dict(
+ value=netgroup_single,
+ summary=u'Added netgroup "%s"' % netgroup_single,
+ result=dict(
+ dn=fuzzy_netgroupdn,
+ cn=[netgroup_single],
+ objectclass=objectclasses.netgroup,
+ description=[u'Test netgroup_single'],
+ nisdomainname=['%s' % api.env.domain],
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % netgroup_single,
+ command=('netgroup_del', [netgroup_single], {}),
+ expected=dict(
+ value=netgroup_single,
+ summary=u'Deleted netgroup "%s"' % netgroup_single,
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % netgroup1,
+ command=('netgroup_add', [netgroup1],
+ dict(description=u'Test netgroup 1')
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'netgroup with name "%s" already exists' % netgroup1),
+ ),
+
+
+ dict(
+ desc='Create host %r' % host1,
+ command=('host_add', [host1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=host1,
+ summary=u'Added host "%s"' % host1,
+ result=dict(
+ dn=host_dn1,
+ fqdn=[host1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (host1, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[host1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % hostgroup1,
+ command=('hostgroup_add', [hostgroup1],
+ dict(description=u'Test hostgroup 1')
+ ),
+ expected=dict(
+ value=hostgroup1,
+ summary=u'Added hostgroup "%s"' % hostgroup1,
+ result=dict(
+ dn=hostgroup_dn1,
+ cn=[hostgroup1],
+ objectclass=objectclasses.hostgroup,
+ description=[u'Test hostgroup 1'],
+ mepmanagedentry=[DN(('cn',hostgroup1),('cn','ng'),('cn','alt'),
+ api.env.basedn)],
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/%s' % user1],
+ krbprincipalname=[u'%s@%s' % (user1, api.env.realm)],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user1),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user1),('cn','users'),('cn','accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Create %r' % user2,
+ command=(
+ 'user_add', [user2], dict(givenname=u'Test', sn=u'User2')
+ ),
+ expected=dict(
+ value=user2,
+ summary=u'Added user "%s"' % user2,
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/%s' % user2],
+ krbprincipalname=[u'%s@%s' % (user2, api.env.realm)],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user2),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user2),('cn','users'),('cn','accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % 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'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn',group1),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add user %r to group %r' % (user2, group1),
+ command=(
+ 'group_add_member', [group1], dict(user=user2)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',group1),('cn','groups'),('cn','accounts'),
+ api.env.basedn),
+ 'member_user': (user2,),
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group1],
+ 'description': [u'Test desc 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add invalid host %r to netgroup %r' % (invalidhost, netgroup1),
+ command=('netgroup_add_member', [netgroup1], dict(host=invalidhost)),
+ expected=errors.ValidationError(name='host',
+ error='only letters, numbers, _, and - are allowed. ' +
+ u'DNS label may not start or end with -'),
+ ),
+
+
+ dict(
+ desc='Add host %r to netgroup %r' % (host1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(host=host1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add hostgroup %r to netgroup %r' % (hostgroup1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(hostgroup=hostgroup1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for netgroups using no_user',
+ command=('netgroup_find', [], dict(no_user=user1)),
+ expected=dict(
+ count=2,
+ truncated=False,
+ summary=u'2 netgroups matched',
+ result=[
+ {
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ {
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup2],
+ 'description': [u'Test netgroup 2'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc="Check %r doesn't match when searching for %s" % (netgroup1, user1),
+ command=('netgroup_find', [], dict(user=user1)),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 netgroups matched',
+ result=[],
+ ),
+ ),
+
+ dict(
+ desc='Add user %r to netgroup %r' % (user1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(user=user1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+ dict(
+ desc="Check %r doesn't match when searching for no %s" % (netgroup1, user1),
+ command=('netgroup_find', [], dict(no_user=user1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 netgroup matched',
+ result=[
+ {
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup2],
+ 'description': [u'Test netgroup 2'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Add group %r to netgroup %r' % (group1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(group=group1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add netgroup %r to netgroup %r' % (netgroup2, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(netgroup=netgroup2)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add non-existent netgroup to netgroup %r' % (netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(netgroup=u'notfound')
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=[(u'notfound', u'no such entry')],
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add duplicate user %r to netgroup %r' % (user1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(user=user1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=[('%s' % user1, u'This entry is already a member')],
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Add duplicate group %r to netgroup %r' % (group1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(group=group1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=[('%s' % group1, u'This entry is already a member')],
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add duplicate host %r to netgroup %r' % (host1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(host=host1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=[('%s' % host1, u'This entry is already a member')],
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add duplicate hostgroup %r to netgroup %r' % (hostgroup1, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(hostgroup=hostgroup1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=[('%s' % hostgroup1, u'This entry is already a member')],
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Add unknown host %r to netgroup %r' % (unknown_host, netgroup1),
+ command=(
+ 'netgroup_add_member', [netgroup1], dict(host=unknown_host)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Add invalid host %r to netgroup %r using setattr' %
+ (invalidhost, netgroup1),
+ command=(
+ 'netgroup_mod', [netgroup1],
+ dict(setattr='externalhost=%s' % invalidhost)
+ ),
+ expected=errors.ValidationError(name='externalhost',
+ error='only letters, numbers, _, and - are allowed. ' +
+ 'DNS label may not start or end with -'),
+ ),
+
+ dict(
+ desc='Add unknown host %r to netgroup %r using addattr' %
+ (unknown_host2, netgroup1),
+ command=(
+ 'netgroup_mod', [netgroup1],
+ dict(addattr='externalhost=%s' % unknown_host2)
+ ),
+ expected=dict(
+ value=u'netgroup1',
+ summary=u'Modified netgroup "netgroup1"',
+ result={
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host, unknown_host2],
+ },
+ )
+ ),
+
+ dict(
+ desc='Remove unknown host %r from netgroup %r using delattr' %
+ (unknown_host2, netgroup1),
+ command=(
+ 'netgroup_mod', [netgroup1],
+ dict(delattr='externalhost=%s' % unknown_host2)
+ ),
+ expected=dict(
+ value=u'netgroup1',
+ summary=u'Modified netgroup "netgroup1"',
+ result={
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ )
+ ),
+
+ dict(
+ desc='Retrieve %r' % netgroup1,
+ command=('netgroup_show', [netgroup1], {}),
+ expected=dict(
+ value=netgroup1,
+ summary=None,
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Search for %r' % netgroup1,
+ command=('netgroup_find', [], dict(cn=netgroup1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 netgroup matched',
+ result=[
+ {
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Search for %r using user' % netgroup1,
+ command=('netgroup_find', [], dict(user=user1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 netgroup matched',
+ result=[
+ {
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Search for all netgroups using empty member user',
+ command=('netgroup_find', [], dict(user=None)),
+ expected=dict(
+ count=2,
+ truncated=False,
+ summary=u'2 netgroups matched',
+ result=[
+ {
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Test netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ {
+ 'dn': fuzzy_netgroupdn,
+ 'memberof_netgroup': (netgroup1,),
+ 'cn': [netgroup2],
+ 'description': [u'Test netgroup 2'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Update %r' % netgroup1,
+ command=('netgroup_mod', [netgroup1],
+ dict(description=u'Updated netgroup 1')
+ ),
+ expected=dict(
+ value=netgroup1,
+ summary=u'Modified netgroup "%s"' % netgroup1,
+ result={
+ 'memberhost_host': (host1,),
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove host %r from netgroup %r' % (host1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(host=host1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberhost_hostgroup': (hostgroup1,),
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove hostgroup %r from netgroup %r' % (hostgroup1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(hostgroup=hostgroup1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberuser_user': (user1,),
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove user %r from netgroup %r' % (user1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(user=user1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'memberuser_group': (group1,),
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove group %r from netgroup %r' % (group1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(group=group1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'member_netgroup': (netgroup2,),
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove netgroup %r from netgroup %r' % (netgroup2, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(netgroup=netgroup2)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove host %r from netgroup %r again' % (host1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(host=host1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=[('%s' % host1, u'This entry is not a member')]
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove hostgroup %r from netgroup %r again' % (hostgroup1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(hostgroup=hostgroup1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=[('%s' % hostgroup1, u'This entry is not a member')],
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove user %r from netgroup %r again' % (user1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(user=user1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=[('%s' % user1, u'This entry is not a member')],
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove group %r from netgroup %r again' % (group1, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(group=group1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=tuple(),
+ ),
+ memberuser=dict(
+ group= [('%s' % group1, u'This entry is not a member')],
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove netgroup %r from netgroup %r again' % (netgroup2, netgroup1),
+ command=(
+ 'netgroup_remove_member', [netgroup1], dict(netgroup=netgroup2)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ netgroup=[('%s' % netgroup2, u'This entry is not a member')],
+ ),
+ memberuser=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ memberhost=dict(
+ hostgroup=tuple(),
+ host=tuple(),
+ ),
+ ),
+ result={
+ 'dn': fuzzy_netgroupdn,
+ 'cn': [netgroup1],
+ 'description': [u'Updated netgroup 1'],
+ 'nisdomainname': [u'%s' % api.env.domain],
+ 'externalhost': [unknown_host],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % netgroup1,
+ command=('netgroup_del', [netgroup1], {}),
+ expected=dict(
+ value=netgroup1,
+ summary=u'Deleted netgroup "%s"' % netgroup1,
+ result=dict(failed=u''),
+ ),
+ ),
+
+ ]
+
+# No way to convert this test just yet.
+
+# def test_6b_netgroup_show(self):
+# """
+# Confirm the underlying triples
+# """
+# # Do an LDAP query to the compat area and verify that the entry
+# # is correct
+# conn = ldap2(shared_instance=False, ldap_uri=api.env.ldap_uri, base_dn=api.env.basedn)
+# conn.connect(ccache=ccache)
+# try:
+# entries = conn.find_entries('cn=%s' % self.ng_cn,
+# base_dn='cn=ng,cn=compat,%s' % api.env.basedn)
+# except errors.NotFound:
+# raise nose.SkipTest('compat and nis are not enabled, skipping test')
+# finally:
+# conn.disconnect()
+# triples = entries[0][0][1]['nisnetgrouptriple']
+#
+# # This may not prove to be reliable since order is not guaranteed
+# # and even which user gets into which triple can be random.
+# assert '(nosuchhost,jexample,example.com)' in triples
+# assert '(ipatesthost.%s,pexample,example.com)' % api.env.domain in triples
diff --git a/ipatests/test_xmlrpc/test_passwd_plugin.py b/ipatests/test_xmlrpc/test_passwd_plugin.py
new file mode 100644
index 000000000..2a44da711
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_passwd_plugin.py
@@ -0,0 +1,69 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+"""
+Test the `ipalib/plugins/passwd.py` module.
+"""
+
+import sys
+
+from nose.tools import assert_raises # pylint: disable=E0611
+
+from xmlrpc_test import XMLRPC_test, assert_attr_equal
+from ipalib import api
+from ipalib import errors
+
+
+class test_passwd(XMLRPC_test):
+ """
+ Test the `passwd` plugin.
+ """
+ uid = u'pwexample'
+ givenname = u'Jim'
+ sn = u'Example'
+ home = u'/home/%s' % uid
+ principalname = u'%s@%s' % (uid, api.env.realm)
+ kw = {'givenname': givenname, 'sn': sn, 'uid': uid, 'homedirectory': home}
+
+ def test_1_user_add(self):
+ """
+ Create a test user
+ """
+ entry = api.Command['user_add'](**self.kw)['result']
+ assert_attr_equal(entry, 'givenname', self.givenname)
+ assert_attr_equal(entry, 'sn', self.sn)
+ assert_attr_equal(entry, 'uid', self.uid)
+ assert_attr_equal(entry, 'homedirectory', self.home)
+ assert_attr_equal(entry, 'objectclass', 'ipaobject')
+
+ def test_2_set_passwd(self):
+ """
+ Test the `xmlrpc.passwd` method.
+ """
+ out = api.Command['passwd'](self.uid, password=u'password1')
+ assert out['result'] is True
+
+ def test_3_user_del(self):
+ """
+ Remove the test user
+ """
+ api.Command['user_del'](self.uid)
+
+ # Verify that it is gone
+ with assert_raises(errors.NotFound):
+ api.Command['user_show'](self.uid)
diff --git a/ipatests/test_xmlrpc/test_permission_plugin.py b/ipatests/test_xmlrpc/test_permission_plugin.py
new file mode 100644
index 000000000..dbd9d6901
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_permission_plugin.py
@@ -0,0 +1,972 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib/plugins/permission.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+permission1 = u'testperm'
+permission1_dn = DN(('cn',permission1),
+ api.env.container_permission,api.env.basedn)
+
+
+permission1_renamed = u'testperm1_rn'
+permission1_renamed_dn = DN(('cn',permission1_renamed),
+ api.env.container_permission,api.env.basedn)
+
+permission1_renamed_ucase = u'Testperm_RN'
+permission1_renamed_ucase_dn = DN(('cn',permission1_renamed_ucase),
+ api.env.container_permission,api.env.basedn)
+
+
+permission2 = u'testperm2'
+permission2_dn = DN(('cn',permission2),
+ api.env.container_permission,api.env.basedn)
+
+permission3 = u'testperm3'
+permission3_dn = DN(('cn',permission3),
+ api.env.container_permission,api.env.basedn)
+permission3_attributelevelrights = {
+ 'member': u'rscwo',
+ 'seealso': u'rscwo',
+ 'ipapermissiontype': u'rscwo',
+ 'cn': u'rscwo',
+ 'businesscategory': u'rscwo',
+ 'objectclass': u'rscwo',
+ 'memberof': u'rscwo',
+ 'aci': u'rscwo',
+ 'subtree': u'rscwo',
+ 'o': u'rscwo',
+ 'filter': u'rscwo',
+ 'attrs': u'rscwo',
+ 'owner': u'rscwo',
+ 'group': u'rscwo',
+ 'ou': u'rscwo',
+ 'targetgroup': u'rscwo',
+ 'type': u'rscwo',
+ 'permissions': u'rscwo',
+ 'nsaccountlock': u'rscwo',
+ 'description': u'rscwo',
+ }
+
+privilege1 = u'testpriv1'
+privilege1_dn = DN(('cn',privilege1),
+ api.env.container_privilege,api.env.basedn)
+
+invalid_permission1 = u'bad;perm'
+
+
+class test_permission(Declarative):
+
+ cleanup_commands = [
+ ('permission_del', [permission1], {}),
+ ('permission_del', [permission2], {}),
+ ('permission_del', [permission3], {}),
+ ('privilege_del', [privilege1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % permission1,
+ command=('permission_show', [permission1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: permission not found' % permission1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % permission1,
+ command=('permission_mod', [permission1], dict(permissions=u'all')),
+ expected=errors.NotFound(
+ reason=u'%s: permission not found' % permission1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % permission1,
+ command=('permission_del', [permission1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: permission not found' % permission1),
+ ),
+
+
+ dict(
+ desc='Search for non-existent %r' % permission1,
+ command=('permission_find', [permission1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 permissions matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % permission1,
+ command=(
+ 'permission_add', [permission1], dict(
+ type=u'user',
+ permissions=u'write',
+ )
+ ),
+ expected=dict(
+ value=permission1,
+ summary=u'Added permission "%s"' % permission1,
+ result=dict(
+ dn=permission1_dn,
+ cn=[permission1],
+ objectclass=objectclasses.permission,
+ type=u'user',
+ permissions=[u'write'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % permission1,
+ command=(
+ 'permission_add', [permission1], dict(
+ type=u'user',
+ permissions=u'write',
+ ),
+ ),
+ expected=errors.DuplicateEntry(),
+ ),
+
+
+ dict(
+ desc='Create %r' % privilege1,
+ command=('privilege_add', [privilege1],
+ dict(description=u'privilege desc. 1')
+ ),
+ expected=dict(
+ value=privilege1,
+ summary=u'Added privilege "%s"' % privilege1,
+ result=dict(
+ dn=privilege1_dn,
+ cn=[privilege1],
+ description=[u'privilege desc. 1'],
+ objectclass=objectclasses.privilege,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add permission %r to privilege %r' % (permission1, privilege1),
+ command=('privilege_add_permission', [privilege1],
+ dict(permission=permission1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ permission=[],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % permission1,
+ command=('permission_show', [permission1], {}),
+ expected=dict(
+ value=permission1,
+ summary=None,
+ result={
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r with --raw' % permission1,
+ command=('permission_show', [permission1], {'raw' : True}),
+ expected=dict(
+ value=permission1,
+ summary=None,
+ result={
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member': [privilege1_dn],
+ 'aci': u'(target = "ldap:///%s")(version 3.0;acl "permission:testperm";allow (write) groupdn = "ldap:///%s";)' % \
+ (DN(('uid', '*'), ('cn', 'users'), ('cn', 'accounts'), api.env.basedn),
+ DN(('cn', 'testperm'), ('cn', 'permissions'), ('cn', 'pbac'), api.env.basedn))
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % permission1,
+ command=('permission_find', [permission1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r using --name' % permission1,
+ command=('permission_find', [], {'cn': permission1}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for non-existence permission using --name',
+ command=('permission_find', [], {'cn': u'notfound'}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 permissions matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % privilege1,
+ command=('permission_find', [privilege1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with --raw' % permission1,
+ command=('permission_find', [permission1], {'raw' : True}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member': [privilege1_dn],
+ 'aci': u'(target = "ldap:///%s")(version 3.0;acl "permission:testperm";allow (write) groupdn = "ldap:///%s";)' % \
+ (DN(('uid', '*'), ('cn', 'users'), ('cn', 'accounts'), api.env.basedn),
+ DN(('cn', 'testperm'), ('cn', 'permissions'), ('cn', 'pbac'), api.env.basedn)),
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % permission2,
+ command=(
+ 'permission_add', [permission2], dict(
+ type=u'user',
+ permissions=u'write',
+ setattr=u'owner=cn=test',
+ addattr=u'owner=cn=test2',
+ )
+ ),
+ expected=dict(
+ value=permission2,
+ summary=u'Added permission "%s"' % permission2,
+ result=dict(
+ dn=permission2_dn,
+ cn=[permission2],
+ objectclass=objectclasses.permission,
+ type=u'user',
+ permissions=[u'write'],
+ owner=[u'cn=test', u'cn=test2'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % permission1,
+ command=('permission_find', [permission1], {}),
+ expected=dict(
+ count=2,
+ truncated=False,
+ summary=u'2 permissions matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ {
+ 'dn': permission2_dn,
+ 'cn': [permission2],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with --pkey-only' % permission1,
+ command=('permission_find', [permission1], {'pkey_only' : True}),
+ expected=dict(
+ count=2,
+ truncated=False,
+ summary=u'2 permissions matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ },
+ {
+ 'dn': permission2_dn,
+ 'cn': [permission2],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search by ACI attribute with --pkey-only',
+ command=('permission_find', [], {'pkey_only': True,
+ 'attrs': [u'krbminpwdlife']}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': DN(('cn','Modify Group Password Policy'),
+ api.env.container_permission, api.env.basedn),
+ 'cn': [u'Modify Group Password Policy'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % privilege1,
+ command=('privilege_find', [privilege1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 privilege matched',
+ result=[
+ {
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with a limit of 1 (truncated)' % permission1,
+ command=('permission_find', [permission1], dict(sizelimit=1)),
+ expected=dict(
+ count=1,
+ truncated=True,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with a limit of 2' % permission1,
+ command=('permission_find', [permission1], dict(sizelimit=2)),
+ expected=dict(
+ count=2,
+ truncated=False,
+ summary=u'2 permissions matched',
+ result=[
+ {
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ {
+ 'dn': permission2_dn,
+ 'cn': [permission2],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ },
+ ],
+ ),
+ ),
+
+
+ # This tests setting truncated to True in the post_callback of
+ # permission_find(). The return order in LDAP is not guaranteed
+ # but in practice this is the first entry it finds. This is subject
+ # to change.
+ dict(
+ desc='Search for permissions by attr with a limit of 1 (truncated)',
+ command=('permission_find', [], dict(attrs=u'ipaenabledflag',
+ sizelimit=1)),
+ expected=dict(
+ count=1,
+ truncated=True,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': DN(('cn', 'Modify HBAC rule'),
+ api.env.container_permission, api.env.basedn),
+ 'cn': [u'Modify HBAC rule'],
+ 'member_privilege': [u'HBAC Administrator'],
+ 'memberindirect_role': [u'IT Security Specialist'],
+ 'permissions' : [u'write'],
+ 'attrs': [u'servicecategory', u'sourcehostcategory', u'cn', u'description', u'ipaenabledflag', u'accesstime', u'usercategory', u'hostcategory', u'accessruletype', u'sourcehost'],
+ 'subtree' : u'ldap:///%s' % DN(('ipauniqueid', '*'), ('cn', 'hbac'), api.env.basedn),
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % permission1,
+ command=(
+ 'permission_mod', [permission1], dict(
+ permissions=u'read',
+ memberof=u'ipausers',
+ setattr=u'owner=cn=other-test',
+ addattr=u'owner=cn=other-test2',
+ )
+ ),
+ expected=dict(
+ value=permission1,
+ summary=u'Modified permission "%s"' % permission1,
+ result=dict(
+ dn=permission1_dn,
+ cn=[permission1],
+ member_privilege=[privilege1],
+ type=u'user',
+ permissions=[u'read'],
+ memberof=u'ipausers',
+ owner=[u'cn=other-test', u'cn=other-test2'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % permission1,
+ command=('permission_show', [permission1], {}),
+ expected=dict(
+ value=permission1,
+ summary=None,
+ result={
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'read'],
+ 'memberof': u'ipausers',
+ },
+ ),
+ ),
+
+
+
+ dict(
+ desc='Try to rename %r to existing permission %r' % (permission1,
+ permission2),
+ command=(
+ 'permission_mod', [permission1], dict(rename=permission2,
+ permissions=u'all',)
+ ),
+ expected=errors.DuplicateEntry(),
+ ),
+
+
+ dict(
+ desc='Try to rename %r to empty name' % (permission1),
+ command=(
+ 'permission_mod', [permission1], dict(rename=u'',
+ permissions=u'all',)
+ ),
+ expected=errors.ValidationError(name='rename',
+ error=u'New name can not be empty'),
+ ),
+
+
+ dict(
+ desc='Check integrity of original permission %r' % permission1,
+ command=('permission_show', [permission1], {}),
+ expected=dict(
+ value=permission1,
+ summary=None,
+ result={
+ 'dn': permission1_dn,
+ 'cn': [permission1],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'read'],
+ 'memberof': u'ipausers',
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Rename %r to permission %r' % (permission1,
+ permission1_renamed),
+ command=(
+ 'permission_mod', [permission1], dict(rename=permission1_renamed,
+ permissions= u'all',)
+ ),
+ expected=dict(
+ value=permission1,
+ summary=u'Modified permission "%s"' % permission1,
+ result={
+ 'dn': permission1_renamed_dn,
+ 'cn': [permission1_renamed],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'all'],
+ 'memberof': u'ipausers',
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Rename %r to permission %r' % (permission1_renamed,
+ permission1_renamed_ucase),
+ command=(
+ 'permission_mod', [permission1_renamed], dict(rename=permission1_renamed_ucase,
+ permissions= u'write',)
+ ),
+ expected=dict(
+ value=permission1_renamed,
+ summary=u'Modified permission "%s"' % permission1_renamed,
+ result={
+ 'dn': permission1_renamed_ucase_dn,
+ 'cn': [permission1_renamed_ucase],
+ 'member_privilege': [privilege1],
+ 'type': u'user',
+ 'permissions': [u'write'],
+ 'memberof': u'ipausers',
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Change %r to a subtree type' % permission1_renamed_ucase,
+ command=(
+ 'permission_mod', [permission1_renamed_ucase],
+ dict(subtree=u'ldap:///%s' % DN(('cn', '*'), ('cn', 'test'), ('cn', 'accounts'), api.env.basedn),
+ type=None)
+ ),
+ expected=dict(
+ value=permission1_renamed_ucase,
+ summary=u'Modified permission "%s"' % permission1_renamed_ucase,
+ result=dict(
+ dn=permission1_renamed_ucase_dn,
+ cn=[permission1_renamed_ucase],
+ member_privilege=[privilege1],
+ subtree=u'ldap:///%s' % DN(('cn', '*'), ('cn', 'test'), ('cn', 'accounts'), api.env.basedn),
+ permissions=[u'write'],
+ memberof=u'ipausers',
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r using --subtree' % permission1,
+ command=('permission_find', [],
+ {'subtree': u'ldap:///%s' % DN(('cn', '*'), ('cn', 'test'), ('cn', 'accounts'), api.env.basedn)}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn':permission1_renamed_ucase_dn,
+ 'cn':[permission1_renamed_ucase],
+ 'member_privilege':[privilege1],
+ 'subtree':u'ldap:///%s' % DN(('cn', '*'), ('cn', 'test'), ('cn', 'accounts'), api.env.basedn),
+ 'permissions':[u'write'],
+ 'memberof':u'ipausers',
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search using nonexistent --subtree',
+ command=('permission_find', [], {'subtree': u'foo'}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 permissions matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Search using --targetgroup',
+ command=('permission_find', [], {'targetgroup': u'ipausers'}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 permission matched',
+ result=[
+ {
+ 'dn': DN(('cn','Add user to default group'),
+ api.env.container_permission, api.env.basedn),
+ 'cn': [u'Add user to default group'],
+ 'member_privilege': [u'User Administrators'],
+ 'attrs': [u'member'],
+ 'targetgroup': u'ipausers',
+ 'memberindirect_role': [u'User Administrator'],
+ 'permissions': [u'write']
+ }
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % permission1_renamed_ucase,
+ command=('permission_del', [permission1_renamed_ucase], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=permission1_renamed_ucase,
+ summary=u'Deleted permission "%s"' % permission1_renamed_ucase,
+ )
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % permission1,
+ command=('permission_del', [permission1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: permission not found' % permission1),
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent %r' % permission1,
+ command=('permission_show', [permission1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: permission not found' % permission1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % permission1,
+ command=('permission_mod', [permission1], dict(rename=u'Foo')),
+ expected=errors.NotFound(
+ reason=u'%s: permission not found' % permission1),
+ ),
+
+
+ dict(
+ desc='Delete %r' % permission2,
+ command=('permission_del', [permission2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=permission2,
+ summary=u'Deleted permission "%s"' % permission2,
+ )
+ ),
+
+
+ dict(
+ desc='Search for %r' % permission1,
+ command=('permission_find', [permission1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 permissions matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % privilege1,
+ command=('privilege_del', [privilege1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=privilege1,
+ summary=u'Deleted privilege "%s"' % privilege1,
+ )
+ ),
+
+ dict(
+ desc='Try to create permission %r with non-existing memberof' % permission1,
+ command=(
+ 'permission_add', [permission1], dict(
+ memberof=u'nonexisting',
+ permissions=u'write',
+ )
+ ),
+ expected=errors.NotFound(reason=u'nonexisting: group not found'),
+ ),
+
+ dict(
+ desc='Create memberof permission %r' % permission1,
+ command=(
+ 'permission_add', [permission1], dict(
+ memberof=u'editors',
+ permissions=u'write',
+ type=u'user',
+ )
+ ),
+ expected=dict(
+ value=permission1,
+ summary=u'Added permission "%s"' % permission1,
+ result=dict(
+ dn=permission1_dn,
+ cn=[permission1],
+ objectclass=objectclasses.permission,
+ memberof=u'editors',
+ permissions=[u'write'],
+ type=u'user',
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Try to update non-existent memberof of %r' % permission1,
+ command=('permission_mod', [permission1], dict(
+ memberof=u'nonexisting')),
+ expected=errors.NotFound(reason=u'nonexisting: group not found'),
+ ),
+
+ dict(
+ desc='Update memberof permission %r' % permission1,
+ command=(
+ 'permission_mod', [permission1], dict(
+ memberof=u'admins',
+ )
+ ),
+ expected=dict(
+ value=permission1,
+ summary=u'Modified permission "%s"' % permission1,
+ result=dict(
+ dn=permission1_dn,
+ cn=[permission1],
+ memberof=u'admins',
+ permissions=[u'write'],
+ type=u'user',
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Unset memberof of permission %r' % permission1,
+ command=(
+ 'permission_mod', [permission1], dict(
+ memberof=None,
+ )
+ ),
+ expected=dict(
+ summary=u'Modified permission "%s"' % permission1,
+ value=permission1,
+ result=dict(
+ dn=permission1_dn,
+ cn=[permission1],
+ permissions=[u'write'],
+ type=u'user',
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % permission1,
+ command=('permission_del', [permission1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=permission1,
+ summary=u'Deleted permission "%s"' % permission1,
+ )
+ ),
+
+
+ dict(
+ desc='Create targetgroup permission %r' % permission1,
+ command=(
+ 'permission_add', [permission1], dict(
+ targetgroup=u'editors',
+ permissions=u'write',
+ )
+ ),
+ expected=dict(
+ value=permission1,
+ summary=u'Added permission "%s"' % permission1,
+ result=dict(
+ dn=permission1_dn,
+ cn=[permission1],
+ objectclass=objectclasses.permission,
+ targetgroup=u'editors',
+ permissions=[u'write'],
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Try to create invalid %r' % invalid_permission1,
+ command=('permission_add', [invalid_permission1], dict(
+ type=u'user',
+ permissions=u'write',
+ )),
+ expected=errors.ValidationError(name='name',
+ error='May only contain letters, numbers, -, _, and space'),
+ ),
+
+ dict(
+ desc='Create %r' % permission3,
+ command=(
+ 'permission_add', [permission3], dict(
+ type=u'user',
+ permissions=u'write',
+ attrs=[u'cn']
+ )
+ ),
+ expected=dict(
+ value=permission3,
+ summary=u'Added permission "%s"' % permission3,
+ result=dict(
+ dn=permission3_dn,
+ cn=[permission3],
+ objectclass=objectclasses.permission,
+ type=u'user',
+ permissions=[u'write'],
+ attrs=(u'cn',),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Retrieve %r with --all --rights' % permission3,
+ command=('permission_show', [permission3], {'all' : True, 'rights' : True}),
+ expected=dict(
+ value=permission3,
+ summary=None,
+ result=dict(
+ dn=permission3_dn,
+ cn=[permission3],
+ objectclass=objectclasses.permission,
+ type=u'user',
+ attrs=(u'cn',),
+ permissions=[u'write'],
+ attributelevelrights=permission3_attributelevelrights
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Modify %r with --all -rights' % permission3,
+ command=('permission_mod', [permission3], {'all' : True, 'rights': True, 'attrs':[u'cn',u'uid']}),
+ expected=dict(
+ value=permission3,
+ summary=u'Modified permission "%s"' % permission3,
+ result=dict(
+ dn=permission3_dn,
+ cn=[permission3],
+ objectclass=objectclasses.permission,
+ type=u'user',
+ attrs=(u'cn',u'uid'),
+ permissions=[u'write'],
+ attributelevelrights=permission3_attributelevelrights,
+ ),
+ ),
+ ),
+ ]
diff --git a/ipatests/test_xmlrpc/test_ping_plugin.py b/ipatests/test_xmlrpc/test_ping_plugin.py
new file mode 100644
index 000000000..1d401993a
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_ping_plugin.py
@@ -0,0 +1,53 @@
+# Authors:
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+
+"""
+Test the `ipalib/plugins/ping.py` module, and XML-RPC in general.
+"""
+
+from ipalib import api, errors, messages, _
+from ipatests.util import Fuzzy
+from xmlrpc_test import Declarative
+from ipapython.version import API_VERSION
+
+
+class test_ping(Declarative):
+
+ tests = [
+ dict(
+ desc='Ping the server',
+ command=('ping', [], {}),
+ expected=dict(
+ summary=Fuzzy('IPA server version .*. API version .*')),
+ ),
+
+ dict(
+ desc='Try to ping with an argument',
+ command=('ping', ['bad_arg'], {}),
+ expected=errors.ZeroArgumentError(name='ping'),
+ ),
+
+ dict(
+ desc='Try to ping with an option',
+ command=('ping', [], dict(bad_arg=True)),
+ expected=errors.OptionError(_('Unknown option: %(option)s'),
+ option='bad_arg'),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_privilege_plugin.py b/ipatests/test_xmlrpc/test_privilege_plugin.py
new file mode 100644
index 000000000..741590dd0
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_privilege_plugin.py
@@ -0,0 +1,412 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib/plugins/privilege.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+permission1 = u'testperm'
+permission1_dn = DN(('cn',permission1),
+ api.env.container_permission,api.env.basedn)
+
+permission2 = u'testperm2'
+permission2_dn = DN(('cn',permission2),
+ api.env.container_permission,api.env.basedn)
+
+privilege1 = u'testpriv1'
+privilege1_dn = DN(('cn',privilege1),
+ api.env.container_privilege,api.env.basedn)
+
+
+class test_privilege(Declarative):
+
+ cleanup_commands = [
+ ('permission_del', [permission1], {}),
+ ('permission_del', [permission2], {}),
+ ('privilege_del', [privilege1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % privilege1,
+ command=('privilege_show', [privilege1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: privilege not found' % privilege1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % privilege1,
+ command=('privilege_mod', [privilege1], dict(description=u'Foo')),
+ expected=errors.NotFound(
+ reason=u'%s: privilege not found' % privilege1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % privilege1,
+ command=('privilege_del', [privilege1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: privilege not found' % privilege1),
+ ),
+
+
+ dict(
+ desc='Search for non-existent %r' % privilege1,
+ command=('privilege_find', [privilege1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 privileges matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % permission1,
+ command=(
+ 'permission_add', [permission1], dict(
+ type=u'user',
+ permissions=[u'add', u'delete'],
+ )
+ ),
+ expected=dict(
+ value=permission1,
+ summary=u'Added permission "%s"' % permission1,
+ result=dict(
+ dn=permission1_dn,
+ cn=[permission1],
+ objectclass=objectclasses.permission,
+ type=u'user',
+ permissions=[u'add', u'delete'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % privilege1,
+ command=('privilege_add', [privilege1],
+ dict(description=u'privilege desc. 1')
+ ),
+ expected=dict(
+ value=privilege1,
+ summary=u'Added privilege "%s"' % privilege1,
+ result=dict(
+ dn=privilege1_dn,
+ cn=[privilege1],
+ description=[u'privilege desc. 1'],
+ objectclass=objectclasses.privilege,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add permission %r to privilege %r' % (permission1, privilege1),
+ command=('privilege_add_permission', [privilege1],
+ dict(permission=permission1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ permission=[],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % privilege1,
+ command=('privilege_show', [privilege1], {}),
+ expected=dict(
+ value=privilege1,
+ summary=None,
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % privilege1,
+ command=('privilege_find', [privilege1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 privilege matched',
+ result=[
+ {
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % privilege1,
+ command=('privilege_find', [privilege1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 privilege matched',
+ result=[
+ {
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % permission2,
+ command=(
+ 'permission_add', [permission2], dict(
+ type=u'user',
+ permissions=u'write',
+ )
+ ),
+ expected=dict(
+ value=permission2,
+ summary=u'Added permission "%s"' % permission2,
+ result=dict(
+ dn=permission2_dn,
+ cn=[permission2],
+ objectclass=objectclasses.permission,
+ type=u'user',
+ permissions=[u'write'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add permission %r to privilege %r' % (permission2, privilege1),
+ command=('privilege_add_permission', [privilege1],
+ dict(permission=permission2)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ permission=[],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1, permission2],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Add permission %r to privilege %r again' % (permission2, privilege1),
+ command=('privilege_add_permission', [privilege1],
+ dict(permission=permission2)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ permission=[(u'testperm2', u'This entry is already a member'),],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1, permission2],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % privilege1,
+ command=('privilege_find', [privilege1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 privilege matched',
+ result=[
+ {
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'privilege desc. 1'],
+ 'memberof_permission': [permission1, permission2],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % privilege1,
+ command=(
+ 'privilege_mod', [privilege1], dict(description=u'New desc 1')
+ ),
+ expected=dict(
+ value=privilege1,
+ summary=u'Modified privilege "%s"' % privilege1,
+ result=dict(
+ cn=[privilege1],
+ description=[u'New desc 1'],
+ memberof_permission=[permission1, permission2],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Remove permission from %r' % privilege1,
+ command=('privilege_remove_permission', [privilege1],
+ dict(permission=permission1),
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ permission=[],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'New desc 1'],
+ 'memberof_permission': [permission2],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Remove permission from %r again' % privilege1,
+ command=('privilege_remove_permission', [privilege1],
+ dict(permission=permission1),
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ permission=[(u'testperm', u'This entry is not a member'),],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'New desc 1'],
+ 'memberof_permission': [permission2],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Add zero permissions to %r' % privilege1,
+ command=('privilege_add_permission', [privilege1],
+ dict(permission=None),
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ permission=[],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'New desc 1'],
+ 'memberof_permission': [permission2],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Remove zero permissions from %r' % privilege1,
+ command=('privilege_remove_permission', [privilege1],
+ dict(permission=None),
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ permission=[],
+ ),
+ ),
+ result={
+ 'dn': privilege1_dn,
+ 'cn': [privilege1],
+ 'description': [u'New desc 1'],
+ 'memberof_permission': [permission2],
+ 'objectclass': objectclasses.privilege,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % privilege1,
+ command=('privilege_del', [privilege1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=privilege1,
+ summary=u'Deleted privilege "%s"' % privilege1,
+ )
+ ),
+
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_pwpolicy_plugin.py b/ipatests/test_xmlrpc/test_pwpolicy_plugin.py
new file mode 100644
index 000000000..3b482ce2d
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_pwpolicy_plugin.py
@@ -0,0 +1,244 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+"""
+Test the `ipalib/plugins/pwpolicy.py` module.
+"""
+
+import sys
+from nose.tools import assert_raises # pylint: disable=E0611
+
+from xmlrpc_test import XMLRPC_test, assert_attr_equal
+from ipalib import api
+from ipalib import errors
+
+
+class test_pwpolicy(XMLRPC_test):
+ """
+ Test the `pwpolicy` plugin.
+ """
+ group = u'testgroup12'
+ group2 = u'testgroup22'
+ group3 = u'testgroup32'
+ user = u'testuser12'
+ kw = {'cospriority': 1, 'krbminpwdlife': 30, 'krbmaxpwdlife': 40, 'krbpwdhistorylength': 5, 'krbpwdminlength': 6 }
+ kw2 = {'cospriority': 2, 'krbminpwdlife': 40, 'krbmaxpwdlife': 60, 'krbpwdhistorylength': 8, 'krbpwdminlength': 9 }
+ kw3 = {'cospriority': 10, 'krbminpwdlife': 50, 'krbmaxpwdlife': 30, 'krbpwdhistorylength': 3, 'krbpwdminlength': 4 }
+ global_policy = u'global_policy'
+
+ def test_1_pwpolicy_add(self):
+ """
+ Test adding a per-group policy using the `xmlrpc.pwpolicy_add` method.
+ """
+ # First set up a group and user that will use this policy
+ self.failsafe_add(
+ api.Object.group, self.group, description=u'pwpolicy test group',
+ )
+ self.failsafe_add(
+ api.Object.user, self.user, givenname=u'Test', sn=u'User'
+ )
+ api.Command.group_add_member(self.group, user=self.user)
+
+ entry = api.Command['pwpolicy_add'](self.group, **self.kw)['result']
+ assert_attr_equal(entry, 'krbminpwdlife', '30')
+ assert_attr_equal(entry, 'krbmaxpwdlife', '40')
+ assert_attr_equal(entry, 'krbpwdhistorylength', '5')
+ assert_attr_equal(entry, 'krbpwdminlength', '6')
+ assert_attr_equal(entry, 'cospriority', '1')
+
+ def test_2_pwpolicy_add(self):
+ """
+ Add a policy with a already used priority.
+
+ The priority validation is done first, so it's OK that the group
+ is the same here.
+ """
+ try:
+ api.Command['pwpolicy_add'](self.group, **self.kw)
+ except errors.ValidationError:
+ pass
+ else:
+ assert False
+
+ def test_3_pwpolicy_add(self):
+ """
+ Add a policy that already exists.
+ """
+ try:
+ # cospriority needs to be unique
+ self.kw['cospriority'] = 3
+ api.Command['pwpolicy_add'](self.group, **self.kw)
+ except errors.DuplicateEntry:
+ pass
+ else:
+ assert False
+
+ def test_4_pwpolicy_add(self):
+ """
+ Test adding another per-group policy using the `xmlrpc.pwpolicy_add` method.
+ """
+ self.failsafe_add(
+ api.Object.group, self.group2, description=u'pwpolicy test group 2'
+ )
+ entry = api.Command['pwpolicy_add'](self.group2, **self.kw2)['result']
+ assert_attr_equal(entry, 'krbminpwdlife', '40')
+ assert_attr_equal(entry, 'krbmaxpwdlife', '60')
+ assert_attr_equal(entry, 'krbpwdhistorylength', '8')
+ assert_attr_equal(entry, 'krbpwdminlength', '9')
+ assert_attr_equal(entry, 'cospriority', '2')
+
+ def test_5_pwpolicy_add(self):
+ """
+ Add a pwpolicy for a non-existent group
+ """
+ try:
+ api.Command['pwpolicy_add'](u'nopwpolicy', cospriority=1, krbminpwdlife=1)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ def test_6_pwpolicy_show(self):
+ """
+ Test the `xmlrpc.pwpolicy_show` method with global policy.
+ """
+ entry = api.Command['pwpolicy_show']()['result']
+ # Note that this assumes an unchanged global policy
+ assert_attr_equal(entry, 'krbminpwdlife', '1')
+ assert_attr_equal(entry, 'krbmaxpwdlife', '90')
+ assert_attr_equal(entry, 'krbpwdhistorylength', '0')
+ assert_attr_equal(entry, 'krbpwdminlength', '8')
+
+ def test_7_pwpolicy_show(self):
+ """
+ Test the `xmlrpc.pwpolicy_show` method.
+ """
+ entry = api.Command['pwpolicy_show'](self.group)['result']
+ assert_attr_equal(entry, 'krbminpwdlife', '30')
+ assert_attr_equal(entry, 'krbmaxpwdlife', '40')
+ assert_attr_equal(entry, 'krbpwdhistorylength', '5')
+ assert_attr_equal(entry, 'krbpwdminlength', '6')
+ assert_attr_equal(entry, 'cospriority', '1')
+
+ def test_8_pwpolicy_mod(self):
+ """
+ Test the `xmlrpc.pwpolicy_mod` method for global policy.
+ """
+ entry = api.Command['pwpolicy_mod'](krbminpwdlife=50)['result']
+ assert_attr_equal(entry, 'krbminpwdlife', '50')
+
+ # Great, now change it back
+ entry = api.Command['pwpolicy_mod'](krbminpwdlife=1)['result']
+ assert_attr_equal(entry, 'krbminpwdlife', '1')
+
+ def test_9_pwpolicy_mod(self):
+ """
+ Test the `xmlrpc.pwpolicy_mod` method.
+ """
+ entry = api.Command['pwpolicy_mod'](self.group, krbminpwdlife=50)['result']
+ assert_attr_equal(entry, 'krbminpwdlife', '50')
+
+ def test_a_pwpolicy_managed(self):
+ """
+ Test adding password policy to a managed group.
+ """
+ try:
+ entry = api.Command['pwpolicy_add'](self.user, krbminpwdlife=50, cospriority=2)['result']
+ except errors.ManagedPolicyError:
+ pass
+ else:
+ assert False
+
+ def test_b_pwpolicy_add(self):
+ """
+ Test adding a third per-group policy using the `xmlrpc.pwpolicy_add` method.
+ """
+ self.failsafe_add(
+ api.Object.group, self.group3, description=u'pwpolicy test group 3'
+ )
+ entry = api.Command['pwpolicy_add'](self.group3, **self.kw3)['result']
+ assert_attr_equal(entry, 'krbminpwdlife', '50')
+ assert_attr_equal(entry, 'krbmaxpwdlife', '30')
+ assert_attr_equal(entry, 'krbpwdhistorylength', '3')
+ assert_attr_equal(entry, 'krbpwdminlength', '4')
+ assert_attr_equal(entry, 'cospriority', '10')
+
+ def test_c_pwpolicy_find(self):
+ """Test that password policies are sorted and reported properly"""
+ result = api.Command['pwpolicy_find']()['result']
+ assert len(result) == 4
+
+ # Test that policies are sorted in numerical order
+ assert result[0]['cn'] == (self.group,)
+ assert result[1]['cn'] == (self.group2,)
+ assert result[2]['cn'] == (self.group3,)
+ assert result[3]['cn'] == ('global_policy',)
+
+ # Test that returned values match the arguments
+ # Only test the second and third results; the first one was modified
+ for entry, expected in (result[1], self.kw2), (result[2], self.kw3):
+ for name, value in expected.iteritems():
+ assert_attr_equal(entry, name, str(value))
+
+ def test_c_pwpolicy_find_pkey_only(self):
+ """Test that password policies are sorted properly with --pkey-only"""
+ result = api.Command['pwpolicy_find'](pkey_only=True)['result']
+ assert len(result) == 4
+ assert result[0]['cn'] == (self.group,)
+ assert result[1]['cn'] == (self.group2,)
+ assert result[2]['cn'] == (self.group3,)
+ assert result[3]['cn'] == ('global_policy',)
+
+ def test_d_pwpolicy_show(self):
+ """Test that deleting a group removes its pwpolicy"""
+ api.Command['group_del'](self.group3)
+ with assert_raises(errors.NotFound):
+ api.Command['pwpolicy_show'](self.group3)
+
+ def test_e_pwpolicy_del(self):
+ """
+ Test the `xmlrpc.pwpolicy_del` method.
+ """
+ api.Command['pwpolicy_del'](self.group)
+ # Verify that it is gone
+ try:
+ api.Command['pwpolicy_show'](self.group)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ # Verify that global policy cannot be deleted
+ try:
+ api.Command['pwpolicy_del'](self.global_policy)
+ except errors.ValidationError:
+ pass
+ else:
+ assert False
+ try:
+ api.Command['pwpolicy_show'](self.global_policy)
+ except errors.NotFound:
+ assert False
+
+ # Remove the groups we created
+ api.Command['group_del'](self.group)
+ api.Command['group_del'](self.group2)
+
+ # Remove the user we created
+ api.Command['user_del'](self.user)
diff --git a/ipatests/test_xmlrpc/test_range_plugin.py b/ipatests/test_xmlrpc/test_range_plugin.py
new file mode 100644
index 000000000..cbb700e99
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_range_plugin.py
@@ -0,0 +1,536 @@
+# Authors:
+# Alexander Bokovoy <abokovoy@redhat.com>
+#
+# Copyright (C) 2012 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/>.
+
+"""
+Test the `ipalib/plugins/idrange.py` module, and XML-RPC in general.
+"""
+
+from ipalib import api, errors, _
+from ipatests.util import assert_equal, Fuzzy
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipatests.test_xmlrpc import objectclasses
+from ipapython.dn import *
+
+import ldap, ldap.sasl, ldap.modlist
+
+testrange1 = u'testrange1'
+testrange1_base_id = 900000
+testrange1_size = 99999
+testrange1_base_rid = 10000
+testrange1_secondary_base_rid = 200000
+
+testrange2 = u'testrange2'
+testrange2_base_id = 100
+testrange2_size = 50
+testrange2_base_rid = 100
+testrange2_secondary_base_rid = 1000
+
+testrange3 = u'testrange3'
+testrange3_base_id = 200
+testrange3_size = 50
+testrange3_base_rid = 70
+testrange3_secondary_base_rid = 1100
+
+testrange4 = u'testrange4'
+testrange4_base_id = 300
+testrange4_size = 50
+testrange4_base_rid = 200
+testrange4_secondary_base_rid = 1030
+
+testrange5 = u'testrange5'
+testrange5_base_id = 400
+testrange5_size = 50
+testrange5_base_rid = 1020
+testrange5_secondary_base_rid = 1200
+
+testrange6 = u'testrange6'
+testrange6_base_id = 130
+testrange6_size = 50
+testrange6_base_rid = 500
+testrange6_secondary_base_rid = 1300
+
+testrange7 = u'testrange7'
+testrange7_base_id = 600
+testrange7_size = 50
+testrange7_base_rid = 600
+testrange7_secondary_base_rid = 649
+
+testrange8 = u'testrange8'
+testrange8_base_id = 700
+testrange8_size = 50
+testrange8_base_rid = 700
+
+testrange9 = u'testrange9'
+testrange9_base_id = 800
+testrange9_size = 50
+testrange9_base_rid = 800
+
+testrange10 = u'testrange10'
+testrange10_base_id = 900
+testrange10_size = 50
+testrange10_base_rid = 900
+
+testrange9_dn = "cn={name},cn=ranges,cn=etc,{basedn}".format(name=testrange9,
+ basedn=api.env.basedn)
+
+testrange9_add = dict(
+ objectClass=["ipaIDrange", "ipatrustedaddomainrange"],
+ ipaBaseID="{base_id}".format(base_id=testrange9_base_id),
+ ipaBaseRID="{base_rid}".format(base_rid=testrange9_base_rid),
+ ipaIDRangeSize="{size}".format(size=testrange9_size),
+ ipaNTTrustedDomainSID="S-1-5-21-259319770-2312917334-591429603",
+ )
+
+testrange10_dn = "cn={name},cn=ranges,cn=etc,{basedn}".format(name=testrange10,
+ basedn=api.env.basedn)
+
+testrange10_add = dict(
+ objectClass=["ipaIDrange", "ipatrustedaddomainrange"],
+ ipaBaseID="{base_id}".format(base_id=testrange10_base_id),
+ ipaBaseRID="{base_rid}".format(base_rid=testrange10_base_rid),
+ ipaIDRangeSize="{size}".format(size=testrange10_size),
+ ipaNTTrustedDomainSID="S-1-5-21-2997650941-1802118864-3094776726",
+ )
+
+testtrust = u'testtrust'
+testtrust_dn = "cn=testtrust,cn=trusts,{basedn}".format(basedn=api.env.basedn)
+
+testtrust_add = dict(
+ objectClass=["ipaNTTrustedDomain", "ipaIDobject", "top"],
+ ipaNTFlatName='TESTTRUST',
+ ipaNTTrustedDomainSID='S-1-5-21-2997650941-1802118864-3094776726',
+ ipaNTSIDBlacklistIncoming='S-1-0',
+ ipaNTTrustPartner='testtrust.mock',
+ ipaNTTrustType='2',
+ ipaNTTrustDirection='3',
+ ipaNTTrustAttributes='8',
+ )
+
+user1 = u'tuser1'
+user1_uid = 900000
+group1 = u'group1'
+group1_gid = 900100
+
+
+class test_range(Declarative):
+
+ def __init__(self):
+ super(test_range, self).__init__()
+ self.connection = None
+
+ @classmethod
+ def connect_ldap(self):
+ self.connection = ldap.initialize('ldap://{host}'
+ .format(host=api.env.host))
+
+ auth = ldap.sasl.gssapi("")
+ self.connection.sasl_interactive_bind_s('', auth)
+
+ @classmethod
+ def add_entry(self, dn, mods):
+ ldif = ldap.modlist.addModlist(mods)
+ self.connection.add_s(dn, ldif)
+
+ @classmethod
+ def setUpClass(self):
+ super(test_range, self).setUpClass()
+
+ self.tearDownClass()
+
+ try:
+ self.connect_ldap()
+
+ self.add_entry(testrange9_dn, testrange9_add)
+ self.add_entry(testrange10_dn, testrange10_add)
+ self.add_entry(testtrust_dn, testtrust_add)
+
+ except ldap.ALREADY_EXISTS:
+ pass
+
+ finally:
+ if self.connection is not None:
+ self.connection.unbind_s()
+
+ @classmethod
+ def tearDownClass(self):
+
+ try:
+ self.connect_ldap()
+ self.connection.delete_s(testrange9_dn)
+ self.connection.delete_s(testrange10_dn)
+ self.connection.delete_s(testtrust_dn)
+
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ finally:
+ if self.connection is not None:
+ self.connection.unbind_s()
+
+ cleanup_commands = [
+ ('idrange_del', [testrange1, testrange2, testrange3, testrange4,
+ testrange5, testrange6, testrange7, testrange8],
+ {'continue': True}),
+ ('user_del', [user1], {}),
+ ('group_del', [group1], {}),
+ ]
+
+ tests = [
+ dict(
+ desc='Create ID range %r' % (testrange1),
+ command=('idrange_add', [testrange1],
+ dict(ipabaseid=testrange1_base_id, ipaidrangesize=testrange1_size,
+ ipabaserid=testrange1_base_rid, ipasecondarybaserid=testrange1_secondary_base_rid)),
+ expected=dict(
+ result=dict(
+ dn=DN(('cn',testrange1),('cn','ranges'),('cn','etc'),
+ api.env.basedn),
+ cn=[testrange1],
+ objectclass=[u'ipaIDrange', u'ipadomainidrange'],
+ ipabaseid=[unicode(testrange1_base_id)],
+ ipabaserid=[unicode(testrange1_base_rid)],
+ ipasecondarybaserid=[unicode(testrange1_secondary_base_rid)],
+ ipaidrangesize=[unicode(testrange1_size)],
+ iparangetype=[u'local domain range'],
+ ),
+ value=testrange1,
+ summary=u'Added ID range "%s"' % (testrange1),
+ ),
+ ),
+
+ dict(
+ desc='Retrieve ID range %r' % (testrange1),
+ command=('idrange_show', [testrange1], dict()),
+ expected=dict(
+ result=dict(
+ dn=DN(('cn',testrange1),('cn','ranges'),('cn','etc'),
+ api.env.basedn),
+ cn=[testrange1],
+ ipabaseid=[unicode(testrange1_base_id)],
+ ipabaserid=[unicode(testrange1_base_rid)],
+ ipasecondarybaserid=[unicode(testrange1_secondary_base_rid)],
+ ipaidrangesize=[unicode(testrange1_size)],
+ iparangetype=[u'local domain range'],
+ ),
+ value=testrange1,
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Create user %r in ID range %r' % (user1, testrange1),
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ uidnumber=user1_uid)
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[unicode(user1_uid)],
+ gidnumber=[unicode(user1_uid)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user1),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid',user1),('cn','users'),('cn','accounts'), api.env.basedn)
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create group %r in ID range %r' % (group1, testrange1),
+ command=(
+ 'group_add', [group1], dict(description=u'Test desc 1',
+ gidnumber=group1_gid)
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added group "%s"' % group1,
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc 1'],
+ gidnumber=[unicode(group1_gid)],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn',group1),('cn','groups'),('cn','accounts'), api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to modify ID range %r to get out bounds object #1' % (testrange1),
+ command=('idrange_mod', [testrange1], dict(ipabaseid=90001)),
+ expected=errors.ValidationError(name='ipabaseid,ipaidrangesize',
+ error=u'range modification leaving objects with ID out of the'
+ u' defined range is not allowed'),
+ ),
+
+
+ dict(
+ desc='Try to modify ID range %r to get out bounds object #2' % (testrange1),
+ command=('idrange_mod', [testrange1], dict(ipaidrangesize=100)),
+ expected=errors.ValidationError(name='ipabaseid,ipaidrangesize',
+ error=u'range modification leaving objects with ID out of the'
+ u' defined range is not allowed'),
+ ),
+
+
+ dict(
+ desc='Try to modify ID range %r to get out bounds object #3' % (testrange1),
+ command=('idrange_mod', [testrange1], dict(ipabaseid=100, ipaidrangesize=100)),
+ expected=errors.ValidationError(name='ipabaseid,ipaidrangesize',
+ error=u'range modification leaving objects with ID out of the'
+ u' defined range is not allowed'),
+ ),
+
+
+ dict(
+ desc='Modify ID range %r' % (testrange1),
+ command=('idrange_mod', [testrange1], dict(ipaidrangesize=90000)),
+ expected=dict(
+ result=dict(
+ cn=[testrange1],
+ ipabaseid=[unicode(testrange1_base_id)],
+ ipabaserid=[unicode(testrange1_base_rid)],
+ ipasecondarybaserid=[unicode(testrange1_secondary_base_rid)],
+ ipaidrangesize=[u'90000'],
+ iparangetype=[u'local domain range'],
+ ),
+ value=testrange1,
+ summary=u'Modified ID range "%s"' % (testrange1),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to delete ID range %r with active IDs inside it' % testrange1,
+ command=('idrange_del', [testrange1], {}),
+ expected=errors.ValidationError(name='ipabaseid,ipaidrangesize',
+ error=u'range modification leaving objects with ID out of the'
+ u' defined range is not allowed'),
+ ),
+
+
+ dict(
+ desc='Delete user %r' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=user1,
+ summary=u'Deleted user "%s"' % user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Delete group %r' % group1,
+ command=('group_del', [group1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=group1,
+ summary=u'Deleted group "%s"' % group1,
+ ),
+ ),
+
+
+ dict(
+ desc='Delete ID range %r' % testrange1,
+ command=('idrange_del', [testrange1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=testrange1,
+ summary=u'Deleted ID range "%s"' % testrange1,
+ ),
+ ),
+
+ dict(
+ desc='Create ID range %r' % (testrange2),
+ command=('idrange_add', [testrange2],
+ dict(ipabaseid=testrange2_base_id,
+ ipaidrangesize=testrange2_size,
+ ipabaserid=testrange2_base_rid,
+ ipasecondarybaserid=testrange2_secondary_base_rid)),
+ expected=dict(
+ result=dict(
+ dn=DN(('cn',testrange2),('cn','ranges'),('cn','etc'),
+ api.env.basedn),
+ cn=[testrange2],
+ objectclass=[u'ipaIDrange', u'ipadomainidrange'],
+ ipabaseid=[unicode(testrange2_base_id)],
+ ipabaserid=[unicode(testrange2_base_rid)],
+ ipasecondarybaserid=[unicode(testrange2_secondary_base_rid)],
+ ipaidrangesize=[unicode(testrange2_size)],
+ iparangetype=[u'local domain range'],
+ ),
+ value=testrange2,
+ summary=u'Added ID range "%s"' % (testrange2),
+ ),
+ ),
+
+ dict(
+ desc='Try to modify ID range %r so that its rid ranges are overlapping themselves' % (testrange2),
+ command=('idrange_mod', [testrange2],
+ dict(ipabaserid=(testrange2_base_rid*10))),
+ expected=errors.ValidationError(
+ name='ID Range setup', error='Primary RID range and secondary RID range cannot overlap'),
+ ),
+
+ dict(
+ desc='Try to create ID range %r with overlapping rid range' % (testrange3),
+ command=('idrange_add', [testrange3],
+ dict(ipabaseid=testrange3_base_id,
+ ipaidrangesize=testrange3_size,
+ ipabaserid=testrange3_base_rid,
+ ipasecondarybaserid=testrange3_secondary_base_rid)),
+ expected=errors.DatabaseError(
+ desc='Constraint violation', info='New primary rid range overlaps with existing primary rid range.'),
+ ),
+
+ dict(
+ desc='Try to create ID range %r with overlapping secondary rid range' % (testrange4),
+ command=('idrange_add', [testrange4],
+ dict(ipabaseid=testrange4_base_id,
+ ipaidrangesize=testrange4_size,
+ ipabaserid=testrange4_base_rid,
+ ipasecondarybaserid=testrange4_secondary_base_rid)),
+ expected=errors.DatabaseError(
+ desc='Constraint violation', info='New secondary rid range overlaps with existing secondary rid range.'),
+ ),
+
+ dict(
+ desc='Try to create ID range %r with primary range overlapping secondary rid range' % (testrange5),
+ command=('idrange_add', [testrange5],
+ dict(ipabaseid=testrange5_base_id,
+ ipaidrangesize=testrange5_size,
+ ipabaserid=testrange5_base_rid,
+ ipasecondarybaserid=testrange5_secondary_base_rid)),
+ expected=errors.DatabaseError(
+ desc='Constraint violation', info='New primary rid range overlaps with existing secondary rid range.'),
+ ),
+
+ dict(
+ desc='Try to create ID range %r with overlapping id range' % (testrange6),
+ command=('idrange_add', [testrange6],
+ dict(ipabaseid=testrange6_base_id,
+ ipaidrangesize=testrange6_size,
+ ipabaserid=testrange6_base_rid,
+ ipasecondarybaserid=testrange6_secondary_base_rid)),
+ expected=errors.DatabaseError(
+ desc='Constraint violation', info='New base range overlaps with existing base range.'),
+ ),
+
+ dict(
+ desc='Try to create ID range %r with rid ranges overlapping themselves' % (testrange7),
+ command=('idrange_add', [testrange7],
+ dict(ipabaseid=testrange7_base_id,
+ ipaidrangesize=testrange7_size,
+ ipabaserid=testrange7_base_rid,
+ ipasecondarybaserid=testrange7_secondary_base_rid)),
+ expected=errors.ValidationError(
+ name='ID Range setup', error='Primary RID range and secondary RID range cannot overlap'),
+ ),
+
+ dict(
+ desc='Delete ID range %r' % testrange2,
+ command=('idrange_del', [testrange2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=testrange2,
+ summary=u'Deleted ID range "%s"' % testrange2,
+ ),
+ ),
+
+ dict(
+ desc='Create ID range %r' % (testrange8),
+ command=('idrange_add', [testrange8],
+ dict(ipabaseid=testrange8_base_id,
+ ipaidrangesize=testrange8_size)),
+ expected=dict(
+ result=dict(
+ dn=DN(('cn',testrange8),('cn','ranges'),('cn','etc'),
+ api.env.basedn),
+ cn=[testrange8],
+ objectclass=[u'ipaIDrange', u'ipadomainidrange'],
+ ipabaseid=[unicode(testrange8_base_id)],
+ ipaidrangesize=[unicode(testrange8_size)],
+ iparangetype=[u'local domain range'],
+ ),
+ value=testrange8,
+ summary=u'Added ID range "%s"' % (testrange8),
+ ),
+ ),
+
+ dict(
+ desc='Try to modify ID range %r so it has only primary rid range set' % (testrange8),
+ command=('idrange_mod', [testrange8],
+ dict(ipabaserid=testrange8_base_rid)),
+ expected=errors.ValidationError(
+ name='ID Range setup', error='Options secondary-rid-base and rid-base must be used together'),
+ ),
+
+ dict(
+ desc='Delete ID range %r' % testrange8,
+ command=('idrange_del', [testrange8], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=testrange8,
+ summary=u'Deleted ID range "%s"' % testrange8,
+ ),
+ ),
+
+ dict(
+ desc='Delete non-active AD trusted range %r' % testrange9,
+ command=('idrange_del', [testrange9], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=testrange9,
+ summary=u'Deleted ID range "%s"' % testrange9,
+ ),
+ ),
+
+ dict(
+ desc='Try to delete active AD trusted range %r' % testrange10,
+ command=('idrange_del', [testrange10], {}),
+ expected=errors.DependentEntry(
+ label='Active Trust',
+ key=testrange10,
+ dependent=testtrust),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_realmdomains_plugin.py b/ipatests/test_xmlrpc/test_realmdomains_plugin.py
new file mode 100644
index 000000000..8abb53e48
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_realmdomains_plugin.py
@@ -0,0 +1,164 @@
+# Authors:
+# Ana Krivokapic <akrivoka@redhat.com>
+#
+# Copyright (C) 2013 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/>.
+"""
+Test the `ipalib/plugins/realmdomains.py` module.
+"""
+
+from ipalib import api, errors
+from ipapython.dn import DN
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative
+
+
+cn = u'Realm Domains'
+dn = DN(('cn', cn), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
+our_domain = api.env.domain
+new_domain_1 = u'example1.com'
+new_domain_2 = u'example2.com'
+bad_domain = u'doesnotexist.test'
+
+
+class test_realmdomains(Declarative):
+
+ cleanup_commands = [
+ ('realmdomains_mod', [], {'associateddomain': [our_domain]}),
+ ]
+
+ tests = [
+ dict(
+ desc='Retrieve realm domains',
+ command=('realmdomains_show', [], {}),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ dn=dn,
+ associateddomain=[our_domain],
+ ),
+ ),
+ ),
+ dict(
+ desc='Retrieve realm domains - print all attributes',
+ command=('realmdomains_show', [], {'all': True}),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ dn=dn,
+ associateddomain=[our_domain],
+ cn=[cn],
+ objectclass=objectclasses.realmdomains,
+ ),
+ ),
+ ),
+ dict(
+ desc='Replace list of realm domains with "%s"' % [our_domain, new_domain_1],
+ command=('realmdomains_mod', [], {'associateddomain': [our_domain, new_domain_1]}),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ associateddomain=[our_domain, new_domain_1],
+ ),
+ ),
+ ),
+ dict(
+ desc='Add domain "%s" to list' % new_domain_2,
+ command=('realmdomains_mod', [], {'add_domain': new_domain_2}),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ associateddomain=[our_domain, new_domain_1, new_domain_2],
+ ),
+ ),
+ ),
+ dict(
+ desc='Delete domain "%s" from list' % new_domain_2,
+ command=('realmdomains_mod', [], {'del_domain': new_domain_2}),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ associateddomain=[our_domain, new_domain_1],
+ ),
+ ),
+ ),
+ dict(
+ desc='Add domain "%s" and delete domain "%s"' % (new_domain_2, new_domain_1),
+ command=('realmdomains_mod', [], {'add_domain': new_domain_2, 'del_domain': new_domain_1}),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ associateddomain=[our_domain, new_domain_2],
+ ),
+ ),
+ ),
+ dict(
+ desc='Try to specify --domain and --add-domain options together',
+ command=('realmdomains_mod', [], {
+ 'associateddomain': [our_domain, new_domain_1],
+ 'add_domain': new_domain_1,
+ }),
+ expected=errors.MutuallyExclusiveError(
+ reason='you cannot specify the --domain option together with --add-domain or --del-domain'),
+ ),
+ dict(
+ desc='Try to replace list of realm domains with a list without our domain',
+ command=('realmdomains_mod', [], {'associateddomain': [new_domain_1]}),
+ expected=errors.ValidationError(
+ name='domain', error='cannot delete domain of IPA server'),
+ ),
+ dict(
+ desc='Try to replace list of realm domains with a list with an invalid domain "%s"' % bad_domain,
+ command=('realmdomains_mod', [], {'associateddomain': [our_domain, bad_domain]}),
+ expected=errors.ValidationError(
+ name='domain', error='no SOA or NS records found for domains: %s' % bad_domain),
+ ),
+ dict(
+ desc='Try to add an invalid domain "%s"' % bad_domain,
+ command=('realmdomains_mod', [], {'add_domain': bad_domain}),
+ expected=errors.ValidationError(
+ name='add_domain', error='no SOA or NS records found for domain %s' % bad_domain),
+ ),
+ dict(
+ desc='Try to delete our domain',
+ command=('realmdomains_mod', [], {'del_domain': our_domain}),
+ expected=errors.ValidationError(
+ name='del_domain', error='cannot delete domain of IPA server'),
+ ),
+ dict(
+ desc='Try to delete domain which is not in list',
+ command=('realmdomains_mod', [], {'del_domain': new_domain_1}),
+ expected=errors.AttrValueNotFound(
+ attr='associateddomain', value=new_domain_1),
+ ),
+ dict(
+ desc='Add an invalid domain "%s" with --force option' % bad_domain,
+ command=('realmdomains_mod', [], {'add_domain': bad_domain, 'force': True}),
+ expected=dict(
+ value=u'',
+ summary=None,
+ result=dict(
+ associateddomain=[our_domain, new_domain_2, bad_domain],
+ ),
+ ),
+ ),
+ ]
diff --git a/ipatests/test_xmlrpc/test_replace.py b/ipatests/test_xmlrpc/test_replace.py
new file mode 100644
index 000000000..281714b3e
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_replace.py
@@ -0,0 +1,236 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2011 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/>.
+
+"""
+Test the modlist replace logic. Some attributes require a MOD_REPLACE
+while others are fine using ADD/DELETE.
+
+Note that member management in other tests also exercises the
+gen_modlist code.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+user1=u'tuser1'
+
+
+class test_replace(Declarative):
+
+ cleanup_commands = [
+ ('user_del', [user1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Create %r with 2 e-mail accounts' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ mail=[u'test1@example.com', u'test2@example.com'])
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "tuser1"',
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ mail=[u'test1@example.com', u'test2@example.com'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),('cn','kerberos'),
+ api.env.basedn)],
+ mepmanagedentry=[DN(('cn',user1),('cn','groups'),('cn','accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=DN(('uid','tuser1'),('cn','users'),('cn','accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Drop one e-mail account, add another to %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(mail=[u'test1@example.com', u'test3@example.com'])
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test1@example.com', u'test3@example.com'],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Set mail to a new single value %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(mail=u'test4@example.com')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test4@example.com'],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Set mail to three new values %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(mail=[u'test5@example.com', u'test6@example.com', u'test7@example.com'])
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'test6@example.com', u'test7@example.com', u'test5@example.com'],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Remove all mail values %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(mail=u'')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Ensure single-value mods work too, replace initials %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(initials=u'ABC')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ initials=[u'ABC'],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Drop a single-value attribute %r' % user1,
+ command=(
+ 'user_mod', [user1], dict(initials=u'')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "tuser1"',
+ value=user1,
+ ),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_role_plugin.py b/ipatests/test_xmlrpc/test_role_plugin.py
new file mode 100644
index 000000000..119bfb1a8
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_role_plugin.py
@@ -0,0 +1,625 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+# John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2009 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/>.
+"""
+Test the `ipalib/plugins/role.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+
+search = u'test-role'
+
+role1 = u'test-role-1'
+role1_dn = DN(('cn',role1),api.env.container_rolegroup,
+ api.env.basedn)
+renamedrole1 = u'test-role'
+invalidrole1 = u' whitespace '
+
+role2 = u'test-role-2'
+role2_dn = DN(('cn',role2),api.env.container_rolegroup,
+ api.env.basedn)
+
+group1 = u'testgroup1'
+group1_dn = DN(('cn',group1),api.env.container_group,
+ api.env.basedn)
+
+privilege1 = u'r,w privilege 1'
+privilege1_dn = DN(('cn', privilege1), DN(api.env.container_privilege),
+ api.env.basedn)
+
+class test_role(Declarative):
+
+ cleanup_commands = [
+ ('role_del', [role1], {}),
+ ('role_del', [role2], {}),
+ ('group_del', [group1], {}),
+ ('privilege_del', [privilege1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % role1,
+ command=('role_show', [role1], {}),
+ expected=errors.NotFound(reason=u'%s: role not found' % role1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % role1,
+ command=('role_mod', [role1], dict(description=u'Foo')),
+ expected=errors.NotFound(reason=u'%s: role not found' % role1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % role1,
+ command=('role_del', [role1], {}),
+ expected=errors.NotFound(reason=u'%s: role not found' % role1),
+ ),
+
+
+ dict(
+ desc='Try to rename non-existent %r' % role1,
+ command=('role_mod', [role1], dict(setattr=u'cn=%s' % renamedrole1)),
+ expected=errors.NotFound(reason=u'%s: role not found' % role1),
+ ),
+
+
+ dict(
+ desc='Search for non-existent %r' % role1,
+ command=('role_find', [role1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 roles matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Create invalid %r' % invalidrole1,
+ command=('role_add', [invalidrole1],
+ dict(description=u'role desc 1')
+ ),
+ expected=errors.ValidationError(name='name',
+ error=u'Leading and trailing spaces are not allowed'),
+ ),
+
+
+ dict(
+ desc='Create %r' % role1,
+ command=('role_add', [role1],
+ dict(description=u'role desc 1')
+ ),
+ expected=dict(
+ value=role1,
+ summary=u'Added role "%s"' % role1,
+ result=dict(
+ dn=role1_dn,
+ cn=[role1],
+ description=[u'role desc 1'],
+ objectclass=objectclasses.role,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % role1,
+ command=('role_show', [role1], {}),
+ expected=dict(
+ value=role1,
+ summary=None,
+ result=dict(
+ dn=role1_dn,
+ cn=[role1],
+ description=[u'role desc 1'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % group1,
+ command=(
+ 'group_add', [group1], dict(description=u'group desc 1',
+ nonposix=True,)
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added group "testgroup1"',
+ result=dict(
+ dn=group1_dn,
+ cn=[group1],
+ description=[u'group desc 1'],
+ objectclass=objectclasses.group,
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % privilege1,
+ command=('privilege_add', [privilege1],
+ dict(description=u'privilege desc. 1')
+ ),
+ expected=dict(
+ value=privilege1,
+ summary=u'Added privilege "%s"' % privilege1,
+ result=dict(
+ dn=privilege1_dn,
+ cn=[privilege1],
+ description=[u'privilege desc. 1'],
+ objectclass=objectclasses.privilege,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add privilege %r to role %r' % (privilege1, role1),
+ command=('role_add_privilege', [role1],
+ dict(privilege=privilege1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ privilege=[],
+ ),
+ ),
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'memberof_privilege': [privilege1],
+ 'objectclass': objectclasses.role,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Add zero privileges to role %r' % role1,
+ command=('role_add_privilege', [role1], dict(privilege=None)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ privilege=[],
+ ),
+ ),
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'memberof_privilege': [privilege1],
+ 'objectclass': objectclasses.role,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Remove zero privileges from role %r' % role1,
+ command=('role_remove_privilege', [role1], dict(privilege=None)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ privilege=[],
+ ),
+ ),
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'memberof_privilege': [privilege1],
+ 'objectclass': objectclasses.role,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Add member %r to %r' % (group1, role1),
+ command=('role_add_member', [role1], dict(group=group1)),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ user=[],
+ group=[],
+ host=[],
+ hostgroup=[],
+ ),
+ ),
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'member_group': [group1],
+ 'memberof_privilege': [privilege1],
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify member-add' % role1,
+ command=('role_show', [role1], {}),
+ expected=dict(
+ value=role1,
+ summary=None,
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'member_group': [group1],
+ 'memberof_privilege': [privilege1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % role1,
+ command=('role_find', [role1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 role matched',
+ result=[
+ {
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'member_group': [group1],
+ 'memberof_privilege': [privilege1],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % search,
+ command=('role_find', [search], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 role matched',
+ result=[
+ {
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'member_group': [group1],
+ 'memberof_privilege': [privilege1],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % role2,
+ command=('role_add', [role2],
+ dict(description=u'role desc 2')
+ ),
+ expected=dict(
+ value=role2,
+ summary=u'Added role "%s"' % role2,
+ result=dict(
+ dn=role2_dn,
+ cn=[role2],
+ description=[u'role desc 2'],
+ objectclass=objectclasses.role,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % role1,
+ command=('role_find', [role1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 role matched',
+ result=[
+ {
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'member_group': [group1],
+ 'memberof_privilege': [privilege1],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % search,
+ command=('role_find', [search], {}),
+ expected=dict(
+ count=2,
+ truncated=False,
+ summary=u'2 roles matched',
+ result=[
+ {
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'role desc 1'],
+ 'member_group': [group1],
+ 'memberof_privilege': [privilege1],
+ },
+ {
+ 'dn': role2_dn,
+ 'cn': [role2],
+ 'description': [u'role desc 2'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % role1,
+ command=(
+ 'role_mod', [role1], dict(description=u'New desc 1')
+ ),
+ expected=dict(
+ value=role1,
+ summary=u'Modified role "%s"' % role1,
+ result=dict(
+ cn=[role1],
+ description=[u'New desc 1'],
+ member_group=[group1],
+ memberof_privilege=[privilege1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % role1,
+ command=('role_show', [role1], {}),
+ expected=dict(
+ value=role1,
+ summary=None,
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'New desc 1'],
+ 'member_group': [group1],
+ 'memberof_privilege': [privilege1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Remove member %r from %r' % (group1, role1),
+ command=('role_remove_member', [role1], dict(group=group1)),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ user=[],
+ group=[],
+ host=[],
+ hostgroup=[],
+ ),
+ ),
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'New desc 1'],
+ 'memberof_privilege': [privilege1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify member-del' % role1,
+ command=('role_show', [role1], {}),
+ expected=dict(
+ value=role1,
+ summary=None,
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'New desc 1'],
+ 'memberof_privilege': [privilege1],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % group1,
+ command=('group_del', [group1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=group1,
+ summary=u'Deleted group "testgroup1"',
+ )
+ ),
+
+
+ dict(
+ desc='Rename %r' % role1,
+ command=('role_mod', [role1], dict(setattr=u'cn=%s' % renamedrole1)),
+ expected=dict(
+ value=role1,
+ result=dict(
+ cn=[renamedrole1],
+ description=[u'New desc 1'],
+ memberof_privilege=[privilege1],
+ ),
+ summary=u'Modified role "%s"' % role1
+ )
+ ),
+
+
+ dict(
+ desc='Rename %r back' % renamedrole1,
+ command=('role_mod', [renamedrole1], dict(setattr=u'cn=%s' % role1)),
+ expected=dict(
+ value=renamedrole1,
+ result=dict(
+ cn=[role1],
+ description=[u'New desc 1'],
+ memberof_privilege=[privilege1],
+ ),
+ summary=u'Modified role "%s"' % renamedrole1
+ )
+ ),
+
+
+ dict(
+ desc='Remove privilege %r from role %r' % (privilege1, role1),
+ command=('role_remove_privilege', [role1],
+ dict(privilege=privilege1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ privilege=[],
+ ),
+ ),
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'New desc 1'],
+ 'objectclass': objectclasses.role,
+ }
+ ),
+ ),
+
+
+ dict(
+ desc='Remove privilege %r from role %r again' % (privilege1, role1),
+ command=('role_remove_privilege', [role1],
+ dict(privilege=privilege1)
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ privilege=[(u'%s' % privilege1, u'This entry is not a member'),],
+ ),
+ ),
+ result={
+ 'dn': role1_dn,
+ 'cn': [role1],
+ 'description': [u'New desc 1'],
+ 'objectclass': objectclasses.role,
+ }
+ ),
+ ),
+
+
+
+ dict(
+ desc='Delete %r' % role1,
+ command=('role_del', [role1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=role1,
+ summary=u'Deleted role "%s"' % role1,
+ )
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % role1,
+ command=('role_del', [role1], {}),
+ expected=errors.NotFound(reason=u'%s: role not found' % role1),
+ ),
+
+
+ dict(
+ desc='Try to show non-existent %r' % role1,
+ command=('role_show', [role1], {}),
+ expected=errors.NotFound(reason=u'%s: role not found' % role1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % role1,
+ command=('role_mod', [role1], dict(description=u'Foo')),
+ expected=errors.NotFound(reason=u'%s: role not found' % role1),
+ ),
+
+
+ dict(
+ desc='Search for %r' % search,
+ command=('role_find', [search], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 role matched',
+ result=[
+ {
+ 'dn': role2_dn,
+ 'cn': [role2],
+ 'description': [u'role desc 2'],
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % role2,
+ command=('role_del', [role2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=role2,
+ summary=u'Deleted role "%s"' % role2,
+ )
+ ),
+
+
+ dict(
+ desc='Search for %r' % search,
+ command=('role_find', [search], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 roles matched',
+ result=[],
+ ),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_selfservice_plugin.py b/ipatests/test_xmlrpc/test_selfservice_plugin.py
new file mode 100644
index 000000000..c78edbc22
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_selfservice_plugin.py
@@ -0,0 +1,290 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib/plugins/selfservice.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+
+selfservice1 = u'testself'
+invalid_selfservice1 = u'bad+name'
+
+class test_selfservice(Declarative):
+
+ cleanup_commands = [
+ ('selfservice_del', [selfservice1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % selfservice1,
+ command=('selfservice_show', [selfservice1], {}),
+ expected=errors.NotFound(
+ reason=u'ACI with name "%s" not found' % selfservice1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % selfservice1,
+ command=('selfservice_mod', [selfservice1],
+ dict(permissions=u'write')),
+ expected=errors.NotFound(
+ reason=u'ACI with name "%s" not found' % selfservice1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % selfservice1,
+ command=('selfservice_del', [selfservice1], {}),
+ expected=errors.NotFound(
+ reason=u'ACI with name "%s" not found' % selfservice1),
+ ),
+
+
+ dict(
+ desc='Search for non-existent %r' % selfservice1,
+ command=('selfservice_find', [selfservice1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 selfservices matched',
+ result=[],
+ ),
+ ),
+
+
+ # Note that we add postalCode but expect postalcode. This tests
+ # the attrs normalizer.
+ dict(
+ desc='Create %r' % selfservice1,
+ command=(
+ 'selfservice_add', [selfservice1], dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalcode'],
+ permissions=u'write',
+ )
+ ),
+ expected=dict(
+ value=selfservice1,
+ summary=u'Added selfservice "%s"' % selfservice1,
+ result=dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalcode'],
+ permissions=[u'write'],
+ selfaci=True,
+ aciname=selfservice1,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % selfservice1,
+ command=(
+ 'selfservice_add', [selfservice1], dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalcode'],
+ permissions=u'write',
+ ),
+ ),
+ expected=errors.DuplicateEntry(),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % selfservice1,
+ command=('selfservice_show', [selfservice1], {}),
+ expected=dict(
+ value=selfservice1,
+ summary=None,
+ result={
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'write'],
+ 'selfaci': True,
+ 'aciname': selfservice1,
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r with --raw' % selfservice1,
+ command=('selfservice_show', [selfservice1], {'raw':True}),
+ expected=dict(
+ value=selfservice1,
+ summary=None,
+ result={
+ 'aci': u'(targetattr = "street || c || l || st || postalcode")(version 3.0;acl "selfservice:testself";allow (write) userdn = "ldap:///self";)',
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % selfservice1,
+ command=('selfservice_find', [selfservice1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 selfservice matched',
+ result=[
+ {
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'write'],
+ 'selfaci': True,
+ 'aciname': selfservice1,
+ },
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Search for %r with --pkey-only' % selfservice1,
+ command=('selfservice_find', [selfservice1], {'pkey_only' : True}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 selfservice matched',
+ result=[
+ {
+ 'aciname': selfservice1,
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with empty attrs and permissions' % selfservice1,
+ command=('selfservice_find', [selfservice1], {'attrs' : None, 'permissions' : None}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 selfservice matched',
+ result=[
+ {
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'write'],
+ 'selfaci': True,
+ 'aciname': selfservice1,
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with --raw' % selfservice1,
+ command=('selfservice_find', [selfservice1], {'raw':True}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 selfservice matched',
+ result=[
+ {
+ 'aci': u'(targetattr = "street || c || l || st || postalcode")(version 3.0;acl "selfservice:testself";allow (write) userdn = "ldap:///self";)'
+ },
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % selfservice1,
+ command=(
+ 'selfservice_mod', [selfservice1], dict(permissions=u'read')
+ ),
+ expected=dict(
+ value=selfservice1,
+ summary=u'Modified selfservice "%s"' % selfservice1,
+ result=dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalcode'],
+ permissions=[u'read'],
+ selfaci=True,
+ aciname=selfservice1,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % selfservice1,
+ command=('selfservice_show', [selfservice1], {}),
+ expected=dict(
+ value=selfservice1,
+ summary=None,
+ result={
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'read'],
+ 'selfaci': True,
+ 'aciname': selfservice1,
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Try to update %r with empty permissions' % selfservice1,
+ command=(
+ 'selfservice_mod', [selfservice1], dict(permissions=None)
+ ),
+ expected=errors.RequirementError(name='permissions'),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify invalid update' % selfservice1,
+ command=('selfservice_show', [selfservice1], {}),
+ expected=dict(
+ value=selfservice1,
+ summary=None,
+ result={
+ 'attrs': [u'street', u'c', u'l', u'st', u'postalcode'],
+ 'permissions': [u'read'],
+ 'selfaci': True,
+ 'aciname': selfservice1,
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % selfservice1,
+ command=('selfservice_del', [selfservice1], {}),
+ expected=dict(
+ result=True,
+ value=selfservice1,
+ summary=u'Deleted selfservice "%s"' % selfservice1,
+ )
+ ),
+
+ dict(
+ desc='Create invalid %r' % invalid_selfservice1,
+ command=(
+ 'selfservice_add', [invalid_selfservice1], dict(
+ attrs=[u'street', u'c', u'l', u'st', u'postalcode'],
+ permissions=u'write',
+ )
+ ),
+ expected=errors.ValidationError(name='name',
+ error='May only contain letters, numbers, -, _, and space'),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_selinuxusermap_plugin.py b/ipatests/test_xmlrpc/test_selinuxusermap_plugin.py
new file mode 100644
index 000000000..5bfe5475c
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_selinuxusermap_plugin.py
@@ -0,0 +1,934 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2011 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/>.
+"""
+Test the `ipalib/plugins/selinuxusermap.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid
+from ipapython.dn import DN
+from ipatests.util import Fuzzy
+
+rule1 = u'selinuxrule1'
+selinuxuser1 = u'guest_u:s0'
+selinuxuser2 = u'xguest_u:s0'
+
+user1 = u'tuser1'
+group1 = u'testgroup1'
+host1 = u'testhost1.%s' % api.env.domain
+hostdn1 = DN(('fqdn', host1), ('cn', 'computers'), ('cn', 'accounts'),
+ api.env.basedn)
+hbacrule1 = u'testhbacrule1'
+hbacrule2 = u'testhbacrule12'
+
+# Note (?i) at the beginning of the regexp is the ingnore case flag
+fuzzy_selinuxusermapdn = Fuzzy(
+ '(?i)ipauniqueid=[0-9a-f]{8}-[0-9a-f]{4}'
+ '-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12},%s,%s'
+ % (api.env.container_selinux, api.env.basedn)
+)
+fuzzy_hbacruledn = Fuzzy(
+ '(?i)ipauniqueid=[0-9a-f]{8}-[0-9a-f]{4}'
+ '-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12},%s,%s'
+ % (api.env.container_hbac, api.env.basedn)
+)
+
+allow_all_rule_dn = api.Command['hbacrule_show'](u'allow_all')['result']['dn']
+
+
+class test_selinuxusermap(Declarative):
+ cleanup_commands = [
+ ('selinuxusermap_del', [rule1], {}),
+ ('group_del', [group1], {}),
+ ('user_del', [user1], {}),
+ ('host_del', [host1], {}),
+ ('hbacrule_del', [hbacrule1], {}),
+ ('hbacrule_del', [hbacrule2], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % rule1,
+ command=('selinuxusermap_show', [rule1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: SELinux User Map rule not found' % rule1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % rule1,
+ command=('selinuxusermap_mod', [rule1], dict(description=u'Foo')),
+ expected=errors.NotFound(
+ reason=u'%s: SELinux User Map rule not found' % rule1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % rule1,
+ command=('selinuxusermap_del', [rule1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: SELinux User Map rule not found' % rule1),
+ ),
+
+
+ dict(
+ desc='Create rule %r' % rule1,
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1)
+ ),
+ expected=dict(
+ value=rule1,
+ summary=u'Added SELinux User Map "%s"' % rule1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser1],
+ objectclass=objectclasses.selinuxusermap,
+ ipauniqueid=[fuzzy_uuid],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % rule1,
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1)
+ ),
+ expected=errors.DuplicateEntry(message=u'SELinux User Map rule ' +
+ u'with name "%s" already exists' % rule1),
+ ),
+
+
+ dict(
+ desc='Retrieve rule %r' % rule1,
+ command=('selinuxusermap_show', [rule1], {}),
+ expected=dict(
+ value=rule1,
+ summary=None,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser1],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Update rule %r' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1],
+ dict(ipaselinuxuser=selinuxuser2)
+ ),
+ expected=dict(
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ ),
+ summary=u'Modified SELinux User Map "%s"' % rule1,
+ value=rule1,
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % rule1,
+ command=('selinuxusermap_show', [rule1], {}),
+ expected=dict(
+ value=rule1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for rule %r' % rule1,
+ command=('selinuxusermap_find', [], dict(cn=rule1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ result=[
+ dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ ],
+ summary=u'1 SELinux User Map matched',
+ ),
+ ),
+
+
+ ###############
+ # Create additional entries needed for testing
+ 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=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/%s' % user1],
+ krbprincipalname=[u'%s@%s' % (user1, api.env.realm)],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn', 'global_policy'),
+ ('cn', api.env.realm),
+ ('cn', 'kerberos'),
+ api.env.basedn)
+ ],
+ mepmanagedentry=[DN(('cn', user1), ('cn', 'groups'),
+ ('cn', 'accounts'), api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ dn=DN(('uid', user1), ('cn', 'users'), ('cn', 'accounts'),
+ api.env.basedn),
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Create group %r' % 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'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn', group1), ('cn', 'groups'), ('cn', 'accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add member %r to %r' % (user1, group1),
+ command=(
+ 'group_add_member', [group1], dict(user=user1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn', group1), ('cn', 'groups'),
+ ('cn', 'accounts'), api.env.basedn),
+ 'member_user': (user1,),
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [group1],
+ 'description': [u'Test desc 1'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Create host %r' % host1,
+ command=('host_add', [host1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=host1,
+ summary=u'Added host "%s"' % host1,
+ result=dict(
+ dn=hostdn1,
+ fqdn=[host1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (host1, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[host1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create HBAC rule %r' % hbacrule1,
+ command=(
+ 'hbacrule_add', [hbacrule1], {}
+ ),
+ expected=dict(
+ value=hbacrule1,
+ summary=u'Added HBAC rule "%s"' % hbacrule1,
+ result=dict(
+ cn=[hbacrule1],
+ objectclass=objectclasses.hbacrule,
+ ipauniqueid=[fuzzy_uuid],
+ accessruletype=[u'allow'],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_hbacruledn,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create HBAC rule %r' % hbacrule2,
+ command=(
+ 'hbacrule_add', [hbacrule2], {}
+ ),
+ expected=dict(
+ value=hbacrule2,
+ summary=u'Added HBAC rule "%s"' % hbacrule2,
+ result=dict(
+ cn=[hbacrule2],
+ objectclass=objectclasses.hbacrule,
+ ipauniqueid=[fuzzy_uuid],
+ accessruletype=[u'allow'],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_hbacruledn,
+ ),
+ ),
+ ),
+
+
+ ###############
+ # Fill out rule with members and/or pointers to HBAC rules
+ dict(
+ desc='Add user to %r' % rule1,
+ command=('selinuxusermap_add_user', [rule1], dict(user=user1)),
+ expected=dict(
+ failed=dict(memberuser=dict(group=[], user=[])),
+ completed=1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ memberuser_user=[user1],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ dict(
+ desc='Add non-existent user to %r' % rule1,
+ command=('selinuxusermap_add_user', [rule1],
+ dict(user=u'notfound')),
+ expected=dict(
+ failed=dict(
+ memberuser=dict(group=[],
+ user=[(u'notfound', u'no such entry')])
+ ),
+ completed=0,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ memberuser_user=[user1],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ dict(
+ desc='Remove user from %r' % rule1,
+ command=('selinuxusermap_remove_user', [rule1], dict(user=user1)),
+ expected=dict(
+ failed=dict(memberuser=dict(group=[], user=[])),
+ completed=1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ dict(
+ desc='Remove non-existent user to %r' % rule1,
+ command=('selinuxusermap_remove_user', [rule1],
+ dict(user=u'notfound')),
+ expected=dict(
+ failed=dict(
+ memberuser=dict(group=[],
+ user=[(u'notfound', u'This entry is not a member')]
+ )
+ ),
+ completed=0,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ dict(
+ desc='Add group to %r' % rule1,
+ command=('selinuxusermap_add_user', [rule1], dict(group=group1)),
+ expected=dict(
+ failed=dict(memberuser=dict(group=[], user=[])),
+ completed=1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ memberuser_group=[group1],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ dict(
+ desc='Add host to %r' % rule1,
+ command=('selinuxusermap_add_host', [rule1], dict(host=host1)),
+ expected=dict(
+ failed=dict(memberhost=dict(hostgroup=[], host=[])),
+ completed=1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ memberhost_host=[host1],
+ memberuser_group=[group1],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ ###############
+ # Test enabling and disabling
+ dict(
+ desc='Disable %r' % rule1,
+ command=('selinuxusermap_disable', [rule1], {}),
+ expected=dict(
+ result=True,
+ value=rule1,
+ summary=u'Disabled SELinux User Map "%s"' % rule1,
+ )
+ ),
+
+
+ dict(
+ desc='Disable %r again' % rule1,
+ command=('selinuxusermap_disable', [rule1], {}),
+ expected=errors.AlreadyInactive(),
+ ),
+
+
+ dict(
+ desc='Enable %r' % rule1,
+ command=('selinuxusermap_enable', [rule1], {}),
+ expected=dict(
+ result=True,
+ value=rule1,
+ summary=u'Enabled SELinux User Map "%s"' % rule1,
+ )
+ ),
+
+
+ dict(
+ desc='Re-enable %r again' % rule1,
+ command=('selinuxusermap_enable', [rule1], {}),
+ expected=errors.AlreadyActive(),
+ ),
+
+
+ # Point to an HBAC Rule
+ dict(
+ desc='Add an HBAC rule to %r that has other members' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1], dict(seealso=hbacrule1)
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+
+ dict(
+ desc='Remove host from %r' % rule1,
+ command=('selinuxusermap_remove_host', [rule1], dict(host=host1)),
+ expected=dict(
+ failed=dict(memberhost=dict(hostgroup=[], host=[])),
+ completed=1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ memberuser_group=[group1],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ dict(
+ desc='Remove group from %r' % rule1,
+ command=('selinuxusermap_remove_user', [rule1],
+ dict(group=group1)),
+ expected=dict(
+ failed=dict(memberuser=dict(group=[], user=[])),
+ completed=1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ )
+ ),
+
+
+ dict(
+ desc='Add non-existent HBAC rule to %r' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1], dict(seealso=u'notfound')
+ ),
+ expected=errors.NotFound(
+ reason=u'HBAC rule notfound not found'),
+ ),
+
+
+ dict(
+ desc='Add an HBAC rule to %r' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1], dict(seealso=hbacrule1)
+ ),
+ expected=dict(
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser2],
+ ipaenabledflag=[u'TRUE'],
+ seealso=hbacrule1,
+ ),
+ summary=u'Modified SELinux User Map "%s"' % rule1,
+ value=rule1,
+ ),
+ ),
+
+
+ dict(
+ desc='Add user to %r that has HBAC' % rule1,
+ command=('selinuxusermap_add_user', [rule1], dict(user=user1)),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+
+ dict(
+ desc='Add host to %r that has HBAC' % rule1,
+ command=('selinuxusermap_add_host', [rule1], dict(host=host1)),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+
+ dict(
+ desc='Try to delete HBAC rule pointed to by %r' % rule1,
+ command=('hbacrule_del', [hbacrule1], {}),
+ expected=errors.DependentEntry(key=hbacrule1,
+ label=u'SELinux User Map', dependent=rule1)
+ ),
+
+
+ # This tests selinuxusermap-find --hbacrule=<foo> returns an
+ # exact match
+ dict(
+ desc='Try to delete similarly named HBAC rule %r' % hbacrule2,
+ command=('hbacrule_del', [hbacrule2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=hbacrule2,
+ summary=u'Deleted HBAC rule "%s"' % hbacrule2,
+ )
+ ),
+
+
+ # Test clean up
+ dict(
+ desc='Delete %r' % rule1,
+ command=('selinuxusermap_del', [rule1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=rule1,
+ summary=u'Deleted SELinux User Map "%s"' % rule1,
+ )
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % rule1,
+ command=('selinuxusermap_del', [rule1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: SELinux User Map rule not found' % rule1),
+ ),
+
+
+ # Some negative tests
+ dict(
+ desc='Create rule with unknown user %r' % rule1,
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=u'notfound:s0:c0')
+ ),
+ expected=errors.NotFound(reason=u'SELinux user notfound:s0:c0 ' +
+ u'not found in ordering list (in config)'),
+ ),
+
+
+ dict(
+ desc='Create rule with invalid user bad+user',
+ command=(
+ 'selinuxusermap_add', [rule1], dict(ipaselinuxuser=u'bad+user')
+ ),
+ expected=errors.ValidationError(name='selinuxuser',
+ error=u'Invalid SELinux user name, only a-Z and _ are allowed'
+ ),
+ ),
+
+
+ dict(
+ desc='Create rule with invalid MCS xguest_u:s999',
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=u'xguest_u:s999')
+ ),
+ expected=errors.ValidationError(name='selinuxuser',
+ error=u'Invalid MLS value, must match s[0-15](-s[0-15])'),
+ ),
+
+
+ dict(
+ desc='Create rule with invalid MLS xguest_u:s0:p88',
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=u'xguest_u:s0:p88')
+ ),
+ expected=errors.ValidationError(name='selinuxuser',
+ error=u'Invalid MCS value, must match c[0-1023].c[0-1023] ' +
+ u'and/or c[0-1023]-c[0-c0123]'),
+ ),
+
+
+ dict(
+ desc='Create rule with invalid MLS xguest_u:s0:c0.c1028',
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=u'xguest_u:s0-s0:c0.c1028')
+ ),
+ expected=errors.ValidationError(name='selinuxuser',
+ error=u'Invalid MCS value, must match c[0-1023].c[0-1023] ' +
+ u'and/or c[0-1023]-c[0-c0123]'),
+ ),
+
+
+ dict(
+ desc='Create rule with invalid user via setattr',
+ command=(
+ 'selinuxusermap_mod', [rule1],
+ dict(setattr=u'ipaselinuxuser=deny')
+ ),
+ expected=errors.ValidationError(name='ipaselinuxuser',
+ error=u'Invalid MLS value, must match s[0-15](-s[0-15])'),
+ ),
+
+ dict(
+ desc='Create rule with both --hbacrule and --usercat set',
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1,
+ seealso=hbacrule1,
+ usercategory=u'all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Create rule with both --hbacrule and --hostcat set',
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1,
+ seealso=hbacrule1,
+ hostcategory=u'all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Create rule with both --hbacrule '
+ 'and --usercat set via setattr',
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1,
+ seealso=hbacrule1,
+ setattr=u'usercategory=all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Create rule with both --hbacrule '
+ 'and --hostcat set via setattr',
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1,
+ seealso=hbacrule1,
+ setattr=u'hostcategory=all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Create rule %r with --hbacrule' % rule1,
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1, seealso=hbacrule1)
+ ),
+ expected=dict(
+ value=rule1,
+ summary=u'Added SELinux User Map "%s"' % rule1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser1],
+ objectclass=objectclasses.selinuxusermap,
+ ipauniqueid=[fuzzy_uuid],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ seealso=hbacrule1
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Add an --usercat to %r that has HBAC set' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1], dict(usercategory=u'all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Add an --hostcat to %r that has HBAC set' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1], dict(hostcategory=u'all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Add an usercat via setattr to %r that has HBAC set' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1],
+ dict(setattr=u'usercategory=all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Add an hostcat via setattr to %r that has HBAC set' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1],
+ dict(setattr=u'hostcategory=all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Delete %r' % rule1,
+ command=('selinuxusermap_del', [rule1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=rule1,
+ summary=u'Deleted SELinux User Map "%s"' % rule1,
+ )
+ ),
+
+ dict(
+ desc='Create rule %r with usercat and hostcat set' % rule1,
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1,
+ usercategory=u'all',
+ hostcategory=u'all')
+ ),
+ expected=dict(
+ value=rule1,
+ summary=u'Added SELinux User Map "%s"' % rule1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser1],
+ objectclass=objectclasses.selinuxusermap,
+ ipauniqueid=[fuzzy_uuid],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ usercategory=[u'all'],
+ hostcategory=[u'all']
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Add HBAC rule to %r that has usercat and hostcat' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1], dict(seealso=hbacrule1)
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Delete %r' % rule1,
+ command=('selinuxusermap_del', [rule1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=rule1,
+ summary=u'Deleted SELinux User Map "%s"' % rule1,
+ )
+ ),
+
+ dict(
+ desc='Create rule %r' % rule1,
+ command=(
+ 'selinuxusermap_add', [rule1],
+ dict(ipaselinuxuser=selinuxuser1)
+ ),
+ expected=dict(
+ value=rule1,
+ summary=u'Added SELinux User Map "%s"' % rule1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser1],
+ objectclass=objectclasses.selinuxusermap,
+ ipauniqueid=[fuzzy_uuid],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Add HBAC rule, hostcat and usercat to %r' % rule1,
+ command=(
+ 'selinuxusermap_mod', [rule1],
+ dict(seealso=hbacrule1,
+ usercategory=u'all',
+ hostcategory=u'all')
+ ),
+ expected=errors.MutuallyExclusiveError(
+ reason=u'HBAC rule and local members cannot both be set'),
+ ),
+
+ dict(
+ desc='Delete %r' % rule1,
+ command=('selinuxusermap_del', [rule1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=rule1,
+ summary=u'Deleted SELinux User Map "%s"' % rule1,
+ )
+ ),
+
+ dict(
+ desc='Create rule %r with '
+ '--setattr=seealso=<allow_all rule DN>' % rule1,
+ command=(
+ 'selinuxusermap_add',
+ [rule1],
+ dict(ipaselinuxuser=selinuxuser1,
+ setattr=u'seealso=%s' % allow_all_rule_dn)
+ ),
+ expected=dict(
+ value=rule1,
+ summary=u'Added SELinux User Map "%s"' % rule1,
+ result=dict(
+ cn=[rule1],
+ ipaselinuxuser=[selinuxuser1],
+ objectclass=objectclasses.selinuxusermap,
+ ipauniqueid=[fuzzy_uuid],
+ ipaenabledflag=[u'TRUE'],
+ dn=fuzzy_selinuxusermapdn,
+ seealso=u'allow_all',
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Delete %r' % rule1,
+ command=('selinuxusermap_del', [rule1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=rule1,
+ summary=u'Deleted SELinux User Map "%s"' % rule1,
+ )
+ ),
+ ]
diff --git a/ipatests/test_xmlrpc/test_service_plugin.py b/ipatests/test_xmlrpc/test_service_plugin.py
new file mode 100644
index 000000000..f51954eb3
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_service_plugin.py
@@ -0,0 +1,632 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@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/>.
+"""
+Test the `ipalib/plugins/service.py` module.
+"""
+
+from ipalib import api, errors, x509
+from ipatests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid, fuzzy_hash
+from ipatests.test_xmlrpc.xmlrpc_test import fuzzy_digits, fuzzy_date, fuzzy_issuer
+from ipatests.test_xmlrpc.xmlrpc_test import fuzzy_hex
+from ipatests.test_xmlrpc import objectclasses
+import base64
+from ipapython.dn import DN
+
+fqdn1 = u'testhost1.%s' % api.env.domain
+fqdn2 = u'testhost2.%s' % api.env.domain
+fqdn3 = u'TestHost3.%s' % api.env.domain
+service1 = u'HTTP/%s@%s' % (fqdn1, api.env.realm)
+hostprincipal1 = u'host/%s@%s' % (fqdn1, api.env.realm)
+service1dn = DN(('krbprincipalname',service1),('cn','services'),('cn','accounts'),api.env.basedn)
+host1dn = DN(('fqdn',fqdn1),('cn','computers'),('cn','accounts'),api.env.basedn)
+host2dn = DN(('fqdn',fqdn2),('cn','computers'),('cn','accounts'),api.env.basedn)
+host3dn = DN(('fqdn',fqdn3),('cn','computers'),('cn','accounts'),api.env.basedn)
+
+fd = open('ipatests/test_xmlrpc/service.crt', 'r')
+servercert = fd.readlines()
+servercert = ''.join(servercert)
+servercert = x509.strip_header(servercert)
+fd.close()
+
+badservercert = 'MIICbzCCAdigAwIBAgICA/4wDQYJKoZIhvcNAQEFBQAwKTEnMCUGA1UEAxMeSVBBIFRlc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTEwMDgwOTE1MDIyN1oXDTIwMDgwOTE1MDIyN1owKTEMMAoGA1UEChMDSVBBMRkwFwYDVQQDExBwdW1hLmdyZXlvYWsuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwYbfEOQPgGenPn9vt1JFKvWm/Je3y2tawGWA3LXDuqfFJyYtZ8ib3TcBUOnLk9WK5g2qCwHaNlei7bj8ggIfr5hegAVe10cun+wYErjnYo7hsHYd+57VZezeipWrXu+7NoNd4+c4A5lk4A/xJay9j3bYx2oOM8BEox4xWYoWge1ljPrc5JK46f0X7AGW4F2VhnKPnf8rwSuzI1U8VGjutyM9TWNy3m9KMWeScjyG/ggIpOjUDMV7HkJL0Di61lznR9jXubpiEC7gWGbTp84eGl/Nn9bgK1AwHfJ2lHwfoY4uiL7ge1gyP6EvuUlHoBzdb7pekiX28iePjW3iEG9IawIDAQABoyIwIDARBglghkgBhvhCAQEEBAMCBkAwCwYDVR0PBAQDAgUgMA0GCSqGSIb3DQEBBQUAA4GBACRESLemRV9BPxfEgbALuxH5oE8jQm8WZ3pm2pALbpDlAd9wQc3yVf6RtkfVthyDnM18bg7IhxKpd77/p3H8eCnS8w5MLVRda6ktUC6tGhFTS4QKAf0WyDGTcIgkXbeDw0OPAoNHivoXbIXIIRxlw/XgaSaMzJQDBG8iROsN4kCv'
+
+
+class test_service(Declarative):
+
+ cleanup_commands = [
+ ('host_del', [fqdn1], {}),
+ ('host_del', [fqdn2], {}),
+ ('host_del', [fqdn3], {}),
+ ('service_del', [service1], {}),
+ ]
+
+ tests = [
+ dict(
+ desc='Try to retrieve non-existent %r' % service1,
+ command=('service_show', [service1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: service not found' % service1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % service1,
+ command=('service_mod', [service1], dict(usercertificate=servercert)),
+ expected=errors.NotFound(
+ reason=u'%s: service not found' % service1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % service1,
+ command=('service_del', [service1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: service not found' % service1),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn1,
+ command=('host_add', [fqdn1],
+ dict(
+ description=u'Test host 1',
+ l=u'Undisclosed location 1',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn1,
+ summary=u'Added host "%s"' % fqdn1,
+ result=dict(
+ dn=host1dn,
+ fqdn=[fqdn1],
+ description=[u'Test host 1'],
+ l=[u'Undisclosed location 1'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[u'%s' % fqdn1],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn2,
+ command=('host_add', [fqdn2],
+ dict(
+ description=u'Test host 2',
+ l=u'Undisclosed location 2',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn2,
+ summary=u'Added host "%s"' % fqdn2,
+ result=dict(
+ dn=host2dn,
+ fqdn=[fqdn2],
+ description=[u'Test host 2'],
+ l=[u'Undisclosed location 2'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn2, api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[u'%s' % fqdn2],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % fqdn3,
+ command=('host_add', [fqdn3],
+ dict(
+ description=u'Test host 3',
+ l=u'Undisclosed location 3',
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=fqdn3.lower(),
+ summary=u'Added host "%s"' % fqdn3.lower(),
+ result=dict(
+ dn=host3dn,
+ fqdn=[fqdn3.lower()],
+ description=[u'Test host 3'],
+ l=[u'Undisclosed location 3'],
+ krbprincipalname=[u'host/%s@%s' % (fqdn3.lower(), api.env.realm)],
+ objectclass=objectclasses.host,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[u'%s' % fqdn3.lower()],
+ has_keytab=False,
+ has_password=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create %r' % service1,
+ command=('service_add', [service1],
+ dict(
+ force=True,
+ ),
+ ),
+ expected=dict(
+ value=service1,
+ summary=u'Added service "%s"' % service1,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ objectclass=objectclasses.service,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % service1,
+ command=('service_add', [service1],
+ dict(
+ force=True,
+ ),
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'service with name "%s" already exists' % service1),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % service1,
+ command=('service_show', [service1], {}),
+ expected=dict(
+ value=service1,
+ summary=None,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ has_keytab=False,
+ managedby_host=[fqdn1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r with all=True' % service1,
+ command=('service_show', [service1], dict(all=True)),
+ expected=dict(
+ value=service1,
+ summary=None,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ ipakrbprincipalalias=[service1],
+ objectclass=objectclasses.service,
+ ipauniqueid=[fuzzy_uuid],
+ managedby_host=[fqdn1],
+ has_keytab=False,
+ ipakrbrequirespreauth=True,
+ ipakrbokasdelegate=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % service1,
+ command=('service_find', [service1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 service matched',
+ result=[
+ dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ has_keytab=False,
+ ),
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r with all=True' % service1,
+ command=('service_find', [service1], dict(all=True)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 service matched',
+ result=[
+ dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ ipakrbprincipalalias=[service1],
+ objectclass=objectclasses.service,
+ ipauniqueid=[fuzzy_uuid],
+ has_keytab=False,
+ managedby_host=[fqdn1],
+ ipakrbrequirespreauth=True,
+ ipakrbokasdelegate=False,
+ ),
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Add non-existent host to %r' % service1,
+ command=('service_add_host', [service1], dict(host=u'notfound')),
+ expected=dict(
+ failed=dict(managedby=dict(host=[(u'notfound', u'no such entry')])),
+ completed=0,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Remove non-existent host from %r' % service1,
+ command=('service_remove_host', [service1], dict(host=u'notfound')),
+ expected=dict(
+ failed=dict(managedby=dict(host=[(u'notfound', u'This entry is not a member')])),
+ completed=0,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add host to %r' % service1,
+ command=('service_add_host', [service1], dict(host=fqdn2)),
+ expected=dict(
+ failed=dict(managedby=dict(host=[])),
+ completed=1,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1, fqdn2],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Remove host from %r' % service1,
+ command=('service_remove_host', [service1], dict(host=fqdn2)),
+ expected=dict(
+ failed=dict(managedby=dict(host=[])),
+ completed=1,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Add mixed-case host to %r' % service1,
+ command=('service_add_host', [service1], dict(host=fqdn3)),
+ expected=dict(
+ failed=dict(managedby=dict(host=[])),
+ completed=1,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1, fqdn3.lower()],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Remove mixed-case host from %r' % service1,
+ command=('service_remove_host', [service1], dict(host=fqdn3)),
+ expected=dict(
+ failed=dict(managedby=dict(host=[])),
+ completed=1,
+ result=dict(
+ dn=service1dn,
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r with a bad certificate' % service1,
+ command=('service_mod', [service1], dict(usercertificate=badservercert)),
+ expected=errors.CertificateOperationError(
+ error=u'Issuer "CN=IPA Test Certificate Authority" does not ' +
+ u'match the expected issuer'),
+ ),
+
+
+ dict(
+ desc='Update %r' % service1,
+ command=('service_mod', [service1], dict(usercertificate=servercert)),
+ expected=dict(
+ value=service1,
+ summary=u'Modified service "%s"' % service1,
+ result=dict(
+ usercertificate=[base64.b64decode(servercert)],
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to update %r with invalid ipakrbauthz data '
+ 'combination' % service1,
+ command=('service_mod', [service1],
+ dict(ipakrbauthzdata=[u'MS-PAC', u'NONE'])),
+ expected=errors.ValidationError(name='ipakrbauthzdata',
+ error=u'NONE value cannot be combined with other PAC types')
+ ),
+
+
+ dict(
+ desc='Update %r with valid ipakrbauthz data '
+ 'combination' % service1,
+ command=('service_mod', [service1],
+ dict(ipakrbauthzdata=[u'MS-PAC'])),
+ expected=dict(
+ value=service1,
+ summary=u'Modified service "%s"' % service1,
+ result=dict(
+ usercertificate=[base64.b64decode(servercert)],
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ipakrbauthzdata=[u'MS-PAC'],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % service1,
+ command=('service_show', [service1], {}),
+ expected=dict(
+ value=service1,
+ summary=None,
+ result=dict(
+ dn=service1dn,
+ usercertificate=[base64.b64decode(servercert)],
+ krbprincipalname=[service1],
+ has_keytab=False,
+ managedby_host=[fqdn1],
+ ipakrbauthzdata=[u'MS-PAC'],
+ # These values come from the servercert that is in this
+ # test case.
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Enable %r OK_AS_DELEGATE Kerberos ticket flag' % service1,
+ command=('service_mod', [service1], dict(ipakrbokasdelegate=True)),
+ expected=dict(
+ value=service1,
+ summary=u'Modified service "%s"' % service1,
+ result=dict(
+ usercertificate=[base64.b64decode(servercert)],
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ipakrbauthzdata=[u'MS-PAC'],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ krbticketflags=[u'1048704'],
+ ipakrbokasdelegate=True,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r Kerberos ticket flags with setattr' % service1,
+ command=('service_mod', [service1],
+ dict(setattr=[u'krbTicketFlags=1048577'])),
+ expected=dict(
+ value=service1,
+ summary=u'Modified service "%s"' % service1,
+ result=dict(
+ usercertificate=[base64.b64decode(servercert)],
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ipakrbauthzdata=[u'MS-PAC'],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ krbticketflags=[u'1048577'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Disable %r OK_AS_DELEGATE Kerberos ticket flag' % service1,
+ command=('service_mod', [service1], dict(ipakrbokasdelegate=False)),
+ expected=dict(
+ value=service1,
+ summary=u'Modified service "%s"' % service1,
+ result=dict(
+ usercertificate=[base64.b64decode(servercert)],
+ krbprincipalname=[service1],
+ managedby_host=[fqdn1],
+ ipakrbauthzdata=[u'MS-PAC'],
+ valid_not_before=fuzzy_date,
+ valid_not_after=fuzzy_date,
+ subject=DN(('CN',api.env.host),x509.subject_base()),
+ serial_number=fuzzy_digits,
+ serial_number_hex=fuzzy_hex,
+ md5_fingerprint=fuzzy_hash,
+ sha1_fingerprint=fuzzy_hash,
+ issuer=fuzzy_issuer,
+ krbticketflags=[u'1'],
+ ipakrbokasdelegate=False,
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete %r' % service1,
+ command=('service_del', [service1], {}),
+ expected=dict(
+ value=service1,
+ summary=u'Deleted service "%s"' % service1,
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent %r' % service1,
+ command=('service_show', [service1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: service not found' % service1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % service1,
+ command=('service_mod', [service1], dict(usercertificate=servercert)),
+ expected=errors.NotFound(
+ reason=u'%s: service not found' % service1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % service1,
+ command=('service_del', [service1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: service not found' % service1),
+ ),
+
+
+ dict(
+ desc='Create service with malformed principal "foo"',
+ command=('service_add', [u'foo'], {}),
+ expected=errors.MalformedServicePrincipal(reason='missing service')
+ ),
+
+
+ dict(
+ desc='Create service with bad realm "HTTP/foo@FOO.NET"',
+ command=('service_add', [u'HTTP/foo@FOO.NET'], {}),
+ expected=errors.RealmMismatch(),
+ ),
+
+
+ dict(
+ desc='Create a host service %r' % hostprincipal1,
+ command=('service_add', [hostprincipal1], {}),
+ expected=errors.HostService()
+ ),
+
+
+ # These tests will only succeed when running against lite-server.py
+ # on same box as IPA install.
+ dict(
+ desc='Delete the current host (master?) %s HTTP service, should be caught' % api.env.host,
+ command=('service_del', ['HTTP/%s' % api.env.host], {}),
+ expected=errors.ValidationError(name='principal', error='This principal is required by the IPA master'),
+ ),
+
+
+ dict(
+ desc='Delete the current host (master?) %s ldap service, should be caught' % api.env.host,
+ command=('service_del', ['ldap/%s' % api.env.host], {}),
+ expected=errors.ValidationError(name='principal', error='This principal is required by the IPA master'),
+ ),
+
+
+ dict(
+ desc='Disable the current host (master?) %s HTTP service, should be caught' % api.env.host,
+ command=('service_disable', ['HTTP/%s' % api.env.host], {}),
+ expected=errors.ValidationError(name='principal', error='This principal is required by the IPA master'),
+ ),
+
+
+ dict(
+ desc='Disable the current host (master?) %s ldap service, should be caught' % api.env.host,
+ command=('service_disable', ['ldap/%s' % api.env.host], {}),
+ expected=errors.ValidationError(name='principal', error='This principal is required by the IPA master'),
+ ),
+
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_sudocmd_plugin.py b/ipatests/test_xmlrpc/test_sudocmd_plugin.py
new file mode 100644
index 000000000..fe91705c2
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_sudocmd_plugin.py
@@ -0,0 +1,327 @@
+# Authors:
+# Jr Aquino <jr.aquino@citrixonline.com>
+#
+# Copyright (C) 2010 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/>.
+
+"""
+Test the `ipalib/plugins/sudocmd.py` module.
+"""
+
+from ipalib import errors
+from ipatests.test_xmlrpc.xmlrpc_test import (Declarative, fuzzy_sudocmddn,
+ fuzzy_uuid)
+from ipatests.test_xmlrpc import objectclasses
+
+sudocmd1 = u'/usr/bin/sudotestcmd1'
+sudocmd1_camelcase = u'/usr/bin/sudoTestCmd1'
+
+sudorule1 = u'test_sudorule1'
+
+
+class test_sudocmd(Declarative):
+
+ cleanup_commands = [
+ ('sudocmd_del', [sudocmd1], {}),
+ ('sudocmd_del', [sudocmd1_camelcase], {}),
+ ('sudorule_del', [sudorule1], {}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent %r' % sudocmd1,
+ command=('sudocmd_show', [sudocmd1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command not found' % sudocmd1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % sudocmd1,
+ command=('sudocmd_mod', [sudocmd1], dict(description=u'Nope')),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command not found' % sudocmd1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % sudocmd1,
+ command=('sudocmd_del', [sudocmd1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command not found' % sudocmd1),
+ ),
+
+
+ dict(
+ desc='Create %r' % sudocmd1,
+ command=('sudocmd_add', [sudocmd1],
+ dict(
+ description=u'Test sudo command 1',
+ ),
+ ),
+ expected=dict(
+ value=sudocmd1,
+ summary=u'Added Sudo Command "%s"' % sudocmd1,
+ result=dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1],
+ description=[u'Test sudo command 1'],
+ objectclass=objectclasses.sudocmd,
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Create %r' % sudocmd1_camelcase,
+ command=('sudocmd_add', [sudocmd1_camelcase],
+ dict(
+ description=u'Test sudo command 2',
+ ),
+ ),
+ expected=dict(
+ value=sudocmd1_camelcase,
+ summary=u'Added Sudo Command "%s"' % sudocmd1_camelcase,
+ result=dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1_camelcase],
+ description=[u'Test sudo command 2'],
+ objectclass=objectclasses.sudocmd,
+ ipauniqueid=[fuzzy_uuid],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % sudocmd1,
+ command=('sudocmd_add', [sudocmd1],
+ dict(
+ description=u'Test sudo command 1',
+ ),
+ ),
+ expected=errors.DuplicateEntry(message=u'sudo command with ' +
+ u'name "%s" already exists' % sudocmd1),
+ ),
+
+ dict(
+ desc='Try to create duplicate %r' % sudocmd1_camelcase,
+ command=('sudocmd_add', [sudocmd1_camelcase],
+ dict(
+ description=u'Test sudo command 2',
+ ),
+ ),
+ expected=errors.DuplicateEntry(message=u'sudo command with ' +
+ u'name "%s" already exists' % sudocmd1_camelcase),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % sudocmd1,
+ command=('sudocmd_show', [sudocmd1], {}),
+ expected=dict(
+ value=sudocmd1,
+ summary=None,
+ result=dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1],
+ description=[u'Test sudo command 1'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % sudocmd1,
+ command=('sudocmd_find', [sudocmd1], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 Sudo Command matched',
+ result=[
+ dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1],
+ description=[u'Test sudo command 1'],
+ ),
+ ],
+ ),
+ ),
+
+ dict(
+ desc='Search for %r' % sudocmd1_camelcase,
+ command=('sudocmd_find', [sudocmd1_camelcase], {}),
+ expected=dict(
+ count=1,
+ truncated=False,
+ summary=u'1 Sudo Command matched',
+ result=[
+ dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1_camelcase],
+ description=[u'Test sudo command 2'],
+ ),
+ ],
+ ),
+ ),
+
+
+ dict(
+ desc='Update %r' % sudocmd1,
+ command=('sudocmd_mod', [sudocmd1], dict(
+ description=u'Updated sudo command 1')),
+ expected=dict(
+ value=sudocmd1,
+ summary=u'Modified Sudo Command "%s"' % sudocmd1,
+ result=dict(
+ sudocmd=[sudocmd1],
+ description=[u'Updated sudo command 1'],
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % sudocmd1,
+ command=('sudocmd_show', [sudocmd1], {}),
+ expected=dict(
+ value=sudocmd1,
+ summary=None,
+ result=dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1],
+ description=[u'Updated sudo command 1'],
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Create %r' % sudorule1,
+ command=('sudorule_add', [sudorule1], {}),
+ expected=lambda e, result: True,
+ ),
+
+ dict(
+ desc='Add %r to %r allow list' % (sudocmd1, sudorule1),
+ command=('sudorule_add_allow_command', [sudorule1],
+ dict(sudocmd=sudocmd1)),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ memberallowcmd=dict(sudocmdgroup=(), sudocmd=())),
+ result=lambda result: True,
+ ),
+ ),
+
+ dict(
+ desc="Test %r can't be deleted when in %r" % (sudocmd1, sudorule1),
+ command=('sudocmd_del', [sudocmd1], {}),
+ expected=errors.DependentEntry(key=sudocmd1, label='sudorule',
+ dependent=sudorule1),
+ ),
+
+ dict(
+ desc='Remove %r from %r' % (sudocmd1, sudorule1),
+ command=('sudorule_remove_allow_command', [sudorule1],
+ dict(sudocmd=sudocmd1)),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ memberallowcmd=dict(sudocmdgroup=(), sudocmd=())),
+ result=lambda result: True,
+ ),
+ ),
+
+ dict(
+ desc='Add %r to %r deny list' % (sudocmd1, sudorule1),
+ command=('sudorule_add_deny_command', [sudorule1],
+ dict(sudocmd=sudocmd1)),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ memberdenycmd=dict(sudocmdgroup=(), sudocmd=())),
+ result=lambda result: True,
+ ),
+ ),
+
+ dict(
+ desc="Test %r can't be deleted when in %r" % (sudocmd1, sudorule1),
+ command=('sudocmd_del', [sudocmd1], {}),
+ expected=errors.DependentEntry(key=sudocmd1, label='sudorule',
+ dependent=sudorule1),
+ ),
+
+ dict(
+ desc='Remove %r from %r' % (sudocmd1, sudorule1),
+ command=('sudorule_remove_deny_command', [sudorule1],
+ dict(sudocmd=sudocmd1)),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ memberdenycmd=dict(sudocmdgroup=(), sudocmd=())),
+ result=lambda result: True,
+ ),
+ ),
+
+ dict(
+ desc='Delete %r' % sudocmd1,
+ command=('sudocmd_del', [sudocmd1], {}),
+ expected=dict(
+ value=sudocmd1,
+ summary=u'Deleted Sudo Command "%s"' % sudocmd1,
+ result=dict(failed=u''),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent %r' % sudocmd1,
+ command=('sudocmd_show', [sudocmd1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command not found' % sudocmd1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % sudocmd1,
+ command=('sudocmd_mod', [sudocmd1], dict(description=u'Nope')),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command not found' % sudocmd1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % sudocmd1,
+ command=('sudocmd_del', [sudocmd1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command not found' % sudocmd1),
+ ),
+
+ dict(
+ desc='Retrieve %r' % sudocmd1_camelcase,
+ command=('sudocmd_show', [sudocmd1_camelcase], {}),
+ expected=dict(
+ value=sudocmd1_camelcase,
+ summary=None,
+ result=dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1_camelcase],
+ description=[u'Test sudo command 2'],
+ ),
+ ),
+ ),
+ ]
diff --git a/ipatests/test_xmlrpc/test_sudocmdgroup_plugin.py b/ipatests/test_xmlrpc/test_sudocmdgroup_plugin.py
new file mode 100644
index 000000000..397d47683
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_sudocmdgroup_plugin.py
@@ -0,0 +1,693 @@
+# Authors:
+# Jr Aquino <jr.aquino@citrixonline.com>
+#
+# Copyright (C) 2010 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/>.
+"""
+Test the `ipalib/plugins/sudocmdgroup.py` module.
+"""
+
+from ipalib import api, errors
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import Declarative, fuzzy_uuid, fuzzy_sudocmddn
+from ipapython.dn import DN
+
+sudocmdgroup1 = u'testsudocmdgroup1'
+sudocmdgroup2 = u'testsudocmdgroup2'
+sudocmd1 = u'/usr/bin/sudotestcmd1'
+sudocmd1_camelcase = u'/usr/bin/sudoTestCmd1'
+sudocmd_plus = u'/bin/ls -l /lost+found/*'
+
+def create_command(sudocmd):
+ return dict(
+ desc='Create %r' % sudocmd,
+ command=(
+ 'sudocmd_add', [], dict(sudocmd=sudocmd,
+ description=u'Test sudo command')
+ ),
+ expected=dict(
+ value=sudocmd,
+ summary=u'Added Sudo Command "%s"' % sudocmd,
+ result=dict(
+ objectclass=objectclasses.sudocmd,
+ sudocmd=[sudocmd],
+ ipauniqueid=[fuzzy_uuid], description=[u'Test sudo command'],
+ dn=fuzzy_sudocmddn,
+ ),
+ ),
+ )
+
+class test_sudocmdgroup(Declarative):
+ cleanup_commands = [
+ ('sudocmdgroup_del', [sudocmdgroup1], {}),
+ ('sudocmdgroup_del', [sudocmdgroup2], {}),
+ ('sudocmd_del', [sudocmd1], {}),
+ ('sudocmd_del', [sudocmd1_camelcase], {}),
+ ('sudocmd_del', [sudocmd_plus], {}),
+ ]
+
+ tests = [
+
+ ################
+ # create sudo command
+ dict(
+ desc='Create %r' % sudocmd1,
+ command=(
+ 'sudocmd_add', [], dict(sudocmd=sudocmd1, description=u'Test sudo command 1')
+ ),
+ expected=dict(
+ value=sudocmd1,
+ summary=u'Added Sudo Command "%s"' % sudocmd1,
+ result=dict(
+ objectclass=objectclasses.sudocmd,
+ sudocmd=[u'/usr/bin/sudotestcmd1'],
+ ipauniqueid=[fuzzy_uuid],
+ description=[u'Test sudo command 1'],
+ dn=fuzzy_sudocmddn,
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Create %r' % sudocmd1_camelcase,
+ command=(
+ 'sudocmd_add', [], dict(sudocmd=sudocmd1_camelcase, description=u'Test sudo command 2')
+ ),
+ expected=dict(
+ value=sudocmd1_camelcase,
+ summary=u'Added Sudo Command "%s"' % sudocmd1_camelcase,
+ result=dict(
+ objectclass=objectclasses.sudocmd,
+ sudocmd=[u'/usr/bin/sudoTestCmd1'],
+ ipauniqueid=[fuzzy_uuid],
+ description=[u'Test sudo command 2'],
+ dn=fuzzy_sudocmddn,
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Verify the managed sudo command %r was created' % sudocmd1,
+ command=('sudocmd_show', [sudocmd1], {}),
+ expected=dict(
+ value=sudocmd1,
+ summary=None,
+ result=dict(
+ sudocmd=[sudocmd1],
+ description=[u'Test sudo command 1'],
+ dn=fuzzy_sudocmddn,
+ ),
+ ),
+ ),
+
+
+ ################
+ # create sudo command group1:
+ dict(
+ desc='Try to retrieve non-existent %r' % sudocmdgroup1,
+ command=('sudocmdgroup_show', [sudocmdgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % sudocmdgroup1,
+ command=('sudocmdgroup_mod', [sudocmdgroup1],
+ dict(description=u'Foo')),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % sudocmdgroup1,
+ command=('sudocmdgroup_del', [sudocmdgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup1),
+ ),
+
+
+ dict(
+ desc='Create %r' % sudocmdgroup1,
+ command=(
+ 'sudocmdgroup_add', [sudocmdgroup1],
+ dict(description=u'Test desc 1')
+ ),
+ expected=dict(
+ value=sudocmdgroup1,
+ summary=u'Added Sudo Command Group "testsudocmdgroup1"',
+ result=dict(
+ cn=[sudocmdgroup1],
+ description=[u'Test desc 1'],
+ objectclass=objectclasses.sudocmdgroup,
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn','testsudocmdgroup1'),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % sudocmdgroup1,
+ command=(
+ 'sudocmdgroup_add', [sudocmdgroup1],
+ dict(description=u'Test desc 1')
+ ),
+ expected=errors.DuplicateEntry(message=u'sudo command group ' +
+ u'with name "%s" already exists' % sudocmdgroup1),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % sudocmdgroup1,
+ command=('sudocmdgroup_show', [sudocmdgroup1], {}),
+ expected=dict(
+ value=sudocmdgroup1,
+ summary=None,
+ result=dict(
+ cn=[sudocmdgroup1],
+ description=[u'Test desc 1'],
+ dn=DN(('cn','testsudocmdgroup1'),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Updated %r' % sudocmdgroup1,
+ command=(
+ 'sudocmdgroup_mod', [sudocmdgroup1],
+ dict(description=u'New desc 1')
+ ),
+ expected=dict(
+ result=dict(
+ cn=[sudocmdgroup1],
+ description=[u'New desc 1'],
+ ),
+ summary=u'Modified Sudo Command Group "testsudocmdgroup1"',
+ value=sudocmdgroup1,
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % sudocmdgroup1,
+ command=('sudocmdgroup_show', [sudocmdgroup1], {}),
+ expected=dict(
+ value=sudocmdgroup1,
+ result=dict(
+ cn=[sudocmdgroup1],
+ description=[u'New desc 1'],
+ dn=DN(('cn','testsudocmdgroup1'),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % sudocmdgroup1,
+ command=('sudocmdgroup_find', [], dict(cn=sudocmdgroup1)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ result=[
+ dict(
+ dn=DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ cn=[sudocmdgroup1],
+ description=[u'New desc 1'],
+ ),
+ ],
+ summary=u'1 Sudo Command Group matched',
+ ),
+ ),
+
+
+
+ ################
+ # create sudocmdgroup2:
+ dict(
+ desc='Try to retrieve non-existent %r' % sudocmdgroup2,
+ command=('sudocmdgroup_show', [sudocmdgroup2], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup2),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % sudocmdgroup2,
+ command=('sudocmdgroup_mod', [sudocmdgroup2],
+ dict(description=u'Foo')),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup2),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % sudocmdgroup2,
+ command=('sudocmdgroup_del', [sudocmdgroup2], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup2),
+ ),
+
+
+ dict(
+ desc='Create %r' % sudocmdgroup2,
+ command=(
+ 'sudocmdgroup_add', [sudocmdgroup2],
+ dict(description=u'Test desc 2')
+ ),
+ expected=dict(
+ value=sudocmdgroup2,
+ summary=u'Added Sudo Command Group "testsudocmdgroup2"',
+ result=dict(
+ cn=[sudocmdgroup2],
+ description=[u'Test desc 2'],
+ objectclass=objectclasses.sudocmdgroup,
+ ipauniqueid=[fuzzy_uuid],
+ dn=DN(('cn','testsudocmdgroup2'),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to create duplicate %r' % sudocmdgroup2,
+ command=(
+ 'sudocmdgroup_add', [sudocmdgroup2],
+ dict(description=u'Test desc 2')
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'sudo command group with name "%s" already exists' %
+ sudocmdgroup2),
+ ),
+
+
+ dict(
+ desc='Retrieve %r' % sudocmdgroup2,
+ command=('sudocmdgroup_show', [sudocmdgroup2], {}),
+ expected=dict(
+ value=sudocmdgroup2,
+ summary=None,
+ result=dict(
+ cn=[sudocmdgroup2],
+ description=[u'Test desc 2'],
+ dn=DN(('cn','testsudocmdgroup2'),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Updated %r' % sudocmdgroup2,
+ command=(
+ 'sudocmdgroup_mod', [sudocmdgroup2],
+ dict(description=u'New desc 2')
+ ),
+ expected=dict(
+ result=dict(
+ cn=[sudocmdgroup2],
+ description=[u'New desc 2'],
+ ),
+ summary=u'Modified Sudo Command Group "testsudocmdgroup2"',
+ value=sudocmdgroup2,
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve %r to verify update' % sudocmdgroup2,
+ command=('sudocmdgroup_show', [sudocmdgroup2], {}),
+ expected=dict(
+ value=sudocmdgroup2,
+ result=dict(
+ cn=[sudocmdgroup2],
+ description=[u'New desc 2'],
+ dn=DN(('cn','testsudocmdgroup2'),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ ),
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for %r' % sudocmdgroup2,
+ command=('sudocmdgroup_find', [], dict(cn=sudocmdgroup2)),
+ expected=dict(
+ count=1,
+ truncated=False,
+ result=[
+ dict(
+ dn=DN(('cn',sudocmdgroup2),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ cn=[sudocmdgroup2],
+ description=[u'New desc 2'],
+ ),
+ ],
+ summary=u'1 Sudo Command Group matched',
+ ),
+ ),
+
+
+ dict(
+ desc='Search for all sudocmdgroups',
+ command=('sudocmdgroup_find', [], {}),
+ expected=dict(
+ summary=u'2 Sudo Command Groups matched',
+ count=2,
+ truncated=False,
+ result=[
+ dict(
+ dn=DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ cn=[sudocmdgroup1],
+ description=[u'New desc 1'],
+ ),
+ dict(
+ dn=DN(('cn',sudocmdgroup2),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ cn=[sudocmdgroup2],
+ description=[u'New desc 2'],
+ ),
+ ],
+ ),
+ ),
+
+
+
+ ###############
+ # member stuff:
+ dict(
+ desc='Add member %r to %r' % (sudocmd1, sudocmdgroup1),
+ command=(
+ 'sudocmdgroup_add_member', [sudocmdgroup1],
+ dict(sudocmd=sudocmd1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ sudocmd=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'member_sudocmd': (sudocmd1,),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Retrieve %r to show membership' % sudocmd1,
+ command=('sudocmd_show', [sudocmd1], {}),
+ expected=dict(
+ value=sudocmd1,
+ summary=None,
+ result=dict(
+ dn=fuzzy_sudocmddn,
+ sudocmd=[sudocmd1],
+ description=[u'Test sudo command 1'],
+ memberof_sudocmdgroup=[u'testsudocmdgroup1'],
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Try to add non-existent member to %r' % sudocmdgroup1,
+ command=(
+ 'sudocmdgroup_add_member', [sudocmdgroup1],
+ dict(sudocmd=u'notfound')
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ sudocmd=[(u'notfound', u'no such entry')],
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'member_sudocmd': (u'/usr/bin/sudotestcmd1',),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Add member %r to %r' % (sudocmd1_camelcase, sudocmdgroup1),
+ command=(
+ 'sudocmdgroup_add_member', [sudocmdgroup1],
+ dict(sudocmd=sudocmd1_camelcase)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ sudocmd=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'member_sudocmd': (sudocmd1, sudocmd1_camelcase),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Remove member %r from %r' % (sudocmd1, sudocmdgroup1),
+ command=('sudocmdgroup_remove_member',
+ [sudocmdgroup1], dict(sudocmd=sudocmd1)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ sudocmd=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'member_sudocmd': (sudocmd1_camelcase,),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Remove member %r from %r' % (sudocmd1_camelcase, sudocmdgroup1),
+ command=('sudocmdgroup_remove_member',
+ [sudocmdgroup1], dict(sudocmd=sudocmd1_camelcase)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ sudocmd=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ dict(
+ # FIXME: Shouldn't this raise a NotFound instead?
+ desc='Try to remove non-existent member from %r' % sudocmdgroup1,
+ command=('sudocmdgroup_remove_member',
+ [sudocmdgroup1], dict(sudocmd=u'notfound')
+ ),
+ expected=dict(
+ completed=0,
+ failed=dict(
+ member=dict(
+ sudocmd=[(u'notfound', u'This entry is not a member')],
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ ################
+ # test a command that needs DN escaping:
+ create_command(sudocmd_plus),
+
+ dict(
+ desc='Add %r to %r' % (sudocmd_plus, sudocmdgroup1),
+ command=('sudocmdgroup_add_member', [sudocmdgroup1],
+ dict(sudocmd=sudocmd_plus)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ sudocmd=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'member_sudocmd': (sudocmd_plus,),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ dict(
+ desc='Remove %r from %r' % (sudocmd_plus, sudocmdgroup1),
+ command=('sudocmdgroup_remove_member', [sudocmdgroup1],
+ dict(sudocmd=sudocmd_plus)
+ ),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ sudocmd=tuple(),
+ ),
+ ),
+ result={
+ 'dn': DN(('cn',sudocmdgroup1),('cn','sudocmdgroups'),
+ ('cn','sudo'),api.env.basedn),
+ 'cn': [sudocmdgroup1],
+ 'description': [u'New desc 1'],
+ },
+ ),
+ ),
+
+ ################
+ # delete sudocmdgroup1:
+ dict(
+ desc='Delete %r' % sudocmdgroup1,
+ command=('sudocmdgroup_del', [sudocmdgroup1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=sudocmdgroup1,
+ summary=u'Deleted Sudo Command Group "testsudocmdgroup1"',
+ )
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % sudocmdgroup1,
+ command=('sudocmdgroup_del', [sudocmdgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup1),
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent %r' % sudocmdgroup1,
+ command=('sudocmdgroup_show', [sudocmdgroup1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % sudocmdgroup1,
+ command=('sudocmdgroup_mod', [sudocmdgroup1],
+ dict(description=u'Foo')),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup1),
+ ),
+
+
+ ################
+ # delete sudocmdgroup2:
+ dict(
+ desc='Delete %r' % sudocmdgroup2,
+ command=('sudocmdgroup_del', [sudocmdgroup2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=sudocmdgroup2,
+ summary=u'Deleted Sudo Command Group "testsudocmdgroup2"',
+ )
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent %r' % sudocmdgroup2,
+ command=('sudocmdgroup_del', [sudocmdgroup2], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup2),
+ ),
+
+
+ dict(
+ desc='Try to retrieve non-existent %r' % sudocmdgroup2,
+ command=('sudocmdgroup_show', [sudocmdgroup2], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup2),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent %r' % sudocmdgroup2,
+ command=('sudocmdgroup_mod', [sudocmdgroup2],
+ dict(description=u'Foo')),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command group not found' % sudocmdgroup2),
+ ),
+
+
+ ##### clean up test Command
+
+ dict(
+ desc='Now delete the sudo command %r' % sudocmd1,
+ command=('sudocmd_del', [sudocmd1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ value=sudocmd1,
+ summary=u'Deleted Sudo Command "%s"' % sudocmd1,
+ )
+ ),
+
+
+ dict(
+ desc='Verify that %r is really gone' % sudocmd1,
+ command=('sudocmd_show', [sudocmd1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: sudo command not found' % sudocmd1),
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/test_sudorule_plugin.py b/ipatests/test_xmlrpc/test_sudorule_plugin.py
new file mode 100644
index 000000000..ec5d16d62
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_sudorule_plugin.py
@@ -0,0 +1,781 @@
+# Authors:
+# Jr Aquino <jr.aquino@citrixonline.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+"""
+Test the `ipalib/plugins/sudorule.py` module.
+"""
+
+from nose.tools import raises, assert_raises # pylint: disable=E0611
+
+from xmlrpc_test import XMLRPC_test, assert_attr_equal
+from ipalib import api
+from ipalib import errors
+
+class test_sudorule(XMLRPC_test):
+ """
+ Test the `sudorule` plugin.
+ """
+ rule_name = u'testing_sudorule1'
+ rule_name2 = u'testing_sudorule2'
+ rule_command = u'/usr/bin/testsudocmd1'
+ rule_desc = u'description'
+ rule_desc_mod = u'description modified'
+
+ test_user = u'sudorule_test_user'
+ test_external_user = u'external_test_user'
+ test_group = u'sudorule_test_group'
+ test_external_group = u'external_test_group'
+ test_host = u'sudorule.testhost'
+ test_external_host = u'external.testhost'
+ test_hostgroup = u'sudorule_test_hostgroup'
+ test_sudoallowcmdgroup = u'sudorule_test_allowcmdgroup'
+ test_sudodenycmdgroup = u'sudorule_test_denycmdgroup'
+ test_command = u'/usr/bin/testsudocmd1'
+ test_denycommand = u'/usr/bin/testdenysudocmd1'
+ test_runasuser = u'manager'
+ test_runasgroup = u'manager'
+ test_category = u'all'
+ test_option = u'authenticate'
+
+ test_invalid_user = u'+invalid#user'
+ test_invalid_host = u'+invalid&host.nonexist.com'
+ test_invalid_group = u'+invalid#group'
+
+ def test_0_sudorule_add(self):
+ """
+ Test adding a new Sudo rule using `xmlrpc.sudorule_add`.
+ """
+ ret = self.failsafe_add(api.Object.sudorule,
+ self.rule_name,
+ description=self.rule_desc,
+ )
+ entry = ret['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert_attr_equal(entry, 'description', self.rule_desc)
+
+ @raises(errors.DuplicateEntry)
+ def test_1_sudorule_add(self):
+ """
+ Test adding an duplicate Sudo rule using `xmlrpc.sudorule_add'.
+ """
+ api.Command['sudorule_add'](
+ self.rule_name
+ )
+
+ def test_2_sudorule_show(self):
+ """
+ Test displaying a Sudo rule using `xmlrpc.sudorule_show`.
+ """
+ entry = api.Command['sudorule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert_attr_equal(entry, 'description', self.rule_desc)
+
+ def test_3_sudorule_mod(self):
+ """
+ Test modifying a Sudo rule using `xmlrpc.sudorule_mod`.
+ """
+ ret = api.Command['sudorule_mod'](
+ self.rule_name, description=self.rule_desc_mod
+ )
+ entry = ret['result']
+ assert_attr_equal(entry, 'description', self.rule_desc_mod)
+
+ def test_6_sudorule_find(self):
+ """
+ Test searching for Sudo rules using `xmlrpc.sudorule_find`.
+ """
+ ret = api.Command['sudorule_find'](
+ cn=self.rule_name,
+ description=self.rule_desc_mod
+ )
+ assert ret['truncated'] is False
+ entries = ret['result']
+ assert_attr_equal(entries[0], 'cn', self.rule_name)
+ assert_attr_equal(entries[0], 'description', self.rule_desc_mod)
+
+ def test_7_sudorule_init_testing_data(self):
+ """
+ Initialize data for more Sudo rule plugin testing.
+ """
+ self.failsafe_add(api.Object.user,
+ self.test_user, givenname=u'first', sn=u'last'
+ )
+ self.failsafe_add(api.Object.user,
+ self.test_runasuser, givenname=u'first', sn=u'last'
+ )
+ self.failsafe_add(api.Object.group,
+ self.test_group, description=u'description'
+ )
+ self.failsafe_add(api.Object.host,
+ self.test_host, force=True
+ )
+ self.failsafe_add(api.Object.hostgroup,
+ self.test_hostgroup, description=u'description'
+ )
+ self.failsafe_add(api.Object.sudocmdgroup,
+ self.test_sudoallowcmdgroup, description=u'desc'
+ )
+ self.failsafe_add(api.Object.sudocmdgroup,
+ self.test_sudodenycmdgroup, description=u'desc'
+ )
+ self.failsafe_add(api.Object.sudocmd,
+ self.test_command, description=u'desc'
+ )
+
+ def test_8_sudorule_add_user(self):
+ """
+ Test adding user and group to Sudo rule using
+ `xmlrpc.sudorule_add_user`.
+ """
+ ret = api.Command['sudorule_add_user'](
+ self.rule_name, user=self.test_user, group=self.test_group
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberuser' in failed
+ assert 'user' in failed['memberuser']
+ assert not failed['memberuser']['user']
+ assert 'group' in failed['memberuser']
+ assert not failed['memberuser']['group']
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberuser_user', self.test_user)
+ assert_attr_equal(entry, 'memberuser_group', self.test_group)
+
+ def test_9_a_show_user(self):
+ """
+ Test showing a user to verify Sudo rule membership
+ `xmlrpc.user_show`.
+ """
+ ret = api.Command['user_show'](self.test_user, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_sudorule', self.rule_name)
+
+ def test_9_b_show_group(self):
+ """
+ Test showing a group to verify Sudo rule membership
+ `xmlrpc.group_show`.
+ """
+ ret = api.Command['group_show'](self.test_group, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_sudorule', self.rule_name)
+
+ def test_9_sudorule_remove_user(self):
+ """
+ Test removing user and group from Sudo rule using
+ `xmlrpc.sudorule_remove_user'.
+ """
+ ret = api.Command['sudorule_remove_user'](
+ self.rule_name, user=self.test_user, group=self.test_group
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberuser' in failed
+ assert 'user' in failed['memberuser']
+ assert not failed['memberuser']['user']
+ assert 'group' in failed['memberuser']
+ assert not failed['memberuser']['group']
+ entry = ret['result']
+ assert 'memberuser_user' not in entry
+ assert 'memberuser_group' not in entry
+
+ def test_a_sudorule_add_runasuser(self):
+ """
+ Test adding run as user to Sudo rule using
+ `xmlrpc.sudorule_add_runasuser`.
+ """
+ ret = api.Command['sudorule_add_runasuser'](
+ self.rule_name, user=self.test_runasuser
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ assert 'ipasudorunas' in failed
+ assert 'user' in failed['ipasudorunas']
+ assert not failed['ipasudorunas']['user']
+ entry = ret['result']
+ assert_attr_equal(entry, 'ipasudorunas_user', self.test_runasuser)
+
+ def test_a_sudorule_add_runasuser_invalid(self):
+ """
+ Test adding run as invalid user to Sudo rule using
+ `xmlrpc.sudorule_add_runasuser`.
+ """
+ try:
+ api.Command['sudorule_add_runasuser'](
+ self.rule_name, user=self.test_invalid_user
+ )
+ except errors.ValidationError:
+ pass
+ else:
+ assert False
+
+ def test_b_sudorule_remove_runasuser(self):
+ """
+ Test removing run as user to Sudo rule using
+ `xmlrpc.sudorule_remove_runasuser'.
+ """
+ ret = api.Command['sudorule_remove_runasuser'](
+ self.rule_name, user=self.test_runasuser
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ assert 'ipasudorunas' in failed
+ assert 'user' in failed['ipasudorunas']
+ assert not failed['ipasudorunas']['user']
+ entry = ret['result']
+ assert 'ipasudorunas_user' not in entry
+
+ def test_a_sudorule_add_runasgroup(self):
+ """
+ Test adding run as group to Sudo rule using
+ `xmlrpc.sudorule_add_runasgroup`.
+ """
+ ret = api.Command['sudorule_add_runasgroup'](
+ self.rule_name, group=self.test_runasgroup
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ assert 'ipasudorunasgroup' in failed
+ assert 'group' in failed['ipasudorunasgroup']
+ assert not failed['ipasudorunasgroup']['group']
+ entry = ret['result']
+ assert_attr_equal(entry, 'ipasudorunasgroup_group',
+ self.test_runasgroup)
+
+ def test_a_sudorule_add_runasgroup_invalid(self):
+ """
+ Test adding run as invalid user to Sudo rule using
+ `xmlrpc.sudorule_add_runasuser`.
+ """
+ try:
+ api.Command['sudorule_add_runasgroup'](
+ self.rule_name, group=self.test_invalid_group
+ )
+ except errors.ValidationError:
+ pass
+ else:
+ assert False
+
+ def test_b_sudorule_remove_runasgroup(self):
+ """
+ Test removing run as group to Sudo rule using
+ `xmlrpc.sudorule_remove_runasgroup'.
+ """
+ ret = api.Command['sudorule_remove_runasgroup'](
+ self.rule_name, group=self.test_runasgroup
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ assert 'ipasudorunasgroup' in failed
+ assert 'group' in failed['ipasudorunasgroup']
+ assert not failed['ipasudorunasgroup']['group']
+ entry = ret['result']
+ assert 'ipasudorunasgroup_group' not in entry
+
+ def test_a_sudorule_add_externaluser(self):
+ """
+ Test adding an external user to Sudo rule using
+ `xmlrpc.sudorule_add_user`.
+ """
+ ret = api.Command['sudorule_add_user'](
+ self.rule_name, user=self.test_external_user
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert_attr_equal(entry, 'externaluser', self.test_external_user)
+
+ def test_a_sudorule_add_externaluser_invalid(self):
+ """
+ Test adding an invalid external user to Sudo rule using
+ `xmlrpc.sudorule_add_user`.
+ """
+ try:
+ api.Command['sudorule_add_user'](
+ self.rule_name, user=self.test_invalid_user
+ )
+ except errors.ValidationError:
+ pass
+ else:
+ assert False
+
+ def test_b_sudorule_remove_externaluser(self):
+ """
+ Test removing an external user from Sudo rule using
+ `xmlrpc.sudorule_remove_user'.
+ """
+ ret = api.Command['sudorule_remove_user'](
+ self.rule_name, user=self.test_external_user
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert entry['externaluser'] == ()
+
+ def test_a_sudorule_add_runasexternaluser(self):
+ """
+ Test adding an external runasuser to Sudo rule using
+ `xmlrpc.sudorule_add_runasuser`.
+ """
+ ret = api.Command['sudorule_add_runasuser'](
+ self.rule_name, user=self.test_external_user
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert_attr_equal(entry, 'ipasudorunasextuser', self.test_external_user)
+
+ def test_b_sudorule_remove_runasexternaluser(self):
+ """
+ Test removing an external runasuser from Sudo rule using
+ `xmlrpc.sudorule_remove_runasuser'.
+ """
+ ret = api.Command['sudorule_remove_runasuser'](
+ self.rule_name, user=self.test_external_user
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert entry['ipasudorunasextuser'] == ()
+
+ def test_a_sudorule_add_runasexternalgroup(self):
+ """
+ Test adding an external runasgroup to Sudo rule using
+ `xmlrpc.sudorule_add_runasgroup`.
+ """
+ ret = api.Command['sudorule_add_runasgroup'](
+ self.rule_name, group=self.test_external_group
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert_attr_equal(entry, 'ipasudorunasextgroup', self.test_external_group)
+
+ def test_b_sudorule_remove_runasexternalgroup(self):
+ """
+ Test removing an external runasgroup from Sudo rule using
+ `xmlrpc.sudorule_remove_runasgroup'.
+ """
+ ret = api.Command['sudorule_remove_runasgroup'](
+ self.rule_name, group=self.test_external_group
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert entry['ipasudorunasextgroup'] == ()
+
+ def test_a_sudorule_add_option(self):
+ """
+ Test adding an option to Sudo rule using
+ `xmlrpc.sudorule_add_option`.
+ """
+ ret = api.Command['sudorule_add_option'](
+ self.rule_name, ipasudoopt=self.test_option
+ )
+ entry = ret['result']
+ assert_attr_equal(entry, 'ipasudoopt', self.test_option)
+
+ def test_b_sudorule_remove_option(self):
+ """
+ Test removing an option from Sudo rule using
+ `xmlrpc.sudorule_remove_option'.
+ """
+ ret = api.Command['sudorule_remove_option'](
+ self.rule_name, ipasudoopt=self.test_option
+ )
+ entry = ret['result']
+ assert 'ipasudoopt' not in entry
+
+ def test_a_sudorule_add_host(self):
+ """
+ Test adding host and hostgroup to Sudo rule using
+ `xmlrpc.sudorule_add_host`.
+ """
+ ret = api.Command['sudorule_add_host'](
+ self.rule_name, host=self.test_host, hostgroup=self.test_hostgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberhost' in failed
+ assert 'host' in failed['memberhost']
+ assert not failed['memberhost']['host']
+ assert 'hostgroup' in failed['memberhost']
+ assert not failed['memberhost']['hostgroup']
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberhost_host', self.test_host)
+ assert_attr_equal(entry, 'memberhost_hostgroup', self.test_hostgroup)
+
+ def test_a_sudorule_show_host(self):
+ """
+ Test showing host to verify Sudo rule membership
+ `xmlrpc.host_show`.
+ """
+ ret = api.Command['host_show'](self.test_host, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_sudorule', self.rule_name)
+
+ def test_a_sudorule_show_hostgroup(self):
+ """
+ Test showing hostgroup to verify Sudo rule membership
+ `xmlrpc.hostgroup_show`.
+ """
+ ret = api.Command['hostgroup_show'](self.test_hostgroup, all=True)
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberof_sudorule', self.rule_name)
+
+ def test_b_sudorule_remove_host(self):
+ """
+ Test removing host and hostgroup from Sudo rule using
+ `xmlrpc.sudorule_remove_host`.
+ """
+ ret = api.Command['sudorule_remove_host'](
+ self.rule_name, host=self.test_host, hostgroup=self.test_hostgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberhost' in failed
+ assert 'host' in failed['memberhost']
+ assert not failed['memberhost']['host']
+ assert 'hostgroup' in failed['memberhost']
+ assert not failed['memberhost']['hostgroup']
+ entry = ret['result']
+ assert 'memberhost_host' not in entry
+ assert 'memberhost_hostgroup' not in entry
+
+ def test_a_sudorule_add_externalhost(self):
+ """
+ Test adding an external host to Sudo rule using
+ `xmlrpc.sudorule_add_host`.
+ """
+ ret = api.Command['sudorule_add_host'](
+ self.rule_name, host=self.test_external_host
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert_attr_equal(entry, 'externalhost', self.test_external_host)
+
+ def test_a_sudorule_add_externalhost_invalid(self):
+ """
+ Test adding an invalid external host to Sudo rule using
+ `xmlrpc.sudorule_add_host`.
+ """
+ try:
+ api.Command['sudorule_add_host'](
+ self.rule_name, host=self.test_invalid_host
+ )
+ except errors.ValidationError:
+ pass
+ else:
+ assert False
+
+ def test_a_sudorule_mod_externalhost_invalid_addattr(self):
+ """
+ Test adding an invalid external host to Sudo rule using
+ `xmlrpc.sudorule_mod --addattr`.
+ """
+ try:
+ api.Command['sudorule_mod'](
+ self.rule_name,
+ addattr='externalhost=%s' % self.test_invalid_host
+ )
+ except errors.ValidationError, e:
+ assert unicode(e) == ("invalid 'externalhost': only letters, " +
+ "numbers, _, and - are allowed. " +
+ "DNS label may not start or end with -")
+ else:
+ assert False
+
+ def test_b_sudorule_remove_externalhost(self):
+ """
+ Test removing an external host from Sudo rule using
+ `xmlrpc.sudorule_remove_host`.
+ """
+ ret = api.Command['sudorule_remove_host'](
+ self.rule_name, host=self.test_external_host
+ )
+ assert ret['completed'] == 1
+ failed = ret['failed']
+ entry = ret['result']
+ assert len(entry['externalhost']) == 0
+
+ def test_a_sudorule_add_allow_command(self):
+ """
+ Test adding allow command and cmdgroup to Sudo rule using
+ `xmlrpc.sudorule_add_allow_command`.
+ """
+ ret = api.Command['sudorule_add_allow_command'](
+ self.rule_name, sudocmd=self.test_command,
+ sudocmdgroup=self.test_sudoallowcmdgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberallowcmd' in failed
+ assert 'sudocmd' in failed['memberallowcmd']
+ assert not failed['memberallowcmd']['sudocmd']
+ assert 'sudocmdgroup' in failed['memberallowcmd']
+ assert not failed['memberallowcmd']['sudocmdgroup']
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberallowcmd_sudocmd', self.test_command)
+ assert_attr_equal(entry, 'memberallowcmd_sudocmdgroup',
+ self.test_sudoallowcmdgroup)
+
+ def test_a_sudorule_remove_allow_command(self):
+ """
+ Test removing allow command and sudocmdgroup from Sudo rule using
+ `xmlrpc.sudorule_remove_command`.
+ """
+ ret = api.Command['sudorule_remove_allow_command'](
+ self.rule_name, sudocmd=self.test_command,
+ sudocmdgroup=self.test_sudoallowcmdgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberallowcmd' in failed
+ assert 'sudocmd' in failed['memberallowcmd']
+ assert not failed['memberallowcmd']['sudocmd']
+ assert 'sudocmdgroup' in failed['memberallowcmd']
+ assert not failed['memberallowcmd']['sudocmdgroup']
+ entry = ret['result']
+ assert 'memberallowcmd_sudocmd' not in entry
+ assert 'memberallowcmd_sudocmdgroup' not in entry
+
+ def test_b_sudorule_add_deny_command(self):
+ """
+ Test adding deny command and cmdgroup to Sudo rule using
+ `xmlrpc.sudorule_add_deny_command`.
+ """
+ ret = api.Command['sudorule_add_deny_command'](
+ self.rule_name, sudocmd=self.test_command,
+ sudocmdgroup=self.test_sudodenycmdgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberdenycmd' in failed
+ assert 'sudocmd' in failed['memberdenycmd']
+ assert not failed['memberdenycmd']['sudocmd']
+ assert 'sudocmdgroup' in failed['memberdenycmd']
+ assert not failed['memberdenycmd']['sudocmdgroup']
+ entry = ret['result']
+ assert_attr_equal(entry, 'memberdenycmd_sudocmd', self.test_command)
+ assert_attr_equal(entry, 'memberdenycmd_sudocmdgroup',
+ self.test_sudodenycmdgroup)
+
+ def test_b_sudorule_remove_deny_command(self):
+ """
+ Test removing deny command and sudocmdgroup from Sudo rule using
+ `xmlrpc.sudorule_remove_deny_command`.
+ """
+ ret = api.Command['sudorule_remove_deny_command'](
+ self.rule_name, sudocmd=self.test_command,
+ sudocmdgroup=self.test_sudodenycmdgroup
+ )
+ assert ret['completed'] == 2
+ failed = ret['failed']
+ assert 'memberdenycmd' in failed
+ assert 'sudocmd' in failed['memberdenycmd']
+ assert not failed['memberdenycmd']['sudocmd']
+ assert 'sudocmdgroup' in failed['memberdenycmd']
+ assert not failed['memberdenycmd']['sudocmdgroup']
+ entry = ret['result']
+ assert 'memberdenycmd_sudocmd' not in entry
+ assert 'memberdenycmd_sudocmdgroup' not in entry
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_c_sudorule_exclusiveuser(self):
+ """
+ Test adding a user to an Sudo rule when usercat='all'
+ """
+ api.Command['sudorule_mod'](self.rule_name, usercategory=u'all')
+ try:
+ api.Command['sudorule_add_user'](self.rule_name, user=u'admin')
+ finally:
+ api.Command['sudorule_mod'](self.rule_name, usercategory=u'')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_d_sudorule_exclusiveuser(self):
+ """
+ Test setting usercat='all' in an Sudo rule when there are users
+ """
+ api.Command['sudorule_add_user'](self.rule_name, user=u'admin')
+ try:
+ api.Command['sudorule_mod'](self.rule_name, usercategory=u'all')
+ finally:
+ api.Command['sudorule_remove_user'](self.rule_name, user=u'admin')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_e_sudorule_exclusivehost(self):
+ """
+ Test adding a host to an Sudo rule when hostcat='all'
+ """
+ api.Command['sudorule_mod'](self.rule_name, hostcategory=u'all')
+ try:
+ api.Command['sudorule_add_host'](self.rule_name, host=self.test_host)
+ finally:
+ api.Command['sudorule_mod'](self.rule_name, hostcategory=u'')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_f_sudorule_exclusivehost(self):
+ """
+ Test setting hostcat='all' in an Sudo rule when there are hosts
+ """
+ api.Command['sudorule_add_host'](self.rule_name, host=self.test_host)
+ try:
+ api.Command['sudorule_mod'](self.rule_name, hostcategory=u'all')
+ finally:
+ api.Command['sudorule_remove_host'](self.rule_name, host=self.test_host)
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_g_sudorule_exclusivecommand(self):
+ """
+ Test adding a command to an Sudo rule when cmdcategory='all'
+ """
+ api.Command['sudorule_mod'](self.rule_name, cmdcategory=u'all')
+ try:
+ api.Command['sudorule_add_allow_command'](self.rule_name, sudocmd=self.test_command)
+ finally:
+ api.Command['sudorule_mod'](self.rule_name, cmdcategory=u'')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_h_sudorule_exclusivecommand(self):
+ """
+ Test setting cmdcategory='all' in an Sudo rule when there are commands
+ """
+ api.Command['sudorule_add_allow_command'](self.rule_name, sudocmd=self.test_command)
+ try:
+ api.Command['sudorule_mod'](self.rule_name, cmdcategory=u'all')
+ finally:
+ api.Command['sudorule_remove_allow_command'](self.rule_name, sudocmd=self.test_command)
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_i_sudorule_exclusiverunas(self):
+ """
+ Test adding a runasuser to an Sudo rule when ipasudorunasusercategory='all'
+ """
+ api.Command['sudorule_mod'](self.rule_name, ipasudorunasusercategory=u'all')
+ try:
+ api.Command['sudorule_add_runasuser'](self.rule_name, user=self.test_user)
+ finally:
+ api.Command['sudorule_mod'](self.rule_name, ipasudorunasusercategory=u'')
+
+ @raises(errors.MutuallyExclusiveError)
+ def test_j_1_sudorule_exclusiverunas(self):
+ """
+ Test setting ipasudorunasusercategory='all' in an Sudo rule when there are runas users
+ """
+ api.Command['sudorule_add_runasuser'](self.rule_name, user=self.test_user)
+ try:
+ api.Command['sudorule_mod'](self.rule_name, ipasudorunasusercategory=u'all')
+ finally:
+ api.Command['sudorule_remove_runasuser'](self.rule_name, user=self.test_command)
+
+ def test_j_2_sudorule_referential_integrity(self):
+ """
+ Test adding various links to Sudo rule
+ """
+ api.Command['sudorule_add_user'](self.rule_name, user=self.test_user)
+ api.Command['sudorule_add_runasuser'](self.rule_name, user=self.test_runasuser,
+ group=self.test_group)
+ api.Command['sudorule_add_runasgroup'](self.rule_name, group=self.test_group)
+ api.Command['sudorule_add_host'](self.rule_name, host=self.test_host)
+ api.Command['sudorule_add_allow_command'](self.rule_name,
+ sudocmd=self.test_command)
+ api.Command['sudorule_add_deny_command'](self.rule_name,
+ sudocmdgroup=self.test_sudodenycmdgroup)
+ entry = api.Command['sudorule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert_attr_equal(entry, 'memberuser_user', self.test_user)
+ assert_attr_equal(entry, 'memberallowcmd_sudocmd', self.test_command)
+ assert_attr_equal(entry, 'memberdenycmd_sudocmdgroup',
+ self.test_sudodenycmdgroup)
+ assert_attr_equal(entry, 'memberhost_host', self.test_host)
+ assert_attr_equal(entry, 'ipasudorunas_user', self.test_runasuser)
+ assert_attr_equal(entry, 'ipasudorunas_group', self.test_group)
+ assert_attr_equal(entry, 'ipasudorunasgroup_group', self.test_group)
+
+
+ def test_k_1_sudorule_clear_testing_data(self):
+ """
+ Clear data for Sudo rule plugin testing.
+ """
+ api.Command['user_del'](self.test_user)
+ api.Command['user_del'](self.test_runasuser)
+ api.Command['group_del'](self.test_group)
+ api.Command['host_del'](self.test_host)
+ api.Command['hostgroup_del'](self.test_hostgroup)
+ api.Command['sudorule_remove_allow_command'](self.rule_name,
+ sudocmd=self.test_command)
+ api.Command['sudocmd_del'](self.test_command)
+ api.Command['sudocmdgroup_del'](self.test_sudoallowcmdgroup)
+ api.Command['sudocmdgroup_del'](self.test_sudodenycmdgroup)
+
+ def test_k_2_sudorule_referential_integrity(self):
+ """
+ Test that links in Sudo rule were removed by referential integrity plugin
+ """
+ entry = api.Command['sudorule_show'](self.rule_name)['result']
+ assert_attr_equal(entry, 'cn', self.rule_name)
+ assert 'memberuser_user' not in entry
+ assert 'memberallowcmd_sudocmd' not in entry
+ assert 'memberdenycmd_sudocmdgroup' not in entry
+ assert 'memberhost_host' not in entry
+ assert 'ipasudorunas_user' not in entry
+ assert 'ipasudorunas_group' not in entry
+ assert 'ipasudorunasgroup_group' not in entry
+
+ def test_l_sudorule_order(self):
+ """
+ Test that order uniqueness is maintained
+ """
+ api.Command['sudorule_mod'](self.rule_name, sudoorder=1)
+
+ api.Command['sudorule_add'](self.rule_name2)
+
+ # mod of rule that has no order and set a duplicate
+ try:
+ api.Command['sudorule_mod'](self.rule_name2, sudoorder=1)
+ except errors.ValidationError:
+ pass
+
+ # Remove the rule so we can re-add it
+ api.Command['sudorule_del'](self.rule_name2)
+
+ # add a new rule with a duplicate order
+ with assert_raises(errors.ValidationError):
+ api.Command['sudorule_add'](self.rule_name2, sudoorder=1)
+
+ # add a new rule with a unique order
+ api.Command['sudorule_add'](self.rule_name2, sudoorder=2)
+ with assert_raises(errors.ValidationError):
+ api.Command['sudorule_mod'](self.rule_name2, sudoorder=1)
+
+ # Try setting both to 0
+ api.Command['sudorule_mod'](self.rule_name2, sudoorder=0)
+ with assert_raises(errors.ValidationError):
+ api.Command['sudorule_mod'](self.rule_name, sudoorder=0)
+
+
+ def test_m_sudorule_del(self):
+ """
+ Test deleting a Sudo rule using `xmlrpc.sudorule_del`.
+ """
+ api.Command['sudorule_del'](self.rule_name)
+ # verify that it's gone
+ with assert_raises(errors.NotFound):
+ api.Command['sudorule_show'](self.rule_name)
+ api.Command['sudorule_del'](self.rule_name2)
diff --git a/ipatests/test_xmlrpc/test_trust_plugin.py b/ipatests/test_xmlrpc/test_trust_plugin.py
new file mode 100644
index 000000000..0223e8b36
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_trust_plugin.py
@@ -0,0 +1,159 @@
+# Authors:
+# Martin Kosek <mkosek@redhat.com>
+#
+# Copyright (C) 2010 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/>.
+"""
+Test the `ipalib/plugins/trust.py` module.
+"""
+
+import nose
+from ipalib import api, errors
+from ipapython.dn import DN
+from ipatests.test_xmlrpc import objectclasses
+from xmlrpc_test import (Declarative, fuzzy_guid, fuzzy_domain_sid, fuzzy_string,
+ fuzzy_uuid, fuzzy_digits)
+
+
+trustconfig_ad_config = DN(('cn', api.env.domain),
+ api.env.container_cifsdomains, api.env.basedn)
+testgroup = u'adtestgroup'
+testgroup_dn = DN(('cn', testgroup), api.env.container_group, api.env.basedn)
+
+default_group = u'Default SMB Group'
+default_group_dn = DN(('cn', default_group), api.env.container_group, api.env.basedn)
+
+class test_trustconfig(Declarative):
+
+ @classmethod
+ def setUpClass(cls):
+ super(test_trustconfig, cls).setUpClass()
+ if not api.Backend.xmlclient.isconnected():
+ api.Backend.xmlclient.connect(fallback=False)
+ try:
+ api.Command['trustconfig_show'](trust_type=u'ad')
+ except errors.NotFound:
+ raise nose.SkipTest('Trusts are not configured')
+
+ cleanup_commands = [
+ ('group_del', [testgroup], {}),
+ ('trustconfig_mod', [], {'trust_type': u'ad',
+ 'ipantfallbackprimarygroup': default_group}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Retrieve trust configuration for AD domains',
+ command=('trustconfig_show', [], {'trust_type': u'ad'}),
+ expected={
+ 'value': u'ad',
+ 'summary': None,
+ 'result': {
+ 'dn': trustconfig_ad_config,
+ 'cn': [api.env.domain],
+ 'ipantdomainguid': [fuzzy_guid],
+ 'ipantfallbackprimarygroup': [default_group],
+ 'ipantflatname': [fuzzy_string],
+ 'ipantsecurityidentifier': [fuzzy_domain_sid]
+ },
+ },
+ ),
+
+ dict(
+ desc='Retrieve trust configuration for AD domains with --raw',
+ command=('trustconfig_show', [], {'trust_type': u'ad', 'raw': True}),
+ expected={
+ 'value': u'ad',
+ 'summary': None,
+ 'result': {
+ 'dn': trustconfig_ad_config,
+ 'cn': [api.env.domain],
+ 'ipantdomainguid': [fuzzy_guid],
+ 'ipantfallbackprimarygroup': [default_group_dn],
+ 'ipantflatname': [fuzzy_string],
+ 'ipantsecurityidentifier': [fuzzy_domain_sid]
+ },
+ },
+ ),
+
+ dict(
+ desc='Create auxiliary group %r' % testgroup,
+ command=(
+ 'group_add', [testgroup], dict(description=u'Test group')
+ ),
+ expected=dict(
+ value=testgroup,
+ summary=u'Added group "%s"' % testgroup,
+ result=dict(
+ cn=[testgroup],
+ description=[u'Test group'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=testgroup_dn,
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Try to change primary fallback group to nonexistent group',
+ command=('trustconfig_mod', [],
+ {'trust_type': u'ad', 'ipantfallbackprimarygroup': u'doesnotexist'}),
+ expected=errors.NotFound(reason=u'%s: group not found' % 'doesnotexist')
+ ),
+
+ dict(
+ desc='Try to change primary fallback group to nonexistent group DN',
+ command=('trustconfig_mod', [], {'trust_type': u'ad',
+ 'ipantfallbackprimarygroup': u'cn=doesnotexist,dc=test'}),
+ expected=errors.NotFound(reason=u'%s: group not found' % 'cn=doesnotexist,dc=test')
+ ),
+
+ dict(
+ desc='Change primary fallback group to "%s"' % testgroup,
+ command=('trustconfig_mod', [], {'trust_type': u'ad',
+ 'ipantfallbackprimarygroup': testgroup}),
+ expected={
+ 'value': u'ad',
+ 'summary': u'Modified "ad" trust configuration',
+ 'result': {
+ 'cn': [api.env.domain],
+ 'ipantdomainguid': [fuzzy_guid],
+ 'ipantfallbackprimarygroup': [testgroup],
+ 'ipantflatname': [fuzzy_string],
+ 'ipantsecurityidentifier': [fuzzy_domain_sid]
+ },
+ },
+ ),
+
+ dict(
+ desc='Change primary fallback group back to "%s" using DN' % default_group,
+ command=('trustconfig_mod', [], {'trust_type': u'ad',
+ 'ipantfallbackprimarygroup': unicode(default_group_dn)}),
+ expected={
+ 'value': u'ad',
+ 'summary': u'Modified "ad" trust configuration',
+ 'result': {
+ 'cn': [api.env.domain],
+ 'ipantdomainguid': [fuzzy_guid],
+ 'ipantfallbackprimarygroup': [default_group],
+ 'ipantflatname': [fuzzy_string],
+ 'ipantsecurityidentifier': [fuzzy_domain_sid]
+ },
+ },
+ ),
+ ]
diff --git a/ipatests/test_xmlrpc/test_user_plugin.py b/ipatests/test_xmlrpc/test_user_plugin.py
new file mode 100644
index 000000000..ca6ff16c6
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_user_plugin.py
@@ -0,0 +1,1837 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2008, 2009 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/>.
+
+"""
+Test the `ipalib/plugins/user.py` module.
+"""
+
+from ipalib import api, errors, messages
+from ipatests.test_xmlrpc import objectclasses
+from ipatests.util import assert_equal, assert_not_equal
+from xmlrpc_test import Declarative, fuzzy_digits, fuzzy_uuid, fuzzy_password, fuzzy_string, fuzzy_dergeneralizedtime
+from ipapython.dn import DN
+from ipapython.version import API_VERSION
+
+user1=u'tuser1'
+user2=u'tuser2'
+admin1=u'admin'
+admin2=u'admin2'
+renameduser1=u'tuser'
+group1=u'group1'
+admins_group=u'admins'
+
+invaliduser1=u'+tuser1'
+invaliduser2=u'tuser1234567890123456789012345678901234567890'
+
+sshpubkey = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGAX3xAeLeaJggwTqMjxNwa6XHBUAikXPGMzEpVrlLDCZtv00djsFTBi38PkgxBJVkgRWMrcBsr/35lq7P6w8KGIwA8GI48Z0qBS2NBMJ2u9WQ2hjLN6GdMlo77O0uJY3251p12pCVIS/bHRSq8kHO2No8g7KA9fGGcagPfQH+ee3t7HUkpbQkFTmbPPN++r3V8oVUk5LxbryB3UIIVzNmcSIn3JrXynlvui4MixvrtX6zx+O/bBo68o8/eZD26QrahVbA09fivrn/4h3TM019Eu/c2jOdckfU3cHUV/3Tno5d6JicibyaoDDK7S/yjdn5jhaz8MSEayQvFkZkiF0L public key test'
+sshpubkeyfp = u'13:67:6B:BF:4E:A2:05:8E:AE:25:8B:A1:31:DE:6F:1B public key test (ssh-rsa)'
+
+def get_user_dn(uid):
+ return DN(('uid', uid), api.env.container_user, api.env.basedn)
+
+def get_group_dn(cn):
+ return DN(('cn', cn), api.env.container_group, api.env.basedn)
+
+def upg_check(response):
+ """Check that the user was assigned to the corresponding private group."""
+ assert_equal(response['result']['uidnumber'],
+ response['result']['gidnumber'])
+ return True
+
+def not_upg_check(response):
+ """Check that the user was not assigned to the corresponding private group."""
+ assert_not_equal(response['result']['uidnumber'],
+ response['result']['gidnumber'])
+ return True
+
+class test_user(Declarative):
+
+ cleanup_commands = [
+ ('user_del', [user1, user2, renameduser1, admin2], {'continue': True}),
+ ('group_del', [group1], {}),
+ ('automember_default_group_remove', [], {'type': u'group'}),
+ ]
+
+ tests = [
+
+ dict(
+ desc='Try to retrieve non-existent "%s"' % user1,
+ command=('user_show', [user1], {}),
+ expected=errors.NotFound(reason=u'%s: user not found' % user1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent "%s"' % user1,
+ command=('user_mod', [user1], dict(givenname=u'Foo')),
+ expected=errors.NotFound(reason=u'%s: user not found' % user1),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=errors.NotFound(reason=u'%s: user not found' % user1),
+ ),
+
+
+ dict(
+ desc='Try to rename non-existent "%s"' % user1,
+ command=('user_mod', [user1], dict(setattr=u'uid=%s' % renameduser1)),
+ expected=errors.NotFound(reason=u'%s: user not found' % user1),
+ ),
+
+
+ dict(
+ desc='Create "%s"' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ extra_check = upg_check,
+ ),
+
+
+ dict(
+ desc='Try to create duplicate "%s"' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=errors.DuplicateEntry(
+ message=u'user with name "%s" already exists' % user1),
+ ),
+
+
+ dict(
+ desc='Retrieve "%s"' % user1,
+ command=(
+ 'user_show', [user1], {}
+ ),
+ expected=dict(
+ result=dict(
+ dn=get_user_dn(user1),
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ value=user1,
+ summary=None,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for "%s" with all=True' % user1,
+ command=(
+ 'user_find', [user1], {'all': True}
+ ),
+ expected=dict(
+ result=[
+ {
+ 'dn': get_user_dn(user1),
+ 'cn': [u'Test User1'],
+ 'gecos': [u'Test User1'],
+ 'givenname': [u'Test'],
+ 'homedirectory': [u'/home/tuser1'],
+ 'krbprincipalname': [u'tuser1@' + api.env.realm],
+ 'loginshell': [u'/bin/sh'],
+ 'memberof_group': [u'ipausers'],
+ 'objectclass': objectclasses.user,
+ 'sn': [u'User1'],
+ 'uid': [user1],
+ 'uidnumber': [fuzzy_digits],
+ 'gidnumber': [fuzzy_digits],
+ 'ipauniqueid': [fuzzy_uuid],
+ 'mepmanagedentry': [get_group_dn(user1)],
+ 'krbpwdpolicyreference': [DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ 'nsaccountlock': False,
+ 'has_keytab': False,
+ 'has_password': False,
+ 'displayname': [u'Test User1'],
+ 'cn': [u'Test User1'],
+ 'initials': [u'TU'],
+ 'mail': [u'%s@%s' % (user1, api.env.domain)],
+ },
+ ],
+ summary=u'1 user matched',
+ count=1, truncated=False,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for "%s" with pkey-only=True' % user1,
+ command=(
+ 'user_find', [user1], {'pkey_only': True}
+ ),
+ expected=dict(
+ result=[
+ {
+ 'dn': get_user_dn(user1),
+ 'uid': [user1],
+ },
+ ],
+ summary=u'1 user matched',
+ count=1, truncated=False,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for "%s" with minimal attributes' % user1,
+ command=(
+ 'user_find', [user1], {}
+ ),
+ expected=dict(
+ result=[
+ dict(
+ dn=get_user_dn(user1),
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ ),
+ ],
+ summary=u'1 user matched',
+ count=1,
+ truncated=False,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for all users',
+ command=(
+ 'user_find', [], {}
+ ),
+ expected=dict(
+ result=[
+ dict(
+ dn=get_user_dn(admin1),
+ homedirectory=[u'/home/admin'],
+ loginshell=[u'/bin/bash'],
+ sn=[u'Administrator'],
+ uid=[admin1],
+ nsaccountlock=False,
+ has_keytab=True,
+ has_password=True,
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ ),
+ dict(
+ dn=get_user_dn(user1),
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ ),
+ ],
+ summary=u'2 users matched',
+ count=2,
+ truncated=False,
+ ),
+ ),
+
+
+ dict(
+ desc='Search for all users with a limit of 1',
+ command=(
+ 'user_find', [], dict(sizelimit=1,),
+ ),
+ expected=dict(
+ result=[
+ dict(
+ dn=get_user_dn(admin1),
+ homedirectory=[u'/home/admin'],
+ loginshell=[u'/bin/bash'],
+ sn=[u'Administrator'],
+ uid=[admin1],
+ nsaccountlock=False,
+ has_keytab=True,
+ has_password=True,
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ ),
+ ],
+ summary=u'1 user matched',
+ count=1,
+ truncated=True,
+ ),
+ ),
+
+
+ dict(
+ desc='Disable "%s"' % user1,
+ command=(
+ 'user_disable', [user1], {}
+ ),
+ expected=dict(
+ result=True,
+ value=user1,
+ summary=u'Disabled user account "%s"' % user1,
+ ),
+ ),
+
+ dict(
+ desc='Assert user is disabled',
+ command=('user_find', [user1], {}),
+ expected=dict(
+ result=[lambda d: d['nsaccountlock'] == True],
+ summary=u'1 user matched',
+ count=1,
+ truncated=False,
+ ),
+ ),
+
+ dict(
+ desc='Enable "%s"' % user1,
+ command=(
+ 'user_enable', [user1], {}
+ ),
+ expected=dict(
+ result=True,
+ value=user1,
+ summary=u'Enabled user account "%s"' % user1,
+ ),
+ ),
+
+ dict(
+ desc='Assert user "%s" is enabled' % user1,
+ command=('user_find', [user1], {}),
+ expected=dict(
+ result=[lambda d: d['nsaccountlock'] == False],
+ summary=u'1 user matched',
+ count=1,
+ truncated=False,
+ ),
+ ),
+
+ dict(
+ desc='Disable "%s" using setattr' % user1,
+ command=('user_mod', [user1], dict(setattr=u'nsaccountlock=True')),
+ expected=dict(
+ result=lambda d: d['nsaccountlock'] == True,
+ value=user1,
+ summary=u'Modified user "%s"' % user1,
+ ),
+ ),
+
+ dict(
+ desc='Enable "%s" using setattr' % user1,
+ command=('user_mod', [user1], dict(setattr=u'nsaccountlock=False')),
+ expected=dict(
+ result=lambda d: d['nsaccountlock'] == False,
+ value=user1,
+ summary=u'Modified user "%s"' % user1,
+ ),
+ ),
+
+ dict(
+ desc='Disable "%s" using user_mod' % user1,
+ command=('user_mod', [user1], dict(nsaccountlock=True)),
+ expected=dict(
+ result=lambda d: d['nsaccountlock'] == True,
+ value=user1,
+ summary=u'Modified user "%s"' % user1,
+ ),
+ ),
+
+ dict(
+ desc='Enable "%s" using user_mod' % user1,
+ command=('user_mod', [user1], dict(nsaccountlock=False)),
+ expected=dict(
+ result=lambda d: d['nsaccountlock'] == False,
+ value=user1,
+ summary=u'Modified user "%s"' % user1,
+ ),
+ ),
+
+ dict(
+ desc='Try setting virtual attribute on "%s" using setattr' % user1,
+ command=('user_mod', [user1], dict(setattr=u'random=xyz123')),
+ expected=errors.ObjectclassViolation(
+ info='attribute "random" not allowed'),
+ ),
+
+ dict(
+ desc='Update "%s"' % user1,
+ command=(
+ 'user_mod', [user1], dict(givenname=u'Finkle')
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Try updating the krb ticket policy of "%s"' % user1,
+ command=(
+ 'user_mod', [user1], dict(setattr=u'krbmaxticketlife=88000')
+ ),
+ expected=errors.ObjectclassViolation(
+ info=u'attribute "krbmaxticketlife" not allowed'),
+ ),
+
+
+ dict(
+ desc='Retrieve "%s" to verify update' % user1,
+ command=('user_show', [user1], {}),
+ expected=dict(
+ result=dict(
+ dn=get_user_dn(user1),
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=None,
+ value=user1,
+ ),
+
+ ),
+
+
+ dict(
+ desc='Rename "%s"' % user1,
+ command=('user_mod', [user1], dict(setattr=u'uid=%s' % renameduser1)),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[renameduser1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Rename "%s" to same value' % renameduser1,
+ command=('user_mod', [renameduser1], dict(setattr=u'uid=%s' % renameduser1)),
+ expected=errors.EmptyModlist(),
+ ),
+
+
+ dict(
+ desc='Rename back "%s"' % renameduser1,
+ command=('user_mod', [renameduser1], dict(setattr=u'uid=%s' % user1)),
+ expected=dict(
+ result=dict(
+ givenname=[u'Finkle'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ memberof_group=[u'ipausers'],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "%s"' % renameduser1,
+ value=renameduser1,
+ ),
+ ),
+
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Try to delete non-existent "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=errors.NotFound(reason=u'tuser1: user not found'),
+ ),
+
+
+ dict(
+ desc='Create user "%s" with krb ticket policy' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ setattr=u'krbmaxticketlife=88000')
+ ),
+ expected=errors.ObjectclassViolation(info='attribute "krbmaxticketlife" not allowed'),
+ ),
+
+
+ dict(
+ desc='Create "%s" with SSH public key' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1', ipasshpubkey=[sshpubkey])
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ ipasshpubkey=[sshpubkey],
+ sshpubkeyfp=[sshpubkeyfp],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ extra_check = upg_check,
+ ),
+
+
+ dict(
+ desc='Add an illegal SSH public key to "%r"' % user1,
+ command=('user_mod', [user1], dict(ipasshpubkey=[u"anal nathrach orth' bhais's bethad do che'l de'nmha"])),
+ expected=errors.ValidationError(name='sshpubkey',
+ error=u'invalid SSH public key'),
+ ),
+
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+
+ dict(
+ desc='Create "%s"' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ extra_check = upg_check,
+ ),
+
+
+ dict(
+ desc='Create "%s"' % user2,
+ command=(
+ 'user_add', [user2], dict(givenname=u'Test', sn=u'User2')
+ ),
+ expected=dict(
+ value=user2,
+ summary=u'Added user "%s"' % user2,
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user2)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user2),
+ ),
+ ),
+ extra_check = upg_check,
+ ),
+
+
+ dict(
+ desc='Make non-existent "%s" the manager of "%s"' % (renameduser1, user2),
+ command=('user_mod', [user2], dict(manager=renameduser1)),
+ expected=errors.NotFound(
+ reason=u'manager %s not found' % renameduser1),
+ ),
+
+
+ dict(
+ desc='Make "%s" the manager of "%s"' % (user1, user2),
+ command=('user_mod', [user2], dict(manager=user1)),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ memberof_group=[u'ipausers'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ manager=[user1],
+ ),
+ summary=u'Modified user "%s"' % user2,
+ value=user2,
+ ),
+ ),
+
+ dict(
+ desc='Search for "%s" with manager "%s"' % (user2, user1),
+ command=(
+ 'user_find', [user2], {'manager': user1}
+ ),
+ expected=dict(
+ result=[
+ dict(
+ dn=get_user_dn(user2),
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User2'],
+ uid=[user2],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ manager=[user1],
+ ),
+ ],
+ summary=u'1 user matched',
+ count=1,
+ truncated=False,
+ ),
+ ),
+
+ dict(
+ desc='Delete "%s" and "%s" at the same time' % (user1, user2),
+ command=('user_del', [user1, user2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "tuser1,tuser2"',
+ value=u','.join((user1, user2)),
+ ),
+ ),
+
+ dict(
+ desc='Try to retrieve non-existent "%s"' % user1,
+ command=('user_show', [user1], {}),
+ expected=errors.NotFound(reason=u'%s: user not found' % user1),
+ ),
+
+
+ dict(
+ desc='Try to update non-existent "%s"' % user1,
+ command=('user_mod', [user1], dict(givenname=u'Foo')),
+ expected=errors.NotFound(reason=u'%s: user not found' % user1),
+ ),
+
+
+ dict(
+ desc='Test an invalid login name "%s"' % invaliduser1,
+ command=('user_add', [invaliduser1], dict(givenname=u'Test', sn=u'User1')),
+ expected=errors.ValidationError(name='login',
+ error=u'may only include letters, numbers, _, -, . and $'),
+ ),
+
+
+ dict(
+ desc='Test a login name that is too long "%s"' % invaliduser2,
+ command=('user_add', [invaliduser2],
+ dict(givenname=u'Test', sn=u'User1')),
+ expected=errors.ValidationError(name='login',
+ error='can be at most 32 characters'),
+ ),
+
+
+ # 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 deletes',
+ command=('user_del', [invaliduser1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: user not found' % invaliduser1),
+ ),
+
+
+ dict(
+ desc='Test that validation is disabled on show',
+ command=('user_show', [invaliduser1], {}),
+ expected=errors.NotFound(
+ reason=u'%s: user not found' % invaliduser1),
+ ),
+
+
+ dict(
+ desc='Test that validation is disabled on find',
+ command=('user_find', [invaliduser1], {}),
+ expected=dict(
+ count=0,
+ truncated=False,
+ summary=u'0 users matched',
+ result=[],
+ ),
+ ),
+
+
+ dict(
+ desc='Try to rename to invalid username "%s"' % user1,
+ command=('user_mod', [user1], dict(rename=invaliduser1)),
+ expected=errors.ValidationError(name='rename',
+ error=u'may only include letters, numbers, _, -, . and $'),
+ ),
+
+
+ dict(
+ desc='Try to rename to a username that is too long "%s"' % user1,
+ command=('user_mod', [user1], dict(rename=invaliduser2)),
+ expected=errors.ValidationError(name='login',
+ error='can be at most 32 characters'),
+ ),
+
+
+ dict(
+ desc='Create "%s"' % group1,
+ command=(
+ 'group_add', [group1], dict(description=u'Test desc')
+ ),
+ expected=dict(
+ value=group1,
+ summary=u'Added group "%s"' % group1,
+ result=dict(
+ cn=[group1],
+ description=[u'Test desc'],
+ gidnumber=[fuzzy_digits],
+ objectclass=objectclasses.group + [u'posixgroup'],
+ ipauniqueid=[fuzzy_uuid],
+ dn=get_group_dn(group1),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Try to user "%s" where the managed group exists' % group1,
+ command=(
+ 'user_add', [group1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=errors.ManagedGroupExistsError(group=group1)
+ ),
+
+
+ dict(
+ desc='Create "%s" with a full address' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ street=u'123 Maple Rd', l=u'Anytown', st=u'MD',
+ telephonenumber=u'410-555-1212', postalcode=u'01234-5678')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ street=[u'123 Maple Rd'],
+ l=[u'Anytown'],
+ st=[u'MD'],
+ postalcode=[u'01234-5678'],
+ telephonenumber=[u'410-555-1212'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Create "%s" with random password' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1', random=True)
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=True,
+ has_password=True,
+ randompassword=fuzzy_password,
+ krbextradata=[fuzzy_string],
+ krbpasswordexpiration=[fuzzy_dergeneralizedtime],
+ krblastpwdchange=[fuzzy_dergeneralizedtime],
+ dn=get_user_dn(user1),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Create "%s"' % user2,
+ command=(
+ 'user_add', [user2], dict(givenname=u'Test', sn=u'User2')
+ ),
+ expected=dict(
+ value=user2,
+ summary=u'Added user "%s"' % user2,
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user2)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user2),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Modify "%s" with random password' % user2,
+ command=(
+ 'user_mod', [user2], dict(random=True)
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ memberof_group=[u'ipausers'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ nsaccountlock=False,
+ has_keytab=True,
+ has_password=True,
+ randompassword=fuzzy_password,
+ ),
+ summary=u'Modified user "%s"' % user2,
+ value=user2,
+ ),
+ ),
+
+ dict(
+ desc='Delete "%s"' % user2,
+ command=('user_del', [user2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user2,
+ value=user2,
+ ),
+ ),
+
+ dict(
+ desc='Create user "%s" with upper-case principal' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ krbprincipalname=user1.upper())
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Create user "%s" with bad realm in principal' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ krbprincipalname='%s@NOTFOUND.ORG' % user1)
+ ),
+ expected=errors.RealmMismatch()
+ ),
+
+
+ dict(
+ desc='Create user "%s" with malformed principal' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1',
+ krbprincipalname='%s@BAD@NOTFOUND.ORG' % user1)
+ ),
+ expected=errors.MalformedUserPrincipal(principal='%s@BAD@NOTFOUND.ORG' % user1),
+ ),
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Change default home directory',
+ command=(
+ 'config_mod', [], dict(ipahomesrootdir=u'/other-home'),
+ ),
+ expected=lambda x, output: x is None,
+ ),
+
+ dict(
+ desc='Create user "%s" with different default home directory' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/other-home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ ),
+
+
+ dict(
+ desc='Reset default home directory',
+ command=(
+ 'config_mod', [], dict(ipahomesrootdir=u'/home'),
+ ),
+ expected=lambda x, output: x is None,
+ ),
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Change default login shell',
+ command=(
+ 'config_mod', [], dict(ipadefaultloginshell=u'/usr/bin/ipython'),
+ ),
+ expected=lambda x, output: x is None,
+ ),
+
+ dict(
+ desc='Create user "%s" with different default login shell' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/usr/bin/ipython'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ initials=[u'TU'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Reset default login shell',
+ command=(
+ 'config_mod', [], dict(ipadefaultloginshell=u'/bin/sh'),
+ ),
+ expected=lambda x, output: x is None,
+ ),
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Create "%s" without UPG' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1', noprivate=True)
+ ),
+ expected=errors.NotFound(reason='Default group for new users is not POSIX'),
+ ),
+
+ dict(
+ desc='Create "%s" without UPG with GID explicitly set' % user2,
+ command=(
+ 'user_add', [user2], dict(givenname=u'Test', sn=u'User2', noprivate=True, gidnumber=1000)
+ ),
+ expected=dict(
+ value=user2,
+ summary=u'Added user "%s"' % user2,
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ description=[],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user_base,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[u'1000'],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user2),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Delete "%s"' % user2,
+ command=('user_del', [user2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user2,
+ value=user2,
+ ),
+ ),
+
+ dict(
+ desc='Change default user group',
+ command=(
+ 'config_mod', [], dict(ipadefaultprimarygroup=group1),
+ ),
+ expected=lambda x, output: x is None,
+ ),
+
+ dict(
+ desc='Create "%s" without UPG' % user1,
+ command=(
+ 'user_add', [user1], dict(givenname=u'Test', sn=u'User1', noprivate=True)
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ description=[],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user_base,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ memberof_group=[group1],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ extra_check = not_upg_check,
+ ),
+
+ dict(
+ desc='Create "%s" without UPG with GID explicitly set' % user2,
+ command=(
+ 'user_add', [user2], dict(givenname=u'Test', sn=u'User2', noprivate=True, gidnumber=1000)
+ ),
+ expected=dict(
+ value=user2,
+ summary=u'Added user "%s"' % user2,
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ description=[],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user_base,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[u'1000'],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ memberof_group=[group1],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user2),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Set %r as manager of %r' % (user1, user2),
+ command=(
+ 'user_mod', [user2], dict(manager=user1)
+ ),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ memberof_group=[group1],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ manager=[user1],
+ ),
+ summary=u'Modified user "%s"' % user2,
+ value=user2,
+ ),
+ ),
+
+ dict(
+ desc='Rename "%s"' % user1,
+ command=('user_mod', [user1], dict(rename=renameduser1)),
+ expected=dict(
+ result=dict(
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ loginshell=[u'/bin/sh'],
+ sn=[u'User1'],
+ uid=[renameduser1],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ memberof_group=[group1],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ ),
+ summary=u'Modified user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Retrieve %r and check that manager is renamed' % user2,
+ command=(
+ 'user_show', [user2], {'all': True}
+ ),
+ expected=dict(
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user_base,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[u'1000'],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ memberof_group=[group1],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user2),
+ manager=[renameduser1],
+ ),
+ value=user2,
+ summary=None,
+ ),
+ ),
+
+ dict(
+ desc='Delete %r' % renameduser1,
+ command=('user_del', [renameduser1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % renameduser1,
+ value=renameduser1,
+ ),
+ ),
+
+ dict(
+ desc='Retrieve %r and check that manager is gone' % user2,
+ command=(
+ 'user_show', [user2], {'all': True}
+ ),
+ expected=dict(
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user_base,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[u'1000'],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ memberof_group=[group1],
+ nsaccountlock=False,
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user2),
+ ),
+ value=user2,
+ summary=None,
+ ),
+ ),
+
+ dict(
+ desc='Reset default user group',
+ command=(
+ 'config_mod', [], dict(ipadefaultprimarygroup=u'ipausers'),
+ ),
+ expected=lambda x, output: x is None,
+ ),
+
+ dict(
+ desc='Try to remove the original admin user "%s"' % admin1,
+ command=('user_del', [admin1], {}),
+ expected=errors.LastMemberError(key=admin1, label=u'group',
+ container=admins_group),
+ ),
+
+ dict(
+ desc='Try to disable the original admin user "%s"' % admin1,
+ command=('user_disable', [admin1], {}),
+ expected=errors.LastMemberError(key=admin1, label=u'group',
+ container=admins_group),
+ ),
+
+
+ dict(
+ desc='Create 2nd admin user "%s"' % admin2,
+ command=(
+ 'user_add', [admin2], dict(givenname=u'Second', sn=u'Admin')
+ ),
+ expected=dict(
+ value=admin2,
+ summary=u'Added user "%s"' % admin2,
+ result=dict(
+ gecos=[u'Second Admin'],
+ givenname=[u'Second'],
+ homedirectory=[u'/home/admin2'],
+ krbprincipalname=[u'admin2@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'Admin'],
+ uid=[admin2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ displayname=[u'Second Admin'],
+ cn=[u'Second Admin'],
+ initials=[u'SA'],
+ mail=[u'%s@%s' % (admin2, api.env.domain)],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(admin2)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(admin2),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Add "%s" to the admins group "%s"' % (admin2, admins_group),
+ command=('group_add_member', [admins_group], dict(user=admin2)),
+ expected=dict(
+ completed=1,
+ failed=dict(
+ member=dict(
+ group=tuple(),
+ user=tuple(),
+ ),
+ ),
+ result={
+ 'dn': get_group_dn(admins_group),
+ 'member_user': [admin1, admin2],
+ 'gidnumber': [fuzzy_digits],
+ 'cn': [admins_group],
+ 'description': [u'Account administrators group'],
+ },
+ ),
+ ),
+
+
+ dict(
+ desc='Retrieve admins group "%s" to verify membership is "%s","%s"' % (admins_group, admin1, admin2),
+ command=('group_show', [admins_group], {}),
+ expected=dict(
+ value=admins_group,
+ result=dict(
+ cn=[admins_group],
+ gidnumber=[fuzzy_digits],
+ description=[u'Account administrators group'],
+ dn=get_group_dn(admins_group),
+ member_user=[admin1, admin2],
+ ),
+ summary=None,
+ ),
+ ),
+
+ dict(
+ desc='Disable 2nd admin user "%s", admins group "%s" should also contain enabled "%s"' % (admin2, admins_group, admin1),
+ command=(
+ 'user_disable', [admin2], {}
+ ),
+ expected=dict(
+ result=True,
+ value=admin2,
+ summary=u'Disabled user account "%s"' % admin2,
+ ),
+ ),
+
+ dict(
+ desc='Assert 2nd admin user "%s" is disabled' % admin2,
+ command=('user_find', [admin2], {}),
+ expected=dict(
+ result=[lambda d: d['nsaccountlock'] == True],
+ summary=u'1 user matched',
+ count=1,
+ truncated=False,
+ ),
+ ),
+
+ dict(
+ desc='Try to disable the origin admin user "%s"' % admin1,
+ command=('user_disable', [admin1], {}),
+ expected=errors.LastMemberError(key=admin1, label=u'group',
+ container=admins_group),
+ ),
+
+ dict(
+ desc='Try to remove the original admin user "%s"' % admin1,
+ command=('user_del', [admin1], {}),
+ expected=errors.LastMemberError(key=admin1, label=u'group',
+ container=admins_group),
+ ),
+
+ dict(
+ desc='Delete 2nd admin "%s"' % admin2,
+ command=('user_del', [admin2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % admin2,
+ value=admin2,
+ ),
+ ),
+
+ dict(
+ desc='Retrieve admins group "%s" to verify membership is "%s"' % (admins_group, admin1),
+ command=('group_show', [admins_group], {}),
+ expected=dict(
+ value=admins_group,
+ result=dict(
+ cn=[admins_group],
+ gidnumber=[fuzzy_digits],
+ description=[u'Account administrators group'],
+ dn=get_group_dn(admins_group),
+ member_user=[admin1],
+ ),
+ summary=None,
+ ),
+ ),
+
+ dict(
+ desc='Assert original admin user "%s" is enabled' % admin1,
+ command=('user_find', [admin1], {}),
+ expected=dict(
+ result=[lambda d: d['nsaccountlock'] == False],
+ summary=u'1 user matched',
+ count=1,
+ truncated=False,
+ ),
+ ),
+
+ dict(
+ desc='Try to remove the original admin user "%s"' % admin1,
+ command=('user_del', [admin1], {}),
+ expected=errors.LastMemberError(key=admin1, label=u'group',
+ container=admins_group),
+ ),
+
+ dict(
+ desc='Try to disable the original admin user "%s"' % admin1,
+ command=('user_disable', [admin1], {}),
+ expected=errors.LastMemberError(key=admin1, label=u'group',
+ container=admins_group),
+ ),
+
+ dict(
+ desc='Set default automember group for groups as ipausers',
+ command=(
+ 'automember_default_group_set', [], dict(
+ type=u'group',
+ automemberdefaultgroup=u'ipausers'
+ )
+ ),
+ expected=dict(
+ result=dict(
+ cn=[u'Group'],
+ automemberdefaultgroup=[DN(('cn', 'ipausers'), ('cn', 'groups'), ('cn', 'accounts'), api.env.basedn)],
+ ),
+ value=u'group',
+ summary=u'Set default (fallback) group for automember "group"',
+ ),
+ ),
+
+ dict(
+ desc='Delete "%s"' % user2,
+ command=('user_del', [user2], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user2,
+ value=user2,
+ ),
+ ),
+
+ dict(
+ desc='Create %r' % user2,
+ command=(
+ 'user_add', [user2], dict(givenname=u'Test', sn=u'User2')
+ ),
+ expected=dict(
+ value=user2,
+ summary=u'Added user "tuser2"',
+ result=dict(
+ gecos=[u'Test User2'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser2'],
+ krbprincipalname=[u'tuser2@' + api.env.realm],
+ has_keytab=False,
+ has_password=False,
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User2'],
+ uid=[user2],
+ uidnumber=[fuzzy_digits],
+ gidnumber=[fuzzy_digits],
+ mail=[u'%s@%s' % (user2, api.env.domain)],
+ displayname=[u'Test User2'],
+ cn=[u'Test User2'],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn', 'global_policy'), ('cn', api.env.realm), ('cn', 'kerberos'),
+ api.env.basedn)],
+ mepmanagedentry=[DN(('cn', user2), ('cn', 'groups'), ('cn', 'accounts'),
+ api.env.basedn)],
+ memberof_group=[u'ipausers'],
+ dn=DN(('uid', 'tuser2'), ('cn', 'users'), ('cn', 'accounts'),
+ api.env.basedn),
+ ),
+ ),
+ ),
+
+ dict(
+ desc='Create "%s" with UID 999' % user1,
+ command=(
+ 'user_add', [user1], dict(
+ givenname=u'Test', sn=u'User1', uidnumber=999)
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[u'999'],
+ gidnumber=[u'999'],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ extra_check = upg_check,
+ ),
+
+ dict(
+ desc='Delete "%s"' % user1,
+ command=('user_del', [user1], {}),
+ expected=dict(
+ result=dict(failed=u''),
+ summary=u'Deleted user "%s"' % user1,
+ value=user1,
+ ),
+ ),
+
+ dict(
+ desc='Create "%s" with old DNA_MAGIC uid 999' % user1,
+ command=(
+ 'user_add', [user1], dict(
+ givenname=u'Test', sn=u'User1', uidnumber=999,
+ version=u'2.49')
+ ),
+ expected=dict(
+ value=user1,
+ summary=u'Added user "%s"' % user1,
+ result=dict(
+ gecos=[u'Test User1'],
+ givenname=[u'Test'],
+ homedirectory=[u'/home/tuser1'],
+ krbprincipalname=[u'tuser1@' + api.env.realm],
+ loginshell=[u'/bin/sh'],
+ objectclass=objectclasses.user,
+ sn=[u'User1'],
+ uid=[user1],
+ uidnumber=[lambda v: int(v) != 999],
+ gidnumber=[lambda v: int(v) != 999],
+ displayname=[u'Test User1'],
+ cn=[u'Test User1'],
+ mail=[u'%s@%s' % (user1, api.env.domain)],
+ initials=[u'TU'],
+ ipauniqueid=[fuzzy_uuid],
+ krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+ ('cn','kerberos'),api.env.basedn)],
+ mepmanagedentry=[get_group_dn(user1)],
+ memberof_group=[u'ipausers'],
+ has_keytab=False,
+ has_password=False,
+ dn=get_user_dn(user1),
+ ),
+ ),
+ extra_check = upg_check,
+ ),
+
+ ]
diff --git a/ipatests/test_xmlrpc/xmlrpc_test.py b/ipatests/test_xmlrpc/xmlrpc_test.py
new file mode 100644
index 000000000..bfe8efa46
--- /dev/null
+++ b/ipatests/test_xmlrpc/xmlrpc_test.py
@@ -0,0 +1,329 @@
+# Authors:
+# Rob Crittenden <rcritten@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/>.
+
+"""
+Base class for all XML-RPC tests
+"""
+
+import sys
+import socket
+import nose
+from ipatests.util import assert_deepequal, Fuzzy
+from ipalib import api, request, errors
+from ipalib.x509 import valid_issuer
+from ipapython.version import API_VERSION
+
+
+# Matches a gidnumber like '1391016742'
+# FIXME: Does it make more sense to return gidnumber, uidnumber, etc. as `int`
+# or `long`? If not, we still need to return them as `unicode` instead of `str`.
+fuzzy_digits = Fuzzy('^\d+$', type=basestring)
+
+uuid_re = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
+
+# Matches an ipauniqueid like u'784d85fd-eae7-11de-9d01-54520012478b'
+fuzzy_uuid = Fuzzy('^%s$' % uuid_re)
+
+# Matches trusted domain GUID, like u'463bf2be-3456-4a57-979e-120304f2a0eb'
+fuzzy_guid = fuzzy_uuid
+
+# Matches SID of a trusted domain
+# SID syntax: http://msdn.microsoft.com/en-us/library/ff632068.aspx
+_sid_identifier_authority = '(0x[0-9a-f]{1,12}|[0-9]{1,10})'
+fuzzy_domain_sid = Fuzzy(
+ '^S-1-5-21-%(idauth)s-%(idauth)s-%(idauth)s$' % dict(idauth=_sid_identifier_authority)
+)
+fuzzy_user_or_group_sid = Fuzzy(
+ '^S-1-5-21-%(idauth)s-%(idauth)s-%(idauth)s-%(idauth)s$' % dict(idauth=_sid_identifier_authority)
+)
+
+# Matches netgroup dn. Note (?i) at the beginning of the regexp is the ingnore case flag
+fuzzy_netgroupdn = Fuzzy(
+ '(?i)ipauniqueid=%s,cn=ng,cn=alt,%s' % (uuid_re, api.env.basedn)
+)
+
+# Matches sudocmd dn
+fuzzy_sudocmddn = Fuzzy(
+ '(?i)ipauniqueid=%s,cn=sudocmds,cn=sudo,%s' % (uuid_re, api.env.basedn)
+)
+
+# Matches a hash signature, not enforcing length
+fuzzy_hash = Fuzzy('^([a-f0-9][a-f0-9]:)+[a-f0-9][a-f0-9]$', type=basestring)
+
+# Matches a date, like Tue Apr 26 17:45:35 2016 UTC
+fuzzy_date = Fuzzy('^[a-zA-Z]{3} [a-zA-Z]{3} \d{2} \d{2}:\d{2}:\d{2} \d{4} UTC$')
+
+fuzzy_issuer = Fuzzy(type=basestring, test=lambda issuer: valid_issuer(issuer))
+
+fuzzy_hex = Fuzzy('^0x[0-9a-fA-F]+$', type=basestring)
+
+# Matches password - password consists of all printable characters without whitespaces
+# The only exception is space, but space cannot be at the beggingin or end of the pwd
+fuzzy_password = Fuzzy('^\S([\S ]*\S)*$')
+
+# Matches generalized time value. Time format is: %Y%m%d%H%M%SZ
+fuzzy_dergeneralizedtime = Fuzzy('^[0-9]{14}Z$')
+
+# match any string
+fuzzy_string = Fuzzy(type=basestring)
+
+# case insensitive match of sets
+def fuzzy_set_ci(s):
+ return Fuzzy(test=lambda other: set(x.lower() for x in other) == set(y.lower() for y in s))
+
+try:
+ if not api.Backend.xmlclient.isconnected():
+ api.Backend.xmlclient.connect(fallback=False)
+ res = api.Command['user_show'](u'notfound')
+except errors.NetworkError:
+ server_available = False
+except IOError:
+ server_available = False
+except errors.NotFound:
+ server_available = True
+
+
+
+def assert_attr_equal(entry, key, value):
+ if type(entry) is not dict:
+ raise AssertionError(
+ 'assert_attr_equal: entry must be a %r; got a %r: %r' % (
+ dict, type(entry), entry)
+ )
+ if key not in entry:
+ raise AssertionError(
+ 'assert_attr_equal: entry has no key %r: %r' % (key, entry)
+ )
+ if value not in entry[key]:
+ raise AssertionError(
+ 'assert_attr_equal: %r: %r not in %r' % (key, value, entry[key])
+ )
+
+
+def assert_is_member(entry, value, key='member'):
+ if type(entry) is not dict:
+ raise AssertionError(
+ 'assert_is_member: entry must be a %r; got a %r: %r' % (
+ dict, type(entry), entry)
+ )
+ if key not in entry:
+ raise AssertionError(
+ 'assert_is_member: entry has no key %r: %r' % (key, entry)
+ )
+ for member in entry[key]:
+ if member.startswith(value):
+ return
+ raise AssertionError(
+ 'assert_is_member: %r: %r not in %r' % (key, value, entry[key])
+ )
+
+
+# Initialize the API. We do this here so that one can run the tests
+# individually instead of at the top-level. If API.bootstrap()
+# has already been called we continue gracefully. Other errors will be
+# raised.
+
+class XMLRPC_test(object):
+ """
+ Base class for all XML-RPC plugin tests
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ if not server_available:
+ raise nose.SkipTest('%r: Server not available: %r' %
+ (cls.__module__, api.env.xmlrpc_uri))
+
+ def setUp(self):
+ if not api.Backend.xmlclient.isconnected():
+ api.Backend.xmlclient.connect(fallback=False)
+
+ def tearDown(self):
+ """
+ nose tear-down fixture.
+ """
+ request.destroy_context()
+
+ def failsafe_add(self, obj, pk, **options):
+ """
+ Delete possible leftover entry first, then add.
+
+ This helps speed us up when a partial test failure has left LDAP in a
+ dirty state.
+
+ :param obj: An Object like api.Object.user
+ :param pk: The primary key of the entry to be created
+ :param options: Kwargs to be passed to obj.add()
+ """
+ try:
+ obj.methods['del'](pk)
+ except errors.NotFound:
+ pass
+ return obj.methods['add'](pk, **options)
+
+
+IGNORE = """Command %r is missing attribute %r in output entry.
+ args = %r
+ options = %r
+ entry = %r"""
+
+
+EXPECTED = """Expected %r to raise %s.
+ args = %r
+ options = %r
+ output = %r"""
+
+
+UNEXPECTED = """Expected %r to raise %s, but caught different.
+ args = %r
+ options = %r
+ %s: %s"""
+
+
+KWARGS = """Command %r raised %s with wrong kwargs.
+ args = %r
+ options = %r
+ kw_expected = %r
+ kw_got = %r"""
+
+
+class Declarative(XMLRPC_test):
+ """A declarative-style test suite
+
+ A Declarative test suite is controlled by the ``tests`` and
+ ``cleanup_commands`` class variables.
+
+ The ``tests`` is a list of dictionaries with the following keys:
+
+ ``desc``
+ A name/description of the test
+ ``command``
+ A (command, args, kwargs) triple specifying the command to run
+ ``expected``
+ Can be either an ``errors.PublicError`` instance, in which case
+ the command must fail with the given error; or the
+ expected result.
+ The result is checked with ``tests.util.assert_deepequal``.
+ ``extra_check`` (optional)
+ A checking function that is called with the response. It must
+ return true for the test to pass.
+
+ The ``cleanup_commands`` is a list of (command, args, kwargs)
+ triples. These are commands get run both before and after tests,
+ and must not fail.
+ """
+
+ cleanup_commands = tuple()
+ tests = tuple()
+
+ def cleanup_generate(self, stage):
+ for (i, command) in enumerate(self.cleanup_commands):
+ func = lambda: self.cleanup(command)
+ func.description = '%s %s-cleanup[%d]: %r' % (
+ self.__class__.__name__, stage, i, command
+ )
+ yield (func,)
+
+ def cleanup(self, command):
+ (cmd, args, options) = command
+ if cmd not in api.Command:
+ raise nose.SkipTest(
+ 'cleanup command %r not in api.Command' % cmd
+ )
+ try:
+ api.Command[cmd](*args, **options)
+ except (errors.NotFound, errors.EmptyModlist):
+ pass
+
+ def test_generator(self):
+ """
+ Iterate through tests.
+
+ nose reports each one as a seperate test.
+ """
+
+ # Iterate through pre-cleanup:
+ for tup in self.cleanup_generate('pre'):
+ yield tup
+
+ # Iterate through the tests:
+ name = self.__class__.__name__
+ for (i, test) in enumerate(self.tests):
+ nice = '%s[%d]: %s: %s' % (
+ name, i, test['command'][0], test.get('desc', '')
+ )
+ func = lambda: self.check(nice, **test)
+ func.description = nice
+ yield (func,)
+
+ # Iterate through post-cleanup:
+ for tup in self.cleanup_generate('post'):
+ yield tup
+
+ def check(self, nice, desc, command, expected, extra_check=None):
+ (cmd, args, options) = command
+ options.setdefault('version', API_VERSION)
+ if cmd not in api.Command:
+ raise nose.SkipTest('%r not in api.Command' % cmd)
+ if isinstance(expected, errors.PublicError):
+ self.check_exception(nice, cmd, args, options, expected)
+ elif hasattr(expected, '__call__'):
+ self.check_callable(nice, cmd, args, options, expected)
+ else:
+ self.check_output(nice, cmd, args, options, expected, extra_check)
+
+ def check_exception(self, nice, cmd, args, options, expected):
+ klass = expected.__class__
+ name = klass.__name__
+ try:
+ output = api.Command[cmd](*args, **options)
+ except StandardError, e:
+ pass
+ else:
+ raise AssertionError(
+ EXPECTED % (cmd, name, args, options, output)
+ )
+ if not isinstance(e, klass):
+ raise AssertionError(
+ UNEXPECTED % (cmd, name, args, options, e.__class__.__name__, e)
+ )
+ # FIXME: the XML-RPC transport doesn't allow us to return structured
+ # information through the exception, so we can't test the kw on the
+ # client side. However, if we switch to using JSON-RPC for the default
+ # transport, the exception is a free-form data structure (dict).
+ # For now just compare the strings
+ assert_deepequal(expected.strerror, e.strerror)
+
+ def check_callable(self, nice, cmd, args, options, expected):
+ output = dict()
+ e = None
+ try:
+ output = api.Command[cmd](*args, **options)
+ except StandardError, e:
+ pass
+ if not expected(e, output):
+ raise AssertionError(
+ UNEXPECTED % (cmd, args, options, e.__class__.__name__, e)
+ )
+
+ def check_output(self, nice, cmd, args, options, expected, extra_check):
+ got = api.Command[cmd](*args, **options)
+ assert_deepequal(expected, got, nice)
+ if extra_check and not extra_check(got):
+ raise AssertionError('Extra check %s failed' % extra_check)
diff --git a/ipatests/util.py b/ipatests/util.py
new file mode 100644
index 000000000..117d2c834
--- /dev/null
+++ b/ipatests/util.py
@@ -0,0 +1,637 @@
+# Authors:
+# Jason Gerard DeRose <jderose@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/>.
+
+"""
+Common utility functions and classes for unit tests.
+"""
+
+import inspect
+import os
+from os import path
+import tempfile
+import shutil
+import re
+import ipalib
+from ipalib.plugable import Plugin
+from ipalib.request import context
+from ipapython.dn import DN
+
+class TempDir(object):
+ def __init__(self):
+ self.__path = tempfile.mkdtemp(prefix='ipa.tests.')
+ assert self.path == self.__path
+
+ def __get_path(self):
+ assert path.abspath(self.__path) == self.__path
+ assert self.__path.startswith('/tmp/ipa.tests.')
+ assert path.isdir(self.__path) and not path.islink(self.__path)
+ return self.__path
+ path = property(__get_path)
+
+ def rmtree(self):
+ if self.__path is not None:
+ shutil.rmtree(self.path)
+ self.__path = None
+
+ def makedirs(self, *parts):
+ d = self.join(*parts)
+ if not path.exists(d):
+ os.makedirs(d)
+ assert path.isdir(d) and not path.islink(d)
+ return d
+
+ def touch(self, *parts):
+ d = self.makedirs(*parts[:-1])
+ f = path.join(d, parts[-1])
+ assert not path.exists(f)
+ open(f, 'w').close()
+ assert path.isfile(f) and not path.islink(f)
+ return f
+
+ def write(self, content, *parts):
+ d = self.makedirs(*parts[:-1])
+ f = path.join(d, parts[-1])
+ assert not path.exists(f)
+ open(f, 'w').write(content)
+ assert path.isfile(f) and not path.islink(f)
+ return f
+
+ def join(self, *parts):
+ return path.join(self.path, *parts)
+
+ def __del__(self):
+ self.rmtree()
+
+
+class TempHome(TempDir):
+ def __init__(self):
+ super(TempHome, self).__init__()
+ self.__home = os.environ['HOME']
+ os.environ['HOME'] = self.path
+
+
+class ExceptionNotRaised(Exception):
+ """
+ Exception raised when an *expected* exception is *not* raised during a
+ unit test.
+ """
+ msg = 'expected %s'
+
+ def __init__(self, expected):
+ self.expected = expected
+
+ def __str__(self):
+ return self.msg % self.expected.__name__
+
+
+def assert_equal(val1, val2):
+ """
+ Assert ``val1`` and ``val2`` are the same type and of equal value.
+ """
+ assert type(val1) is type(val2), '%r != %r' % (val1, val2)
+ assert val1 == val2, '%r != %r' % (val1, val2)
+
+
+def assert_not_equal(val1, val2):
+ """
+ Assert ``val1`` and ``val2`` are the same type and of non-equal value.
+ """
+ assert type(val1) is type(val2), '%r != %r' % (val1, val2)
+ assert val1 != val2, '%r == %r' % (val1, val2)
+
+
+class Fuzzy(object):
+ """
+ Perform a fuzzy (non-strict) equality tests.
+
+ `Fuzzy` instances will likely be used when comparing nesting data-structures
+ using `assert_deepequal()`.
+
+ By default a `Fuzzy` instance is equal to everything. For example, all of
+ these evaluate to ``True``:
+
+ >>> Fuzzy() == False
+ True
+ >>> 7 == Fuzzy() # Order doesn't matter
+ True
+ >>> Fuzzy() == u'Hello False, Lucky 7!'
+ True
+
+ The first optional argument *regex* is a regular expression pattern to
+ match. For example, you could match a phone number like this:
+
+ >>> phone = Fuzzy('^\d{3}-\d{3}-\d{4}$')
+ >>> u'123-456-7890' == phone
+ True
+
+ Use of a regular expression by default implies the ``unicode`` type, so
+ comparing with an ``str`` instance will evaluate to ``False``:
+
+ >>> phone.type
+ <type 'unicode'>
+ >>> '123-456-7890' == phone
+ False
+
+ The *type* kwarg allows you to specify a type constraint, so you can force
+ the above to work on ``str`` instances instead:
+
+ >>> '123-456-7890' == Fuzzy('^\d{3}-\d{3}-\d{4}$', type=str)
+ True
+
+ You can also use the *type* constraint on its own without the *regex*, for
+ example:
+
+ >>> 42 == Fuzzy(type=int)
+ True
+ >>> 42.0 == Fuzzy(type=int)
+ False
+ >>> 42.0 == Fuzzy(type=(int, float))
+ True
+
+ Finally the *test* kwarg is an optional callable that will be called to
+ perform the loose equality test. For example:
+
+ >>> 42 == Fuzzy(test=lambda other: other > 42)
+ False
+ >>> 43 == Fuzzy(test=lambda other: other > 42)
+ True
+
+ You can use *type* and *test* together. For example:
+
+ >>> 43 == Fuzzy(type=float, test=lambda other: other > 42)
+ False
+ >>> 42.5 == Fuzzy(type=float, test=lambda other: other > 42)
+ True
+
+ The *regex*, *type*, and *test* kwargs are all availabel via attributes on
+ the `Fuzzy` instance:
+
+ >>> fuzzy = Fuzzy('.+', type=str, test=lambda other: True)
+ >>> fuzzy.regex
+ '.+'
+ >>> fuzzy.type
+ <type 'str'>
+ >>> fuzzy.test # doctest:+ELLIPSIS
+ <function <lambda> at 0x...>
+
+ To aid debugging, `Fuzzy.__repr__()` revealse these kwargs as well:
+
+ >>> fuzzy # doctest:+ELLIPSIS
+ Fuzzy('.+', <type 'str'>, <function <lambda> at 0x...>)
+ """
+
+ def __init__(self, regex=None, type=None, test=None):
+ """
+ Initialize.
+
+ :param regex: A regular expression pattern to match, e.g.
+ ``u'^\d+foo'``
+
+ :param type: A type or tuple of types to test using ``isinstance()``,
+ e.g. ``(int, float)``
+
+ :param test: A callable used to perform equality test, e.g.
+ ``lambda other: other >= 18``
+ """
+ assert regex is None or isinstance(regex, basestring)
+ assert test is None or callable(test)
+ if regex is None:
+ self.re = None
+ else:
+ self.re = re.compile(regex)
+ if type is None:
+ type = unicode
+ assert type in (unicode, str, basestring)
+ self.regex = regex
+ self.type = type
+ self.test = test
+
+ def __repr__(self):
+ return '%s(%r, %r, %r)' % (
+ self.__class__.__name__, self.regex, self.type, self.test
+ )
+
+ def __eq__(self, other):
+ if not (self.type is None or isinstance(other, self.type)):
+ return False
+ if not (self.re is None or self.re.search(other)):
+ return False
+ if not (self.test is None or self.test(other)):
+ return False
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+VALUE = """assert_deepequal: expected != got.
+ %s
+ expected = %r
+ got = %r
+ path = %r"""
+
+TYPE = """assert_deepequal: type(expected) is not type(got).
+ %s
+ type(expected) = %r
+ type(got) = %r
+ expected = %r
+ got = %r
+ path = %r"""
+
+LEN = """assert_deepequal: list length mismatch.
+ %s
+ len(expected) = %r
+ len(got) = %r
+ expected = %r
+ got = %r
+ path = %r"""
+
+KEYS = """assert_deepequal: dict keys mismatch.
+ %s
+ missing keys = %r
+ extra keys = %r
+ expected = %r
+ got = %r
+ path = %r"""
+
+
+def assert_deepequal(expected, got, doc='', stack=tuple()):
+ """
+ Recursively check for type and equality.
+
+ If a value in expected is callable then it will used as a callback to
+ test for equality on the got value. The callback is passed the got
+ value and returns True if equal, False otherwise.
+
+ If the tests fails, it will raise an ``AssertionError`` with detailed
+ information, including the path to the offending value. For example:
+
+ >>> expected = [u'Hello', dict(world=u'how are you?')]
+ >>> got = [u'Hello', dict(world='how are you?')]
+ >>> expected == got
+ True
+ >>> assert_deepequal(expected, got, doc='Testing my nested data')
+ Traceback (most recent call last):
+ ...
+ AssertionError: assert_deepequal: type(expected) is not type(got).
+ Testing my nested data
+ type(expected) = <type 'unicode'>
+ type(got) = <type 'str'>
+ expected = u'how are you?'
+ got = 'how are you?'
+ path = (0, 'world')
+
+ Note that lists and tuples are considered equivalent, and the order of
+ their elements does not matter.
+ """
+ if isinstance(expected, tuple):
+ expected = list(expected)
+ if isinstance(got, tuple):
+ got = list(got)
+ if isinstance(expected, DN):
+ if isinstance(got, basestring):
+ got = DN(got)
+ if not (isinstance(expected, Fuzzy) or callable(expected) or type(expected) is type(got)):
+ raise AssertionError(
+ TYPE % (doc, type(expected), type(got), expected, got, stack)
+ )
+ if isinstance(expected, (list, tuple)):
+ if len(expected) != len(got):
+ raise AssertionError(
+ LEN % (doc, len(expected), len(got), expected, got, stack)
+ )
+ # Sort list elements, unless they are dictionaries
+ if expected and isinstance(expected[0], dict):
+ s_got = got
+ s_expected = expected
+ else:
+ s_got = sorted(got)
+ s_expected = sorted(expected)
+ for (i, e_sub) in enumerate(s_expected):
+ g_sub = s_got[i]
+ assert_deepequal(e_sub, g_sub, doc, stack + (i,))
+ elif isinstance(expected, dict):
+ missing = set(expected).difference(got)
+ extra = set(got).difference(expected)
+ if missing or extra:
+ raise AssertionError(KEYS % (
+ doc, sorted(missing), sorted(extra), expected, got, stack
+ )
+ )
+ for key in sorted(expected):
+ e_sub = expected[key]
+ g_sub = got[key]
+ assert_deepequal(e_sub, g_sub, doc, stack + (key,))
+ elif callable(expected):
+ if not expected(got):
+ raise AssertionError(
+ VALUE % (doc, expected, got, stack)
+ )
+ elif expected != got:
+ raise AssertionError(
+ VALUE % (doc, expected, got, stack)
+ )
+
+
+def raises(exception, callback, *args, **kw):
+ """
+ Tests that the expected exception is raised; raises ExceptionNotRaised
+ if test fails.
+ """
+ raised = False
+ try:
+ callback(*args, **kw)
+ except exception, e:
+ raised = True
+ if not raised:
+ raise ExceptionNotRaised(exception)
+ return e
+
+
+def getitem(obj, key):
+ """
+ Works like getattr but for dictionary interface. Use this in combination
+ with raises() to test that, for example, KeyError is raised.
+ """
+ return obj[key]
+
+
+def setitem(obj, key, value):
+ """
+ Works like setattr but for dictionary interface. Use this in combination
+ with raises() to test that, for example, TypeError is raised.
+ """
+ obj[key] = value
+
+
+def delitem(obj, key):
+ """
+ Works like delattr but for dictionary interface. Use this in combination
+ with raises() to test that, for example, TypeError is raised.
+ """
+ del obj[key]
+
+
+def no_set(obj, name, value='some_new_obj'):
+ """
+ Tests that attribute cannot be set.
+ """
+ raises(AttributeError, setattr, obj, name, value)
+
+
+def no_del(obj, name):
+ """
+ Tests that attribute cannot be deleted.
+ """
+ raises(AttributeError, delattr, obj, name)
+
+
+def read_only(obj, name, value='some_new_obj'):
+ """
+ Tests that attribute is read-only. Returns attribute.
+ """
+ # Test that it cannot be set:
+ no_set(obj, name, value)
+
+ # Test that it cannot be deleted:
+ no_del(obj, name)
+
+ # Return the attribute
+ return getattr(obj, name)
+
+
+def is_prop(prop):
+ return type(prop) is property
+
+
+class ClassChecker(object):
+ __cls = None
+ __subcls = None
+
+ def __get_cls(self):
+ if self.__cls is None:
+ self.__cls = self._cls
+ assert inspect.isclass(self.__cls)
+ return self.__cls
+ cls = property(__get_cls)
+
+ def __get_subcls(self):
+ if self.__subcls is None:
+ self.__subcls = self.get_subcls()
+ assert inspect.isclass(self.__subcls)
+ return self.__subcls
+ subcls = property(__get_subcls)
+
+ def get_subcls(self):
+ raise NotImplementedError(
+ self.__class__.__name__,
+ 'get_subcls()'
+ )
+
+ def tearDown(self):
+ """
+ nose tear-down fixture.
+ """
+ context.__dict__.clear()
+
+
+
+
+
+
+
+
+def check_TypeError(value, type_, name, callback, *args, **kw):
+ """
+ Tests a standard TypeError raised with `errors.raise_TypeError`.
+ """
+ e = raises(TypeError, callback, *args, **kw)
+ assert e.value is value
+ assert e.type is type_
+ assert e.name == name
+ assert type(e.name) is str
+ assert str(e) == ipalib.errors.TYPE_ERROR % (name, type_, value)
+ return e
+
+
+def get_api(**kw):
+ """
+ Returns (api, home) tuple.
+
+ This function returns a tuple containing an `ipalib.plugable.API`
+ instance and a `TempHome` instance.
+ """
+ home = TempHome()
+ api = ipalib.create_api(mode='unit_test')
+ api.env.in_tree = True
+ for (key, value) in kw.iteritems():
+ api.env[key] = value
+ return (api, home)
+
+
+def create_test_api(**kw):
+ """
+ Returns (api, home) tuple.
+
+ This function returns a tuple containing an `ipalib.plugable.API`
+ instance and a `TempHome` instance.
+ """
+ home = TempHome()
+ api = ipalib.create_api(mode='unit_test')
+ api.env.in_tree = True
+ for (key, value) in kw.iteritems():
+ api.env[key] = value
+ return (api, home)
+
+
+class PluginTester(object):
+ __plugin = None
+
+ def __get_plugin(self):
+ if self.__plugin is None:
+ self.__plugin = self._plugin
+ assert issubclass(self.__plugin, Plugin)
+ return self.__plugin
+ plugin = property(__get_plugin)
+
+ def register(self, *plugins, **kw):
+ """
+ Create a testing api and register ``self.plugin``.
+
+ This method returns an (api, home) tuple.
+
+ :param plugins: Additional \*plugins to register.
+ :param kw: Additional \**kw args to pass to `create_test_api`.
+ """
+ (api, home) = create_test_api(**kw)
+ api.register(self.plugin)
+ for p in plugins:
+ api.register(p)
+ return (api, home)
+
+ def finalize(self, *plugins, **kw):
+ (api, home) = self.register(*plugins, **kw)
+ api.finalize()
+ return (api, home)
+
+ def instance(self, namespace, *plugins, **kw):
+ (api, home) = self.finalize(*plugins, **kw)
+ o = api[namespace][self.plugin.__name__]
+ return (o, api, home)
+
+ def tearDown(self):
+ """
+ nose tear-down fixture.
+ """
+ context.__dict__.clear()
+
+
+class dummy_ugettext(object):
+ __called = False
+
+ def __init__(self, translation=None):
+ if translation is None:
+ translation = u'The translation'
+ self.translation = translation
+ assert type(self.translation) is unicode
+
+ def __call__(self, message):
+ assert self.__called is False
+ self.__called = True
+ assert type(message) is str
+ assert not hasattr(self, 'message')
+ self.message = message
+ assert type(self.translation) is unicode
+ return self.translation
+
+ def called(self):
+ return self.__called
+
+ def reset(self):
+ assert type(self.translation) is unicode
+ assert type(self.message) is str
+ del self.message
+ assert self.__called is True
+ self.__called = False
+
+
+class dummy_ungettext(object):
+ __called = False
+
+ def __init__(self):
+ self.translation_singular = u'The singular translation'
+ self.translation_plural = u'The plural translation'
+
+ def __call__(self, singular, plural, n):
+ assert type(singular) is str
+ assert type(plural) is str
+ assert type(n) is int
+ assert self.__called is False
+ self.__called = True
+ self.singular = singular
+ self.plural = plural
+ self.n = n
+ if n == 1:
+ return self.translation_singular
+ return self.translation_plural
+
+
+class DummyMethod(object):
+ def __init__(self, callback, name):
+ self.__callback = callback
+ self.__name = name
+
+ def __call__(self, *args, **kw):
+ return self.__callback(self.__name, args, kw)
+
+
+class DummyClass(object):
+ def __init__(self, *calls):
+ self.__calls = calls
+ self.__i = 0
+ for (name, args, kw, result) in calls:
+ method = DummyMethod(self.__process, name)
+ setattr(self, name, method)
+
+ def __process(self, name_, args_, kw_):
+ if self.__i >= len(self.__calls):
+ raise AssertionError(
+ 'extra call: %s, %r, %r' % (name_, args_, kw_)
+ )
+ (name, args, kw, result) = self.__calls[self.__i]
+ self.__i += 1
+ i = self.__i
+ if name_ != name:
+ raise AssertionError(
+ 'call %d should be to method %r; got %r' % (i, name, name_)
+ )
+ if args_ != args:
+ raise AssertionError(
+ 'call %d to %r should have args %r; got %r' % (i, name, args, args_)
+ )
+ if kw_ != kw:
+ raise AssertionError(
+ 'call %d to %r should have kw %r, got %r' % (i, name, kw, kw_)
+ )
+ if isinstance(result, Exception):
+ raise result
+ return result
+
+ def _calledall(self):
+ return self.__i == len(self.__calls)