Repository: fedora-infra/bodhi

Issue:
Authentication errors print a traceback in the bodhi CLI
If you type the wrong password when performing an action with the bodhi CLI, it will print a traceback:

```
Traceback (most recent call last):
  File "/usr/bin/bodhi", line 9, in <module>
    load_entry_point('bodhi-client', 'console_scripts', 'bodhi')()
  File "/usr/lib/python2.7/site-packages/click/core.py", line 716, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/click/core.py", line 696, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python2.7/site-packages/click/core.py", line 1060, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python2.7/site-packages/click/core.py", line 1060, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python2.7/site-packages/click/core.py", line 889, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python2.7/site-packages/click/core.py", line 534, in invoke
    return callback(*args, **kwargs)
  File "/vagrant/bodhi/client/__init__.py", line 620, in request
    resp = client.request(update, state)
  File "/vagrant/bodhi/client/__init__.py", line 58, in wrapper
    result = method(*args, **kwargs)
  File "/vagrant/bodhi/client/__init__.py", line 153, in request
    'csrf_token': self.csrf()})
  File "/vagrant/bodhi/client/__init__.py", line 58, in wrapper
    result = method(*args, **kwargs)
  File "/vagrant/bodhi/client/__init__.py", line 293, in csrf
    self.login(self.username, self.password)
  File "/usr/lib/python2.7/site-packages/fedora/client/openidbaseclient.py", line 289, in login
    openid_insecure=self.openid_insecure)
  File "/usr/lib/python2.7/site-packages/fedora/client/openidproxyclient.py", line 138, in openid_login
    raise AuthError(output['message'])
fedora.client.AuthError: Authentication failed
```

It should just print a nice error message instead.


Discussion:
I want to work on this can you please tell me how should I start.
Thanks.
@ankit01ojha You need to investigate into bodhi CLI codebase https://github.com/fedora-infra/bodhi/blob/develop/bodhi/client/__init__.py
I am getting this error with bodhi CLI http://imgur.com/YjTT8yS . I am not able to reproduce the bug because of this. 
@ankit01ojha it appears that you are not using the current development version of Bodhi, but bodhi 0.9 instead. If you use bodhi inside the Vagrant environment, it should be the development version. You can tell if you have the right version by using the --version flag. For example, right now I see:

```
[vagrant@localhost vagrant]$ bodhi --version
2.3.3
```

I don't believe bodhi 0.9 had a --version flag.
@bowlofeggs yes , its 0.9.12.1, so what should I do now?
@ankit01ojha Try to setup the development environment with Vagrant.
@bowlofeggs now I am having version 2.3.3 but I am unable to reproduce the bug can you just tell me how should I reproduce this bug( I don't know how should I set password to bodhi CLI).
@ankit01ojha There are several actions in the bodhi CLI that require authentication. For example, you could try to create a new update as me:

```
$ bodhi updates new --user bowlofeggs ejabberd-16.09-5.fc25
```

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -18,10 +18,12 @@
 import sys
 import traceback
 import re
+import functools
 
 import click
 
 from bodhi.client import bindings
+from fedora.client import AuthError
 
 
 def _warn_if_url_and_staging_set(ctx, param, value):
@@ -101,6 +103,21 @@ def _add_options(func):
     return _add_options
 
 
+def handle_errors(method):
+    """ A decorator that echoes neat error messages on AuthError or BodhiClientException. """
+    @functools.wraps(method)
+    def wrapper(*args, **kwargs):
+        try:
+            method(*args, **kwargs)
+        except AuthError as e:
+            click.echo("%s: Check your FAS username & password" % (e))
+            sys.exit(1)
+        except bindings.BodhiClientException as e:
+            click.echo(str(e))
+            sys.exit(2)
+    return wrapper
+
+
 def _save_override(url, user, password, staging, edit=False, **kwargs):
     """
     Helper function to create or edit a buildroot override.
@@ -157,6 +174,7 @@ def updates():
 @add_options(new_edit_options)
 @click.argument('builds')
 @click.option('--file', help='A text file containing all the update details')
+@handle_errors
 @url_option
 def new(user, password, url, **kwargs):
     # User Docs that show in the --help
@@ -214,6 +232,7 @@ def _validate_edit_update(ctx, param, value):
 @add_options(new_edit_options)
 @click.argument('update', callback=_validate_edit_update)
 @url_option
+@handle_errors
 def edit(user, password, url, **kwargs):
     # User Docs that show in the --help
     """
@@ -289,6 +308,7 @@ def edit(user, password, url, **kwargs):
 @click.option('--staging', help='Use the staging bodhi instance',
               is_flag=True, default=False)
 @url_option
+@handle_errors
 def query(url, mine=False, **kwargs):
     # User Docs that show in the --help
     """
@@ -324,6 +344,7 @@ def query(url, mine=False, **kwargs):
 @click.option('--staging', help='Use the staging bodhi instance',
               is_flag=True, default=False)
 @url_option
+@handle_errors
 def request(update, state, user, password, url, **kwargs):
     # User Docs that show in the --help
     """
@@ -370,6 +391,7 @@ def request(update, state, user, password, url, **kwargs):
 @click.option('--staging', help='Use the staging bodhi instance',
               is_flag=True, default=False)
 @url_option
+@handle_errors
 def comment(update, text, karma, user, password, url, **kwargs):
     # User Docs that show in the --help
     """
@@ -409,6 +431,7 @@ def comment(update, text, karma, user, password, url, **kwargs):
 @click.option('--updateid', help='Download update(s) by ID(s) (comma-separated list)')
 @click.option('--builds', help='Download update(s) by build NVR(s) (comma-separated list)')
 @url_option
+@handle_errors
 def download(url, **kwargs):
     # User Docs that show in the --help
     """
@@ -513,6 +536,7 @@ def overrides():
 @click.option('--builds', default=None,
               help='Query by comma-separated build id(s)')
 @url_option
+@handle_errors
 def query_buildroot_overrides(url, user=None, mine=False, packages=None,
                               expired=None, releases=None, builds=None, **kwargs):
     # Docs that show in the --help
@@ -550,6 +574,7 @@ def query_buildroot_overrides(url, user=None, mine=False, packages=None,
 
 @overrides.command('save')
 @add_options(save_edit_options)
+@handle_errors
 def save_buildroot_overrides(user, password, url, staging, **kwargs):
     # Docs that show in the --help
     """
@@ -585,6 +610,7 @@ def save_buildroot_overrides(user, password, url, staging, **kwargs):
 @overrides.command('edit')
 @add_options(save_edit_options)
 @click.option('--expire', help='Expire the override', is_flag=True, default=False)
+@handle_errors
 def edit_buildroot_overrides(user, password, url, staging, **kwargs):
     # Docs that show in the --help
     """


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
@@ -24,7 +24,7 @@
 import mock
 
 from bodhi import client
-from bodhi.client import bindings
+from bodhi.client import bindings, AuthError
 from bodhi.tests import client as client_test_data
 
 
@@ -416,23 +416,6 @@ def test_existing_override_error_message(self, send_request):
         self.assertIn("Use `overrides edit` to edit an existing override",
                       result.output)
 
-    @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_other_client_exception(self, send_request):
-        """
-        Assert that any other BodhiClientExceptions are raised as expected
-        """
-        send_request.side_effect = bindings.BodhiClientException("Pants Exception")
-        runner = testing.CliRunner()
-
-        result = runner.invoke(
-            client.save_buildroot_overrides,
-            ['--user', 'bowlofeggs', '--password', 's3kr3t', 'js-tag-it-2.0-1.fc25'])
-
-        self.assertEqual(result.exit_code, -1)
-        self.assertEqual(str(result.exception), "Pants Exception", result.output)
-
 
 class TestWarnIfUrlAndStagingSet(unittest.TestCase):
     """
@@ -705,3 +688,44 @@ def test_expired_override(self, send_request):
                 'nvr': u'js-tag-it-2.0-1.fc25', 'edited': u'js-tag-it-2.0-1.fc25',
                 'csrf_token': 'a_csrf_token', 'expired': True})
         self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
+
+
+class TestHandleErrors(unittest.TestCase):
+    """
+    Test the handle_errors decorator
+    """
+
+    @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_bodhi_client_exception(self, send_request):
+        """
+        Assert that BodhiClientExceptions are presented as expected
+        """
+        send_request.side_effect = bindings.BodhiClientException("Pants Exception")
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.save_buildroot_overrides,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', 'js-tag-it-2.0-1.fc25'])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertEqual("Pants Exception\n", result.output)
+
+    @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_other_client_exception(self, send_request):
+        """
+        Assert that AuthErrors are presented as expected
+        """
+        send_request.side_effect = AuthError("Authentication failed")
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.save_buildroot_overrides,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', 'js-tag-it-2.0-1.fc25'])
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual("Authentication failed: Check your FAS username & password\n",
+                         result.output)