Bug 1244738: Upgrade Bugsy to v0.7.0 r=gps
authorDavid Burns <dburns@mozilla.com>
Mon, 01 Feb 2016 15:36:27 +0000
changeset 362175 258e7ae1a885f29417f9496b577eb5657e5323fd
parent 362174 82c4760e61a825796657af0af9e93d9f2c9174de
child 362176 e6b7002791d62f96d7cbc50a6c24795aaae1891c
push id16998
push userrwood@mozilla.com
push dateMon, 02 May 2016 19:42:03 +0000
reviewersgps
bugs1244738
Bug 1244738: Upgrade Bugsy to v0.7.0 r=gps Change log: * Correct pyflakes error * Add error codes returned from Bugzilla to errror messages * Handle when Bugzilla returns errors with a 200 status code * Correct test when ting into an existing bug * Handle the case where isnt on the * Add the ability to get the blockers and dependent bugs * Pop messages on the bug that were added for creation otherwise you get an error * Get the ability to get keywords associated to the bug. * Add the ability to get the cc list of a bug
pylib/Bugsy/History.md
pylib/Bugsy/bugsy/bug.py
pylib/Bugsy/bugsy/bugsy.py
pylib/Bugsy/bugsy/errors.py
pylib/Bugsy/bugsy/search.py
pylib/Bugsy/setup.py
pylib/Bugsy/tests/test_bugs.py
pylib/Bugsy/tests/test_bugsy.py
pylib/Bugsy/tests/test_errors.py
pylib/Bugsy/tests/test_search.py
--- a/pylib/Bugsy/History.md
+++ b/pylib/Bugsy/History.md
@@ -1,8 +1,20 @@
+0.7.0 / 2016-02-01
+==================
+
+  * Correct pyflakes error
+  * Add error codes returned from Bugzilla to errror messages
+  * Handle when Bugzilla returns errors with a 200 status code
+  * Correct test when ting into an existing bug
+  * Handle the case where  isnt on the
+  * Add the ability to get the blockers and dependent bugs
+  * Pop messages on the bug that were added for creation otherwise you get an error
+  * Get the ability to get keywords associated to the bug.
+  * Add the ability to get the cc list of a bug
 
 0.6.0 / 2015-10-08
 ==================
 
   * Update flake8 config to only check the correct items.
   * Fix flake8 errors.
   * Move final requests to use central request and handler methods
   * Move Exception to BugsyException
--- a/pylib/Bugsy/bugsy/bug.py
+++ b/pylib/Bugsy/bugsy/bug.py
@@ -217,16 +217,63 @@ class Bug(object):
     def assigned_to(self, value):
         """
             Property to set the bug assignee
 
             >>> bug.assigned_to = "automatedtester@mozilla.com"
         """
         self._bug['assigned_to'] = value
 
+    @property
+    def cc(self):
+        """
+            Property to get the cc list for the bug. It returns emails for people
+
+            >>> bug.cc
+            [u'dburns@mozilla.com', u'automatedtester@mozilla.com']
+        """
+        cc_list = [cc_detail['email'] for cc_detail in self._bug['cc_detail']]
+        return cc_list
+
+    @property
+    def keywords(self):
+        """
+            Property to get the keywords list for the bug. It returns multiple
+            keywords in a list.
+
+            >>> bug.keywords
+            [u"ateam-marionette-runner", u"regression"]
+        """
+        keywords = [keyword for keyword in self._bug['keywords']]
+        return keywords
+
+    @property
+    def depends_on(self):
+        """
+            Property to get the bug numbers that depend on the current bug. It returns multiple
+            bug numbers in a list.
+
+            >>> bug.depends_on
+            [123456, 678901]
+        """
+        depends_on = [dep for dep in self._bug['depends_on']]
+        return depends_on
+
+    @property
+    def blocks(self):
+        """
+            Property to get the bug numbers that block on the current bug. It returns multiple
+            bug numbers in a list.
+
+            >>> bug.blocks
+            [123456, 678901]
+        """
+        depends_on = [dep for dep in self._bug['blocks']]
+        return depends_on
+
     def to_dict(self):
         """
             Return the raw dict that is used inside this object
         """
         return self._bug
 
     def update(self):
         """
--- a/pylib/Bugsy/bugsy/bugsy.py
+++ b/pylib/Bugsy/bugsy/bugsy.py
@@ -139,21 +139,29 @@ class Bugsy(object):
             raise BugsyException("Please pass in a Bug object when posting"
                                  " to Bugzilla")
 
         if not bug.id:
             result = self.request('bug', 'POST', data=bug.to_dict())
             if 'error' not in result:
                 bug._bug['id'] = result['id']
                 bug._bugsy = self
+                try:
+                    bug._bug.pop('comment')
+                except Exception:
+                    # If we don't have a `comment` we will error so let's just
+                    # swallow it.
+                    pass
             else:
                 raise BugsyException(result['message'])
         else:
             result = self.request('bug/%s' % bug.id, 'PUT',
                                   data=bug.to_dict())
+            updated_bug = self.get(bug.id)
+            return updated_bug
 
     @property
     def search_for(self):
         return Search(self)
 
     def request(self, path, method='GET', **kwargs):
         """Perform a HTTP request.
 
@@ -165,15 +173,18 @@ class Bugsy(object):
         kwargs['headers'] = headers
         url = '%s/%s' % (self.bugzilla_url, path)
         return self._handle_errors(self.session.request(method, url, **kwargs))
 
     def _handle_errors(self, response):
         if response.status_code >= 500:
             raise BugsyException("We received a {0} error with the following: {1}"
                                  .format(response.status_code, response.text))
-        if response.status_code > 399 and response.status_code < 500:
-            result = response.json()
+        result = response.json()
+        if (response.status_code > 399 and response.status_code < 500) \
+            or (isinstance(result, dict) and 'error' in result and
+                result.get('error', False) is True):
+
             if "API key" in result['message'] or "username or password" in result['message']:
-                raise LoginException(result['message'])
+                raise LoginException(result['message'], result.get("code"))
             else:
-                raise BugsyException(result["message"])
-        return response.json()
+                raise BugsyException(result["message"], result.get("code"))
+        return result
--- a/pylib/Bugsy/bugsy/errors.py
+++ b/pylib/Bugsy/bugsy/errors.py
@@ -1,18 +1,20 @@
 class BugsyException(Exception):
     """
         If while interacting with Bugzilla and we try do something that is not
         supported this error will be raised.
     """
-    def __init__(self, msg):
+    def __init__(self, msg, error_code=None):
         self.msg = msg
+        self.code = error_code
 
     def __str__(self):
-        return "Message: %s" % self.msg
+        return "Message: {message} Code: {code}".format(message=self.msg,
+                                                        code=self.code)
 
 
 class LoginException(BugsyException):
     """
         If a username and password are passed in but we don't receive a token
         then this error will be raised.
     """
     pass
--- a/pylib/Bugsy/bugsy/search.py
+++ b/pylib/Bugsy/bugsy/search.py
@@ -172,13 +172,14 @@ class Search(object):
             if self._whiteboard:
                 params['short_desc_type'] = 'allwordssubstr'
                 params['whiteboard'] = list(self._whiteboard)
             if self._change_history['fields']:
                 params['chfield'] = self._change_history['fields']
             if self._change_history.get('value', None):
                 params['chfieldvalue'] = self._change_history['value']
 
-            results = self._bugsy.request('bug', params=params)
-            error = results.get("error", None)
-            if error:
-                raise SearchException(results['message'])
+            try:
+                results = self._bugsy.request('bug', params=params)
+            except Exception as e:
+                raise SearchException(e.msg, e.code)
+
             return [Bug(self._bugsy, **bug) for bug in results['bugs']]
--- a/pylib/Bugsy/setup.py
+++ b/pylib/Bugsy/setup.py
@@ -1,12 +1,12 @@
 from setuptools import setup, find_packages
 
 setup(name='bugsy',
-      version='0.6.0',
+      version='0.7.0',
       description='A library for interacting Bugzilla Native REST API',
       author='David Burns',
       author_email='david.burns at theautomatedtester dot co dot uk',
       url='https://github.com/AutomatedTester/Bugsy',
       classifiers=['Development Status :: 3 - Alpha',
                   'Intended Audience :: Developers',
                   'License :: OSI Approved :: Apache Software License',
                   'Operating System :: POSIX',
--- a/pylib/Bugsy/tests/test_bugs.py
+++ b/pylib/Bugsy/tests/test_bugs.py
@@ -2,23 +2,23 @@ import datetime
 import responses
 import json
 
 from . import rest_url
 from bugsy import Bugsy, Bug
 from bugsy.errors import (BugsyException, BugException)
 
 example_return = {u'faults': [], u'bugs': [{u'cf_tracking_firefox29': u'---', u'classification': u'Other', u'creator': u'jgriffin@mozilla.com', u'cf_status_firefox30':
-u'---', u'depends_on': [], u'cf_status_firefox32': u'---', u'creation_time': u'2014-05-28T23:57:58Z', u'product': u'Release Engineering', u'cf_user_story': u'', u'dupe_of': None, u'cf_tracking_firefox_relnote': u'---', u'keywords': [], u'cf_tracking_b2g18': u'---', u'summary': u'Schedule Mn tests on o\
+u'---', u'depends_on': [123456], u'cf_status_firefox32': u'---', u'creation_time': u'2014-05-28T23:57:58Z', u'product': u'Release Engineering', u'cf_user_story': u'', u'dupe_of': None, u'cf_tracking_firefox_relnote': u'---', u'keywords': [u'regression'], u'cf_tracking_b2g18': u'---', u'summary': u'Schedule Mn tests on o\
 pt Linux builds on cedar', u'id': 1017315, u'assigned_to_detail': {u'id': 347295, u'email': u'jgriffin@mozilla.com', u'name': u'jgriffin@mozilla.com',
 u'real_name': u'Jonathan Griffin (:jgriffin)'}, u'severity': u'normal', u'is_confirmed': True, u'is_creator_accessible': True, u'cf_status_b2g_1_1_hd':
  u'---', u'qa_contact_detail': {u'id': 20203, u'email': u'catlee@mozilla.com', u'name': u'catlee@mozilla.com', u'real_name': u'Chris AtLee [:catlee]'},
  u'priority': u'--', u'platform': u'All', u'cf_crash_signature': u'', u'version': u'unspecified', u'cf_qa_whiteboard': u'', u'cf_status_b2g_1_3t': u'--\
 -', u'cf_status_firefox31': u'---', u'is_open': False, u'cf_blocking_fx': u'---', u'status': u'RESOLVED', u'cf_tracking_relnote_b2g': u'---', u'cf_stat\
-us_firefox29': u'---', u'blocks': [], u'qa_contact': u'catlee@mozilla.com', u'see_also': [], u'component': u'General Automation', u'cf_tracking_firefox\
+us_firefox29': u'---', u'blocks': [654321], u'qa_contact': u'catlee@mozilla.com', u'see_also': [], u'component': u'General Automation', u'cf_tracking_firefox\
 32': u'---', u'cf_tracking_firefox31': u'---', u'cf_tracking_firefox30': u'---', u'op_sys': u'All', u'groups': [], u'cf_blocking_b2g': u'---', u'target\
 _milestone': u'---', u'is_cc_accessible': True, u'cf_tracking_firefox_esr24': u'---', u'cf_status_b2g_1_2': u'---', u'cf_status_b2g_1_3': u'---', u'cf_\
 status_b2g18': u'---', u'cf_status_b2g_1_4': u'---', u'url': u'', u'creator_detail': {u'id': 347295, u'email': u'jgriffin@mozilla.com', u'name': u'jgri\
 ffin@mozilla.com', u'real_name': u'Jonathan Griffin (:jgriffin)'}, u'whiteboard': u'', u'cf_status_b2g_2_0': u'---', u'cc_detail': [{u'id': 30066, u'em\
 ail': u'coop@mozilla.com', u'name': u'coop@mozilla.com', u'real_name': u'Chris Cooper [:coop]'}, {u'id': 397261, u'email': u'dburns@mozilla.com', u'nam\
 e': u'dburns@mozilla.com', u'real_name': u'David Burns :automatedtester'}, {u'id': 438921, u'email': u'jlund@mozilla.com', u'name': u'jlund@mozilla.com ', u'real_name': u'Jordan Lund (:jlund)'}, {u'id': 418814, u'email': u'mdas@mozilla.com', u'name': u'mdas@mozilla.com', u'real_name': u'Malini Das [:md\
 as]'}], u'alias': None, u'cf_tracking_b2g_v1_2': u'---', u'cf_tracking_b2g_v1_3': u'---', u'flags': [], u'assigned_to': u'jgriffin@mozilla.com', u'cf_s\
 tatus_firefox_esr24': u'---', u'resolution': u'FIXED', u'last_change_time': u'2014-05-30T21:20:17Z', u'cc': [u'coop@mozilla.com', u'dburns@mozilla.com'
@@ -67,71 +67,96 @@ def test_can_create_bug_and_set_summary_
     assert bug.summary == 'Foo', "Summary is not being set"
     assert bug.status == '', 'Status has been set'
 
 def test_we_cant_set_status_unless_there_is_a_bug_id():
     bug = Bug()
     try:
         bug.status = 'RESOLVED'
     except BugException as e:
-        assert str(e) == "Message: Can not set status unless there is a bug id. Please call Update() before setting"
+        assert str(e) == "Message: Can not set status unless there is a bug id. Please call Update() before setting Code: None"
 
 def test_we_can_get_OS_set_from_default():
     bug = Bug()
     assert bug.OS == "All"
 
 def test_we_can_get_OS_we_set():
     bug = Bug(op_sys="Linux")
     assert bug.OS == "Linux"
 
 def test_we_can_get_Product_set_from_default():
     bug = Bug()
     assert bug.product == "core"
 
+def test_we_can_get_get_the_keywords():
+    bug = Bug(**example_return['bugs'][0])
+    keywords = bug.keywords
+    assert [u'regression'] == keywords
+
 def test_we_can_get_Product_we_set():
     bug = Bug(product="firefox")
     assert bug.product == "firefox"
 
+def test_we_can_get_get_cc_list():
+    bug = Bug(**example_return['bugs'][0])
+    cced = bug.cc
+    assert isinstance(cced, list)
+    assert [u"coop@mozilla.com", u"dburns@mozilla.com",
+            u"jlund@mozilla.com", u"mdas@mozilla.com"] == cced
+
+
 def test_we_throw_an_error_for_invalid_status_types():
     bug = Bug(**example_return['bugs'][0])
     try:
         bug.status = "foo"
         assert 1 == 0, "Should have thrown an error about invalid type"
     except BugException as e:
-        assert str(e) == "Message: Invalid status type was used"
+        assert str(e) == "Message: Invalid status type was used Code: None"
 
 def test_we_can_get_the_resolution():
     bug = Bug(**example_return['bugs'][0])
     assert "FIXED" == bug.resolution
 
 def test_we_can_set_the_resolution():
     bug = Bug(**example_return['bugs'][0])
     bug.resolution = 'INVALID'
     assert bug.resolution == 'INVALID'
 
 def test_we_cant_set_the_resolution_when_not_valid():
     bug = Bug(**example_return['bugs'][0])
     try:
         bug.resolution = 'FOO'
         assert 1==0, "Should thrown an error"
     except BugException as e:
-        assert str(e) == "Message: Invalid resolution type was used"
+        assert str(e) == "Message: Invalid resolution type was used Code: None"
 
 def test_we_can_pass_in_dict_and_get_a_bug():
     bug = Bug(**example_return['bugs'][0])
     assert bug.id == 1017315
     assert bug.status == 'RESOLVED'
     assert bug.summary == 'Schedule Mn tests on opt Linux builds on cedar'
     assert bug.assigned_to == "jgriffin@mozilla.com"
 
 def test_we_can_get_a_dict_version_of_the_bug():
     bug = Bug(**example_return['bugs'][0])
     result = bug.to_dict()
     assert example_return['bugs'][0]['id'] == result['id']
 
+def test_we_can_get_depends_on_list():
+    bug = Bug(**example_return['bugs'][0])
+    depends_on = bug.depends_on
+    assert isinstance(depends_on, list)
+    assert depends_on == [123456]
+
+def test_we_can_get_blocks_list():
+    bug = Bug(**example_return['bugs'][0])
+    blocks = bug.blocks
+    assert isinstance(blocks, list)
+    assert blocks == [654321]
+
 @responses.activate
 def test_we_can_update_a_bug_from_bugzilla():
     responses.add(responses.GET, rest_url('bug', 1017315),
                       body=json.dumps(example_return), status=200,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy()
     bug = bugzilla.get(1017315)
     import copy
@@ -144,17 +169,17 @@ def test_we_can_update_a_bug_from_bugzil
     bug.update()
     assert bug.status == 'REOPENED'
 
 def test_we_cant_update_unless_we_have_a_bug_id():
     bug = Bug()
     try:
         bug.update()
     except BugException as e:
-        assert str(e) == "Message: Unable to update bug that isn't in Bugzilla"
+        assert str(e) == "Message: Unable to update bug that isn't in Bugzilla Code: None"
 
 @responses.activate
 def test_we_can_update_a_bug_with_login_token():
   responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                         body='{"token": "foobar"}', status=200,
                         content_type='application/json', match_querystring=True)
 
   responses.add(responses.GET, rest_url('bug', 1017315, token='foobar'),
@@ -263,17 +288,17 @@ def test_we_raise_an_exception_when_gett
 
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/bug/1017315/comment?token=foobar',
                     body=json.dumps(error_response), status=400,
                     content_type='application/json', match_querystring=True)
     try:
         comments = bug.get_comments()
         assert False, "Should have raised an BugException for the bug not existing"
     except BugsyException as e:
-        assert str(e) == "Message: The requested method 'Bug.comments' was not found."
+        assert str(e) == "Message: The requested method 'Bug.comments' was not found. Code: 67399"
 
 @responses.activate
 def test_we_raise_an_exception_if_commenting_on_a_bug_that_returns_an_error():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                           body='{"token": "foobar"}', status=200,
                           content_type='application/json', match_querystring=True)
 
     responses.add(responses.GET, rest_url('bug', 1017315, token='foobar'),
@@ -290,17 +315,17 @@ def test_we_raise_an_exception_if_commen
                       'error': True}
     responses.add(responses.POST, 'https://bugzilla.mozilla.org/rest/bug/1017315/comment?token=foobar',
                       body=json.dumps(error_response), status=404,
                       content_type='application/json', match_querystring=True)
     try:
         bug.add_comment("I like sausages")
         assert False, "Should have raised an BugException for the bug not existing"
     except BugsyException as e:
-        assert str(e) == "Message: Bug 1017315 does not exist."
+        assert str(e) == "Message: Bug 1017315 does not exist. Code: 101"
 
     assert len(responses.calls) == 3
 
 @responses.activate
 def test_we_can_add_tags_to_bug_comments():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                           body='{"token": "foobar"}', status=200,
                           content_type='application/json', match_querystring=True)
--- a/pylib/Bugsy/tests/test_bugsy.py
+++ b/pylib/Bugsy/tests/test_bugsy.py
@@ -24,41 +24,41 @@ tatus_firefox_esr24': u'---', u'resoluti
 , u'jlund@mozilla.com', u'mdas@mozilla.com'], u'cf_blocking_fennec': u'---'}]}
 
 def test_we_cant_post_without_a_username_or_password():
     bugzilla = Bugsy()
     try:
         bugzilla.put("foo")
         assert 1 == 0, "Should have thrown when calling put"
     except BugsyException as e:
-        assert str(e) == "Message: Unfortunately you can't put bugs in Bugzilla without credentials"
+        assert str(e) == "Message: Unfortunately you can't put bugs in Bugzilla without credentials Code: None"
 
 @responses.activate
 def test_we_get_a_login_exception_when_details_are_wrong():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                       body='{"message": "The username or password you entered is not valid."}', status=400,
                       content_type='application/json', match_querystring=True)
     try:
         Bugsy("foo", "bar")
         assert 1 == 0, "Should have thrown an error"
     except LoginException as e:
-        assert str(e) == "Message: The username or password you entered is not valid."
+        assert str(e) == "Message: The username or password you entered is not valid. Code: None"
 
 @responses.activate
 def test_bad_api_key():
     responses.add(responses.GET,
                   'https://bugzilla.mozilla.org/rest/valid_login?login=foo&api_key=badkey',
                   body='{"documentation":"http://www.bugzilla.org/docs/tip/en/html/api/","error":true,"code":306,"message":"The API key you specified is invalid. Please check that you typed it correctly."}',
                   status=400,
                   content_type='application/json', match_querystring=True)
     try:
         Bugsy(username='foo', api_key='badkey')
         assert False, 'Should have thrown'
     except LoginException as e:
-        assert str(e) == 'Message: The API key you specified is invalid. Please check that you typed it correctly.'
+        assert str(e) == 'Message: The API key you specified is invalid. Please check that you typed it correctly. Code: 306'
 
 @responses.activate
 def test_validate_api_key():
     responses.add(responses.GET,
                   'https://bugzilla.mozilla.org/rest/valid_login?login=foo&api_key=goodkey',
                   body='true', status=200, content_type='application/json',
                   match_querystring=True)
     Bugsy(username='foo', api_key='goodkey')
@@ -68,17 +68,17 @@ def test_we_cant_post_without_passing_a_
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                       body='{"token": "foobar"}', status=200,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy("foo", "bar")
     try:
         bugzilla.put("foo")
         assert 1 == 0, "Should have thrown an error about type when calling put"
     except BugsyException as e:
-        assert str(e) == "Message: Please pass in a Bug object when posting to Bugzilla"
+        assert str(e) == "Message: Please pass in a Bug object when posting to Bugzilla Code: None"
 
 @responses.activate
 def test_we_can_get_a_bug():
     responses.add(responses.GET, rest_url('bug', 1017315),
                       body=json.dumps(example_return), status=200,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy()
     bug = bugzilla.get(1017315)
@@ -131,16 +131,19 @@ def test_we_can_put_a_current_bug():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                       body='{"token": "foobar"}', status=200,
                       content_type='application/json', match_querystring=True)
     bug_dict = example_return.copy()
     bug_dict['summary'] = 'I love foo but hate bar'
     responses.add(responses.PUT, 'https://bugzilla.mozilla.org/rest/bug/1017315',
                       body=json.dumps(bug_dict), status=200,
                       content_type='application/json')
+    responses.add(responses.GET, rest_url('bug', 1017315, token="foobar"),
+                      body=json.dumps(example_return), status=200,
+                      content_type='application/json', match_querystring=True)
     bugzilla = Bugsy("foo", "bar")
     bug = Bug(**example_return['bugs'][0])
     bug.summary = 'I love foo but hate bar'
     bug.assigned_to = "automatedtester@mozilla.com"
 
     bugzilla.put(bug)
     assert bug.summary == 'I love foo but hate bar'
     assert bug.assigned_to == "automatedtester@mozilla.com"
@@ -155,17 +158,17 @@ def test_we_handle_errors_from_bugzilla_
                     content_type='application/json')
 
   bugzilla = Bugsy("foo", "bar")
   bug = Bug()
   try:
       bugzilla.put(bug)
       assert 1 == 0, "Put should have raised an error"
   except BugsyException as e:
-      assert str(e) == "Message: You must select/enter a component."
+      assert str(e) == "Message: You must select/enter a component. Code: 50"
 
 @responses.activate
 def test_we_handle_errors_from_bugzilla_when_updating_a_bug():
   responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                     body='{"token": "foobar"}', status=200,
                     content_type='application/json', match_querystring=True)
   responses.add(responses.PUT, 'https://bugzilla.mozilla.org/rest/bug/1017315',
                     body='{"error":true,"code":50,"message":"You must select/enter a component."}', status=400,
@@ -173,17 +176,17 @@ def test_we_handle_errors_from_bugzilla_
   bugzilla = Bugsy("foo", "bar")
 
   bug_dict = example_return.copy()
   bug_dict['summary'] = 'I love foo but hate bar'
   bug = Bug(**bug_dict['bugs'][0])
   try:
       bugzilla.put(bug)
   except BugsyException as e:
-      assert str(e) == "Message: You must select/enter a component."
+      assert str(e) == "Message: You must select/enter a component. Code: 50"
 
 @responses.activate
 def test_we_can_set_the_user_agent_to_bugsy():
   responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                     body='{"token": "foobar"}', status=200,
                     content_type='application/json', match_querystring=True)
   Bugsy("foo", "bar")
   assert responses.calls[0].request.headers['User-Agent'] == "Bugsy"
@@ -199,11 +202,11 @@ def test_we_can_handle_errors_when_retri
     responses.add(responses.GET, rest_url('bug', 111111111),
                       body=json.dumps(error_response), status=404,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy()
     try:
         bug = bugzilla.get(111111111)
         assert False, "A BugsyException should have been thrown"
     except BugsyException as e:
-        assert str(e) == "Message: Bug 111111111111 does not exist."
+        assert str(e) == "Message: Bug 111111111111 does not exist. Code: 101"
     except Exception as e:
         assert False, "Wrong type of exception was thrown"
--- a/pylib/Bugsy/tests/test_errors.py
+++ b/pylib/Bugsy/tests/test_errors.py
@@ -10,37 +10,37 @@ import json
 @responses.activate
 def test_an_exception_is_raised_when_we_hit_an_error():
     responses.add(responses.GET, rest_url('bug', 1017315),
                       body="It's all broken", status=500,
                       content_type='application/json', match_querystring=True)
     bugzilla = Bugsy()
     with pytest.raises(BugsyException) as e:
         bugzilla.get(1017315)
-    assert str(e.value) == "Message: We received a 500 error with the following: It's all broken"
+    assert str(e.value) == "Message: We received a 500 error with the following: It's all broken Code: None"
 
 
 @responses.activate
 def test_bugsyexception_raised_for_http_502_when_retrieving_bugs():
     responses.add(responses.GET, rest_url('bug', 123456),
                   body='Bad Gateway', status=502,
                   content_type='text/html', match_querystring=True)
     bugzilla = Bugsy()
     with pytest.raises(BugsyException) as e:
         r = bugzilla.get(123456)
-    assert str(e.value) == "Message: We received a 502 error with the following: Bad Gateway"
+    assert str(e.value) == "Message: We received a 502 error with the following: Bad Gateway Code: None"
 
 
 @responses.activate
 def test_bugsyexception_raised_for_http_503_when_verifying_api_key():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/valid_login',
                   body='Service Unavailable', status=503, content_type='text/html')
     with pytest.raises(BugsyException) as e:
         Bugsy(username='foo', api_key='goodkey')
-    assert str(e.value) == "Message: We received a 503 error with the following: Service Unavailable"
+    assert str(e.value) == "Message: We received a 503 error with the following: Service Unavailable Code: None"
 
 
 @responses.activate
 def test_bugsyexception_raised_for_http_500_when_commenting_on_a_bug():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                   body='{"token": "foobar"}', status=200,
                   content_type='application/json', match_querystring=True)
     responses.add(responses.GET, rest_url('bug', 1017315, token='foobar'),
@@ -49,17 +49,17 @@ def test_bugsyexception_raised_for_http_
     bugzilla = Bugsy("foo", "bar")
     bug = bugzilla.get(1017315)
 
     responses.add(responses.POST, 'https://bugzilla.mozilla.org/rest/bug/1017315/comment?token=foobar',
                       body='Internal Server Error', status=500,
                       content_type='text/html', match_querystring=True)
     with pytest.raises(BugsyException) as e:
         bug.add_comment("I like sausages")
-    assert str(e.value) == "Message: We received a 500 error with the following: Internal Server Error"
+    assert str(e.value) == "Message: We received a 500 error with the following: Internal Server Error Code: None"
 
 
 @responses.activate
 def test_bugsyexception_raised_for_http_500_when_adding_tags_to_bug_comments():
     responses.add(responses.GET, 'https://bugzilla.mozilla.org/rest/login?login=foo&password=bar',
                           body='{"token": "foobar"}', status=200,
                           content_type='application/json', match_querystring=True)
 
@@ -75,9 +75,9 @@ def test_bugsyexception_raised_for_http_
 
     comments = bug.get_comments()
 
     responses.add(responses.PUT, 'https://bugzilla.mozilla.org/rest/bug/comment/8589785/tags?token=foobar',
                     body='Internal Server Error', status=500,
                     content_type='text/html', match_querystring=True)
     with pytest.raises(BugsyException) as e:
         comments[0].add_tags("foo")
-    assert str(e.value) == "Message: We received a 500 error with the following: Internal Server Error"
+    assert str(e.value) == "Message: We received a 500 error with the following: Internal Server Error Code: None"
--- a/pylib/Bugsy/tests/test_search.py
+++ b/pylib/Bugsy/tests/test_search.py
@@ -429,9 +429,9 @@ def test_we_can_handle_errors_coming_bac
 
     bugzilla = Bugsy()
     try:
         bugzilla.search_for\
                 .change_history_fields(['[Bug Creation]', 'Alias'], 'foo')\
                 .timeframe('2014-12-01', '2014-12-05')\
                 .search()
     except SearchException as e:
-        assert str(e) == "Message: Can't use [Bug Creation] as a field name."
+        assert str(e) == "Message: Can't use [Bug Creation] as a field name. Code: 108"