Repository: fedora-infra/bodhi

Issue:
The CLI gives unhelpful error messages if --notes are not given
I've not identified the cause of the issue yet, but I noticed that the CLI cannot create updates:

```
$ bodhi updates new s2i-core-0-13.f27container --url http://localhost:6543
Required
You may not specify an empty list of builds.
ACL validation mechanism was unable to determine ACLs.
```


Discussion:
Surprisingly, it seems that the error is due to missing notes. If I include a ```--notes``` flag, we do not get the above error:

```
$ bodhi updates new s2i-core-0-13.f27container --notes "a note"
Invalid tag: s2i-core-0-13.f27container not tagged with any of the following tags [u'f22-updates-candidate', u'dist-6E-epel-testing-candidate', u'f21-updates-candidate', u'f25-updates-candidate', u'f24-updates-candidate', u'epel7-testing-candidate', u'f27-modular-updates-candidate', u'dist-5E-epel-testing-candidate', u'f23-updates-candidate', u'f26-updates-candidate', u'f27-updates-candidate', u'f28-updates-candidate', u'f28-modular-updates-candidate', u'f27container-updates-candidate']
```

Thus it seems that we simply have an error message propagation issue, which is important to fix but not critical.
Note: This "smells" like an API issue to me, but @cverna pointed out that it would be good for the CLI to also require the notes field. Thus, I suggest ensuring that the API hands back a reasonable error message in this case (for non CLI users) and that the CLI just errors immediately when notes aren't given (for CLI users).

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -305,6 +305,10 @@ def new(user, password, url, **kwargs):
 
     kwargs['notes'] = _get_notes(**kwargs)
 
+    if not kwargs['notes']:
+        click.echo("ERROR: must specify at least one of --notes, --notes-file")
+        sys.exit(1)
+
     for update in updates:
         try:
             resp = client.save(**update)
@@ -364,6 +368,10 @@ def edit(user, password, url, **kwargs):
 
     kwargs['notes'] = _get_notes(**kwargs)
 
+    if not kwargs['notes']:
+        click.echo("ERROR: must specify at least one of --notes, --notes-file")
+        sys.exit(1)
+
     try:
         if re.search(bindings.UPDATE_ID_RE, kwargs['update']):
             query_param = {'updateid': kwargs['update']}
diff --git a/bodhi/server/schemas.py b/bodhi/server/schemas.py
--- a/bodhi/server/schemas.py
+++ b/bodhi/server/schemas.py
@@ -213,6 +213,7 @@ class SaveUpdateSchema(CSRFProtectedSchema, colander.MappingSchema):
     notes = colander.SchemaNode(
         colander.String(),
         validator=colander.Length(min=2),
+        missing_msg='A description is required for the update.'
     )
     autokarma = colander.SchemaNode(
         colander.Boolean(),


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
@@ -299,7 +299,7 @@ def test_severity_flag(self, send_request):
         result = runner.invoke(
             client.new,
             ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7',
-             '--severity', 'urgent'])
+             '--severity', 'urgent', '--notes', 'No description.'])
 
         self.assertEqual(result.exit_code, 0)
         expected_output = client_test_data.EXPECTED_UPDATE_OUTPUT.replace('unspecified', 'urgent')
@@ -311,7 +311,7 @@ def test_severity_flag(self, send_request):
                 data={
                     'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': u'bodhi-2.2.4-1.el7', 'autokarma': True,
-                    'suggest': None, 'notes': None, 'request': None, 'bugs': u'',
+                    'suggest': None, 'notes': u'No description.', 'request': None, 'bugs': u'',
                     'requirements': None, 'unstable_karma': None, 'file': None,
                     'notes_file': None, 'type': 'bugfix', 'severity': 'urgent'
                 }
@@ -337,7 +337,7 @@ def test_url_flag(self, send_request):
         result = runner.invoke(
             client.new,
             ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7',
-             '--url', 'http://localhost:6543'])
+             '--url', 'http://localhost:6543', '--notes', 'No description.'])
 
         self.assertEqual(result.exit_code, 0)
         expected_output = client_test_data.EXPECTED_UPDATE_OUTPUT.replace('example.com/tests',
@@ -350,7 +350,7 @@ def test_url_flag(self, send_request):
                 data={
                     'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': u'bodhi-2.2.4-1.el7', 'autokarma': True,
-                    'suggest': None, 'notes': None, 'request': None, 'bugs': u'',
+                    'suggest': None, 'notes': u'No description.', 'request': None, 'bugs': u'',
                     'requirements': None, 'unstable_karma': None, 'file': None,
                     'notes_file': None, 'type': 'bugfix', 'severity': None
                 }
@@ -395,7 +395,8 @@ def test_bodhi_client_exception(self, send_request):
 
         result = runner.invoke(
             client.new,
-            ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7'])
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7',
+             '--notes', 'No description.'])
 
         self.assertEqual(result.exit_code, 0)
         self.assertIn("This is a BodhiClientException message", result.output)
@@ -413,7 +414,8 @@ def test_exception(self, send_request):
 
         result = runner.invoke(
             client.new,
-            ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7'])
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7',
+             '--notes', 'No description.'])
 
         self.assertEqual(result.exit_code, 0)
         self.assertIn("Traceback (most recent call last):", result.output)
@@ -432,7 +434,8 @@ def test_close_bugs_flag(self, send_request):
         result = runner.invoke(
             client.new,
             ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7',
-             '--bugs', '1234567', '--close-bugs', '--url', 'http://localhost:6543'])
+             '--bugs', '1234567', '--close-bugs', '--url', 'http://localhost:6543', '--notes',
+             'No description.'])
 
         self.assertEqual(result.exit_code, 0)
         expected_output = client_test_data.EXPECTED_UPDATE_OUTPUT.replace('example.com/tests',
@@ -445,8 +448,8 @@ def test_close_bugs_flag(self, send_request):
                 data={
                     'close_bugs': True, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': u'bodhi-2.2.4-1.el7', 'autokarma': True,
-                    'suggest': None, 'notes': None, 'request': None, 'bugs': u'1234567',
-                    'requirements': None, 'unstable_karma': None, 'file': None,
+                    'suggest': None, 'notes': u'No description.', 'request': None,
+                    'bugs': u'1234567', 'requirements': None, 'unstable_karma': None, 'file': None,
                     'notes_file': None, 'type': 'bugfix', 'severity': None
                 }
             ),
@@ -459,6 +462,22 @@ def test_close_bugs_flag(self, send_request):
         self.assertEqual(send_request.mock_calls, calls)
         self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
 
+    def test_new_update_without_notes(self):
+        """
+        Assert providing neither --notes-file nor --notes parameters to new update request
+        results in an error.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.new,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7',
+             '--url', 'http://localhost:6543'])
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual(result.output, u'ERROR: must specify at least one of --notes, '
+                                        '--notes-file\n')
+
 
 class TestPrintOverrideKojiHint(unittest.TestCase):
     """
@@ -1110,7 +1129,8 @@ def test_severity_flag(self, send_request, query):
 
         result = runner.invoke(
             client.edit, ['FEDORA-2017-cc8582d738', '--user', 'bowlofeggs',
-                          '--password', 's3kr3t', '--severity', 'low'])
+                          '--password', 's3kr3t', '--severity', 'low',
+                          '--notes', 'Updated package.'])
 
         self.assertEqual(result.exit_code, 0)
         bindings_client = query.mock_calls[0][1][0]
@@ -1124,7 +1144,7 @@ def test_severity_flag(self, send_request, query):
                     'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
                     'staging': False, 'builds': u'nodejs-grunt-wrap-0.3.0-2.fc25',
                     'autokarma': False, 'edited': u'nodejs-grunt-wrap-0.3.0-2.fc25',
-                    'suggest': u'unspecified', 'notes': u'New package.',
+                    'suggest': u'unspecified', 'notes': u'Updated package.',
                     'notes_file': None, 'request': None, 'unstable_karma': -3,
                     'bugs': '1420605', 'requirements': u'', 'type': 'bugfix', 'severity': u'low'
                 }
@@ -1380,11 +1400,27 @@ def test_bodhi_client_exception(self, send_request):
 
         result = runner.invoke(
             client.edit, ['FEDORA-2017-cc8582d738', '--user', 'bowlofeggs',
-                          '--password', 's3kr3t'])
+                          '--password', 's3kr3t', '--notes', 'No description.'])
 
         self.assertEqual(result.exit_code, 0)
         self.assertIn("This is a BodhiClientException message", result.output)
 
+    def test_edit_update_without_notes(self):
+        """
+        Assert providing neither --notes-file nor --notes parameters to updates edit request
+        results in an error.
+        """
+        runner = testing.CliRunner()
+
+        result = runner.invoke(
+            client.edit,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'bodhi-2.2.4-1.el7',
+             '--url', 'http://localhost:6543'])
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual(result.output, u'ERROR: must specify at least one of --notes, '
+                                        '--notes-file\n')
+
 
 class TestEditBuilrootOverrides(unittest.TestCase):
     """