diff options
Diffstat (limited to 'ipatests')
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 =' ' + '<script>do_bad_stuff();</script>' + assert_equal( + f.not_found(None, s, url, None), + [t % dict(url='&nbsp;<script>do_bad_stuff();</script>')] + ) + 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) |