Browse Source

[2713] Addressed review comments

- More tests
- a few bugfixes and refactors
- renamed methods to be private/protected
- documentation updates
Jelte Jansen 12 years ago
parent
commit
d3d28bdcd3

+ 58 - 54
src/bin/usermgr/b10-cmdctl-usermgr.py.in

@@ -20,6 +20,7 @@ This tool implements user management for b10-cmdctl. It is used to
 add and remove users from the accounts file.
 add and remove users from the accounts file.
 '''
 '''
 from bind10_config import SYSCONFPATH
 from bind10_config import SYSCONFPATH
+from collections import OrderedDict
 import random
 import random
 from hashlib import sha1
 from hashlib import sha1
 import csv
 import csv
@@ -50,16 +51,17 @@ class UserManager:
         self.options = options
         self.options = options
         self.args = args
         self.args = args
 
 
-    def print(self, msg):
+    def __print(self, msg):
         if not self.options.quiet:
         if not self.options.quiet:
             print(msg)
             print(msg)
 
 
-    def gen_password_hash(self, password):
-        salt = "".join(chr(random.randint(33, 127)) for x in range(64))
+    def __gen_password_hash(self, password):
+        salt = "".join(chr(random.randint(ord('!'), ord('~')))\
+                    for x in range(64))
         saltedpwd = sha1((password + salt).encode()).hexdigest()
         saltedpwd = sha1((password + salt).encode()).hexdigest()
         return salt, saltedpwd
         return salt, saltedpwd
 
 
-    def read_user_info(self):
+    def __read_user_info(self):
         """
         """
         Read the existing user info
         Read the existing user info
         Raises an IOError if the file can't be read
         Raises an IOError if the file can't be read
@@ -68,80 +70,77 @@ class UserManager:
         # check that the file is modified between read and write)
         # check that the file is modified between read and write)
         # But taking multiple simultaneous users of this tool on the
         # But taking multiple simultaneous users of this tool on the
         # same file seems unnecessary at this point.
         # same file seems unnecessary at this point.
-        self.user_info = []
+        self.user_info = OrderedDict()
         if os.path.exists(self.options.output_file):
         if os.path.exists(self.options.output_file):
             # Just let any file read error bubble up; it will
             # Just let any file read error bubble up; it will
             # be caught in the run() method
             # be caught in the run() method
-            with open(self.options.output_file) as csvfile:
+            with open(self.options.output_file, newline='') as csvfile:
                 reader = csv.reader(csvfile)
                 reader = csv.reader(csvfile)
                 for row in reader:
                 for row in reader:
-                    self.user_info.append(row)
+                    self.user_info[row[0]] = row
 
 
-    def save_userinfo(self):
+    def __save_user_info(self):
         """
         """
         Write out the (modified) user info
         Write out the (modified) user info
         Raises an IOError if the file can't be written
         Raises an IOError if the file can't be written
         """
         """
         # Just let any file write error bubble up; it will
         # Just let any file write error bubble up; it will
         # be caught in the run() method
         # be caught in the run() method
-        with open(self.options.output_file, 'w') as csvfile:
+        with open(self.options.output_file, 'w',
+                  newline='') as csvfile:
             writer = csv.writer(csvfile)
             writer = csv.writer(csvfile)
-            writer.writerows(self.user_info)
+            for row in self.user_info.values():
+                writer.writerow(row)
 
 
-    def username_exists(self, name):
-        """
-        Returns True if the username exists
-        """
-        for row in self.user_info:
-            if name == row[0]:
-                return True
-        return False
-
-    def add_user(self, name, password):
+    def __add_user(self, name, password):
         """
         """
         Add the given username/password combination to the stored user_info.
         Add the given username/password combination to the stored user_info.
         First checks if the username exists, and returns False if so.
         First checks if the username exists, and returns False if so.
         If not, it is added, and this method returns True.
         If not, it is added, and this method returns True.
         """
         """
-        if self.username_exists(name):
+        if name in self.user_info:
             return False
             return False
-        salt, pw = self.gen_password_hash(password)
-        self.user_info.append([name, pw, salt])
+        salt, pw = self.__gen_password_hash(password)
+        self.user_info[name] = [name, pw, salt]
         return True
         return True
 
 
-    def delete_user(self, name):
+    def __delete_user(self, name):
         """
         """
         Removes the row with the given name from the stored user_info
         Removes the row with the given name from the stored user_info
         First checks if the username exists, and returns False if not.
         First checks if the username exists, and returns False if not.
         Otherwise, it is removed, and this mehtod returns True
         Otherwise, it is removed, and this mehtod returns True
         """
         """
-        if not self.username_exists(name):
+        if name not in self.user_info:
             return False
             return False
-        new_user_info = []
-        for row in self.user_info:
-            if row[0] != name:
-                new_user_info.append(row)
-        self.user_info = new_user_info
+        del self.user_info[name]
         return True
         return True
 
 
-    def prompt_for_username(self, command):
+    # overridable input() call, used in testing
+    def _input(self, prompt):
+        return input(prompt)
+
+    # in essence this is private, but made 'protected' for ease
+    # of testing
+    def _prompt_for_username(self, command):
         # Note, direct prints here are intentional
         # Note, direct prints here are intentional
-        while True :
-            name = input("Username to " + command + ": ")
+        while True:
+            name = self._input("Username to " + command + ": ")
             if name == "":
             if name == "":
                 print("Error username can't be empty")
                 print("Error username can't be empty")
                 continue
                 continue
 
 
-            if command == COMMAND_ADD and self.username_exists(name):
+            if command == COMMAND_ADD and name in self.user_info:
                  print("user already exists")
                  print("user already exists")
                  continue
                  continue
-            elif command == COMMAND_DELETE and not self.username_exists(name):
+            elif command == COMMAND_DELETE and name not in self.user_info:
                  print("user does not exist")
                  print("user does not exist")
                  continue
                  continue
 
 
             return name
             return name
 
 
-    def prompt_for_password(self):
+    # in essence this is private, but made 'protected' for ease
+    # of testing
+    def _prompt_for_password(self):
         # Note, direct prints here are intentional
         # Note, direct prints here are intentional
         while True:
         while True:
             pwd1 = getpass.getpass("Choose a password: ")
             pwd1 = getpass.getpass("Choose a password: ")
@@ -154,58 +153,61 @@ class UserManager:
                 continue
                 continue
             return pwd1
             return pwd1
 
 
-    def verify_options_and_args(self):
+    def __verify_options_and_args(self):
         """
         """
         Basic sanity checks on command line arguments.
         Basic sanity checks on command line arguments.
         Returns False if there is a problem, True if everything seems OK.
         Returns False if there is a problem, True if everything seems OK.
         """
         """
         if len(self.args) < 1:
         if len(self.args) < 1:
-            self.print("Error: no command specified")
+            self.__print("Error: no command specified")
             return False
             return False
         if len(self.args) > 3:
         if len(self.args) > 3:
-            self.print("Error: extraneous arguments")
+            self.__print("Error: extraneous arguments")
             return False
             return False
         if self.args[0] not in [ COMMAND_ADD, COMMAND_DELETE ]:
         if self.args[0] not in [ COMMAND_ADD, COMMAND_DELETE ]:
-            self.print("Error: command must be either add or delete")
+            self.__print("Error: command must be either add or delete")
             return False
             return False
         if self.args[0] == COMMAND_DELETE and len(self.args) > 2:
         if self.args[0] == COMMAND_DELETE and len(self.args) > 2:
-            self.print("Error: delete only needs username, not a password")
+            self.__print("Error: delete only needs username, not a password")
             return False
             return False
         return True
         return True
 
 
     def run(self):
     def run(self):
-        if not self.verify_options_and_args():
+        if not self.__verify_options_and_args():
             return BAD_ARGUMENTS
             return BAD_ARGUMENTS
 
 
         try:
         try:
-            self.print("Using accounts file: " + self.options.output_file)
-            self.read_user_info()
+            self.__print("Using accounts file: " + self.options.output_file)
+            self.__read_user_info()
 
 
             command = self.args[0]
             command = self.args[0]
 
 
             if len(self.args) > 1:
             if len(self.args) > 1:
                 username = self.args[1]
                 username = self.args[1]
             else:
             else:
-                username = self.prompt_for_username(command)
+                username = self._prompt_for_username(command)
 
 
             if command == COMMAND_ADD:
             if command == COMMAND_ADD:
                 if len(self.args) > 2:
                 if len(self.args) > 2:
                     password = self.args[2]
                     password = self.args[2]
                 else:
                 else:
-                    password = self.prompt_for_password()
-                if not self.add_user(username, password):
+                    password = self.__prompt_for_password()
+                if not self.__add_user(username, password):
                     print("Error: username exists")
                     print("Error: username exists")
                     return USER_EXISTS
                     return USER_EXISTS
             elif command == COMMAND_DELETE:
             elif command == COMMAND_DELETE:
-                if not self.delete_user(username):
+                if not self.__delete_user(username):
                     print("Error: username does not exist")
                     print("Error: username does not exist")
                     return USER_DOES_NOT_EXIST
                     return USER_DOES_NOT_EXIST
 
 
-            self.save_userinfo()
+            self.__save_user_info()
             return 0
             return 0
         except IOError as ioe:
         except IOError as ioe:
-            self.print("Error accessing " + ioe.filename +\
-                       ": " + str(ioe.strerror))
+            self.__print("Error accessing " + ioe.filename +\
+                         ": " + str(ioe.strerror))
+            return FILE_ERROR
+        except csv.Error as csve:
+            self.__print("Error parsing csv file: " + str(csve))
             return FILE_ERROR
             return FILE_ERROR
 
 
 def set_options(parser):
 def set_options(parser):
@@ -226,7 +228,10 @@ def main():
             "  password\t\tthe password to set for the added user\n"\
             "  password\t\tthe password to set for the added user\n"\
             "\n"\
             "\n"\
             "If username or password are not specified, %prog will\n"\
             "If username or password are not specified, %prog will\n"\
-            "prompt for them."
+            "prompt for them. It is recommended practice to let the\n"\
+            "tool prompt for the password, as command-line\n"\
+            "arguments can be visible through history or process\n"\
+            "viewers."
     parser = OptionParser(usage=usage, version=VERSION_STRING)
     parser = OptionParser(usage=usage, version=VERSION_STRING)
     set_options(parser)
     set_options(parser)
     (options, args) = parser.parse_args()
     (options, args) = parser.parse_args()
@@ -235,6 +240,5 @@ def main():
     return usermgr.run()
     return usermgr.run()
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
-    result = main()
-    sys.exit(result)
+    sys.exit(main())
 
 

+ 4 - 2
src/bin/usermgr/b10-cmdctl-usermgr.xml

@@ -90,7 +90,9 @@
     <para>
     <para>
       If a username and password are given (or just a username in case of
       If a username and password are given (or just a username in case of
       deletion), these are used. Otherwise, the tool shall prompt for a
       deletion), these are used. Otherwise, the tool shall prompt for a
-      username and/or password.
+      username and/or password. It is recommended practice to let the
+      tool prompt for the password, as command-line arguments can be
+      visible through history or process viewers.
     </para>
     </para>
 
 
     <variablelist>
     <variablelist>
@@ -107,7 +109,7 @@
         <term><option>-f <replaceable>filename</replaceable></option></term>
         <term><option>-f <replaceable>filename</replaceable></option></term>
         <term><option>--file <replaceable>filename</replaceable></option></term>
         <term><option>--file <replaceable>filename</replaceable></option></term>
         <listitem><para>
         <listitem><para>
-          Define the filename to append the account to. The default is
+          Specify the accounts file to update. The default is
           <filename>cmdctl-accounts.csv</filename> in the system config
           <filename>cmdctl-accounts.csv</filename> in the system config
           directory.
           directory.
         </para></listitem>
         </para></listitem>

+ 0 - 8
src/bin/usermgr/tests/Makefile.am

@@ -2,14 +2,6 @@ PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
 PYTESTS = b10-cmdctl-usermgr_test.py
 PYTESTS = b10-cmdctl-usermgr_test.py
 EXTRA_DIST = $(PYTESTS)
 EXTRA_DIST = $(PYTESTS)
 
 
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-#LIBRARY_PATH_PLACEHOLDER =
-#if SET_ENV_LIBRARY_PATH
-#LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-#endif
-
-#CLEANFILES = test-keyfile.pem test-certfile.pem
 CLEANFILES = *.csv
 CLEANFILES = *.csv
 
 
 # test using command-line arguments, so use check-local target instead of TESTS
 # test using command-line arguments, so use check-local target instead of TESTS

+ 203 - 12
src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py

@@ -15,21 +15,77 @@
 
 
 import csv
 import csv
 from hashlib import sha1
 from hashlib import sha1
+import getpass
 import imp
 import imp
 import os
 import os
 import subprocess
 import subprocess
 import stat
 import stat
+import sys
 import unittest
 import unittest
 from bind10_config import SYSCONFPATH
 from bind10_config import SYSCONFPATH
 
 
-def run(command):
+class PrintCatcher:
+    def __init__(self):
+        self.stdout_lines = []
+
+    def __enter__(self):
+        self.orig_stdout_write = sys.stdout.write
+        def new_write(line):
+            self.stdout_lines.append(line)
+
+        sys.stdout.write = new_write
+        return self
+
+    def __exit__(self, type, value, traceback):
+        sys.stdout.write = self.orig_stdout_write
+        return
+
+class OverrideGetpass:
+    def __init__(self, new_getpass):
+        self.new_getpass = new_getpass
+        self.orig_getpass = getpass.getpass
+
+    def __enter__(self):
+        getpass.getpass = self.new_getpass
+        return self
+
+    def __exit__(self, type, value, traceback):
+        getpass.getpass = self.orig_getpass
+
+# input() is a built-in function and not easily overridable
+# so this one uses usermgr for that
+class OverrideInput:
+    def __init__(self, usermgr, new_getpass):
+        self.usermgr = usermgr
+        self.new_input = new_getpass
+        self.orig_input = usermgr._input
+
+    def __enter__(self):
+        self.usermgr._input = self.new_input
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.usermgr._input = self.orig_input
+
+def run(command, input=None):
     """
     """
     Small helper function that returns a tuple of (rcode, stdout, stderr)
     Small helper function that returns a tuple of (rcode, stdout, stderr)
     after running the given command (an array of command and arguments, as
     after running the given command (an array of command and arguments, as
     passed on to subprocess).
     passed on to subprocess).
+    Parameters:
+    command: an array of command and argument strings, which will be
+             passed to subprocess.Popen()
+    input: if not None, a string that is written to the process stdin
+           stream
     """
     """
+    if input is not None:
+        stdin = subprocess.PIPE
+    else:
+        stdin = None
     subp = subprocess.Popen(command, stdout=subprocess.PIPE,
     subp = subprocess.Popen(command, stdout=subprocess.PIPE,
-                            stderr=subprocess.PIPE)
+                            stderr=subprocess.PIPE, stdin=stdin)
+    if input is not None:
+        subp.stdin.write(bytes(input, 'UTF-8'))
     (stdout, stderr) = subp.communicate()
     (stdout, stderr) = subp.communicate()
     return (subp.returncode, stdout, stderr)
     return (subp.returncode, stdout, stderr)
 
 
@@ -39,6 +95,11 @@ class TestUserMgr(unittest.TestCase):
 
 
     def setUp(self):
     def setUp(self):
         self.delete_output_file()
         self.delete_output_file()
+        # For access to the actual module, we load it directly
+        self.usermgr_module = imp.load_source('usermgr',
+                                              '../b10-cmdctl-usermgr.py')
+        # And instantiate 1 instance (with fake options/args)
+        self.usermgr = self.usermgr_module.UserManager(object(), object())
 
 
     def tearDown(self):
     def tearDown(self):
         self.delete_output_file()
         self.delete_output_file()
@@ -51,10 +112,9 @@ class TestUserMgr(unittest.TestCase):
         self.assertTrue(os.path.exists(self.OUTPUT_FILE))
         self.assertTrue(os.path.exists(self.OUTPUT_FILE))
 
 
         csv_entries = []
         csv_entries = []
-        with open(self.OUTPUT_FILE) as csvfile:
+        with open(self.OUTPUT_FILE, newline='') as csvfile:
             reader = csv.reader(csvfile)
             reader = csv.reader(csvfile)
-            for row in reader:
-                csv_entries.append(row)
+            csv_entries = [row for row in reader]
 
 
         self.assertEqual(len(expected_content), len(csv_entries))
         self.assertEqual(len(expected_content), len(csv_entries))
         csv_entries.reverse()
         csv_entries.reverse()
@@ -73,7 +133,7 @@ class TestUserMgr(unittest.TestCase):
             self.assertEqual(expected_hash, entry_hash)
             self.assertEqual(expected_hash, entry_hash)
 
 
     def run_check(self, expected_returncode, expected_stdout, expected_stderr,
     def run_check(self, expected_returncode, expected_stdout, expected_stderr,
-                  command):
+                  command, stdin=None):
         """
         """
         Runs the given command, and checks return code, and outputs (if provided).
         Runs the given command, and checks return code, and outputs (if provided).
         Arguments:
         Arguments:
@@ -83,7 +143,7 @@ class TestUserMgr(unittest.TestCase):
         expected_stderr, (multiline) string that is checked against stderr.
         expected_stderr, (multiline) string that is checked against stderr.
                          May be None, in which case the check is skipped.
                          May be None, in which case the check is skipped.
         """
         """
-        (returncode, stdout, stderr) = run(command)
+        (returncode, stdout, stderr) = run(command, stdin)
         if expected_stderr is not None:
         if expected_stderr is not None:
             self.assertEqual(expected_stderr, stderr.decode())
             self.assertEqual(expected_stderr, stderr.decode())
         if expected_stdout is not None:
         if expected_stdout is not None:
@@ -100,7 +160,10 @@ Arguments:
   password		the password to set for the added user
   password		the password to set for the added user
 
 
 If username or password are not specified, b10-cmdctl-usermgr will
 If username or password are not specified, b10-cmdctl-usermgr will
-prompt for them.
+prompt for them. It is recommended practice to let the
+tool prompt for the password, as command-line
+arguments can be visible through history or process
+viewers.
 
 
 Options:
 Options:
   --version             show program's version number and exit
   --version             show program's version number and exit
@@ -224,14 +287,102 @@ Options:
 
 
     def test_default_file(self):
     def test_default_file(self):
         """
         """
+        A few checks that couldn't be done though external calls
+        of the tool.
         Check the default file is the correct one.
         Check the default file is the correct one.
-        Only checks the internal variable, as we don't want to overwrite
-        the actual file here
+        Check that the prompting methods do verification
         """
         """
         # Hardcoded path .. should be ok since this is run from make check
         # Hardcoded path .. should be ok since this is run from make check
-        usermgr = imp.load_source('usermgr', '../b10-cmdctl-usermgr.py')
         self.assertEqual(SYSCONFPATH + '/cmdctl-accounts.csv',
         self.assertEqual(SYSCONFPATH + '/cmdctl-accounts.csv',
-                         usermgr.DEFAULT_FILE)
+                         self.usermgr_module.DEFAULT_FILE)
+
+    def test_prompt_for_password_different(self):
+        # returns a different string (the representation of the number
+        # of times it has been called), until it has been called
+        # over 10 times, in which case it will always return "11"
+        getpass_different_called = 0
+        def getpass_different(question):
+            nonlocal getpass_different_called
+            getpass_different_called = getpass_different_called + 1
+            if getpass_different_called > 10:
+                return "11"
+            else:
+                return str(getpass_different_called)
+
+        with PrintCatcher() as pc:
+            with OverrideGetpass(getpass_different):
+                pwd = self.usermgr._prompt_for_password()
+                self.assertEqual(12, getpass_different_called)
+                self.assertEqual("11", pwd)
+                # stdout should be 5 times the no match string;
+                expected_output = "passwords do not match, try again\n"*5
+                self.assertEqual(expected_output, ''.join(pc.stdout_lines))
+
+    def test_prompt_for_password_empty(self):
+        # returns an empty string until it has been called over 10
+        # times
+        getpass_empty_called = 0
+        def getpass_empty(prompt):
+            nonlocal getpass_empty_called
+            getpass_empty_called = getpass_empty_called + 1
+            if getpass_empty_called > 10:
+                return "nonempty"
+            else:
+                return ""
+
+        usermgr_module = imp.load_source('usermgr',
+                                         '../b10-cmdctl-usermgr.py')
+        options = object()
+        args = object()
+        usermgr = usermgr_module.UserManager(options, args)
+
+        with PrintCatcher() as pc:
+            with OverrideGetpass(getpass_empty):
+                pwd = usermgr._prompt_for_password()
+                self.assertEqual("nonempty", pwd)
+                self.assertEqual(12, getpass_empty_called)
+                # stdout should be 10 times the 'cannot be empty' string
+                expected_output = "Error: password cannot be empty\n"*10
+                self.assertEqual(expected_output, ''.join(pc.stdout_lines))
+
+    def test_prompt_for_user(self):
+        new_input_called = 0
+        input_results = [ '', '', 'existinguser', 'nonexistinguser',
+                          '', '', 'nonexistinguser', 'existinguser' ]
+        def new_input(prompt):
+            nonlocal new_input_called
+
+            if new_input_called < len(input_results):
+                result = input_results[new_input_called]
+            else:
+                result = 'empty'
+            new_input_called += 1
+            return result
+
+        # add fake user (value doesn't matter, method only checks for key)
+        self.usermgr.user_info = { 'existinguser': None }
+
+        expected_output = ''
+
+        with PrintCatcher() as pc:
+            with OverrideInput(self.usermgr, new_input):
+                # should skip the first three since empty or existing
+                # are not allowed, then return 'nonexistinguser'
+                username = self.usermgr._prompt_for_username(
+                                self.usermgr_module.COMMAND_ADD)
+                self.assertEqual('nonexistinguser', username)
+                expected_output += "Error username can't be empty\n"*2
+                expected_output += "user already exists\n"
+                self.assertEqual(expected_output, ''.join(pc.stdout_lines))
+
+                # For delete, should again not accept empty (in a while true
+                # loop), and this time should not accept nonexisting users
+                username = self.usermgr._prompt_for_username(
+                                self.usermgr_module.COMMAND_DELETE)
+                self.assertEqual('existinguser', username)
+                expected_output += "Error username can't be empty\n"*2
+                expected_output += "user does not exist\n"
+                self.assertEqual(expected_output, ''.join(pc.stdout_lines))
 
 
     def test_bad_file(self):
     def test_bad_file(self):
         """
         """
@@ -291,6 +442,46 @@ Options:
                          'delete', 'user1'
                          'delete', 'user1'
                        ])
                        ])
 
 
+    def test_missing_fields(self):
+        """
+        Test that an invalid csv file is handled gracefully
+        """
+        # Valid but incomplete csv; should be handled
+        # correctly
+        with open(self.OUTPUT_FILE, 'w', newline='') as f:
+            f.write('onlyuserfield\n')
+            f.write('userfield,saltfield\n')
+            f.write(',emptyuserfield,passwordfield\n')
+
+        self.run_check(0, None, None,
+                       [ self.TOOL,
+                         '-f', self.OUTPUT_FILE,
+                         'add', 'user1', 'pass1'
+                       ])
+        self.run_check(0, None, None,
+                       [ self.TOOL,
+                         '-f', self.OUTPUT_FILE,
+                         'delete', 'onlyuserfield'
+                       ])
+        self.run_check(0, None, None,
+                       [ self.TOOL,
+                         '-f', self.OUTPUT_FILE,
+                         'delete', ''
+                       ])
+
+    def test_bad_data(self):
+        # I can only think of one invalid format, an unclosed string
+        with open(self.OUTPUT_FILE, 'w', newline='') as f:
+            f.write('a,"\n')
+        self.run_check(2,
+                       'Using accounts file: test_users.csv\n'
+                       'Error parsing csv file: newline inside string\n',
+                       '',
+                       [ self.TOOL,
+                         '-f', self.OUTPUT_FILE,
+                         'add', 'user1', 'pass1'
+                       ])
+
 
 
 if __name__== '__main__':
 if __name__== '__main__':
     unittest.main()
     unittest.main()

+ 3 - 0
src/lib/python/bind10_config.py.in

@@ -51,6 +51,9 @@ def reload():
     #  When "FROM_SOURCE", it lists the directories where the programs are
     #  When "FROM_SOURCE", it lists the directories where the programs are
     #  built so that when BIND 10 is experimentally started on the source
     #  built so that when BIND 10 is experimentally started on the source
     #  tree the programs in the tree (not installed ones) will be used.
     #  tree the programs in the tree (not installed ones) will be used.
+    # SYSCONFPATH: Path where the system-wide configuration files are
+    # stored (e.g. <prefix>/var/<package name>). This value is *not*
+    # overwritten if B10_FROM_SOURCE is specified.
     #
     #
     # B10_FROM_SOURCE_LOCALSTATEDIR is specifically intended to be used for
     # B10_FROM_SOURCE_LOCALSTATEDIR is specifically intended to be used for
     # tests where we want to use various types of configuration within the test
     # tests where we want to use various types of configuration within the test