Repository: fedora-infra/bodhi

Issue:
bodhi-push doesn't load the config file before starting the database connection
```bodhi-push``` doesn't load the config file before starting the database connection, which causes it to crash:

```
[bowlofeggs@bodhi-backend01 ~][PROD]$ sudo -u apache bodhi-push --releases 'f26,f25,f24,epel-7,EL-6' --username bowlofeggs
Traceback (most recent call last):
  File "/bin/bodhi-push", line 9, in <module>
    load_entry_point('bodhi-server==2.5.0', 'console_scripts', 'bodhi-push')()
  File "/usr/lib/python2.7/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python2.7/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python2.7/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/bodhi/server/push.py", line 62, in push
    with get_db_factory()() as session:
  File "/usr/lib/python2.7/site-packages/bodhi/server/models.py", line 2438, in get_db_factory
    engine = engine_from_config(config, 'sqlalchemy.')
  File "/usr/lib64/python2.7/site-packages/sqlalchemy/engine/__init__.py", line 426, in engine_from_config
    url = options.pop('url')
KeyError: 'url'
```


Discussion:

Resolving patch:
diff --git a/bodhi/server/__init__.py b/bodhi/server/__init__.py
--- a/bodhi/server/__init__.py
+++ b/bodhi/server/__init__.py
@@ -144,9 +144,10 @@ def initialize_db(config):
     Returns:
         sqlalchemy.engine: The database engine created from the configuration.
     """
-    #: The SQLAlchemy database engine. This is constructed using the value of
-    #: ``DB_URL`` in :data:`config``.
-    engine = engine_from_config(config, 'sqlalchemy.')
+    # The SQLAlchemy database engine. This is constructed using the value of
+    # ``DB_URL`` in :data:`config``. Note: A copy is provided since ``engine_from_config``
+    # uses ``pop``.
+    engine = engine_from_config(config.copy(), 'sqlalchemy.')
     # When using SQLite we need to make sure foreign keys are enabled:
     # http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#foreign-key-support
     if config['sqlalchemy.url'].startswith('sqlite:'):
diff --git a/bodhi/server/config.py b/bodhi/server/config.py
--- a/bodhi/server/config.py
+++ b/bodhi/server/config.py
@@ -51,6 +51,16 @@ def get(self, *args, **kw):
             self.load_config()
         return super(BodhiConfig, self).get(*args, **kw)
 
+    def pop(self, *args, **kw):
+        if not self.loaded:
+            self.load_config()
+        return super(BodhiConfig, self).pop(*args, **kw)
+
+    def copy(self, *args, **kw):
+        if not self.loaded:
+            self.load_config()
+        return super(BodhiConfig, self).copy(*args, **kw)
+
     def load_config(self):
         configfile = get_configfile()
         self.update(get_appsettings(configfile))


Test patch:
diff --git a/bodhi/tests/server/test_config.py b/bodhi/tests/server/test_config.py
new file mode 100644
--- /dev/null
+++ b/bodhi/tests/server/test_config.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# 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 2
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+import unittest
+
+import mock
+
+from bodhi.server.config import BodhiConfig
+
+
+class BodhiConfigGetItemTests(unittest.TestCase):
+    """Tests for the ``__getitem__`` method on the :class:`BodhiConfig` class."""
+
+    def setUp(self):
+        self.config = BodhiConfig()
+        self.config.load_config = mock.Mock()
+        self.config['password'] = 'hunter2'
+
+    def test_not_loaded(self):
+        """Assert calling ``__getitem__`` causes the config to load."""
+        self.assertFalse(self.config.loaded)
+        self.assertEqual('hunter2', self.config['password'])
+        self.config.load_config.assert_called_once()
+
+    def test_loaded(self):
+        """Assert calling ``__getitem__`` when the config is loaded doesn't reload the config."""
+        self.config.loaded = True
+
+        self.assertEqual('hunter2', self.config['password'])
+        self.assertEqual(0, self.config.load_config.call_count)
+
+    def test_missing(self):
+        """Assert you still get normal dictionary errors from the config."""
+        self.assertRaises(KeyError, self.config.__getitem__, 'somemissingkey')
+
+
+class BodhiConfigGetTests(unittest.TestCase):
+    """Tests for the ``get`` method on the :class:`BodhiConfig` class."""
+
+    def setUp(self):
+        self.config = BodhiConfig()
+        self.config.load_config = mock.Mock()
+        self.config['password'] = 'hunter2'
+
+    def test_not_loaded(self):
+        """Assert calling ``get`` causes the config to load."""
+        self.assertFalse(self.config.loaded)
+        self.assertEqual('hunter2', self.config.get('password'))
+        self.config.load_config.assert_called_once()
+
+    def test_loaded(self):
+        """Assert calling ``get`` when the config is loaded doesn't reload the config."""
+        self.config.loaded = True
+
+        self.assertEqual('hunter2', self.config.get('password'))
+        self.assertEqual(0, self.config.load_config.call_count)
+
+    def test_missing(self):
+        """Assert you get ``None`` when the key is missing."""
+        self.assertEqual(None, self.config.get('somemissingkey'))
+
+
+class BodhiConfigPopItemTests(unittest.TestCase):
+    """Tests for the ``pop`` method on the :class:`BodhiConfig` class."""
+
+    def setUp(self):
+        self.config = BodhiConfig()
+        self.config.load_config = mock.Mock()
+        self.config['password'] = 'hunter2'
+
+    def test_not_loaded(self):
+        """Assert calling ``pop`` causes the config to load."""
+        self.assertFalse(self.config.loaded)
+        self.assertEqual('hunter2', self.config.pop('password'))
+        self.config.load_config.assert_called_once()
+
+    def test_loaded(self):
+        """Assert calling ``pop`` when the config is loaded doesn't reload the config."""
+        self.config.loaded = True
+
+        self.assertEqual('hunter2', self.config.pop('password'))
+        self.assertEqual(0, self.config.load_config.call_count)
+
+    def test_removes(self):
+        """Assert the configuration is removed with ``pop``."""
+        self.assertEqual('hunter2', self.config.pop('password'))
+        self.assertRaises(KeyError, self.config.pop, 'password')
+
+    def test_get_missing(self):
+        """Assert you still get normal dictionary errors from the config."""
+        self.assertRaises(KeyError, self.config.pop, 'somemissingkey')
+
+
+class BodhiConfigCopyTests(unittest.TestCase):
+    """Tests for the ``copy`` method on the :class:`BodhiConfig` class."""
+
+    def setUp(self):
+        self.config = BodhiConfig()
+        self.config.load_config = mock.Mock()
+        self.config['password'] = 'hunter2'
+
+    def test_not_loaded(self):
+        """Assert calling ``copy`` causes the config to load."""
+        self.assertFalse(self.config.loaded)
+        self.assertEqual({'password': 'hunter2'}, self.config.copy())
+        self.config.load_config.assert_called_once()
+
+    def test_loaded(self):
+        """Assert calling ``copy`` when the config is loaded doesn't reload the config."""
+        self.config.loaded = True
+
+        self.assertEqual({'password': 'hunter2'}, self.config.copy())
+        self.assertEqual(0, self.config.load_config.call_count)
+
+
+class BodhiConfigLoadConfig(unittest.TestCase):
+
+    @mock.patch('bodhi.server.config.get_configfile', mock.Mock(return_value='/some/config.ini'))
+    @mock.patch('bodhi.server.config.get_appsettings')
+    def test_marks_loaded(self, mock_appsettings):
+        config = BodhiConfig()
+        mock_appsettings.return_value = {'password': 'hunter2'}
+
+        config.load_config()
+
+        mock_appsettings.assert_called_once_with('/some/config.ini')
+        self.assertEqual([('password', 'hunter2')], config.items())
+        self.assertTrue(config.loaded)