Repository: fedora-infra/bodhi

Issue:
Client crash when submitting multiple BuildrootOverrides in one call
Mamoru TASAKA [reported](https://bugzilla.redhat.com/show_bug.cgi?id=1513836) to Red Hat Bugzilla that the Bodhi client crashes when a list of BuildrootOverrides are created in one call.

> Description of problem:
> Submitting override with multiple packages at once causes bodhi crash
> (although the override seems to be registered anyway)
> 
> 
> Version-Release number of selected component (if applicable):
> bodhi-client-2.12.2-2.fc27
> 
> How reproducible:
> 100%
> 
> Steps to Reproduce:
> 1. See below
> 2.
> 3.
> 
> Actual results:
> [mtasaka@localhost fedora-specific]$ bodhi overrides save --user mtasaka --duration 4 --notes "ruby-gnome2 3.2.0 suite stage 2" "rubygem-gobject-introspection-3.2.0-1.fc27 rubygem-cairo-gobject-3.2.0-1.fc27"
> mtasaka's rubygem-gobject-introspection-3.2.0-1.fc27 override (expires 2017-11-20 04:53:57)
> mtasaka's rubygem-cairo-gobject-3.2.0-1.fc27 override (expires 2017-11-20 04:53:57)
> Traceback (most recent call last):
>   File "/bin/bodhi", line 11, in <module>
>     load_entry_point('bodhi-client==2.12.2', 'console_scripts', 'bodhi')()
>   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 1066, in invoke
>     return _process_result(sub_ctx.command.invoke(sub_ctx))
>   File "/usr/lib/python2.7/site-packages/click/core.py", line 1066, in invoke
>     return _process_result(sub_ctx.command.invoke(sub_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/client/__init__.py", line 111, in wrapper
>     method(*args, **kwargs)
>   File "/usr/lib/python2.7/site-packages/bodhi/client/__init__.py", line 600, in save_buildroot_overrides
>     _save_override(url=url, user=user, password=password, staging=staging, **kwargs)
>   File "/usr/lib/python2.7/site-packages/bodhi/client/__init__.py", line 141, in _save_override
>     print_resp(resp, client)
>   File "/usr/lib/python2.7/site-packages/bodhi/client/__init__.py", line 658, in print_resp
>     '%s overrides found (%d shown)' % (resp.total, len(resp.overrides)))
>   File "/usr/lib/python2.7/site-packages/munch/__init__.py", line 103, in __getattr__
>     raise AttributeError(k)
> AttributeError: total


Discussion:

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -1031,8 +1031,9 @@ def print_resp(resp, client, verbose=False, override_hint=True):
         else:
             for override in resp.overrides:
                 click.echo(client.override_str(override).strip())
-        click.echo(
-            '%s overrides found (%d shown)' % (resp.total, len(resp.overrides)))
+        if 'total' in resp:
+            click.echo(
+                '%s overrides found (%d shown)' % (resp.total, len(resp.overrides)))
     elif 'build' in resp:
         click.echo(client.override_str(resp, minimal=False))
         if override_hint:


Test patch:
diff --git a/bodhi/tests/client/test___init__.py b/bodhi/tests/client/test___init__.py
--- a/bodhi/tests/client/test___init__.py
+++ b/bodhi/tests/client/test___init__.py
@@ -1341,6 +1341,65 @@ def _send_request(*args, **kwargs):
             ('koji', 'wait-repo', 'f25-build', '--build=js-tag-it-2.0-1.fc25'),
             stderr=-1, stdout=-1)
 
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request', autospec=True)
+    def test_create_multiple_overrides(self, send_request):
+        """
+        Assert correct behavior when user creates multiple overrides.
+        """
+        runner = testing.CliRunner()
+
+        def _send_request(*args, **kwargs):
+            """Mock the response from send_request()."""
+            response = client_test_data.EXAMPLE_QUERY_OVERRIDES_MUNCH
+            del response['total']
+            return response
+
+        send_request.side_effect = _send_request
+        expected_output = client_test_data.EXPECTED_QUERY_OVERRIDES_OUTPUT
+        expected_output = expected_output.replace("11 overrides found (11 shown)\n", "")
+
+        overrides_nvrs = [
+            'nodejs-grunt-wrap-0.3.0-2.fc25',
+            'python-pyramid-1.5.6-3.el7',
+            'erlang-esip-1.0.8-1.fc25',
+            'erlang-stun-1.0.7-1.fc25',
+            'erlang-iconv-1.0.2-1.fc25',
+            'erlang-stringprep-1.0.6-1.fc25',
+            'erlang-fast_tls-1.0.7-1.fc25',
+            'erlang-fast_yaml-1.0.6-1.fc25',
+            'erlang-fast_xml-1.1.15-1.fc25',
+            'python-fedmsg-atomic-composer-2016.3-1.el7',
+            'python-fedmsg-atomic-composer-2016.3-1.fc24',
+        ]
+
+        overrides_nvrs_str = " ".join(overrides_nvrs)
+
+        result = runner.invoke(
+            client.save_buildroot_overrides,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', overrides_nvrs_str, '--url',
+             'http://localhost:6543/'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, expected_output)
+        bindings_client = send_request.mock_calls[0][1][0]
+        # datetime is a C extension that can't be mocked, so let's just assert that the time is
+        # about a week away.
+        expire_time = send_request.mock_calls[0][2]['data']['expiration_date']
+        self.assertTrue((datetime.datetime.utcnow() - expire_time) < datetime.timedelta(seconds=5))
+        # There should be one calls to send_request().
+        self.assertEqual(send_request.call_count, 1)
+        self.assertEqual(
+            send_request.mock_calls[0],
+            mock.call(
+                bindings_client, 'overrides/', verb='POST', auth=True,
+                data={
+                    'expiration_date': expire_time,
+                    'notes': u'No explanation given...', 'nvr': overrides_nvrs_str,
+                    'csrf_token': 'a_csrf_token'}))
+        self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
+
 
 class TestWarnIfUrlAndStagingSet(unittest.TestCase):
     """