diff test/test_config.py @ 6578:b1f1539c6a31

issue2551182 - ... allow loading values from external file. flake8 cleanups Secrets (passwords, secrets) can specify a file using file:// or file:///. The first line of the file is used as the secret. This allows committing config.ini to a VCS. Following settings are changed: [tracker] secret_key [tracker] jwt_secret [rdbms] password [mail] password details: in roundup/configuration.py: Defined SecretMandatoryOptions and SecretNullableOptions. Converted all secret keys and password to one of the above. Also if [mail] username is defined but [mail] password is not it throws an error at load. Cleaned up a couple of methods whose call signature included: def ...(..., settings={}): settings=None and it is set to empty dict inside the method. Also replace exception.message with str(exception) for python3 compatibility. in test/test_config: changed munge_configini to support changing only within a section, replacing keyword text.
author John Rouillard <rouilj@ieee.org>
date Mon, 03 Jan 2022 22:18:57 -0500
parents fdfe3b7387ea
children 770503bd211e
line wrap: on
line diff
--- a/test/test_config.py	Wed Dec 29 18:17:36 2021 -0500
+++ b/test/test_config.py	Mon Jan 03 22:18:57 2022 -0500
@@ -349,34 +349,59 @@
         except OSError as error:
             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
 
-    def munge_configini(self, mods = None):
+    def munge_configini(self, mods = None, section=None):
         """ modify config.ini to meet testing requirements
 
             mods is a list of tuples:
-               [ ( "a = ", "b" ), ("c = ", None) ]
+               [ ( "a = ", "b" ), ("c = ", None), ("d = ", "b", "z = ") ]
             Match line with first tuple element e.g. "a = ". Note specify
             trailing "=" and space to delimit keyword and properly format
-            replacement line. If first tuple element matches, the line is
+            replacement line. If there are two elements in the tuple,
+            and the first element matches, the line is
             replaced with the concatenation of the first and second elements.
             If second element is None ("" doesn't work), the line will be
-            deleted.
+            deleted. If there are three elements in the tuple, the line
+            is replaced with the contcatentation of the third and second
+            elements (used to replace commented out parameters).
 
-            Note the key/first element of tuple must be unique in config.ini.
-            It is possible to have duplicates in different sections. This
-            method doesn't handle that. TBD option third element of tuple
-            defining section if needed.
+            Note if the key/first element of tuple is not unique in
+            config.ini, you must set the section to match the bracketed
+            section name.
         """
 
         if mods is None:
             return
 
+        # if section is defined, the tests in the loop will turn
+        # it off on [main] if section != '[main]'.
+        in_section = True 
+
         for line in fileinput.input(os.path.join(self.dirname, "config.ini"),
                                     inplace=True):
-            for match, value in mods:
-                if line.startswith(match):
-                    if value is not None:
-                        print(match + value)
-                    break
+            if section:
+                if line.startswith('['):
+                    in_section = False
+
+                if line.startswith(section):
+                    in_section = True
+
+            if in_section:
+                for rule in mods:
+                    if len(rule) == 3:
+                        match, value, repl = rule
+                    else:
+                        match, value = rule
+                        repl = None
+
+                    if line.startswith(match):
+                        if value is not None:
+                            if repl:
+                                print(repl + value)
+                            else:
+                                print(match + value)
+                        break
+                else:
+                    print(line[:-1]) # remove trailing \n
             else:
                 print(line[:-1]) # remove trailing \n
 
@@ -397,6 +422,186 @@
         self.assertEqual("RDBMS_BACKEND is not set"
                       " and has no default", cm.exception.__str__())
 
+    def testUnsetMailPassword_with_set_username(self):
+        """ Set [mail] username but don't set the 
+            [mail] password. Should get an OptionValueError. 
+        """
+        # SETUP: set mail username
+        self.munge_configini(mods=[ ("username = ", "foo"), ],
+                             section="[mail]")
+
+        config = configuration.CoreConfig()
+
+        with self.assertRaises(configuration.OptionValueError) as cm:
+            config.load(self.dirname)
+
+        print(cm.exception)
+        # test repr. The type is right since it passed assertRaises.
+        self.assertIn("OptionValueError", repr(cm.exception))
+        # look for 'not defined'
+        self.assertEqual("not defined", cm.exception.args[1])
+
+    def testUnsetMailPassword_with_unset_username(self):
+        """ Set [mail] username but don't set the 
+            [mail] password. Should get an OptionValueError. 
+        """
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        self.assertEqual(config['MAIL_USERNAME'], '')
+
+        with self.assertRaises(configuration.OptionUnsetError) as cm:
+            self.assertEqual(config['MAIL_PASSWORD'], 'NO DEFAULT')
+
+    def testSecretMandatory_missing_file(self):
+
+        # SETUP: 
+        self.munge_configini(mods=[ ("secret_key = ", "file://secret_key"), ])
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        with self.assertRaises(configuration.OptionValueError) as cm:
+            self.assertEqual(config['WEB_SECRET_KEY'],"")
+
+        print(cm.exception)
+        self.assertEqual(cm.exception.args[0].setting, "secret_key")
+
+    def testSecretMandatory_load_from_file(self):
+
+        # SETUP: 
+        self.munge_configini(mods=[ ("secret_key = ", "file://secret_key"), ])
+
+        secret = "ASDQWEZXCRFVBGTYHNMJU"
+        with open(self.dirname + "/secret_key", "w") as f:
+            f.write(secret + "\n")
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        self.assertEqual(config['WEB_SECRET_KEY'], secret)
+
+
+    def testSecretMandatory_load_from_abs_file(self):
+
+        abs_file = "/tmp/secret_key.%s"%os.getpid()
+
+        # SETUP: 
+        self.munge_configini(mods=[ ("secret_key = ", "file://%s"%abs_file), ])
+
+        secret = "ASDQWEZXCRFVBGTYHNMJU"
+        with open(abs_file, "w") as f:
+            f.write(secret + "\n")
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        self.assertEqual(config['WEB_SECRET_KEY'], secret)
+
+        os.remove(abs_file)
+
+    def testSecretMandatory_empty_file(self):
+
+        self.munge_configini(mods=[ ("secret_key = ", "file:// secret_key"), ])
+
+        # file with no value just newline.
+        with open(self.dirname + "/secret_key", "w") as f:
+            f.write("\n")
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        with self.assertRaises(configuration.OptionValueError) as cm:
+            config['WEB_SECRET_KEY']
+
+        print(cm.exception.args)
+        self.assertEqual(cm.exception.args[2],"Value must not be empty.")
+
+    def testNullableSecret_empty_file(self):
+
+        self.munge_configini(mods=[ ("password = ", "file://db_password"), ])
+
+        # file with no value just newline.
+        with open(self.dirname + "/db_password", "w") as f:
+            f.write("\n")
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        v = config['RDBMS_PASSWORD']
+
+        self.assertEqual(v, None)
+
+    def testNullableSecret_with_file_value(self):
+
+        self.munge_configini(mods=[ ("password = ", "file://db_password"), ])
+
+        # file with no value just newline.
+        with open(self.dirname + "/db_password", "w") as f:
+            f.write("test\n")
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        v = config['RDBMS_PASSWORD']
+
+        self.assertEqual(v, "test")
+
+    def testNullableSecret_with_value(self):
+
+        self.munge_configini(mods=[ ("password = ", "test"), ])
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        v = config['RDBMS_PASSWORD']
+
+        self.assertEqual(v, "test")
+
+    def testSetMailPassword_with_set_username(self):
+        """ Set [mail] username and set the password.
+            Should have both values set.
+        """
+        # SETUP: set mail username
+        self.munge_configini(mods=[ ("username = ", "foo"),
+                                    ("#password = ", "passwordfoo",
+                                    "password = ") ],
+                             section="[mail]")
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        self.assertEqual(config['MAIL_USERNAME'], 'foo')
+        self.assertEqual(config['MAIL_PASSWORD'], 'passwordfoo')
+
+    def testSetMailPassword_from_file(self):
+        """ Set [mail] username and set the password.
+            Should have both values set.
+        """
+        # SETUP: set mail username
+        self.munge_configini(mods=[ ("username = ", "foo"),
+                                    ("#password = ", "file://password",
+                                     "password = ") ],
+                             section="[mail]")
+        with open(self.dirname + "/password", "w") as f:
+            f.write("passwordfoo\n")
+
+        config = configuration.CoreConfig()
+
+        config.load(self.dirname)
+
+        self.assertEqual(config['MAIL_USERNAME'], 'foo')
+        self.assertEqual(config['MAIL_PASSWORD'], 'passwordfoo')
+
     @skip_xapian
     def testInvalidIndexerLanguage_w_empty(self):
         """ make sure we have a reasonable error message if

Roundup Issue Tracker: http://roundup-tracker.org/