Repository: fedora-infra/bodhi

Issue:
Bodhi's REST API should separate the create and edit endpoints
While working on #1814 today I discovered that the REST API will set an update's severity to "unspecified" if the POST data does not include the severity field. This means that users who edit an update with the CLI and don't specify a ```--severity``` will end up altering the update to reset its severity to "unspecified".


Discussion:
It turns out that it's not just severity that gets reset, but all fields get reset if not specified in a follow-on post. If I use the CLI to change an update to have a stable karma of 5 and then use it to make a different change and don't specify that karma, it gets set back to 3 in the later update. I think this is rather surprising.

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -370,11 +370,23 @@ def edit(user, password, url, **kwargs):
             resp = client.query(**query_param)
             title = resp['updates'][0]['title']
         elif re.search(bindings.UPDATE_TITLE_RE, kwargs['update']):
+            query_param = {'like': kwargs['update']}
+            resp = client.query(**query_param)
             title = kwargs['update']
         del(kwargs['update'])
         kwargs['builds'] = title
         kwargs['edited'] = title
 
+        # Convert list of 'Bug' instances in DB to comma separated bug_ids for parsing.
+        former_update = resp['updates'][0]
+        if not kwargs['bugs']:
+            kwargs['bugs'] = ",".join([str(bug['bug_id']) for bug in former_update['bugs']])
+
+        # Replace empty fields with former values from database.
+        for field in kwargs:
+            if kwargs[field] in (None, '') and field in former_update:
+                kwargs[field] = former_update[field]
+
         resp = client.save(**kwargs)
         print_resp(resp, client)
     except bindings.BodhiClientException as e:


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
@@ -1121,12 +1121,12 @@ def test_severity_flag(self, send_request, query):
             mock.call(
                 bindings_client, 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
+                    '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': None, 'notes': None, 'notes_file': None, 'requirements': None,
-                    'request': None, 'bugs': u'', 'unstable_karma': None, 'type': 'bugfix',
-                    'severity': 'low'
+                    'suggest': u'unspecified', 'notes': u'New package.',
+                    'notes_file': None, 'request': None, 'unstable_karma': -3,
+                    'bugs': '1420605', 'requirements': u'', 'type': 'bugfix', 'severity': u'low'
                 }
             ),
             mock.call(
@@ -1163,12 +1163,12 @@ def test_url_flag(self, send_request, query):
             mock.call(
                 bindings_client, 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
+                    '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': None, 'notes': u'this is an edited note', 'notes_file': None,
-                    'requirements': None, 'request': None, 'bugs': u'', 'unstable_karma': None,
-                    'type': 'bugfix', 'severity': None
+                    'suggest': u'unspecified', 'notes': u'this is an edited note',
+                    'notes_file': None, 'request': None, 'severity': u'low',
+                    'bugs': '1420605', 'requirements': u'', 'unstable_karma': -3, 'type': 'bugfix'
                 }
             ),
             mock.call(
@@ -1210,13 +1210,13 @@ def test_notes_file(self, send_request, query):
                 mock.call(
                     bindings_client, 'updates/', auth=True, verb='POST',
                     data={
-                        'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
+                        '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': None, 'notes': 'This is a --notes-file note!',
-                        'notes_file': 'notefile.txt', 'request': None, 'bugs': u'',
-                        'requirements': None, 'unstable_karma': None, 'type': 'bugfix',
-                        'severity': None
+                        'suggest': 'unspecified', 'notes': 'This is a --notes-file note!',
+                        'notes_file': 'notefile.txt', 'request': None, 'severity': 'low',
+                        'bugs': '1420605', 'requirements': u'', 'unstable_karma': -3,
+                        'type': 'bugfix'
                     }
                 ),
                 mock.call(
@@ -1248,9 +1248,11 @@ def test_notes_and_notes_file(self):
 
     @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
                 mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.query',
+                return_value=client_test_data.EXAMPLE_QUERY_MUNCH, autospec=True)
     @mock.patch('bodhi.client.bindings.BodhiClient.send_request',
                 return_value=client_test_data.EXAMPLE_UPDATE_MUNCH, autospec=True)
-    def test_update_title(self, send_request):
+    def test_update_title(self, send_request, query):
         """
         Assert that we can successfully edit an update using the update title.
         """
@@ -1267,12 +1269,12 @@ def test_update_title(self, send_request):
             mock.call(
                 bindings_client, 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
-                    'staging': False, 'builds': u'drupal7-i18n-1.17-1.fc26', 'autokarma': False,
-                    'edited': u'drupal7-i18n-1.17-1.fc26', 'suggest': None, 'requirements': None,
-                    'notes': u'this is an edited note', 'notes_file': None,
-                    'request': None, 'bugs': u'', 'unstable_karma': None, 'type': 'bugfix',
-                    'severity': None
+                    'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
+                    'staging': False, 'builds': u'drupal7-i18n-1.17-1.fc26',
+                    'autokarma': False, 'edited': u'drupal7-i18n-1.17-1.fc26',
+                    'suggest': u'unspecified', 'notes': u'this is an edited note',
+                    'notes_file': None, 'request': None, 'bugs': '1420605',
+                    'unstable_karma': -3, 'type': 'bugfix', 'severity': u'low', 'requirements': u''
                 }
             ),
             mock.call(
@@ -1347,12 +1349,13 @@ def test_required_tasks(self, send_request, query):
             mock.call(
                 bindings_client, 'updates/', auth=True, verb='POST',
                 data={
-                    'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
+                    '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': None, 'notes': u'testing required tasks', 'notes_file': None,
-                    'requirements': u'dist.depcheck dist.rpmdeplint', 'request': None,
-                    'bugs': u'', 'unstable_karma': None, 'type': 'bugfix', 'severity': None
+                    'suggest': u'unspecified', 'notes': u'testing required tasks',
+                    'notes_file': None, 'request': None, 'severity': u'low',
+                    'bugs': '1420605', 'unstable_karma': -3,
+                    'requirements': u'dist.depcheck dist.rpmdeplint', 'type': 'bugfix'
                 }
             ),
             mock.call(