From 9f08bd59df41b0fed656b348161a3f7c55563d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 9 Aug 2017 17:40:41 -0400 Subject: [PATCH 001/393] Python 2 binary package renamed to python2-bugzilla %python_provide added to python3 subpackage Signed-off-by: Cole Robinson --- python-bugzilla.spec | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 1418eb7b..144bba7b 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -28,6 +28,14 @@ BuildRequires: python3-requests BuildRequires: python3-setuptools %endif # if with_python3 +%global _description\ +python-bugzilla is a python library for interacting with bugzilla instances\ +over XML-RPC.\ + +%description %_description + +%package -n python2-bugzilla +Summary: %summary Requires: python-requests Requires: python-magic %if 0%{?el6} @@ -36,22 +44,18 @@ Requires: python-argparse # This dep is for back compat, so that installing python-bugzilla continues # to give the cli tool Requires: python-bugzilla-cli +%{?python_provide:%python_provide python2-bugzilla} - -%description -python-bugzilla is a python 2 library for interacting with bugzilla instances -over XML-RPC. - +%description -n python2-bugzilla %_description %if 0%{?with_python3} %package -n python3-bugzilla -Summary: python 3 library for interacting with Bugzilla +Summary: %summary Requires: python3-requests Requires: python3-magic +%{?python_provide:%python_provide python3-bugzilla} -%description -n python3-bugzilla -python3-bugzilla is a python 3 library for interacting with bugzilla instances -over XML-RPC. +%description -n python3-bugzilla %_description %endif # if with_python3 @@ -118,7 +122,7 @@ done -%files +%files -n python2-bugzilla %doc COPYING README.md NEWS.md %{python2_sitelib}/* From 343760b5672628f892b82f29a28d2d118e502982 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 20 Oct 2017 18:56:40 -0400 Subject: [PATCH 002/393] cli: Document how to clear a flag like --flag needinfoX https://bugzilla.redhat.com/show_bug.cgi?id=1504870 --- bin/bugzilla | 3 ++- bugzilla.1 | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/bugzilla b/bin/bugzilla index 38588257..0ea5b589 100755 --- a/bin/bugzilla +++ b/bin/bugzilla @@ -230,7 +230,8 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('-f', '--flag', action='append', help="Bug flags state. Ex:\n" " --flag needinfo?\n" - " --flag dev_ack+") + " --flag dev_ack+ \n" + " clear with --flag needinfoX") p.add_argument("--tags", action="append", help="Tags field.") p.add_argument('-w', "--whiteboard", '--status_whiteboard', diff --git a/bugzilla.1 b/bugzilla.1 index 88e54213..a8ff5f36 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -111,6 +111,8 @@ RHBZ internal whiteboard field RHBZ QA whiteboard field .IP "--fixed_in FIXED_IN, -F FIXED_IN RHBZ 'Fixed in version' field +.IP "--flag=FLAG" +Set or unset a flag. For example, to set a flag named devel_ack, do --flag devel_ack+ Unset a flag with the 'X' value, like --flag needinfoX .IP "--field=FIELD=VALUE" Manually specify a bugzilla XMLRPC field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE From 49f7d78aaf36c9cc9e3a30a9f492ae4ed364d06e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 20 Oct 2017 19:38:53 -0400 Subject: [PATCH 003/393] Fix uploading binary attachments (bz 1496821) https://bugzilla.redhat.com/show_bug.cgi?id=1496821 --- bin/bugzilla | 2 +- bugzilla/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/bugzilla b/bin/bugzilla index 0ea5b589..8de30330 100755 --- a/bin/bugzilla +++ b/bin/bugzilla @@ -929,7 +929,7 @@ def _do_set_attach(bz, opt, parser, args): if sys.stdin.isatty(): if not opt.file: parser.error("--file must be specified") - fileobj = open(opt.file) + fileobj = open(opt.file, "rb") else: # piped input on stdin if not opt.desc: diff --git a/bugzilla/base.py b/bugzilla/base.py index b7f14f40..d593221e 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1481,7 +1481,7 @@ def attachfile(self, idlist, attachfile, description, **kwargs): attachment was added, we return the single int ID for back compat ''' if isinstance(attachfile, str): - f = open(attachfile) + f = open(attachfile, "rb") elif hasattr(attachfile, 'read'): f = attachfile else: From 2ebbdfd2c96b23ec8a7fbaa3a9fbe86eeff082d9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 20 Nov 2017 07:21:01 -0500 Subject: [PATCH 004/393] rhbugzilla: Fix Bug.get with sub_components (bz 1503491) At some point we started passing 'sub_component' instead of 'sub_components' to extra_fields, which isn't correct https://bugzilla.redhat.com/show_bug.cgi?id=1503491 --- bugzilla/rhbugzilla.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 04910c2e..55ee601b 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -43,7 +43,8 @@ def _add_both_alias(newname, origname): self._add_field_alias('component', 'components', is_bug=False) self._add_field_alias('version', 'versions', is_bug=False) - self._add_field_alias('sub_component', 'sub_components', is_bug=False) + # Yes, sub_components is the field name the API expects + self._add_field_alias('sub_components', 'sub_component', is_bug=False) # flags format isn't exactly the same but it's the closest approx self._add_field_alias('flags', 'flag_types') From 3d87c290682f75ffad0657f498759526184f8a11 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 21 Nov 2017 15:44:19 -0500 Subject: [PATCH 005/393] bin: Give consistent error with py3 if missing command (bz 1513819) There was a py3 behavior change here: https://stackoverflow.com/a/18283730 https://bugzilla.redhat.com/show_bug.cgi?id=1513819 --- bin/bugzilla | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/bugzilla b/bin/bugzilla index 8de30330..ba26cd2f 100755 --- a/bin/bugzilla +++ b/bin/bugzilla @@ -392,7 +392,8 @@ def _setup_action_login_parser(subparsers): def setup_parser(): rootparser = _setup_root_parser() - subparsers = rootparser.add_subparsers(dest="command_name") + subparsers = rootparser.add_subparsers(dest="command") + subparsers.required = True _setup_action_new_parser(subparsers) _setup_action_query_parser(subparsers) _setup_action_info_parser(subparsers) @@ -1028,7 +1029,7 @@ def _handle_login(opt, parser, args, action, bz): def main(unittest_bz_instance=None): parser = setup_parser() opt, args = parser.parse_known_args() - action = opt.command_name + action = opt.command if opt.debug: log.setLevel(DEBUG) From 0ee584156c6b5d75d3aaf1800c809b84fbdc48ab Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 21 Nov 2017 15:48:49 -0500 Subject: [PATCH 006/393] bin: Use python3 in shebang It's what we use for distros nowadays --- bin/bugzilla | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/bugzilla b/bin/bugzilla index ba26cd2f..46488546 100755 --- a/bin/bugzilla +++ b/bin/bugzilla @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # bugzilla - a commandline frontend for the python bugzilla module # From 468238a569ac7701651c172de642cde4160e7b96 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 9 Nov 2017 14:09:21 +0100 Subject: [PATCH 007/393] Work around Bugzilla 4 no longer returning creator field As documented in bz#1515284, Bugzilla 4 is currently broken. So we need to fallback asking author field as creator field will likely not be returned until Bugzilla 5. Closes: #56 Signed-off-by: Benoit Donneaux --- bin/bugzilla | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/bugzilla b/bin/bugzilla index 46488546..21290d69 100755 --- a/bin/bugzilla +++ b/bin/bugzilla @@ -687,7 +687,7 @@ def _format_output(bz, opt, buglist): val = "" for c in getattr(b, "comments", []): val += ("\n* %s - %s:\n%s\n" % - (c['time'], c.get("creator", ""), c['text'])) + (c['time'], c.get("creator", c.get("author", "")), c['text'])) elif fieldname == "__unicode__": val = b.__unicode__() From da66662adcd00b5338977806bd3c9c51797aeec5 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Tue, 6 Feb 2018 16:29:16 +0100 Subject: [PATCH 008/393] base: Fix api_key authentication regression Identifiers with a leading "__" are mangled, ensure that the original method is overridden such that authentication tokens are added. Fixes: v2.1.0-11-g3c692f4f4e ("_BugzillaServerProxy as new-style class") --- bugzilla/transport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 92cc3fa6..ba9af721 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -83,7 +83,7 @@ def use_api_key(self, api_key): def clear_token(self): self.token_cache.value = None - def __request(self, methodname, params): + def _ServerProxy__request(self, methodname, params): if len(params) == 0: params = ({}, ) @@ -94,7 +94,7 @@ def __request(self, methodname, params): if 'Bugzilla_token' not in params[0]: params[0]['Bugzilla_token'] = self.token_cache.value - ret = super(_BugzillaServerProxy, self).__request(methodname, params) + ret = super(_BugzillaServerProxy, self)._ServerProxy__request(methodname, params) if isinstance(ret, dict) and 'token' in ret.keys(): self.token_cache.value = ret.get('token') From 45e14585ac2811c6e569192e7a2b5277049537a3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 12:21:14 -0500 Subject: [PATCH 009/393] tests: rw: Add api_key test Needs bugzilla 5 Suggested-by: Peter Wu --- tests/rw_functional.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/rw_functional.py b/tests/rw_functional.py index 050ab9d4..1e288d66 100644 --- a/tests/rw_functional.py +++ b/tests/rw_functional.py @@ -959,3 +959,17 @@ def test16ModifyTags(self): bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() self.assertEqual(bug.tags, []) + + def test17LoginAPIKey(self): + api_key = "somefakeapikey1234" + bz = self.bzclass(url=self.url, use_creds=False, api_key=api_key) + if bz.bz_ver_major < 5: + self.skipTest("can only test apikey on bugzilla 5+") + + try: + self.assertTrue(bz.logged_in, False) + + # Use this to trigger a warning about api_key + bz.createbug(bz.build_createbug()) + except Exception as e: + self.assertTrue("The API key you specified is invalid" in str(e)) From ea23c04140a2f9a1a99c11d2f92cfecb1863ac4c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 13:24:06 -0500 Subject: [PATCH 010/393] base: Reimplement openattachment to use get_attachments This impl predates any attachment APIs, so it grabs attachment from a URL. This is broken with current bugzilla.redhat.com though, and since it requires silly tricks anyways, just convert to using standard APIs --- bugzilla/base.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index d593221e..541f25d4 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1531,34 +1531,13 @@ def attachfile(self, idlist, attachfile, description, **kwargs): def openattachment(self, attachid): '''Get the contents of the attachment with the given attachment ID. Returns a file-like object.''' - - def get_filename(headers): - import re - - match = re.search( - r'^.*filename="?(.*)"$', - headers.get('content-disposition', '') - ) - - # default to attchid if no match was found - return match.group(1) if match else attachid - - att_uri = self._attachment_uri(attachid) - - defaults = self._transport.request_defaults.copy() - defaults["headers"] = defaults["headers"].copy() - del(defaults["headers"]["Content-Type"]) - - response = self._transport.session.get( - att_uri, stream=True, **defaults) + attachments = self.get_attachments(None, attachid) + data = attachments["attachments"][str(attachid)] + xmlrpcbinary = data["data"] ret = BytesIO() - for chunk in response.iter_content(chunk_size=1024): - if chunk: - ret.write(chunk) - ret.name = get_filename(response.headers) - - # Hooray, now we have a file-like object with .read() and .name + ret.write(xmlrpcbinary.data) + ret.name = data["file_name"] ret.seek(0) return ret From 937834cbc4d3f0927fb801ce313372f6dbed26c5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 13:32:22 -0500 Subject: [PATCH 011/393] base: Fix editing dictionary during iteration --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 541f25d4..e31ce20f 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -784,7 +784,7 @@ def _component_data_convert(self, data, update=False): names = {"product": data.pop("product"), "component": data.pop("component")} updates = {} - for k in data.keys(): + for k in list(data.keys()): updates[k] = data.pop(k) data["names"] = [names] From 5e112ae28cd854b5cb5543841347800a725a2663 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 15:09:33 -0500 Subject: [PATCH 012/393] Convert to 'Exception as' syntax --- bin/bugzilla | 19 +++++++------------ bugzilla/base.py | 15 +++++---------- bugzilla/transport.py | 5 ++--- tests/__init__.py | 3 +-- tests/rw_functional.py | 21 +++++++-------------- 5 files changed, 22 insertions(+), 41 deletions(-) diff --git a/bin/bugzilla b/bin/bugzilla index 21290d69..ba9d0ac8 100755 --- a/bin/bugzilla +++ b/bin/bugzilla @@ -79,8 +79,7 @@ def open_without_clobber(name, *args): while fd is None: try: fd = os.open(name, os.O_CREAT | os.O_EXCL, 0o666) - except OSError: - err = sys.exc_info()[1] + except OSError as err: if err.errno == os.errno.EEXIST: name = "%s.%i" % (orig_name, count) count += 1 @@ -1008,8 +1007,8 @@ def _handle_login(opt, parser, args, action, bz): print("Logging into %s" % urlparse(bz.url)[1]) bz.interactive_login( opt.username, opt.password) - except bugzilla.BugzillaError: - print(str(sys.exc_info()[1])) + except bugzilla.BugzillaError as e: + print(str(e)) sys.exit(1) if opt.ensure_logged_in and not bz.logged_in: @@ -1120,13 +1119,11 @@ if __name__ == '__main__': log.debug("", exc_info=True) print("\nExited at user request.") sys.exit(1) - except (Fault, bugzilla.BugzillaError): - e = sys.exc_info()[1] + except (Fault, bugzilla.BugzillaError) as e: log.debug("", exc_info=True) print("\nServer error: %s" % str(e)) sys.exit(3) - except ProtocolError: - e = sys.exc_info()[1] + except ProtocolError as e: log.debug("", exc_info=True) print("\nInvalid server response: %d %s" % (e.errcode, e.errmsg)) # Detect redirect @@ -1135,8 +1132,7 @@ if __name__ == '__main__': print("\nServer was attempting a redirect. Try: " " bugzilla --bugzilla %s ..." % redir) sys.exit(4) - except requests.exceptions.SSLError: - e = sys.exc_info()[1] + except requests.exceptions.SSLError as e: log.debug("", exc_info=True) # Give SSL recommendations @@ -1147,8 +1143,7 @@ if __name__ == '__main__': sys.exit(4) except (socket.error, requests.exceptions.HTTPError, - requests.exceptions.ConnectionError): - e = sys.exc_info()[1] + requests.exceptions.ConnectionError) as e: log.debug("", exc_info=True) print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) diff --git a/bugzilla/base.py b/bugzilla/base.py index e31ce20f..71a066b3 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -54,8 +54,7 @@ def _detect_filetype(fname): import magic mimemagic = magic.open(getattr(magic, "MAGIC_MIME_TYPE", 16)) mimemagic.load() - except ImportError: - e = sys.exc_info()[1] + except ImportError as e: log.debug("Could not load python-magic: %s", e) mimemagic = None if not mimemagic: @@ -66,8 +65,7 @@ def _detect_filetype(fname): try: return mimemagic.file(fname) - except Exception: - e = sys.exc_info()[1] + except Exception as e: log.debug("Could not detect content_type: %s", e) return None @@ -611,8 +609,7 @@ def login(self, user=None, password=None): self.password = '' log.info("login successful for user=%s", self.user) return ret - except Fault: - e = sys.exc_info()[1] + except Fault as e: raise BugzillaError("Login failed: %s" % str(e.faultString)) def interactive_login(self, user=None, password=None, force=False): @@ -667,8 +664,7 @@ def logged_in(self): try: self._proxy.User.get({'ids': []}) return True - except Fault: - e = sys.exc_info()[1] + except Fault as e: if e.faultCode == 505 or e.faultCode == 32000: return False raise e @@ -1215,8 +1211,7 @@ def query(self, query): log.debug("Calling Bug.search with: %s", query) try: r = self._proxy.Bug.search(query) - except Fault: - e = sys.exc_info()[1] + except Fault as e: # Try to give a hint in the error message if url_to_query # isn't supported by this bugzilla instance diff --git a/bugzilla/transport.py b/bugzilla/transport.py index ba9af721..09c77f3e 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -167,14 +167,13 @@ def _request_helper(self, url, request_body): response.raise_for_status() return self.parse_response(response) - except requests.RequestException: - e = sys.exc_info()[1] + except requests.RequestException as e: if not response: raise raise ProtocolError( url, response.status_code, str(e), response.headers) except Fault: - raise sys.exc_info()[1] + raise except Exception: # pylint: disable=W0201 e = BugzillaError(str(sys.exc_info()[1])) diff --git a/tests/__init__.py b/tests/__init__.py index a7442ead..d6f1573c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -101,8 +101,7 @@ def clicomm(argv, bzinstance, returnmain=False, printcliout=False, print() mainout = bugzillascript.main(unittest_bz_instance=bzinstance) - except SystemExit: - sys_e = sys.exc_info()[1] + except SystemExit as sys_e: ret = sys_e.code outt = "" diff --git a/tests/rw_functional.py b/tests/rw_functional.py index 1e288d66..7b27d974 100644 --- a/tests/rw_functional.py +++ b/tests/rw_functional.py @@ -720,8 +720,7 @@ def test11UserUpdate(self): bz.updateperms(email, "remove", [group]) user.refresh() self.assertTrue(group not in user.groupnames) - except: - e = sys.exc_info()[1] + except Exception as e: if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) @@ -731,8 +730,7 @@ def test11UserUpdate(self): bz.updateperms(email, "add", group) user.refresh() self.assertTrue(group in user.groupnames) - except: - e = sys.exc_info()[1] + except Exception as e: if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) @@ -745,8 +743,7 @@ def test11UserUpdate(self): bz.updateperms(email, "set", newgroups) user.refresh() self.assertTrue(group not in user.groupnames) - except: - e = sys.exc_info()[1] + except Exception as e: if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) @@ -754,8 +751,7 @@ def test11UserUpdate(self): # Reset everything try: bz.updateperms(email, "set", origgroups) - except: - e = sys.exc_info()[1] + except Exception as e: if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) @@ -809,8 +805,7 @@ def compare(data, newid): print("Created product=%s component=%s" % ( basedata["product"], basedata["component"])) compare(data, newid) - except: - e = sys.exc_info()[1] + except Exception as e: if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) @@ -829,8 +824,7 @@ def compare(data, newid): try: bz.editcomponent(data) compare(data, newid) - except: - e = sys.exc_info()[1] + except Exception as e: if have_admin: raise self.assertTrue("Sorry, you aren't a member" in str(e)) @@ -843,8 +837,7 @@ def test12SetCookie(self): bz.cookiefile = None raise AssertionError("Setting cookiefile for active connection " "should fail.") - except RuntimeError: - e = sys.exc_info()[1] + except RuntimeError as e: self.assertTrue("disconnect()" in str(e)) bz.disconnect() From ef6771149de01cab227e6947c052a410314c4777 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 15:16:40 -0500 Subject: [PATCH 013/393] tests: Fix admin failure tests with bz5 --- tests/rw_functional.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/rw_functional.py b/tests/rw_functional.py index 7b27d974..5939e7a7 100644 --- a/tests/rw_functional.py +++ b/tests/rw_functional.py @@ -800,6 +800,7 @@ def compare(data, newid): "initialcclist": ["wwoods@redhat.com", "toshio@fedoraproject.org"], "is_active": True, }) + newid = None try: newid = bz.addcomponent(data)['id'] print("Created product=%s component=%s" % ( @@ -808,7 +809,10 @@ def compare(data, newid): except Exception as e: if have_admin: raise - self.assertTrue("Sorry, you aren't a member" in str(e)) + self.assertTrue( + ("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) # Edit component @@ -823,11 +827,15 @@ def compare(data, newid): }) try: bz.editcomponent(data) - compare(data, newid) + if newid is not None: + compare(data, newid) except Exception as e: if have_admin: raise - self.assertTrue("Sorry, you aren't a member" in str(e)) + self.assertTrue( + ("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) def test12SetCookie(self): bz = self.bzclass(self.url, From 9aad8aab08ea48b3049587542ff348a72b80008c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 15:30:17 -0500 Subject: [PATCH 014/393] tests: Don't spam version printing for --redhat-url --- tests/ro_functional.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/ro_functional.py b/tests/ro_functional.py index 6aa883f4..d5c11075 100644 --- a/tests/ro_functional.py +++ b/tests/ro_functional.py @@ -39,10 +39,9 @@ def _testBZVersion(self): bz = Bugzilla(self.url, use_creds=False) self.assertEqual(bz.__class__, self.bzclass) if tests.REDHAT_URL: - print("BZ version=%s.%s" % (bz.bz_ver_major, bz.bz_ver_minor)) - else: - self.assertEqual(bz.bz_ver_major, self.bzversion[0]) - self.assertEqual(bz.bz_ver_minor, self.bzversion[1]) + return + self.assertEqual(bz.bz_ver_major, self.bzversion[0]) + self.assertEqual(bz.bz_ver_minor, self.bzversion[1]) # Since we are running these tests against bugzilla instances in # the wild, we can't depend on certain data like product lists From 82df2eca5db5fbe76f0dc231c8547046d48e043f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 16:02:26 -0500 Subject: [PATCH 015/393] setup: Switch to pycodestyle and pylint imports Which seamlessly let's us run pylint/pycodestyle depending on the invoking python version, 2 or 3 --- CONTRIBUTING.md | 8 +++---- setup.py | 37 +++++++++++++++++++---------- test-requirements.txt | 3 ++- tests/{pep8.cfg => pycodestyle.cfg} | 2 +- 4 files changed, 31 insertions(+), 19 deletions(-) rename tests/{pep8.cfg => pycodestyle.cfg} (96%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 638ebc97..ec9b93d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,19 +39,19 @@ To test all supported python versions, run tox using any of the following. tox -- --rw-functional -# pylint and pep8 +# pylint and pycodestyle -To test for pylint or pep8 violations, you can run: +To test for pylint or pycodestyle violations, you can run: python setup.py pylint -Note: This expects that you already have pylint and pep8 installed. +Note: This expects that you already have pylint and pycodestyle installed. # Patch Submission If you are submitting a patch, ensure the following: - [REQ] verify that no new pylint or pep8 violations + [REQ] verify that no new pylint or pycodestyle violations [REQ] run basic unit test suite across all python versions as described above. diff --git a/setup.py b/setup.py index 456962ee..5aca8306 100755 --- a/setup.py +++ b/setup.py @@ -144,21 +144,32 @@ def finalize_options(self): pass def _run(self): - files = ["bugzilla/", "bin-bugzilla", "examples/*.py", "tests/*.py"] + import pylint.lint + import pycodestyle + + files = (["bugzilla/", "bin-bugzilla"] + + glob.glob("examples/*.py") + + glob.glob("tests/*.py")) output_format = sys.stdout.isatty() and "colorized" or "text" - if os.path.exists("/usr/bin/pylint-2"): - cmd = "pylint-2 " - else: - cmd = "pylint " - cmd += "--output-format=%s " % output_format - cmd += " ".join(files) - os.system(cmd + " --rcfile tests/pylint.cfg") - - print("running pep8") - cmd = "pep8 " - cmd += " ".join(files) - os.system(cmd + " --config tests/pep8.cfg --exclude oldclasses.py") + print("running pycodestyle") + style_guide = pycodestyle.StyleGuide( + config_file='tests/pycodestyle.cfg', + paths=files, + ) + style_guide.options.exclude = pycodestyle.normalize_paths( + "bugzilla/oldclasses.py", + ) + report = style_guide.check_files() + if style_guide.options.count: + sys.stderr.write(str(report.total_errors) + '\n') + + print("running pylint") + pylint_opts = [ + "--rcfile", "tests/pylint.cfg", + "--output-format=%s" % output_format, + ] + pylint.lint.Run(files + pylint_opts) def run(self): os.link("bin/bugzilla", "bin-bugzilla") diff --git a/test-requirements.txt b/test-requirements.txt index f45fca24..1851b0e9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ # additional packages needed for testing coverage -pep8 +pycodestyle +pylint diff --git a/tests/pep8.cfg b/tests/pycodestyle.cfg similarity index 96% rename from tests/pep8.cfg rename to tests/pycodestyle.cfg index e6179eac..b9f16e23 100644 --- a/tests/pep8.cfg +++ b/tests/pycodestyle.cfg @@ -1,4 +1,4 @@ -[pep8] +[pycodestyle] format = pylint From 91df70dcc36b776b62bc1605c91c7d3afe8651dc Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 16:08:19 -0500 Subject: [PATCH 016/393] setup: Default to running as python3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5aca8306..f2dd5b23 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function From 8d136fc05d9c587ac0e26b4b45cd4aec74f4e68d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 16:39:10 -0500 Subject: [PATCH 017/393] Clean up pylint/pycodestyle warnings for py2 and py3 --- bin/bugzilla | 10 ++++++---- bugzilla/base.py | 17 +++++------------ bugzilla/transport.py | 17 ++++++++++------- examples/apikey.py | 2 ++ tests/__init__.py | 2 ++ tests/ro_functional.py | 14 ++++++-------- tests/rw_functional.py | 20 ++++++++++---------- 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/bin/bugzilla b/bin/bugzilla index ba9d0ac8..d8cff075 100755 --- a/bin/bugzilla +++ b/bin/bugzilla @@ -23,14 +23,16 @@ import socket import sys import tempfile +# pylint: disable=import-error if sys.version_info[0] >= 3: - # pylint: disable=F0401,W0622,E0611 + # pylint: disable=no-name-in-module,redefined-builtin from xmlrpc.client import Fault, ProtocolError from urllib.parse import urlparse basestring = (str, bytes) else: from xmlrpclib import Fault, ProtocolError from urlparse import urlparse +# pylint: enable=import-error import requests.exceptions @@ -415,7 +417,7 @@ def _merge_field_opts(query, opt, parser): try: f, v = f.split('=', 1) query[f] = v - except: + except Exception: parser.error("Invalid field argument provided: %s" % (f)) @@ -685,8 +687,8 @@ def _format_output(bz, opt, buglist): elif fieldname == "comments": val = "" for c in getattr(b, "comments", []): - val += ("\n* %s - %s:\n%s\n" % - (c['time'], c.get("creator", c.get("author", "")), c['text'])) + val += ("\n* %s - %s:\n%s\n" % (c['time'], + c.get("creator", c.get("author", "")), c['text'])) elif fieldname == "__unicode__": val = b.__unicode__() diff --git a/bugzilla/base.py b/bugzilla/base.py index 71a066b3..31e57f20 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -17,9 +17,9 @@ from io import BytesIO -# pylint: disable=ungrouped-imports +# pylint: disable=import-error if sys.version_info[0] >= 3: - # pylint: disable=F0401,E0611 + # pylint: disable=no-name-in-module from configparser import SafeConfigParser from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse, parse_qsl @@ -29,6 +29,7 @@ from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse, parse_qsl from xmlrpclib import Binary, Fault +# pylint: enable=import-error from .apiversion import __version__ @@ -42,15 +43,11 @@ def _detect_filetype(fname): - # pylint: disable=E1103 - # E1103: Instance of 'bool' has no '%s' member - # pylint confuses mimemagic to be of type 'bool' global mimemagic if mimemagic is None: try: - # pylint: disable=F0401 - # F0401: Unable to import 'magic' (import-error) + # pylint: disable=import-error import magic mimemagic = magic.open(getattr(magic, "MAGIC_MIME_TYPE", 16)) mimemagic.load() @@ -513,7 +510,7 @@ def _set_bz_version(self, version): try: self.bz_ver_major, self.bz_ver_minor = [ int(i) for i in version.split(".")[0:2]] - except: + except Exception: log.debug("version doesn't match expected format X.Y.Z, " "assuming 5.0", exc_info=True) self.bz_ver_major = 5 @@ -1349,10 +1346,6 @@ def build_update(self, https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug """ - # pylint: disable=W0221 - # Argument number differs from overridden method - # Base defines it with *args, **kwargs, so we don't have to maintain - # the master argument list in 2 places ret = {} # These are only supported for rhbugzilla diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 09c77f3e..ac32810b 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -7,15 +7,16 @@ from logging import getLogger import sys +# pylint: disable=import-error if sys.version_info[0] >= 3: - # pylint: disable=import-error,no-name-in-module from configparser import SafeConfigParser - from urllib.parse import urlparse + from urllib.parse import urlparse # pylint: disable=no-name-in-module from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport else: from ConfigParser import SafeConfigParser - from urlparse import urlparse # pylint: disable=ungrouped-imports + from urlparse import urlparse from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport +# pylint: enable=import-error import requests @@ -94,7 +95,10 @@ def _ServerProxy__request(self, methodname, params): if 'Bugzilla_token' not in params[0]: params[0]['Bugzilla_token'] = self.token_cache.value - ret = super(_BugzillaServerProxy, self)._ServerProxy__request(methodname, params) + # pylint: disable=no-member + ret = super(_BugzillaServerProxy, + self)._ServerProxy__request(methodname, params) + # pylint: enable=no-member if isinstance(ret, dict) and 'token' in ret.keys(): self.token_cache.value = ret.get('token') @@ -106,8 +110,6 @@ class _RequestsTransport(Transport): def __init__(self, url, cookiejar=None, sslverify=True, sslcafile=None, debug=0): - # pylint: disable=W0231 - # pylint does not handle multiple import of Transport well if hasattr(Transport, "__init__"): Transport.__init__(self, use_datetime=False) @@ -175,9 +177,10 @@ def _request_helper(self, url, request_body): except Fault: raise except Exception: - # pylint: disable=W0201 e = BugzillaError(str(sys.exc_info()[1])) + # pylint: disable=attribute-defined-outside-init e.__traceback__ = sys.exc_info()[2] + # pylint: enable=attribute-defined-outside-init raise e def request(self, host, handler, request_body, verbose=0): diff --git a/examples/apikey.py b/examples/apikey.py index 95ed3412..a9bb33b4 100644 --- a/examples/apikey.py +++ b/examples/apikey.py @@ -21,10 +21,12 @@ " https://landfill.bugzilla.org/bugzilla-5.0-branch/userprefs.cgi") print("This is a test site, so no harm will come!\n") +# pylint: disable=undefined-variable if sys.version_info[0] >= 3: api_key = input("Enter Bugzilla API Key: ") else: api_key = raw_input("Enter Bugzilla API Key: ") +# pylint: enable=undefined-variable # API key usage assumes the API caller is storing the API key; if you would # like to use one of the login options that stores credentials on-disk for diff --git a/tests/__init__.py b/tests/__init__.py index d6f1573c..249954ee 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,10 +8,12 @@ import shlex import sys +# pylint: disable=import-error if sys.version_info[0] >= 3: from io import StringIO else: from StringIO import StringIO +# pylint: enable=import-error from bugzilla import Bugzilla, RHBugzilla diff --git a/tests/ro_functional.py b/tests/ro_functional.py index d5c11075..65389c61 100644 --- a/tests/ro_functional.py +++ b/tests/ro_functional.py @@ -11,7 +11,6 @@ Unit tests that do readonly functional tests against real bugzilla instances. ''' -import sys import unittest from bugzilla import Bugzilla, BugzillaError, RHBugzilla @@ -80,13 +79,14 @@ def _testQuery(self, args, mincount, expectbug): return self.assertTrue(len(out.splitlines()) >= mincount) - self.assertTrue(bool([l for l in out.splitlines() if - l.startswith("#" + expectbug)])) + self.assertTrue(bool([l1 for l1 in out.splitlines() if + l1.startswith("#" + expectbug)])) # Check --ids output option out2 = self.clicomm(cli + " --ids") self.assertTrue(len(out.splitlines()) == len(out2.splitlines())) - self.assertTrue(bool([l for l in out2.splitlines() if l == expectbug])) + self.assertTrue(bool([l2 for l2 in out2.splitlines() if + l2 == expectbug])) def _testQueryFull(self, bugid, mincount, expectstr): @@ -173,8 +173,7 @@ def testURLQuery(self): bz = Bugzilla(url=self.url, use_creds=False) try: bz.query(bz.url_to_query(query_url)) - except BugzillaError: - e = sys.exc_info()[1] + except BugzillaError as e: self.assertTrue("derived from bugzilla" in str(e)) @@ -316,8 +315,7 @@ def testBugAutoRefresh(self): self.assertFalse(hasattr(bug, "component")) try: self.assertFalse(bool(bug.component)) - except: - e = sys.exc_info()[1] + except Exception as e: self.assertTrue("adjust your include_fields" in str(e)) def testExtraFields(self): diff --git a/tests/rw_functional.py b/tests/rw_functional.py index 5939e7a7..a719eca5 100644 --- a/tests/rw_functional.py +++ b/tests/rw_functional.py @@ -17,11 +17,12 @@ import sys import unittest +# pylint: disable=import-error if sys.version_info[0] >= 3: - # pylint: disable=F0401,E0611 - from urllib.parse import urlparse + from urllib.parse import urlparse # pylint: disable=no-name-in-module else: from urlparse import urlparse +# pylint: enable=import-error import bugzilla from bugzilla import Bugzilla @@ -122,8 +123,7 @@ def test03NewBugBasic(self): self.assertEqual(bug.summary, summary) # Close the bug - tests.clicomm("bugzilla modify --close NOTABUG %s" % bugid, - bz) + tests.clicomm("bugzilla modify --close NOTABUG %s" % bugid, bz) bug.refresh() self.assertEqual(bug.status, "CLOSED") self.assertEqual(bug.resolution, "NOTABUG") @@ -810,9 +810,9 @@ def compare(data, newid): if have_admin: raise self.assertTrue( - ("Sorry, you aren't a member" in str(e)) or - # bugzilla 5 error string - ("You are not allowed" in str(e))) + ("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) # Edit component @@ -833,9 +833,9 @@ def compare(data, newid): if have_admin: raise self.assertTrue( - ("Sorry, you aren't a member" in str(e)) or - # bugzilla 5 error string - ("You are not allowed" in str(e))) + ("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) def test12SetCookie(self): bz = self.bzclass(self.url, From 6c780557c4eac2c6276b57604ac6b67ae00b9dbd Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 16:42:07 -0500 Subject: [PATCH 018/393] CONTRIBUTING.md: fix a command typo --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec9b93d6..d7d137b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ Before running rw-functional tests, make sure you have logged into bugzilla using. These currently run against the test bugzilla instance at partner-bugzilla.redhat.com, and requires a valid login there: - bugzilla-cli --bugzilla=partner-bugzilla.redhat.com --username=$USER login + ./bugzilla-cli --bugzilla=partner-bugzilla.redhat.com --username=$USER login python setup.py test --rw-functional ## Testing across python versions From 69bb2a26dc69db8a37928f51e810c410ab168367 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 16:46:02 -0500 Subject: [PATCH 019/393] setup: Enable test verbose if --ro/rw-functional is passed --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f2dd5b23..53569741 100755 --- a/setup.py +++ b/setup.py @@ -120,7 +120,10 @@ def run(self): print() - t = unittest.TextTestRunner(verbosity=1) + verbosity = 1 + if self.ro_functional or self.rw_functional: + verbosity = 2 + t = unittest.TextTestRunner(verbosity=verbosity) result = t.run(tests) From 05e0460e37da07e00ebd8b81f446ac649fb3cf56 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 16:58:38 -0500 Subject: [PATCH 020/393] Move bin/bugzilla to bugzilla/_cli.py Simplifies testing and let's us use entry_points, which are more standard --- bugzilla-cli | 8 +++++-- bin/bugzilla => bugzilla/_cli.py | 40 ++++++++++++++++++-------------- setup.py | 16 +++---------- tests/__init__.py | 22 ++---------------- 4 files changed, 33 insertions(+), 53 deletions(-) rename bin/bugzilla => bugzilla/_cli.py (99%) diff --git a/bugzilla-cli b/bugzilla-cli index 12e3ca12..8a5ddb76 100755 --- a/bugzilla-cli +++ b/bugzilla-cli @@ -1,3 +1,7 @@ -#!/bin/sh +#!/usr/bin/env python3 -PYTHONPATH=. exec bin/bugzilla "$@" +# This is a small wrapper script to simplify running the 'bugzilla' +# cli tool from a git checkout + +from bugzilla import _cli +_cli.main() diff --git a/bin/bugzilla b/bugzilla/_cli.py similarity index 99% rename from bin/bugzilla rename to bugzilla/_cli.py index d8cff075..ed5f0249 100755 --- a/bin/bugzilla +++ b/bugzilla/_cli.py @@ -45,11 +45,6 @@ format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}") log = getLogger(bugzilla.__name__) -handler = StreamHandler(sys.stderr) -handler.setFormatter(Formatter( - "[%(asctime)s] %(levelname)s (%(module)s:%(lineno)d) %(message)s", - "%H:%M:%S")) -log.addHandler(handler) ################ @@ -107,6 +102,24 @@ def get_default_url(): return DEFAULT_BZ +def setup_logging(debug, verbose): + handler = StreamHandler(sys.stderr) + handler.setFormatter(Formatter( + "[%(asctime)s] %(levelname)s (%(module)s:%(lineno)d) %(message)s", + "%H:%M:%S")) + log.addHandler(handler) + + if debug: + log.setLevel(DEBUG) + elif verbose: + log.setLevel(INFO) + else: + log.setLevel(WARN) + + if _is_unittest_debug: + log.setLevel(DEBUG) + + ################## # Option parsing # ################## @@ -1027,20 +1040,11 @@ def _handle_login(opt, parser, args, action, bz): sys.exit(0) -def main(unittest_bz_instance=None): +def _main(unittest_bz_instance): parser = setup_parser() opt, args = parser.parse_known_args() action = opt.command - - if opt.debug: - log.setLevel(DEBUG) - elif opt.verbose: - log.setLevel(INFO) - else: - log.setLevel(WARN) - - if _is_unittest_debug: - log.setLevel(DEBUG) + setup_logging(opt.debug, opt.verbose) log.debug("Launched with command line: %s", " ".join(sys.argv)) @@ -1114,9 +1118,9 @@ def main(unittest_bz_instance=None): _format_output(bz, opt, buglist) -if __name__ == '__main__': +def main(unittest_bz_instance=None): try: - main() + return _main(unittest_bz_instance) except KeyboardInterrupt: log.debug("", exc_info=True) print("\nExited at user request.") diff --git a/setup.py b/setup.py index 53569741..34fb403c 100755 --- a/setup.py +++ b/setup.py @@ -146,11 +146,11 @@ def initialize_options(self): def finalize_options(self): pass - def _run(self): + def run(self): import pylint.lint import pycodestyle - files = (["bugzilla/", "bin-bugzilla"] + + files = (["bugzilla-cli", "bugzilla"] + glob.glob("examples/*.py") + glob.glob("tests/*.py")) output_format = sys.stdout.isatty() and "colorized" or "text" @@ -174,16 +174,6 @@ def _run(self): ] pylint.lint.Run(files + pylint_opts) - def run(self): - os.link("bin/bugzilla", "bin-bugzilla") - try: - self._run() - finally: - try: - os.unlink("bin-bugzilla") - except: - pass - class RPMCommand(Command): description = "Build src and binary rpms." @@ -238,7 +228,7 @@ def _parse_requirements(fname): 'Programming Language :: Python :: 3.6', ], packages = ['bugzilla'], - scripts=['bin/bugzilla'], + entry_points={'console_scripts': ['bugzilla = bugzilla._cli:main']}, data_files=[('share/man/man1', ['bugzilla.1'])], install_requires=_parse_requirements("requirements.txt"), diff --git a/tests/__init__.py b/tests/__init__.py index 249954ee..12beb611 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,7 +3,6 @@ import atexit import difflib -import imp import os import shlex import sys @@ -15,26 +14,9 @@ from StringIO import StringIO # pylint: enable=import-error -from bugzilla import Bugzilla, RHBugzilla +from bugzilla import Bugzilla, RHBugzilla, _cli -_cleanup = [] - - -def _import(name, path): - _cleanup.append(path + "c") - return imp.load_source(name, path) - - -def _cleanup_cb(): - for f in _cleanup: - if os.path.exists(f): - os.unlink(f) - - -atexit.register(_cleanup_cb) -bugzillascript = _import("bugzillascript", "bin/bugzilla") - # This is overwritten by python setup.py test --redhat-url, and then # used in ro/rw tests REDHAT_URL = None @@ -102,7 +84,7 @@ def clicomm(argv, bzinstance, returnmain=False, printcliout=False, print(" ".join(argv)) print() - mainout = bugzillascript.main(unittest_bz_instance=bzinstance) + mainout = _cli.main(unittest_bz_instance=bzinstance) except SystemExit as sys_e: ret = sys_e.code From a1d7cfb9fce20926f0cec8122f7f346420cccc3b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 4 Mar 2018 19:21:05 -0500 Subject: [PATCH 021/393] cli: Convert to explicit positional args There might be some subtle semantic CLI differences here, but it covers more argparse validation cases and simplifies the code, so let's give it a spin --- bugzilla/_cli.py | 64 +++++++++++++++++++----------------------------- tests/misc.py | 10 ++++++++ 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index ed5f0249..0b73fb26 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -365,6 +365,7 @@ def _setup_action_modify_parser(subparsers): _parser_add_bz_fields(p, "modify") g = p.add_argument_group("'modify' specific options") + g.add_argument("ids", nargs="+", help="Bug IDs to modify") g.add_argument('-k', '--close', metavar="RESOLUTION", help='Close with the given resolution (WONTFIX, NOTABUG, etc.)') g.add_argument('-d', '--dupeid', metavar="ORIGINAL", @@ -385,6 +386,7 @@ def _setup_action_attach_parser(subparsers): description = "Attach files or download attachments." p = subparsers.add_parser("attach", description=description, usage=usage) + p.add_argument("ids", nargs="*", help="BUGID references") p.add_argument('-f', '--file', metavar="FILENAME", help='File to attach, or filename for data provided on stdin') p.add_argument('-d', '--description', '--summary', @@ -401,7 +403,11 @@ def _setup_action_attach_parser(subparsers): def _setup_action_login_parser(subparsers): usage = 'bugzilla login [username [password]]' description = "Log into bugzilla and save a login cookie or token." - subparsers.add_parser("login", description=description, usage=usage) + p = subparsers.add_parser("login", description=description, usage=usage) + p.add_argument("pos_username", nargs="?", help="Optional username", + metavar="username") + p.add_argument("pos_password", nargs="?", help="Optional password", + metavar="password") def setup_parser(): @@ -784,8 +790,8 @@ def parse_multi(val): return [b] -def _do_modify(bz, parser, opt, args): - bugid_list = [bugid for a in args for bugid in a.split(',')] +def _do_modify(bz, parser, opt): + bugid_list = [bugid for a in opt.ids for bugid in a.split(',')] add_wb, rm_wb, set_wb = _parse_triset(opt.whiteboard) add_devwb, rm_devwb, set_devwb = _parse_triset(opt.devel_whiteboard) @@ -917,11 +923,7 @@ def _do_modify(bz, parser, opt, args): bz.build_update(**{wb: " ".join(newval)})) -def _do_get_attach(bz, opt, parser, args): - if args: - parser.error("Extra args '%s' not used for getting attachments" % - args) - +def _do_get_attach(bz, opt): for bug in bz.getbugs(opt.getall): opt.get += bug.get_attachment_ids() @@ -937,8 +939,8 @@ def _do_get_attach(bz, opt, parser, args): return -def _do_set_attach(bz, opt, parser, args): - if not args: +def _do_set_attach(bz, opt, parser): + if not opt.ids: parser.error("Bug ID must be specified for setting attachments") if sys.stdin.isatty(): @@ -969,7 +971,7 @@ def _do_set_attach(bz, opt, parser, args): desc = opt.desc or os.path.basename(fileobj.name) # Upload attachments - for bugid in args: + for bugid in opt.ids: attid = bz.attachfile(bugid, fileobj, desc, **kwargs) print("Created attachment %i on bug %s" % (attid, bugid)) @@ -999,7 +1001,7 @@ def _make_bz_instance(opt): return bz -def _handle_login(opt, parser, args, action, bz): +def _handle_login(opt, action, bz): """ Handle all login related bits """ @@ -1007,21 +1009,14 @@ def _handle_login(opt, parser, args, action, bz): do_interactive_login = (is_login_command or opt.login or opt.username or opt.password) - - if is_login_command: - if len(args) == 2: - (opt.username, opt.password) = args - elif len(args) == 1: - (opt.username, ) = args - elif len(args) > 2: - parser.error("Too many arguments for login") + username = getattr(opt, "pos_username", None) or opt.username + password = getattr(opt, "pos_password", None) or opt.password try: if do_interactive_login: if bz.url: print("Logging into %s" % urlparse(bz.url)[1]) - bz.interactive_login( - opt.username, opt.password) + bz.interactive_login(username, password) except bugzilla.BugzillaError as e: print(str(e)) sys.exit(1) @@ -1042,7 +1037,7 @@ def _handle_login(opt, parser, args, action, bz): def _main(unittest_bz_instance): parser = setup_parser() - opt, args = parser.parse_known_args() + opt = parser.parse_args() action = opt.command setup_logging(opt.debug, opt.verbose) @@ -1057,7 +1052,7 @@ def _main(unittest_bz_instance): bz = _make_bz_instance(opt) # Handle login options - _handle_login(opt, parser, args, action, bz) + _handle_login(opt, action, bz) ########################### @@ -1070,9 +1065,6 @@ def _main(unittest_bz_instance): buglist = [] if action == 'info': - if args: - parser.error("Extra arguments '%s'" % args) - if not (opt.products or opt.components or opt.component_owners or @@ -1082,32 +1074,26 @@ def _main(unittest_bz_instance): _do_info(bz, opt) elif action == 'query': - if args: - parser.error("Extra arguments '%s'" % args) - buglist = _do_query(bz, opt, parser) if opt.test_return_result: return buglist elif action == 'new': - if args: - parser.error("Extra arguments '%s'" % args) buglist = _do_new(bz, opt, parser) if opt.test_return_result: return buglist elif action == 'attach': if opt.get or opt.getall: - _do_get_attach(bz, opt, parser, args) + if opt.ids: + parser.error("Bug IDs '%s' not used for " + "getting attachments" % opt.ids) + _do_get_attach(bz, opt) else: - _do_set_attach(bz, opt, parser, args) + _do_set_attach(bz, opt, parser) elif action == 'modify': - if not args: - parser.error('No bug IDs given ' - '(maybe you forgot an argument somewhere?)') - - modout = _do_modify(bz, parser, opt, args) + modout = _do_modify(bz, parser, opt) if opt.test_return_result: return modout else: diff --git a/tests/misc.py b/tests/misc.py index 78ee5ab7..9c56f5ff 100644 --- a/tests/misc.py +++ b/tests/misc.py @@ -38,6 +38,16 @@ def testVersion(self): out = tests.clicomm("bugzilla --version", None) self.assertTrue(len(out.splitlines()) >= 2) + def testPositionalArgs(self): + # Make sure cli correctly rejects ambiguous positional args + out = tests.clicomm("bugzilla login --xbadarg foo", + None, expectfail=True) + self.assertTrue("unrecognized arguments: --xbadarg" in out) + + out = tests.clicomm("bugzilla modify 123456 --foobar --status NEW", + None, expectfail=True) + self.assertTrue("unrecognized arguments: --foobar" in out) + class MiscAPI(unittest.TestCase): """ From 641450955354c194e246eae8c2d9fd74f3cac7a4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 09:28:34 -0500 Subject: [PATCH 022/393] test-requirements: pycodestyle, pylint aren't required Causes travis failures and they aren't required for running the test suite, so just drop it: https://travis-ci.org/python-bugzilla/python-bugzilla/jobs/349094685 --- test-requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 1851b0e9..df484477 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,2 @@ # additional packages needed for testing coverage -pycodestyle -pylint From d89587f9ffaefc2dafab35296aef953c35e3e525 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 12:23:44 -0500 Subject: [PATCH 023/393] tests: Ignore travis SSL error This one test case seems to cause errors on travis ubuntu environment, so try to skip it. https://travis-ci.org/python-bugzilla/python-bugzilla/builds/304713566 --- tests/ro_functional.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/ro_functional.py b/tests/ro_functional.py index 65389c61..6047bd1a 100644 --- a/tests/ro_functional.py +++ b/tests/ro_functional.py @@ -129,10 +129,17 @@ class BZMozilla(BaseTest): def testVersion(self): # bugzilla.mozilla.org returns version values in YYYY-MM-DD # format, so just try to confirm that - bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) - self.assertEqual(bz.__class__, Bugzilla) - self.assertTrue(bz.bz_ver_major >= 2016) - self.assertTrue(bz.bz_ver_minor in range(1, 13)) + try: + bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) + self.assertEqual(bz.__class__, Bugzilla) + self.assertTrue(bz.bz_ver_major >= 2016) + self.assertTrue(bz.bz_ver_minor in range(1, 13)) + except Exception as e: + # travis environment throws SSL errors here + # https://travis-ci.org/python-bugzilla/python-bugzilla/builds/304713566 + if "EOF occurred" not in str(e): + raise + self.skipTest("travis environment SSL error hit: %s" % str(e)) class BZGentoo(BaseTest): From 4ee47270d2e66fba717237c0785a1c7b9a0ab7e9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 14:24:05 -0500 Subject: [PATCH 024/393] travis: Updates - Drop travis deploy step; I manually do this and don't think it adds much for python-bugzilla - Ignore pip errors to make sonar step happy - Link to the sonar dashboard --- .travis.yml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 01f2e480..5cdeb751 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,16 @@ python: - pypy-5.3.1 install: - - pip install tox - - pip install tox-travis + - pip install tox || true + - pip install tox-travis || true script: - tox -- --ro-functional jobs: include: + + # https://sonarcloud.io/dashboard?id=com.github%3Apython-bugzilla%3Apython-bugzilla - stage: sonar dist: trusty language: java @@ -32,17 +34,6 @@ jobs: branches: only: - master - - stage: deploy - python: 3.6 - script: ignore - deploy: - provider: pypi - user: python-bugzilla-admin - password: - secure: "hQKQ/EAD/zPndc+wL/wLiJXsP9lqXfSlousP5YuemaUenQNVrR5wXRDGr+Jm+S2rdaqVMq+1/lVcW3Mnd5MgqzFc8pL7Mg+6OpNZ8PAP3XLFZulc/Fyt0q98KVBQCQgHLXaQfMpitaC6/XYrOE22DfTyL2UbidWWcPMz+fLsM3jKcItv8jVLB8LlFVnDNpUt/HlTkatc9maNG+zR/aaWVsVPD5QEl+Ss01gnCj1yAoKSKxKF2A1PYh23Oue/yYb+n2dnaWqRGB3gUatFM+7cadEf7qfaRVwyghtso+CP4t9inldNugPuqBv3Ji7wZWA2+vLV18cWonPd7H7LJofRQmhDHDU+deGRtR0WYyZ2qjGi1b7ufP0ofU0NeqHlT3gTivzXXWviAjiYyYwN8Fa9AVsgietaLrxLo/HFYdr7fA6QS106Era1IlwQDgc32fOR+gpW03LuSFVEFEsfm2Vj8hvgxQZSLeL/Xb3YZDvAGd1ieLAvwotZgUd+QLlw1KMiZ141Kp63hz03Us6rftAhcOEYb/w2DnM/WsEwNNjuJSJZdKRyeV/rCMUH8MPx3bdooXPYNZcPOMEnbKYep4lvmDfhG8dO6A5N8IHXc4Yhu2TpRTafLd9DyvIZOBJuJdDN8Y0Z5NwBrjXlqLTCmrWpuCpFwJyat7zd+pWYFzb7gp0=" - distributions: "sdist bdist_wheel" - on: - tags: true notifications: email: true From a2c69281360e16e74777b8b33c531124e0d87057 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 15:08:18 -0500 Subject: [PATCH 025/393] cli: Tweak --tags documentation And actually add it to the manpage Resolves: #61 --- bugzilla.1 | 6 ++++-- bugzilla/_cli.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bugzilla.1 b/bugzilla.1 index a8ff5f36..714e527d 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -101,6 +101,10 @@ CC list Bug assignee .IP "--qa_contact=QA_CONTACT, -q QA_CONTACT" QA contact +.IP "--flag=FLAG" +Set or unset a flag. For example, to set a flag named devel_ack, do --flag devel_ack+ Unset a flag with the 'X' value, like --flag needinfoX +.IP "--tags=TAG" +Set (personal) tags field .IP "--whiteboard WHITEBOARD, -w WHITEBOARD, --status_whiteboard WHITEBOARD" Whiteboard field .IP "--devel_whiteboard DEVEL_WHITEBOARD" @@ -111,8 +115,6 @@ RHBZ internal whiteboard field RHBZ QA whiteboard field .IP "--fixed_in FIXED_IN, -F FIXED_IN RHBZ 'Fixed in version' field -.IP "--flag=FLAG" -Set or unset a flag. For example, to set a flag named devel_ack, do --flag devel_ack+ Unset a flag with the 'X' value, like --flag needinfoX .IP "--field=FIELD=VALUE" Manually specify a bugzilla XMLRPC field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 0b73fb26..6ab8e6ff 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -246,7 +246,8 @@ def _parser_add_bz_fields(rootp, command): " --flag needinfo?\n" " --flag dev_ack+ \n" " clear with --flag needinfoX") - p.add_argument("--tags", action="append", help="Tags field.") + p.add_argument("--tags", action="append", + help="Tags/Personal Tags field.") p.add_argument('-w', "--whiteboard", '--status_whiteboard', action="append", help='Whiteboard field') From dc26d84f43939316ab28d2af86784c4f1b009cb3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 15:12:34 -0500 Subject: [PATCH 026/393] Change version to 2.2.0.dev0 to shut up distutils --- bugzilla/apiversion.py | 2 +- sonar-project.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 9c5cf823..4e6e2c18 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -7,5 +7,5 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -version = "2.2.0-dev" +version = "2.2.0.dev0" __version__ = version diff --git a/sonar-project.properties b/sonar-project.properties index 4fc2efb3..90b09bf2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ # Required metadata sonar.projectKey=com.github:python-bugzilla:python-bugzilla sonar.projectName=Python Bugzilla -sonar.projectVersion=2.2.0-dev +sonar.projectVersion=2.2.0.dev0 # Comma-separated paths to directories with sources (required) sonar.sources=bugzilla From f459466a83ac7ae5eec175ac4fde5259a848eafa Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 15:23:52 -0500 Subject: [PATCH 027/393] xmlrpc-api-notes: Fix link to 5.0 docs --- xmlrpc-api-notes.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlrpc-api-notes.txt b/xmlrpc-api-notes.txt index 4b4759fe..593cdd91 100644 --- a/xmlrpc-api-notes.txt +++ b/xmlrpc-api-notes.txt @@ -97,7 +97,7 @@ Bugzilla 4.4: Util.params_to_objects Bugzilla 5.0: (July 2015) - https://www.bugzilla.org/docs/5.0/en/html/integrating/api/index.html + https://bugzilla.readthedocs.io/en/5.0/api/index.html Bug.update_attachment Bug.search/update_comment_tags Bug.search: From f535954bc53b9993be732d8cf297768db82fa052 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 15:57:34 -0500 Subject: [PATCH 028/393] cli: Add --comment-tag option This seems to be a bugzilla5 extension, although redhat's instance doesn't seem to support it yet Resolves: #62 --- bugzilla.1 | 2 ++ bugzilla/_cli.py | 5 +++++ bugzilla/base.py | 9 ++++++--- tests/createbug.py | 6 ++++-- tests/modify.py | 2 ++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/bugzilla.1 b/bugzilla.1 index 714e527d..239ef953 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -67,6 +67,8 @@ Component name Bug summary .IP "--comment=DESCRIPTION, -l DESCRIPTION" Set initial bug comment/description +.IP "--comment-tag=TAG" +Comment tag for the new comment .IP "--sub-component=SUB_COMPONENT" RHBZ sub component name .IP "--os=OS, -o OS" diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 6ab8e6ff..b6630b46 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -214,6 +214,9 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('-c', '--component', help="Component name") p.add_argument('-t', '--summary', '--short_desc', help="Bug summary") p.add_argument('-l', '--comment', '--long_desc', help=comment_help) + if not cmd_query: + p.add_argument("--comment-tag", action="append", + help="Comment tag for the new comment") p.add_argument("--sub-component", action="append", help="RHBZ sub component field") p.add_argument('-o', '--os', help="Operating system") @@ -779,6 +782,7 @@ def parse_multi(val): qa_contact=opt.qa_contact or None, sub_component=opt.sub_component or None, alias=opt.alias or None, + comment_tags=opt.comment_tag or None, ) _merge_field_opts(ret, opt, parser) @@ -864,6 +868,7 @@ def _do_modify(bz, parser, opt): sub_component=opt.sub_component or None, alias=opt.alias or None, flags=flags or None, + comment_tags=opt.comment_tag or None, ) # We make this a little convoluted to facilitate unit testing diff --git a/bugzilla/base.py b/bugzilla/base.py index 31e57f20..a190195d 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1338,7 +1338,8 @@ def build_update(self, devel_whiteboard=None, internal_whiteboard=None, sub_component=None, - flags=None): + flags=None, + comment_tags=None): """ Returns a python dict() with properly formatted parameters to pass to update_bugs(). See bugzilla documentation for the format @@ -1414,6 +1415,7 @@ def c(val): s("whiteboard", whiteboard) s("work_time", work_time, float) s("flags", flags) + s("comment_tags", comment_tags, self._listify) add_dict("blocks", blocks_add, blocks_remove, blocks_set, convert=int) @@ -1601,7 +1603,8 @@ def build_createbug(self, target_release=None, url=None, sub_component=None, - alias=None): + alias=None, + comment_tags=None): """" Returns a python dict() with properly formatted parameters to pass to createbug(). See bugzilla documentation for the format @@ -1635,7 +1638,7 @@ def build_createbug(self, target_milestone=target_milestone, target_release=target_release, url=url, assigned_to=assigned_to, sub_component=sub_component, - alias=alias) + alias=alias, comment_tags=comment_tags) ret.update(localdict) return ret diff --git a/tests/createbug.py b/tests/createbug.py index f9c7b1bc..e94af17a 100644 --- a/tests/createbug.py +++ b/tests/createbug.py @@ -59,10 +59,12 @@ def testSeverity(self): ) def testMisc(self): - self.clicomm( - "--alias some-alias", + self.clicomm("--alias some-alias", {"alias": "some-alias"} ) + self.clicomm("--comment 'foo bar' --comment-tag tag1 " + "--comment-tag tag2", + {'comment_tags': ['tag1', 'tag2'], 'description': 'foo bar'}) def testMultiOpts(self): # Test all opts that can take lists diff --git a/tests/modify.py b/tests/modify.py index 99527e14..49f0723d 100644 --- a/tests/modify.py +++ b/tests/modify.py @@ -139,6 +139,8 @@ def testMisc(self): "--alias some-alias", {"alias": "some-alias"} ) + self.clicomm("--comment 'foo bar' --comment-tag tag1 ", + {'comment': {'comment': 'foo bar'}, 'comment_tags': ['tag1']}) def testField(self): From 238f055af371dc08b1705ddd56f6a0652fb27652 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 18:27:20 -0500 Subject: [PATCH 029/393] cli: convert outputformat %{external_bugs} to full URLs Resolves: #65 --- bugzilla/_cli.py | 8 ++++++++ tests/ro_functional.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index b6630b46..c2ef8ff1 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -713,6 +713,14 @@ def bug_field(matchobj): val += ("\n* %s - %s:\n%s\n" % (c['time'], c.get("creator", c.get("author", "")), c['text'])) + elif fieldname == "external_bugs": + val = "" + for e in getattr(b, "external_bugs", []): + url = e["type"]["full_url"].replace("%id%", e["ext_bz_bug_id"]) + if not val: + val += "\n" + val += "External bug: %s\n" % url + elif fieldname == "__unicode__": val = b.__unicode__() else: diff --git a/tests/ro_functional.py b/tests/ro_functional.py index 6047bd1a..07fcef19 100644 --- a/tests/ro_functional.py +++ b/tests/ro_functional.py @@ -337,3 +337,10 @@ def testExtraFields(self): bug = bz.getbug(720773, include_fields=["summary"]) self.assertTrue("summary" in dir(bug)) self.assertTrue("comments" not in dir(bug)) + + def testExternalBugsOutput(self): + out = self.clicomm('query --bug_id 989253 ' + '--outputformat="%{external_bugs}"') + expect = ("http://bugzilla.gnome.org/show_bug.cgi?id=703421\n" + + "External bug: https://bugs.launchpad.net/bugs/1203576") + self.assertTrue(expect in out) From 42a2ea63754c732fc6000e8e976a6a38e63dd39d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Mar 2018 19:29:48 -0500 Subject: [PATCH 030/393] base: Reorg and document product/component functions * Make a raw Product.get wrapper available * Document the caching semantics * Make cache updating more intelligent * Make implicit product fetching more intelligent --- bugzilla/_cli.py | 4 +- bugzilla/base.py | 319 +++++++++++++++++++++++++++-------------------- 2 files changed, 186 insertions(+), 137 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index c2ef8ff1..7f149da2 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -606,8 +606,8 @@ def _do_info(bz, opt): component_details = bz.getcomponentsdetails(opt.component_owners) for c in sorted(component_details): - print(to_encoding(u"%s: %s" % - (c, component_details[c]['initialowner']))) + print(to_encoding(u"%s: %s" % (c, + component_details[c]['default_assigned_to']))) if opt.versions: for p in products: diff --git a/bugzilla/base.py b/bugzilla/base.py index a190195d..86dd7ff0 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -9,6 +9,7 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. +import collections import getpass import locale from logging import getLogger @@ -67,6 +68,17 @@ def _detect_filetype(fname): return None +def _nested_update(d, u): + # Helper for nested dict update() + # https://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth + for k, v in list(u.items()): + if isinstance(v, collections.Mapping): + d[k] = _nested_update(d.get(k, {}), v) + else: + d[k] = v + return d + + def _default_auth_location(filename): """ Determine auth location for filename, like 'bugzillacookies'. If @@ -157,9 +169,8 @@ class _BugzillaAPICache(object): """ def __init__(self): self.products = [] + self.component_names = {} self.bugfields = [] - self.components = {} - self.components_details = {} class Bugzilla(object): @@ -389,20 +400,6 @@ def _check_version(self, major, minor): return True return False - def _product_id_to_name(self, productid): - '''Convert a product ID (int) to a product name (str).''' - for p in self.products: - if p['id'] == productid: - return p['name'] - raise ValueError('No product with id #%i' % productid) - - def _product_name_to_id(self, product): - '''Convert a product name (str) to a product ID (int).''' - for p in self.products: - if p['name'] == product: - return p['id'] - raise ValueError('No product named "%s"' % product) - def _add_field_alias(self, *args, **kwargs): self._field_aliases.append(_FieldAlias(*args, **kwargs)) @@ -667,9 +664,9 @@ def logged_in(self): raise e - ############################################# - # Fetching info about the bugzilla instance # - ############################################# + ###################### + # Bugfields querying # + ###################### def _getbugfields(self): ''' @@ -695,73 +692,202 @@ def getbugfields(self, force_refresh=False): fdel=lambda self: setattr(self, '_bugfields', None)) + #################### + # Product querying # + #################### + + def product_get(self, ids=None, names=None, + include_fields=None, exclude_fields=None, + ptype=None): + """ + Raw wrapper around Product.get + https://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product + + This does not perform any caching like other product API calls. + If ids, names, or ptype is not specified, we default to + ptype=accessible for historical reasons + + @ids: List of product IDs to lookup + @names: List of product names to lookup + @ptype: Either 'accessible', 'selectable', or 'enterable'. If + specified, we return data for all those + @include_fields: Only include these fields in the output + @exclude_fields: Do not include these fields in the output + """ + if ids is None and names is None and ptype is None: + ptype = "accessible" + + if ptype: + raw = None + if ptype == "accessible": + raw = self._proxy.Product.get_accessible_products() + elif ptype == "selectable": + raw = self._proxy.Product.get_selectable_products() + elif ptype == "enterable": + raw = self._proxy.Product.get_enterable_products() + + if raw is None: + raise RuntimeError("Unknown ptype=%s" % ptype) + ids = raw['ids'] + log.debug("For ptype=%s found ids=%s", ptype, ids) + + kwargs = {} + if ids: + kwargs["ids"] = self._listify(ids) + if names: + kwargs["names"] = self._listify(names) + if include_fields: + kwargs["include_fields"] = include_fields + if exclude_fields: + kwargs["exclude_fields"] = exclude_fields + + log.debug("Calling Product.get with: %s", kwargs) + ret = self._proxy.Product.get(kwargs) + return ret['products'] + def refresh_products(self, **kwargs): """ - Refresh a product's cached info - Takes same arguments as _getproductinfo + Refresh a product's cached info. Basically calls product_get + with the passed arguments, and tries to intelligently update + our product cache. + + For example, if we already have cached info for product=foo, + and you pass in names=["bar", "baz"], the new cache will have + info for products foo, bar, baz. Individual product fields are + also updated. """ - for product in self._getproductinfo(**kwargs): - added = False + for product in self.product_get(**kwargs): + updated = False for current in self._cache.products[:]: if (current.get("id", -1) != product.get("id", -2) and current.get("name", -1) != product.get("name", -2)): continue - self._cache.products.remove(current) - self._cache.products.append(product) - added = True + _nested_update(current, product) + updated = True break - if not added: + if not updated: self._cache.products.append(product) def getproducts(self, force_refresh=False, **kwargs): - '''Get product data: names, descriptions, etc. - The data varies between Bugzilla versions but the basic format is a - list of dicts, where the dicts will have at least the following keys: - {'id':1, 'name':"Some Product", 'description':"This is a product"} + """ + Query all products and return the raw dict info. Takes all the + same arguments as product_get. - Any method that requires a 'product' can be given either the - id or the name.''' + On first invocation this will contact bugzilla and internally + cache the results. Subsequent getproducts calls or accesses to + self.products will return this cached data only. + + :param force_refresh: force refreshing via refresh_products() + """ if force_refresh or not self._cache.products: - self._cache.products = self._getproducts(**kwargs) + self.refresh_products(**kwargs) return self._cache.products - products = property(fget=lambda self: self.getproducts(), - fdel=lambda self: setattr(self, '_products', None)) + products = property( + fget=lambda self: self.getproducts(), + fdel=lambda self: setattr(self, '_products', None), + doc="Helper for accessing the products cache. If nothing " + "has been cached yet, this calls getproducts()") + + ####################### + # components querying # + ####################### + + def _lookup_product_in_cache(self, productname): + prodstr = isinstance(productname, str) and productname or None + prodint = isinstance(productname, int) and productname or None + for proddict in self._cache.products: + if prodstr == proddict.get("name", -1): + return proddict + if prodint == proddict.get("id", "nope"): + return proddict + return {} def getcomponentsdetails(self, product, force_refresh=False): - '''Returns a dict of dicts, containing detailed component information - for the given product. The keys of the dict are component names. For - each component, the value is a dict with the following keys: - description, initialowner, initialqacontact''' - if force_refresh or product not in self._cache.components_details: - clist = self._getcomponentsdetails(product) - cdict = {} - for item in clist: - name = item['component'] - del item['component'] - cdict[name] = item - self._cache.components_details[product] = cdict - - return self._cache.components_details[product] + """ + Wrapper around Product.get(include_fields=["components"]), + returning only the "components" data for the requested product, + slightly reworked to a dict mapping of components.name: components, + for historical reasons. + + This uses the product cache, but will update it if the product + isn't found or "components" isn't cached for the product. + + In cases like bugzilla.redhat.com where there are tons of + components for some products, this API will time out. You + should use product_get instead. + """ + proddict = self._lookup_product_in_cache(product) + + if (force_refresh or not proddict or "components" not in proddict): + self.refresh_products(names=[product], + include_fields=["name", "id", "components"]) + proddict = self._lookup_product_in_cache(product) + + ret = {} + for compdict in proddict["components"]: + ret[compdict["name"]] = compdict + return ret def getcomponentdetails(self, product, component, force_refresh=False): - '''Get details for a single component. See bugzilla documentation - for a list of returned keys.''' + """ + Helper for accessing a single component's info. This is a wrapper + around getcomponentsdetails, see that for explanation + """ d = self.getcomponentsdetails(product, force_refresh) return d[component] def getcomponents(self, product, force_refresh=False): - '''Return a dict of components:descriptions for the given product.''' - if force_refresh or product not in self._cache.components: - self._cache.components[product] = self._getcomponents(product) - return self._cache.components[product] + """ + Return a list of component names for the passed product. + + This can be implemented with Product.get, but behind the + scenes it uses Bug.legal_values. Reason being that on bugzilla + instances with tons of components, like bugzilla.redhat.com + Product=Fedora for example, there's a 10x speed difference + even with properly limited Product.get calls. + + On first invocation the value is cached, and subsequent calls + will return the cached data. + + :param force_refresh: Force refreshing the cache, and return + the new data + """ + proddict = self._lookup_product_in_cache(product) + product_id = proddict.get("id", None) + + if (force_refresh or + product_id is None or + product_id not in self._cache.component_names): + self.refresh_products(names=[product], + include_fields=["names", "id"]) + proddict = self._lookup_product_in_cache(product) + product_id = proddict["id"] + + opts = {'product_id': product_id, 'field': 'component'} + log.debug("Calling Bug.legal_values with: %s", opts) + names = self._proxy.Bug.legal_values(opts)["values"] + self._cache.component_names[product_id] = names + + return self._cache.component_names[product_id] + + + ############################ + # component adding/editing # + ############################ def _component_data_convert(self, data, update=False): - if isinstance(data['product'], int): - data['product'] = self._product_id_to_name(data['product']) + def product_id_to_name(productid): + '''Convert a product ID (int) to a product name (str).''' + for p in self.products: + if p['id'] == productid: + return p['name'] + raise ValueError('No product with id #%i' % productid) + if isinstance(data['product'], int): + data['product'] = product_id_to_name(data['product']) # Back compat for the old RH interface convert_fields = [ @@ -819,83 +945,6 @@ def editcomponent(self, data): return self._proxy.Component.update(data) - def _getproductinfo(self, ids=None, names=None, - include_fields=None, exclude_fields=None): - ''' - Get all info for the requested products. - - @ids: List of product IDs to lookup - @names: List of product names to lookup (since bz 4.2, - though we emulate it for older versions) - @include_fields: Only include these fields in the output (since bz 4.2) - @exclude_fields: Do not include these fields in the output (since - bz 4.2) - ''' - if ids is None and names is None: - raise RuntimeError("Products must be specified") - - kwargs = {} - if not self._check_version(4, 2): - if names: - ids = [self._product_name_to_id(name) for name in names] - names = None - include_fields = None - exclude_fields = None - - if ids: - kwargs["ids"] = self._listify(ids) - if names: - kwargs["names"] = self._listify(names) - if include_fields: - kwargs["include_fields"] = include_fields - if exclude_fields: - kwargs["exclude_fields"] = exclude_fields - - log.debug("Calling Product.get with: %s", kwargs) - ret = self._proxy.Product.get(kwargs) - return ret['products'] - - def _getproducts(self, **kwargs): - product_ids = self._proxy.Product.get_accessible_products() - r = self._getproductinfo(product_ids['ids'], **kwargs) - return r - - def _getcomponents(self, product): - if isinstance(product, str): - product = self._product_name_to_id(product) - r = self._proxy.Bug.legal_values({'product_id': product, - 'field': 'component'}) - return r['values'] - - def _getcomponentsdetails(self, product): - def _find_comps(): - for p in self._cache.products: - if p["name"] != product: - continue - return p.get("components", None) - - comps = _find_comps() - if comps is None: - self.refresh_products(names=[product], - include_fields=["name", "id", "components"]) - comps = _find_comps() - - if comps is None: - raise ValueError("Unknown product '%s'" % product) - - # Convert to old style dictionary to maintain back compat - # with original RH bugzilla call - ret = [] - for comp in comps: - row = {} - row["component"] = comp["name"] - row["initialqacontact"] = comp["default_qa_contact"] - row["initialowner"] = comp["default_assigned_to"] - row["description"] = comp["description"] - ret.append(row) - return ret - - ################### # getbug* methods # ################### From fc0c66bb9ab97c6133579d4126e1f4d06b3e2501 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Mar 2018 11:30:53 -0500 Subject: [PATCH 031/393] Add pypy3 to tox Because why not --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f53f4c1d..4f764449 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,py35,py36,pypy +envlist = py27,py33,py34,py35,py36,pypy,pypy3 [testenv] sitepackages = True From cd5a9f0489ff8e8c274349cd36355d4f5b1234e6 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Mar 2018 11:48:19 -0500 Subject: [PATCH 032/393] base: Drop ID mapping from editcomponents API The only user of this is pkgdb, and their syncscript hasn't passed ID values for a while, so let's drop it and see if anyone complains. --- bugzilla/base.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 86dd7ff0..02ecf3a0 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -879,16 +879,6 @@ def getcomponents(self, product, force_refresh=False): ############################ def _component_data_convert(self, data, update=False): - def product_id_to_name(productid): - '''Convert a product ID (int) to a product name (str).''' - for p in self.products: - if p['id'] == productid: - return p['name'] - raise ValueError('No product with id #%i' % productid) - - if isinstance(data['product'], int): - data['product'] = product_id_to_name(data['product']) - # Back compat for the old RH interface convert_fields = [ ("initialowner", "default_assignee"), From 447202e3063eb654e9756095bd7aa74a75a9e9d8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Mar 2018 14:33:15 -0500 Subject: [PATCH 033/393] cli: Make 'info' options mutually exclusive Doing otherwise doesn't really make much sense --- bugzilla.1 | 2 +- bugzilla/_cli.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bugzilla.1 b/bugzilla.1 index 239ef953..26e7908f 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -213,7 +213,7 @@ Get a list of products List the components in the given product .IP "--component_owners=PRODUCT, -o PRODUCT" List components (and their owners) -.IP "--versions=VERSION, -v VERSION" +.IP "--versions=PRODUCT, -v PRODUCT" List the versions for the given product diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 7f149da2..4e455b28 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -346,13 +346,14 @@ def _setup_action_info_parser(subparsers): "bugzilla server.") p = subparsers.add_parser("info", description=description) - p.add_argument('-p', '--products', action='store_true', + x = p.add_mutually_exclusive_group() + x.add_argument('-p', '--products', action='store_true', help='Get a list of products') - p.add_argument('-c', '--components', metavar="PRODUCT", + x.add_argument('-c', '--components', metavar="PRODUCT", help='List the components in the given product') - p.add_argument('-o', '--component_owners', metavar="PRODUCT", + x.add_argument('-o', '--component_owners', metavar="PRODUCT", help='List components (and their owners)') - p.add_argument('-v', '--versions', metavar="VERSION", + x.add_argument('-v', '--versions', metavar="PRODUCT", help='List the versions for the given product') From 79b0ec662abeec9a37faa66484db7a3dcfa546c4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Mar 2018 14:43:33 -0500 Subject: [PATCH 034/393] cli: Make info a bit more efficient Figure out the products data we use upfront so we only need to make one Product.get call. Also make it more clear in the code that things are mutually exclusive. --- bugzilla/_cli.py | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 4e455b28..3043f5bb 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -581,44 +581,38 @@ def _do_info(bz, opt): """ # All these commands call getproducts internally, so do it up front # with minimal include_fields for speed + productname = (opt.components or opt.component_owners or opt.versions) include_fields = ["name", "id"] if opt.versions: include_fields.append("versions") - products = bz.getproducts(include_fields=include_fields) + if opt.component_owners: + include_fields += [ + "components.default_assigned_to", + "components.name", + ] + + bz.refresh_products(names=productname and [productname] or None, + include_fields=include_fields) if opt.products: - for name in sorted([p["name"] for p in products]): + for name in sorted([p["name"] for p in bz.getproducts()]): print(name) - if opt.components: - for name in sorted(bz.getcomponents(opt.components)): + elif opt.components: + for name in sorted(bz.getcomponents(productname)): print(name) - if opt.component_owners: - # Looking up this info for rhbz 'Fedora' product is sloooow - # since there are so many components. So delay getting this - # info until as late as possible - bz.refresh_products(names=[opt.component_owners], - include_fields=include_fields + [ - "components.default_assigned_to", - "components.default_qa_contact", - "components.name", - "components.description"]) - - component_details = bz.getcomponentsdetails(opt.component_owners) + elif opt.versions: + proddict = bz.getproducts()[0] + for v in proddict['versions']: + print(to_encoding(v["name"])) + + elif opt.component_owners: + component_details = bz.getcomponentsdetails(productname) for c in sorted(component_details): print(to_encoding(u"%s: %s" % (c, component_details[c]['default_assigned_to']))) - if opt.versions: - for p in products: - if p['name'] != opt.versions: - continue - if "versions" in p: - for v in p['versions']: - print(to_encoding(v["name"])) - break - def _convert_to_outputformat(output): fmt = "" From 1bf1e93ab198f4860d97e41c6e1e2f497c80a083 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Mar 2018 15:10:34 -0500 Subject: [PATCH 035/393] cli: Add info --active-components Resolves: #63 --- bugzilla.1 | 2 ++ bugzilla/_cli.py | 31 +++++++++++++++++++++++++------ tests/ro_functional.py | 10 +++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/bugzilla.1 b/bugzilla.1 index 26e7908f..d601e98a 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -215,6 +215,8 @@ List the components in the given product List components (and their owners) .IP "--versions=PRODUCT, -v PRODUCT" List the versions for the given product +.IP "--active-components" +Only show active components. Combine with --components* .SH AUTHENTICATION COOKIES AND TOKENS diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 3043f5bb..a823093b 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -346,7 +346,7 @@ def _setup_action_info_parser(subparsers): "bugzilla server.") p = subparsers.add_parser("info", description=description) - x = p.add_mutually_exclusive_group() + x = p.add_mutually_exclusive_group(required=True) x.add_argument('-p', '--products', action='store_true', help='Get a list of products') x.add_argument('-c', '--components', metavar="PRODUCT", @@ -355,6 +355,9 @@ def _setup_action_info_parser(subparsers): help='List components (and their owners)') x.add_argument('-v', '--versions', metavar="PRODUCT", help='List the versions for the given product') + p.add_argument('--active-components', action="store_true", + help='Only show active components. Combine with --components*') + def _setup_action_modify_parser(subparsers): @@ -581,15 +584,26 @@ def _do_info(bz, opt): """ # All these commands call getproducts internally, so do it up front # with minimal include_fields for speed + def _filter_components(compdetails): + ret = {} + for k, v in compdetails.items(): + if v.get("is_active", True): + ret[k] = v + return ret + productname = (opt.components or opt.component_owners or opt.versions) include_fields = ["name", "id"] + fastcomponents = (opt.components and not opt.active_components) if opt.versions: - include_fields.append("versions") + include_fields += ["versions"] if opt.component_owners: include_fields += [ "components.default_assigned_to", "components.name", ] + if (opt.active_components and + any(["components" in i for i in include_fields])): + include_fields += ["components.is_active"] bz.refresh_products(names=productname and [productname] or None, include_fields=include_fields) @@ -598,20 +612,25 @@ def _do_info(bz, opt): for name in sorted([p["name"] for p in bz.getproducts()]): print(name) - elif opt.components: + elif fastcomponents: for name in sorted(bz.getcomponents(productname)): print(name) + elif opt.components: + details = bz.getcomponentsdetails(productname) + for name in sorted(_filter_components(details)): + print(name) + elif opt.versions: proddict = bz.getproducts()[0] for v in proddict['versions']: print(to_encoding(v["name"])) elif opt.component_owners: - component_details = bz.getcomponentsdetails(productname) - for c in sorted(component_details): + details = bz.getcomponentsdetails(productname) + for c in sorted(_filter_components(details)): print(to_encoding(u"%s: %s" % (c, - component_details[c]['default_assigned_to']))) + details[c]['default_assigned_to']))) def _convert_to_outputformat(output): diff --git a/tests/ro_functional.py b/tests/ro_functional.py index 07fcef19..9592db32 100644 --- a/tests/ro_functional.py +++ b/tests/ro_functional.py @@ -213,7 +213,7 @@ class RHTest(BaseTest): test01 = lambda s: BaseTest._testInfoProducts(s, 125, "Virtualization Tools") test02 = lambda s: BaseTest._testInfoComps(s, "Virtualization Tools", - 10, "virt-manager") + 10, "virtinst") test03 = lambda s: BaseTest._testInfoVers(s, "Fedora", 19, "rawhide") test04 = lambda s: BaseTest._testInfoCompOwners(s, "Virtualization Tools", "libvirt: Libvirt Maintainers") @@ -344,3 +344,11 @@ def testExternalBugsOutput(self): expect = ("http://bugzilla.gnome.org/show_bug.cgi?id=703421\n" + "External bug: https://bugs.launchpad.net/bugs/1203576") self.assertTrue(expect in out) + + def testActiveComps(self): + out = self.clicomm("info --components 'Virtualization Tools' " + "--active-components") + self.assertTrue("virtinst" not in out) + out = self.clicomm("info --component_owners 'Virtualization Tools' " + "--active-components") + self.assertTrue("virtinst" not in out) From f11f210955cde88873680675f903f6bfcf2ece69 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Mar 2018 15:27:30 -0500 Subject: [PATCH 036/393] api-notes: Link to some fedora consumer code --- xmlrpc-api-notes.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xmlrpc-api-notes.txt b/xmlrpc-api-notes.txt index 593cdd91..1a99be7a 100644 --- a/xmlrpc-api-notes.txt +++ b/xmlrpc-api-notes.txt @@ -120,3 +120,8 @@ Redhat Bugzilla: 4.4 based with extensions. Bits on top of 4.4 Bug.update has more hashing support extra_fields for fetching comments, attachments, etc at Bug.get time ExternalBugs extension: https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html + + +Fedora infrastructure python-bugzilla consumers: + https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/distgit/pagure/templates/pagure-sync-bugzilla.py.j2 + https://github.com/fedora-infra/bodhi/blob/develop/bodhi/server/bugs.py From 21425847e11b920ef9dd3a99d232c317b2b39c4d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 15:10:09 -0500 Subject: [PATCH 037/393] tests: Add explicit tests for our main() error wrappers --- bugzilla/_cli.py | 24 +++++++++++++----------- setup.py | 2 +- tests/ro_functional.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index a823093b..7bb1bd7f 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1134,27 +1134,22 @@ def _main(unittest_bz_instance): def main(unittest_bz_instance=None): try: - return _main(unittest_bz_instance) - except KeyboardInterrupt: - log.debug("", exc_info=True) - print("\nExited at user request.") - sys.exit(1) + try: + return _main(unittest_bz_instance) + except (Exception, KeyboardInterrupt): + log.debug("", exc_info=True) + raise except (Fault, bugzilla.BugzillaError) as e: - log.debug("", exc_info=True) print("\nServer error: %s" % str(e)) sys.exit(3) except ProtocolError as e: - log.debug("", exc_info=True) print("\nInvalid server response: %d %s" % (e.errcode, e.errmsg)) - # Detect redirect redir = (e.headers and 'location' in e.headers) if redir: print("\nServer was attempting a redirect. Try: " " bugzilla --bugzilla %s ..." % redir) sys.exit(4) except requests.exceptions.SSLError as e: - log.debug("", exc_info=True) - # Give SSL recommendations print("SSL error: %s" % e) print("\nIf you trust the remote server, you can work " @@ -1164,6 +1159,13 @@ def main(unittest_bz_instance=None): except (socket.error, requests.exceptions.HTTPError, requests.exceptions.ConnectionError) as e: - log.debug("", exc_info=True) print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) + +def cli(): + try: + main() + except KeyboardInterrupt: + log.debug("", exc_info=True) + print("\nExited at user request.") + sys.exit(1) diff --git a/setup.py b/setup.py index 34fb403c..a70418c8 100755 --- a/setup.py +++ b/setup.py @@ -228,7 +228,7 @@ def _parse_requirements(fname): 'Programming Language :: Python :: 3.6', ], packages = ['bugzilla'], - entry_points={'console_scripts': ['bugzilla = bugzilla._cli:main']}, + entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, data_files=[('share/man/man1', ['bugzilla.1'])], install_requires=_parse_requirements("requirements.txt"), diff --git a/tests/ro_functional.py b/tests/ro_functional.py index 9592db32..f0d56308 100644 --- a/tests/ro_functional.py +++ b/tests/ro_functional.py @@ -352,3 +352,21 @@ def testActiveComps(self): out = self.clicomm("info --component_owners 'Virtualization Tools' " "--active-components") self.assertTrue("virtinst" not in out) + + def testFaults(self): + # Test special error wrappers in bugzilla/_cli.py + bzinstance = Bugzilla(self.url, use_creds=False) + out = tests.clicomm("bugzilla query --field=IDONTEXIST=FOO", + bzinstance, expectfail=True) + self.assertTrue("Server error:" in out) + + out = tests.clicomm("bugzilla " + "--bugzilla https://example.com/xmlrpc.cgi " + "query --field=IDONTEXIST=FOO", None, expectfail=True) + self.assertTrue("Connection lost/failed" in out) + + out = tests.clicomm("bugzilla " + "--bugzilla https://expired.badssl.com/ " + "query --bug_id 1234", None, expectfail=True) + self.assertTrue(("trust the remote server" in out) and + ("--nosslverify" in out)) From c57e0bbc0c8e1ef285fa3d761497fa6e7cfc57dd Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 16:14:53 -0500 Subject: [PATCH 038/393] cli: Drop explicit ProtocolError handling Not sure what prompted the original redirect suggestion, but I can't find a reproducer, so just wrap it into our connection fail --- bugzilla/_cli.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 7bb1bd7f..0a93b283 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1142,13 +1142,6 @@ def main(unittest_bz_instance=None): except (Fault, bugzilla.BugzillaError) as e: print("\nServer error: %s" % str(e)) sys.exit(3) - except ProtocolError as e: - print("\nInvalid server response: %d %s" % (e.errcode, e.errmsg)) - redir = (e.headers and 'location' in e.headers) - if redir: - print("\nServer was attempting a redirect. Try: " - " bugzilla --bugzilla %s ..." % redir) - sys.exit(4) except requests.exceptions.SSLError as e: # Give SSL recommendations print("SSL error: %s" % e) @@ -1158,10 +1151,12 @@ def main(unittest_bz_instance=None): sys.exit(4) except (socket.error, requests.exceptions.HTTPError, - requests.exceptions.ConnectionError) as e: + requests.exceptions.ConnectionError, + ProtocolError) as e: print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) + def cli(): try: main() From 4b7ec4aad5c2863e97935ff77ebe0ba2fb1f646f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 16:50:20 -0500 Subject: [PATCH 039/393] tests: Simplify clicomm() helper a bit --- tests/__init__.py | 24 +++++++++--------------- tests/rw_functional.py | 6 ++++-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 12beb611..1cee574d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -54,8 +54,7 @@ def difffile(expect, filename): raise AssertionError("Output was different:\n%s" % ret) -def clicomm(argv, bzinstance, returnmain=False, printcliout=False, - stdin=None, stdinstr=None, expectfail=False): +def clicomm(argv, bzinstance, returnmain=False, stdin=None, expectfail=False): """ Run bin/bugzilla.main() directly with passed argv """ @@ -67,14 +66,11 @@ def clicomm(argv, bzinstance, returnmain=False, printcliout=False, oldstdin = sys.stdin oldargv = sys.argv try: - if not printcliout: - out = StringIO() - sys.stdout = out - sys.stderr = out - if stdin: - sys.stdin = stdin - elif stdinstr: - sys.stdin = StringIO(stdinstr) + out = StringIO() + sys.stdout = out + sys.stderr = out + if stdin: + sys.stdin = stdin sys.argv = argv @@ -88,11 +84,9 @@ def clicomm(argv, bzinstance, returnmain=False, printcliout=False, except SystemExit as sys_e: ret = sys_e.code - outt = "" - if not printcliout: - outt = out.getvalue() - if outt.endswith("\n"): - outt = outt[:-1] + outt = out.getvalue() + if outt.endswith("\n"): + outt = outt[:-1] if ret != 0 and not expectfail: raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % diff --git a/tests/rw_functional.py b/tests/rw_functional.py index a719eca5..fc4a7848 100644 --- a/tests/rw_functional.py +++ b/tests/rw_functional.py @@ -20,8 +20,10 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: from urllib.parse import urlparse # pylint: disable=no-name-in-module + from io import StringIO else: from urlparse import urlparse + from StringIO import StringIO # pylint: enable=import-error import bugzilla @@ -691,9 +693,9 @@ def fakegetpass(prompt): # bare 'login' - stdinstr = "foobar@example.com\n\rfoobar\n\r" + stdinstr = StringIO("foobar@example.com\n\rfoobar\n\r") ret = tests.clicomm("bugzilla --bugzilla %s login" % self.url, - None, expectfail=True, stdinstr=stdinstr) + None, expectfail=True, stdin=stdinstr) self.assertTrue("Bugzilla Username:" in ret) self.assertTrue("Bugzilla Password:" in ret) self.assertTrue("Login failed: " in ret) From 30e5b573281078a8388ef3037a51bff61d1a8635 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 16:57:08 -0500 Subject: [PATCH 040/393] cli: s/--test-return-result/--__test-return-result/g Makes it harder to potentially conflict --- bugzilla/_cli.py | 4 ++-- tests/createbug.py | 2 +- tests/modify.py | 2 +- tests/query.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 0a93b283..9621434e 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -272,8 +272,8 @@ def _parser_add_bz_fields(rootp, command): " --field cf_my_field=VALUE") # Used by unit tests, not for end user consumption - p.add_argument('--test-return-result', action="store_true", - help=argparse.SUPPRESS) + p.add_argument('--__test-return-result', action="store_true", + dest="test_return_result", help=argparse.SUPPRESS) if not cmd_modify: _parser_add_output_options(rootp) diff --git a/tests/createbug.py b/tests/createbug.py index e94af17a..dc21ac25 100644 --- a/tests/createbug.py +++ b/tests/createbug.py @@ -29,7 +29,7 @@ def assertDictEqual(self, *args, **kwargs): return self.assertEqual(*args, **kwargs) def clicomm(self, argstr, out): - comm = "bugzilla new --test-return-result " + argstr + comm = "bugzilla new --__test-return-result " + argstr if out is None: self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) diff --git a/tests/modify.py b/tests/modify.py index 49f0723d..21ee02a4 100644 --- a/tests/modify.py +++ b/tests/modify.py @@ -29,7 +29,7 @@ def assertDictEqual(self, *args, **kwargs): return self.assertEqual(*args, **kwargs) def clicomm(self, argstr, out, wbout=None, tags_add=None, tags_rm=None): - comm = "bugzilla modify --test-return-result 123456 224466 " + argstr + comm = "bugzilla modify --__test-return-result 123456 224466 " + argstr # pylint: disable=unpacking-non-sequence if out is None: diff --git a/tests/query.py b/tests/query.py index 32b3702c..44195dcd 100644 --- a/tests/query.py +++ b/tests/query.py @@ -35,7 +35,7 @@ def assertDictEqual(self, *args, **kwargs): return self.assertEqual(*args, **kwargs) def clicomm(self, argstr, out): - comm = "bugzilla query --test-return-result " + argstr + comm = "bugzilla query --__test-return-result " + argstr if out is None: self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) From e8c928c6ab2c7caf86811f5a7b80b850b3f090e5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 17:05:33 -0500 Subject: [PATCH 041/393] tests: Remove unused diff functions --- tests/__init__.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 1cee574d..c12b25e8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,6 @@ from __future__ import print_function -import atexit -import difflib import os import shlex import sys @@ -35,25 +33,6 @@ def make_bz(version, *args, **kwargs): return bz -def diff(orig, new): - """ - Return a unified diff string between the passed strings - """ - return "".join(difflib.unified_diff(orig.splitlines(1), - new.splitlines(1), - fromfile="Orig", - tofile="New")) - - -def difffile(expect, filename): - expect += '\n' - if not os.path.exists(filename) or os.getenv("__BUGZILLA_UNITTEST_REGEN"): - open(filename, "w").write(expect) - ret = diff(open(filename).read(), expect) - if ret: - raise AssertionError("Output was different:\n%s" % ret) - - def clicomm(argv, bzinstance, returnmain=False, stdin=None, expectfail=False): """ Run bin/bugzilla.main() directly with passed argv From 3e5101fbfc077106b7416bd9976699aae44b4853 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 18:06:53 -0500 Subject: [PATCH 042/393] spec: Drop rhel6 compat I'm not going to build there anymore --- python-bugzilla.spec | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 144bba7b..9844b6c5 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -18,9 +18,6 @@ BuildArch: noarch BuildRequires: python2-devel BuildRequires: python-requests BuildRequires: python-setuptools -%if 0%{?el6} -BuildRequires: python-argparse -%endif %if 0%{?with_python3} BuildRequires: python3-devel @@ -38,9 +35,6 @@ over XML-RPC.\ Summary: %summary Requires: python-requests Requires: python-magic -%if 0%{?el6} -Requires: python-argparse -%endif # This dep is for back compat, so that installing python-bugzilla continues # to give the cli tool Requires: python-bugzilla-cli From 7b9e8708cc2405b71883af40b7e3b03e274567ec Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 17:51:52 -0500 Subject: [PATCH 043/393] CONTRIBUTING: Remove bumpversion mentions This isn't really relevant to average contributors IMO --- CONTRIBUTING.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7d137b3..3d9b1378 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,19 +65,3 @@ at python-bugzilla@lists.fedorahosted.org using 'git send-email'. # Bug reports Bug reports should be submitted as github issues, or sent to the mailing list - -# Release and tag management - -This project uses [bumpversion](https://github.com/peritus/bumpversion) to manage releases. - -## Example release steps -```sh -# release the current version, eg: 2.2.0-dev -> 2.2.0 -bumpversion release - -# prepare the next patch (z-stream) version, eg: 2.2.0 -> 2.2.1-dev -bumpversion --no-tag patch - -# else, prepare the next minor (y-stream) version, eg: 2.2.0 -> 2.3.0-dev -bumpversion --no-tag minor -``` From d6c948b11057bb70da347f8aafe49dc67f9e21d1 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 13:24:08 -0500 Subject: [PATCH 044/393] tests: Initial port to pytest This is just switching to pytest conventions and command line arguments, but not using much more than that yet --- .gitignore | 1 + CONTRIBUTING.md | 32 +++--- bugzilla/_cli.py | 14 ++- python-bugzilla.spec | 7 +- setup.py | 108 +----------------- test-requirements.txt | 2 +- tests/__init__.py | 10 +- tests/conftest.py | 44 +++++++ tests/{bug.py => test_bug.py} | 0 tests/{createbug.py => test_createbug.py} | 0 tests/{misc.py => test_misc.py} | 0 tests/{modify.py => test_modify.py} | 0 tests/{query.py => test_query.py} | 0 ...ro_functional.py => test_ro_functional.py} | 5 +- ...rw_functional.py => test_rw_functional.py} | 2 +- tox.ini | 14 ++- 16 files changed, 110 insertions(+), 129 deletions(-) create mode 100644 tests/conftest.py rename tests/{bug.py => test_bug.py} (100%) rename tests/{createbug.py => test_createbug.py} (100%) rename tests/{misc.py => test_misc.py} (100%) rename tests/{modify.py => test_modify.py} (100%) rename tests/{query.py => test_query.py} (100%) rename tests/{ro_functional.py => test_ro_functional.py} (99%) rename tests/{rw_functional.py => test_rw_functional.py} (99%) diff --git a/.gitignore b/.gitignore index 17f44c0c..2c7745ed 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ MANIFEST dist build +.cache .coverage .tox diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d9b1378..fe304abb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,27 +9,33 @@ dependencies, running the command line from git is as simple as doing: # Running tests -Once you have already activated an environment, you can use the following. +Our test suite uses pytest. If your system has dependencies already, the +quick unit test suite is invoked simply with: -## Basic unit test suite - - python setup.py test + pytest ## Read-Only Functional tests -There are more comprehensive tests that are disabled by default. Readonly -functional tests that run against several public bugzilla instances. No -login account is required: - python setup.py test --ro-functional +There are more comprehensive, readonly functional tests that run against +several public bugzilla instances, but they are not run by default. No +login account is required. Run them with: + + pytest --ro-functional ## Read/Write Functional Tests. -Before running rw-functional tests, make sure you have logged into bugzilla -using. These currently run against the test bugzilla instance at -partner-bugzilla.redhat.com, and requires a valid login there: +Read/Write functional tests use partner-bugzilla.redhat.com, which is a +bugzilla instance specifically for this type of testing. Data is occasionally +hard synced with regular bugzilla.redhat.com, and all local edits are +removed. Login accounts are also synced. If you want access to +partner-bugzilla.redhat.com, sign up for a regular bugzilla.redhat.com login +and wait for the next sync period. + +Before running these tests, you'll need to cache login credentials. +Example: ./bugzilla-cli --bugzilla=partner-bugzilla.redhat.com --username=$USER login - python setup.py test --rw-functional + pytest --rw-functional ## Testing across python versions To test all supported python versions, run tox using any of the following. @@ -43,7 +49,7 @@ To test all supported python versions, run tox using any of the following. To test for pylint or pycodestyle violations, you can run: - python setup.py pylint + ./setup.py pylint Note: This expects that you already have pylint and pycodestyle installed. diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 9621434e..23aca0f5 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -40,8 +40,6 @@ DEFAULT_BZ = 'https://bugzilla.redhat.com/xmlrpc.cgi' -_is_unittest = bool(os.getenv("__BUGZILLA_UNITTEST")) -_is_unittest_debug = bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}") log = getLogger(bugzilla.__name__) @@ -51,6 +49,14 @@ # Util helpers # ################ +def _is_unittest(): + return bool(os.getenv("__BUGZILLA_UNITTEST")) + + +def _is_unittest_debug(): + return bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) + + def to_encoding(ustring): string = '' if isinstance(ustring, basestring): @@ -62,7 +68,7 @@ def to_encoding(ustring): return string preferred = locale.getpreferredencoding() - if _is_unittest: + if _is_unittest(): preferred = "UTF-8" return string.encode(preferred, 'replace') @@ -116,7 +122,7 @@ def setup_logging(debug, verbose): else: log.setLevel(WARN) - if _is_unittest_debug: + if _is_unittest_debug(): log.setLevel(DEBUG) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 9844b6c5..e7d462ed 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -18,11 +18,13 @@ BuildArch: noarch BuildRequires: python2-devel BuildRequires: python-requests BuildRequires: python-setuptools +BuildRequires: pytest %if 0%{?with_python3} BuildRequires: python3-devel BuildRequires: python3-requests BuildRequires: python3-setuptools +BuildRequires: python3-pytest %endif # if with_python3 %global _description\ @@ -112,7 +114,10 @@ done %check -%{__python2} setup.py test +pytest +%if 0%{?with_python3} +pytest-3 +%endif diff --git a/setup.py b/setup.py index a70418c8..ce3c4902 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ import glob import os import sys -import unittest from distutils.core import Command from setuptools import setup @@ -28,114 +27,16 @@ def get_version(): class TestCommand(Command): - user_options = [ - ("ro-functional", None, - "Run readonly functional tests against actual bugzilla instances. " - "This will be very slow."), - ("rw-functional", None, - "Run read/write functional tests against actual bugzilla instances. " - "As of now this only runs against partner-bugzilla.redhat.com, " - "which requires an RH bugzilla account with cached cookies. " - "This will also be very slow."), - ("only=", None, - "Run only tests whose name contains the passed string"), - ("redhat-url=", None, - "Redhat bugzilla URL to use for ro/rw_functional tests"), - ("debug", None, - "Enable python-bugzilla debug output. This may break output " - "comparison tests."), - ] + user_options = [] def initialize_options(self): - self.ro_functional = False - self.rw_functional = False - self.only = None - self.redhat_url = None - self.debug = False - + pass def finalize_options(self): pass def run(self): - os.environ["__BUGZILLA_UNITTEST"] = "1" - - try: - import coverage - usecov = int(coverage.__version__.split(".")[0]) >= 3 - except: - usecov = False - - if usecov: - cov = coverage.coverage(omit=[ - "/*/tests/*", "/usr/*", "*dev-env*", "*.tox/*"]) - cov.erase() - cov.start() - - testfiles = [] - for t in glob.glob(os.path.join(os.getcwd(), 'tests', '*.py')): - if t.endswith("__init__.py"): - continue - - base = os.path.basename(t) - if (base == "ro_functional.py" and not self.ro_functional): - continue - - if (base == "rw_functional.py" and not self.rw_functional): - continue - - testfiles.append('.'.join(['tests', os.path.splitext(base)[0]])) - - - if hasattr(unittest, "installHandler"): - try: - unittest.installHandler() - except: - print("installHandler hack failed") - - import tests as testsmodule - testsmodule.REDHAT_URL = self.redhat_url - if self.debug: - import logging - import bugzilla - logging.getLogger(bugzilla.__name__).setLevel(logging.DEBUG) - os.environ["__BUGZILLA_UNITTEST_DEBUG"] = "1" - - tests = unittest.TestLoader().loadTestsFromNames(testfiles) - if self.only: - newtests = [] - for suite1 in tests: - for suite2 in suite1: - for testcase in suite2: - if self.only in str(testcase): - newtests.append(testcase) - - if not newtests: - print("--only didn't find any tests") - sys.exit(1) - - tests = unittest.TestSuite(newtests) - print("Running only:") - for test in newtests: - print("%s" % test) - print() - - - verbosity = 1 - if self.ro_functional or self.rw_functional: - verbosity = 2 - t = unittest.TextTestRunner(verbosity=verbosity) - - result = t.run(tests) - - if usecov: - cov.stop() - cov.save() - - err = int(bool(len(result.failures) > 0 or - len(result.errors) > 0)) - if not err and usecov: - cov.report(show_missing=False) - sys.exit(err) + print("\n* Tests are now run with the 'pytest' tool.\n" + "* See CONTRIBUTING.md for details.") class PylintCommand(Command): @@ -206,6 +107,7 @@ def _parse_requirements(fname): ret.append(line) return ret + setup(name='python-bugzilla', version=get_version(), description='Bugzilla XMLRPC access module', diff --git a/test-requirements.txt b/test-requirements.txt index df484477..c588a62a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,2 @@ # additional packages needed for testing -coverage +pytest diff --git a/tests/__init__.py b/tests/__init__.py index c12b25e8..cdba248f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -15,9 +15,13 @@ from bugzilla import Bugzilla, RHBugzilla, _cli -# This is overwritten by python setup.py test --redhat-url, and then -# used in ro/rw tests -REDHAT_URL = None +class _CLICONFIG(object): + def __init__(self): + self.REDHAT_URL = None + + +CLICONFIG = _CLICONFIG() +os.environ["__BUGZILLA_UNITTEST"] = "1" def make_bz(version, *args, **kwargs): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..af96ac3a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,44 @@ +import logging +import os + +import tests + +import bugzilla + + +# pytest plugin adding custom options. Hooks are documented here: +# https://docs.pytest.org/en/latest/writing_plugins.html + +def pytest_addoption(parser): + parser.addoption("--ro-functional", action="store_true", default=False, + help=("Run readonly functional tests against actual " + "bugzilla instances. This will be very slow.")) + parser.addoption("--rw-functional", action="store_true", default=False, + help=("Run read/write functional tests against actual bugzilla " + "instances. As of now this only runs against " + "partner-bugzilla.redhat.com, which requires an RH " + "bugzilla account with cached login creds. This will " + "also be very slow.")) + parser.addoption("--redhat-url", + help="Redhat bugzilla URL to use for ro/rw_functional tests") + parser.addoption("--pybz-debug", action="store_true", default=False, + help=("Enable python-bugzilla debug output. This may break " + "output comparison tests.")) + + +def pytest_ignore_collect(path, config): + if ((os.path.basename(str(path)) == "test_ro_functional.py") and + not config.getoption("--ro-functional")): + return True + + if ((os.path.basename(str(path)) == "test_rw_functional.py") and + not config.getoption("--rw-functional")): + return True + + +def pytest_configure(config): + if config.getoption("--redhat-url"): + tests.CLICONFIG.REDHAT_URL = config.getoption("--redhat-url") + if config.getoption("--pybz-debug"): + logging.getLogger(bugzilla.__name__).setLevel(logging.DEBUG) + os.environ["__BUGZILLA_UNITTEST_DEBUG"] = "1" diff --git a/tests/bug.py b/tests/test_bug.py similarity index 100% rename from tests/bug.py rename to tests/test_bug.py diff --git a/tests/createbug.py b/tests/test_createbug.py similarity index 100% rename from tests/createbug.py rename to tests/test_createbug.py diff --git a/tests/misc.py b/tests/test_misc.py similarity index 100% rename from tests/misc.py rename to tests/test_misc.py diff --git a/tests/modify.py b/tests/test_modify.py similarity index 100% rename from tests/modify.py rename to tests/test_modify.py diff --git a/tests/query.py b/tests/test_query.py similarity index 100% rename from tests/query.py rename to tests/test_query.py diff --git a/tests/ro_functional.py b/tests/test_ro_functional.py similarity index 99% rename from tests/ro_functional.py rename to tests/test_ro_functional.py index f0d56308..e34ee5f4 100644 --- a/tests/ro_functional.py +++ b/tests/test_ro_functional.py @@ -37,7 +37,7 @@ def clicomm(self, argstr, expectexc=False, bz=None): def _testBZVersion(self): bz = Bugzilla(self.url, use_creds=False) self.assertEqual(bz.__class__, self.bzclass) - if tests.REDHAT_URL: + if tests.CLICONFIG.REDHAT_URL: return self.assertEqual(bz.bz_ver_major, self.bzversion[0]) self.assertEqual(bz.bz_ver_minor, self.bzversion[1]) @@ -205,7 +205,8 @@ class BZFDO(BaseTest): class RHTest(BaseTest): - url = tests.REDHAT_URL or "https://bugzilla.redhat.com/xmlrpc.cgi" + url = (tests.CLICONFIG.REDHAT_URL or + "https://bugzilla.redhat.com/xmlrpc.cgi") bzclass = RHBugzilla bzversion = (4, 4) diff --git a/tests/rw_functional.py b/tests/test_rw_functional.py similarity index 99% rename from tests/rw_functional.py rename to tests/test_rw_functional.py index fc4a7848..2ea3c69c 100644 --- a/tests/rw_functional.py +++ b/tests/test_rw_functional.py @@ -68,7 +68,7 @@ class RHPartnerTest(BaseTest): # Despite its name, this instance is simply for bugzilla testing, # doesn't send out emails and is blown away occasionally. The front # page has some info. - url = tests.REDHAT_URL or "partner-bugzilla.redhat.com" + url = tests.CLICONFIG.REDHAT_URL or "partner-bugzilla.redhat.com" bzclass = bugzilla.RHBugzilla diff --git a/tox.ini b/tox.ini index 4f764449..fcea2790 100644 --- a/tox.ini +++ b/tox.ini @@ -7,4 +7,16 @@ deps = -rrequirements.txt -rtest-requirements.txt commands = - python setup.py test [] + pytest [] + + +[pytest] +addopts = -q --tb=native + + +[coverage:run] +omit = + /*/tests/* + /usr/* + *dev-env* + *.tox/* From 9d012afd82c3ea819ca26954c88b932ea1b4389d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 8 Mar 2018 07:19:11 -0500 Subject: [PATCH 045/393] Drop references to dev-env, it's not pybz specific --- .gitignore | 3 --- tox.ini | 1 - 2 files changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2c7745ed..e4e704ff 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,3 @@ build .cache .coverage .tox - -#python-bugzilla venvs -dev-env* diff --git a/tox.ini b/tox.ini index fcea2790..5712b07b 100644 --- a/tox.ini +++ b/tox.ini @@ -18,5 +18,4 @@ addopts = -q --tb=native omit = /*/tests/* /usr/* - *dev-env* *.tox/* From e8e9322388d6e56a1dbb4501f1840b1f08201312 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Mar 2018 18:57:20 -0500 Subject: [PATCH 046/393] tests: Convert to bare assert With pytest this simplifies the code and improves error reporting --- tests/__init__.py | 32 +-- tests/test_bug.py | 27 ++- tests/test_createbug.py | 22 +-- tests/test_misc.py | 54 +++--- tests/test_modify.py | 43 ++--- tests/test_query.py | 40 ++-- tests/test_ro_functional.py | 128 ++++++------ tests/test_rw_functional.py | 375 ++++++++++++++++-------------------- 8 files changed, 323 insertions(+), 398 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index cdba248f..e946e205 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -37,50 +37,52 @@ def make_bz(version, *args, **kwargs): return bz -def clicomm(argv, bzinstance, returnmain=False, stdin=None, expectfail=False): +def clicomm(argvstr, bzinstance, + returnmain=False, stdin=None, expectfail=False): """ Run bin/bugzilla.main() directly with passed argv """ - argv = shlex.split(argv) + argv = shlex.split(argvstr) oldstdout = sys.stdout oldstderr = sys.stderr oldstdin = sys.stdin oldargv = sys.argv try: - out = StringIO() - sys.stdout = out - sys.stderr = out + out_io = StringIO() + sys.stdout = out_io + sys.stderr = out_io if stdin: sys.stdin = stdin sys.argv = argv ret = 0 - mainout = None + test_return = None try: print(" ".join(argv)) print() - mainout = _cli.main(unittest_bz_instance=bzinstance) + test_return = _cli.main(unittest_bz_instance=bzinstance) except SystemExit as sys_e: ret = sys_e.code - outt = out.getvalue() - if outt.endswith("\n"): - outt = outt[:-1] + outstr = out_io.getvalue() + if outstr.endswith("\n"): + outstr = outstr[:-1] if ret != 0 and not expectfail: raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % - (ret, argv, outt)) - elif ret == 0 and expectfail: + (ret, argvstr, outstr)) + if ret == 0 and expectfail: raise RuntimeError("Command succeeded but we expected success\n" - "ret=%d\ncmd=%s\nout=%s" % (ret, argv, outt)) + "ret=%d\ncmd=%s\nout=%s" % + (ret, argvstr, outstr)) if returnmain: - return mainout - return outt + return test_return + return outstr finally: sys.stdout = oldstdout sys.stderr = oldstderr diff --git a/tests/test_bug.py b/tests/test_bug.py index 5f9f79bb..774afe94 100644 --- a/tests/test_bug.py +++ b/tests/test_bug.py @@ -23,7 +23,6 @@ class BugTest(unittest.TestCase): - maxDiff = None bz = rhbz def testBasic(self): @@ -42,22 +41,20 @@ def testBasic(self): bug = Bug(bugzilla=self.bz, dict=data) def _assert_bug(): - self.assertEqual(hasattr(bug, "component"), True) - self.assertEqual(getattr(bug, "components"), ["foo"]) - self.assertEqual(getattr(bug, "product"), "bar") - self.assertEqual(hasattr(bug, "short_desc"), True) - self.assertEqual(getattr(bug, "summary"), "some short desc") - self.assertEqual(bool(getattr(bug, "cf_fixed_in")), True) - self.assertEqual(getattr(bug, "fixed_in"), "1.2.3.4") - self.assertEqual(bool(getattr(bug, "cf_devel_whiteboard")), True) - self.assertEqual(getattr(bug, "devel_whiteboard"), - "some status value") + assert hasattr(bug, "component") is True + assert getattr(bug, "components") == ["foo"] + assert getattr(bug, "product") == "bar" + assert hasattr(bug, "short_desc") is True + assert getattr(bug, "summary") == "some short desc" + assert bool(getattr(bug, "cf_fixed_in")) is True + assert getattr(bug, "fixed_in") == "1.2.3.4" + assert bool(getattr(bug, "cf_devel_whiteboard")) is True + assert getattr(bug, "devel_whiteboard") == "some status value" _assert_bug() - self.assertEqual(str(bug), - "#123456 NEW - foo@bar.com - some short desc") - self.assertTrue(repr(bug).startswith(" 18) + assert len(out.splitlines()) > 18 def testCmdHelp(self): out = tests.clicomm("bugzilla query --help", None) - self.assertTrue(len(out.splitlines()) > 40) + assert len(out.splitlines()) > 40 def testVersion(self): out = tests.clicomm("bugzilla --version", None) - self.assertTrue(len(out.splitlines()) >= 2) + assert len(out.splitlines()) >= 2 def testPositionalArgs(self): # Make sure cli correctly rejects ambiguous positional args out = tests.clicomm("bugzilla login --xbadarg foo", None, expectfail=True) - self.assertTrue("unrecognized arguments: --xbadarg" in out) + assert "unrecognized arguments: --xbadarg" in out out = tests.clicomm("bugzilla modify 123456 --foobar --status NEW", None, expectfail=True) - self.assertTrue("unrecognized arguments: --foobar" in out) + assert "unrecognized arguments: --foobar" in out class MiscAPI(unittest.TestCase): @@ -55,7 +56,7 @@ class MiscAPI(unittest.TestCase): """ def testUserAgent(self): b3 = tests.make_bz("3.0.0") - self.assertTrue("python-bugzilla" in b3.user_agent) + assert "python-bugzilla" in b3.user_agent def testCookies(self): cookiesbad = os.path.join(os.getcwd(), "tests/data/cookies-bad.txt") @@ -64,22 +65,11 @@ def testCookies(self): # We used to convert LWP cookies, but it shouldn't matter anymore, # so verify they fail at least - try: + with pytest.raises(bugzilla.BugzillaError): tests.make_bz("3.0.0", cookiefile=cookieslwp) - raise AssertionError("Expected BugzillaError from parsing %s" % - os.path.basename(cookieslwp)) - except bugzilla.BugzillaError: - # Expected result - pass - - # Make sure bad cookies raise an error - try: + + with pytest.raises(bugzilla.BugzillaError): tests.make_bz("3.0.0", cookiefile=cookiesbad) - raise AssertionError("Expected BugzillaError from parsing %s" % - os.path.basename(cookiesbad)) - except bugzilla.BugzillaError: - # Expected result - pass # Mozilla should 'just work' tests.make_bz("3.0.0", cookiefile=cookiesmoz) @@ -98,9 +88,9 @@ def test_readconfig(self): temp.write(content) temp.flush() bzapi.readconfig(temp.name) - self.assertEqual(bzapi.user, "test1") - self.assertEqual(bzapi.password, "test2") - self.assertEqual(bzapi.api_key, None) + assert bzapi.user == "test1" + assert bzapi.password == "test2" + assert bzapi.api_key is None content = """ [foo.example.com] @@ -111,29 +101,29 @@ def test_readconfig(self): temp.write(content) temp.flush() bzapi.readconfig(temp.name) - self.assertEqual(bzapi.user, "test3") - self.assertEqual(bzapi.password, "test4") - self.assertEqual(bzapi.api_key, "123abc") + assert bzapi.user == "test3" + assert bzapi.password == "test4" + assert bzapi.api_key == "123abc" bzapi.url = "bugzilla.redhat.com" bzapi.user = None bzapi.password = None bzapi.api_key = None bzapi.readconfig(temp.name) - self.assertEqual(bzapi.user, None) - self.assertEqual(bzapi.password, None) - self.assertEqual(bzapi.api_key, None) + assert bzapi.user is None + assert bzapi.password is None + assert bzapi.api_key is None def testPostTranslation(self): def _testPostCompare(bz, indict, outexpect): outdict = indict.copy() bz.post_translation({}, outdict) - self.assertTrue(outdict == outexpect) + assert outdict == outexpect # Make sure multiple calls don't change anything bz.post_translation({}, outdict) - self.assertTrue(outdict == outexpect) + assert outdict == outexpect bug3 = tests.make_bz("3.4.0") rhbz = tests.make_bz("4.4.0", rhbz=True) diff --git a/tests/test_modify.py b/tests/test_modify.py index 21ee02a4..983b08e6 100644 --- a/tests/test_modify.py +++ b/tests/test_modify.py @@ -11,41 +11,30 @@ import unittest -import tests +import pytest +import tests rhbz = tests.make_bz("4.4.0", rhbz=True) class ModifyTest(unittest.TestCase): - maxDiff = None bz = rhbz - def assertDictEqual(self, *args, **kwargs): - # pylint: disable=arguments-differ - # EPEL5 back compat - if hasattr(unittest.TestCase, "assertDictEqual"): - return unittest.TestCase.assertDictEqual(self, *args, **kwargs) - return self.assertEqual(*args, **kwargs) - def clicomm(self, argstr, out, wbout=None, tags_add=None, tags_rm=None): comm = "bugzilla modify --__test-return-result 123456 224466 " + argstr - # pylint: disable=unpacking-non-sequence - - if out is None: - self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) - else: - (mdict, wdict, tagsa, tagsr) = tests.clicomm( - comm, self.bz, returnmain=True) - - if wbout: - self.assertDictEqual(wbout, wdict) - if out: - self.assertDictEqual(out, mdict) - if tags_add: - self.assertEqual(tags_add, tagsa) - if tags_rm: - self.assertEqual(tags_rm, tagsr) + + (mdict, wdict, tagsa, tagsr) = tests.clicomm( + comm, self.bz, returnmain=True) + + if wbout: + assert wbout == wdict + if out: + assert out == mdict + if tags_add: + assert tags_add == tagsa + if tags_rm: + assert tags_rm == tagsr def testBasic(self): self.clicomm( @@ -204,5 +193,5 @@ def testSubComponents(self): {"component": "foo", "sub_components": {"foo": ["bar baz"]}}) def testSubComponentFail(self): - self.assertRaises(ValueError, self.bz.build_update, - sub_component="some sub component") + with pytest.raises(ValueError): + self.bz.build_update(sub_component="some sub component") diff --git a/tests/test_query.py b/tests/test_query.py index 44195dcd..e3d61327 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -13,6 +13,8 @@ import os import unittest +import pytest + import tests bz34 = tests.make_bz("3.4.0") @@ -25,23 +27,16 @@ class BZ34Test(unittest.TestCase): This is the base query class, but it's also functional on its own. """ - maxDiff = None - - def assertDictEqual(self, *args, **kwargs): - # pylint: disable=arguments-differ - # EPEL5 back compat - if hasattr(unittest.TestCase, "assertDictEqual"): - return unittest.TestCase.assertDictEqual(self, *args, **kwargs) - return self.assertEqual(*args, **kwargs) - def clicomm(self, argstr, out): comm = "bugzilla query --__test-return-result " + argstr - if out is None: - self.assertRaises(RuntimeError, tests.clicomm, comm, self.bz) + if not out: + with pytest.raises(RuntimeError): + tests.clicomm(comm, self.bz) + else: q = tests.clicomm(comm, self.bz, returnmain=True) - self.assertDictEqual(out, q) + assert out == q def testBasicQuery(self): self.clicomm("--product foo --component foo,bar --bug_id 1234,2480", @@ -239,21 +234,21 @@ def translate(_in): in_query["include_fields"] = [ "cf_devel_whiteboard", "cf_fixed_in", "component", "id"] - self.assertDictEqual(in_query, out_query) + assert in_query == out_query in_query = {"bug_id": "123,456", "component": "foo,bar"} out_query = translate(in_query) - self.assertEqual(out_query["id"], ["123", "456"]) - self.assertEqual(out_query["component"], ["foo", "bar"]) + assert out_query["id"] == ["123", "456"] + assert out_query["component"] == ["foo", "bar"] in_query = {"bug_id": [123, 124], "column_list": ["id"]} out_query = translate(in_query) - self.assertEqual(out_query["id"], [123, 124]) - self.assertEqual(out_query["include_fields"], in_query["column_list"]) + assert out_query["id"] == [123, 124] + assert out_query["include_fields"] == in_query["column_list"] def testInvalidBoolean(self): - self.assertRaises(RuntimeError, self.bz.build_query, - boolean_query="foobar") + with pytest.raises(RuntimeError): + self.bz.build_query(boolean_query="foobar") def testBooleans(self): out = { @@ -288,9 +283,6 @@ def testBooleans(self): class TestURLToQuery(BZ34Test): - def _check(self, url, query): - self.assertDictEqual(bz4.url_to_query(url), query) - def testSavedSearch(self): url = ("https://bugzilla.redhat.com/buglist.cgi?" "cmdtype=dorem&list_id=2342312&namedcmd=" @@ -300,7 +292,7 @@ def testSavedSearch(self): 'sharer_id': '321167', 'savedsearch': 'RHEL7 new assigned virt-maint' } - self._check(url, query) + assert bz4.url_to_query(url) == query def testStandardQuery(self): url = ("https://bugzilla.redhat.com/buglist.cgi?" @@ -318,4 +310,4 @@ def testStandardQuery(self): 'component': 'virt-manager', 'order': 'bug_status,bug_id' } - self._check(url, query) + assert bz4.url_to_query(url) == query diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index e34ee5f4..6dbf7c1b 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -13,8 +13,9 @@ import unittest -from bugzilla import Bugzilla, BugzillaError, RHBugzilla +import pytest +from bugzilla import Bugzilla, BugzillaError, RHBugzilla import tests @@ -30,17 +31,18 @@ def clicomm(self, argstr, expectexc=False, bz=None): if not bz: bz = Bugzilla(url=self.url, use_creds=False) if expectexc: - self.assertRaises(Exception, tests.clicomm, comm, bz) + with pytest.raises(Exception): + tests.clicomm(comm, bz) else: return tests.clicomm(comm, bz) def _testBZVersion(self): bz = Bugzilla(self.url, use_creds=False) - self.assertEqual(bz.__class__, self.bzclass) + assert bz.__class__ == self.bzclass if tests.CLICONFIG.REDHAT_URL: return - self.assertEqual(bz.bz_ver_major, self.bzversion[0]) - self.assertEqual(bz.bz_ver_minor, self.bzversion[1]) + assert bz.bz_ver_major == self.bzversion[0] + assert bz.bz_ver_minor == self.bzversion[1] # Since we are running these tests against bugzilla instances in # the wild, we can't depend on certain data like product lists @@ -48,19 +50,19 @@ def _testBZVersion(self): def _testInfoProducts(self, mincount, expectstr): out = self.clicomm("info --products").splitlines() - self.assertTrue(len(out) >= mincount) - self.assertTrue(expectstr in out) + assert len(out) >= mincount + assert expectstr in out def _testInfoComps(self, comp, mincount, expectstr): out = self.clicomm("info --components \"%s\"" % comp).splitlines() - self.assertTrue(len(out) >= mincount) - self.assertTrue(expectstr in out) + assert len(out) >= mincount + assert expectstr in out def _testInfoVers(self, comp, mincount, expectstr): out = self.clicomm("info --versions \"%s\"" % comp).splitlines() - self.assertTrue(len(out) >= mincount) + assert len(out) >= mincount if expectstr: - self.assertTrue(expectstr in out) + assert expectstr in out def _testInfoCompOwners(self, comp, expectstr): expectexc = (expectstr == "FAIL") @@ -69,7 +71,7 @@ def _testInfoCompOwners(self, comp, expectstr): if expectexc: return - self.assertTrue(expectstr in out.splitlines()) + assert expectstr in out.splitlines() def _testQuery(self, args, mincount, expectbug): expectexc = (expectbug == "FAIL") @@ -78,41 +80,41 @@ def _testQuery(self, args, mincount, expectbug): if expectexc: return - self.assertTrue(len(out.splitlines()) >= mincount) - self.assertTrue(bool([l1 for l1 in out.splitlines() if - l1.startswith("#" + expectbug)])) + assert len(out.splitlines()) >= mincount + assert bool([l1 for l1 in out.splitlines() if + l1.startswith("#" + expectbug)]) # Check --ids output option out2 = self.clicomm(cli + " --ids") - self.assertTrue(len(out.splitlines()) == len(out2.splitlines())) - self.assertTrue(bool([l2 for l2 in out2.splitlines() if - l2 == expectbug])) + assert len(out.splitlines()) == len(out2.splitlines()) + assert bool([l2 for l2 in out2.splitlines() if + l2 == expectbug]) def _testQueryFull(self, bugid, mincount, expectstr): out = self.clicomm("query --full --bug_id %s" % bugid) - self.assertTrue(len(out.splitlines()) >= mincount) - self.assertTrue(expectstr in out) + assert len(out.splitlines()) >= mincount + assert expectstr in out def _testQueryRaw(self, bugid, mincount, expectstr): out = self.clicomm("query --raw --bug_id %s" % bugid) - self.assertTrue(len(out.splitlines()) >= mincount) - self.assertTrue(expectstr in out) + assert len(out.splitlines()) >= mincount + assert expectstr in out def _testQueryOneline(self, bugid, expectstr): out = self.clicomm("query --oneline --bug_id %s" % bugid) - self.assertTrue(len(out.splitlines()) == 3) - self.assertTrue(out.splitlines()[2].startswith("#%s" % bugid)) - self.assertTrue(expectstr in out) + assert len(out.splitlines()) == 3 + assert out.splitlines()[2].startswith("#%s" % bugid) + assert expectstr in out def _testQueryExtra(self, bugid, expectstr): out = self.clicomm("query --extra --bug_id %s" % bugid) - self.assertTrue(("#%s" % bugid) in out) - self.assertTrue(expectstr in out) + assert ("#%s" % bugid) in out + assert expectstr in out def _testQueryFormat(self, args, expectstr): out = self.clicomm("query %s" % args) - self.assertTrue(expectstr in out) + assert expectstr in out def _testQueryURL(self, querystr, count, expectstr): url = self.url @@ -121,8 +123,8 @@ def _testQueryURL(self, querystr, count, expectstr): else: url += querystr out = self.clicomm("query --from-url \"%s\"" % url) - self.assertEqual(len(out.splitlines()), count) - self.assertTrue(expectstr in out) + assert len(out.splitlines()) == count + assert expectstr in out class BZMozilla(BaseTest): @@ -131,9 +133,9 @@ def testVersion(self): # format, so just try to confirm that try: bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) - self.assertEqual(bz.__class__, Bugzilla) - self.assertTrue(bz.bz_ver_major >= 2016) - self.assertTrue(bz.bz_ver_minor in range(1, 13)) + assert bz.__class__ == Bugzilla + assert bz.bz_ver_major >= 2016 + assert bz.bz_ver_minor in range(1, 13) except Exception as e: # travis environment throws SSL errors here # https://travis-ci.org/python-bugzilla/python-bugzilla/builds/304713566 @@ -154,7 +156,7 @@ def testURLQuery(self): "&query_format=advanced&resolution=FIXED") bz = Bugzilla(url=self.url, use_creds=False) ret = bz.query(bz.url_to_query(query_url)) - self.assertTrue(len(ret) > 0) + assert len(ret) > 0 class BZGnome(BaseTest): @@ -181,7 +183,7 @@ def testURLQuery(self): try: bz.query(bz.url_to_query(query_url)) except BugzillaError as e: - self.assertTrue("derived from bugzilla" in str(e)) + assert "derived from bugzilla" in str(e) class BZFDO(BaseTest): @@ -267,22 +269,21 @@ def testQueryFlags(self): out = self.clicomm("query --product 'Red Hat Enterprise Linux 5' " "--component virt-manager --bug_status CLOSED " "--flag rhel-5.4.0+", bz=bz) - self.assertTrue(len(out.splitlines()) > 15) - self.assertTrue(len(out.splitlines()) < 28) - self.assertTrue("223805" in out) + assert len(out.splitlines()) > 15 + assert len(out.splitlines()) < 28 + assert "223805" in out def testQueryFixedIn(self): out = self.clicomm("query --fixed_in anaconda-15.29-1") - self.assertEqual(len(out.splitlines()), 6) - self.assertTrue("#629311 CLOSED" in out) + assert len(out.splitlines()) == 6 + assert "#629311 CLOSED" in out def testComponentsDetails(self): """ Fresh call to getcomponentsdetails should properly refresh """ bz = self.bzclass(url=self.url, use_creds=False) - self.assertTrue( - bool(bz.getcomponentsdetails("Red Hat Developer Toolset"))) + assert bool(bz.getcomponentsdetails("Red Hat Developer Toolset")) def testGetBugAlias(self): """ @@ -290,21 +291,20 @@ def testGetBugAlias(self): """ bz = self.bzclass(url=self.url, use_creds=False) bug = bz.getbug("CVE-2011-2527") - self.assertTrue(bug.bug_id == 720773) + assert bug.bug_id == 720773 def testQuerySubComponent(self): out = self.clicomm("query --product 'Red Hat Enterprise Linux 7' " "--component lvm2 --sub-component 'Thin Provisioning'") - self.assertTrue(len(out.splitlines()) >= 5) - self.assertTrue("#1060931 " in out) + assert len(out.splitlines()) >= 5 + assert "#1060931 " in out def testBugFields(self): bz = self.bzclass(url=self.url, use_creds=False) fields1 = bz.getbugfields()[:] fields2 = bz.getbugfields(force_refresh=True)[:] - self.assertTrue(bool([f for f in fields1 if - f.startswith("attachments")])) - self.assertEqual(fields1, fields2) + assert bool([f for f in fields1 if f.startswith("attachments")]) + assert fields1 == fields2 def testBugAutoRefresh(self): bz = self.bzclass(self.url, use_creds=False) @@ -313,61 +313,61 @@ def testBugAutoRefresh(self): bug = bz.query(bz.build_query(bug_id=720773, include_fields=["summary"]))[0] - self.assertTrue(hasattr(bug, "component")) - self.assertTrue(bool(bug.component)) + assert hasattr(bug, "component") + assert bool(bug.component) bz.bug_autorefresh = False bug = bz.query(bz.build_query(bug_id=720773, include_fields=["summary"]))[0] - self.assertFalse(hasattr(bug, "component")) + assert not hasattr(bug, "component") try: - self.assertFalse(bool(bug.component)) + assert bool(bug.component) except Exception as e: - self.assertTrue("adjust your include_fields" in str(e)) + assert "adjust your include_fields" in str(e) def testExtraFields(self): bz = self.bzclass(self.url, cookiefile=None, tokenfile=None) # Check default extra_fields will pull in comments bug = bz.getbug(720773, exclude_fields=["product"]) - self.assertTrue("comments" in dir(bug)) - self.assertTrue("product" not in dir(bug)) + assert "comments" in dir(bug) + assert "product" not in dir(bug) # Ensure that include_fields overrides default extra_fields bug = bz.getbug(720773, include_fields=["summary"]) - self.assertTrue("summary" in dir(bug)) - self.assertTrue("comments" not in dir(bug)) + assert "summary" in dir(bug) + assert "comments" not in dir(bug) def testExternalBugsOutput(self): out = self.clicomm('query --bug_id 989253 ' '--outputformat="%{external_bugs}"') expect = ("http://bugzilla.gnome.org/show_bug.cgi?id=703421\n" + "External bug: https://bugs.launchpad.net/bugs/1203576") - self.assertTrue(expect in out) + assert expect in out def testActiveComps(self): out = self.clicomm("info --components 'Virtualization Tools' " "--active-components") - self.assertTrue("virtinst" not in out) + assert "virtinst" not in out out = self.clicomm("info --component_owners 'Virtualization Tools' " "--active-components") - self.assertTrue("virtinst" not in out) + assert "virtinst" not in out def testFaults(self): # Test special error wrappers in bugzilla/_cli.py bzinstance = Bugzilla(self.url, use_creds=False) out = tests.clicomm("bugzilla query --field=IDONTEXIST=FOO", bzinstance, expectfail=True) - self.assertTrue("Server error:" in out) + assert "Server error:" in out out = tests.clicomm("bugzilla " "--bugzilla https://example.com/xmlrpc.cgi " "query --field=IDONTEXIST=FOO", None, expectfail=True) - self.assertTrue("Connection lost/failed" in out) + assert "Connection lost/failed" in out out = tests.clicomm("bugzilla " "--bugzilla https://expired.badssl.com/ " "query --bug_id 1234", None, expectfail=True) - self.assertTrue(("trust the remote server" in out) and - ("--nosslverify" in out)) + assert "trust the remote server" in out + assert "--nosslverify" in out diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 2ea3c69c..fdc52b7b 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -19,59 +19,37 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: - from urllib.parse import urlparse # pylint: disable=no-name-in-module from io import StringIO else: - from urlparse import urlparse from StringIO import StringIO # pylint: enable=import-error +import pytest + import bugzilla from bugzilla import Bugzilla -from bugzilla.transport import _BugzillaTokenCache - import tests -cf = os.path.expanduser("~/.bugzillacookies") -tf = os.path.expanduser("~/.bugzillatoken") + +RHURL = tests.CLICONFIG.REDHAT_URL or "partner-bugzilla.redhat.com" def _split_int(s): return [int(i) for i in s.split(",")] -class BaseTest(unittest.TestCase): - url = None - bzclass = None - - def _testBZClass(self): - bz = Bugzilla(url=self.url, use_creds=False) - self.assertTrue(bz.__class__ is self.bzclass) - - def _testCookieOrToken(self): - domain = urlparse(self.url)[1] - if os.path.exists(cf): - out = open(cf).read(1024) - if domain in out: - return - - if os.path.exists(tf): - token = _BugzillaTokenCache(self.url, tokenfilename=tf) - if token.value is not None: - return +if not bugzilla.RHBugzilla(url=RHURL).logged_in: + print("R/W tests require cached login credentials for url=%s" % RHURL) + sys.exit(1) - raise RuntimeError("%s or %s must exist and contain domain '%s'" % - (cf, tf, domain)) - -class RHPartnerTest(BaseTest): +class RHPartnerTest(unittest.TestCase): # Despite its name, this instance is simply for bugzilla testing, # doesn't send out emails and is blown away occasionally. The front # page has some info. - url = tests.CLICONFIG.REDHAT_URL or "partner-bugzilla.redhat.com" + url = RHURL bzclass = bugzilla.RHBugzilla - def _check_have_admin(self, bz, funcname): # groupnames is empty for any user if our logged in user does not # have admin privs. @@ -81,18 +59,13 @@ def _check_have_admin(self, bz, funcname): print("\nNo admin privs, reduced testing of %s" % funcname) return ret - test2 = BaseTest._testBZClass - - - def test00LoginState(self): - bz = self.bzclass(url=self.url) - self.assertTrue(bz.logged_in, - "R/W tests require cached login credentials for url=%s" % self.url) - + def test0LoggedInNoCreds(self): bz = self.bzclass(url=self.url, use_creds=False) - self.assertFalse(bz.logged_in, - "Login state check failed for logged out user.") + assert not bz.logged_in + def test2(self): + bz = Bugzilla(url=self.url, use_creds=False) + assert bz.__class__ is self.bzclass def test03NewBugBasic(self): """ @@ -110,25 +83,25 @@ def test03NewBugBasic(self): "--outputformat \"%%{bug_id}\"" % (component, version, summary), bz) - self.assertTrue(len(newout.splitlines()) == 3) + assert len(newout.splitlines()) == 3 bugid = int(newout.splitlines()[2]) bug = bz.getbug(bugid) print("\nCreated bugid: %s" % bugid) # Verify hasattr works - self.assertTrue(hasattr(bug, "id")) - self.assertTrue(hasattr(bug, "bug_id")) + assert hasattr(bug, "id") + assert hasattr(bug, "bug_id") - self.assertEqual(bug.component, component) - self.assertEqual(bug.version, version) - self.assertEqual(bug.summary, summary) + assert bug.component == component + assert bug.version == version + assert bug.summary == summary # Close the bug tests.clicomm("bugzilla modify --close NOTABUG %s" % bugid, bz) bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, "NOTABUG") + assert bug.status == "CLOSED" + assert bug.resolution == "NOTABUG" def test04NewBugAllFields(self): @@ -159,21 +132,21 @@ def test04NewBugAllFields(self): (sub_component, summary, comment, url, osval, cc, blocked, dependson, alias), bz) - self.assertTrue(len(newout.splitlines()) == 3) + assert len(newout.splitlines()) == 3 bugid = int(newout.splitlines()[2]) bug = bz.getbug(bugid, extra_fields=["sub_components"]) print("\nCreated bugid: %s" % bugid) - self.assertEqual(bug.summary, summary) - self.assertEqual(bug.bug_file_loc, url) - self.assertEqual(bug.op_sys, osval) - self.assertEqual(bug.blocks, _split_int(blocked)) - self.assertEqual(bug.depends_on, _split_int(dependson)) - self.assertTrue(all([e in bug.cc for e in cc.split(",")])) - self.assertEqual(bug.longdescs[0]["text"], comment) - self.assertEqual(bug.sub_components, {"lvm2": [sub_component]}) - self.assertEqual(bug.alias, [alias]) + assert bug.summary == summary + assert bug.bug_file_loc == url + assert bug.op_sys == osval + assert bug.blocks == _split_int(blocked) + assert bug.depends_on == _split_int(dependson) + assert all([e in bug.cc for e in cc.split(",")]) + assert bug.longdescs[0]["text"] == comment + assert bug.sub_components == {"lvm2": [sub_component]} + assert bug.alias == [alias] # Close the bug @@ -185,14 +158,14 @@ def test04NewBugAllFields(self): "--close WONTFIX %s " % bugid, bz) bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, "WONTFIX") - self.assertEqual(bug.alias, [alias]) + assert bug.status == "CLOSED" + assert bug.resolution == "WONTFIX" + assert bug.alias == [alias] # Check bug's minimal history ret = bug.get_history_raw() - self.assertTrue(len(ret["bugs"]) == 1) - self.assertTrue(len(ret["bugs"][0]["history"]) == 1) + assert len(ret["bugs"]) == 1 + assert len(ret["bugs"][0]["history"]) == 1 def test05ModifyStatus(self): @@ -209,7 +182,7 @@ def test05ModifyStatus(self): if bug.status == "CLOSED": tests.clicomm(cmd + "--status ASSIGNED", bz) bug.refresh() - self.assertEqual(bug.status, "ASSIGNED") + assert bug.status == "ASSIGNED" origstatus = bug.status @@ -221,9 +194,9 @@ def test05ModifyStatus(self): "--status %s --comment \"%s\" --private" % (status, comment), bz) bug.refresh() - self.assertEqual(bug.status, status) - self.assertEqual(bug.longdescs[-1]["is_private"], 1) - self.assertEqual(bug.longdescs[-1]["text"], comment) + assert bug.status == status + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment # Close bug as DEFERRED with a private comment resolution = "DEFERRED" @@ -234,10 +207,10 @@ def test05ModifyStatus(self): (resolution, comment), bz) bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, resolution) - self.assertEqual(bug.comments[-1]["is_private"], 1) - self.assertEqual(bug.comments[-1]["text"], comment) + assert bug.status == "CLOSED" + assert bug.resolution == resolution + assert bug.comments[-1]["is_private"] == 1 + assert bug.comments[-1]["text"] == comment # Close bug as dup with no comment dupeid = "461686" @@ -246,40 +219,40 @@ def test05ModifyStatus(self): "--close DUPLICATE --dupeid %s" % dupeid, bz) bug.refresh() - self.assertEqual(bug.dupe_of, int(dupeid)) - self.assertEqual(len(bug.longdescs), desclen + 1) - self.assertTrue("marked as a duplicate" in bug.longdescs[-1]["text"]) + assert bug.dupe_of == int(dupeid) + assert len(bug.longdescs) == (desclen + 1) + assert "marked as a duplicate" in bug.longdescs[-1]["text"] # bz.setstatus test comment = ("adding lone comment at %s" % datetime.datetime.today()) bug.setstatus("POST", comment=comment, private=True) bug.refresh() - self.assertEqual(bug.longdescs[-1]["is_private"], 1) - self.assertEqual(bug.longdescs[-1]["text"], comment) - self.assertEqual(bug.status, "POST") + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment + assert bug.status == "POST" # bz.close test fixed_in = str(datetime.datetime.today()) bug.close("ERRATA", fixedin=fixed_in) bug.refresh() - self.assertEqual(bug.status, "CLOSED") - self.assertEqual(bug.resolution, "ERRATA") - self.assertEqual(bug.fixed_in, fixed_in) + assert bug.status == "CLOSED" + assert bug.resolution == "ERRATA" + assert bug.fixed_in == fixed_in # bz.addcomment test comment = ("yet another test comment %s" % datetime.datetime.today()) bug.addcomment(comment, private=False) bug.refresh() - self.assertEqual(bug.longdescs[-1]["text"], comment) - self.assertEqual(bug.longdescs[-1]["is_private"], 0) + assert bug.longdescs[-1]["text"] == comment + assert bug.longdescs[-1]["is_private"] == 0 # Confirm comments is same as getcomments - self.assertEqual(bug.comments, bug.getcomments()) + assert bug.comments == bug.getcomments() # Reset state tests.clicomm(cmd + "--status %s" % origstatus, bz) bug.refresh() - self.assertEqual(bug.status, origstatus) + assert bug.status == origstatus def test06ModifyEmails(self): @@ -302,32 +275,32 @@ def test06ModifyEmails(self): bug.addcc(email1) bug.refresh() - self.assertTrue(email1 in bug.cc) - self.assertTrue(email2 in bug.cc) - self.assertEqual(len(bug.cc), 2) + assert email1 in bug.cc + assert email2 in bug.cc + assert len(bug.cc) == 2 tests.clicomm(cmd + "--cc=-%s" % email1, bz) bug.refresh() - self.assertTrue(email1 not in bug.cc) + assert email1 not in bug.cc # Test assigned target tests.clicomm(cmd + "--assignee %s" % email1, bz) bug.refresh() - self.assertEqual(bug.assigned_to, email1) + assert bug.assigned_to == email1 # Test QA target tests.clicomm(cmd + "--qa_contact %s" % email1, bz) bug.refresh() - self.assertEqual(bug.qa_contact, email1) + assert bug.qa_contact == email1 # Reset values bug.deletecc(bug.cc) tests.clicomm(cmd + "--reset-qa-contact --reset-assignee", bz) bug.refresh() - self.assertEqual(bug.cc, []) - self.assertEqual(bug.assigned_to, "crobinso@redhat.com") - self.assertEqual(bug.qa_contact, "extras-qa@fedoraproject.org") + assert bug.cc == [] + assert bug.assigned_to == "crobinso@redhat.com" + assert bug.qa_contact == "extras-qa@fedoraproject.org" def test07ModifyMultiFlags(self): @@ -379,10 +352,10 @@ def cleardict_new(b): bug1.refresh() bug2.refresh() - self.assertEqual(flagstr(bug1), setflags) - self.assertEqual(flagstr(bug2), setflags) - self.assertEqual(bug1.get_flags("needinfo")[0]["status"], "?") - self.assertEqual(bug1.get_flag_status("requires_doc_text"), "-") + assert flagstr(bug1) == setflags + assert flagstr(bug2) == setflags + assert bug1.get_flags("needinfo")[0]["status"] == "?" + assert bug1.get_flag_status("requires_doc_text") == "-" # Clear flags if cleardict_new(bug1): @@ -392,8 +365,8 @@ def cleardict_new(b): bz.update_flags(bug2.id, cleardict_new(bug2)) bug2.refresh() - self.assertEqual(cleardict_old(bug1), {}) - self.assertEqual(cleardict_old(bug2), {}) + assert cleardict_old(bug1) == {} + assert cleardict_old(bug2) == {} # Set "Fixed In" field origfix1 = bug1.fixed_in @@ -407,16 +380,16 @@ def cleardict_new(b): bug1.refresh() bug2.refresh() - self.assertEqual(bug1.fixed_in, newfix) - self.assertEqual(bug2.fixed_in, newfix) + assert bug1.fixed_in == newfix + assert bug2.fixed_in == newfix # Reset fixed_in tests.clicomm(cmd + "--fixed_in=\"-\"", bz) bug1.refresh() bug2.refresh() - self.assertEqual(bug1.fixed_in, "-") - self.assertEqual(bug2.fixed_in, "-") + assert bug1.fixed_in == "-" + assert bug2.fixed_in == "-" def test07ModifyMisc(self): @@ -428,30 +401,30 @@ def test07ModifyMisc(self): # modify --dependson tests.clicomm(cmd + "--dependson 123456", bz) bug.refresh() - self.assertTrue(123456 in bug.depends_on) + assert 123456 in bug.depends_on tests.clicomm(cmd + "--dependson =111222", bz) bug.refresh() - self.assertEqual([111222], bug.depends_on) + assert [111222] == bug.depends_on tests.clicomm(cmd + "--dependson=-111222", bz) bug.refresh() - self.assertEqual([], bug.depends_on) + assert [] == bug.depends_on # modify --blocked tests.clicomm(cmd + "--blocked 123,456", bz) bug.refresh() - self.assertEqual([123, 456], bug.blocks) + assert [123, 456] == bug.blocks tests.clicomm(cmd + "--blocked =", bz) bug.refresh() - self.assertEqual([], bug.blocks) + assert [] == bug.blocks # modify --keywords tests.clicomm(cmd + "--keywords +Documentation --keywords EasyFix", bz) bug.refresh() - self.assertEqual(["Documentation", "EasyFix"], bug.keywords) + assert ["Documentation", "EasyFix"] == bug.keywords tests.clicomm(cmd + "--keywords=-EasyFix --keywords=-Documentation", bz) bug.refresh() - self.assertEqual([], bug.keywords) + assert [] == bug.keywords # modify --target_release # modify --target_milestone @@ -461,24 +434,24 @@ def test07ModifyMisc(self): tests.clicomm(targetcmd + "--target_milestone beta --target_release 6.2", bz) targetbug.refresh() - self.assertEqual(targetbug.target_milestone, "beta") - self.assertEqual(targetbug.target_release, ["6.2"]) + assert targetbug.target_milestone == "beta" + assert targetbug.target_release == ["6.2"] tests.clicomm(targetcmd + "--target_milestone rc --target_release 6.0", bz) targetbug.refresh() - self.assertEqual(targetbug.target_milestone, "rc") - self.assertEqual(targetbug.target_release, ["6.0"]) + assert targetbug.target_milestone == "rc" + assert targetbug.target_release == ["6.0"] # modify --priority # modify --severity tests.clicomm(cmd + "--priority low --severity high", bz) bug.refresh() - self.assertEqual(bug.priority, "low") - self.assertEqual(bug.severity, "high") + assert bug.priority == "low" + assert bug.severity == "high" tests.clicomm(cmd + "--priority medium --severity medium", bz) bug.refresh() - self.assertEqual(bug.priority, "medium") - self.assertEqual(bug.severity, "medium") + assert bug.priority == "medium" + assert bug.severity == "medium" # modify --os # modify --platform @@ -486,25 +459,25 @@ def test07ModifyMisc(self): tests.clicomm(cmd + "--version rawhide --os Windows --arch ppc " "--url http://example.com", bz) bug.refresh() - self.assertEqual(bug.version, "rawhide") - self.assertEqual(bug.op_sys, "Windows") - self.assertEqual(bug.platform, "ppc") - self.assertEqual(bug.url, "http://example.com") + assert bug.version == "rawhide" + assert bug.op_sys == "Windows" + assert bug.platform == "ppc" + assert bug.url == "http://example.com" tests.clicomm(cmd + "--version rawhide --os Linux --arch s390 " "--url http://example.com/fribby", bz) bug.refresh() - self.assertEqual(bug.version, "rawhide") - self.assertEqual(bug.op_sys, "Linux") - self.assertEqual(bug.platform, "s390") - self.assertEqual(bug.url, "http://example.com/fribby") + assert bug.version == "rawhide" + assert bug.op_sys == "Linux" + assert bug.platform == "s390" + assert bug.url == "http://example.com/fribby" # modify --field tests.clicomm(cmd + "--field cf_fixed_in=foo-bar-1.2.3 \ --field=cf_release_notes=baz", bz) bug.refresh() - self.assertEqual(bug.fixed_in, "foo-bar-1.2.3") - self.assertEqual(bug.cf_release_notes, "baz") + assert bug.fixed_in == "foo-bar-1.2.3" + assert bug.cf_release_notes == "baz" def test08Attachments(self): @@ -537,7 +510,8 @@ def _test8Attachments(self): # Add attachment from CLI with mime guessing desc1 = "python-bugzilla cli upload %s" % datetime.datetime.today() out1 = tests.clicomm(cmd + "%s --description \"%s\" --file %s" % - (setbugid, desc1, testfile), bz) + (setbugid, desc1, testfile), bz, + stdin=open("/dev/tty", "rb")) desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() out2 = tests.clicomm(cmd + "%s --file test --summary \"%s\"" % @@ -547,29 +521,29 @@ def _test8Attachments(self): # Created attachment on bug setbug.refresh() - self.assertEqual(len(setbug.attachments), orignumattach + 2) - self.assertEqual(setbug.attachments[-2]["summary"], desc1) - self.assertEqual(setbug.attachments[-2]["id"], - int(out1.splitlines()[2].split()[2])) - self.assertEqual(setbug.attachments[-1]["summary"], desc2) - self.assertEqual(setbug.attachments[-1]["id"], - int(out2.splitlines()[2].split()[2])) + assert len(setbug.attachments) == (orignumattach + 2) + assert setbug.attachments[-2]["summary"] == desc1 + assert (setbug.attachments[-2]["id"] == + int(out1.splitlines()[2].split()[2])) + assert setbug.attachments[-1]["summary"] == desc2 + assert (setbug.attachments[-1]["id"] == + int(out2.splitlines()[2].split()[2])) attachid = setbug.attachments[-2]["id"] # Set attachment flags - self.assertEqual(setbug.attachments[-1]["flags"], []) + assert setbug.attachments[-1]["flags"] == [] bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], "review", status="+") setbug.refresh() - self.assertEqual(len(setbug.attachments[-1]["flags"]), 1) - self.assertEqual(setbug.attachments[-1]["flags"][0]["name"], "review") - self.assertEqual(setbug.attachments[-1]["flags"][0]["status"], "+") + assert len(setbug.attachments[-1]["flags"]) == 1 + assert setbug.attachments[-1]["flags"][0]["name"] == "review" + assert setbug.attachments[-1]["flags"][0]["status"] == "+" bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], "review", status="X") setbug.refresh() - self.assertEqual(setbug.attachments[-1]["flags"], []) + assert setbug.attachments[-1]["flags"] == [] # Get attachment, verify content @@ -579,10 +553,9 @@ def _test8Attachments(self): # Wrote fname = out[2].split()[1].strip() - self.assertEqual(len(out), 3) - self.assertEqual(fname, "bz-attach-get1.txt") - self.assertEqual(open(fname).read(), - open(testfile).read()) + assert len(out) == 3 + assert fname == "bz-attach-get1.txt" + assert open(fname).read() == open(testfile).read() os.unlink(fname) # Get all attachments @@ -591,9 +564,9 @@ def _test8Attachments(self): numattach = len(getbug.attachments) out = tests.clicomm(cmd + "--getall %s" % getallbugid, bz).splitlines() - self.assertEqual(len(out), numattach + 2) + assert len(out) == (numattach + 2) fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] - self.assertEqual(len(fnames), numattach) + assert len(fnames) == numattach for f in fnames: if not os.path.exists(f): raise AssertionError("filename '%s' not found" % f) @@ -616,11 +589,11 @@ def test09Whiteboards(self): (initval, initval, initval, initval), bz) bug.refresh() - self.assertEqual(bug.whiteboard, initval + "status") - self.assertEqual(bug.qa_whiteboard, initval + "qa") - self.assertEqual(bug.devel_whiteboard, initval + "devel") - self.assertEqual(bug.internal_whiteboard, - initval + "internal, security, foo security1") + assert bug.whiteboard == (initval + "status") + assert bug.qa_whiteboard == (initval + "qa") + assert bug.devel_whiteboard == (initval + "devel") + assert (bug.internal_whiteboard == + (initval + "internal, security, foo security1")) # Modify whiteboards tests.clicomm(cmd + @@ -629,9 +602,9 @@ def test09Whiteboards(self): "--devel_whiteboard =pre-%s" % bug.devel_whiteboard, bz) bug.refresh() - self.assertEqual(bug.qa_whiteboard, initval + "qa" + " _app") - self.assertEqual(bug.devel_whiteboard, "pre-" + initval + "devel") - self.assertEqual(bug.status_whiteboard, "foobar") + assert bug.qa_whiteboard == (initval + "qa" + " _app") + assert bug.devel_whiteboard == ("pre-" + initval + "devel") + assert bug.status_whiteboard == "foobar" # Verify that tag manipulation is smart about separator tests.clicomm(cmd + @@ -639,9 +612,8 @@ def test09Whiteboards(self): "--internal_whiteboard=-security,", bz) bug.refresh() - self.assertEqual(bug.qa_whiteboard, initval + "qa") - self.assertEqual(bug.internal_whiteboard, - initval + "internal, foo security1") + assert bug.qa_whiteboard == (initval + "qa") + assert bug.internal_whiteboard == (initval + "internal, foo security1") # Clear whiteboards update = bz.build_update( @@ -650,10 +622,10 @@ def test09Whiteboards(self): bz.update_bugs(bug.id, update) bug.refresh() - self.assertEqual(bug.whiteboard, "") - self.assertEqual(bug.qa_whiteboard, "") - self.assertEqual(bug.devel_whiteboard, "") - self.assertEqual(bug.internal_whiteboard, "") + assert bug.whiteboard == "" + assert bug.qa_whiteboard == "" + assert bug.devel_whiteboard == "" + assert bug.internal_whiteboard == "" def test10Login(self): @@ -676,29 +648,29 @@ def fakegetpass(prompt): "--user foobar@example.com " "--password foobar query -b 123456" % self.url, None, expectfail=True) - self.assertTrue("Login failed: " in ret) + assert "Login failed: " in ret # 'login' with explicit options ret = tests.clicomm("bugzilla --bugzilla %s " "--user foobar@example.com " "--password foobar login" % self.url, None, expectfail=True) - self.assertTrue("Login failed: " in ret) + assert "Login failed: " in ret # 'login' with positional options ret = tests.clicomm("bugzilla --bugzilla %s " "login foobar@example.com foobar" % self.url, None, expectfail=True) - self.assertTrue("Login failed: " in ret) + assert "Login failed: " in ret # bare 'login' stdinstr = StringIO("foobar@example.com\n\rfoobar\n\r") ret = tests.clicomm("bugzilla --bugzilla %s login" % self.url, None, expectfail=True, stdin=stdinstr) - self.assertTrue("Bugzilla Username:" in ret) - self.assertTrue("Bugzilla Password:" in ret) - self.assertTrue("Login failed: " in ret) + assert "Bugzilla Username:" in ret + assert "Bugzilla Password:" in ret + assert "Login failed: " in ret finally: getpass.getpass = oldgetpass @@ -714,28 +686,28 @@ def test11UserUpdate(self): user = bz.getuser(email) if have_admin: - self.assertTrue(group in user.groupnames) + assert group in user.groupnames origgroups = user.groupnames # Remove the group try: bz.updateperms(email, "remove", [group]) user.refresh() - self.assertTrue(group not in user.groupnames) + assert group not in user.groupnames except Exception as e: if have_admin: raise - self.assertTrue("Sorry, you aren't a member" in str(e)) + assert "Sorry, you aren't a member" in str(e) # Re add it try: bz.updateperms(email, "add", group) user.refresh() - self.assertTrue(group in user.groupnames) + assert group in user.groupnames except Exception as e: if have_admin: raise - self.assertTrue("Sorry, you aren't a member" in str(e)) + assert "Sorry, you aren't a member" in str(e) # Set groups try: @@ -744,11 +716,11 @@ def test11UserUpdate(self): newgroups.remove(group) bz.updateperms(email, "set", newgroups) user.refresh() - self.assertTrue(group not in user.groupnames) + assert group not in user.groupnames except Exception as e: if have_admin: raise - self.assertTrue("Sorry, you aren't a member" in str(e)) + assert "Sorry, you aren't a member" in str(e) # Reset everything try: @@ -756,10 +728,10 @@ def test11UserUpdate(self): except Exception as e: if have_admin: raise - self.assertTrue("Sorry, you aren't a member" in str(e)) + assert "Sorry, you aren't a member" in str(e) user.refresh() - self.assertEqual(user.groupnames, origgroups) + assert user.groupnames == origgroups def test11ComponentEditing(self): @@ -783,14 +755,12 @@ def compare(data, newid): compdata = c break - self.assertTrue(bool(compdata)) - self.assertEqual(data["component"], compdata["name"]) - self.assertEqual(data["description"], compdata["description"]) - self.assertEqual(data["initialowner"], - compdata["default_assigned_to"]) - self.assertEqual(data["initialqacontact"], - compdata["default_qa_contact"]) - self.assertEqual(data["is_active"], compdata["is_active"]) + assert bool(compdata) + assert data["component"] == compdata["name"] + assert data["description"] == compdata["description"] + assert data["initialowner"] == compdata["default_assigned_to"] + assert data["initialqacontact"] == compdata["default_qa_contact"] + assert data["is_active"] == compdata["is_active"] # Create component @@ -811,8 +781,7 @@ def compare(data, newid): except Exception as e: if have_admin: raise - self.assertTrue( - ("Sorry, you aren't a member" in str(e)) or + assert (("Sorry, you aren't a member" in str(e)) or # bugzilla 5 error string ("You are not allowed" in str(e))) @@ -834,8 +803,7 @@ def compare(data, newid): except Exception as e: if have_admin: raise - self.assertTrue( - ("Sorry, you aren't a member" in str(e)) or + assert (("Sorry, you aren't a member" in str(e)) or # bugzilla 5 error string ("You are not allowed" in str(e))) @@ -848,34 +816,33 @@ def test12SetCookie(self): raise AssertionError("Setting cookiefile for active connection " "should fail.") except RuntimeError as e: - self.assertTrue("disconnect()" in str(e)) + assert "disconnect()" in str(e) bz.disconnect() bz.cookiefile = None bz.connect() - self.assertFalse(bz.logged_in) + assert not bz.logged_in def test13SubComponents(self): bz = self.bzclass(url=self.url) # Long closed RHEL5 lvm2 bug. This component has sub_components bug = bz.getbug("185526") bug.autorefresh = True - self.assertEqual(bug.component, "lvm2") + assert bug.component == "lvm2" bz.update_bugs(bug.id, bz.build_update( component="lvm2", sub_component="Command-line tools (RHEL5)")) bug.refresh() - self.assertEqual(bug.sub_components, - {"lvm2": ["Command-line tools (RHEL5)"]}) + assert bug.sub_components == {"lvm2": ["Command-line tools (RHEL5)"]} bz.update_bugs(bug.id, bz.build_update(sub_component={})) bug.refresh() - self.assertEqual(bug.sub_components, {}) + assert bug.sub_components == {} def test13ExternalTrackerQuery(self): bz = self.bzclass(url=self.url) - self.assertRaises(RuntimeError, - bz.build_external_tracker_boolean_query) + with pytest.raises(RuntimeError): + bz.build_external_tracker_boolean_query() def _deleteAllExistingExternalTrackers(self, bugid): bz = self.bzclass(url=self.url) @@ -949,19 +916,19 @@ def test16ModifyTags(self): if bug.tags: bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() - self.assertEqual(bug.tags, []) + assert bug.tags == [] tests.clicomm(cmd + "--tags foo --tags +bar --tags baz", bz) bug.refresh() - self.assertEqual(bug.tags, ["foo", "bar", "baz"]) + assert bug.tags, ["foo", "bar" == "baz"] tests.clicomm(cmd + "--tags=-bar", bz) bug.refresh() - self.assertEqual(bug.tags, ["foo", "baz"]) + assert bug.tags, ["foo" == "baz"] bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() - self.assertEqual(bug.tags, []) + assert bug.tags == [] def test17LoginAPIKey(self): api_key = "somefakeapikey1234" @@ -970,9 +937,9 @@ def test17LoginAPIKey(self): self.skipTest("can only test apikey on bugzilla 5+") try: - self.assertTrue(bz.logged_in, False) + assert bz.logged_in is False # Use this to trigger a warning about api_key bz.createbug(bz.build_createbug()) except Exception as e: - self.assertTrue("The API key you specified is invalid" in str(e)) + assert "The API key you specified is invalid" in str(e) From 55c329ec940ae93ccaa6800479355007c1996296 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 12 Mar 2018 17:38:36 -0400 Subject: [PATCH 047/393] pylint: enable 'fixme' warnings Useful to have an incode todo list while writing patches, but not for long term TODO tracking --- tests/pylint.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/pylint.cfg b/tests/pylint.cfg index 021cdf03..6560d461 100644 --- a/tests/pylint.cfg +++ b/tests/pylint.cfg @@ -8,6 +8,7 @@ persistent=no # multiple time (only on the command line, not in the configuration file where # it should appear only once). disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return +enable=fixme [REPORTS] From c5501c949768581df7e80b4693e8358ba6526a97 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 12 Mar 2018 20:10:47 -0400 Subject: [PATCH 048/393] Drop sonar CI support There's really not much code here that warrants having a second pylint like CI setup, especially one I can't run locally and requires funky travis config. --- .bumpversion.cfg | 2 -- .travis.yml | 24 ++---------------------- sonar-project.properties | 13 ------------- 3 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 sonar-project.properties diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 87536b59..2211d417 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -9,8 +9,6 @@ serialize = [bumpversion:file:bugzilla/apiversion.py] -[bumpversion:file:sonar-project.properties] - [bumpversion:part:release] optional_value = gamma values = diff --git a/.travis.yml b/.travis.yml index 5cdeb751..c0a6b64e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,32 +9,12 @@ python: - pypy-5.3.1 install: - - pip install tox || true - - pip install tox-travis || true + - pip install tox + - pip install tox-travis script: - tox -- --ro-functional -jobs: - include: - - # https://sonarcloud.io/dashboard?id=com.github%3Apython-bugzilla%3Apython-bugzilla - - stage: sonar - dist: trusty - language: java - addons: - sonarcloud: - organization: "python-bugzilla" - token: - secure: "fk6YMhht7X5a60QWeeLWOrUu6zjS/1fhVfM7PicaDhkhwOaadzjuKgVj+xAmQL8cMh98hK8wk1aE9uDuwTbKMMydDqY9pH8agtyk3j/2ArbOl1hphYWo3FeoB6q8FTiKN1fiT+8hPy7MIjjFthe3e6dyUR2Le5QtH2B+BCYrDMdqbvN+GXLyBPTAvzbDerpnw1j5o2QWjaLu+juznaR+q/PL58796P6GU2WmpfkpOaMYJA2CA/w0Ehivp4zlbZpmuC/v6NopSjuT6RpO3ppPtTRmlwMZEGTAwXhxlU2CHlIyCq44Rsm0kxf1+xQK2aL/SWJzCDk6rHAd16B10r3AKwYW05Rclw49x9HV29zGeUTJD3qgRscf0v5yYyCISv1r9D4jJ+y9tIVHvMzuMvl6G0rZNiwxu6OwlzpyzQkgPbO183HGCyjYKaBn6jQmCWHzxnk+FRUt7Rvbd53Mz3TYbx7LeTnRDPNc69nIbybjzIRXEw/4SVsncue3whrj4s81NJzkvDaABzLdnd1X9G2aG8h1Vk1XpuUnP7wVyKr1L1wcSdvtIbZ7ekgOA8xQG9XHh4E+5FTZEKOYeb7ZeDKijvUnl1Qo3dXKMS2/Y+18Ogl+4M9w7c3PCjxhDAsuv42uNyxH66qxdzrzckjsyJVS6VTwTBf8fsjzgQ5hmHjKPYA=" - branches: - - master - script: - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then sonar-scanner; fi' - branches: - only: - - master - notifications: email: true on_success: never diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 90b09bf2..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,13 +0,0 @@ -# Required metadata -sonar.projectKey=com.github:python-bugzilla:python-bugzilla -sonar.projectName=Python Bugzilla -sonar.projectVersion=2.2.0.dev0 - -# Comma-separated paths to directories with sources (required) -sonar.sources=bugzilla - -# Language -sonar.language=py - -# Encoding of the source files -sonar.sourceEncoding=UTF-8 From 78aa88412cf813143566788fe6d063f8c20ea2a8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 18 Mar 2018 10:30:46 -0400 Subject: [PATCH 049/393] xmlrpc-api-notes: Add link to fedora fas2 usage --- xmlrpc-api-notes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/xmlrpc-api-notes.txt b/xmlrpc-api-notes.txt index 1a99be7a..3dadc63a 100644 --- a/xmlrpc-api-notes.txt +++ b/xmlrpc-api-notes.txt @@ -125,3 +125,4 @@ Redhat Bugzilla: 4.4 based with extensions. Bits on top of 4.4 Fedora infrastructure python-bugzilla consumers: https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/distgit/pagure/templates/pagure-sync-bugzilla.py.j2 https://github.com/fedora-infra/bodhi/blob/develop/bodhi/server/bugs.py + https://github.com/fedora-infra/fas/blob/develop/tools/export-bugzilla.py From 190006aa5ea39262e4d9d0fde6b7491390303ab5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 19 Mar 2018 18:28:19 -0400 Subject: [PATCH 050/393] spec: Merge python2 and rhel8 packaging changes from Fedora --- python-bugzilla.spec | 79 +++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index e7d462ed..7c372dce 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,26 +1,40 @@ -%if 0%{?fedora} || 0%{?rhel} >= 8 -%global with_python3 1 +%if 0%{?fedora} || 0%{?rhel} > 7 +# Enable python3 by default +%bcond_without python3 %else +%bcond_with python3 +%endif + +%if 0%{?rhel} > 7 +# Disable python2 build by default +%bcond_with python2 +%else +%bcond_without python2 %{!?__python2: %global __python2 /usr/bin/python2} -%{!?python2_sitelib2: %global python2_sitelib %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} +%{!?python2_sitelib: %global python2_sitelib %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} %endif Name: python-bugzilla Version: 2.1.0 Release: 1%{?dist} -Summary: python2 library for interacting with Bugzilla +Summary: Python library for interacting with Bugzilla License: GPLv2+ URL: https://github.com/python-bugzilla/python-bugzilla Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz BuildArch: noarch +%if %{with python2} BuildRequires: python2-devel -BuildRequires: python-requests -BuildRequires: python-setuptools -BuildRequires: pytest +BuildRequires: python2-requests +BuildRequires: python2-setuptools +BuildRequires: python2-pytest +%if 0%{?el6} +BuildRequires: python-argparse +%endif +%endif # with python2 -%if 0%{?with_python3} +%if %{with python3} BuildRequires: python3-devel BuildRequires: python3-requests BuildRequires: python3-setuptools @@ -33,10 +47,15 @@ over XML-RPC.\ %description %_description + +%if %{with python2} %package -n python2-bugzilla Summary: %summary -Requires: python-requests -Requires: python-magic +Requires: python2-requests +Requires: python2-magic +%if 0%{?el6} +Requires: python-argparse +%endif # This dep is for back compat, so that installing python-bugzilla continues # to give the cli tool Requires: python-bugzilla-cli @@ -44,23 +63,31 @@ Requires: python-bugzilla-cli %description -n python2-bugzilla %_description -%if 0%{?with_python3} +%endif # with python2 + + +%if %{with python3} %package -n python3-bugzilla Summary: %summary Requires: python3-requests Requires: python3-magic %{?python_provide:%python_provide python3-bugzilla} +%if %{without python2} +Obsoletes: python-bugzilla < %{version}-%{release} +Obsoletes: python2-bugzilla < %{version}-%{release} +%endif # without python2 + %description -n python3-bugzilla %_description %endif # if with_python3 %package cli Summary: Command line tool for interacting with Bugzilla -%if 0%{?with_python3} +%if %{with python3} Requires: python3-bugzilla = %{version}-%{release} %else -Requires: python-bugzilla = %{version}-%{release} +Requires: python2-bugzilla = %{version}-%{release} %endif %description cli @@ -71,7 +98,7 @@ This package includes the 'bugzilla' command-line tool for interacting with bugz %prep %setup -q -%if 0%{?with_python3} +%if %{with python3} rm -rf %{py3dir} cp -a . %{py3dir} %endif # with_python3 @@ -79,30 +106,38 @@ cp -a . %{py3dir} %build -%if 0%{?with_python3} +%if %{with python3} pushd %{py3dir} %{__python3} setup.py build popd %endif # with_python3 +%if %{with python2} %{__python2} setup.py build +%endif # with python2 %install -%if 0%{?with_python3} +%if %{with python3} pushd %{py3dir} %{__python3} setup.py install -O1 --skip-build --root %{buildroot} + +%if %{with python2} rm %{buildroot}/usr/bin/bugzilla +%endif + popd %endif # with_python3 +%if %{with python2} %{__python2} setup.py install -O1 --skip-build --root %{buildroot} +%endif # with python2 # Replace '#!/usr/bin/env python' with '#!/usr/bin/python2' # The format is ideal for upstream, but not a distro. See: # https://fedoraproject.org/wiki/Features/SystemPythonExecutablesUseSystemPython -%if 0%{?with_python3} +%if %{with python3} %global python_env_path %{__python3} %else %global python_env_path %{__python2} @@ -114,18 +149,22 @@ done %check +%if %{with python2} pytest -%if 0%{?with_python3} +%endif # with python2 +%if %{with python3} pytest-3 -%endif +%endif # with python3 +%if %{with python2} %files -n python2-bugzilla %doc COPYING README.md NEWS.md %{python2_sitelib}/* +%endif # with python2 -%if 0%{?with_python3} +%if %{with python3} %files -n python3-bugzilla %doc COPYING README.md NEWS.md %{python3_sitelib}/* From 13783acf3ef11724f3b92cc3e8e2b625f6147ad4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 19 Mar 2018 18:30:00 -0400 Subject: [PATCH 051/393] spec: Drop el6 building, won't be doing it anymore --- python-bugzilla.spec | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 7c372dce..9384a69c 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -29,9 +29,6 @@ BuildRequires: python2-devel BuildRequires: python2-requests BuildRequires: python2-setuptools BuildRequires: python2-pytest -%if 0%{?el6} -BuildRequires: python-argparse -%endif %endif # with python2 %if %{with python3} @@ -53,9 +50,6 @@ over XML-RPC.\ Summary: %summary Requires: python2-requests Requires: python2-magic -%if 0%{?el6} -Requires: python-argparse -%endif # This dep is for back compat, so that installing python-bugzilla continues # to give the cli tool Requires: python-bugzilla-cli From 4d72aae86471f2aff176f95be58025141613fafd Mon Sep 17 00:00:00 2001 From: Tobias Wolter Date: Thu, 5 Apr 2018 12:21:45 +0200 Subject: [PATCH 052/393] Client side certificate support Adds the `cert` command line and configuration options that allow you to specify a client-side certificate file to use for TLS authentication against the web server. File format should be concatenated certificate and (unencrypted) private key. Encrypted private key should also be possible, but would have to deal with passing the password along to `requests`, and I'm not comfortable enough with Python to actually facilitate that. --- bugzilla/_cli.py | 5 ++++- bugzilla/base.py | 10 ++++++++-- bugzilla/transport.py | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 23aca0f5..6e9f80ce 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -148,6 +148,8 @@ def _setup_root_parser(): 'specified command.') p.add_argument('--username', help="Log in with this username") p.add_argument('--password', help="Log in with this password") + p.add_argument('--cert', default=None, help="Log in with this " + "certificate") p.add_argument('--ensure-logged-in', action="store_true", help="Raise an error if we aren't logged in to bugzilla. " @@ -1031,7 +1033,8 @@ def _make_bz_instance(opt): url=opt.bugzilla, cookiefile=cookiefile, tokenfile=tokenfile, - sslverify=opt.sslverify) + sslverify=opt.sslverify, + cert=opt.cert) return bz diff --git a/bugzilla/base.py b/bugzilla/base.py index 02ecf3a0..0f36430f 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -260,7 +260,7 @@ def _listify(val): def __init__(self, url=-1, user=None, password=None, cookiefile=-1, - sslverify=True, tokenfile=-1, use_creds=True, api_key=None): + sslverify=True, tokenfile=-1, use_creds=True, api_key=None, cert=None): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at @@ -268,6 +268,8 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, url=None and calling connect(URL) manually :param user: optional username to connect with :param password: optional password for the connecting user + :param cert: optional certificate file for client side certificate + authentication :param cookiefile: Location to cache the login session cookies so you don't have to keep specifying username/password. Bugzilla 5+ will use tokens instead of cookies. @@ -294,6 +296,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self.user = user or '' self.password = password or '' self.api_key = api_key + self.cert = cert or '' self.url = '' self._proxy = None @@ -500,6 +503,9 @@ def readconfig(self, configpath=None): elif key == "password": log.debug("bugzillarc: setting password") self.password = val + elif key == "cert": + log.debug("bugzillarc: setting cert") + self.cert = val else: log.debug("bugzillarc: unknown key=%s", key) @@ -533,7 +539,7 @@ def connect(self, url=None): url = self.fix_url(url) self._transport = _RequestsTransport( - url, self._cookiejar, sslverify=self._sslverify) + url, self._cookiejar, sslverify=self._sslverify, cert=self.cert) self._transport.user_agent = self.user_agent self._proxy = _BugzillaServerProxy(url, self.tokenfile, self._transport) diff --git a/bugzilla/transport.py b/bugzilla/transport.py index ac32810b..0c3e47ba 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -109,7 +109,7 @@ class _RequestsTransport(Transport): user_agent = 'Python/Bugzilla' def __init__(self, url, cookiejar=None, - sslverify=True, sslcafile=None, debug=0): + sslverify=True, sslcafile=None, debug=True, cert=None): if hasattr(Transport, "__init__"): Transport.__init__(self, use_datetime=False) @@ -137,6 +137,8 @@ def __init__(self, url, cookiejar=None, # Using an explicit Session, rather than requests.get, will use # HTTP KeepAlive if the server supports it. self.session = requests.Session() + if cert: + self.session.cert = cert def parse_response(self, response): """ Parse XMLRPC response """ @@ -167,6 +169,7 @@ def _request_helper(self, url, request_body): # Save is required only if we have a filename self._cookiejar.save() + log.debug(response.text) response.raise_for_status() return self.parse_response(response) except requests.RequestException as e: From d2cf735dc484db9610ae8770bc684d81c002b242 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 5 Apr 2018 16:48:47 -0400 Subject: [PATCH 053/393] Some --cert additions * Add a test case * Add it to manpage * Tweak how it's exposed in cli --help * Fix some style issues --- bugzilla.1 | 2 ++ bugzilla/_cli.py | 4 ++-- bugzilla/base.py | 3 ++- tests/test_ro_functional.py | 10 ++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bugzilla.1 b/bugzilla.1 index d601e98a..7226bad5 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -32,6 +32,8 @@ show this help message and exit bugzilla XMLRPC URI. default: https://bugzilla.redhat.com/xmlrpc.cgi .IP "--nosslverify" Don't error on invalid bugzilla SSL certificate +.IP "--cert=CERTFILE" +client side certificate file needed by the webserver. .IP "--login" Run interactive "login" before performing the specified command. .IP "--username=USERNAME" diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 6e9f80ce..d210483f 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -142,14 +142,14 @@ def _setup_root_parser(): p.add_argument("--nosslverify", dest="sslverify", action="store_false", default=True, help="Don't error on invalid bugzilla SSL certificate") + p.add_argument('--cert', + help="client side certificate file needed by the webserver") p.add_argument('--login', action="store_true", help='Run interactive "login" before performing the ' 'specified command.') p.add_argument('--username', help="Log in with this username") p.add_argument('--password', help="Log in with this password") - p.add_argument('--cert', default=None, help="Log in with this " - "certificate") p.add_argument('--ensure-logged-in', action="store_true", help="Raise an error if we aren't logged in to bugzilla. " diff --git a/bugzilla/base.py b/bugzilla/base.py index 0f36430f..88528e6a 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -260,7 +260,8 @@ def _listify(val): def __init__(self, url=-1, user=None, password=None, cookiefile=-1, - sslverify=True, tokenfile=-1, use_creds=True, api_key=None, cert=None): + sslverify=True, tokenfile=-1, use_creds=True, api_key=None, + cert=None): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 6dbf7c1b..1b53f799 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -11,6 +11,7 @@ Unit tests that do readonly functional tests against real bugzilla instances. ''' +import os import unittest import pytest @@ -371,3 +372,12 @@ def testFaults(self): "query --bug_id 1234", None, expectfail=True) assert "trust the remote server" in out assert "--nosslverify" in out + + def testCertFail(self): + # No public setup that I know of to test cert succeeds, so + # let's give it a bogus file and ensure it fails + badcert = os.path.join(os.path.dirname(__file__), "..", "README.md") + out = tests.clicomm( + "bugzilla --cert %s query --bug_id 123456" % badcert, + None, expectfail=True) + assert "PEM" in out From 46568635386c0b5ccb5dfee5f3987f45f0ab9c9d Mon Sep 17 00:00:00 2001 From: Jeff Mahoney Date: Wed, 11 Apr 2018 10:09:57 -0400 Subject: [PATCH 054/393] cli: add ability to post comment while sending attachment This allows the user to post a comment along with an attachment. --- bugzilla.1 | 2 ++ bugzilla/_cli.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/bugzilla.1 b/bugzilla.1 index 7226bad5..1d05f631 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -206,6 +206,8 @@ Mime-type for the file being attached Download the attachment with the given ID .IP "--getall=BUGID, --get-all=BUGID" Download all attachments on the given bug +.IP "--comment=COMMENT, -l COMMENT" +Add comment with attachment .SH \[oq]info\[cq] options diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index d210483f..4b4c37ea 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -414,6 +414,8 @@ def _setup_action_attach_parser(subparsers): default=[], help="Download the attachment with the given ID") p.add_argument("--getall", "--get-all", metavar="BUGID", action="append", default=[], help="Download all attachments on the given bug") + p.add_argument('-l', '--comment', '--long_desc', + help="Add comment with attachment") def _setup_action_login_parser(subparsers): @@ -1004,6 +1006,8 @@ def _do_set_attach(bz, opt, parser): kwargs["contenttype"] = opt.type if opt.type in ["text/x-patch"]: kwargs["ispatch"] = True + if opt.comment: + kwargs["comment"] = opt.comment desc = opt.desc or os.path.basename(fileobj.name) # Upload attachments From 693e47395a0caea4422546d414d55c9b82005561 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 16 Apr 2018 12:01:41 -0400 Subject: [PATCH 055/393] tox: Drop pypy targets I doubt this project will ever have issues with pypy, but my present motivation is its busted in travis --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5712b07b..41726f86 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,py35,py36,pypy,pypy3 +envlist = py27,py33,py34,py35,py36 [testenv] sitepackages = True From 1e766b4ba262f52adf22e2592ecdec9db5755835 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 16 Apr 2018 12:07:19 -0400 Subject: [PATCH 056/393] travis: Drop pypy as well --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c0a6b64e..70f7b330 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ python: - 3.4 - 3.5 - 3.6 - - pypy-5.3.1 install: - pip install tox From 00bfd08428fc0c44fc3c573f1cfc8c5b6603be6c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 16 Apr 2018 13:28:41 -0400 Subject: [PATCH 057/393] Drop bumpversion config It doesn't support python preferred dev version names easily, the app is dead upstream, and it isn't currently wired to work with our RPM spec file. And I already have a local version to deal with this for releases, so it's not too useful presently --- .bumpversion.cfg | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 2211d417..00000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[bumpversion] -commit = True -tag = True -current_version = 2.2.0-dev -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? -serialize = - {major}.{minor}.{patch}-{release} - {major}.{minor}.{patch} - -[bumpversion:file:bugzilla/apiversion.py] - -[bumpversion:part:release] -optional_value = gamma -values = - dev - gamma - From 5e33437cfa242cedc78c9f8510e725a41d6bd855 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 16 Apr 2018 13:27:58 -0400 Subject: [PATCH 058/393] apiversion: Set it back to 2.1.0 To fix rpm builds. If we want to have dev versions we need to come up with a complete solution --- bugzilla/apiversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 4e6e2c18..cf196cd7 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -7,5 +7,5 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -version = "2.2.0.dev0" +version = "2.1.0" __version__ = version From 9d250ce78ac010e291dfde016a7c20a0e79bd2c3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 26 Jun 2018 16:47:59 -0400 Subject: [PATCH 059/393] tests: Fix with latest beta-bugzilla.redhat.com --- tests/test_ro_functional.py | 5 ++--- tests/test_rw_functional.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 1b53f799..960e45d0 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -343,9 +343,8 @@ def testExtraFields(self): def testExternalBugsOutput(self): out = self.clicomm('query --bug_id 989253 ' '--outputformat="%{external_bugs}"') - expect = ("http://bugzilla.gnome.org/show_bug.cgi?id=703421\n" + - "External bug: https://bugs.launchpad.net/bugs/1203576") - assert expect in out + assert "bugzilla.gnome.org/show_bug.cgi?id=703421" in out + assert "External bug: https://bugs.launchpad.net/bugs/1203576" in out def testActiveComps(self): out = self.clicomm("info --components 'Virtualization Tools' " diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index fdc52b7b..2d7ab40d 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -437,10 +437,10 @@ def test07ModifyMisc(self): assert targetbug.target_milestone == "beta" assert targetbug.target_release == ["6.2"] tests.clicomm(targetcmd + - "--target_milestone rc --target_release 6.0", bz) + "--target_milestone rc --target_release 6.10", bz) targetbug.refresh() assert targetbug.target_milestone == "rc" - assert targetbug.target_release == ["6.0"] + assert targetbug.target_release == ["6.10"] # modify --priority # modify --severity From 38c66eb0575cacca9c9d890a34ff0e8a9381508c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 26 Jun 2018 17:14:14 -0400 Subject: [PATCH 060/393] tests: Fix attachment tests The test bugzilla instances delete a lot of attachments now, so create a new bug and upload our own attachments --- tests/test_rw_functional.py | 41 ++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 2d7ab40d..58f4ebbd 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -67,11 +67,7 @@ def test2(self): bz = Bugzilla(url=self.url, use_creds=False) assert bz.__class__ is self.bzclass - def test03NewBugBasic(self): - """ - Create a bug with minimal amount of fields, then close it - """ - bz = self.bzclass(url=self.url) + def _makebug(self, bz): component = "python-bugzilla" version = "rawhide" summary = ("python-bugzilla test basic bug %s" % @@ -84,21 +80,29 @@ def test03NewBugBasic(self): (component, version, summary), bz) assert len(newout.splitlines()) == 3 - bugid = int(newout.splitlines()[2]) bug = bz.getbug(bugid) - print("\nCreated bugid: %s" % bugid) - - # Verify hasattr works - assert hasattr(bug, "id") - assert hasattr(bug, "bug_id") + print("\nCreated bugid: %s" % bug.id) assert bug.component == component assert bug.version == version assert bug.summary == summary + return bug + + def test03NewBugBasic(self): + """ + Create a bug with minimal amount of fields, then close it + """ + bz = self.bzclass(url=self.url) + bug = self._makebug(bz) + + # Verify hasattr works + assert hasattr(bug, "id") + assert hasattr(bug, "bug_id") + # Close the bug - tests.clicomm("bugzilla modify --close NOTABUG %s" % bugid, bz) + tests.clicomm("bugzilla modify --close NOTABUG %s" % bug.id, bz) bug.refresh() assert bug.status == "CLOSED" assert bug.resolution == "NOTABUG" @@ -498,24 +502,23 @@ def _test8Attachments(self): Get and set attachments for a bug """ bz = self.bzclass(url=self.url) - getallbugid = "663674" - setbugid = "461686" cmd = "bugzilla attach " testfile = "../tests/data/bz-attach-get1.txt" # Add attachment as CLI option - setbug = bz.getbug(setbugid, extra_fields=["attachments"]) + setbug = self._makebug(bz) + setbug = bz.getbug(setbug.id, extra_fields=["attachments"]) orignumattach = len(setbug.attachments) # Add attachment from CLI with mime guessing desc1 = "python-bugzilla cli upload %s" % datetime.datetime.today() out1 = tests.clicomm(cmd + "%s --description \"%s\" --file %s" % - (setbugid, desc1, testfile), bz, + (setbug.id, desc1, testfile), bz, stdin=open("/dev/tty", "rb")) desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() out2 = tests.clicomm(cmd + "%s --file test --summary \"%s\"" % - (setbugid, desc2), bz, stdin=open(testfile)) + (setbug.id, desc2), bz, stdin=open(testfile)) # Expected output format: # Created attachment on bug @@ -559,10 +562,10 @@ def _test8Attachments(self): os.unlink(fname) # Get all attachments - getbug = bz.getbug(getallbugid) + getbug = bz.getbug(setbug.id) getbug.autorefresh = True numattach = len(getbug.attachments) - out = tests.clicomm(cmd + "--getall %s" % getallbugid, bz).splitlines() + out = tests.clicomm(cmd + "--getall %s" % getbug.id, bz).splitlines() assert len(out) == (numattach + 2) fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] From 82c8e99dbe022fd08945a954a05fa624e44d1cb6 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Aug 2018 13:44:04 -0400 Subject: [PATCH 061/393] cli: Debug log bugzilla module path --- bugzilla/_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 4b4c37ea..82eddcef 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1083,6 +1083,7 @@ def _main(unittest_bz_instance): setup_logging(opt.debug, opt.verbose) log.debug("Launched with command line: %s", " ".join(sys.argv)) + log.debug("Bugzilla module: %s", bugzilla) # Connect to bugzilla log.info('Connecting to %s', opt.bugzilla) From 9dbb4b2420ca8da3105d6415e7715888e0a48ddd Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Aug 2018 13:46:43 -0400 Subject: [PATCH 062/393] transport: Remove request response debugging Too noisy --- bugzilla/transport.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 0c3e47ba..97b34ed6 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -169,7 +169,6 @@ def _request_helper(self, url, request_body): # Save is required only if we have a filename self._cookiejar.save() - log.debug(response.text) response.raise_for_status() return self.parse_response(response) except requests.RequestException as e: From 1fbc3e21659f7c00c0e3986aec3c0f20cbdf99d7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Aug 2018 13:50:36 -0400 Subject: [PATCH 063/393] transport: Centralize XMLRPC method+args logging --- bugzilla/base.py | 13 ------------- bugzilla/transport.py | 2 ++ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 88528e6a..c0d827e4 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -748,7 +748,6 @@ def product_get(self, ids=None, names=None, if exclude_fields: kwargs["exclude_fields"] = exclude_fields - log.debug("Calling Product.get with: %s", kwargs) ret = self._proxy.Product.get(kwargs) return ret['products'] @@ -874,7 +873,6 @@ def getcomponents(self, product, force_refresh=False): product_id = proddict["id"] opts = {'product_id': product_id, 'field': 'component'} - log.debug("Calling Bug.legal_values with: %s", opts) names = self._proxy.Bug.legal_values(opts)["values"] self._cache.component_names[product_id] = names @@ -926,7 +924,6 @@ def addcomponent(self, data): ''' data = data.copy() self._component_data_convert(data) - log.debug("Calling Component.create with: %s", data) return self._proxy.Component.create(data) def editcomponent(self, data): @@ -938,7 +935,6 @@ def editcomponent(self, data): ''' data = data.copy() self._component_data_convert(data, update=True) - log.debug("Calling Component.update with: %s", data) return self._proxy.Component.update(data) @@ -1023,7 +1019,6 @@ def _getbugs(self, idlist, permissive, getbugdata.update(self._process_include_fields( include_fields, exclude_fields, extra_fields)) - log.debug("Calling Bug.get with: %s", getbugdata) r = self._proxy.Bug.get(getbugdata) if self._check_version(4, 0): @@ -1251,7 +1246,6 @@ def query(self, query): Also see the _query() method for details about the underlying implementation. ''' - log.debug("Calling Bug.search with: %s", query) try: r = self._proxy.Bug.search(query) except Fault as e: @@ -1307,7 +1301,6 @@ def update_bugs(self, ids, updates): tmp = updates.copy() tmp["ids"] = self._listify(ids) - log.debug("Calling Bug.update with: %s", tmp) return self._proxy.Bug.update(tmp) def update_tags(self, idlist, tags_add=None, tags_remove=None): @@ -1325,7 +1318,6 @@ def update_tags(self, idlist, tags_add=None, tags_remove=None): "tags": tags, } - log.debug("Calling Bug.update_tags with: %s", d) return self._proxy.Bug.update_tags(d) def update_flags(self, idlist, flags): @@ -1592,7 +1584,6 @@ def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): flags.update(kwargs) update = {'ids': [int(attachid)], 'flags': [flags]} - log.debug("Calling Bug.update_attachment(%s)", update) return self._proxy.Bug.update_attachment(update) def get_attachments(self, ids, attachment_ids, @@ -1614,7 +1605,6 @@ def get_attachments(self, ids, attachment_ids, if exclude_fields: params["exclude_fields"] = self._listify(exclude_fields) - log.debug("Calling Bug.attachments(%s)", params) return self._proxy.Bug.attachments(params) @@ -1727,7 +1717,6 @@ def createbug(self, *args, **kwargs): be passed. ''' data = self._validate_createbug(*args, **kwargs) - log.debug("Calling Bug.create with: %s", data) rawbug = self._proxy.Bug.create(data) return Bug(self, bug_id=rawbug["id"], autorefresh=self.bug_autorefresh) @@ -1764,7 +1753,6 @@ def _getusers(self, ids=None, names=None, match=None): raise BugzillaError('_get() needs one of ids, ' ' names, or match kwarg.') - log.debug("Calling User.get with: %s", params) return self._proxy.User.get(params) def getuser(self, username): @@ -1845,5 +1833,4 @@ def updateperms(self, user, action, groups): } } - log.debug("Call User.update with: %s", update) return self._proxy.User.update(update) diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 97b34ed6..e3f7c8a8 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -88,6 +88,8 @@ def _ServerProxy__request(self, methodname, params): if len(params) == 0: params = ({}, ) + log.debug("XMLRPC call: %s(%s)", methodname, params[0]) + if self.api_key is not None: if 'Bugzilla_api_key' not in params[0]: params[0]['Bugzilla_api_key'] = self.api_key From c8ad5589f6a4acffe07b696c546135e6da47f141 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Aug 2018 16:36:22 -0400 Subject: [PATCH 064/393] Standardize on """ for docstrings And use consistent newlines --- bugzilla/_cli.py | 6 +- bugzilla/base.py | 150 +++++++++++++++++++++--------------- bugzilla/bug.py | 64 ++++++++------- bugzilla/rhbugzilla.py | 12 +-- bugzilla/transport.py | 8 +- tests/test_bug.py | 4 +- tests/test_createbug.py | 4 +- tests/test_misc.py | 4 +- tests/test_modify.py | 4 +- tests/test_query.py | 4 +- tests/test_ro_functional.py | 4 +- tests/test_rw_functional.py | 4 +- 12 files changed, 154 insertions(+), 114 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 82eddcef..0158625d 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -74,8 +74,10 @@ def to_encoding(ustring): def open_without_clobber(name, *args): - '''Try to open the given file with the given mode; if that filename exists, - try "name.1", "name.2", etc. until we find an unused filename.''' + """ + Try to open the given file with the given mode; if that filename exists, + try "name.1", "name.2", etc. until we find an unused filename. + """ fd = None count = 1 orig_name = name diff --git a/bugzilla/base.py b/bugzilla/base.py index c0d827e4..62ca3090 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -205,10 +205,10 @@ class Bugzilla(object): @staticmethod def url_to_query(url): - ''' + """ Given a big huge bugzilla query URL, returns a query dict that can be passed along to the Bugzilla.query() method. - ''' + """ q = {} # pylint: disable=unpacking-non-sequence @@ -421,9 +421,10 @@ def _get_api_aliases(self): ################### def _getcookiefile(self): - '''cookiefile is the file that bugzilla session cookies are loaded + """ + cookiefile is the file that bugzilla session cookies are loaded and saved from. - ''' + """ return self._cookiejar.filename def _delcookiefile(self): @@ -521,7 +522,7 @@ def _set_bz_version(self, version): self.bz_ver_minor = 0 def connect(self, url=None): - ''' + """ Connect to the bugzilla instance with the given url. This is called by __init__ if a URL is passed. Or it can be called manually at any time with a passed URL. @@ -531,7 +532,7 @@ def connect(self, url=None): If 'user' and 'password' are both set, we'll run login(). Otherwise you'll have to login() yourself before some methods will work. - ''' + """ if self._transport: self.disconnect() @@ -562,24 +563,29 @@ def connect(self, url=None): self._set_bz_version(version) def disconnect(self): - ''' + """ Disconnect from the given bugzilla instance. - ''' + """ self._proxy = None self._transport = None self._cache = _BugzillaAPICache() def _login(self, user, password): - '''Backend login method for Bugzilla3''' + """ + Backend login method for Bugzilla3 + """ return self._proxy.User.login({'login': user, 'password': password}) def _logout(self): - '''Backend login method for Bugzilla3''' + """ + Backend login method for Bugzilla3 + """ return self._proxy.User.logout() def login(self, user=None, password=None): - '''Attempt to log in using the given username and password. Subsequent + """ + Attempt to log in using the given username and password. Subsequent method calls will use this username and password. Returns False if login fails, otherwise returns some kind of login info - typically either a numeric userid, or a dict of user info. @@ -591,7 +597,7 @@ def login(self, user=None, password=None): This method will be called implicitly at the end of connect() if user and password are both set. So under most circumstances you won't need to call this yourself. - ''' + """ if self.api_key: raise ValueError("cannot login when using an API key") @@ -636,8 +642,10 @@ def interactive_login(self, user=None, password=None, force=False): log.info('Authorization cookie received.') def logout(self): - '''Log out of bugzilla. Drops server connection and user info, and - destroys authentication cookies.''' + """ + Log out of bugzilla. Drops server connection and user info, and + destroys authentication cookies. + """ self._logout() self.disconnect() self.user = '' @@ -676,18 +684,18 @@ def logged_in(self): ###################### def _getbugfields(self): - ''' + """ Get the list of valid fields for Bug objects - ''' + """ r = self._proxy.Bug.fields({'include_fields': ['name']}) return [f['name'] for f in r['fields']] def getbugfields(self, force_refresh=False): - ''' + """ Calls getBugFields, which returns a list of fields in each bug for this bugzilla instance. This can be used to set the list of attrs on the Bug object. - ''' + """ if force_refresh or not self._cache.bugfields: log.debug("Refreshing bugfields") self._cache.bugfields = self._getbugfields() @@ -906,7 +914,7 @@ def _component_data_convert(self, data, update=False): def addcomponent(self, data): - ''' + """ A method to create a component in Bugzilla. Takes a dict, with the following elements: @@ -921,18 +929,18 @@ def addcomponent(self, data): new bugs for the component. is_active: (optional) If False, the component is hidden from the component list when filing new bugs. - ''' + """ data = data.copy() self._component_data_convert(data) return self._proxy.Component.create(data) def editcomponent(self, data): - ''' + """ A method to edit a component in Bugzilla. Takes a dict, with mandatory elements of product. component, and initialowner. All other elements are optional and use the same names as the addcomponent() method. - ''' + """ data = data.copy() self._component_data_convert(data, update=True) return self._proxy.Component.update(data) @@ -996,10 +1004,10 @@ def _set_bug_autorefresh(self, val): def _getbugs(self, idlist, permissive, include_fields=None, exclude_fields=None, extra_fields=None): - ''' + """ Return a list of dicts of full bug info for each given bug id. bug ids that couldn't be found will return None instead of a dict. - ''' + """ oldidlist = idlist idlist = [] for i in oldidlist: @@ -1055,8 +1063,10 @@ def _getbug(self, objid, **kwargs): def getbug(self, objid, include_fields=None, exclude_fields=None, extra_fields=None): - '''Return a Bug object with the full complement of bug data - already loaded.''' + """ + Return a Bug object with the full complement of bug data + already loaded. + """ data = self._getbug(objid, include_fields=include_fields, exclude_fields=exclude_fields, extra_fields=extra_fields) @@ -1065,9 +1075,11 @@ def getbug(self, objid, def getbugs(self, idlist, include_fields=None, exclude_fields=None, extra_fields=None, permissive=True): - '''Return a list of Bug objects with the full complement of bug data + """ + Return a list of Bug objects with the full complement of bug data already loaded. If there's a problem getting the data for a given id, - the corresponding item in the returned list will be None.''' + the corresponding item in the returned list will be None. + """ data = self._getbugs(idlist, include_fields=include_fields, exclude_fields=exclude_fields, extra_fields=extra_fields, permissive=permissive) @@ -1076,8 +1088,10 @@ def getbugs(self, idlist, for b in data] def get_comments(self, idlist): - '''Returns a dictionary of bugs and comments. The comments key will - be empty. See bugzilla docs for details''' + """ + Returns a dictionary of bugs and comments. The comments key will + be empty. See bugzilla docs for details + """ return self._proxy.Bug.comments({'ids': idlist}) @@ -1240,12 +1254,13 @@ def add_email(key, value, count): return query def query(self, query): - '''Query bugzilla and return a list of matching bugs. + """ + Query bugzilla and return a list of matching bugs. query must be a dict with fields like those in in querydata['fields']. Returns a list of Bug objects. Also see the _query() method for details about the underlying implementation. - ''' + """ try: r = self._proxy.Bug.search(query) except Fault as e: @@ -1265,22 +1280,24 @@ def query(self, query): autorefresh=self.bug_autorefresh) for b in r['bugs']] def pre_translation(self, query): - '''In order to keep the API the same, Bugzilla4 needs to process the + """ + In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function - ''' + """ pass def post_translation(self, query, bug): - '''In order to keep the API the same, Bugzilla4 needs to process the + """ + In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function - ''' + """ pass def bugs_history_raw(self, bug_ids): - ''' + """ Experimental. Gets the history of changes for particular bugs in the database. - ''' + """ return self._proxy.Bug.history({'ids': bug_ids}) @@ -1304,9 +1321,9 @@ def update_bugs(self, ids, updates): return self._proxy.Bug.update(tmp) def update_tags(self, idlist, tags_add=None, tags_remove=None): - ''' + """ Updates the 'tags' field for a bug. - ''' + """ tags = {} if tags_add: tags["add"] = self._listify(tags_add) @@ -1477,13 +1494,15 @@ def c(val): ######################################## def _attachment_uri(self, attachid): - '''Returns the URI for the given attachment ID.''' + """ + Returns the URI for the given attachment ID. + """ att_uri = self.url.replace('xmlrpc.cgi', 'attachment.cgi') att_uri = att_uri + '?id=%s' % attachid return att_uri def attachfile(self, idlist, attachfile, description, **kwargs): - ''' + """ Attach a file to the given bug IDs. Returns the ID of the attachment or raises XMLRPC Fault if something goes wrong. @@ -1507,7 +1526,7 @@ def attachfile(self, idlist, attachfile, description, **kwargs): Returns the list of attachment ids that were added. If only one attachment was added, we return the single int ID for back compat - ''' + """ if isinstance(attachfile, str): f = open(attachfile, "rb") elif hasattr(attachfile, 'read'): @@ -1557,8 +1576,10 @@ def attachfile(self, idlist, attachfile, description, **kwargs): def openattachment(self, attachid): - '''Get the contents of the attachment with the given attachment ID. - Returns a file-like object.''' + """ + Get the contents of the attachment with the given attachment ID. + Returns a file-like object. + """ attachments = self.get_attachments(None, attachid) data = attachments["attachments"][str(attachid)] xmlrpcbinary = data["data"] @@ -1570,12 +1591,12 @@ def openattachment(self, attachid): return ret def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): - ''' + """ Updates a flag for the given attachment ID. Optional keyword args are: status: new status for the flag ('-', '+', '?', 'X') requestee: new requestee for the flag - ''' + """ # Bug ID was used for the original custom redhat API, no longer # needed though ignore = bugid @@ -1641,7 +1662,7 @@ def build_createbug(self, sub_component=None, alias=None, comment_tags=None): - """" + """ Returns a python dict() with properly formatted parameters to pass to createbug(). See bugzilla documentation for the format of the individual fields: @@ -1710,12 +1731,12 @@ def _validate_createbug(self, *args, **kwargs): return data def createbug(self, *args, **kwargs): - ''' + """ Create a bug with the given info. Returns a new Bug object. Check bugzilla API documentation for valid values, at least product, component, summary, version, and description need to be passed. - ''' + """ data = self._validate_createbug(*args, **kwargs) rawbug = self._proxy.Bug.create(data) return Bug(self, bug_id=rawbug["id"], @@ -1727,7 +1748,8 @@ def createbug(self, *args, **kwargs): ############################## def _getusers(self, ids=None, names=None, match=None): - '''Return a list of users that match criteria. + """ + Return a list of users that match criteria. :kwarg ids: list of user ids to return data on :kwarg names: list of user names to return data on @@ -1741,7 +1763,7 @@ def _getusers(self, ids=None, names=None, match=None): parameter. Available in Bugzilla-3.4+ - ''' + """ params = {} if ids: params['ids'] = self._listify(ids) @@ -1756,21 +1778,23 @@ def _getusers(self, ids=None, names=None, match=None): return self._proxy.User.get(params) def getuser(self, username): - '''Return a bugzilla User for the given username + """ + Return a bugzilla User for the given username :arg username: The username used in bugzilla. :raises XMLRPC Fault: Code 51 if the username does not exist :returns: User record for the username - ''' + """ ret = self.getusers(username) return ret and ret[0] def getusers(self, userlist): - '''Return a list of Users from . + """ + Return a list of Users from . :userlist: List of usernames to lookup :returns: List of User records - ''' + """ userobjs = [User(self, **rawuser) for rawuser in self._getusers(names=userlist).get('users', [])] @@ -1787,16 +1811,18 @@ def getusers(self, userlist): def searchusers(self, pattern): - '''Return a bugzilla User for the given list of patterns + """ + Return a bugzilla User for the given list of patterns :arg pattern: List of patterns to match against. :returns: List of User records - ''' + """ return [User(self, **rawuser) for rawuser in self._getusers(match=pattern).get('users', [])] def createuser(self, email, name='', password=''): - '''Return a bugzilla User for the given username + """ + Return a bugzilla User for the given username :arg email: The email address to use in bugzilla :kwarg name: Real name to associate with the account @@ -1806,12 +1832,12 @@ def createuser(self, email, name='', password=''): Code 502 if the password is too short Code 503 if the password is too long :return: User record for the username - ''' + """ self._proxy.User.create(email, name, password) return self.getuser(email) def updateperms(self, user, action, groups): - ''' + """ A method to update the permissions (group membership) of a bugzilla user. @@ -1819,7 +1845,7 @@ def updateperms(self, user, action, groups): also be a list of emails. :arg action: add, remove, or set :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) - ''' + """ groups = self._listify(groups) if action == "rem": action = "remove" diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 366a8c12..d5b581d9 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -18,14 +18,15 @@ class Bug(object): - '''A container object for a bug report. Requires a Bugzilla instance - + """ + A container object for a bug report. Requires a Bugzilla instance - every Bug is on a Bugzilla, obviously. Optional keyword args: dict=DICT - populate attributes with the result of a getBug() call bug_id=ID - if dict does not contain bug_id, this is required before you can read any attributes or make modifications to this bug. - ''' + """ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): # pylint: disable=redefined-builtin # API had pre-existing issue that we can't change ('dict' usage) @@ -46,12 +47,13 @@ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): 'show_bug.cgi?id=%i' % self.bug_id) def __str__(self): - '''Return a simple string representation of this bug + """ + Return a simple string representation of this bug This is available only for compatibility. Using 'str(bug)' and 'print(bug)' is not recommended because of potential encoding issues. Please use unicode(bug) where possible. - ''' + """ if sys.version_info[0] >= 3: return self.__unicode__() else: @@ -59,7 +61,9 @@ def __str__(self): locale.getpreferredencoding(), 'replace') def __unicode__(self): - '''Return a simple unicode string representation of this bug''' + """ + Return a simple unicode string representation of this bug + """ return "#%-6s %-10s - %s - %s" % (self.bug_id, self.bug_status, self.assigned_to, self.summary) @@ -112,9 +116,9 @@ def __getattr__(self, name): def refresh(self, include_fields=None, exclude_fields=None, extra_fields=None): - ''' + """ Refresh the bug with the latest data from bugzilla - ''' + """ # pylint: disable=protected-access r = self.bugzilla._getbug(self.bug_id, include_fields=include_fields, exclude_fields=exclude_fields, @@ -124,10 +128,10 @@ def refresh(self, include_fields=None, exclude_fields=None, reload = refresh def _update_dict(self, newdict): - ''' + """ Update internal dictionary, in a way that ensures no duplicate entries are stored WRT field aliases - ''' + """ if self.bugzilla: self.bugzilla.post_translation({}, newdict) @@ -178,12 +182,12 @@ def __setstate__(self, vals): ##################### def setstatus(self, status, comment=None, private=False): - ''' + """ Update the status for this bug report. Commonly-used values are ASSIGNED, MODIFIED, and NEEDINFO. To change bugs to CLOSED, use .close() instead. - ''' + """ # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(status=status, comment=comment, @@ -194,7 +198,8 @@ def setstatus(self, status, comment=None, private=False): def close(self, resolution, dupeid=None, fixedin=None, comment=None, isprivate=False): - '''Close this bug. + """ + Close this bug. Valid values for resolution are in bz.querydefaults['resolution_list'] For bugzilla.redhat.com that's: ['NOTABUG', 'WONTFIX', 'DEFERRED', 'WORKSFORME', 'CURRENTRELEASE', @@ -206,7 +211,7 @@ def close(self, resolution, dupeid=None, fixedin=None, version that fixes the bug. You can optionally add a comment while closing the bug. Set 'isprivate' to True if you want that comment to be private. - ''' + """ # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(comment=comment, comment_private=isprivate, @@ -225,7 +230,7 @@ def close(self, resolution, dupeid=None, fixedin=None, def setassignee(self, assigned_to=None, qa_contact=None, comment=None): - ''' + """ Set any of the assigned_to or qa_contact fields to a new bugzilla account, with an optional comment, e.g. setassignee(assigned_to='wwoods@redhat.com') @@ -235,7 +240,7 @@ def setassignee(self, assigned_to=None, will throw a ValueError. Returns [bug_id, mailresults]. - ''' + """ if not (assigned_to or qa_contact): raise ValueError("You must set one of assigned_to " " or qa_contact") @@ -248,11 +253,11 @@ def setassignee(self, assigned_to=None, return self.bugzilla.update_bugs(self.bug_id, vals) def addcc(self, cclist, comment=None): - ''' + """ Adds the given email addresses to the CC list for this bug. cclist: list of email addresses (strings) comment: optional comment to add to the bug - ''' + """ vals = self.bugzilla.build_update(comment=comment, cc_add=cclist) log.debug("addcc: update=%s", vals) @@ -260,9 +265,9 @@ def addcc(self, cclist, comment=None): return self.bugzilla.update_bugs(self.bug_id, vals) def deletecc(self, cclist, comment=None): - ''' + """ Removes the given email addresses from the CC list for this bug. - ''' + """ vals = self.bugzilla.build_update(comment=comment, cc_remove=cclist) log.debug("deletecc: update=%s", vals) @@ -275,10 +280,10 @@ def deletecc(self, cclist, comment=None): #################### def addcomment(self, comment, private=False): - ''' + """ Add the given comment to this bug. Set private to True to mark this comment as private. - ''' + """ # Note: fedora bodhi uses this function vals = self.bugzilla.build_update(comment=comment, comment_private=private) @@ -287,9 +292,9 @@ def addcomment(self, comment, private=False): return self.bugzilla.update_bugs(self.bug_id, vals) def getcomments(self): - ''' + """ Returns an array of comment dictionaries for this bug - ''' + """ comment_list = self.bugzilla.get_comments([self.bug_id]) return comment_list['bugs'][str(self.bug_id)]['comments'] @@ -377,18 +382,19 @@ def get_attachment_ids(self): return [a["id"] for a in self.get_attachments(exclude_fields=["data"])] def get_history_raw(self): - ''' + """ Experimental. Get the history of changes for this bug. - ''' + """ return self.bugzilla.bugs_history_raw([self.bug_id]) class User(object): - '''Container object for a bugzilla User. + """ + Container object for a bugzilla User. :arg bugzilla: Bugzilla instance that this User belongs to. Rest of the params come straight from User.get() - ''' + """ def __init__(self, bugzilla, **kwargs): self.bugzilla = bugzilla self.__userid = kwargs.get('id') @@ -440,11 +446,11 @@ def refresh(self): self.__dict__.update(newuser.__dict__) def updateperms(self, action, groups): - ''' + """ A method to update the permissions (group membership) of a bugzilla user. :arg action: add, remove, or set :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) - ''' + """ self.bugzilla.updateperms(self.name, action, groups) diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 55ee601b..b82365ee 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -17,7 +17,7 @@ class RHBugzilla(Bugzilla): - ''' + """ Bugzilla class for connecting Red Hat's forked bugzilla instance, bugzilla.redhat.com @@ -30,7 +30,7 @@ class RHBugzilla(Bugzilla): This class was written using bugzilla.redhat.com's API docs: https://bugzilla.redhat.com/docs/en/html/api/ - ''' + """ def _init_class_state(self): def _add_both_alias(newname, origname): self._add_field_alias(newname, origname, is_api=False) @@ -250,7 +250,9 @@ def remove_external_tracker(self, ids=None, ext_type_id=None, ################# def pre_translation(self, query): - '''Translates the query for possible aliases''' + """ + Translates the query for possible aliases + """ old = query.copy() if 'bug_id' in query: @@ -282,10 +284,10 @@ def pre_translation(self, query): log.debug("RHBugzilla pretranslated query to: %s", query) def post_translation(self, query, bug): - ''' + """ Convert the results of getbug back to the ancient RHBZ value formats - ''' + """ ignore = query # RHBZ _still_ returns component and version as lists, which diff --git a/bugzilla/transport.py b/bugzilla/transport.py index e3f7c8a8..7330b861 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -25,7 +25,9 @@ class BugzillaError(Exception): - '''Error raised in the Bugzilla client code.''' + """ + Error raised in the Bugzilla client code. + """ pass @@ -143,7 +145,9 @@ def __init__(self, url, cookiejar=None, self.session.cert = cert def parse_response(self, response): - """ Parse XMLRPC response """ + """ + Parse XMLRPC response + """ parser, unmarshaller = self.getparser() parser.feed(response.text.encode('utf-8')) parser.close() diff --git a/tests/test_bug.py b/tests/test_bug.py index 774afe94..0f28b1ca 100644 --- a/tests/test_bug.py +++ b/tests/test_bug.py @@ -5,9 +5,9 @@ # See the COPYING file in the top-level directory. # -''' +""" Unit tests for testing some bug.py magic -''' +""" import pickle import sys diff --git a/tests/test_createbug.py b/tests/test_createbug.py index 9cb46dda..23b195b1 100644 --- a/tests/test_createbug.py +++ b/tests/test_createbug.py @@ -5,9 +5,9 @@ # See the COPYING file in the top-level directory. # -''' +""" Unit tests for building createbug dictionaries with bin/bugzilla -''' +""" import unittest diff --git a/tests/test_misc.py b/tests/test_misc.py index 1a5c7c64..75110d21 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -5,9 +5,9 @@ # See the COPYING file in the top-level directory. # -''' +""" Unit tests for building query strings with bin/bugzilla -''' +""" from __future__ import print_function diff --git a/tests/test_modify.py b/tests/test_modify.py index 983b08e6..09f8589d 100644 --- a/tests/test_modify.py +++ b/tests/test_modify.py @@ -5,9 +5,9 @@ # See the COPYING file in the top-level directory. # -''' +""" Unit tests for building update dictionaries with 'bugzilla modify' -''' +""" import unittest diff --git a/tests/test_query.py b/tests/test_query.py index e3d61327..078254c5 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -5,9 +5,9 @@ # See the COPYING file in the top-level directory. # -''' +""" Unit tests for building query strings with bin/bugzilla -''' +""" import copy import os diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 960e45d0..680c38cc 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -7,9 +7,9 @@ # See the COPYING file in the top-level directory. # -''' +""" Unit tests that do readonly functional tests against real bugzilla instances. -''' +""" import os import unittest diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 58f4ebbd..ae35c94d 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -5,9 +5,9 @@ # See the COPYING file in the top-level directory. # -''' +""" Unit tests that do permanent functional against a real bugzilla instances. -''' +""" from __future__ import print_function From dcb5728638c377829d049de508cf76fb37ea87e9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Aug 2018 10:12:47 -0400 Subject: [PATCH 065/393] Prep for release 2.2.0 --- NEWS.md | 8 ++++++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 012156ec..9bc642ab 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # python-bugzilla release news +## Release 2.2.0 (August 11, 2018) +- Port tests to pytest +- cli: --cert Client side certificate support (Tobias Wolter) +- cli: add ability to post comment while sending attachment (Jeff Mahoney) +- cli: Add --comment-tag option +- cli: Add info --active-components +- Add a raw Product.get wrapper API + ## Release 2.1.0 (March 30, 2017) - Support for bugzilla 5 API Keys (Dustin J. Mitchell) - bugzillarc can be used to set default URL for the cli tool diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index cf196cd7..9762d4ae 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -7,5 +7,5 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -version = "2.1.0" +version = "2.2.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 9384a69c..61c877d2 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -15,7 +15,7 @@ %endif Name: python-bugzilla -Version: 2.1.0 +Version: 2.2.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From 365d4c79a26127f30ce23c59358728b26ec7fcea Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Aug 2018 10:39:49 -0400 Subject: [PATCH 066/393] spec: Fix pytest invocation on RHEL7 --- python-bugzilla.spec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 61c877d2..9fb39170 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -144,7 +144,8 @@ done %check %if %{with python2} -pytest +# py.test naming is needed for RHEL7 compat, works fine with Fedora +py.test %endif # with python2 %if %{with python3} pytest-3 From 584e1919e086e699ce8b4fd82631356dd5679b11 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Aug 2018 13:46:01 -0400 Subject: [PATCH 067/393] tests: Simplify attachment test syntax a bit --- tests/test_rw_functional.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index ae35c94d..8c9399a0 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -525,18 +525,21 @@ def _test8Attachments(self): setbug.refresh() assert len(setbug.attachments) == (orignumattach + 2) - assert setbug.attachments[-2]["summary"] == desc1 - assert (setbug.attachments[-2]["id"] == - int(out1.splitlines()[2].split()[2])) - assert setbug.attachments[-1]["summary"] == desc2 - assert (setbug.attachments[-1]["id"] == - int(out2.splitlines()[2].split()[2])) - attachid = setbug.attachments[-2]["id"] + + att1 = setbug.attachments[-2] + attachid = att1["id"] + assert att1["summary"] == desc1 + assert att1["id"] == int(out1.splitlines()[2].split()[2]) + assert att1["content_type"] == "application/octet-stream" + + att2 = setbug.attachments[-1] + assert att2["summary"] == desc2 + assert att2["id"] == int(out2.splitlines()[2].split()[2]) + assert att2["content_type"] == "text/x-diff" # Set attachment flags - assert setbug.attachments[-1]["flags"] == [] - bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], - "review", status="+") + assert att1["flags"] == [] + bz.updateattachmentflags(setbug.id, att2["id"], "review", status="+") setbug.refresh() assert len(setbug.attachments[-1]["flags"]) == 1 From 5dcfeb60d85ef58a3ca22bf9b6f005df2ad5613e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Aug 2018 13:48:58 -0400 Subject: [PATCH 068/393] base: Drop python-magic usage, just check file extension python-magic is overkill, bugzilla attachment autodetect doesn't even inspect files and just checks file extensions. Simplify by switching to that method. Plus it's a bit of a pain to manage the conditional dependency --- bugzilla/base.py | 37 ++++++------------------------------- python-bugzilla.spec | 2 -- tests/test_rw_functional.py | 4 ++-- 3 files changed, 8 insertions(+), 35 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 62ca3090..229ea32d 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -13,6 +13,7 @@ import getpass import locale from logging import getLogger +import mimetypes import os import sys @@ -40,33 +41,6 @@ log = getLogger(__name__) -mimemagic = None - - -def _detect_filetype(fname): - global mimemagic - - if mimemagic is None: - try: - # pylint: disable=import-error - import magic - mimemagic = magic.open(getattr(magic, "MAGIC_MIME_TYPE", 16)) - mimemagic.load() - except ImportError as e: - log.debug("Could not load python-magic: %s", e) - mimemagic = None - if not mimemagic: - return None - - if not os.path.isabs(fname): - return None - - try: - return mimemagic.file(fname) - except Exception as e: - log.debug("Could not detect content_type: %s", e) - return None - def _nested_update(d, u): # Helper for nested dict update() @@ -1556,10 +1530,11 @@ def attachfile(self, idlist, attachfile, description, **kwargs): if 'file_name' not in kwargs and hasattr(f, "name"): kwargs['file_name'] = os.path.basename(f.name) if 'content_type' not in kwargs: - ctype = _detect_filetype(getattr(f, "name", None)) - if not ctype: - ctype = 'application/octet-stream' - kwargs['content_type'] = ctype + ctype = None + if kwargs['file_name']: + ctype = mimetypes.guess_type( + kwargs['file_name'], strict=False)[0] + kwargs['content_type'] = ctype or 'application/octet-stream' ret = self._proxy.Bug.add_attachment(kwargs) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 9fb39170..a029cbd7 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -49,7 +49,6 @@ over XML-RPC.\ %package -n python2-bugzilla Summary: %summary Requires: python2-requests -Requires: python2-magic # This dep is for back compat, so that installing python-bugzilla continues # to give the cli tool Requires: python-bugzilla-cli @@ -64,7 +63,6 @@ Requires: python-bugzilla-cli %package -n python3-bugzilla Summary: %summary Requires: python3-requests -Requires: python3-magic %{?python_provide:%python_provide python3-bugzilla} %if %{without python2} diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 8c9399a0..64a059a2 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -530,12 +530,12 @@ def _test8Attachments(self): attachid = att1["id"] assert att1["summary"] == desc1 assert att1["id"] == int(out1.splitlines()[2].split()[2]) - assert att1["content_type"] == "application/octet-stream" + assert att1["content_type"] == "text/plain" att2 = setbug.attachments[-1] assert att2["summary"] == desc2 assert att2["id"] == int(out2.splitlines()[2].split()[2]) - assert att2["content_type"] == "text/x-diff" + assert att2["content_type"] == "application/octet-stream" # Set attachment flags assert att1["flags"] == [] From 5a0680b4ceb3efd7d71d20c4f2b57ce0331e364b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Aug 2018 14:38:10 -0400 Subject: [PATCH 069/393] Drop py3.3 support, minimum is 3.4 tox/virtualenv says its only supporting 3.4 in travis, which seems to be the trend with most other libs, so drop 3.3 support --- .travis.yml | 1 - setup.py | 2 +- tox.ini | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 70f7b330..5d9c3d36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python python: - 2.7 - - 3.3 - 3.4 - 3.5 - 3.6 diff --git a/setup.py b/setup.py index ce3c4902..5ff82f51 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def unsupported_python_version(): return sys.version_info < (2, 7) \ - or (sys.version_info > (3,) and sys.version_info < (3, 3)) + or (sys.version_info > (3,) and sys.version_info < (3, 4)) if unsupported_python_version(): diff --git a/tox.ini b/tox.ini index 41726f86..2ac3715b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,py35,py36 +envlist = py27,py34,py35,py36 [testenv] sitepackages = True From d40de42d77ef828bdaed1439bfce509abb13199e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 4 Sep 2018 10:06:40 -0400 Subject: [PATCH 070/393] Fix SafeConfigParser deprecation warnings Closes: #79 --- bugzilla/base.py | 6 +++--- bugzilla/transport.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 229ea32d..20ebe796 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -22,12 +22,12 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: # pylint: disable=no-name-in-module - from configparser import SafeConfigParser + from configparser import ConfigParser from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse, parse_qsl from xmlrpc.client import Binary, Fault else: - from ConfigParser import SafeConfigParser + from ConfigParser import SafeConfigParser as ConfigParser from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse, parse_qsl from xmlrpclib import Binary, Fault @@ -105,7 +105,7 @@ def _open_bugzillarc(configpaths=-1): configpaths = [os.path.expanduser(p) for p in Bugzilla._listify(configpaths)] # pylint: enable=protected-access - cfg = SafeConfigParser() + cfg = ConfigParser() read_files = cfg.read(configpaths) if not read_files: return diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 7330b861..3dbb425a 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -9,11 +9,11 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: - from configparser import SafeConfigParser + from configparser import ConfigParser from urllib.parse import urlparse # pylint: disable=no-name-in-module from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport else: - from ConfigParser import SafeConfigParser + from ConfigParser import SafeConfigParser as ConfigParser from urlparse import urlparse from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport # pylint: enable=import-error @@ -39,7 +39,7 @@ class _BugzillaTokenCache(object): def __init__(self, uri, tokenfilename): self.tokenfilename = tokenfilename - self.tokenfile = SafeConfigParser() + self.tokenfile = ConfigParser() self.domain = urlparse(uri)[1] if self.tokenfilename: From 71360b380379037dc11aedee7038e6f326a7aaf2 Mon Sep 17 00:00:00 2001 From: Viliam Krizan Date: Thu, 6 Sep 2018 10:46:48 +0200 Subject: [PATCH 071/393] base: Support restricted login --- bugzilla/base.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 20ebe796..c97cb4a2 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -544,12 +544,15 @@ def disconnect(self): self._transport = None self._cache = _BugzillaAPICache() - - def _login(self, user, password): + def _login(self, user, password, restrict_login=None): """ Backend login method for Bugzilla3 """ - return self._proxy.User.login({'login': user, 'password': password}) + payload = {'login': user, 'password': password} + if restrict_login: + payload['restrict_login'] = True + + return self._proxy.User.login(payload) def _logout(self): """ @@ -557,7 +560,7 @@ def _logout(self): """ return self._proxy.User.logout() - def login(self, user=None, password=None): + def login(self, user=None, password=None, restrict_login=None): """ Attempt to log in using the given username and password. Subsequent method calls will use this username and password. Returns False if @@ -568,6 +571,9 @@ def login(self, user=None, password=None): is not set, ValueError will be raised. If login fails, BugzillaError will be raised. + The login session can be restricted to current user IP address + with restrict_login argument. (Bugzilla 4.4+) + This method will be called implicitly at the end of connect() if user and password are both set. So under most circumstances you won't need to call this yourself. @@ -585,8 +591,11 @@ def login(self, user=None, password=None): if not self.password: raise ValueError("missing password") + if restrict_login: + log.info("logging in with restrict_login=True") + try: - ret = self._login(self.user, self.password) + ret = self._login(self.user, self.password, restrict_login) self.password = '' log.info("login successful for user=%s", self.user) return ret From b219612b85d6217e520f89d34b589776d21da9da Mon Sep 17 00:00:00 2001 From: Viliam Krizan Date: Thu, 6 Sep 2018 10:49:16 +0200 Subject: [PATCH 072/393] cli: Add option --restrict-login --- bugzilla/_cli.py | 6 +++++- bugzilla/base.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 0158625d..b151833a 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -152,6 +152,9 @@ def _setup_root_parser(): 'specified command.') p.add_argument('--username', help="Log in with this username") p.add_argument('--password', help="Log in with this password") + p.add_argument('--restrict-login', action="store_true", + help="The session (login token) will be restricted to " + "the current IP address.") p.add_argument('--ensure-logged-in', action="store_true", help="Raise an error if we aren't logged in to bugzilla. " @@ -1059,7 +1062,8 @@ def _handle_login(opt, action, bz): if do_interactive_login: if bz.url: print("Logging into %s" % urlparse(bz.url)[1]) - bz.interactive_login(username, password) + bz.interactive_login(username, password, + restrict_login=opt.restrict_login) except bugzilla.BugzillaError as e: print(str(e)) sys.exit(1) diff --git a/bugzilla/base.py b/bugzilla/base.py index c97cb4a2..702d76e8 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -602,13 +602,15 @@ def login(self, user=None, password=None, restrict_login=None): except Fault as e: raise BugzillaError("Login failed: %s" % str(e.faultString)) - def interactive_login(self, user=None, password=None, force=False): + def interactive_login(self, user=None, password=None, force=False, + restrict_login=None): """ Helper method to handle login for this bugzilla instance. :param user: bugzilla username. If not specified, prompt for it. :param password: bugzilla password. If not specified, prompt for it. :param force: Unused + :param restrict_login: restricts session to IP address """ ignore = force log.debug('Calling interactive_login') @@ -621,7 +623,7 @@ def interactive_login(self, user=None, password=None, force=False): password = getpass.getpass('Bugzilla Password: ') log.info('Logging in... ') - self.login(user, password) + self.login(user, password, restrict_login) log.info('Authorization cookie received.') def logout(self): From 17260364badc488b8125243e891a46bdf00f2a83 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 6 Sep 2018 16:00:40 -0400 Subject: [PATCH 073/393] man: Add --restrict-login --- bugzilla.1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bugzilla.1 b/bugzilla.1 index 1d05f631..49f05c44 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -40,6 +40,8 @@ Run interactive "login" before performing the specified command. Log in with this username .IP "--password=PASSWORD" Log in with this password +.IP "--restrict-login" +The session (login token) will be restricted to the current IP address. .IP "--ensure-logged-in" Raise an error if we aren't logged in to bugzilla. Consider using this if you are depending on cached credentials, to ensure that when they expire the tool errors, rather than subtly change output. .IP "--no-cache-credentials" From 8327d47f2821af8af31d5c380284227ef41ede96 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 11 Dec 2018 13:12:19 -0500 Subject: [PATCH 074/393] tests: bugzilla.redhat.com is now bugzilla5 --- tests/test_ro_functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 680c38cc..c0f28a29 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -211,7 +211,7 @@ class RHTest(BaseTest): url = (tests.CLICONFIG.REDHAT_URL or "https://bugzilla.redhat.com/xmlrpc.cgi") bzclass = RHBugzilla - bzversion = (4, 4) + bzversion = (5, 0) test0 = BaseTest._testBZVersion test01 = lambda s: BaseTest._testInfoProducts(s, 125, From 26f88eb34072929bdf199e0eb886d07874680ca0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Jan 2019 13:50:00 -0500 Subject: [PATCH 075/393] xmlrpc-api-notes: rhbz is now bugzilla 5 --- xmlrpc-api-notes.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlrpc-api-notes.txt b/xmlrpc-api-notes.txt index 3dadc63a..ef1ce670 100644 --- a/xmlrpc-api-notes.txt +++ b/xmlrpc-api-notes.txt @@ -114,7 +114,7 @@ Bugzilla latest/tip: https://bugzilla.readthedocs.io/en/latest/api/index.html -Redhat Bugzilla: 4.4 based with extensions. Bits on top of 4.4 +Redhat Bugzilla: 5.0 based with extensions https://bugzilla.redhat.com/docs/en/html/api/ Bug.search has --from-url extension Bug.update has more hashing support From 48fec23f8249aa8d708cd931260367ef12888091 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Jan 2019 13:50:12 -0500 Subject: [PATCH 076/393] base: Fix raw getcomponents call Wrong field name in include_fields threw this off Fixes: #86 --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 702d76e8..c25a12e1 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -861,7 +861,7 @@ def getcomponents(self, product, force_refresh=False): product_id is None or product_id not in self._cache.component_names): self.refresh_products(names=[product], - include_fields=["names", "id"]) + include_fields=["name", "id"]) proddict = self._lookup_product_in_cache(product) product_id = proddict["id"] From bfea21a605eae03713d5a26c761abe81d1ebc3d8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Jan 2019 13:52:27 -0500 Subject: [PATCH 077/393] base: Raise an explicit error if product lookup fails --- bugzilla/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bugzilla/base.py b/bugzilla/base.py index c25a12e1..05696019 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -863,6 +863,8 @@ def getcomponents(self, product, force_refresh=False): self.refresh_products(names=[product], include_fields=["name", "id"]) proddict = self._lookup_product_in_cache(product) + if "id" not in proddict: + raise BugzillaError("Product '%s' not found" % product) product_id = proddict["id"] opts = {'product_id': product_id, 'field': 'component'} From 04429977a76ec204db52edd6d853478cba50aff0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Jan 2019 14:44:43 -0500 Subject: [PATCH 078/393] base: Bugzilla: add configpaths __init__ property To control configpath setting at startup. Needed for API symmetry and for the test suite to skip API keys --- bugzilla/base.py | 12 ++++++++---- tests/test_rw_functional.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 05696019..674f2217 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -235,7 +235,7 @@ def _listify(val): def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None, - cert=None): + cert=None, configpaths=-1): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at @@ -262,7 +262,8 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, :param sslverify: Maps to 'requests' sslverify parameter. Set to False to disable SSL verification, but it can also be a path to file or directory for custom certs. - :param api_key: A bugzilla + :param api_key: A bugzilla5+ API key + :param configpaths: A list of possible bugzillarc locations. """ if url == -1: raise TypeError("Specify a valid bugzilla url, or pass url=None") @@ -284,19 +285,22 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._field_aliases = [] self._init_field_aliases() - self.configpath = _default_configpaths[:] if not use_creds: cookiefile = None tokenfile = None - self.configpath = [] + configpaths = [] if cookiefile == -1: cookiefile = _default_auth_location("bugzillacookies") if tokenfile == -1: tokenfile = _default_auth_location("bugzillatoken") + if configpaths == -1: + configpaths = _default_configpaths[:] + log.debug("Using tokenfile=%s", tokenfile) self.cookiefile = cookiefile self.tokenfile = tokenfile + self.configpath = configpaths if url: self.connect(url) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 64a059a2..d339fc43 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -815,7 +815,7 @@ def compare(data, newid): def test12SetCookie(self): bz = self.bzclass(self.url, - cookiefile=-1, tokenfile=None) + cookiefile=-1, tokenfile=None, configpaths=[]) try: bz.cookiefile = None From ac92f99df9e6ca49d47b362ab79a68aeafc3607e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Jan 2019 14:50:10 -0500 Subject: [PATCH 079/393] cli: Use no_creds for --no-cache-credentials Rather than try to reimplement it. This makes sure we skip bugzillarc values as well --- bugzilla/_cli.py | 3 +++ tests/test_rw_functional.py | 16 +++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index b151833a..77fe408f 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1034,15 +1034,18 @@ def _make_bz_instance(opt): cookiefile = None tokenfile = None + use_creds = False if opt.cache_credentials: cookiefile = opt.cookiefile or -1 tokenfile = opt.tokenfile or -1 + use_creds = True bz = bugzilla.Bugzilla( url=opt.bugzilla, cookiefile=cookiefile, tokenfile=tokenfile, sslverify=opt.sslverify, + use_creds=use_creds, cert=opt.cert) return bz diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index d339fc43..a7f04cf7 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -649,30 +649,28 @@ def fakegetpass(prompt): getpass.getpass = fakegetpass try: + cmd = "bugzilla --no-cache-credentials --bugzilla %s" % self.url # Implied login with --username and --password - ret = tests.clicomm("bugzilla --bugzilla %s " - "--user foobar@example.com " - "--password foobar query -b 123456" % self.url, + ret = tests.clicomm("%s --user foobar@example.com " + "--password foobar query -b 123456" % cmd, None, expectfail=True) assert "Login failed: " in ret # 'login' with explicit options - ret = tests.clicomm("bugzilla --bugzilla %s " - "--user foobar@example.com " - "--password foobar login" % self.url, + ret = tests.clicomm("%s --user foobar@example.com " + "--password foobar login" % cmd, None, expectfail=True) assert "Login failed: " in ret # 'login' with positional options - ret = tests.clicomm("bugzilla --bugzilla %s " - "login foobar@example.com foobar" % self.url, + ret = tests.clicomm("%s login foobar@example.com foobar" % cmd, None, expectfail=True) assert "Login failed: " in ret # bare 'login' stdinstr = StringIO("foobar@example.com\n\rfoobar\n\r") - ret = tests.clicomm("bugzilla --bugzilla %s login" % self.url, + ret = tests.clicomm("%s login" % cmd, None, expectfail=True, stdin=stdinstr) assert "Bugzilla Username:" in ret assert "Bugzilla Password:" in ret From 096c624138beaaab25d188963462532e5d005e78 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Jan 2019 15:17:51 -0500 Subject: [PATCH 080/393] Fix some pylint/pep8 issues, silence others --- bugzilla/_cli.py | 7 +++---- bugzilla/transport.py | 1 + tests/pycodestyle.cfg | 3 ++- tests/pylint.cfg | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 77fe408f..4cea8a3b 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -14,6 +14,7 @@ from __future__ import print_function +import errno import locale from logging import getLogger, DEBUG, INFO, WARN, StreamHandler, Formatter import argparse @@ -85,7 +86,7 @@ def open_without_clobber(name, *args): try: fd = os.open(name, os.O_CREAT | os.O_EXCL, 0o666) except OSError as err: - if err.errno == os.errno.EEXIST: + if err.errno == errno.EEXIST: name = "%s.%i" % (orig_name, count) count += 1 else: @@ -705,7 +706,7 @@ def bug_field(matchobj): if fieldname == "flag" and rest: val = b.get_flag_status(rest) - elif fieldname == "flags" or fieldname == "flags_requestee": + elif fieldname in ["flags", "flags_requestee"]: tmpstr = [] for f in getattr(b, "flags", []): requestee = f.get('requestee', "") @@ -979,8 +980,6 @@ def _do_get_attach(bz, opt): data = att.read(4096) print("Wrote %s" % outfile.name) - return - def _do_set_attach(bz, opt, parser): if not opt.ids: diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 3dbb425a..a274e61f 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -159,6 +159,7 @@ def _request_helper(self, url, request_body): response. """ response = None + # pylint: disable=try-except-raise try: response = self.session.post( url, data=request_body, **self.request_defaults) diff --git a/tests/pycodestyle.cfg b/tests/pycodestyle.cfg index b9f16e23..25ca28b0 100644 --- a/tests/pycodestyle.cfg +++ b/tests/pycodestyle.cfg @@ -8,5 +8,6 @@ format = pylint # [E303] Too many blank lines # [E402] module level import not at top of file # [E731] do not assign a lambda expression, use a def +# [W504] line break after binary operator -ignore=E125,E128,E129,E303,E402,E731 +ignore=E125,E128,E129,E303,E402,E731,W504 diff --git a/tests/pylint.cfg b/tests/pylint.cfg index 6560d461..f5c361a8 100644 --- a/tests/pylint.cfg +++ b/tests/pylint.cfg @@ -7,7 +7,7 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension enable=fixme From f6e2a7dd7460ccec0247652e01c399e9ce9e9366 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Jan 2019 17:58:47 -0500 Subject: [PATCH 081/393] man: Rework auth section, prioritize API keys + bugzillarc --- bugzilla.1 | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/bugzilla.1 b/bugzilla.1 index 49f05c44..c128d9df 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -225,23 +225,33 @@ List the versions for the given product Only show active components. Combine with --components* -.SH AUTHENTICATION COOKIES AND TOKENS +.SH AUTHENTICATION CACHE AND API KEYS -Older bugzilla instances use cookie-based authentication, and -newer bugzilla instances (around 5.0) use a non-cookie token system. +Some command usage will require an active login to the bugzilla +instance. For example, if the bugzilla instance has some private +bugs, those bugs will be missing from 'query' output if you do +not have an active login. -When you log into bugzilla with the "login" subcommand or the "--login" -argument, we cache the login credentials in ~/.cache/python-bugzilla/ -Previously we cached credentials in ~/.. If you want to see -which file the tool is using, check --debug output. +If you are connecting to a bugzilla 5.0 or later instance, the +best option is to use bugzilla API keys. From the bugzilla +web UI, log in, naviagte to Preferences->API Keys, and generate +a key (it will be a long string of characters and numbers). +Then create a ~/.config/python-bugzilla/bugzillarc like this: -To perform an authenticated bugzilla command on a new machine, run a one time -"bugzilla login" to cache credentials before running the desired command. You -can also run "bugzilla --login" and the login process will be initiated before -invoking the command. + $ cat ~/.config/python-bugzilla/bugzillarc + [bugzilla.example.com] + api_key=YOUR_API_KEY -Additionally, the --no-cache-credentials option will tell the bugzilla tool to -_not_ save any credentials in $HOME, or use any previously cached credentials. +Replace 'bugzilla.example.com' with your bugzilla host name, +and YOUR_API_KEY with the generated API Key from the Web UI. + +For older bugzilla instances, you will need to cache a login +cookie or token with the "login" subcommand or the "--login" +argument. + +Additionally, the --no-cache-credentials option will tell the +bugzilla tool to _not_ save or use any authentication cache, +including the bugzillarc file. .SH EXAMPLES .PP From 085aca8e91dc12bf05577fb8f9e8a975e257316b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 10 Jan 2019 07:52:28 -0500 Subject: [PATCH 082/393] base: Smarter substring matching in bugzillarc Otherwise a bugzilla.redhat.com entry will try to match against partner-bugzilla.redhat.com, which are two separate instances --- bugzilla/base.py | 24 +++++++++++++++++++----- tests/test_misc.py | 7 ++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 674f2217..efca9f45 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -463,11 +463,25 @@ def readconfig(self, configpath=None): section = "" log.debug("bugzillarc: Searching for config section matching %s", self.url) - for s in sorted(cfg.sections()): - # Substring match - prefer the longest match found - if s in self.url: - log.debug("bugzillarc: Found matching section: %s", s) - section = s + + def _parse_hostname(_u): + # If http://example.com is passed, netloc=example.com path="" + # If just example.com is passed, netloc="" path=example.com + parsedbits = urlparse(self.url) + return parsedbits.netloc or parsedbits.path + + urlhost = _parse_hostname(self.url) + for sectionhost in sorted(cfg.sections()): + # If the section is just a hostname, make it match + # If the section has a / in it, do a substring match + if "/" not in sectionhost: + if sectionhost == urlhost: + section = sectionhost + elif sectionhost in self.url: + section = sectionhost + if section: + log.debug("bugzillarc: Found matching section: %s", section) + break if not section: log.debug("bugzillarc: No section found") diff --git a/tests/test_misc.py b/tests/test_misc.py index 75110d21..02b1092e 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -77,7 +77,7 @@ def testCookies(self): def test_readconfig(self): # Testing for bugzillarc handling bzapi = tests.make_bz("4.4.0", rhbz=True) - bzapi.url = "foo.example.com" + bzapi.url = "example.com" temp = tempfile.NamedTemporaryFile(mode="w") content = """ @@ -92,6 +92,11 @@ def test_readconfig(self): assert bzapi.password == "test2" assert bzapi.api_key is None + bzapi.url = "foo.example.com" + bzapi.user = None + bzapi.readconfig(temp.name) + assert bzapi.user is None + content = """ [foo.example.com] user=test3 From cb7e0099c3833b7cb04d84a0c6e596f22d9adbff Mon Sep 17 00:00:00 2001 From: Ken Dreyer Date: Mon, 21 Jan 2019 08:53:02 -0700 Subject: [PATCH 083/393] man: fix spelling of "navigate" naviagte -> navigate --- bugzilla.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla.1 b/bugzilla.1 index c128d9df..7aca93f4 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -234,7 +234,7 @@ not have an active login. If you are connecting to a bugzilla 5.0 or later instance, the best option is to use bugzilla API keys. From the bugzilla -web UI, log in, naviagte to Preferences->API Keys, and generate +web UI, log in, navigate to Preferences->API Keys, and generate a key (it will be a long string of characters and numbers). Then create a ~/.config/python-bugzilla/bugzillarc like this: From b4e62a8fc9c293d31d1a7dad00b805bc00d54847 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 26 Jan 2019 18:13:51 -0500 Subject: [PATCH 084/393] pycodestyle: Ignore an error --- tests/pycodestyle.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/pycodestyle.cfg b/tests/pycodestyle.cfg index 25ca28b0..f304d347 100644 --- a/tests/pycodestyle.cfg +++ b/tests/pycodestyle.cfg @@ -5,9 +5,10 @@ format = pylint # [E125] Continuation indent isn't different from next block # [E128] Not indented for visual style # [E129] visually indented line with same indent as next logical line +# [E301] Blank lines between function definitions # [E303] Too many blank lines # [E402] module level import not at top of file # [E731] do not assign a lambda expression, use a def # [W504] line break after binary operator -ignore=E125,E128,E129,E303,E402,E731,W504 +ignore=E125,E128,E129,E301,E303,E402,E731,W504 From 28f3143abe521d0d7894be8cd1d9b3c875c6b538 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 26 Jan 2019 18:15:28 -0500 Subject: [PATCH 085/393] Move tests/pycodestyle.cfg to tox.ini This makes manual pycodestyle calls pick up our project config --- setup.py | 2 +- tests/pycodestyle.cfg | 14 -------------- tox.ini | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 5ff82f51..9261c608 100755 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ def run(self): print("running pycodestyle") style_guide = pycodestyle.StyleGuide( - config_file='tests/pycodestyle.cfg', + config_file='tox.ini', paths=files, ) style_guide.options.exclude = pycodestyle.normalize_paths( diff --git a/tests/pycodestyle.cfg b/tests/pycodestyle.cfg index f304d347..e69de29b 100644 --- a/tests/pycodestyle.cfg +++ b/tests/pycodestyle.cfg @@ -1,14 +0,0 @@ -[pycodestyle] - -format = pylint - -# [E125] Continuation indent isn't different from next block -# [E128] Not indented for visual style -# [E129] visually indented line with same indent as next logical line -# [E301] Blank lines between function definitions -# [E303] Too many blank lines -# [E402] module level import not at top of file -# [E731] do not assign a lambda expression, use a def -# [W504] line break after binary operator - -ignore=E125,E128,E129,E301,E303,E402,E731,W504 diff --git a/tox.ini b/tox.ini index 2ac3715b..99eda2ad 100644 --- a/tox.ini +++ b/tox.ini @@ -19,3 +19,18 @@ omit = /*/tests/* /usr/* *.tox/* + + +[pycodestyle] +format = pylint + +# [E125] Continuation indent isn't different from next block +# [E128] Not indented for visual style +# [E129] visually indented line with same indent as next logical line +# [E301] Blank lines between function definitions +# [E303] Too many blank lines +# [E402] module level import not at top of file +# [E731] do not assign a lambda expression, use a def +# [W504] line break after binary operator + +ignore=E125,E128,E129,E301,E303,E402,E731,W504 From f07d59395967eb44cde3c2d5e5487fbed64f9af9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 26 Jan 2019 18:18:49 -0500 Subject: [PATCH 086/393] Run 'setup.py pylint' on setup.py, and fix errors --- setup.py | 71 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/setup.py b/setup.py index 9261c608..2a74d1e5 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def get_version(): f = open("bugzilla/apiversion.py") for line in f: if line.startswith('version = '): - return eval(line.split('=')[-1]) + return eval(line.split('=')[-1]) # pylint: disable=eval-used class TestCommand(Command): @@ -51,7 +51,7 @@ def run(self): import pylint.lint import pycodestyle - files = (["bugzilla-cli", "bugzilla"] + + files = (["bugzilla-cli", "bugzilla", "setup.py"] + glob.glob("examples/*.py") + glob.glob("tests/*.py")) output_format = sys.stdout.isatty() and "colorized" or "text" @@ -108,37 +108,38 @@ def _parse_requirements(fname): return ret -setup(name='python-bugzilla', - version=get_version(), - description='Bugzilla XMLRPC access module', - author='Cole Robinson', - author_email='python-bugzilla@lists.fedorahosted.org', - license="GPLv2", - url='https://github.com/python-bugzilla/python-bugzilla', - classifiers=[ - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - ], - packages = ['bugzilla'], - entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, - data_files=[('share/man/man1', ['bugzilla.1'])], - - install_requires=_parse_requirements("requirements.txt"), - tests_require=_parse_requirements("test-requirements.txt"), - - cmdclass={ - "pylint" : PylintCommand, - "rpm" : RPMCommand, - "test" : TestCommand, - }, +setup( + name='python-bugzilla', + version=get_version(), + description='Bugzilla XMLRPC access module', + author='Cole Robinson', + author_email='python-bugzilla@lists.fedorahosted.org', + license="GPLv2", + url='https://github.com/python-bugzilla/python-bugzilla', + classifiers=[ + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + packages=['bugzilla'], + entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, + data_files=[('share/man/man1', ['bugzilla.1'])], + + install_requires=_parse_requirements("requirements.txt"), + tests_require=_parse_requirements("test-requirements.txt"), + + cmdclass={ + "pylint": PylintCommand, + "rpm": RPMCommand, + "test": TestCommand, + }, ) From 821d7a8234d5866034e6fabf83025e931612216d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 26 Jan 2019 18:20:00 -0500 Subject: [PATCH 087/393] setup.py: Update language version classifiers --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2a74d1e5..4368a07f 100755 --- a/setup.py +++ b/setup.py @@ -125,10 +125,10 @@ def _parse_requirements(fname): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ], packages=['bugzilla'], entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, From 02e10141609ac560a10aca1bc1d7f349e754913d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 26 Jan 2019 18:20:41 -0500 Subject: [PATCH 088/393] pycodestyle: Only use format=pylint from setup.py Don't force it on users who are running the tool manually --- setup.py | 1 + tox.ini | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4368a07f..b03aa257 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ def run(self): print("running pycodestyle") style_guide = pycodestyle.StyleGuide( config_file='tox.ini', + format="pylint", paths=files, ) style_guide.options.exclude = pycodestyle.normalize_paths( diff --git a/tox.ini b/tox.ini index 99eda2ad..d81e3403 100644 --- a/tox.ini +++ b/tox.ini @@ -22,8 +22,6 @@ omit = [pycodestyle] -format = pylint - # [E125] Continuation indent isn't different from next block # [E128] Not indented for visual style # [E129] visually indented line with same indent as next logical line From 9abb7f58567c50f988b59da014bfcb6766dad565 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 26 Jan 2019 18:21:30 -0500 Subject: [PATCH 089/393] Move tests/pylint.cfg to pylintrc This makes manual pylint calls pick up our project config --- tests/pylint.cfg => pylintrc | 0 setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/pylint.cfg => pylintrc (100%) diff --git a/tests/pylint.cfg b/pylintrc similarity index 100% rename from tests/pylint.cfg rename to pylintrc diff --git a/setup.py b/setup.py index b03aa257..68bb68f6 100755 --- a/setup.py +++ b/setup.py @@ -71,7 +71,7 @@ def run(self): print("running pylint") pylint_opts = [ - "--rcfile", "tests/pylint.cfg", + "--rcfile", "pylintrc", "--output-format=%s" % output_format, ] pylint.lint.Run(files + pylint_opts) From fe0a224dfe432af53e249c361c7b24f7466952bb Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 26 Jan 2019 18:25:34 -0500 Subject: [PATCH 090/393] oldclasses: Fix pycodestyle formatting --- bugzilla/oldclasses.py | 50 ++++++++++++++++++++++++++++++++---------- setup.py | 3 --- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/bugzilla/oldclasses.py b/bugzilla/oldclasses.py index 18169e78..6155355b 100644 --- a/bugzilla/oldclasses.py +++ b/bugzilla/oldclasses.py @@ -7,17 +7,45 @@ from .base import Bugzilla from .rhbugzilla import RHBugzilla - # These are old compat classes. Nothing new should be added here, # and these should not be altered -class Bugzilla3(Bugzilla): pass -class Bugzilla32(Bugzilla): pass -class Bugzilla34(Bugzilla): pass -class Bugzilla36(Bugzilla): pass -class Bugzilla4(Bugzilla): pass -class Bugzilla42(Bugzilla): pass -class Bugzilla44(Bugzilla): pass -class NovellBugzilla(Bugzilla): pass -class RHBugzilla3(RHBugzilla): pass -class RHBugzilla4(RHBugzilla): pass + +class Bugzilla3(Bugzilla): + pass + + +class Bugzilla32(Bugzilla): + pass + + +class Bugzilla34(Bugzilla): + pass + + +class Bugzilla36(Bugzilla): + pass + + +class Bugzilla4(Bugzilla): + pass + + +class Bugzilla42(Bugzilla): + pass + + +class Bugzilla44(Bugzilla): + pass + + +class NovellBugzilla(Bugzilla): + pass + + +class RHBugzilla3(RHBugzilla): + pass + + +class RHBugzilla4(RHBugzilla): + pass diff --git a/setup.py b/setup.py index 68bb68f6..e62c9dd6 100755 --- a/setup.py +++ b/setup.py @@ -62,9 +62,6 @@ def run(self): format="pylint", paths=files, ) - style_guide.options.exclude = pycodestyle.normalize_paths( - "bugzilla/oldclasses.py", - ) report = style_guide.check_files() if style_guide.options.count: sys.stderr.write(str(report.total_errors) + '\n') From b4177c6b476c23d9da9df50028af01f1591e8d3a Mon Sep 17 00:00:00 2001 From: Jon Schlueter Date: Tue, 22 Jan 2019 15:45:50 -0500 Subject: [PATCH 091/393] Spelling typo in Bugzilla class doctext --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index efca9f45..d88111e2 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -161,7 +161,7 @@ class Bugzilla(object): log in, you can either pass auth options to __init__, or call a login helper like interactive_login(). - If you are not logged in, you won be able to access restricted data like + If you are not logged in, you won't be able to access restricted data like user email, or perform write actions like bug create/update. But simple querys will work correctly. From 7e5860859140dc323c54d45ebf08639135b60458 Mon Sep 17 00:00:00 2001 From: Jon Schlueter Date: Tue, 22 Jan 2019 15:49:14 -0500 Subject: [PATCH 092/393] Fix spelling typo for description --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index d88111e2..273b8b17 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -925,7 +925,7 @@ def addcomponent(self, data): product: The product to create the component in component: The name of the component to create - desription: A one sentence summary of the component + description: A one sentence summary of the component default_assignee: The bugzilla login (email address) of the initial owner of the component default_qa_contact (optional): The bugzilla login of the From 785b8a5cc6fee7137bb3d8bcf6ce610cc829660e Mon Sep 17 00:00:00 2001 From: Jon Schlueter Date: Tue, 22 Jan 2019 15:51:22 -0500 Subject: [PATCH 093/393] missing commas from topy --- bugzilla/_cli.py | 2 +- bugzilla/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 4cea8a3b..e21ec138 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -281,7 +281,7 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('--field', metavar="FIELD=VALUE", action="append", dest="fields", help="Manually specify a bugzilla XMLRPC field. FIELD is " - "the raw name used by the bugzilla instance. For example if your " + "the raw name used by the bugzilla instance. For example, if your " "bugzilla instance has a custom field cf_my_field, do:\n" " --field cf_my_field=VALUE") diff --git a/bugzilla/base.py b/bugzilla/base.py index 273b8b17..e1b89d6a 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1149,7 +1149,7 @@ def build_query(self, Build a query string from passed arguments. Will handle query parameter differences between various bugzilla versions. - Most of the parameters should be self explanatory. However + Most of the parameters should be self-explanatory. However, if you want to perform a complex query, and easy way is to create it with the bugzilla web UI, copy the entire URL it generates, and pass it to the static method From 01f41fc07b20a9b033be3e4de8fa16c4d89a4fe8 Mon Sep 17 00:00:00 2001 From: Ken Dreyer Date: Tue, 26 Feb 2019 15:11:17 -0700 Subject: [PATCH 094/393] cli: add API key message to "bugzilla login --help" Add a note that "bugzilla login" will eventually stop working with tokens, and users must begin using API keys instead. --- bugzilla/_cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index e21ec138..81bf7c52 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -426,7 +426,10 @@ def _setup_action_attach_parser(subparsers): def _setup_action_login_parser(subparsers): usage = 'bugzilla login [username [password]]' - description = "Log into bugzilla and save a login cookie or token." + description = """Log into bugzilla and save a login cookie or token. +Note: These tokens are short-lived, and future Bugzilla versions will no +longer support token authentication at all. Please use a +~/.config/python-bugzilla/bugzillarc file with an API key instead.""" p = subparsers.add_parser("login", description=description, usage=usage) p.add_argument("pos_username", nargs="?", help="Optional username", metavar="username") From 1a810744a83b834a5b5a989a03f95caaf824f02d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 27 Feb 2019 15:25:03 -0500 Subject: [PATCH 095/393] gitignore: Add .pytest_cache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e4e704ff..a3f16c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build .cache .coverage .tox +.pytest_cache From 7de8b225104f24a1eee3e837bf1e02d60aefe69f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 27 Feb 2019 15:43:04 -0500 Subject: [PATCH 096/393] travis: Drop --ro-functional, it can be flakey --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5d9c3d36..26c8166c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,8 @@ install: - pip install tox-travis script: - - tox -- --ro-functional + # --ro-functional can be flakey, so drop it + - tox notifications: email: true From 7316f258d9889a67920dff1c2c335e82cd05700e Mon Sep 17 00:00:00 2001 From: Brian 'Redbeard' Harrington Date: Mon, 15 Jul 2019 12:14:34 -0700 Subject: [PATCH 097/393] cli: Add support for private attachments As per the [Bugzilla Attachment API][bzapi] the `is_private` keyword can be included to make an attachment private. This commit adds that support to the attachment verb. It has been tested against Bugzilla v5.0.6 [bzapi]: https://bugzilla.readthedocs.io/en/stable/api/core/v1/attachment.html#create-attachment --- bugzilla/_cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 81bf7c52..53eb2aaa 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -422,6 +422,8 @@ def _setup_action_attach_parser(subparsers): default=[], help="Download all attachments on the given bug") p.add_argument('-l', '--comment', '--long_desc', help="Add comment with attachment") + p.add_argument('--private', action='store_true', default=False, + help='Mark new comment as private') def _setup_action_login_parser(subparsers): @@ -1015,6 +1017,8 @@ def _do_set_attach(bz, opt, parser): kwargs["ispatch"] = True if opt.comment: kwargs["comment"] = opt.comment + if opt.private: + kwargs["is_private"] = True desc = opt.desc or os.path.basename(fileobj.name) # Upload attachments From ebc7c9cc56c71a21f5df585f2c0b20cb96421203 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 21 Aug 2019 11:37:09 -0400 Subject: [PATCH 098/393] base: Fix collectons deprecation warning /home/crobinso/src/python-bugzilla/bugzilla/base.py:271: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working Signed-off-by: Cole Robinson --- bugzilla/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index e1b89d6a..fc92e7a7 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -9,7 +9,6 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -import collections import getpass import locale from logging import getLogger @@ -22,11 +21,13 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: # pylint: disable=no-name-in-module + from collections.abc import Mapping from configparser import ConfigParser from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse, parse_qsl from xmlrpc.client import Binary, Fault else: + from collections import Mapping from ConfigParser import SafeConfigParser as ConfigParser from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse, parse_qsl @@ -44,9 +45,8 @@ def _nested_update(d, u): # Helper for nested dict update() - # https://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth for k, v in list(u.items()): - if isinstance(v, collections.Mapping): + if isinstance(v, Mapping): d[k] = _nested_update(d.get(k, {}), v) else: d[k] = v From 0b29f5ca61fddf5196e1681051bd9f8fc0e8c295 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 22 Aug 2019 19:13:52 -0400 Subject: [PATCH 099/393] base: Fix backtrace on missing getbugs bug ID Fixes: #104 Signed-off-by: Cole Robinson --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index fc92e7a7..219137bf 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1047,7 +1047,7 @@ def _getbugs(self, idlist, permissive, else: # Need to map an alias for valdict in bugdict.values(): - if i in self._listify(valdict.get("alias", None)): + if i in self._listify(valdict.get("alias", None) or []): found = valdict break From 72d32cda532faf6d63e1d61b79d6b4b2f91ea57a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 26 Aug 2019 17:29:10 -0400 Subject: [PATCH 100/393] Prep for release 2.3.0 --- NEWS.md | 6 ++++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9bc642ab..510e7a8d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # python-bugzilla release news +## Release 2.3.0 (August 26, 2019) +- restrict-login suppot (Viliam Krizan) +- cli: Add support for private attachments (Brian 'Redbeard' Harrington) +- Fix python3 deprecation warnings +- Drop python 3.3 support, minimum python3 is python 3.4 now + ## Release 2.2.0 (August 11, 2018) - Port tests to pytest - cli: --cert Client side certificate support (Tobias Wolter) diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 9762d4ae..6d75a1fe 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -7,5 +7,5 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -version = "2.2.0" +version = "2.3.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index a029cbd7..b5319946 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -15,7 +15,7 @@ %endif Name: python-bugzilla -Version: 2.2.0 +Version: 2.3.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From fc5178a6873e8a3c6906131572a83d1d79be3e3b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 4 Sep 2019 16:33:05 -0400 Subject: [PATCH 101/393] spec: Disable python2 build for f32+ Signed-off-by: Cole Robinson --- python-bugzilla.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index b5319946..7a893b38 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -5,7 +5,7 @@ %bcond_with python3 %endif -%if 0%{?rhel} > 7 +%if 0%{?fedora} > 31 || 0%{?rhel} > 7 # Disable python2 build by default %bcond_with python2 %else From 0480016e3c8abd062b8899b896f11d379de22be1 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 10 Oct 2019 16:56:08 -0400 Subject: [PATCH 102/393] Fix pylint Signed-off-by: Cole Robinson --- bugzilla/base.py | 10 +++------- bugzilla/transport.py | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 219137bf..8c2422c3 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -18,9 +18,8 @@ from io import BytesIO -# pylint: disable=import-error +# pylint: disable=import-error,no-name-in-module,ungrouped-imports if sys.version_info[0] >= 3: - # pylint: disable=no-name-in-module from collections.abc import Mapping from configparser import ConfigParser from http.cookiejar import LoadError, MozillaCookieJar @@ -32,7 +31,7 @@ from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse, parse_qsl from xmlrpclib import Binary, Fault -# pylint: enable=import-error +# pylint: enable=import-error,no-name-in-module,ungrouped-imports from .apiversion import __version__ @@ -311,7 +310,7 @@ def _init_class_from_url(self): """ Detect if we should use RHBugzilla class, and if so, set it """ - from bugzilla import RHBugzilla + from .rhbugzilla import RHBugzilla # pylint: disable=cyclic-import if isinstance(self, RHBugzilla): return @@ -338,7 +337,6 @@ def _init_class_state(self): """ Hook for subclasses to do any __init__ time setup """ - pass def _init_field_aliases(self): # List of field aliases. Maps old style RHBZ parameter @@ -1289,14 +1287,12 @@ def pre_translation(self, query): In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function """ - pass def post_translation(self, query, bug): """ In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function """ - pass def bugs_history_raw(self, bug_ids): """ diff --git a/bugzilla/transport.py b/bugzilla/transport.py index a274e61f..12422bb3 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -28,7 +28,6 @@ class BugzillaError(Exception): """ Error raised in the Bugzilla client code. """ - pass class _BugzillaTokenCache(object): From aeb0ab3e46019970c73b72bf47624bef3ecbdda6 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Thu, 10 Oct 2019 12:27:15 +0200 Subject: [PATCH 103/393] Rewrite rules using standard urlparse. --- bugzilla/base.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 8c2422c3..b94004a4 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -23,13 +23,13 @@ from collections.abc import Mapping from configparser import ConfigParser from http.cookiejar import LoadError, MozillaCookieJar - from urllib.parse import urlparse, parse_qsl + from urllib.parse import urlparse, urlunparse, parse_qsl from xmlrpc.client import Binary, Fault else: from collections import Mapping from ConfigParser import SafeConfigParser as ConfigParser from cookielib import LoadError, MozillaCookieJar - from urlparse import urlparse, parse_qsl + from urlparse import urlparse, urlunparse, parse_qsl from xmlrpclib import Binary, Fault # pylint: enable=import-error,no-name-in-module,ungrouped-imports @@ -215,13 +215,16 @@ def fix_url(url): """ Turn passed url into a bugzilla XMLRPC web url """ - if '://' not in url: + scheme, netloc, path, params, query, fragment = urlparse(url) + if not scheme: log.debug('No scheme given for url, assuming https') - url = 'https://' + url - if url.count('/') < 3: + scheme = 'https' + + if not path: log.debug('No path given for url, assuming /xmlrpc.cgi') - url = url + '/xmlrpc.cgi' - return url + path = 'xmlrpc.cgi' + + return urlunparse((scheme, netloc, path, params, query, fragment)) @staticmethod def _listify(val): From bf682fabb3096bfbed54695a7ff30ca592a63fa7 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk Date: Thu, 10 Oct 2019 12:57:45 +0200 Subject: [PATCH 104/393] Set basic authentication mode in case this is required by the infrastructure --- bugzilla/base.py | 7 ++++++- bugzilla/transport.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index b94004a4..2597a04b 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -237,7 +237,7 @@ def _listify(val): def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None, - cert=None, configpaths=-1): + cert=None, configpaths=-1, basic_auth=False): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at @@ -266,6 +266,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, to file or directory for custom certs. :param api_key: A bugzilla5+ API key :param configpaths: A list of possible bugzillarc locations. + :param basic_auth: Use headers with HTTP Basic authentication """ if url == -1: raise TypeError("Specify a valid bugzilla url, or pass url=None") @@ -303,6 +304,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self.cookiefile = cookiefile self.tokenfile = tokenfile self.configpath = configpaths + self._basic_auth = basic_auth if url: self.connect(url) @@ -567,6 +569,9 @@ def _login(self, user, password, restrict_login=None): """ Backend login method for Bugzilla3 """ + if self._basic_auth: + self._transport.set_basic_auth(user, password) + payload = {'login': user, 'password': password} if restrict_login: payload['restrict_login'] = True diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 12422bb3..d7502023 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -4,6 +4,7 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. +import base64 from logging import getLogger import sys @@ -143,6 +144,16 @@ def __init__(self, url, cookiejar=None, if cert: self.session.cert = cert + def set_basic_auth(self, user, password): + """ + Set basic authentication method. + + :return: + """ + b64str = str(base64.b64encode("{}:{}".format(user, password))) + authstr = "Basic {}".format(b64str.encode("utf-8").decode("utf-8")) + self.request_defaults["headers"]["Authorization"] = authstr + def parse_response(self, response): """ Parse XMLRPC response From d3c7ae71969abcaafcce85afae4ab711c3ab03b7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 17 Nov 2019 19:47:32 -0500 Subject: [PATCH 105/393] base: Fix url fixup for passed scheme-less URL Like plain 'partner-bugzilla.redhat.com' Fixes: #109 Signed-off-by: Cole Robinson --- bugzilla/base.py | 9 ++++++++- tests/test_misc.py | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 2597a04b..5e89ed19 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -220,11 +220,18 @@ def fix_url(url): log.debug('No scheme given for url, assuming https') scheme = 'https' + if path and not netloc: + netloc = path.split("/", 1)[0] + path = "/".join(path.split("/")[1:]) or None + if not path: log.debug('No path given for url, assuming /xmlrpc.cgi') path = 'xmlrpc.cgi' - return urlunparse((scheme, netloc, path, params, query, fragment)) + newurl = urlunparse((scheme, netloc, path, params, query, fragment)) + if newurl != url: + log.debug("Generated fixed URL: %s", newurl) + return newurl @staticmethod def _listify(val): diff --git a/tests/test_misc.py b/tests/test_misc.py index 02b1092e..6c5f16a3 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -58,6 +58,14 @@ def testUserAgent(self): b3 = tests.make_bz("3.0.0") assert "python-bugzilla" in b3.user_agent + def test_fixurl(self): + assert (bugzilla.Bugzilla.fix_url("example.com") == + "https://example.com/xmlrpc.cgi") + assert (bugzilla.Bugzilla.fix_url("example.com/xmlrpc.cgi") == + "https://example.com/xmlrpc.cgi") + assert (bugzilla.Bugzilla.fix_url("http://example.com/somepath.cgi") == + "http://example.com/somepath.cgi") + def testCookies(self): cookiesbad = os.path.join(os.getcwd(), "tests/data/cookies-bad.txt") cookieslwp = os.path.join(os.getcwd(), "tests/data/cookies-lwp.txt") From b5d78155d16992470afac9069f91f91dea1b7b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=8Cestm=C3=ADr=20Kalina?= Date: Tue, 12 Nov 2019 15:22:39 +0100 Subject: [PATCH 106/393] cli: add the option to skip obsolete attachments with --get-all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce --ignore-obsolete. When used (e.g., with attach --get-all), obsolete attachments are ignored and not downloaded. Signed-off-by: Čestmír Kalina --- bugzilla/_cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 53eb2aaa..c0b91bac 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -403,7 +403,7 @@ def _setup_action_modify_parser(subparsers): def _setup_action_attach_parser(subparsers): usage = """ bugzilla attach --file=FILE --desc=DESC [--type=TYPE] BUGID [BUGID...] -bugzilla attach --get=ATTACHID --getall=BUGID [...] +bugzilla attach --get=ATTACHID --getall=BUGID [--ignore-obsolete] [...] bugzilla attach --type=TYPE BUGID [BUGID...]""" description = "Attach files or download attachments." p = subparsers.add_parser("attach", description=description, usage=usage) @@ -420,6 +420,8 @@ def _setup_action_attach_parser(subparsers): default=[], help="Download the attachment with the given ID") p.add_argument("--getall", "--get-all", metavar="BUGID", action="append", default=[], help="Download all attachments on the given bug") + p.add_argument('--ignore-obsolete', action="store_true", + help='Do not download attachments marked as obsolete.') p.add_argument('-l', '--comment', '--long_desc', help="Add comment with attachment") p.add_argument('--private', action='store_true', default=False, @@ -977,6 +979,11 @@ def _do_get_attach(bz, opt): opt.get += bug.get_attachment_ids() for attid in set(opt.get): + if opt.ignore_obsolete: + metadata = bz.get_attachments(None, attid, + include_fields=["is_obsolete"]) + if metadata["attachments"][str(attid)]['is_obsolete'] == 1: + continue att = bz.openattachment(attid) outfile = open_without_clobber(att.name, "wb") data = att.read(4096) From 3e5b2a38ad23b6134ef6a123e8b5bc044471893a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 18 Nov 2019 11:38:36 -0500 Subject: [PATCH 107/393] tests: Add --ignore-obsolete test Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index a7f04cf7..7aa89090 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -551,6 +551,13 @@ def _test8Attachments(self): setbug.refresh() assert setbug.attachments[-1]["flags"] == [] + # Set attachment obsolete + bz._proxy.Bug.update_attachment({ # pylint: disable=protected-access + "ids": [setbug.attachments[-1]["id"]], + "is_obsolete": 1}) + setbug.refresh() + assert setbug.attachments[-1]["is_obsolete"] == 1 + # Get attachment, verify content out = tests.clicomm(cmd + "--get %s" % attachid, bz).splitlines() @@ -578,6 +585,18 @@ def _test8Attachments(self): raise AssertionError("filename '%s' not found" % f) os.unlink(f) + # Get all attachments, but ignore obsolete + ignorecmd = cmd + "--getall %s --ignore-obsolete" % getbug.id + out = tests.clicomm(ignorecmd, bz).splitlines() + + assert len(out) == (numattach + 1) + fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] + assert len(fnames) == (numattach - 1) + for f in fnames: + if not os.path.exists(f): + raise AssertionError("filename '%s' not found" % f) + os.unlink(f) + def test09Whiteboards(self): bz = self.bzclass(url=self.url) From c652c3bc0a917ee91a449c9bbbc3ddaa20f9bebb Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 18 Nov 2019 11:42:37 -0500 Subject: [PATCH 108/393] man: Add attach --ignore-obsolete Signed-off-by: Cole Robinson --- bugzilla.1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bugzilla.1 b/bugzilla.1 index 7aca93f4..d214e9bd 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -208,6 +208,8 @@ Mime-type for the file being attached Download the attachment with the given ID .IP "--getall=BUGID, --get-all=BUGID" Download all attachments on the given bug +.IP "--ignore-obsolete" +Do not download attachments marked as obsolete. .IP "--comment=COMMENT, -l COMMENT" Add comment with attachment From 86dea084f5f02beb8d6521f8ed3abdaa395de136 Mon Sep 17 00:00:00 2001 From: "Danilo C. L. de Paula" Date: Fri, 18 Oct 2019 14:44:46 -0300 Subject: [PATCH 109/393] Add `login --api-key` support for bugzilla login API This change introduces a new feature for the command line client and the Bugzilla python API. In the python API, it changes the interactive_login method to receive a new flag: "use_api_key" that, in case is defined will ask the user to provide an API key instead of regular username/password. In case the API_KEY provide works correctly, it will also update the ~/config/python-bugzilla/bugzillarc file (or ~/.bugzillarc). In the bugzilla-cli side, it will add a new parameter to the login command: "--api-key". Use key will trigger the usage of this new python API. Default behaviors were not change, neither in bugzilla-cli or the python API. Resolves: #82 --- bugzilla/_cli.py | 23 +++++++++------ bugzilla/base.py | 73 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index c0b91bac..2ae7b324 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -429,16 +429,19 @@ def _setup_action_attach_parser(subparsers): def _setup_action_login_parser(subparsers): - usage = 'bugzilla login [username [password]]' + usage = 'bugzilla login [--api-key] [username [password]]' description = """Log into bugzilla and save a login cookie or token. Note: These tokens are short-lived, and future Bugzilla versions will no -longer support token authentication at all. Please use a -~/.config/python-bugzilla/bugzillarc file with an API key instead.""" +longer support token authentication at all. Please use an API key instead.""" p = subparsers.add_parser("login", description=description, usage=usage) - p.add_argument("pos_username", nargs="?", help="Optional username", - metavar="username") - p.add_argument("pos_password", nargs="?", help="Optional password", - metavar="password") + p.add_argument('--api-key', action='store_true', default=False, + help='Use an API-KEY instead of username/password.') + p.add_argument("pos_username", nargs="?", help="Optional username " \ + "(ignored if --api-key is provided)", + metavar="username") + p.add_argument("pos_password", nargs="?", help="Optional password " \ + "(ignored if --api-key is provided)", + metavar="password") def setup_parser(): @@ -1073,9 +1076,13 @@ def _handle_login(opt, action, bz): opt.login or opt.username or opt.password) username = getattr(opt, "pos_username", None) or opt.username password = getattr(opt, "pos_password", None) or opt.password + use_key = getattr(opt, "api_key", False) try: - if do_interactive_login: + if is_login_command and use_key: + bz.interactive_login(use_api_key=True, + restrict_login=opt.restrict_login) + elif do_interactive_login: if bz.url: print("Logging into %s" % urlparse(bz.url)[1]) bz.interactive_login(username, password, diff --git a/bugzilla/base.py b/bugzilla/base.py index 5e89ed19..5651bc51 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -41,6 +41,12 @@ log = getLogger(__name__) +def _parse_hostname(url): + # If http://example.com is passed, netloc=example.com path="" + # If just example.com is passed, netloc="" path=example.com + parsedbits = urlparse(url) + return parsedbits.netloc or parsedbits.path + def _nested_update(d, u): # Helper for nested dict update() @@ -52,14 +58,14 @@ def _nested_update(d, u): return d -def _default_auth_location(filename): +def _default_location(filename, kind='cache'): """ - Determine auth location for filename, like 'bugzillacookies'. If + Determine default location for filename, like 'bugzillacookies'. If old style ~/.bugzillacookies exists, we use that, otherwise we - use ~/.cache/python-bugzilla/bugzillacookies. Same for bugzillatoken + use ~/.cache/python-bugzilla/bugzillacookies. Same for bugzillatoken and bugzilarc """ homepath = os.path.expanduser("~/.%s" % filename) - xdgpath = os.path.expanduser("~/.cache/python-bugzilla/%s" % filename) + xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) if os.path.exists(xdgpath): return xdgpath if os.path.exists(homepath): @@ -301,9 +307,9 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, configpaths = [] if cookiefile == -1: - cookiefile = _default_auth_location("bugzillacookies") + cookiefile = _default_location("bugzillacookies") if tokenfile == -1: - tokenfile = _default_auth_location("bugzillatoken") + tokenfile = _default_location("bugzillatoken") if configpaths == -1: configpaths = _default_configpaths[:] @@ -474,12 +480,6 @@ def readconfig(self, configpath=None): log.debug("bugzillarc: Searching for config section matching %s", self.url) - def _parse_hostname(_u): - # If http://example.com is passed, netloc=example.com path="" - # If just example.com is passed, netloc="" path=example.com - parsedbits = urlparse(self.url) - return parsedbits.netloc or parsedbits.path - urlhost = _parse_hostname(self.url) for sectionhost in sorted(cfg.sections()): # If the section is just a hostname, make it match @@ -633,8 +633,36 @@ def login(self, user=None, password=None, restrict_login=None): except Fault as e: raise BugzillaError("Login failed: %s" % str(e.faultString)) + def _save_api_key(self): + """ + Save the API_KEY in the config file. + + If toklenfile and cookiefile are undefined, it meas that the + API was called with --no-cache-credentials and no change will be + made + """ + if self.tokenfile is None and self.cookiefile is None: + log.info("API Key won't be updated") + return + + config_filename = _default_location('bugzillarc', kind='config') + section = _parse_hostname(self.url) + + cfg = ConfigParser() + cfg.read(config_filename) + + if section not in cfg.sections(): + cfg.add_section(section) + + cfg[section]['api_key'] = self.api_key.strip() + + with open(config_filename, 'w') as configfile: + cfg.write(configfile) + + log.info("API Key updated in %s", config_filename) + def interactive_login(self, user=None, password=None, force=False, - restrict_login=None): + restrict_login=None, use_api_key=False): """ Helper method to handle login for this bugzilla instance. @@ -642,10 +670,29 @@ def interactive_login(self, user=None, password=None, force=False, :param password: bugzilla password. If not specified, prompt for it. :param force: Unused :param restrict_login: restricts session to IP address + :param use_api_key: True if the login should be done using an api_key """ ignore = force log.debug('Calling interactive_login') + if use_api_key: + sys.stdout.write('API Key: ') + sys.stdout.flush() + api_key = sys.stdin.readline().strip() + + self.disconnect() + self.api_key = api_key + + log.info('Checking API key... ') + self.connect() + + if not self.logged_in: + raise BugzillaError("Login with API_KEY failed") + log.info('API Key acepted') + + self._save_api_key() + return + if not user: sys.stdout.write('Bugzilla Username: ') sys.stdout.flush() From 2f3724ea924964ff4783b93304582c8b16632036 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 22 Nov 2019 17:32:11 -0500 Subject: [PATCH 110/393] cli: Improve docs for new login --api-key option * Make the help text more explicit * Add it to the man page Signed-off-by: Cole Robinson --- bugzilla.1 | 3 +++ bugzilla/_cli.py | 13 +++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bugzilla.1 b/bugzilla.1 index d214e9bd..f4e63281 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -247,6 +247,9 @@ Then create a ~/.config/python-bugzilla/bugzillarc like this: Replace 'bugzilla.example.com' with your bugzilla host name, and YOUR_API_KEY with the generated API Key from the Web UI. +Alternatively, you can use 'bugzilla login --api-key', which +will ask for the API key, and save it to bugzillarc for you. + For older bugzilla instances, you will need to cache a login cookie or token with the "login" subcommand or the "--login" argument. diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 2ae7b324..675231a6 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -432,15 +432,16 @@ def _setup_action_login_parser(subparsers): usage = 'bugzilla login [--api-key] [username [password]]' description = """Log into bugzilla and save a login cookie or token. Note: These tokens are short-lived, and future Bugzilla versions will no -longer support token authentication at all. Please use an API key instead.""" +longer support token authentication at all. Please use a +~/.config/python-bugzilla/bugzillarc file with an API key instead, or +use 'bugzilla login --api-key' and we will save it for you.""" p = subparsers.add_parser("login", description=description, usage=usage) p.add_argument('--api-key', action='store_true', default=False, - help='Use an API-KEY instead of username/password.') - p.add_argument("pos_username", nargs="?", help="Optional username " \ - "(ignored if --api-key is provided)", + help='Prompt for and save an API key into bugzillarc, ' + 'rather than prompt for username and password.') + p.add_argument("pos_username", nargs="?", help="Optional username ", metavar="username") - p.add_argument("pos_password", nargs="?", help="Optional password " \ - "(ignored if --api-key is provided)", + p.add_argument("pos_password", nargs="?", help="Optional password ", metavar="password") From ba8b9dfdf6889c6de3dff90e8f3654a22842d66b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 22 Nov 2019 17:53:22 -0500 Subject: [PATCH 111/393] base: Add _default_location wrappers Makes the usage a bit more clear Signed-off-by: Cole Robinson --- bugzilla/base.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 5651bc51..fa1758bb 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -41,6 +41,7 @@ log = getLogger(__name__) + def _parse_hostname(url): # If http://example.com is passed, netloc=example.com path="" # If just example.com is passed, netloc="" path=example.com @@ -58,11 +59,12 @@ def _nested_update(d, u): return d -def _default_location(filename, kind='cache'): +def _default_location(filename, kind): """ Determine default location for filename, like 'bugzillacookies'. If old style ~/.bugzillacookies exists, we use that, otherwise we - use ~/.cache/python-bugzilla/bugzillacookies. Same for bugzillatoken and bugzilarc + use ~/.cache/python-bugzilla/bugzillacookies. + Same for bugzillatoken and bugzillarc """ homepath = os.path.expanduser("~/.%s" % filename) xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) @@ -76,6 +78,14 @@ def _default_location(filename, kind='cache'): return xdgpath +def _default_cache_location(filename): + return _default_location(filename, 'cache') + + +def _default_config_location(filename): + return _default_location(filename, 'config') + + def _build_cookiejar(cookiefile): cj = MozillaCookieJar(cookiefile) if cookiefile is None: @@ -307,9 +317,9 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, configpaths = [] if cookiefile == -1: - cookiefile = _default_location("bugzillacookies") + cookiefile = _default_cache_location("bugzillacookies") if tokenfile == -1: - tokenfile = _default_location("bugzillatoken") + tokenfile = _default_cache_location("bugzillatoken") if configpaths == -1: configpaths = _default_configpaths[:] @@ -645,7 +655,7 @@ def _save_api_key(self): log.info("API Key won't be updated") return - config_filename = _default_location('bugzillarc', kind='config') + config_filename = _default_config_location('bugzillarc') section = _parse_hostname(self.url) cfg = ConfigParser() From 0191f5fafccd27c2cdd3134a7d55165b55ac9816 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 22 Nov 2019 18:46:34 -0500 Subject: [PATCH 112/393] base: Don't overwrite pre-set auth bits in readconfig() Otherwise the user has no way to overwrite these values from the API object, and the bugzillarc file always takes precedence Signed-off-by: Cole Robinson --- bugzilla/base.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index fa1758bb..9999b4ba 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -452,7 +452,7 @@ def _setcookiefile(self, cookiefile): # Login/connection handling # ############################# - def readconfig(self, configpath=None): + def readconfig(self, configpath=None, overwrite=True): """ :param configpath: Optional bugzillarc path to read, instead of the default list. @@ -481,6 +481,9 @@ def readconfig(self, configpath=None): Be sure to set appropriate permissions on bugzillarc if you choose to store your password in it! + + :param overwrite: If True, bugzillarc will clobber any already + set self.user/password/api_key/cert value. """ cfg = _open_bugzillarc(configpath or self.configpath) if not cfg: @@ -508,16 +511,16 @@ def readconfig(self, configpath=None): return for key, val in cfg.items(section): - if key == "api_key": + if key == "api_key" and (overwrite or not self.api_key): log.debug("bugzillarc: setting api_key") self.api_key = val - elif key == "user": + elif key == "user" and (overwrite or not self.user): log.debug("bugzillarc: setting user=%s", val) self.user = val - elif key == "password": + elif key == "password" and (overwrite or not self.password): log.debug("bugzillarc: setting password") self.password = val - elif key == "cert": + elif key == "cert" and not (overwrite or not self.cert): log.debug("bugzillarc: setting cert") self.cert = val else: @@ -560,7 +563,7 @@ def connect(self, url=None): self.url = url # we've changed URLs - reload config - self.readconfig() + self.readconfig(overwrite=False) if (self.user and self.password): log.info("user and password present - doing login()") From cd955c658b7b0fc3ceaf052a6196a5efb62254e7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 22 Nov 2019 18:47:15 -0500 Subject: [PATCH 113/393] base: Cleanups and tweaks with use_api_key * Break out save_api_key from the class object * Add explicit use_creds tracking * Tweak the output strings we print * Minor other improvements Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 10 +++----- bugzilla/base.py | 66 +++++++++++++++++++++++++----------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 675231a6..c4105bf8 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1080,13 +1080,11 @@ def _handle_login(opt, action, bz): use_key = getattr(opt, "api_key", False) try: - if is_login_command and use_key: - bz.interactive_login(use_api_key=True, - restrict_login=opt.restrict_login) - elif do_interactive_login: - if bz.url: + if do_interactive_login or use_key: + if bz.url and not use_key: print("Logging into %s" % urlparse(bz.url)[1]) bz.interactive_login(username, password, + use_api_key=use_key, restrict_login=opt.restrict_login) except bugzilla.BugzillaError as e: print(str(e)) @@ -1099,7 +1097,7 @@ def _handle_login(opt, action, bz): if is_login_command: msg = "Login successful." - if bz.cookiefile or bz.tokenfile: + if (bz.cookiefile or bz.tokenfile) and not use_key: msg = "Login successful, token cache updated." print(msg) diff --git a/bugzilla/base.py b/bugzilla/base.py index 9999b4ba..556ccb40 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -129,6 +129,32 @@ def _open_bugzillarc(configpaths=-1): return cfg +def _save_api_key(url, api_key): + """ + Save the API_KEY in the config file. + + If tokenfile and cookiefile are undefined, it means that the + API was called with --no-cache-credentials and no change will be + made + """ + config_filename = _default_config_location('bugzillarc') + section = _parse_hostname(url) + + cfg = ConfigParser() + cfg.read(config_filename) + + if section not in cfg.sections(): + cfg.add_section(section) + + cfg[section]['api_key'] = api_key.strip() + + with open(config_filename, 'w') as configfile: + cfg.write(configfile) + + log.info("API key written to %s", config_filename) + print("API key written to %s" % config_filename) + + class _FieldAlias(object): """ Track API attribute names that differ from what we expose in users. @@ -311,7 +337,8 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._field_aliases = [] self._init_field_aliases() - if not use_creds: + self._use_creds = use_creds + if not self._use_creds: cookiefile = None tokenfile = None configpaths = [] @@ -646,34 +673,6 @@ def login(self, user=None, password=None, restrict_login=None): except Fault as e: raise BugzillaError("Login failed: %s" % str(e.faultString)) - def _save_api_key(self): - """ - Save the API_KEY in the config file. - - If toklenfile and cookiefile are undefined, it meas that the - API was called with --no-cache-credentials and no change will be - made - """ - if self.tokenfile is None and self.cookiefile is None: - log.info("API Key won't be updated") - return - - config_filename = _default_config_location('bugzillarc') - section = _parse_hostname(self.url) - - cfg = ConfigParser() - cfg.read(config_filename) - - if section not in cfg.sections(): - cfg.add_section(section) - - cfg[section]['api_key'] = self.api_key.strip() - - with open(config_filename, 'w') as configfile: - cfg.write(configfile) - - log.info("API Key updated in %s", config_filename) - def interactive_login(self, user=None, password=None, force=False, restrict_login=None, use_api_key=False): """ @@ -683,7 +682,7 @@ def interactive_login(self, user=None, password=None, force=False, :param password: bugzilla password. If not specified, prompt for it. :param force: Unused :param restrict_login: restricts session to IP address - :param use_api_key: True if the login should be done using an api_key + :param use_api_key: If True, prompt for an api_key instead """ ignore = force log.debug('Calling interactive_login') @@ -701,9 +700,12 @@ def interactive_login(self, user=None, password=None, force=False, if not self.logged_in: raise BugzillaError("Login with API_KEY failed") - log.info('API Key acepted') + log.info('API Key accepted') - self._save_api_key() + if self._use_creds: + _save_api_key(self.url, self.api_key) + else: + log.info("API Key won't be updated because use_creds=False") return if not user: From 3776bf33fbe19fed1337eabf4f3cb53a8e3090e2 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 16 Dec 2019 02:35:07 +0000 Subject: [PATCH 114/393] Factor out rc-handling and listify This is in preparation for #75. Reviewed-by: Cole Robinson --- bugzilla/_cli.py | 4 +- bugzilla/_rc.py | 43 ++++++++++++++++++ bugzilla/_util.py | 15 +++++++ bugzilla/base.py | 99 +++++++++++++++--------------------------- bugzilla/rhbugzilla.py | 19 ++++---- 5 files changed, 104 insertions(+), 76 deletions(-) create mode 100644 bugzilla/_rc.py create mode 100644 bugzilla/_util.py diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index c4105bf8..326d05a0 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -101,8 +101,8 @@ def get_default_url(): """ Grab a default URL from bugzillarc [DEFAULT] url=X """ - from bugzilla.base import _open_bugzillarc - cfg = _open_bugzillarc() + from bugzilla._rc import open_bugzillarc + cfg = open_bugzillarc() if cfg: cfgurl = cfg.defaults().get("url", None) if cfgurl is not None: diff --git a/bugzilla/_rc.py b/bugzilla/_rc.py new file mode 100644 index 00000000..64b388ca --- /dev/null +++ b/bugzilla/_rc.py @@ -0,0 +1,43 @@ +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +import os +import sys +from logging import getLogger + +# pylint: disable=import-error,no-name-in-module,ungrouped-imports +if sys.version_info[0] >= 3: + from configparser import ConfigParser +else: + from ConfigParser import SafeConfigParser as ConfigParser +# pylint: enable=import-error,no-name-in-module,ungrouped-imports + +from ._util import listify + +log = getLogger(__name__) + +DEFAULT_CONFIGPATHS = [ + '/etc/bugzillarc', + '~/.bugzillarc', + '~/.config/python-bugzilla/bugzillarc', +] + + +def open_bugzillarc(configpaths=-1): + if configpaths == -1: + configpaths = DEFAULT_CONFIGPATHS[:] + + # pylint: disable=protected-access + configpaths = [os.path.expanduser(p) for p in + listify(configpaths)] + # pylint: enable=protected-access + cfg = ConfigParser() + read_files = cfg.read(configpaths) + if not read_files: + return + + log.info("Found bugzillarc files: %s", read_files) + return cfg diff --git a/bugzilla/_util.py b/bugzilla/_util.py new file mode 100644 index 00000000..796cd497 --- /dev/null +++ b/bugzilla/_util.py @@ -0,0 +1,15 @@ +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + + +def listify(val): + """Ensure that value is either None or a list, converting single values + into 1-element lists""" + if val is None: + return val + if isinstance(val, list): + return val + return [val] diff --git a/bugzilla/base.py b/bugzilla/base.py index 556ccb40..ca65f706 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -34,6 +34,8 @@ # pylint: enable=import-error,no-name-in-module,ungrouped-imports +from ._util import listify +from ._rc import DEFAULT_CONFIGPATHS, open_bugzillarc from .apiversion import __version__ from .bug import Bug, User from .transport import BugzillaError, _BugzillaServerProxy, _RequestsTransport @@ -105,30 +107,6 @@ def _build_cookiejar(cookiefile): cookiefile) -_default_configpaths = [ - '/etc/bugzillarc', - '~/.bugzillarc', - '~/.config/python-bugzilla/bugzillarc', -] - - -def _open_bugzillarc(configpaths=-1): - if configpaths == -1: - configpaths = _default_configpaths[:] - - # pylint: disable=protected-access - configpaths = [os.path.expanduser(p) for p in - Bugzilla._listify(configpaths)] - # pylint: enable=protected-access - cfg = ConfigParser() - read_files = cfg.read(configpaths) - if not read_files: - return - - log.info("Found bugzillarc files: %s", read_files) - return cfg - - def _save_api_key(url, api_key): """ Save the API_KEY in the config file. @@ -275,15 +253,6 @@ def fix_url(url): log.debug("Generated fixed URL: %s", newurl) return newurl - @staticmethod - def _listify(val): - if val is None: - return val - if isinstance(val, list): - return val - return [val] - - def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None, cert=None, configpaths=-1, basic_auth=False): @@ -348,7 +317,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, if tokenfile == -1: tokenfile = _default_cache_location("bugzillatoken") if configpaths == -1: - configpaths = _default_configpaths[:] + configpaths = DEFAULT_CONFIGPATHS[:] log.debug("Using tokenfile=%s", tokenfile) self.cookiefile = cookiefile @@ -512,7 +481,7 @@ def readconfig(self, configpath=None, overwrite=True): :param overwrite: If True, bugzillarc will clobber any already set self.user/password/api_key/cert value. """ - cfg = _open_bugzillarc(configpath or self.configpath) + cfg = open_bugzillarc(configpath or self.configpath) if not cfg: return @@ -826,9 +795,9 @@ def product_get(self, ids=None, names=None, kwargs = {} if ids: - kwargs["ids"] = self._listify(ids) + kwargs["ids"] = listify(ids) if names: - kwargs["names"] = self._listify(names) + kwargs["names"] = listify(names) if include_fields: kwargs["include_fields"] = include_fields if exclude_fields: @@ -1097,7 +1066,7 @@ def _getbugs(self, idlist, permissive, # String aliases can be passed as well idlist.append(i) - extra_fields = self._listify(extra_fields or []) + extra_fields = listify(extra_fields or []) extra_fields += self._getbug_extra_fields getbugdata = {"ids": idlist} @@ -1122,7 +1091,7 @@ def _getbugs(self, idlist, permissive, else: # Need to map an alias for valdict in bugdict.values(): - if i in self._listify(valdict.get("alias", None) or []): + if i in listify(valdict.get("alias", None) or []): found = valdict break @@ -1243,8 +1212,8 @@ def build_query(self, query = { "alias": alias, - "product": self._listify(product), - "component": self._listify(component), + "product": listify(product), + "component": listify(component), "version": version, "id": bug_id, "short_desc": short_desc, @@ -1253,17 +1222,17 @@ def build_query(self, "priority": priority, "target_release": target_release, "target_milestone": target_milestone, - "tag": self._listify(tags), + "tag": listify(tags), "quicksearch": quicksearch, "savedsearch": savedsearch, "sharer_id": savedsearch_sharer_id, # RH extensions... don't add any more. See comment below - "sub_components": self._listify(sub_component), + "sub_components": listify(sub_component), } def add_bool(bzkey, value, bool_id, booltype=None): - value = self._listify(value) + value = listify(value) if value is None: return bool_id @@ -1394,7 +1363,7 @@ def update_bugs(self, ids, updates): build_update(), otherwise we cannot guarantee back compatibility. """ tmp = updates.copy() - tmp["ids"] = self._listify(ids) + tmp["ids"] = listify(ids) return self._proxy.Bug.update(tmp) @@ -1404,12 +1373,12 @@ def update_tags(self, idlist, tags_add=None, tags_remove=None): """ tags = {} if tags_add: - tags["add"] = self._listify(tags_add) + tags["add"] = listify(tags_add) if tags_remove: - tags["remove"] = self._listify(tags_remove) + tags["remove"] = listify(tags_remove) d = { - "ids": self._listify(idlist), + "ids": listify(idlist), "tags": tags, } @@ -1506,7 +1475,7 @@ def add_dict(key, add, remove, _set=None, convert=None): return def c(val): - val = self._listify(val) + val = listify(val) if convert: val = [convert(v) for v in val] return val @@ -1548,7 +1517,7 @@ def c(val): s("whiteboard", whiteboard) s("work_time", work_time, float) s("flags", flags) - s("comment_tags", comment_tags, self._listify) + s("comment_tags", comment_tags, listify) add_dict("blocks", blocks_add, blocks_remove, blocks_set, convert=int) @@ -1629,7 +1598,7 @@ def attachfile(self, idlist, attachfile, description, **kwargs): data = data.encode(locale.getpreferredencoding()) kwargs['data'] = Binary(data) - kwargs['ids'] = self._listify(idlist) + kwargs['ids'] = listify(idlist) if 'file_name' not in kwargs and hasattr(f, "name"): kwargs['file_name'] = os.path.basename(f.name) @@ -1697,13 +1666,13 @@ def get_attachments(self, ids, attachment_ids, https://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment """ params = { - "ids": self._listify(ids) or [], - "attachment_ids": self._listify(attachment_ids) or [], + "ids": listify(ids) or [], + "attachment_ids": listify(attachment_ids) or [], } if include_fields: - params["include_fields"] = self._listify(include_fields) + params["include_fields"] = listify(include_fields) if exclude_fields: - params["exclude_fields"] = self._listify(exclude_fields) + params["exclude_fields"] = listify(exclude_fields) return self._proxy.Bug.attachments(params) @@ -1751,15 +1720,15 @@ def build_createbug(self, localdict = {} if blocks: - localdict["blocks"] = self._listify(blocks) + localdict["blocks"] = listify(blocks) if cc: - localdict["cc"] = self._listify(cc) + localdict["cc"] = listify(cc) if depends_on: - localdict["depends_on"] = self._listify(depends_on) + localdict["depends_on"] = listify(depends_on) if groups: - localdict["groups"] = self._listify(groups) + localdict["groups"] = listify(groups) if keywords: - localdict["keywords"] = self._listify(keywords) + localdict["keywords"] = listify(keywords) if description: localdict["description"] = description if comment_private: @@ -1845,11 +1814,11 @@ def _getusers(self, ids=None, names=None, match=None): """ params = {} if ids: - params['ids'] = self._listify(ids) + params['ids'] = listify(ids) if names: - params['names'] = self._listify(names) + params['names'] = listify(names) if match: - params['match'] = self._listify(match) + params['match'] = listify(match) if not params: raise BugzillaError('_get() needs one of ids, ' ' names, or match kwarg.') @@ -1925,14 +1894,14 @@ def updateperms(self, user, action, groups): :arg action: add, remove, or set :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) """ - groups = self._listify(groups) + groups = listify(groups) if action == "rem": action = "remove" if action not in ["add", "remove", "set"]: raise BugzillaError("Unknown user permission action '%s'" % action) update = { - "names": self._listify(user), + "names": listify(user), "groups": { action: groups, } diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index b82365ee..15c1e0c7 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -12,6 +12,7 @@ from logging import getLogger from .base import Bugzilla +from ._util import listify log = getLogger(__name__) @@ -77,7 +78,7 @@ def get_sub_component(): return if not isinstance(val, dict): - component = self._listify(kwargs.get("component")) + component = listify(kwargs.get("component")) if not component: raise ValueError("component must be specified if " "specifying sub_component") @@ -150,7 +151,7 @@ def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, if ext_priority is not None: param_dict['ext_priority'] = ext_priority params = { - 'bug_ids': self._listify(bug_ids), + 'bug_ids': listify(bug_ids), 'external_bugs': [param_dict], } @@ -185,7 +186,7 @@ def update_external_tracker(self, ids=None, ext_type_id=None, """ params = {} if ids is not None: - params['ids'] = self._listify(ids) + params['ids'] = listify(ids) if ext_type_id is not None: params['ext_type_id'] = ext_type_id if ext_type_description is not None: @@ -193,9 +194,9 @@ def update_external_tracker(self, ids=None, ext_type_id=None, if ext_type_url is not None: params['ext_type_url'] = ext_type_url if ext_bz_bug_id is not None: - params['ext_bz_bug_id'] = self._listify(ext_bz_bug_id) + params['ext_bz_bug_id'] = listify(ext_bz_bug_id) if bug_ids is not None: - params['bug_ids'] = self._listify(bug_ids) + params['bug_ids'] = listify(bug_ids) if ext_status is not None: params['ext_status'] = ext_status if ext_description is not None: @@ -229,7 +230,7 @@ def remove_external_tracker(self, ids=None, ext_type_id=None, """ params = {} if ids is not None: - params['ids'] = self._listify(ids) + params['ids'] = listify(ids) if ext_type_id is not None: params['ext_type_id'] = ext_type_id if ext_type_description is not None: @@ -237,9 +238,9 @@ def remove_external_tracker(self, ids=None, ext_type_id=None, if ext_type_url is not None: params['ext_type_url'] = ext_type_url if ext_bz_bug_id is not None: - params['ext_bz_bug_id'] = self._listify(ext_bz_bug_id) + params['ext_bz_bug_id'] = listify(ext_bz_bug_id) if bug_ids is not None: - params['bug_ids'] = self._listify(bug_ids) + params['bug_ids'] = listify(bug_ids) log.debug("Calling ExternalBugs.remove_external_bug(%s)", params) return self._proxy.ExternalBugs.remove_external_bug(params) @@ -331,7 +332,7 @@ def build_query(self, **kwargs): # support now, so point people to that instead so we don't have # to document and maintain this logic anymore def _warn_bool(kwkey): - vallist = self._listify(kwargs.get(kwkey, None)) + vallist = listify(kwargs.get(kwkey, None)) for value in vallist or []: for s in value.split(" "): if s not in ["|", "&", "!"]: From a155d67339d50b98363373441f632df2454beee9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 1 Jan 2020 15:45:03 -0500 Subject: [PATCH 115/393] transport: Try to give hints if we weren't passed an XMLRPC URL Signed-off-by: Cole Robinson --- bugzilla/transport.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/bugzilla/transport.py b/bugzilla/transport.py index d7502023..6fe66cf3 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -137,6 +137,7 @@ def __init__(self, url, cookiejar=None, 'User-Agent': self.user_agent, } } + self._seen_valid_xml = False # Using an explicit Session, rather than requests.get, will use # HTTP KeepAlive if the server supports it. @@ -159,7 +160,14 @@ def parse_response(self, response): Parse XMLRPC response """ parser, unmarshaller = self.getparser() - parser.feed(response.text.encode('utf-8')) + msg = response.text.encode('utf-8') + try: + parser.feed(msg) + except Exception: + log.debug("Failed to parse this XMLRPC response:\n%s", msg) + raise + + self._seen_valid_xml = True parser.close() return unmarshaller.close() @@ -196,7 +204,10 @@ def _request_helper(self, url, request_body): except Fault: raise except Exception: - e = BugzillaError(str(sys.exc_info()[1])) + msg = str(sys.exc_info()[1]) + if not self._seen_valid_xml: + msg += "\nThe URL may not be an XMLRPC URL: %s" % url + e = BugzillaError(msg) # pylint: disable=attribute-defined-outside-init e.__traceback__ = sys.exc_info()[2] # pylint: enable=attribute-defined-outside-init From e94598c213b5054e1388da0c62edfce366bac0cb Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 1 Jan 2020 15:45:34 -0500 Subject: [PATCH 116/393] tests: Drop bugs.freedesktop.org usage The bugzilla instance is closed now, and the API entry point is gone Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 20 -------------------- xmlrpc-api-notes.txt | 1 - 2 files changed, 21 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index c0f28a29..1fcd3ad3 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -187,26 +187,6 @@ def testURLQuery(self): assert "derived from bugzilla" in str(e) -class BZFDO(BaseTest): - url = "https://bugs.freedesktop.org/xmlrpc.cgi" - bzversion = (5, 0) - closestatus = "CLOSED,RESOLVED" - - test0 = BaseTest._testBZVersion - - test1 = lambda s: BaseTest._testQuery(s, "--product avahi", 10, "3450") - test2 = lambda s: BaseTest._testQueryFull(s, "3450", 10, "Blocked: \n") - test2 = lambda s: BaseTest._testQueryRaw(s, "3450", 30, - "ATTRIBUTE[creator]: daniel@fooishbar.org") - test3 = lambda s: BaseTest._testQueryOneline(s, "3450", - "daniel@fooishbar.org libavahi") - test4 = lambda s: BaseTest._testQueryExtra(s, "3450", "Error") - test5 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 3450 --outputformat " - "\"%{bug_id} %{assigned_to} %{summary}\"", - "3450 daniel@fooishbar.org Error") - - class RHTest(BaseTest): url = (tests.CLICONFIG.REDHAT_URL or "https://bugzilla.redhat.com/xmlrpc.cgi") diff --git a/xmlrpc-api-notes.txt b/xmlrpc-api-notes.txt index ef1ce670..ac38684f 100644 --- a/xmlrpc-api-notes.txt +++ b/xmlrpc-api-notes.txt @@ -15,7 +15,6 @@ Some trackers in the wild to use for API testing: bugzilla.mozilla.org bugzilla.kernel.org bugzilla.gnome.org - bugs.freedesktop.org bugzilla.novell.com bugzilla.zimbra.com bugzilla.samba.org From f3eeea69d5b0d9a4ac82c15b46421d31a0bb676f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 13:59:23 -0500 Subject: [PATCH 117/393] tests: Fix bogus 'assert' Some bad sed'ing from the pytest conversion Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 7aa89090..9c8cac5e 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -943,11 +943,11 @@ def test16ModifyTags(self): tests.clicomm(cmd + "--tags foo --tags +bar --tags baz", bz) bug.refresh() - assert bug.tags, ["foo", "bar" == "baz"] + assert bug.tags == ["foo", "bar", "baz"] tests.clicomm(cmd + "--tags=-bar", bz) bug.refresh() - assert bug.tags, ["foo" == "baz"] + assert bug.tags == ["foo", "baz"] bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() From 19244afc7359310f73854986c4992dfd1c689d07 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jan 2020 13:37:50 -0500 Subject: [PATCH 118/393] tests: Fix latest --rw-functional tests Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 9c8cac5e..b0d700cf 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -858,9 +858,11 @@ def test13SubComponents(self): bug.refresh() assert bug.sub_components == {"lvm2": ["Command-line tools (RHEL5)"]} - bz.update_bugs(bug.id, bz.build_update(sub_component={})) + bz.update_bugs(bug.id, bz.build_update( + component="lvm2", sub_component="Default / Unclassified (RHEL5)")) bug.refresh() - assert bug.sub_components == {} + assert bug.sub_components == {"lvm2": [ + "Default / Unclassified (RHEL5)"]} def test13ExternalTrackerQuery(self): bz = self.bzclass(url=self.url) @@ -890,9 +892,6 @@ def test14ExternalTrackersAddUpdateRemoveQuery(self): 'ext_type_id': 6, 'ext_type_url': url, 'ext_type_description': 'Mozilla Foundation', - 'ext_status': 'Original Status', - 'ext_description': 'the description', - 'ext_priority': 'the priority' } bz.add_external_tracker(bugid, ext_bug_id, **kwargs) added_bug = bz.getbug(bugid).external_bugs[0] @@ -900,9 +899,6 @@ def test14ExternalTrackersAddUpdateRemoveQuery(self): assert added_bug['type']['url'] == kwargs['ext_type_url'] assert (added_bug['type']['description'] == kwargs['ext_type_description']) - assert added_bug['ext_status'] == kwargs['ext_status'] - assert added_bug['ext_description'] == kwargs['ext_description'] - assert added_bug['ext_priority'] == kwargs['ext_priority'] # test updating status, description, and priority by id kwargs = { From c3bcc3e8fa9c379b922e3751b4c22e78a86db60e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jan 2020 18:59:20 -0500 Subject: [PATCH 119/393] transport: Break out _BugzillaSession Which handles all the requests.session interaction. This will eventually be shared with a REST API implementation, and more cleanly separates the XMLRPC specific pieces Signed-off-by: Cole Robinson --- bugzilla/base.py | 13 ++-- bugzilla/transport.py | 152 ++++++++++++++++++++++++++---------------- 2 files changed, 103 insertions(+), 62 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index ca65f706..00e438ca 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -38,7 +38,9 @@ from ._rc import DEFAULT_CONFIGPATHS, open_bugzillarc from .apiversion import __version__ from .bug import Bug, User -from .transport import BugzillaError, _BugzillaServerProxy, _RequestsTransport +from .transport import (BugzillaError, + _BugzillaXMLRPCProxy, + _BugzillaXMLRPCTransport) log = getLogger(__name__) @@ -551,10 +553,11 @@ def connect(self, url=None): url = self.url url = self.fix_url(url) - self._transport = _RequestsTransport( - url, self._cookiejar, sslverify=self._sslverify, cert=self.cert) - self._transport.user_agent = self.user_agent - self._proxy = _BugzillaServerProxy(url, self.tokenfile, + self._transport = _BugzillaXMLRPCTransport(url, self.user_agent, + cookiejar=self._cookiejar, + sslverify=self._sslverify, + cert=self.cert) + self._proxy = _BugzillaXMLRPCProxy(url, self.tokenfile, self._transport) self.url = url diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 6fe66cf3..83ec61f7 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -74,9 +74,9 @@ def __repr__(self): return '' % self.value -class _BugzillaServerProxy(ServerProxy, object): +class _BugzillaXMLRPCProxy(ServerProxy, object): def __init__(self, uri, tokenfile, *args, **kwargs): - super(_BugzillaServerProxy, self).__init__(uri, *args, **kwargs) + ServerProxy.__init__(self, uri, *args, **kwargs) self.token_cache = _BugzillaTokenCache(uri, tokenfile) self.api_key = None @@ -100,8 +100,7 @@ def _ServerProxy__request(self, methodname, params): params[0]['Bugzilla_token'] = self.token_cache.value # pylint: disable=no-member - ret = super(_BugzillaServerProxy, - self)._ServerProxy__request(methodname, params) + ret = ServerProxy._ServerProxy__request(self, methodname, params) # pylint: enable=no-member if isinstance(ret, dict) and 'token' in ret.keys(): @@ -109,90 +108,98 @@ def _ServerProxy__request(self, methodname, params): return ret -class _RequestsTransport(Transport): - user_agent = 'Python/Bugzilla' - - def __init__(self, url, cookiejar=None, - sslverify=True, sslcafile=None, debug=True, cert=None): - if hasattr(Transport, "__init__"): - Transport.__init__(self, use_datetime=False) - - self.verbose = debug +class _BugzillaSession(object): + """ + Class to handle the backend agnostic 'requests' setup + """ + def __init__(self, url, user_agent, + cookiejar=None, sslverify=True, sslcafile=None, cert=None): + self._user_agent = user_agent + self._scheme = urlparse(url)[0] self._cookiejar = cookiejar - # transport constructor needs full url too, as xmlrpc does not pass - # scheme to request - self.scheme = urlparse(url)[0] - if self.scheme not in ["http", "https"]: - raise Exception("Invalid URL scheme: %s (%s)" % (self.scheme, url)) - - self.use_https = self.scheme == 'https' + if self._scheme not in ["http", "https"]: + raise Exception("Invalid URL scheme: %s (%s)" % ( + self._scheme, url)) + use_https = self._scheme == 'https' - self.request_defaults = { - 'cert': sslcafile if self.use_https else None, - 'cookies': cookiejar, + self._request_defaults = { + 'cert': sslcafile if use_https else None, + 'cookies': self._cookiejar, 'verify': sslverify, 'headers': { 'Content-Type': 'text/xml', - 'User-Agent': self.user_agent, + 'User-Agent': self._user_agent, } } - self._seen_valid_xml = False # Using an explicit Session, rather than requests.get, will use # HTTP KeepAlive if the server supports it. - self.session = requests.Session() + self._session = requests.Session() if cert: - self.session.cert = cert + self._session.cert = cert + + def get_user_agent(self): + return self._user_agent + def get_scheme(self): + return self._scheme def set_basic_auth(self, user, password): """ Set basic authentication method. - - :return: """ b64str = str(base64.b64encode("{}:{}".format(user, password))) authstr = "Basic {}".format(b64str.encode("utf-8").decode("utf-8")) - self.request_defaults["headers"]["Authorization"] = authstr + self._request_defaults["headers"]["Authorization"] = authstr - def parse_response(self, response): + def set_response_cookies(self, response): """ - Parse XMLRPC response + Save any cookies received from the passed requests response """ - parser, unmarshaller = self.getparser() - msg = response.text.encode('utf-8') - try: - parser.feed(msg) - except Exception: - log.debug("Failed to parse this XMLRPC response:\n%s", msg) - raise + if self._cookiejar is None: + return - self._seen_valid_xml = True - parser.close() - return unmarshaller.close() + for cookie in response.cookies: + self._cookiejar.set_cookie(cookie) + + if self._cookiejar.filename is not None: + # Save is required only if we have a filename + self._cookiejar.save() + + def post(self, url, data): + return self._session.post(url, data=data, **self._request_defaults) + + +class _BugzillaXMLRPCTransport(Transport): + def __init__(self, *args, **kwargs): + if hasattr(Transport, "__init__"): + Transport.__init__(self, use_datetime=False) - def _request_helper(self, url, request_body): + self.__bugzillasession = _BugzillaSession(*args, **kwargs) + self.__seen_valid_xml = False + + # Override Transport.user_agent + self.user_agent = self.__bugzillasession.get_user_agent() + + + ############################ + # Bugzilla private helpers # + ############################ + + def __request_helper(self, url, request_body): """ - A helper method to assist in making a request and provide a parsed - response. + A helper method to assist in making a request and parsing the response. """ response = None # pylint: disable=try-except-raise try: - response = self.session.post( - url, data=request_body, **self.request_defaults) + response = self.__bugzillasession.post(url, request_body) # We expect utf-8 from the server response.encoding = 'UTF-8' # update/set any cookies - if self._cookiejar is not None: - for cookie in response.cookies: - self._cookiejar.set_cookie(cookie) - - if self._cookiejar.filename is not None: - # Save is required only if we have a filename - self._cookiejar.save() + self.__bugzillasession.set_response_cookies(response) response.raise_for_status() return self.parse_response(response) @@ -205,7 +212,7 @@ def _request_helper(self, url, request_body): raise except Exception: msg = str(sys.exc_info()[1]) - if not self._seen_valid_xml: + if not self.__seen_valid_xml: msg += "\nThe URL may not be an XMLRPC URL: %s" % url e = BugzillaError(msg) # pylint: disable=attribute-defined-outside-init @@ -213,11 +220,42 @@ def _request_helper(self, url, request_body): # pylint: enable=attribute-defined-outside-init raise e + def set_basic_auth(self, user, password): + self.__bugzillasession.set_basic_auth(user, password) + + + ###################### + # Tranport overrides # + ###################### + + def parse_response(self, response): + """ + Override Transport.parse_response + """ + parser, unmarshaller = self.getparser() + msg = response.text.encode('utf-8') + try: + parser.feed(msg) + except Exception: + log.debug("Failed to parse this XMLRPC response:\n%s", msg) + raise + + self.__seen_valid_xml = True + parser.close() + return unmarshaller.close() + def request(self, host, handler, request_body, verbose=0): + """ + Override Transport.request + """ + # Setting self.verbose here matches overrided request() behavior + # pylint: disable=attribute-defined-outside-init self.verbose = verbose - url = "%s://%s%s" % (self.scheme, host, handler) + + url = "%s://%s%s" % (self.__bugzillasession.get_scheme(), + host, handler) # xmlrpclib fails to escape \r request_body = request_body.replace(b'\r', b' ') - return self._request_helper(url, request_body) + return self.__request_helper(url, request_body) From f3a1ba9088b3896092e4e79a486cd07048eed5c1 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jan 2020 19:19:19 -0500 Subject: [PATCH 120/393] transport: Move all auth data into _BugzillaSession The XMLRPC bits now interact with _BugzillaSession as the primary API object. XMLRPCTransport is now an implementation detail of XMLRPCProxy. The Bugzilla base class now instantiates Session directly. This will make it easier to handle XMLRPC vs REST Signed-off-by: Cole Robinson --- bugzilla/base.py | 22 +++++------ bugzilla/transport.py | 87 +++++++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 00e438ca..c89c65af 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -39,8 +39,8 @@ from .apiversion import __version__ from .bug import Bug, User from .transport import (BugzillaError, - _BugzillaXMLRPCProxy, - _BugzillaXMLRPCTransport) + _BugzillaSession, + _BugzillaXMLRPCProxy) log = getLogger(__name__) @@ -299,7 +299,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self.url = '' self._proxy = None - self._transport = None + self._session = None self._cookiejar = None self._sslverify = sslverify self._cache = _BugzillaAPICache() @@ -546,19 +546,20 @@ def connect(self, url=None): If 'user' and 'password' are both set, we'll run login(). Otherwise you'll have to login() yourself before some methods will work. """ - if self._transport: + if self._session: self.disconnect() if url is None and self.url: url = self.url url = self.fix_url(url) - self._transport = _BugzillaXMLRPCTransport(url, self.user_agent, + self._session = _BugzillaSession(url, self.user_agent, cookiejar=self._cookiejar, sslverify=self._sslverify, - cert=self.cert) - self._proxy = _BugzillaXMLRPCProxy(url, self.tokenfile, - self._transport) + cert=self.cert, + tokenfile=self.tokenfile, + api_key=self.api_key) + self._proxy = _BugzillaXMLRPCProxy(url, self._session) self.url = url # we've changed URLs - reload config @@ -570,7 +571,6 @@ def connect(self, url=None): if self.api_key: log.debug("using API key") - self._proxy.use_api_key(self.api_key) version = self._proxy.Bugzilla.version()["version"] log.debug("Bugzilla version string: %s", version) @@ -581,7 +581,7 @@ def disconnect(self): Disconnect from the given bugzilla instance. """ self._proxy = None - self._transport = None + self._session = None self._cache = _BugzillaAPICache() def _login(self, user, password, restrict_login=None): @@ -589,7 +589,7 @@ def _login(self, user, password, restrict_login=None): Backend login method for Bugzilla3 """ if self._basic_auth: - self._transport.set_basic_auth(user, password) + self._session.set_basic_auth(user, password) payload = {'login': user, 'password': password} if restrict_login: diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 83ec61f7..818c4ec3 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -74,49 +74,18 @@ def __repr__(self): return '' % self.value -class _BugzillaXMLRPCProxy(ServerProxy, object): - def __init__(self, uri, tokenfile, *args, **kwargs): - ServerProxy.__init__(self, uri, *args, **kwargs) - self.token_cache = _BugzillaTokenCache(uri, tokenfile) - self.api_key = None - - def use_api_key(self, api_key): - self.api_key = api_key - - def clear_token(self): - self.token_cache.value = None - - def _ServerProxy__request(self, methodname, params): - if len(params) == 0: - params = ({}, ) - - log.debug("XMLRPC call: %s(%s)", methodname, params[0]) - - if self.api_key is not None: - if 'Bugzilla_api_key' not in params[0]: - params[0]['Bugzilla_api_key'] = self.api_key - elif self.token_cache.value is not None: - if 'Bugzilla_token' not in params[0]: - params[0]['Bugzilla_token'] = self.token_cache.value - - # pylint: disable=no-member - ret = ServerProxy._ServerProxy__request(self, methodname, params) - # pylint: enable=no-member - - if isinstance(ret, dict) and 'token' in ret.keys(): - self.token_cache.value = ret.get('token') - return ret - - class _BugzillaSession(object): """ Class to handle the backend agnostic 'requests' setup """ def __init__(self, url, user_agent, - cookiejar=None, sslverify=True, sslcafile=None, cert=None): + cookiejar=None, sslverify=True, sslcafile=None, cert=None, + tokenfile=None, api_key=None): self._user_agent = user_agent self._scheme = urlparse(url)[0] self._cookiejar = cookiejar + self._token_cache = _BugzillaTokenCache(url, tokenfile) + self._api_key = api_key if self._scheme not in ["http", "https"]: raise Exception("Invalid URL scheme: %s (%s)" % ( @@ -143,6 +112,10 @@ def get_user_agent(self): return self._user_agent def get_scheme(self): return self._scheme + def get_api_key(self): + return self._api_key + def get_token_cache(self): + return self._token_cache def set_basic_auth(self, user, password): """ @@ -171,11 +144,11 @@ def post(self, url, data): class _BugzillaXMLRPCTransport(Transport): - def __init__(self, *args, **kwargs): + def __init__(self, bugzillasession): if hasattr(Transport, "__init__"): Transport.__init__(self, use_datetime=False) - self.__bugzillasession = _BugzillaSession(*args, **kwargs) + self.__bugzillasession = bugzillasession self.__seen_valid_xml = False # Override Transport.user_agent @@ -220,9 +193,6 @@ def __request_helper(self, url, request_body): # pylint: enable=attribute-defined-outside-init raise e - def set_basic_auth(self, user, password): - self.__bugzillasession.set_basic_auth(user, password) - ###################### # Tranport overrides # @@ -259,3 +229,40 @@ def request(self, host, handler, request_body, verbose=0): request_body = request_body.replace(b'\r', b' ') return self.__request_helper(url, request_body) + + +class _BugzillaXMLRPCProxy(ServerProxy, object): + """ + Override of xmlrpc ServerProxy, to insert bugzilla API auth + into the XMLRPC request data + """ + def __init__(self, uri, bugzillasession, *args, **kwargs): + self.__bugzillasession = bugzillasession + transport = _BugzillaXMLRPCTransport(self.__bugzillasession) + ServerProxy.__init__(self, uri, transport, *args, **kwargs) + + def _ServerProxy__request(self, methodname, params): + """ + Overrides ServerProxy _request method + """ + if len(params) == 0: + params = ({}, ) + + log.debug("XMLRPC call: %s(%s)", methodname, params[0]) + api_key = self.__bugzillasession.get_api_key() + token_cache = self.__bugzillasession.get_token_cache() + + if api_key is not None: + if 'Bugzilla_api_key' not in params[0]: + params[0]['Bugzilla_api_key'] = api_key + elif token_cache.value is not None: + if 'Bugzilla_token' not in params[0]: + params[0]['Bugzilla_token'] = token_cache.value + + # pylint: disable=no-member + ret = ServerProxy._ServerProxy__request(self, methodname, params) + # pylint: enable=no-member + + if isinstance(ret, dict) and 'token' in ret.keys(): + token_cache.value = ret.get('token') + return ret From 8abd0fdc2804dcdfdce41086cc02ba6a7dd2564c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jan 2020 09:26:29 -0500 Subject: [PATCH 121/393] Move BugzillaError to its own file exceptions.py Rather than grouping it with transport.py, which was only for simplicity Signed-off-by: Cole Robinson --- bugzilla/__init__.py | 2 +- bugzilla/base.py | 5 ++--- bugzilla/exceptions.py | 11 +++++++++++ bugzilla/transport.py | 8 ++------ 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 bugzilla/exceptions.py diff --git a/bugzilla/__init__.py b/bugzilla/__init__.py index 74f55148..bd3df073 100644 --- a/bugzilla/__init__.py +++ b/bugzilla/__init__.py @@ -11,7 +11,7 @@ from .apiversion import version, __version__ from .base import Bugzilla -from .transport import BugzillaError +from .exceptions import BugzillaError from .rhbugzilla import RHBugzilla from .oldclasses import (Bugzilla3, Bugzilla32, Bugzilla34, Bugzilla36, Bugzilla4, Bugzilla42, Bugzilla44, diff --git a/bugzilla/base.py b/bugzilla/base.py index c89c65af..82ed32b1 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -38,9 +38,8 @@ from ._rc import DEFAULT_CONFIGPATHS, open_bugzillarc from .apiversion import __version__ from .bug import Bug, User -from .transport import (BugzillaError, - _BugzillaSession, - _BugzillaXMLRPCProxy) +from .exceptions import BugzillaError +from .transport import _BugzillaSession, _BugzillaXMLRPCProxy log = getLogger(__name__) diff --git a/bugzilla/exceptions.py b/bugzilla/exceptions.py new file mode 100644 index 00000000..ef82e649 --- /dev/null +++ b/bugzilla/exceptions.py @@ -0,0 +1,11 @@ +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + + +class BugzillaError(Exception): + """ + Error raised in the Bugzilla client code. + """ diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 818c4ec3..a062efae 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -21,14 +21,10 @@ import requests - -log = getLogger(__name__) +from .exceptions import BugzillaError -class BugzillaError(Exception): - """ - Error raised in the Bugzilla client code. - """ +log = getLogger(__name__) class _BugzillaTokenCache(object): From 1c193ba1e36a1134ed2441cff4e78d6ee7c8a5c8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 15:36:40 -0500 Subject: [PATCH 122/393] Use consistent minimal license header for code files Signed-off-by: Cole Robinson --- bugzilla/__init__.py | 7 ++----- bugzilla/_cli.py | 7 ++----- bugzilla/_rc.py | 7 ++----- bugzilla/_util.py | 7 ++----- bugzilla/apiversion.py | 7 ++----- bugzilla/base.py | 7 ++----- bugzilla/bug.py | 7 ++----- bugzilla/exceptions.py | 7 ++----- bugzilla/oldclasses.py | 7 ++----- bugzilla/rhbugzilla.py | 7 ++----- bugzilla/transport.py | 7 ++----- examples/apikey.py | 7 ++----- examples/bug_autorefresh.py | 7 ++----- examples/create.py | 7 ++----- examples/getbug.py | 7 ++----- examples/query.py | 7 ++----- examples/update.py | 7 ++----- tests/test_bug.py | 2 +- tests/test_createbug.py | 2 +- tests/test_misc.py | 2 +- tests/test_modify.py | 2 +- tests/test_query.py | 2 +- tests/test_ro_functional.py | 2 +- tests/test_rw_functional.py | 2 +- 24 files changed, 41 insertions(+), 92 deletions(-) diff --git a/bugzilla/__init__.py b/bugzilla/__init__.py index bd3df073..26d4de17 100644 --- a/bugzilla/__init__.py +++ b/bugzilla/__init__.py @@ -3,11 +3,8 @@ # Copyright (C) 2007, 2008 Red Hat Inc. # Author: Will Woods # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. from .apiversion import version, __version__ from .base import Bugzilla diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 326d05a0..4cab0d51 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -6,11 +6,8 @@ # Author: Will Woods # Author: Cole Robinson # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. from __future__ import print_function diff --git a/bugzilla/_rc.py b/bugzilla/_rc.py index 64b388ca..8c1d0972 100644 --- a/bugzilla/_rc.py +++ b/bugzilla/_rc.py @@ -1,8 +1,5 @@ -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. import os import sys diff --git a/bugzilla/_util.py b/bugzilla/_util.py index 796cd497..04555779 100644 --- a/bugzilla/_util.py +++ b/bugzilla/_util.py @@ -1,8 +1,5 @@ -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. def listify(val): diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 6d75a1fe..7d6a67b2 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -1,11 +1,8 @@ # # Copyright (C) 2014 Red Hat Inc. # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. version = "2.3.0" __version__ = version diff --git a/bugzilla/base.py b/bugzilla/base.py index 82ed32b1..bf43519b 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -3,11 +3,8 @@ # Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. # Author: Will Woods # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. import getpass import locale diff --git a/bugzilla/bug.py b/bugzilla/bug.py index d5b581d9..46dd9868 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -3,11 +3,8 @@ # Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. # Author: Will Woods # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. from __future__ import unicode_literals import locale diff --git a/bugzilla/exceptions.py b/bugzilla/exceptions.py index ef82e649..1369290f 100644 --- a/bugzilla/exceptions.py +++ b/bugzilla/exceptions.py @@ -1,8 +1,5 @@ -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. class BugzillaError(Exception): diff --git a/bugzilla/oldclasses.py b/bugzilla/oldclasses.py index 6155355b..87da3f10 100644 --- a/bugzilla/oldclasses.py +++ b/bugzilla/oldclasses.py @@ -1,8 +1,5 @@ -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. from .base import Bugzilla from .rhbugzilla import RHBugzilla diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 15c1e0c7..99a2727c 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -3,11 +3,8 @@ # Copyright (C) 2008-2012 Red Hat Inc. # Author: Will Woods # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. from logging import getLogger diff --git a/bugzilla/transport.py b/bugzilla/transport.py index a062efae..7c150a08 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -1,8 +1,5 @@ -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. import base64 from logging import getLogger diff --git a/examples/apikey.py b/examples/apikey.py index a9bb33b4..ee8a43c9 100644 --- a/examples/apikey.py +++ b/examples/apikey.py @@ -1,10 +1,7 @@ #!/usr/bin/env python # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # create.py: Create a new bug report diff --git a/examples/bug_autorefresh.py b/examples/bug_autorefresh.py index 66984d01..b95ff584 100644 --- a/examples/bug_autorefresh.py +++ b/examples/bug_autorefresh.py @@ -1,10 +1,7 @@ #!/usr/bin/env python # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # bug_autorefresh.py: Show what bug_autorefresh is all about, and explain # how to handle the default change via python-bugzilla in 2016 diff --git a/examples/create.py b/examples/create.py index 727600f4..067a0942 100644 --- a/examples/create.py +++ b/examples/create.py @@ -1,10 +1,7 @@ #!/usr/bin/env python # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # create.py: Create a new bug report diff --git a/examples/getbug.py b/examples/getbug.py index b84a43a0..e532a289 100644 --- a/examples/getbug.py +++ b/examples/getbug.py @@ -1,10 +1,7 @@ #!/usr/bin/env python # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # getbug.py: Simple demonstration of connecting to bugzilla, fetching # a bug, and printing some details. diff --git a/examples/query.py b/examples/query.py index 2da9893b..468d586a 100644 --- a/examples/query.py +++ b/examples/query.py @@ -1,10 +1,7 @@ #!/usr/bin/env python # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # query.py: Perform a few varieties of queries diff --git a/examples/update.py b/examples/update.py index 7e07d266..7de33a34 100644 --- a/examples/update.py +++ b/examples/update.py @@ -1,10 +1,7 @@ #!/usr/bin/env python # -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. # update.py: Make changes to an existing bug diff --git a/tests/test_bug.py b/tests/test_bug.py index 0f28b1ca..d0176ee6 100644 --- a/tests/test_bug.py +++ b/tests/test_bug.py @@ -1,7 +1,7 @@ # # Copyright Red Hat, Inc. 2014 # -# This work is licensed under the terms of the GNU GPL, version 2 or later. +# This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. # diff --git a/tests/test_createbug.py b/tests/test_createbug.py index 23b195b1..a5c3e28b 100644 --- a/tests/test_createbug.py +++ b/tests/test_createbug.py @@ -1,7 +1,7 @@ # # Copyright Red Hat, Inc. 2013 # -# This work is licensed under the terms of the GNU GPL, version 2 or later. +# This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. # diff --git a/tests/test_misc.py b/tests/test_misc.py index 6c5f16a3..9c2e7404 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,7 +1,7 @@ # # Copyright Red Hat, Inc. 2012 # -# This work is licensed under the terms of the GNU GPL, version 2 or later. +# This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. # diff --git a/tests/test_modify.py b/tests/test_modify.py index 09f8589d..1380b8f6 100644 --- a/tests/test_modify.py +++ b/tests/test_modify.py @@ -1,7 +1,7 @@ # # Copyright Red Hat, Inc. 2013 # -# This work is licensed under the terms of the GNU GPL, version 2 or later. +# This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. # diff --git a/tests/test_query.py b/tests/test_query.py index 078254c5..9b47e1eb 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,7 +1,7 @@ # # Copyright Red Hat, Inc. 2012 # -# This work is licensed under the terms of the GNU GPL, version 2 or later. +# This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. # diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 1fcd3ad3..3cce243c 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -3,7 +3,7 @@ # # Copyright Red Hat, Inc. 2012 # -# This work is licensed under the terms of the GNU GPL, version 2 or later. +# This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. # diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index b0d700cf..450b1b2d 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -1,7 +1,7 @@ # # Copyright Red Hat, Inc. 2012 # -# This work is licensed under the terms of the GNU GPL, version 2 or later. +# This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. # From cd059e125f1d1fb12c4fe32c2bd639185181bda1 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Aug 2018 13:44:39 -0400 Subject: [PATCH 123/393] Add _Backend API and _BackendXMLRPC implementation _Backend is a class that provides thin wrappers around raw bugzilla API calls. Implement it for XMLRPC, and convert all the internal code to use it. In the future we will add a REST implementation. Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 234 ++++++++++++++++++++++++++++++++++++ bugzilla/_backendxmlrpc.py | 80 ++++++++++++ bugzilla/base.py | 160 +++++++++++------------- tests/test_rw_functional.py | 4 +- 4 files changed, 383 insertions(+), 95 deletions(-) create mode 100644 bugzilla/_backendbase.py create mode 100644 bugzilla/_backendxmlrpc.py diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py new file mode 100644 index 00000000..388d334d --- /dev/null +++ b/bugzilla/_backendbase.py @@ -0,0 +1,234 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + + +class _BackendBase(object): + """ + Backends are thin wrappers around the different bugzilla API paradigms + (XMLRPC, REST). This base class defines the public API for the rest of + the code, but this is all internal to the library. + """ + def __init__(self, bugzillasession): + self._bugzillasession = bugzillasession + + ################# + # Internal APIs # + ################# + + def get_xmlrpc_proxy(self): + """ + Provides the raw XMLRPC proxy to API users of Bugzilla._proxy + """ + raise NotImplementedError() + + + ###################### + # Bugzilla info APIs # + ###################### + + def bugzilla_version(self): + """ + Fetch bugzilla version string + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bugzilla.html#version + """ + raise NotImplementedError() + + def bugzilla_extensions(self): + """ + Return info about Bugzilla extensions + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bugzilla.html#extensions + """ + raise NotImplementedError() + + + ####################### + # Bug attachment APIs # + ####################### + + def bug_attachment_get(self, attachment_ids, paramdict): + """ + Fetch bug attachments IDs. One part of: + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment + """ + raise NotImplementedError() + + def bug_attachment_get_all(self, bug_ids, paramdict): + """ + Fetch all bug attachments IDs. One part of + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment + """ + raise NotImplementedError() + + def bug_attachment_create(self, paramdict): + """ + Create a bug attachment + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#create-attachment + """ + raise NotImplementedError() + + def bug_attachment_update(self, paramdict): + """ + Update a bug attachment + http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#update-attachment + """ + raise NotImplementedError() + + + ############ + # bug APIs # + ############ + + def bug_comments(self, paramdict): + """ + Fetch bug comments + http://bugzilla.readthedocs.io/en/latest/api/core/v1/comment.html#get-comments + """ + raise NotImplementedError() + + def bug_create(self, paramdict): + """ + Create a new bug + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug + """ + raise NotImplementedError() + + def bug_fields(self, paramdict): + """ + Query available bug field values + http://bugzilla.readthedocs.io/en/latest/api/core/v1/field.html#fields + """ + raise NotImplementedError() + + def bug_get(self, paramdict): + """ + Lookup bug data by ID + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#get-bug + """ + raise NotImplementedError() + + def bug_history(self, paramdict): + """ + Lookup bug history + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#bug-history + """ + raise NotImplementedError() + + def bug_legal_values(self, paramdict): + """ + Old style fields querying + http://bugzilla.readthedocs.io/en/latest/api/core/v1/field.html#legal-values + """ + raise NotImplementedError() + + def bug_search(self, paramdict): + """ + Search/query bugs + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs + """ + raise NotImplementedError() + + def bug_update(self, paramdict): + """ + Update bugs + http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug + """ + raise NotImplementedError() + + def bug_update_tags(self, paramdict): + """ + Update bug tags + https://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#update_tags + """ + raise NotImplementedError() + + + ################## + # Component APIs # + ################## + + def component_create(self, paramdict): + """ + Create component + https://bugzilla.readthedocs.io/en/latest/api/core/v1/component.html#create-component + """ + raise NotImplementedError() + + def component_update(self, paramdict): + """ + Update component + https://bugzilla.readthedocs.io/en/latest/api/core/v1/component.html#update-component + """ + raise NotImplementedError() + + + ################ + # Product APIs # + ################ + + def product_get(self, paramdict): + """ + Fetch product details + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product + """ + raise NotImplementedError() + + def product_get_accessible(self): + """ + List accessible products + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#list-products + """ + raise NotImplementedError() + + def product_get_enterable(self): + """ + List enterable products + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#list-products + """ + raise NotImplementedError() + + def product_get_selectable(self): + """ + List selectable products + http://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#list-products + """ + raise NotImplementedError() + + + ############# + # User APIs # + ############# + + def user_create(self, paramdict): + """ + Create user + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#create-user + """ + raise NotImplementedError() + + def user_get(self, paramdict): + """ + Get user info + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#get-user + """ + raise NotImplementedError() + + def user_login(self, paramdict): + """ + Log in to bugzilla + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#login + """ + raise NotImplementedError() + + def user_logout(self): + """ + Log out of bugzilla + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#logout + """ + raise NotImplementedError() + + def user_update(self, paramdict): + """ + Update user + http://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#update-user + """ + raise NotImplementedError() diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py new file mode 100644 index 00000000..1fa722ca --- /dev/null +++ b/bugzilla/_backendxmlrpc.py @@ -0,0 +1,80 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +from ._backendbase import _BackendBase +from .transport import _BugzillaXMLRPCProxy +from ._util import listify + + +class _BackendXMLRPC(_BackendBase): + """ + Internal interface for direct calls to bugzilla's XMLRPC API + """ + def __init__(self, url, bugzillasession): + _BackendBase.__init__(self, bugzillasession) + self._xmlrpc_proxy = _BugzillaXMLRPCProxy(url, self._bugzillasession) + + def get_xmlrpc_proxy(self): + return self._xmlrpc_proxy + + def bugzilla_version(self): + return self._xmlrpc_proxy.Bugzilla.version()["version"] + def bugzilla_extensions(self): + return self._xmlrpc_proxy.Bugzilla.extensions() + + def bug_attachment_get(self, attachment_ids, paramdict): + data = paramdict.copy() + data["attachment_ids"] = listify(attachment_ids) + return self._xmlrpc_proxy.Bug.attachments(data) + def bug_attachment_get_all(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.attachments(data) + def bug_attachment_create(self, paramdict): + return self._xmlrpc_proxy.Bug.add_attachment(paramdict) + def bug_attachment_update(self, paramdict): + return self._xmlrpc_proxy.Bug.update_attachment(paramdict) + + def bug_comments(self, paramdict): + return self._xmlrpc_proxy.Bug.comments(paramdict) + def bug_create(self, paramdict): + return self._xmlrpc_proxy.Bug.create(paramdict) + def bug_fields(self, paramdict): + return self._xmlrpc_proxy.Bug.fields(paramdict) + def bug_get(self, paramdict): + return self._xmlrpc_proxy.Bug.get(paramdict) + def bug_history(self, paramdict): + return self._xmlrpc_proxy.Bug.history(paramdict) + def bug_legal_values(self, paramdict): + return self._xmlrpc_proxy.Bug.legal_values(paramdict) + def bug_search(self, paramdict): + return self._xmlrpc_proxy.Bug.search(paramdict) + def bug_update(self, paramdict): + return self._xmlrpc_proxy.Bug.update(paramdict) + def bug_update_tags(self, paramdict): + return self._xmlrpc_proxy.Bug.update_tags(paramdict) + + def component_create(self, paramdict): + return self._xmlrpc_proxy.Component.create(paramdict) + def component_update(self, paramdict): + return self._xmlrpc_proxy.Component.update(paramdict) + + def product_get(self, paramdict): + return self._xmlrpc_proxy.Product.get(paramdict) + def product_get_accessible(self): + return self._xmlrpc_proxy.Product.get_accessible_products() + def product_get_enterable(self): + return self._xmlrpc_proxy.Product.get_enterable_products() + def product_get_selectable(self): + return self._xmlrpc_proxy.Product.get_selectable_products() + + def user_create(self, paramdict): + return self._xmlrpc_proxy.User.create(paramdict) + def user_get(self, paramdict): + return self._xmlrpc_proxy.User.get(paramdict) + def user_login(self, paramdict): + return self._xmlrpc_proxy.User.login(paramdict) + def user_logout(self): + return self._xmlrpc_proxy.User.logout() + def user_update(self, paramdict): + return self._xmlrpc_proxy.User.update(paramdict) diff --git a/bugzilla/base.py b/bugzilla/base.py index bf43519b..4388b41a 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -34,9 +34,10 @@ from ._util import listify from ._rc import DEFAULT_CONFIGPATHS, open_bugzillarc from .apiversion import __version__ +from ._backendxmlrpc import _BackendXMLRPC from .bug import Bug, User from .exceptions import BugzillaError -from .transport import _BugzillaSession, _BugzillaXMLRPCProxy +from .transport import _BugzillaSession log = getLogger(__name__) @@ -294,7 +295,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self.cert = cert or '' self.url = '' - self._proxy = None + self._backend = None self._session = None self._cookiejar = None self._sslverify = sslverify @@ -342,7 +343,7 @@ def _init_class_from_url(self): c = RHBugzilla else: try: - extensions = self._proxy.Bugzilla.extensions() + extensions = self._backend.bugzilla_extensions() if "RedHat" in extensions.get('extensions', {}): log.info("Found RedHat bugzilla extension, " "using RHBugzilla") @@ -432,7 +433,7 @@ def _setcookiefile(self, cookiefile): if (self._cookiejar and cookiefile == self._cookiejar.filename): return - if self._proxy is not None: + if self._backend is not None: raise RuntimeError("Can't set cookies with an open connection, " "disconnect() first.") @@ -555,7 +556,7 @@ def connect(self, url=None): cert=self.cert, tokenfile=self.tokenfile, api_key=self.api_key) - self._proxy = _BugzillaXMLRPCProxy(url, self._session) + self._backend = _BackendXMLRPC(url, self._session) self.url = url # we've changed URLs - reload config @@ -568,36 +569,29 @@ def connect(self, url=None): if self.api_key: log.debug("using API key") - version = self._proxy.Bugzilla.version()["version"] + version = self._backend.bugzilla_version() log.debug("Bugzilla version string: %s", version) self._set_bz_version(version) - def disconnect(self): - """ - Disconnect from the given bugzilla instance. - """ - self._proxy = None - self._session = None - self._cache = _BugzillaAPICache() - def _login(self, user, password, restrict_login=None): - """ - Backend login method for Bugzilla3 + @property + def _proxy(self): """ - if self._basic_auth: - self._session.set_basic_auth(user, password) - - payload = {'login': user, 'password': password} - if restrict_login: - payload['restrict_login'] = True + Return an xmlrpc ServerProxy instance that will work seamlessly + with bugzilla - return self._proxy.User.login(payload) + Some apps have historically accessed _proxy directly, like + fedora infrastrucutre pieces. So we consider it part of the API + """ + return self._backend.get_xmlrpc_proxy() - def _logout(self): + def disconnect(self): """ - Backend login method for Bugzilla3 + Disconnect from the given bugzilla instance. """ - return self._proxy.User.logout() + self._backend = None + self._session = None + self._cache = _BugzillaAPICache() def login(self, user=None, password=None, restrict_login=None): """ @@ -634,7 +628,14 @@ def login(self, user=None, password=None, restrict_login=None): log.info("logging in with restrict_login=True") try: - ret = self._login(self.user, self.password, restrict_login) + if self._basic_auth: + self._session.set_basic_auth(user, password) + + payload = {'login': user, 'password': password} + if restrict_login: + payload['restrict_login'] = True + + ret = self._backend.user_login(payload) self.password = '' log.info("login successful for user=%s", self.user) return ret @@ -692,7 +693,7 @@ def logout(self): Log out of bugzilla. Drops server connection and user info, and destroys authentication cookies. """ - self._logout() + self._backend.user_logout() self.disconnect() self.user = '' self.password = '' @@ -717,7 +718,7 @@ def logged_in(self): http://bugzilla.readthedocs.org/en/latest/api/core/v1/user.html#valid-login """ try: - self._proxy.User.get({'ids': []}) + self._backend.user_get({"ids": []}) return True except Fault as e: if e.faultCode == 505 or e.faultCode == 32000: @@ -729,22 +730,19 @@ def logged_in(self): # Bugfields querying # ###################### - def _getbugfields(self): - """ - Get the list of valid fields for Bug objects - """ - r = self._proxy.Bug.fields({'include_fields': ['name']}) - return [f['name'] for f in r['fields']] - def getbugfields(self, force_refresh=False): """ Calls getBugFields, which returns a list of fields in each bug for this bugzilla instance. This can be used to set the list of attrs on the Bug object. """ + def _fieldnames(): + r = self._backend.bug_fields({'include_fields': ['name']}) + return [f['name'] for f in r['fields']] + if force_refresh or not self._cache.bugfields: log.debug("Refreshing bugfields") - self._cache.bugfields = self._getbugfields() + self._cache.bugfields = _fieldnames() self._cache.bugfields.sort() log.debug("bugfields = %s", self._cache.bugfields) @@ -781,11 +779,11 @@ def product_get(self, ids=None, names=None, if ptype: raw = None if ptype == "accessible": - raw = self._proxy.Product.get_accessible_products() - elif ptype == "selectable": - raw = self._proxy.Product.get_selectable_products() + raw = self._backend.product_get_accessible() elif ptype == "enterable": - raw = self._proxy.Product.get_enterable_products() + raw = self._backend.product_get_enterable() + elif ptype == "selectable": + raw = self._backend.product_get_selectable() if raw is None: raise RuntimeError("Unknown ptype=%s" % ptype) @@ -802,7 +800,7 @@ def product_get(self, ids=None, names=None, if exclude_fields: kwargs["exclude_fields"] = exclude_fields - ret = self._proxy.Product.get(kwargs) + ret = self._backend.product_get(kwargs) return ret['products'] def refresh_products(self, **kwargs): @@ -929,7 +927,7 @@ def getcomponents(self, product, force_refresh=False): product_id = proddict["id"] opts = {'product_id': product_id, 'field': 'component'} - names = self._proxy.Bug.legal_values(opts)["values"] + names = self._backend.bug_legal_values(opts)["values"] self._cache.component_names[product_id] = names return self._cache.component_names[product_id] @@ -980,7 +978,7 @@ def addcomponent(self, data): """ data = data.copy() self._component_data_convert(data) - return self._proxy.Component.create(data) + return self._backend.component_create(data) def editcomponent(self, data): """ @@ -991,7 +989,7 @@ def editcomponent(self, data): """ data = data.copy() self._component_data_convert(data, update=True) - return self._proxy.Component.update(data) + return self._backend.component_update(data) ################### @@ -1075,7 +1073,7 @@ def _getbugs(self, idlist, permissive, getbugdata.update(self._process_include_fields( include_fields, exclude_fields, extra_fields)) - r = self._proxy.Bug.get(getbugdata) + r = self._backend.bug_get(getbugdata) if self._check_version(4, 0): bugdict = dict([(b['id'], b) for b in r['bugs']]) @@ -1140,7 +1138,7 @@ def get_comments(self, idlist): Returns a dictionary of bugs and comments. The comments key will be empty. See bugzilla docs for details """ - return self._proxy.Bug.comments({'ids': idlist}) + return self._backend.bug_comments({'ids': idlist}) ################# @@ -1310,7 +1308,7 @@ def query(self, query): implementation. """ try: - r = self._proxy.Bug.search(query) + r = self._backend.bug_search(query) except Fault as e: # Try to give a hint in the error message if url_to_query @@ -1344,7 +1342,7 @@ def bugs_history_raw(self, bug_ids): Experimental. Gets the history of changes for particular bugs in the database. """ - return self._proxy.Bug.history({'ids': bug_ids}) + return self._backend.bug_history({'ids': bug_ids}) ####################################### @@ -1364,7 +1362,7 @@ def update_bugs(self, ids, updates): tmp = updates.copy() tmp["ids"] = listify(ids) - return self._proxy.Bug.update(tmp) + return self._backend.bug_update(tmp) def update_tags(self, idlist, tags_add=None, tags_remove=None): """ @@ -1381,7 +1379,7 @@ def update_tags(self, idlist, tags_add=None, tags_remove=None): "tags": tags, } - return self._proxy.Bug.update_tags(d) + return self._backend.bug_update_tags(d) def update_flags(self, idlist, flags): """ @@ -1608,7 +1606,7 @@ def attachfile(self, idlist, attachfile, description, **kwargs): kwargs['file_name'], strict=False)[0] kwargs['content_type'] = ctype or 'application/octet-stream' - ret = self._proxy.Bug.add_attachment(kwargs) + ret = self._backend.bug_attachment_create(kwargs) if "attachments" in ret: # Up to BZ 4.2 @@ -1652,7 +1650,7 @@ def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): flags.update(kwargs) update = {'ids': [int(attachid)], 'flags': [flags]} - return self._proxy.Bug.update_attachment(update) + return self._backend.bug_attachment_update(update) def get_attachments(self, ids, attachment_ids, include_fields=None, exclude_fields=None): @@ -1664,16 +1662,15 @@ def get_attachments(self, ids, attachment_ids, https://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment """ - params = { - "ids": listify(ids) or [], - "attachment_ids": listify(attachment_ids) or [], - } + params = {} if include_fields: params["include_fields"] = listify(include_fields) if exclude_fields: params["exclude_fields"] = listify(exclude_fields) - return self._proxy.Bug.attachments(params) + if attachment_ids: + return self._backend.bug_attachment_get(attachment_ids, params) + return self._backend.bug_attachment_get_all(ids, params) ##################### @@ -1785,7 +1782,7 @@ def createbug(self, *args, **kwargs): be passed. """ data = self._validate_createbug(*args, **kwargs) - rawbug = self._proxy.Bug.create(data) + rawbug = self._backend.bug_create(data) return Bug(self, bug_id=rawbug["id"], autorefresh=self.bug_autorefresh) @@ -1794,36 +1791,6 @@ def createbug(self, *args, **kwargs): # Methods for handling Users # ############################## - def _getusers(self, ids=None, names=None, match=None): - """ - Return a list of users that match criteria. - - :kwarg ids: list of user ids to return data on - :kwarg names: list of user names to return data on - :kwarg match: list of patterns. Returns users whose real name or - login name match the pattern. - :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the - names array. - Code 304: if the user was not authorized to see user they - requested. - Code 505: user is logged out and can't use the match or ids - parameter. - - Available in Bugzilla-3.4+ - """ - params = {} - if ids: - params['ids'] = listify(ids) - if names: - params['names'] = listify(names) - if match: - params['match'] = listify(match) - if not params: - raise BugzillaError('_get() needs one of ids, ' - ' names, or match kwarg.') - - return self._proxy.User.get(params) - def getuser(self, username): """ Return a bugzilla User for the given username @@ -1842,8 +1809,9 @@ def getusers(self, userlist): :userlist: List of usernames to lookup :returns: List of User records """ + rawusers = self._backend.user_get({"names": listify(userlist)}) userobjs = [User(self, **rawuser) for rawuser in - self._getusers(names=userlist).get('users', [])] + rawusers.get('users', [])] # Return users in same order they were passed in ret = [] @@ -1864,8 +1832,9 @@ def searchusers(self, pattern): :arg pattern: List of patterns to match against. :returns: List of User records """ + rawusers = self._backend.user_get({"match": listify(pattern)}) return [User(self, **rawuser) for rawuser in - self._getusers(match=pattern).get('users', [])] + rawusers.get('users', [])] def createuser(self, email, name='', password=''): """ @@ -1880,7 +1849,12 @@ def createuser(self, email, name='', password=''): Code 503 if the password is too long :return: User record for the username """ - self._proxy.User.create(email, name, password) + args = {"email": email} + if name: + args["name"] = name + if password: + args["password"] = password + self._backend.user_create(args) return self.getuser(email) def updateperms(self, user, action, groups): @@ -1906,4 +1880,4 @@ def updateperms(self, user, action, groups): } } - return self._proxy.User.update(update) + return self._backend.user_update(update) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 450b1b2d..180bb639 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -770,8 +770,8 @@ def test11ComponentEditing(self): have_admin = self._check_have_admin(bz, fn) def compare(data, newid): - proxy = bz._proxy # pylint: disable=protected-access - products = proxy.Product.get({"names": [basedata["product"]]}) + # pylint: disable=protected-access + products = bz._proxy.Product.get({"names": [basedata["product"]]}) compdata = None for c in products["products"][0]["components"]: if int(c["id"]) == int(newid): From 649aa78288c6c339f6ace5d78c35ec4275371395 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:02:14 -0500 Subject: [PATCH 124/393] cli: attachments: Don't call getbugs with empty bug list Which happens when opt.getall is empty. The XMLRPC API doesn't care, but the REST API errors Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 4cab0d51..30572432 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -976,8 +976,9 @@ def _do_modify(bz, parser, opt): def _do_get_attach(bz, opt): - for bug in bz.getbugs(opt.getall): - opt.get += bug.get_attachment_ids() + if opt.getall: + for bug in bz.getbugs(opt.getall): + opt.get += bug.get_attachment_ids() for attid in set(opt.get): if opt.ignore_obsolete: From aa0a0d6f30371842f10d5db854ab1a817c1e47ee Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:03:31 -0500 Subject: [PATCH 125/393] cli: Drop the explicit xmlrpc.cgi from DEFAULT_URL In the future bugzilla will drop the XMLRPC API. Let's be safe and not hardcode this path, and depend on Bugzilla infrastructure to figure out the correct entrypoint for us Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 30572432..c6e6ba00 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -36,7 +36,7 @@ import bugzilla -DEFAULT_BZ = 'https://bugzilla.redhat.com/xmlrpc.cgi' +DEFAULT_BZ = 'https://bugzilla.redhat.com' format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}") From d2f983d0fd5fd3957cfc0f9d264a830795e40d6f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:11:09 -0500 Subject: [PATCH 126/393] Rename _rc.py to _authfiles.py We will move more code there shortly Signed-off-by: Cole Robinson --- bugzilla/{_rc.py => _authfiles.py} | 0 bugzilla/_cli.py | 2 +- bugzilla/base.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename bugzilla/{_rc.py => _authfiles.py} (100%) diff --git a/bugzilla/_rc.py b/bugzilla/_authfiles.py similarity index 100% rename from bugzilla/_rc.py rename to bugzilla/_authfiles.py diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index c6e6ba00..ba8053ca 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -98,7 +98,7 @@ def get_default_url(): """ Grab a default URL from bugzillarc [DEFAULT] url=X """ - from bugzilla._rc import open_bugzillarc + from bugzilla._authfiles import open_bugzillarc cfg = open_bugzillarc() if cfg: cfgurl = cfg.defaults().get("url", None) diff --git a/bugzilla/base.py b/bugzilla/base.py index 4388b41a..72d37515 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -31,13 +31,13 @@ # pylint: enable=import-error,no-name-in-module,ungrouped-imports -from ._util import listify -from ._rc import DEFAULT_CONFIGPATHS, open_bugzillarc +from ._authfiles import DEFAULT_CONFIGPATHS, open_bugzillarc from .apiversion import __version__ from ._backendxmlrpc import _BackendXMLRPC from .bug import Bug, User from .exceptions import BugzillaError from .transport import _BugzillaSession +from ._util import listify log = getLogger(__name__) From 53c8aff55802bba3aa6c9362ea1b1e2e75d0b84b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:16:36 -0500 Subject: [PATCH 127/393] Move _BugzillaTokenCache to _authfiles.py Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 45 +++++++++++++++++++++++++++++++++++++++++ bugzilla/transport.py | 46 +----------------------------------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 8c1d0972..380dfaf6 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -8,7 +8,9 @@ # pylint: disable=import-error,no-name-in-module,ungrouped-imports if sys.version_info[0] >= 3: from configparser import ConfigParser + from urllib.parse import urlparse # pylint: disable=no-name-in-module else: + from urlparse import urlparse from ConfigParser import SafeConfigParser as ConfigParser # pylint: enable=import-error,no-name-in-module,ungrouped-imports @@ -38,3 +40,46 @@ def open_bugzillarc(configpaths=-1): log.info("Found bugzillarc files: %s", read_files) return cfg + + +class _BugzillaTokenCache(object): + """ + Cache for tokens, including, with apologies for the duplicative + terminology, both Bugzilla Tokens and API Keys. + """ + + def __init__(self, uri, tokenfilename): + self.tokenfilename = tokenfilename + self.tokenfile = ConfigParser() + self.domain = urlparse(uri)[1] + + if self.tokenfilename: + self.tokenfile.read(self.tokenfilename) + + if self.domain not in self.tokenfile.sections(): + self.tokenfile.add_section(self.domain) + + @property + def value(self): + if self.tokenfile.has_option(self.domain, 'token'): + return self.tokenfile.get(self.domain, 'token') + else: + return None + + @value.setter + def value(self, value): + if self.value == value: + return + + if value is None: + self.tokenfile.remove_option(self.domain, 'token') + else: + self.tokenfile.set(self.domain, 'token', value) + + if self.tokenfilename: + with open(self.tokenfilename, 'w') as tokenfile: + log.debug("Saving to tokenfile") + self.tokenfile.write(tokenfile) + + def __repr__(self): + return '' % self.value diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 7c150a08..90519121 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -7,66 +7,22 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: - from configparser import ConfigParser from urllib.parse import urlparse # pylint: disable=no-name-in-module from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport else: - from ConfigParser import SafeConfigParser as ConfigParser from urlparse import urlparse from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport # pylint: enable=import-error import requests +from ._authfiles import _BugzillaTokenCache from .exceptions import BugzillaError log = getLogger(__name__) -class _BugzillaTokenCache(object): - """ - Cache for tokens, including, with apologies for the duplicative - terminology, both Bugzilla Tokens and API Keys. - """ - - def __init__(self, uri, tokenfilename): - self.tokenfilename = tokenfilename - self.tokenfile = ConfigParser() - self.domain = urlparse(uri)[1] - - if self.tokenfilename: - self.tokenfile.read(self.tokenfilename) - - if self.domain not in self.tokenfile.sections(): - self.tokenfile.add_section(self.domain) - - @property - def value(self): - if self.tokenfile.has_option(self.domain, 'token'): - return self.tokenfile.get(self.domain, 'token') - else: - return None - - @value.setter - def value(self, value): - if self.value == value: - return - - if value is None: - self.tokenfile.remove_option(self.domain, 'token') - else: - self.tokenfile.set(self.domain, 'token', value) - - if self.tokenfilename: - with open(self.tokenfilename, 'w') as tokenfile: - log.debug("Saving to tokenfile") - self.tokenfile.write(tokenfile) - - def __repr__(self): - return '' % self.value - - class _BugzillaSession(object): """ Class to handle the backend agnostic 'requests' setup From 1ff1ad72bcc8b10d8b8b29e1d0783680ac426b56 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:22:53 -0500 Subject: [PATCH 128/393] authfiles: Slightly rework _BugzillaTokenCache API * Privatize more details * Use explicit methods rather than getter/setter properties * Hide the token cache object within the _BugzillaSession Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 49 +++++++++++++++++++----------------------- bugzilla/transport.py | 14 ++++++------ 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 380dfaf6..c6fdcc30 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -44,42 +44,37 @@ def open_bugzillarc(configpaths=-1): class _BugzillaTokenCache(object): """ - Cache for tokens, including, with apologies for the duplicative - terminology, both Bugzilla Tokens and API Keys. + Class for interacting with a .bugzillatoken cache file """ + def __init__(self, uri, filename): + self._filename = filename + self._cfg = ConfigParser() + self._domain = urlparse(uri)[1] - def __init__(self, uri, tokenfilename): - self.tokenfilename = tokenfilename - self.tokenfile = ConfigParser() - self.domain = urlparse(uri)[1] + if self._filename: + self._cfg.read(self._filename) - if self.tokenfilename: - self.tokenfile.read(self.tokenfilename) + if self._domain not in self._cfg.sections(): + self._cfg.add_section(self._domain) - if self.domain not in self.tokenfile.sections(): - self.tokenfile.add_section(self.domain) + def get_value(self): + if self._cfg.has_option(self._domain, 'token'): + return self._cfg.get(self._domain, 'token') + return None - @property - def value(self): - if self.tokenfile.has_option(self.domain, 'token'): - return self.tokenfile.get(self.domain, 'token') - else: - return None - - @value.setter - def value(self, value): - if self.value == value: + def set_value(self, value): + if self.get_value() == value: return if value is None: - self.tokenfile.remove_option(self.domain, 'token') + self._cfg.remove_option(self._domain, 'token') else: - self.tokenfile.set(self.domain, 'token', value) + self._cfg.set(self._domain, 'token', value) - if self.tokenfilename: - with open(self.tokenfilename, 'w') as tokenfile: - log.debug("Saving to tokenfile") - self.tokenfile.write(tokenfile) + if self._filename: + with open(self._filename, 'w') as _cfg: + log.debug("Saving to _cfg") + self._cfg.write(_cfg) def __repr__(self): - return '' % self.value + return '' % self.get_value() diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 90519121..58f118a8 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -63,8 +63,10 @@ def get_scheme(self): return self._scheme def get_api_key(self): return self._api_key - def get_token_cache(self): - return self._token_cache + def get_token_value(self): + return self._token_cache.get_value() + def set_token_value(self, value): + return self._token_cache.set_value(value) def set_basic_auth(self, user, password): """ @@ -199,19 +201,19 @@ def _ServerProxy__request(self, methodname, params): log.debug("XMLRPC call: %s(%s)", methodname, params[0]) api_key = self.__bugzillasession.get_api_key() - token_cache = self.__bugzillasession.get_token_cache() + token_value = self.__bugzillasession.get_token_value() if api_key is not None: if 'Bugzilla_api_key' not in params[0]: params[0]['Bugzilla_api_key'] = api_key - elif token_cache.value is not None: + elif token_value is not None: if 'Bugzilla_token' not in params[0]: - params[0]['Bugzilla_token'] = token_cache.value + params[0]['Bugzilla_token'] = token_value # pylint: disable=no-member ret = ServerProxy._ServerProxy__request(self, methodname, params) # pylint: enable=no-member if isinstance(ret, dict) and 'token' in ret.keys(): - token_cache.value = ret.get('token') + self.__bugzillasession.set_token_value(ret.get('token')) return ret From 82186138d5af922f358c7e8664966b549860ce56 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:26:42 -0500 Subject: [PATCH 129/393] transport: Drop sslcafile It's never been exposed to API users since it was first introduced, so it's all dead code Signed-off-by: Cole Robinson --- bugzilla/transport.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 58f118a8..04e12a0c 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -28,7 +28,7 @@ class _BugzillaSession(object): Class to handle the backend agnostic 'requests' setup """ def __init__(self, url, user_agent, - cookiejar=None, sslverify=True, sslcafile=None, cert=None, + cookiejar=None, sslverify=True, cert=None, tokenfile=None, api_key=None): self._user_agent = user_agent self._scheme = urlparse(url)[0] @@ -42,7 +42,6 @@ def __init__(self, url, user_agent, use_https = self._scheme == 'https' self._request_defaults = { - 'cert': sslcafile if use_https else None, 'cookies': self._cookiejar, 'verify': sslverify, 'headers': { From 7bb8cb38206255e858923ccadb78e081e4366ce7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:35:28 -0500 Subject: [PATCH 130/393] transport: Set options directly on RequestsSession Rather than track data as a dict() that we pass to each request. This lets internal callers interact directly with the requests Session without us having to add wrapper APIs for every action Signed-off-by: Cole Robinson --- bugzilla/transport.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/bugzilla/transport.py b/bugzilla/transport.py index 04e12a0c..bc4c13bf 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -39,22 +39,18 @@ def __init__(self, url, user_agent, if self._scheme not in ["http", "https"]: raise Exception("Invalid URL scheme: %s (%s)" % ( self._scheme, url)) - use_https = self._scheme == 'https' - - self._request_defaults = { - 'cookies': self._cookiejar, - 'verify': sslverify, - 'headers': { - 'Content-Type': 'text/xml', - 'User-Agent': self._user_agent, - } - } - - # Using an explicit Session, rather than requests.get, will use - # HTTP KeepAlive if the server supports it. + self._session = requests.Session() if cert: self._session.cert = cert + if self._cookiejar: + self._session.cookies = self._cookiejar + + self._session.verify = sslverify + self._session.headers["User-Agent"] = self._user_agent + self._session.headers["Content-Type"] = 'text/xml' + self._session.params["Bugzilla_api_key"] = self._api_key + self._set_token_cache_param() def get_user_agent(self): return self._user_agent @@ -65,7 +61,11 @@ def get_api_key(self): def get_token_value(self): return self._token_cache.get_value() def set_token_value(self, value): - return self._token_cache.set_value(value) + self._token_cache.set_value(value) + self._set_token_cache_param() + + def _set_token_cache_param(self): + self._session.params["Bugzilla_token"] = self._token_cache.get_value() def set_basic_auth(self, user, password): """ @@ -73,7 +73,7 @@ def set_basic_auth(self, user, password): """ b64str = str(base64.b64encode("{}:{}".format(user, password))) authstr = "Basic {}".format(b64str.encode("utf-8").decode("utf-8")) - self._request_defaults["headers"]["Authorization"] = authstr + self._session.headers["Authorization"] = authstr def set_response_cookies(self, response): """ @@ -89,8 +89,8 @@ def set_response_cookies(self, response): # Save is required only if we have a filename self._cookiejar.save() - def post(self, url, data): - return self._session.post(url, data=data, **self._request_defaults) + def get_requests_session(self): + return self._session class _BugzillaXMLRPCTransport(Transport): @@ -116,7 +116,8 @@ def __request_helper(self, url, request_body): response = None # pylint: disable=try-except-raise try: - response = self.__bugzillasession.post(url, request_body) + session = self.__bugzillasession.get_requests_session() + response = session.post(url, data=request_body) # We expect utf-8 from the server response.encoding = 'UTF-8' From 6df3ec7144cb18387d665c68a4794cec03657461 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:46:41 -0500 Subject: [PATCH 131/393] base: Move cookie and rcfile handling to _authfiles.py Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 84 +++++++++++++++++++++++++++++++++++++++- bugzilla/base.py | 87 ++---------------------------------------- 2 files changed, 86 insertions(+), 85 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index c6fdcc30..6927802b 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -8,12 +8,15 @@ # pylint: disable=import-error,no-name-in-module,ungrouped-imports if sys.version_info[0] >= 3: from configparser import ConfigParser + from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse # pylint: disable=no-name-in-module else: - from urlparse import urlparse from ConfigParser import SafeConfigParser as ConfigParser + from cookielib import LoadError, MozillaCookieJar + from urlparse import urlparse # pylint: enable=import-error,no-name-in-module,ungrouped-imports +from .exceptions import BugzillaError from ._util import listify log = getLogger(__name__) @@ -78,3 +81,82 @@ def set_value(self, value): def __repr__(self): return '' % self.get_value() + + +def _parse_hostname(url): + # If http://example.com is passed, netloc=example.com path="" + # If just example.com is passed, netloc="" path=example.com + parsedbits = urlparse(url) + return parsedbits.netloc or parsedbits.path + + +def _default_location(filename, kind): + """ + Determine default location for filename, like 'bugzillacookies'. If + old style ~/.bugzillacookies exists, we use that, otherwise we + use ~/.cache/python-bugzilla/bugzillacookies. + Same for bugzillatoken and bugzillarc + """ + homepath = os.path.expanduser("~/.%s" % filename) + xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) + if os.path.exists(xdgpath): + return xdgpath + if os.path.exists(homepath): + return homepath + + if not os.path.exists(os.path.dirname(xdgpath)): + os.makedirs(os.path.dirname(xdgpath), 0o700) + return xdgpath + + +def _default_cache_location(filename): + return _default_location(filename, 'cache') + + +def _default_config_location(filename): + return _default_location(filename, 'config') + + +def _save_api_key(url, api_key): + """ + Save the API_KEY in the config file. + + If tokenfile and cookiefile are undefined, it means that the + API was called with --no-cache-credentials and no change will be + made + """ + config_filename = _default_config_location('bugzillarc') + section = _parse_hostname(url) + + cfg = ConfigParser() + cfg.read(config_filename) + + if section not in cfg.sections(): + cfg.add_section(section) + + cfg[section]['api_key'] = api_key.strip() + + with open(config_filename, 'w') as configfile: + cfg.write(configfile) + + log.info("API key written to %s", config_filename) + print("API key written to %s" % config_filename) + + +def _build_cookiejar(cookiefile): + cj = MozillaCookieJar(cookiefile) + if cookiefile is None: + return cj + if not os.path.exists(cookiefile): + # Make sure a new file has correct permissions + open(cookiefile, 'a').close() + os.chmod(cookiefile, 0o600) + cj.save() + return cj + + try: + cj.load() + return cj + except LoadError: + raise BugzillaError("cookiefile=%s not in Mozilla format" % + cookiefile) diff --git a/bugzilla/base.py b/bugzilla/base.py index 72d37515..4c1cf442 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -18,20 +18,18 @@ # pylint: disable=import-error,no-name-in-module,ungrouped-imports if sys.version_info[0] >= 3: from collections.abc import Mapping - from configparser import ConfigParser - from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse, urlunparse, parse_qsl from xmlrpc.client import Binary, Fault else: from collections import Mapping - from ConfigParser import SafeConfigParser as ConfigParser - from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse, urlunparse, parse_qsl from xmlrpclib import Binary, Fault # pylint: enable=import-error,no-name-in-module,ungrouped-imports -from ._authfiles import DEFAULT_CONFIGPATHS, open_bugzillarc +from ._authfiles import (DEFAULT_CONFIGPATHS, open_bugzillarc, + _build_cookiejar, _default_cache_location, + _parse_hostname, _save_api_key) from .apiversion import __version__ from ._backendxmlrpc import _BackendXMLRPC from .bug import Bug, User @@ -43,13 +41,6 @@ log = getLogger(__name__) -def _parse_hostname(url): - # If http://example.com is passed, netloc=example.com path="" - # If just example.com is passed, netloc="" path=example.com - parsedbits = urlparse(url) - return parsedbits.netloc or parsedbits.path - - def _nested_update(d, u): # Helper for nested dict update() for k, v in list(u.items()): @@ -60,78 +51,6 @@ def _nested_update(d, u): return d -def _default_location(filename, kind): - """ - Determine default location for filename, like 'bugzillacookies'. If - old style ~/.bugzillacookies exists, we use that, otherwise we - use ~/.cache/python-bugzilla/bugzillacookies. - Same for bugzillatoken and bugzillarc - """ - homepath = os.path.expanduser("~/.%s" % filename) - xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) - if os.path.exists(xdgpath): - return xdgpath - if os.path.exists(homepath): - return homepath - - if not os.path.exists(os.path.dirname(xdgpath)): - os.makedirs(os.path.dirname(xdgpath), 0o700) - return xdgpath - - -def _default_cache_location(filename): - return _default_location(filename, 'cache') - - -def _default_config_location(filename): - return _default_location(filename, 'config') - - -def _build_cookiejar(cookiefile): - cj = MozillaCookieJar(cookiefile) - if cookiefile is None: - return cj - if not os.path.exists(cookiefile): - # Make sure a new file has correct permissions - open(cookiefile, 'a').close() - os.chmod(cookiefile, 0o600) - cj.save() - return cj - - try: - cj.load() - return cj - except LoadError: - raise BugzillaError("cookiefile=%s not in Mozilla format" % - cookiefile) - - -def _save_api_key(url, api_key): - """ - Save the API_KEY in the config file. - - If tokenfile and cookiefile are undefined, it means that the - API was called with --no-cache-credentials and no change will be - made - """ - config_filename = _default_config_location('bugzillarc') - section = _parse_hostname(url) - - cfg = ConfigParser() - cfg.read(config_filename) - - if section not in cfg.sections(): - cfg.add_section(section) - - cfg[section]['api_key'] = api_key.strip() - - with open(config_filename, 'w') as configfile: - cfg.write(configfile) - - log.info("API key written to %s", config_filename) - print("API key written to %s" % config_filename) - - class _FieldAlias(object): """ Track API attribute names that differ from what we expose in users. From 0f745a335be74a9ce752f5839d4627ba6ad34918 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:50:32 -0500 Subject: [PATCH 132/393] tox: Tweak come default coverage options Signed-off-by: Cole Robinson --- tox.ini | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index d81e3403..11cb1d7e 100644 --- a/tox.ini +++ b/tox.ini @@ -15,10 +15,15 @@ addopts = -q --tb=native [coverage:run] -omit = - /*/tests/* - /usr/* - *.tox/* +source = bugzilla/ +[coverage:report] +skip_covered = yes +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError [pycodestyle] From 7dab079e99d7ebef6bf9794afcbdee0a2ab8464d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:58:25 -0500 Subject: [PATCH 133/393] transport: Move all XMLRPC specific pieces to _backendxmlrpc.py Signed-off-by: Cole Robinson --- bugzilla/_backendxmlrpc.py | 143 ++++++++++++++++++++++++++++++++++++- bugzilla/transport.py | 129 --------------------------------- 2 files changed, 142 insertions(+), 130 deletions(-) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 1fa722ca..10379b60 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -1,11 +1,152 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +from logging import getLogger +import sys + +# pylint: disable=import-error +if sys.version_info[0] >= 3: + from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport +else: + from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport +# pylint: enable=import-error + +from requests import RequestException + from ._backendbase import _BackendBase -from .transport import _BugzillaXMLRPCProxy +from .exceptions import BugzillaError from ._util import listify +log = getLogger(__name__) + + +class _BugzillaXMLRPCTransport(Transport): + def __init__(self, bugzillasession): + if hasattr(Transport, "__init__"): + Transport.__init__(self, use_datetime=False) + + self.__bugzillasession = bugzillasession + self.__seen_valid_xml = False + + # Override Transport.user_agent + self.user_agent = self.__bugzillasession.get_user_agent() + + + ############################ + # Bugzilla private helpers # + ############################ + + def __request_helper(self, url, request_body): + """ + A helper method to assist in making a request and parsing the response. + """ + response = None + # pylint: disable=try-except-raise + try: + session = self.__bugzillasession.get_requests_session() + response = session.post(url, data=request_body) + + # We expect utf-8 from the server + response.encoding = 'UTF-8' + + # update/set any cookies + self.__bugzillasession.set_response_cookies(response) + + response.raise_for_status() + return self.parse_response(response) + except RequestException as e: + if not response: + raise + raise ProtocolError( + url, response.status_code, str(e), response.headers) + except Fault: + raise + except Exception: + msg = str(sys.exc_info()[1]) + if not self.__seen_valid_xml: + msg += "\nThe URL may not be an XMLRPC URL: %s" % url + e = BugzillaError(msg) + # pylint: disable=attribute-defined-outside-init + e.__traceback__ = sys.exc_info()[2] + # pylint: enable=attribute-defined-outside-init + raise e + + + ###################### + # Tranport overrides # + ###################### + + def parse_response(self, response): + """ + Override Transport.parse_response + """ + parser, unmarshaller = self.getparser() + msg = response.text.encode('utf-8') + try: + parser.feed(msg) + except Exception: + log.debug("Failed to parse this XMLRPC response:\n%s", msg) + raise + + self.__seen_valid_xml = True + parser.close() + return unmarshaller.close() + + def request(self, host, handler, request_body, verbose=0): + """ + Override Transport.request + """ + # Setting self.verbose here matches overrided request() behavior + # pylint: disable=attribute-defined-outside-init + self.verbose = verbose + + url = "%s://%s%s" % (self.__bugzillasession.get_scheme(), + host, handler) + + # xmlrpclib fails to escape \r + request_body = request_body.replace(b'\r', b' ') + + return self.__request_helper(url, request_body) + + +class _BugzillaXMLRPCProxy(ServerProxy, object): + """ + Override of xmlrpc ServerProxy, to insert bugzilla API auth + into the XMLRPC request data + """ + def __init__(self, uri, bugzillasession, *args, **kwargs): + self.__bugzillasession = bugzillasession + transport = _BugzillaXMLRPCTransport(self.__bugzillasession) + ServerProxy.__init__(self, uri, transport, *args, **kwargs) + + def _ServerProxy__request(self, methodname, params): + """ + Overrides ServerProxy _request method + """ + if len(params) == 0: + params = ({}, ) + + log.debug("XMLRPC call: %s(%s)", methodname, params[0]) + api_key = self.__bugzillasession.get_api_key() + token_value = self.__bugzillasession.get_token_value() + + if api_key is not None: + if 'Bugzilla_api_key' not in params[0]: + params[0]['Bugzilla_api_key'] = api_key + elif token_value is not None: + if 'Bugzilla_token' not in params[0]: + params[0]['Bugzilla_token'] = token_value + + # pylint: disable=no-member + ret = ServerProxy._ServerProxy__request(self, methodname, params) + # pylint: enable=no-member + + if isinstance(ret, dict) and 'token' in ret.keys(): + self.__bugzillasession.set_token_value(ret.get('token')) + return ret + + class _BackendXMLRPC(_BackendBase): """ Internal interface for direct calls to bugzilla's XMLRPC API diff --git a/bugzilla/transport.py b/bugzilla/transport.py index bc4c13bf..544f10f1 100644 --- a/bugzilla/transport.py +++ b/bugzilla/transport.py @@ -8,16 +8,13 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: from urllib.parse import urlparse # pylint: disable=no-name-in-module - from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport else: from urlparse import urlparse - from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport # pylint: enable=import-error import requests from ._authfiles import _BugzillaTokenCache -from .exceptions import BugzillaError log = getLogger(__name__) @@ -91,129 +88,3 @@ def set_response_cookies(self, response): def get_requests_session(self): return self._session - - -class _BugzillaXMLRPCTransport(Transport): - def __init__(self, bugzillasession): - if hasattr(Transport, "__init__"): - Transport.__init__(self, use_datetime=False) - - self.__bugzillasession = bugzillasession - self.__seen_valid_xml = False - - # Override Transport.user_agent - self.user_agent = self.__bugzillasession.get_user_agent() - - - ############################ - # Bugzilla private helpers # - ############################ - - def __request_helper(self, url, request_body): - """ - A helper method to assist in making a request and parsing the response. - """ - response = None - # pylint: disable=try-except-raise - try: - session = self.__bugzillasession.get_requests_session() - response = session.post(url, data=request_body) - - # We expect utf-8 from the server - response.encoding = 'UTF-8' - - # update/set any cookies - self.__bugzillasession.set_response_cookies(response) - - response.raise_for_status() - return self.parse_response(response) - except requests.RequestException as e: - if not response: - raise - raise ProtocolError( - url, response.status_code, str(e), response.headers) - except Fault: - raise - except Exception: - msg = str(sys.exc_info()[1]) - if not self.__seen_valid_xml: - msg += "\nThe URL may not be an XMLRPC URL: %s" % url - e = BugzillaError(msg) - # pylint: disable=attribute-defined-outside-init - e.__traceback__ = sys.exc_info()[2] - # pylint: enable=attribute-defined-outside-init - raise e - - - ###################### - # Tranport overrides # - ###################### - - def parse_response(self, response): - """ - Override Transport.parse_response - """ - parser, unmarshaller = self.getparser() - msg = response.text.encode('utf-8') - try: - parser.feed(msg) - except Exception: - log.debug("Failed to parse this XMLRPC response:\n%s", msg) - raise - - self.__seen_valid_xml = True - parser.close() - return unmarshaller.close() - - def request(self, host, handler, request_body, verbose=0): - """ - Override Transport.request - """ - # Setting self.verbose here matches overrided request() behavior - # pylint: disable=attribute-defined-outside-init - self.verbose = verbose - - url = "%s://%s%s" % (self.__bugzillasession.get_scheme(), - host, handler) - - # xmlrpclib fails to escape \r - request_body = request_body.replace(b'\r', b' ') - - return self.__request_helper(url, request_body) - - -class _BugzillaXMLRPCProxy(ServerProxy, object): - """ - Override of xmlrpc ServerProxy, to insert bugzilla API auth - into the XMLRPC request data - """ - def __init__(self, uri, bugzillasession, *args, **kwargs): - self.__bugzillasession = bugzillasession - transport = _BugzillaXMLRPCTransport(self.__bugzillasession) - ServerProxy.__init__(self, uri, transport, *args, **kwargs) - - def _ServerProxy__request(self, methodname, params): - """ - Overrides ServerProxy _request method - """ - if len(params) == 0: - params = ({}, ) - - log.debug("XMLRPC call: %s(%s)", methodname, params[0]) - api_key = self.__bugzillasession.get_api_key() - token_value = self.__bugzillasession.get_token_value() - - if api_key is not None: - if 'Bugzilla_api_key' not in params[0]: - params[0]['Bugzilla_api_key'] = api_key - elif token_value is not None: - if 'Bugzilla_token' not in params[0]: - params[0]['Bugzilla_token'] = token_value - - # pylint: disable=no-member - ret = ServerProxy._ServerProxy__request(self, methodname, params) - # pylint: enable=no-member - - if isinstance(ret, dict) and 'token' in ret.keys(): - self.__bugzillasession.set_token_value(ret.get('token')) - return ret From 991eb76f347957ce4513b1881b7fc586c1a1ea94 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 16:59:13 -0500 Subject: [PATCH 134/393] Rename transport.py to _session.py Because it is only tracking the _BugzillaSession class now Signed-off-by: Cole Robinson --- bugzilla/{transport.py => _session.py} | 0 bugzilla/base.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename bugzilla/{transport.py => _session.py} (100%) diff --git a/bugzilla/transport.py b/bugzilla/_session.py similarity index 100% rename from bugzilla/transport.py rename to bugzilla/_session.py diff --git a/bugzilla/base.py b/bugzilla/base.py index 4c1cf442..3a947c35 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -34,7 +34,7 @@ from ._backendxmlrpc import _BackendXMLRPC from .bug import Bug, User from .exceptions import BugzillaError -from .transport import _BugzillaSession +from ._session import _BugzillaSession from ._util import listify From 923f20ddae7357f780d3d7dbd57858935c75a24e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 17:07:58 -0500 Subject: [PATCH 135/393] xmlrpc: Explicitly set the content type on _BugzillaSession To text/xml, rather than hardcode it. REST will want to use something different Signed-off-by: Cole Robinson --- bugzilla/_backendxmlrpc.py | 1 + bugzilla/_session.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 10379b60..0c52458e 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -27,6 +27,7 @@ def __init__(self, bugzillasession): Transport.__init__(self, use_datetime=False) self.__bugzillasession = bugzillasession + self.__bugzillasession.set_content_type("text/xml") self.__seen_valid_xml = False # Override Transport.user_agent diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 544f10f1..72626f76 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -45,7 +45,6 @@ def __init__(self, url, user_agent, self._session.verify = sslverify self._session.headers["User-Agent"] = self._user_agent - self._session.headers["Content-Type"] = 'text/xml' self._session.params["Bugzilla_api_key"] = self._api_key self._set_token_cache_param() @@ -60,6 +59,8 @@ def get_token_value(self): def set_token_value(self, value): self._token_cache.set_value(value) self._set_token_cache_param() + def set_content_type(self, value): + self._session.headers["Content-Type"] = value def _set_token_cache_param(self): self._session.params["Bugzilla_token"] = self._token_cache.get_value() From fb85b178091c32376805558cbf47b2aa837e2a1c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 17:13:48 -0500 Subject: [PATCH 136/393] tests: ro: Drop travis references We don't run this test suite in travis anymore Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 3cce243c..b49ec345 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -132,17 +132,10 @@ class BZMozilla(BaseTest): def testVersion(self): # bugzilla.mozilla.org returns version values in YYYY-MM-DD # format, so just try to confirm that - try: - bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) - assert bz.__class__ == Bugzilla - assert bz.bz_ver_major >= 2016 - assert bz.bz_ver_minor in range(1, 13) - except Exception as e: - # travis environment throws SSL errors here - # https://travis-ci.org/python-bugzilla/python-bugzilla/builds/304713566 - if "EOF occurred" not in str(e): - raise - self.skipTest("travis environment SSL error hit: %s" % str(e)) + bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) + assert bz.__class__ == Bugzilla + assert bz.bz_ver_major >= 2016 + assert bz.bz_ver_minor in range(1, 13) class BZGentoo(BaseTest): From 038bc2d613193774c8d6e33591d1ae12ce583842 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 17:17:14 -0500 Subject: [PATCH 137/393] backend: Return full 'version' output to caller To be consistent with how we handle the other APIs Signed-off-by: Cole Robinson --- bugzilla/_backendxmlrpc.py | 2 +- bugzilla/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 0c52458e..65a3c631 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -160,7 +160,7 @@ def get_xmlrpc_proxy(self): return self._xmlrpc_proxy def bugzilla_version(self): - return self._xmlrpc_proxy.Bugzilla.version()["version"] + return self._xmlrpc_proxy.Bugzilla.version() def bugzilla_extensions(self): return self._xmlrpc_proxy.Bugzilla.extensions() diff --git a/bugzilla/base.py b/bugzilla/base.py index 3a947c35..3a2564bd 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -488,7 +488,7 @@ def connect(self, url=None): if self.api_key: log.debug("using API key") - version = self._backend.bugzilla_version() + version = self._backend.bugzilla_version()["version"] log.debug("Bugzilla version string: %s", version) self._set_bz_version(version) From 8a3f1ce5788df21496f2971647108e9a7ce60a6d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 17:36:47 -0500 Subject: [PATCH 138/393] base: Add names= to getbugfields This lets us test only a subset of the field refresh, which only takes a few seconds, compared to the multi minute pain that is asking bugzilla.redhat.com for all fields Signed-off-by: Cole Robinson --- bugzilla/base.py | 11 +++++++++-- tests/test_ro_functional.py | 10 +++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 3a2564bd..1128cedc 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -649,14 +649,21 @@ def logged_in(self): # Bugfields querying # ###################### - def getbugfields(self, force_refresh=False): + def getbugfields(self, force_refresh=False, names=None): """ Calls getBugFields, which returns a list of fields in each bug for this bugzilla instance. This can be used to set the list of attrs on the Bug object. + + :param force_refresh: If True, overwrite the bugfield cache + with these newly checked values. + :param names: Only check for the passed bug field names """ def _fieldnames(): - r = self._backend.bug_fields({'include_fields': ['name']}) + data = {"include_fields": ["name"]} + if names: + data["names"] = names + r = self._backend.bug_fields(data) return [f['name'] for f in r['fields']] if force_refresh or not self._cache.bugfields: diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index b49ec345..f42f8c8e 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -274,11 +274,11 @@ def testQuerySubComponent(self): assert "#1060931 " in out def testBugFields(self): - bz = self.bzclass(url=self.url, use_creds=False) - fields1 = bz.getbugfields()[:] - fields2 = bz.getbugfields(force_refresh=True)[:] - assert bool([f for f in fields1 if f.startswith("attachments")]) - assert fields1 == fields2 + bz = self.bzclass(self.url, use_creds=False) + fields = bz.getbugfields(names=["product"])[:] + assert fields == ["product"] + bz.getbugfields(names=["product", "bug_status"], force_refresh=True) + assert set(bz.bugfields) == set(["product", "bug_status"]) def testBugAutoRefresh(self): bz = self.bzclass(self.url, use_creds=False) From b4286766d9774ea784d875509453bd09880fc537 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 17:24:59 -0500 Subject: [PATCH 139/393] tests: Centralize more Bugzilla init This will help with future REST testing Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 44 ++++++++++++++------------- tests/test_rw_functional.py | 59 +++++++++++++++++-------------------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index f42f8c8e..5024b7f9 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -16,21 +16,25 @@ import pytest -from bugzilla import Bugzilla, BugzillaError, RHBugzilla +from bugzilla import Bugzilla, BugzillaError import tests class BaseTest(unittest.TestCase): url = None - bzclass = Bugzilla bzversion = (0, 0) closestatus = "CLOSED" + def _open_bz(self, **kwargs): + if "use_creds" not in kwargs: + kwargs["use_creds"] = False + return Bugzilla(self.url, **kwargs) + def clicomm(self, argstr, expectexc=False, bz=None): comm = "bugzilla " + argstr if not bz: - bz = Bugzilla(url=self.url, use_creds=False) + bz = self._open_bz() if expectexc: with pytest.raises(Exception): tests.clicomm(comm, bz) @@ -38,10 +42,9 @@ def clicomm(self, argstr, expectexc=False, bz=None): return tests.clicomm(comm, bz) def _testBZVersion(self): - bz = Bugzilla(self.url, use_creds=False) - assert bz.__class__ == self.bzclass if tests.CLICONFIG.REDHAT_URL: return + bz = self._open_bz() assert bz.bz_ver_major == self.bzversion[0] assert bz.bz_ver_minor == self.bzversion[1] @@ -129,10 +132,12 @@ def _testQueryURL(self, querystr, count, expectstr): class BZMozilla(BaseTest): + url = "bugzilla.mozilla.org" + def testVersion(self): # bugzilla.mozilla.org returns version values in YYYY-MM-DD # format, so just try to confirm that - bz = Bugzilla("bugzilla.mozilla.org", use_creds=False) + bz = self._open_bz() assert bz.__class__ == Bugzilla assert bz.bz_ver_major >= 2016 assert bz.bz_ver_minor in range(1, 13) @@ -148,13 +153,13 @@ def testURLQuery(self): query_url = ("https://bugs.gentoo.org/buglist.cgi?" "component=[CS]&product=Doc%20Translations" "&query_format=advanced&resolution=FIXED") - bz = Bugzilla(url=self.url, use_creds=False) + bz = self._open_bz() ret = bz.query(bz.url_to_query(query_url)) assert len(ret) > 0 class BZGnome(BaseTest): - url = "https://bugzilla.gnome.org/xmlrpc.cgi" + url = "https://bugzilla.gnome.org" bzversion = (4, 4) closestatus = "RESOLVED" @@ -173,7 +178,7 @@ def testURLQuery(self): query_url = ("https://bugzilla.gnome.org/buglist.cgi?" "bug_status=RESOLVED&order=Importance&product=accerciser" "&query_format=advanced&resolution=NOTABUG") - bz = Bugzilla(url=self.url, use_creds=False) + bz = self._open_bz() try: bz.query(bz.url_to_query(query_url)) except BugzillaError as e: @@ -182,8 +187,7 @@ def testURLQuery(self): class RHTest(BaseTest): url = (tests.CLICONFIG.REDHAT_URL or - "https://bugzilla.redhat.com/xmlrpc.cgi") - bzclass = RHBugzilla + "https://bugzilla.redhat.com") bzversion = (5, 0) test0 = BaseTest._testBZVersion @@ -231,11 +235,11 @@ class RHTest(BaseTest): " CVE-2011-2527") def testDoubleConnect(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() bz.connect(self.url) def testQueryFlags(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() if not bz.logged_in: print("not logged in, skipping testQueryFlags") return @@ -256,14 +260,14 @@ def testComponentsDetails(self): """ Fresh call to getcomponentsdetails should properly refresh """ - bz = self.bzclass(url=self.url, use_creds=False) + bz = self._open_bz() assert bool(bz.getcomponentsdetails("Red Hat Developer Toolset")) def testGetBugAlias(self): """ getbug() works if passed an alias """ - bz = self.bzclass(url=self.url, use_creds=False) + bz = self._open_bz() bug = bz.getbug("CVE-2011-2527") assert bug.bug_id == 720773 @@ -274,14 +278,14 @@ def testQuerySubComponent(self): assert "#1060931 " in out def testBugFields(self): - bz = self.bzclass(self.url, use_creds=False) + bz = self._open_bz() fields = bz.getbugfields(names=["product"])[:] assert fields == ["product"] bz.getbugfields(names=["product", "bug_status"], force_refresh=True) assert set(bz.bugfields) == set(["product", "bug_status"]) def testBugAutoRefresh(self): - bz = self.bzclass(self.url, use_creds=False) + bz = self._open_bz() bz.bug_autorefresh = True @@ -301,7 +305,7 @@ def testBugAutoRefresh(self): assert "adjust your include_fields" in str(e) def testExtraFields(self): - bz = self.bzclass(self.url, cookiefile=None, tokenfile=None) + bz = self._open_bz() # Check default extra_fields will pull in comments bug = bz.getbug(720773, exclude_fields=["product"]) @@ -329,9 +333,9 @@ def testActiveComps(self): def testFaults(self): # Test special error wrappers in bugzilla/_cli.py - bzinstance = Bugzilla(self.url, use_creds=False) + bz = self._open_bz() out = tests.clicomm("bugzilla query --field=IDONTEXIST=FOO", - bzinstance, expectfail=True) + bz, expectfail=True) assert "Server error:" in out out = tests.clicomm("bugzilla " diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 180bb639..81f259f7 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -27,7 +27,6 @@ import pytest import bugzilla -from bugzilla import Bugzilla import tests @@ -39,17 +38,11 @@ def _split_int(s): if not bugzilla.RHBugzilla(url=RHURL).logged_in: - print("R/W tests require cached login credentials for url=%s" % RHURL) + print("\nR/W tests require cached login credentials for url=%s\n" % RHURL) sys.exit(1) class RHPartnerTest(unittest.TestCase): - # Despite its name, this instance is simply for bugzilla testing, - # doesn't send out emails and is blown away occasionally. The front - # page has some info. - url = RHURL - bzclass = bugzilla.RHBugzilla - def _check_have_admin(self, bz, funcname): # groupnames is empty for any user if our logged in user does not # have admin privs. @@ -59,13 +52,16 @@ def _check_have_admin(self, bz, funcname): print("\nNo admin privs, reduced testing of %s" % funcname) return ret + def _open_bz(self, **kwargs): + return bugzilla.RHBugzilla(url=RHURL, **kwargs) + def test0LoggedInNoCreds(self): - bz = self.bzclass(url=self.url, use_creds=False) + bz = self._open_bz(use_creds=False) assert not bz.logged_in def test2(self): - bz = Bugzilla(url=self.url, use_creds=False) - assert bz.__class__ is self.bzclass + bz = bugzilla.Bugzilla(RHURL, use_creds=False) + assert bz.__class__ is bugzilla.RHBugzilla def _makebug(self, bz): component = "python-bugzilla" @@ -94,7 +90,7 @@ def test03NewBugBasic(self): """ Create a bug with minimal amount of fields, then close it """ - bz = self.bzclass(url=self.url) + bz = self._open_bz() bug = self._makebug(bz) # Verify hasattr works @@ -112,7 +108,7 @@ def test04NewBugAllFields(self): """ Create a bug using all 'new' fields, check some values, close it """ - bz = self.bzclass(url=self.url) + bz = self._open_bz() summary = ("python-bugzilla test manyfields bug %s" % datetime.datetime.today()) @@ -176,7 +172,7 @@ def test05ModifyStatus(self): """ Modify status and comment fields for an existing bug """ - bz = self.bzclass(url=self.url) + bz = self._open_bz() bugid = "663674" cmd = "bugzilla modify %s " % bugid @@ -263,7 +259,7 @@ def test06ModifyEmails(self): """ Modify cc, assignee, qa_contact for existing bug """ - bz = self.bzclass(url=self.url) + bz = self._open_bz() bugid = "663674" cmd = "bugzilla modify %s " % bugid @@ -311,7 +307,7 @@ def test07ModifyMultiFlags(self): """ Modify flags and fixed_in for 2 bugs """ - bz = self.bzclass(url=self.url) + bz = self._open_bz() bugid1 = "461686" bugid2 = "461687" cmd = "bugzilla modify %s %s " % (bugid1, bugid2) @@ -399,7 +395,7 @@ def cleardict_new(b): def test07ModifyMisc(self): bugid = "461686" cmd = "bugzilla modify %s " % bugid - bz = self.bzclass(url=self.url) + bz = self._open_bz() bug = bz.getbug(bugid) # modify --dependson @@ -501,7 +497,7 @@ def _test8Attachments(self): """ Get and set attachments for a bug """ - bz = self.bzclass(url=self.url) + bz = self._open_bz() cmd = "bugzilla attach " testfile = "../tests/data/bz-attach-get1.txt" @@ -552,7 +548,7 @@ def _test8Attachments(self): assert setbug.attachments[-1]["flags"] == [] # Set attachment obsolete - bz._proxy.Bug.update_attachment({ # pylint: disable=protected-access + bz._backend.bug_attachment_update({ # pylint: disable=protected-access "ids": [setbug.attachments[-1]["id"]], "is_obsolete": 1}) setbug.refresh() @@ -599,7 +595,7 @@ def _test8Attachments(self): def test09Whiteboards(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() bug_id = "663674" cmd = "bugzilla modify %s " % bug_id bug = bz.getbug(bug_id) @@ -668,7 +664,7 @@ def fakegetpass(prompt): getpass.getpass = fakegetpass try: - cmd = "bugzilla --no-cache-credentials --bugzilla %s" % self.url + cmd = "bugzilla --no-cache-credentials --bugzilla %s" % RHURL # Implied login with --username and --password ret = tests.clicomm("%s --user foobar@example.com " "--password foobar query -b 123456" % cmd, @@ -700,7 +696,7 @@ def fakegetpass(prompt): def test11UserUpdate(self): # This won't work if run by the same user we are using - bz = self.bzclass(url=self.url) + bz = self._open_bz() email = "anaconda-maint-list@redhat.com" group = "fedora_contrib" @@ -758,7 +754,7 @@ def test11UserUpdate(self): def test11ComponentEditing(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() component = ("python-bugzilla-testcomponent-%s" % str(random.randint(1, 1024 * 1024 * 1024))) basedata = { @@ -831,8 +827,7 @@ def compare(data, newid): ("You are not allowed" in str(e))) def test12SetCookie(self): - bz = self.bzclass(self.url, - cookiefile=-1, tokenfile=None, configpaths=[]) + bz = self._open_bz(cookiefile=-1, tokenfile=None, configpaths=[]) try: bz.cookiefile = None @@ -847,7 +842,7 @@ def test12SetCookie(self): assert not bz.logged_in def test13SubComponents(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() # Long closed RHEL5 lvm2 bug. This component has sub_components bug = bz.getbug("185526") bug.autorefresh = True @@ -865,18 +860,18 @@ def test13SubComponents(self): "Default / Unclassified (RHEL5)"]} def test13ExternalTrackerQuery(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() with pytest.raises(RuntimeError): bz.build_external_tracker_boolean_query() def _deleteAllExistingExternalTrackers(self, bugid): - bz = self.bzclass(url=self.url) + bz = self._open_bz() ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] if ids != []: bz.remove_external_tracker(ids=ids) def test14ExternalTrackersAddUpdateRemoveQuery(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() bugid = 461686 ext_bug_id = 380489 @@ -922,14 +917,14 @@ def test14ExternalTrackersAddUpdateRemoveQuery(self): assert len(ids) == 0 def test15EnsureLoggedIn(self): - bz = self.bzclass(url=self.url) + bz = self._open_bz() comm = "bugzilla --ensure-logged-in query --bug_id 979546" tests.clicomm(comm, bz) def test16ModifyTags(self): bugid = "461686" cmd = "bugzilla modify %s " % bugid - bz = self.bzclass(url=self.url) + bz = self._open_bz() bug = bz.getbug(bugid) if bug.tags: @@ -951,7 +946,7 @@ def test16ModifyTags(self): def test17LoginAPIKey(self): api_key = "somefakeapikey1234" - bz = self.bzclass(url=self.url, use_creds=False, api_key=api_key) + bz = self._open_bz(use_creds=False, api_key=api_key) if bz.bz_ver_major < 5: self.skipTest("can only test apikey on bugzilla 5+") From 370b9a0dc0668f094e88621ca1d53435e06c11b3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 5 Jan 2020 19:38:43 -0500 Subject: [PATCH 140/393] exceptions: Add helper functions to retrieve error msg/codes The XMLRPC APIs raise Fault errors, and we can't change that otherwise it breaks API. REST will use regular BugzillaError. Add some helper functions that can be used to get the error code and message from either variant Signed-off-by: Cole Robinson --- bugzilla/base.py | 20 ++++++++++++-------- bugzilla/exceptions.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 1128cedc..6b7d20e0 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -19,11 +19,11 @@ if sys.version_info[0] >= 3: from collections.abc import Mapping from urllib.parse import urlparse, urlunparse, parse_qsl - from xmlrpc.client import Binary, Fault + from xmlrpc.client import Binary else: from collections import Mapping from urlparse import urlparse, urlunparse, parse_qsl - from xmlrpclib import Binary, Fault + from xmlrpclib import Binary # pylint: enable=import-error,no-name-in-module,ungrouped-imports @@ -267,7 +267,7 @@ def _init_class_from_url(self): log.info("Found RedHat bugzilla extension, " "using RHBugzilla") c = RHBugzilla - except Fault: + except Exception: log.debug("Failed to fetch bugzilla extensions", exc_info=True) if not c: @@ -558,8 +558,9 @@ def login(self, user=None, password=None, restrict_login=None): self.password = '' log.info("login successful for user=%s", self.user) return ret - except Fault as e: - raise BugzillaError("Login failed: %s" % str(e.faultString)) + except Exception as e: + raise BugzillaError("Login failed: %s" % + BugzillaError.get_bugzilla_error_string(e)) def interactive_login(self, user=None, password=None, force=False, restrict_login=None, use_api_key=False): @@ -639,8 +640,9 @@ def logged_in(self): try: self._backend.user_get({"ids": []}) return True - except Fault as e: - if e.faultCode == 505 or e.faultCode == 32000: + except Exception as e: + code = BugzillaError.get_bugzilla_error_code(e) + if code in [505, 32000]: return False raise e @@ -1235,7 +1237,9 @@ def query(self, query): """ try: r = self._backend.bug_search(query) - except Fault as e: + except Exception as e: + if not BugzillaError.get_bugzilla_error_code(e): + raise # Try to give a hint in the error message if url_to_query # isn't supported by this bugzilla instance diff --git a/bugzilla/exceptions.py b/bugzilla/exceptions.py index 1369290f..8ceb3004 100644 --- a/bugzilla/exceptions.py +++ b/bugzilla/exceptions.py @@ -6,3 +6,24 @@ class BugzillaError(Exception): """ Error raised in the Bugzilla client code. """ + @staticmethod + def get_bugzilla_error_string(exc): + """ + Helper to return the bugzilla instance error message from an + XMLRPC Fault, or any other exception type that's raised from bugzilla + interaction + """ + if hasattr(exc, "faultString"): + return getattr(exc, "faultString") + return str(exc) + + @staticmethod + def get_bugzilla_error_code(exc): + """ + Helper to return the bugzilla instance error code from an + XMLRPC Fault, or any other exception type that's raised from bugzilla + interaction + """ + if hasattr(exc, "faultCode"): + return getattr(exc, "faultCode") + return None From 425378bb24304598ec83bf52915804b97776b2ab Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Aug 2018 14:23:56 -0400 Subject: [PATCH 141/393] spec: Use nowadays Fedora preferred github URL --- python-bugzilla.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 7a893b38..1c36678c 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -21,7 +21,7 @@ Summary: Python library for interacting with Bugzilla License: GPLv2+ URL: https://github.com/python-bugzilla/python-bugzilla -Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz +Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{version}/%{name}-%{version}.tar.gz BuildArch: noarch %if %{with python2} From 0ba07f4f9fa0a9a6c92b896921096eb390fb7ac8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 6 Jan 2020 09:59:12 -0500 Subject: [PATCH 142/393] base: Push XMLRPC Binary handling down into Backend Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 6 +++++- bugzilla/_backendxmlrpc.py | 9 ++++++--- bugzilla/base.py | 5 +---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 388d334d..1ad09d71 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -59,10 +59,14 @@ def bug_attachment_get_all(self, bug_ids, paramdict): """ raise NotImplementedError() - def bug_attachment_create(self, paramdict): + def bug_attachment_create(self, data, paramdict): """ Create a bug attachment http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#create-attachment + + :param data: raw Bytes data of the attachment to attach. API will + encode this correctly if you pass it in and 'data' is not in + paramdict. """ raise NotImplementedError() diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 65a3c631..6938adaf 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -6,9 +6,10 @@ # pylint: disable=import-error if sys.version_info[0] >= 3: - from xmlrpc.client import Fault, ProtocolError, ServerProxy, Transport + from xmlrpc.client import (Binary, Fault, ProtocolError, + ServerProxy, Transport) else: - from xmlrpclib import Fault, ProtocolError, ServerProxy, Transport + from xmlrpclib import Binary, Fault, ProtocolError, ServerProxy, Transport # pylint: enable=import-error from requests import RequestException @@ -172,7 +173,9 @@ def bug_attachment_get_all(self, bug_ids, paramdict): data = paramdict.copy() data["ids"] = listify(bug_ids) return self._xmlrpc_proxy.Bug.attachments(data) - def bug_attachment_create(self, paramdict): + def bug_attachment_create(self, data, paramdict): + if data is not None and "data" not in paramdict: + paramdict["data"] = Binary(data) return self._xmlrpc_proxy.Bug.add_attachment(paramdict) def bug_attachment_update(self, paramdict): return self._xmlrpc_proxy.Bug.update_attachment(paramdict) diff --git a/bugzilla/base.py b/bugzilla/base.py index 6b7d20e0..df3f5b09 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -19,11 +19,9 @@ if sys.version_info[0] >= 3: from collections.abc import Mapping from urllib.parse import urlparse, urlunparse, parse_qsl - from xmlrpc.client import Binary else: from collections import Mapping from urlparse import urlparse, urlunparse, parse_qsl - from xmlrpclib import Binary # pylint: enable=import-error,no-name-in-module,ungrouped-imports @@ -1523,7 +1521,6 @@ def attachfile(self, idlist, attachfile, description, **kwargs): data = f.read() if not isinstance(data, bytes): data = data.encode(locale.getpreferredencoding()) - kwargs['data'] = Binary(data) kwargs['ids'] = listify(idlist) @@ -1536,7 +1533,7 @@ def attachfile(self, idlist, attachfile, description, **kwargs): kwargs['file_name'], strict=False)[0] kwargs['content_type'] = ctype or 'application/octet-stream' - ret = self._backend.bug_attachment_create(kwargs) + ret = self._backend.bug_attachment_create(data, kwargs) if "attachments" in ret: # Up to BZ 4.2 From f9cc95bb0afe6c1fa24aa89205a9d7a0a7b15982 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 6 Jan 2020 10:02:56 -0500 Subject: [PATCH 143/393] tests: ro: Remove Gnome bugzilla tests The gnome bugzilla instance is frozen, and doesn't support the REST API which will complicate future testing. Just remove the test cases Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 5024b7f9..596ba0c4 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -16,7 +16,7 @@ import pytest -from bugzilla import Bugzilla, BugzillaError +import bugzilla import tests @@ -28,7 +28,7 @@ class BaseTest(unittest.TestCase): def _open_bz(self, **kwargs): if "use_creds" not in kwargs: kwargs["use_creds"] = False - return Bugzilla(self.url, **kwargs) + return bugzilla.Bugzilla(self.url, **kwargs) def clicomm(self, argstr, expectexc=False, bz=None): comm = "bugzilla " + argstr @@ -138,7 +138,7 @@ def testVersion(self): # bugzilla.mozilla.org returns version values in YYYY-MM-DD # format, so just try to confirm that bz = self._open_bz() - assert bz.__class__ == Bugzilla + assert bz.__class__ == bugzilla.Bugzilla assert bz.bz_ver_major >= 2016 assert bz.bz_ver_minor in range(1, 13) @@ -158,33 +158,6 @@ def testURLQuery(self): assert len(ret) > 0 -class BZGnome(BaseTest): - url = "https://bugzilla.gnome.org" - bzversion = (4, 4) - closestatus = "RESOLVED" - - test0 = BaseTest._testBZVersion - test1 = lambda s: BaseTest._testQuery(s, - "--product dogtail --component sniff", - 9, "321654") - # BZ < 4 doesn't report values for --full - test2 = lambda s: BaseTest._testQueryRaw(s, "321654", 30, - "ATTRIBUTE[version]: CVS HEAD") - test3 = lambda s: BaseTest._testQueryOneline(s, "321654", "Sniff") - - def testURLQuery(self): - # This instance is old and doesn't support URL queries, we are - # just verifying our extra error message report - query_url = ("https://bugzilla.gnome.org/buglist.cgi?" - "bug_status=RESOLVED&order=Importance&product=accerciser" - "&query_format=advanced&resolution=NOTABUG") - bz = self._open_bz() - try: - bz.query(bz.url_to_query(query_url)) - except BugzillaError as e: - assert "derived from bugzilla" in str(e) - - class RHTest(BaseTest): url = (tests.CLICONFIG.REDHAT_URL or "https://bugzilla.redhat.com") From eb9360478a5d4ed000ac795863cd452aaf0e2040 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 6 Jan 2020 12:04:47 -0500 Subject: [PATCH 144/393] backendbase: Match up __init__ with _BackendXMLRPC impl Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 3 ++- bugzilla/_backendxmlrpc.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 1ad09d71..41b38374 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -8,7 +8,8 @@ class _BackendBase(object): (XMLRPC, REST). This base class defines the public API for the rest of the code, but this is all internal to the library. """ - def __init__(self, bugzillasession): + def __init__(self, url, bugzillasession): + dummy = url self._bugzillasession = bugzillasession ################# diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 6938adaf..a58a01e3 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -154,7 +154,7 @@ class _BackendXMLRPC(_BackendBase): Internal interface for direct calls to bugzilla's XMLRPC API """ def __init__(self, url, bugzillasession): - _BackendBase.__init__(self, bugzillasession) + _BackendBase.__init__(self, url, bugzillasession) self._xmlrpc_proxy = _BugzillaXMLRPCProxy(url, self._bugzillasession) def get_xmlrpc_proxy(self): From 16a17f29bed322b37aaa5e0688f5ca5b67394490 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 7 Jan 2020 11:32:11 -0500 Subject: [PATCH 145/393] base: Log bug_search raw return values Signed-off-by: Cole Robinson --- bugzilla/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index df3f5b09..5d5b2d3e 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -448,6 +448,10 @@ def _set_bz_version(self, version): self.bz_ver_major = 5 self.bz_ver_minor = 0 + def _get_backend_class(self): + # This is a hook for the test suite to do some mock hackery + return _BackendXMLRPC + def connect(self, url=None): """ Connect to the bugzilla instance with the given url. This is @@ -473,7 +477,8 @@ def connect(self, url=None): cert=self.cert, tokenfile=self.tokenfile, api_key=self.api_key) - self._backend = _BackendXMLRPC(url, self._session) + backendclass = self._get_backend_class() + self._backend = backendclass(url, self._session) self.url = url # we've changed URLs - reload config @@ -1235,6 +1240,7 @@ def query(self, query): """ try: r = self._backend.bug_search(query) + log.debug("bug_search returned:\n%s", str(r)) except Exception as e: if not BugzillaError.get_bugzilla_error_code(e): raise From 189a9c1783ac3ad569d35b45b2e6a0b5d41798d2 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 7 Jan 2020 11:32:39 -0500 Subject: [PATCH 146/393] bug: Drop logging of updated dict keys, it's not interesting Signed-off-by: Cole Robinson --- bugzilla/bug.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 46dd9868..f5040f2e 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -37,9 +37,7 @@ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): if bug_id: dict["id"] = bug_id - log.debug("Bug(%s)", sorted(dict.keys())) self._update_dict(dict) - self.weburl = bugzilla.url.replace('xmlrpc.cgi', 'show_bug.cgi?id=%i' % self.bug_id) From 1de6a2f6760b3c6b16379a3d0b63d5934feedbca Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 15:45:27 -0500 Subject: [PATCH 147/393] tests: Make functional tests disable traditional unit tests This makes it easier to distinguish coverage of functional vs unit tests Signed-off-by: Cole Robinson --- tests/conftest.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index af96ac3a..84caf0c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,12 +27,19 @@ def pytest_addoption(parser): def pytest_ignore_collect(path, config): - if ((os.path.basename(str(path)) == "test_ro_functional.py") and - not config.getoption("--ro-functional")): - return True - - if ((os.path.basename(str(path)) == "test_rw_functional.py") and - not config.getoption("--rw-functional")): + has_ro = config.getoption("--ro-functional") + has_rw = config.getoption("--rw-functional") + skip_rest = has_ro or has_rw + + base = os.path.basename(str(path)) + is_ro = base == "test_ro_functional.py" + is_rw = base == "test_rw_functional.py" + if is_ro or is_rw: + if is_ro and not has_ro: + return True + if is_rw and not has_rw: + return True + elif skip_rest: return True From 8ef832335090c9fd63dba118e117abd0f3949822 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:10:54 -0500 Subject: [PATCH 148/393] base: Move readconfig() call before session setup Otherwise we don't pull api_key and other values from the rc file, which is needed for proper session init Signed-off-by: Cole Robinson --- bugzilla/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 5d5b2d3e..46ee72d2 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -471,6 +471,10 @@ def connect(self, url=None): url = self.url url = self.fix_url(url) + self.url = url + # we've changed URLs - reload config + self.readconfig(overwrite=False) + self._session = _BugzillaSession(url, self.user_agent, cookiejar=self._cookiejar, sslverify=self._sslverify, @@ -480,10 +484,6 @@ def connect(self, url=None): backendclass = self._get_backend_class() self._backend = backendclass(url, self._session) - self.url = url - # we've changed URLs - reload config - self.readconfig(overwrite=False) - if (self.user and self.password): log.info("user and password present - doing login()") self.login() From b633eaabb5ee5530eb4703a5a71cac35d4b2bbc2 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:15:59 -0500 Subject: [PATCH 149/393] pylint: Ignore 'dummy' variables, like standard pylint config Signed-off-by: Cole Robinson --- pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index f5c361a8..28a9a6fe 100644 --- a/pylintrc +++ b/pylintrc @@ -34,7 +34,7 @@ notes=FIXME,XXX,TODO [VARIABLES] # A regular expression matching the beginning of the name of dummy variables # (i.e. not used). -dummy-variables-rgx=ignore.*|.*_ignore +dummy-variables-rgx=dummy.*|ignore.*|.*_ignore # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. From fa3cae3d19c4f7721e5cc073b851b427b8d5d2de Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:32:01 -0500 Subject: [PATCH 150/393] Add _compatimports.py to simplify handling py2 vs py3 Groups all the annoying module naming differences Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 14 ++------------ bugzilla/_backendxmlrpc.py | 10 ++-------- bugzilla/_cli.py | 17 +++++------------ bugzilla/_compatimports.py | 23 +++++++++++++++++++++++ bugzilla/_session.py | 9 +-------- bugzilla/base.py | 11 +---------- bugzilla/bug.py | 7 +++++-- 7 files changed, 39 insertions(+), 52 deletions(-) create mode 100644 bugzilla/_compatimports.py diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 6927802b..2ca594c8 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -2,20 +2,10 @@ # See the COPYING file in the top-level directory. import os -import sys from logging import getLogger -# pylint: disable=import-error,no-name-in-module,ungrouped-imports -if sys.version_info[0] >= 3: - from configparser import ConfigParser - from http.cookiejar import LoadError, MozillaCookieJar - from urllib.parse import urlparse # pylint: disable=no-name-in-module -else: - from ConfigParser import SafeConfigParser as ConfigParser - from cookielib import LoadError, MozillaCookieJar - from urlparse import urlparse -# pylint: enable=import-error,no-name-in-module,ungrouped-imports - +from ._compatimports import (ConfigParser, LoadError, + MozillaCookieJar, urlparse) from .exceptions import BugzillaError from ._util import listify diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index a58a01e3..cc38f0d1 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -4,17 +4,11 @@ from logging import getLogger import sys -# pylint: disable=import-error -if sys.version_info[0] >= 3: - from xmlrpc.client import (Binary, Fault, ProtocolError, - ServerProxy, Transport) -else: - from xmlrpclib import Binary, Fault, ProtocolError, ServerProxy, Transport -# pylint: enable=import-error - from requests import RequestException from ._backendbase import _BackendBase +from ._compatimports import (Binary, Fault, ProtocolError, + ServerProxy, Transport) from .exceptions import BugzillaError from ._util import listify diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index ba8053ca..1d157a9c 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -21,20 +21,11 @@ import sys import tempfile -# pylint: disable=import-error -if sys.version_info[0] >= 3: - # pylint: disable=no-name-in-module,redefined-builtin - from xmlrpc.client import Fault, ProtocolError - from urllib.parse import urlparse - basestring = (str, bytes) -else: - from xmlrpclib import Fault, ProtocolError - from urlparse import urlparse -# pylint: enable=import-error - import requests.exceptions import bugzilla +from bugzilla._compatimports import Fault, ProtocolError, urlparse, IS_PY3 + DEFAULT_BZ = 'https://bugzilla.redhat.com' @@ -57,12 +48,14 @@ def _is_unittest_debug(): def to_encoding(ustring): string = '' + if IS_PY3: + basestring = (str, bytes) if isinstance(ustring, basestring): string = ustring elif ustring is not None: string = str(ustring) - if sys.version_info[0] >= 3: + if IS_PY3: return string preferred = locale.getpreferredencoding() diff --git a/bugzilla/_compatimports.py b/bugzilla/_compatimports.py new file mode 100644 index 00000000..11e93f1f --- /dev/null +++ b/bugzilla/_compatimports.py @@ -0,0 +1,23 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import sys + +IS_PY3 = sys.version_info[0] >= 3 + +# pylint: disable=import-error,unused-import,ungrouped-imports +# pylint: disable=no-name-in-module +if IS_PY3: + from collections.abc import Mapping + from configparser import ConfigParser + from http.cookiejar import LoadError, MozillaCookieJar + from urllib.parse import urlparse, urlunparse, parse_qsl + from xmlrpc.client import (Binary, Fault, ProtocolError, + ServerProxy, Transport) +else: # pragma: no cover + from collections import Mapping + from ConfigParser import SafeConfigParser as ConfigParser + from cookielib import LoadError, MozillaCookieJar + from urlparse import urlparse + from xmlrpclib import Binary, Fault, ProtocolError, ServerProxy, Transport + from urlparse import urlparse, urlunparse, parse_qsl diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 72626f76..83f49dcc 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -3,18 +3,11 @@ import base64 from logging import getLogger -import sys - -# pylint: disable=import-error -if sys.version_info[0] >= 3: - from urllib.parse import urlparse # pylint: disable=no-name-in-module -else: - from urlparse import urlparse -# pylint: enable=import-error import requests from ._authfiles import _BugzillaTokenCache +from ._compatimports import urlparse log = getLogger(__name__) diff --git a/bugzilla/base.py b/bugzilla/base.py index 46ee72d2..6d201b07 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -15,21 +15,12 @@ from io import BytesIO -# pylint: disable=import-error,no-name-in-module,ungrouped-imports -if sys.version_info[0] >= 3: - from collections.abc import Mapping - from urllib.parse import urlparse, urlunparse, parse_qsl -else: - from collections import Mapping - from urlparse import urlparse, urlunparse, parse_qsl -# pylint: enable=import-error,no-name-in-module,ungrouped-imports - - from ._authfiles import (DEFAULT_CONFIGPATHS, open_bugzillarc, _build_cookiejar, _default_cache_location, _parse_hostname, _save_api_key) from .apiversion import __version__ from ._backendxmlrpc import _BackendXMLRPC +from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl from .bug import Bug, User from .exceptions import BugzillaError from ._session import _BugzillaSession diff --git a/bugzilla/bug.py b/bugzilla/bug.py index f5040f2e..58e23870 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -7,9 +7,12 @@ # See the COPYING file in the top-level directory. from __future__ import unicode_literals + import locale from logging import getLogger -import sys + +from ._compatimports import IS_PY3 + log = getLogger(__name__) @@ -49,7 +52,7 @@ def __str__(self): 'print(bug)' is not recommended because of potential encoding issues. Please use unicode(bug) where possible. """ - if sys.version_info[0] >= 3: + if IS_PY3: return self.__unicode__() else: return self.__unicode__().encode( From b26b066a0f717444e4c9b5f5ee9292c2a75ed089 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:36:26 -0500 Subject: [PATCH 151/393] BugzillaError: Add a 'code' value We will use this in the REST backend to report a the bugzilla error code Signed-off-by: Cole Robinson --- bugzilla/exceptions.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/bugzilla/exceptions.py b/bugzilla/exceptions.py index 8ceb3004..3060392a 100644 --- a/bugzilla/exceptions.py +++ b/bugzilla/exceptions.py @@ -24,6 +24,17 @@ def get_bugzilla_error_code(exc): XMLRPC Fault, or any other exception type that's raised from bugzilla interaction """ - if hasattr(exc, "faultCode"): - return getattr(exc, "faultCode") + for propname in ["faultCode", "code"]: + if hasattr(exc, propname): + return getattr(exc, propname) return None + + def __init__(self, message, code=None): + """ + :param code: The error code from the remote bugzilla instance. Only + set if the error came directly from the remove bugzilla + """ + self.code = code + if self.code: + message += " (code=%s)" % self.code + Exception.__init__(self, message) From 63ac060eec1f01fdea1dd43d7fb70324e60b15a0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:41:58 -0500 Subject: [PATCH 152/393] cli: Reduce 'info' components API calls We weren't being entirely optimal with our calls here. Rework things slightly to minimize the possible number of calls. Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 18 +++++++++--------- bugzilla/base.py | 5 ++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 1d157a9c..68a59dc2 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -609,18 +609,18 @@ def _filter_components(compdetails): return ret productname = (opt.components or opt.component_owners or opt.versions) - include_fields = ["name", "id"] fastcomponents = (opt.components and not opt.active_components) + + include_fields = ["name", "id"] + if opt.components or opt.component_owners: + include_fields += ["components.name"] + if opt.component_owners: + include_fields += ["components.default_assigned_to"] + if opt.active_components: + include_fields += ["components.is_active"] + if opt.versions: include_fields += ["versions"] - if opt.component_owners: - include_fields += [ - "components.default_assigned_to", - "components.name", - ] - if (opt.active_components and - any(["components" in i for i in include_fields])): - include_fields += ["components.is_active"] bz.refresh_products(names=productname and [productname] or None, include_fields=include_fields) diff --git a/bugzilla/base.py b/bugzilla/base.py index 6d201b07..ada84b3d 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -838,9 +838,7 @@ def getcomponents(self, product, force_refresh=False): proddict = self._lookup_product_in_cache(product) product_id = proddict.get("id", None) - if (force_refresh or - product_id is None or - product_id not in self._cache.component_names): + if force_refresh or product_id is None: self.refresh_products(names=[product], include_fields=["name", "id"]) proddict = self._lookup_product_in_cache(product) @@ -848,6 +846,7 @@ def getcomponents(self, product, force_refresh=False): raise BugzillaError("Product '%s' not found" % product) product_id = proddict["id"] + if product_id not in self._cache.component_names: opts = {'product_id': product_id, 'field': 'component'} names = self._backend.bug_legal_values(opts)["values"] self._cache.component_names[product_id] = names From 87e9a107ecc2d68daabdfe4f5dd7a48b43f21a29 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:45:34 -0500 Subject: [PATCH 153/393] cli: Don't print some internal fields with --raw Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 68a59dc2..dd1e5afc 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -689,7 +689,12 @@ def _format_output(bz, opt, buglist): buglist = bz.getbugs([b.bug_id for b in buglist]) for b in buglist: print("Bugzilla %s: " % b.bug_id) + SKIP_NAMES = ["bugzilla"] for attrname in sorted(b.__dict__): + if attrname in SKIP_NAMES: + continue + if attrname.startswith("_"): + continue print(to_encoding(u"ATTRIBUTE[%s]: %s" % (attrname, b.__dict__[attrname]))) print("\n\n") From 38ca81658b4826449f4113c5fa3f67fca9614502 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:47:21 -0500 Subject: [PATCH 154/393] base: Add openattachment_data This is a helper API for taking an attachment dict() and converting the raw data into a python fileobj Signed-off-by: Cole Robinson --- bugzilla/base.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index ada84b3d..c1676756 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1542,6 +1542,25 @@ def attachfile(self, idlist, attachfile, description, **kwargs): ret = ret[0] return ret + def openattachment_data(self, attachment_dict): + """ + Helper for turning passed API attachment dictionary into a + filelike object + """ + ret = BytesIO() + data = attachment_dict["data"] + + if hasattr(data, "data"): + # This is for xmlrpc Binary + content = data.data + else: + import base64 + content = base64.b64decode(data) + + ret.write(content) + ret.name = attachment_dict["file_name"] + ret.seek(0) + return ret def openattachment(self, attachid): """ @@ -1550,13 +1569,7 @@ def openattachment(self, attachid): """ attachments = self.get_attachments(None, attachid) data = attachments["attachments"][str(attachid)] - xmlrpcbinary = data["data"] - - ret = BytesIO() - ret.write(xmlrpcbinary.data) - ret.name = data["file_name"] - ret.seek(0) - return ret + return self.openattachment_data(data) def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): """ From 8df270d836ebf37a9593c4a6246e267a6ec0e7f5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:46:44 -0500 Subject: [PATCH 155/393] cli: Reduce number of 'attach' API calls Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index dd1e5afc..4ca90a9f 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -974,17 +974,25 @@ def _do_modify(bz, parser, opt): def _do_get_attach(bz, opt): + data = {} + + def _process_attachment_data(_attlist): + for _att in _attlist: + data[_att["id"]] = _att + if opt.getall: - for bug in bz.getbugs(opt.getall): - opt.get += bug.get_attachment_ids() - - for attid in set(opt.get): - if opt.ignore_obsolete: - metadata = bz.get_attachments(None, attid, - include_fields=["is_obsolete"]) - if metadata["attachments"][str(attid)]['is_obsolete'] == 1: - continue - att = bz.openattachment(attid) + for attlist in bz.get_attachments(opt.getall, None)["bugs"].values(): + _process_attachment_data(attlist) + if opt.get: + _process_attachment_data( + bz.get_attachments(None, opt.get)["attachments"].values()) + + for attdata in data.values(): + is_obsolete = attdata.get("is_obsolete", None) == 1 + if opt.ignore_obsolete and is_obsolete: + continue + + att = bz.openattachment_data(attdata) outfile = open_without_clobber(att.name, "wb") data = att.read(4096) while data: From ba468774fce3bf5cee157312bef3afd1533b3b9e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:53:11 -0500 Subject: [PATCH 156/393] cli: Reduce number of whiteboard 'modify' API calls Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 4ca90a9f..29d6f78e 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -955,22 +955,24 @@ def _do_modify(bz, parser, opt): # here. This is a bit weird for traditional bugzilla XMLRPC log.debug("Adjusting whiteboard fields one by one") for bug in bz.getbugs(bugid_list): - for wb, (add_list, rm_list) in wbmap.items(): + update_kwargs = {} + for wbkey, (add_list, rm_list) in wbmap.items(): + bugval = getattr(bug, wbkey) or "" for tag in add_list: - newval = getattr(bug, wb) or "" - if newval: - newval += " " - newval += tag - bz.update_bugs([bug.id], - bz.build_update(**{wb: newval})) + if bugval: + bugval += " " + bugval += tag for tag in rm_list: - newval = (getattr(bug, wb) or "").split() - for t in newval[:]: + bugsplit = bugval.split() + for t in bugsplit[:]: if t == tag: - newval.remove(t) - bz.update_bugs([bug.id], - bz.build_update(**{wb: " ".join(newval)})) + bugsplit.remove(t) + bugval = " ".join(bugsplit) + + update_kwargs[wbkey] = bugval + + bz.update_bugs([bug.id], bz.build_update(**update_kwargs)) def _do_get_attach(bz, opt): From 09a5ad2a6174e6e6b97529f7953d4f8aafb0d2f8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:55:20 -0500 Subject: [PATCH 157/393] session: Fix basicauth setup on py2 vs py3 Signed-off-by: Cole Robinson --- bugzilla/_session.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 83f49dcc..07920db1 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -62,8 +62,9 @@ def set_basic_auth(self, user, password): """ Set basic authentication method. """ - b64str = str(base64.b64encode("{}:{}".format(user, password))) - authstr = "Basic {}".format(b64str.encode("utf-8").decode("utf-8")) + formatstr = "{}:{}".format(user, password).encode("utf-8") + b64str = base64.b64encode(formatstr).decode("utf-8") + authstr = "Basic {}".format(b64str) self._session.headers["Authorization"] = authstr def set_response_cookies(self, response): From 97f9b7a1a024099abf148620f647419b36fb5eb5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:56:12 -0500 Subject: [PATCH 158/393] base: Log full exception when login fails Signed-off-by: Cole Robinson --- bugzilla/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bugzilla/base.py b/bugzilla/base.py index c1676756..2c0877c4 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -553,6 +553,7 @@ def login(self, user=None, password=None, restrict_login=None): log.info("login successful for user=%s", self.user) return ret except Exception as e: + log.debug("Login exception: %s", str(e), exc_info=True) raise BugzillaError("Login failed: %s" % BugzillaError.get_bugzilla_error_string(e)) From 4c4873abb2cfd480dbdbf5931d1045b4cb7a27df Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:57:27 -0500 Subject: [PATCH 159/393] base: Abide specified configpath when writing API key Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 9 ++++++--- bugzilla/base.py | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 2ca594c8..2b701650 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -107,7 +107,7 @@ def _default_config_location(filename): return _default_location(filename, 'config') -def _save_api_key(url, api_key): +def _save_api_key(url, api_key, configpaths): """ Save the API_KEY in the config file. @@ -115,7 +115,10 @@ def _save_api_key(url, api_key): API was called with --no-cache-credentials and no change will be made """ - config_filename = _default_config_location('bugzillarc') + if configpaths: + config_filename = configpaths[0] + else: + config_filename = _default_config_location('bugzillarc') section = _parse_hostname(url) cfg = ConfigParser() @@ -124,7 +127,7 @@ def _save_api_key(url, api_key): if section not in cfg.sections(): cfg.add_section(section) - cfg[section]['api_key'] = api_key.strip() + cfg.set(section, 'api_key', api_key.strip()) with open(config_filename, 'w') as configfile: cfg.write(configfile) diff --git a/bugzilla/base.py b/bugzilla/base.py index 2c0877c4..191ac9a9 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -582,13 +582,13 @@ def interactive_login(self, user=None, password=None, force=False, log.info('Checking API key... ') self.connect() - if not self.logged_in: + if not self.logged_in: # pragma: no cover raise BugzillaError("Login with API_KEY failed") log.info('API Key accepted') - if self._use_creds: - _save_api_key(self.url, self.api_key) - else: + if self._use_creds or self.configpath: + _save_api_key(self.url, self.api_key, self.configpath) + else: # pragma: no cover log.info("API Key won't be updated because use_creds=False") return From f98adeb70181c639296d7c73adb7e19467f8c4ca Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 16:58:19 -0500 Subject: [PATCH 160/393] tests: Big rework Add a mockbackend.py infrastructure for mocking API input and output. Use this to rework the unit test infrastructure to get near complete cli testing coverage and set the stage for more indepth testing coverage elsewhere in the code. Delete the old style unit tests Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 65 +-- bugzilla/base.py | 5 +- tests/__init__.py | 14 +- tests/conftest.py | 28 ++ .../clioutput/test_info_components-active.txt | 2 + .../clioutput/test_info_components-owners.txt | 2 + tests/data/clioutput/test_info_components.txt | 3 + tests/data/clioutput/test_info_products.txt | 2 + tests/data/clioutput/test_info_versions.txt | 2 + .../test_interactive_login_apikey_rcfile.txt | 3 + tests/data/clioutput/test_new1.txt | 1 + tests/data/clioutput/test_query1-ids.txt | 2 + tests/data/clioutput/test_query1.txt | 2 + tests/data/clioutput/test_query2.txt | 64 +++ tests/data/clioutput/test_query3.txt | 23 + tests/data/clioutput/test_query4.txt | 26 ++ tests/data/clioutput/test_query5.txt | 6 + tests/data/clioutput/test_query6.txt | 1 + tests/data/clioutput/test_query7.txt | 1 + tests/data/mockargs/test_api_login1.txt | 1 + tests/data/mockargs/test_attach1.txt | 7 + tests/data/mockargs/test_attach2.txt | 4 + tests/data/mockargs/test_attach_get1.txt | 1 + tests/data/mockargs/test_attach_get2.txt | 1 + .../mockargs/test_info_components-active.txt | 2 + .../test_info_components-legalvalues.txt | 1 + .../mockargs/test_info_components-owners.txt | 5 + tests/data/mockargs/test_info_components.txt | 2 + tests/data/mockargs/test_info_products.txt | 1 + tests/data/mockargs/test_info_versions.txt | 1 + .../data/mockargs/test_interactive_login.txt | 1 + tests/data/mockargs/test_login-restrict.txt | 1 + tests/data/mockargs/test_login.txt | 1 + tests/data/mockargs/test_modify1.txt | 1 + tests/data/mockargs/test_modify2.txt | 10 + tests/data/mockargs/test_modify3-tags.txt | 1 + tests/data/mockargs/test_modify3.txt | 4 + tests/data/mockargs/test_new1.txt | 6 + tests/data/mockargs/test_query1-ids.txt | 4 + tests/data/mockargs/test_query1.txt | 4 + tests/data/mockargs/test_query2.txt | 1 + tests/data/mockargs/test_query3.txt | 10 + tests/data/mockargs/test_query4.txt | 16 + tests/data/mockargs/test_query5.txt | 10 + tests/data/mockargs/test_query6.txt | 11 + tests/data/mockargs/test_query7.txt | 15 + tests/data/mockargs/test_query_cve_getbug.txt | 1 + tests/data/mockreturn/test_attach_get1.txt | 16 + tests/data/mockreturn/test_attach_get2.txt | 43 ++ tests/data/mockreturn/test_getbug.txt | 72 ++++ tests/data/mockreturn/test_getbug_rhel.txt | 196 +++++++++ tests/data/mockreturn/test_query1.txt | 1 + .../data/mockreturn/test_query_cve_getbug.txt | 71 +++ tests/mockbackend.py | 110 +++++ tests/test_api_bug.py | 80 ++++ tests/test_api_misc.py | 312 ++++++++++++++ tests/test_bug.py | 81 ---- tests/test_cli_commands.py | 407 ++++++++++++++++++ tests/test_cli_misc.py | 93 ++++ tests/test_createbug.py | 81 ---- tests/test_misc.py | 173 -------- tests/test_modify.py | 197 --------- tests/test_query.py | 313 -------------- tests/test_rw_functional.py | 12 +- tests/utils.py | 90 ++++ 65 files changed, 1807 insertions(+), 916 deletions(-) create mode 100644 tests/data/clioutput/test_info_components-active.txt create mode 100644 tests/data/clioutput/test_info_components-owners.txt create mode 100644 tests/data/clioutput/test_info_components.txt create mode 100644 tests/data/clioutput/test_info_products.txt create mode 100644 tests/data/clioutput/test_info_versions.txt create mode 100644 tests/data/clioutput/test_interactive_login_apikey_rcfile.txt create mode 100644 tests/data/clioutput/test_new1.txt create mode 100644 tests/data/clioutput/test_query1-ids.txt create mode 100644 tests/data/clioutput/test_query1.txt create mode 100644 tests/data/clioutput/test_query2.txt create mode 100644 tests/data/clioutput/test_query3.txt create mode 100644 tests/data/clioutput/test_query4.txt create mode 100644 tests/data/clioutput/test_query5.txt create mode 100644 tests/data/clioutput/test_query6.txt create mode 100644 tests/data/clioutput/test_query7.txt create mode 100644 tests/data/mockargs/test_api_login1.txt create mode 100644 tests/data/mockargs/test_attach1.txt create mode 100644 tests/data/mockargs/test_attach2.txt create mode 100644 tests/data/mockargs/test_attach_get1.txt create mode 100644 tests/data/mockargs/test_attach_get2.txt create mode 100644 tests/data/mockargs/test_info_components-active.txt create mode 100644 tests/data/mockargs/test_info_components-legalvalues.txt create mode 100644 tests/data/mockargs/test_info_components-owners.txt create mode 100644 tests/data/mockargs/test_info_components.txt create mode 100644 tests/data/mockargs/test_info_products.txt create mode 100644 tests/data/mockargs/test_info_versions.txt create mode 100644 tests/data/mockargs/test_interactive_login.txt create mode 100644 tests/data/mockargs/test_login-restrict.txt create mode 100644 tests/data/mockargs/test_login.txt create mode 100644 tests/data/mockargs/test_modify1.txt create mode 100644 tests/data/mockargs/test_modify2.txt create mode 100644 tests/data/mockargs/test_modify3-tags.txt create mode 100644 tests/data/mockargs/test_modify3.txt create mode 100644 tests/data/mockargs/test_new1.txt create mode 100644 tests/data/mockargs/test_query1-ids.txt create mode 100644 tests/data/mockargs/test_query1.txt create mode 100644 tests/data/mockargs/test_query2.txt create mode 100644 tests/data/mockargs/test_query3.txt create mode 100644 tests/data/mockargs/test_query4.txt create mode 100644 tests/data/mockargs/test_query5.txt create mode 100644 tests/data/mockargs/test_query6.txt create mode 100644 tests/data/mockargs/test_query7.txt create mode 100644 tests/data/mockargs/test_query_cve_getbug.txt create mode 100644 tests/data/mockreturn/test_attach_get1.txt create mode 100644 tests/data/mockreturn/test_attach_get2.txt create mode 100644 tests/data/mockreturn/test_getbug.txt create mode 100644 tests/data/mockreturn/test_getbug_rhel.txt create mode 100644 tests/data/mockreturn/test_query1.txt create mode 100644 tests/data/mockreturn/test_query_cve_getbug.txt create mode 100644 tests/mockbackend.py create mode 100644 tests/test_api_bug.py create mode 100644 tests/test_api_misc.py delete mode 100644 tests/test_bug.py create mode 100644 tests/test_cli_commands.py create mode 100644 tests/test_cli_misc.py delete mode 100644 tests/test_createbug.py delete mode 100644 tests/test_misc.py delete mode 100644 tests/test_modify.py delete mode 100644 tests/test_query.py create mode 100644 tests/utils.py diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 29d6f78e..b2e7c628 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -38,10 +38,6 @@ # Util helpers # ################ -def _is_unittest(): - return bool(os.getenv("__BUGZILLA_UNITTEST")) - - def _is_unittest_debug(): return bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) @@ -49,8 +45,10 @@ def _is_unittest_debug(): def to_encoding(ustring): string = '' if IS_PY3: - basestring = (str, bytes) - if isinstance(ustring, basestring): + strtype = (str, bytes) + else: # pragma: no cover + strtype = basestring + if isinstance(ustring, strtype): string = ustring elif ustring is not None: string = str(ustring) @@ -59,8 +57,6 @@ def to_encoding(ustring): return string preferred = locale.getpreferredencoding() - if _is_unittest(): - preferred = "UTF-8" return string.encode(preferred, 'replace') @@ -79,7 +75,7 @@ def open_without_clobber(name, *args): if err.errno == errno.EEXIST: name = "%s.%i" % (orig_name, count) count += 1 - else: + else: # pragma: no cover raise IOError(err.errno, err.strerror, err.filename) fobj = open(name, *args) if fd != fobj.fileno(): @@ -116,7 +112,7 @@ def setup_logging(debug, verbose): log.setLevel(WARN) if _is_unittest_debug(): - log.setLevel(DEBUG) + log.setLevel(DEBUG) # pragma: no cover ################## @@ -275,10 +271,6 @@ def _parser_add_bz_fields(rootp, command): "bugzilla instance has a custom field cf_my_field, do:\n" " --field cf_my_field=VALUE") - # Used by unit tests, not for end user consumption - p.add_argument('--__test-return-result', action="store_true", - dest="test_return_result", help=argparse.SUPPRESS) - if not cmd_modify: _parser_add_output_options(rootp) @@ -590,8 +582,6 @@ def _do_query(bz, opt, parser): if not q: parser.error("'query' command requires additional arguments") - if opt.test_return_result: - return q return bz.query(q) @@ -678,7 +668,7 @@ def _convert_to_outputformat(output): fmt += "#%{bug_id} %{status} %{assigned_to} %{component}\t" fmt += "[%{target_milestone}] %{flags} %{cve}" - else: + else: # pragma: no cover raise RuntimeError("Unknown output type '%s'" % output) return fmt @@ -737,9 +727,8 @@ def bug_field(matchobj): for bl in b.blocks: cvebug = bz.getbug(bl) for cb in cvebug.alias: - if cb.find("CVE") == -1: - continue - if cb.strip() not in cves: + if (cb.find("CVE") != -1 and + cb.strip() not in cves): cves.append(cb) val = ",".join(cves) @@ -831,9 +820,6 @@ def parse_multi(val): _merge_field_opts(ret, opt, parser) - if opt.test_return_result: - return ret - b = bz.createbug(ret) b.refresh() return [b] @@ -935,9 +921,6 @@ def _do_modify(bz, parser, opt): if not any([update, wbmap, add_tags, rm_tags]): parser.error("'modify' command requires additional arguments") - if opt.test_return_result: - return (update, wbmap, add_tags, rm_tags) - if add_tags or rm_tags: ret = bz.update_tags(bugid_list, tags_add=add_tags, tags_remove=rm_tags) @@ -1063,14 +1046,13 @@ def _make_bz_instance(opt): tokenfile = opt.tokenfile or -1 use_creds = True - bz = bugzilla.Bugzilla( + return bugzilla.Bugzilla( url=opt.bugzilla, cookiefile=cookiefile, tokenfile=tokenfile, sslverify=opt.sslverify, use_creds=use_creds, cert=opt.cert) - return bz def _handle_login(opt, action, bz): @@ -1141,23 +1123,13 @@ def _main(unittest_bz_instance): buglist = [] if action == 'info': - if not (opt.products or - opt.components or - opt.component_owners or - opt.versions): - parser.error("'info' command requires additional arguments") - _do_info(bz, opt) elif action == 'query': buglist = _do_query(bz, opt, parser) - if opt.test_return_result: - return buglist elif action == 'new': buglist = _do_new(bz, opt, parser) - if opt.test_return_result: - return buglist elif action == 'attach': if opt.get or opt.getall: @@ -1169,10 +1141,8 @@ def _main(unittest_bz_instance): _do_set_attach(bz, opt, parser) elif action == 'modify': - modout = _do_modify(bz, parser, opt) - if opt.test_return_result: - return modout - else: + _do_modify(bz, parser, opt) + else: # pragma: no cover raise RuntimeError("Unexpected action '%s'" % action) # If we're doing new/query/modify, output our results @@ -1187,6 +1157,9 @@ def main(unittest_bz_instance=None): except (Exception, KeyboardInterrupt): log.debug("", exc_info=True) raise + except KeyboardInterrupt: + print("\nExited at user request.") + sys.exit(1) except (Fault, bugzilla.BugzillaError) as e: print("\nServer error: %s" % str(e)) sys.exit(3) @@ -1200,15 +1173,11 @@ def main(unittest_bz_instance=None): except (socket.error, requests.exceptions.HTTPError, requests.exceptions.ConnectionError, + requests.exceptions.InvalidURL, ProtocolError) as e: print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) def cli(): - try: - main() - except KeyboardInterrupt: - log.debug("", exc_info=True) - print("\nExited at user request.") - sys.exit(1) + main() diff --git a/bugzilla/base.py b/bugzilla/base.py index 191ac9a9..097d8820 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -234,7 +234,6 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, if url: self.connect(url) - self._init_class_from_url() self._init_class_state() def _init_class_from_url(self): @@ -263,6 +262,7 @@ def _init_class_from_url(self): return self.__class__ = c + c._init_class_state(self) # disable=protected-access def _init_class_state(self): """ @@ -441,7 +441,7 @@ def _set_bz_version(self, version): def _get_backend_class(self): # This is a hook for the test suite to do some mock hackery - return _BackendXMLRPC + return _BackendXMLRPC # pragma: no cover def connect(self, url=None): """ @@ -485,6 +485,7 @@ def connect(self, url=None): version = self._backend.bugzilla_version()["version"] log.debug("Bugzilla version string: %s", version) self._set_bz_version(version) + self._init_class_from_url() @property diff --git a/tests/__init__.py b/tests/__init__.py index e946e205..35c496b7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -18,25 +18,13 @@ class _CLICONFIG(object): def __init__(self): self.REDHAT_URL = None + self.REGENERATE_OUTPUT = False CLICONFIG = _CLICONFIG() os.environ["__BUGZILLA_UNITTEST"] = "1" -def make_bz(version, *args, **kwargs): - cls = Bugzilla - if kwargs.pop("rhbz", False): - cls = RHBugzilla - if "cookiefile" not in kwargs and "tokenfile" not in kwargs: - kwargs["use_creds"] = False - if "url" not in kwargs: - kwargs["url"] = None - bz = cls(*args, **kwargs) - bz._set_bz_version(version) # pylint: disable=protected-access - return bz - - def clicomm(argvstr, bzinstance, returnmain=False, stdin=None, expectfail=False): """ diff --git a/tests/conftest.py b/tests/conftest.py index 84caf0c5..68b601f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,22 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import locale import logging import os +import pytest + import tests +import tests.utils import bugzilla +# Use consistent locale for tests +locale.setlocale(locale.LC_CTYPE, 'en_US.UTF-8') + + # pytest plugin adding custom options. Hooks are documented here: # https://docs.pytest.org/en/latest/writing_plugins.html @@ -24,6 +35,9 @@ def pytest_addoption(parser): parser.addoption("--pybz-debug", action="store_true", default=False, help=("Enable python-bugzilla debug output. This may break " "output comparison tests.")) + parser.addoption("--regenerate-output", + action="store_true", default=False, + help=("Force regeneration of generated test output")) def pytest_ignore_collect(path, config): @@ -49,3 +63,17 @@ def pytest_configure(config): if config.getoption("--pybz-debug"): logging.getLogger(bugzilla.__name__).setLevel(logging.DEBUG) os.environ["__BUGZILLA_UNITTEST_DEBUG"] = "1" + if config.getoption("--regenerate-output"): + tests.CLICONFIG.REGENERATE_OUTPUT = config.getoption( + "--regenerate-output") + + +@pytest.fixture +def run_cli(capsys, monkeypatch): + """ + Custom pytest fixture to pass a function for testing + a bugzilla cli command. + """ + def _do_run(*args, **kwargs): + return tests.utils.do_run_cli(capsys, monkeypatch, *args, **kwargs) + yield _do_run diff --git a/tests/data/clioutput/test_info_components-active.txt b/tests/data/clioutput/test_info_components-active.txt new file mode 100644 index 00000000..265e86d7 --- /dev/null +++ b/tests/data/clioutput/test_info_components-active.txt @@ -0,0 +1,2 @@ +backend/kernel +client-interfaces diff --git a/tests/data/clioutput/test_info_components-owners.txt b/tests/data/clioutput/test_info_components-owners.txt new file mode 100644 index 00000000..68c2aa89 --- /dev/null +++ b/tests/data/clioutput/test_info_components-owners.txt @@ -0,0 +1,2 @@ +client-interfaces: Fake Guy +configuration: ANother fake dude! diff --git a/tests/data/clioutput/test_info_components.txt b/tests/data/clioutput/test_info_components.txt new file mode 100644 index 00000000..7f2c49bc --- /dev/null +++ b/tests/data/clioutput/test_info_components.txt @@ -0,0 +1,3 @@ +comp1 +hey-imma-comp +test-comp-2 diff --git a/tests/data/clioutput/test_info_products.txt b/tests/data/clioutput/test_info_products.txt new file mode 100644 index 00000000..e3612b11 --- /dev/null +++ b/tests/data/clioutput/test_info_products.txt @@ -0,0 +1,2 @@ +Prod 1 Test +test-fake-product diff --git a/tests/data/clioutput/test_info_versions.txt b/tests/data/clioutput/test_info_versions.txt new file mode 100644 index 00000000..5bbac933 --- /dev/null +++ b/tests/data/clioutput/test_info_versions.txt @@ -0,0 +1,2 @@ +7.1 +fooversion! diff --git a/tests/data/clioutput/test_interactive_login_apikey_rcfile.txt b/tests/data/clioutput/test_interactive_login_apikey_rcfile.txt new file mode 100644 index 00000000..3d266dd5 --- /dev/null +++ b/tests/data/clioutput/test_interactive_login_apikey_rcfile.txt @@ -0,0 +1,3 @@ +[example.com] +api_key = MY-FAKE-KEY + diff --git a/tests/data/clioutput/test_new1.txt b/tests/data/clioutput/test_new1.txt new file mode 100644 index 00000000..0e1e2bde --- /dev/null +++ b/tests/data/clioutput/test_new1.txt @@ -0,0 +1 @@ +#1694158 CLOSED - crobinso@redhat.com - python-bugzilla test bug for API minor_update diff --git a/tests/data/clioutput/test_query1-ids.txt b/tests/data/clioutput/test_query1-ids.txt new file mode 100644 index 00000000..71aca18a --- /dev/null +++ b/tests/data/clioutput/test_query1-ids.txt @@ -0,0 +1,2 @@ +508645 +668543 diff --git a/tests/data/clioutput/test_query1.txt b/tests/data/clioutput/test_query1.txt new file mode 100644 index 00000000..5d93087c --- /dev/null +++ b/tests/data/clioutput/test_query1.txt @@ -0,0 +1,2 @@ +#508645 NEW - Libvirt Maintainers - RFE: qemu: Support a managed autoconnect mode for host USB devices +#668543 NEW - Cole Robinson - RFE: warn users at guest start if networks/storage pools are inactive diff --git a/tests/data/clioutput/test_query2.txt b/tests/data/clioutput/test_query2.txt new file mode 100644 index 00000000..f7a3b723 --- /dev/null +++ b/tests/data/clioutput/test_query2.txt @@ -0,0 +1,64 @@ +Bugzilla 1165434: +ATTRIBUTE[actual_time]: 0.0 +ATTRIBUTE[alias]: [] +ATTRIBUTE[assigned_to]: lvm-team@redhat.com +ATTRIBUTE[assigned_to_detail]: 'DICT SCRUBBED' +ATTRIBUTE[autorefresh]: False +ATTRIBUTE[blocks]: [123456] +ATTRIBUTE[cc]: ['example@redhat.com', 'example2@redhat.com'] +ATTRIBUTE[cc_detail]: ['DICT SCRUBBED'] +ATTRIBUTE[cf_build_id]: +ATTRIBUTE[cf_conditional_nak]: [] +ATTRIBUTE[cf_cust_facing]: --- +ATTRIBUTE[cf_devel_whiteboard]: somedeveltag,someothertag +ATTRIBUTE[cf_doc_type]: Bug Fix +ATTRIBUTE[cf_environment]: +ATTRIBUTE[cf_fixed_in]: +ATTRIBUTE[cf_internal_whiteboard]: someinternal TAG +ATTRIBUTE[cf_last_closed]: 2016-03-03T22:15:07 +ATTRIBUTE[cf_partner]: [] +ATTRIBUTE[cf_pgm_internal]: +ATTRIBUTE[cf_pm_score]: 0 +ATTRIBUTE[cf_qa_whiteboard]: foo bar baz +ATTRIBUTE[cf_qe_conditional_nak]: [] +ATTRIBUTE[cf_release_notes]: +ATTRIBUTE[cf_target_upstream_version]: +ATTRIBUTE[cf_verified]: [] +ATTRIBUTE[classification]: Red Hat +ATTRIBUTE[comments]: ['DICT SCRUBBED'] +ATTRIBUTE[depends_on]: [112233] +ATTRIBUTE[docs_contact]: +ATTRIBUTE[estimated_time]: 0.0 +ATTRIBUTE[external_bugs]: ['DICT SCRUBBED'] +ATTRIBUTE[flags]: ['DICT SCRUBBED'] +ATTRIBUTE[groups]: ['somegroup'] +ATTRIBUTE[id]: 1165434 +ATTRIBUTE[is_cc_accessible]: True +ATTRIBUTE[is_confirmed]: True +ATTRIBUTE[is_creator_accessible]: True +ATTRIBUTE[is_open]: False +ATTRIBUTE[keywords]: ['key1', 'keyword2', 'Security'] +ATTRIBUTE[last_change_time]: 2018-12-09T19:12:12 +ATTRIBUTE[op_sys]: Linux +ATTRIBUTE[platform]: All +ATTRIBUTE[priority]: medium +ATTRIBUTE[product]: Red Hat Enterprise Linux 5 +ATTRIBUTE[qa_contact]: mspqa-list@redhat.com +ATTRIBUTE[qa_contact_detail]: 'DICT SCRUBBED' +ATTRIBUTE[remaining_time]: 0.0 +ATTRIBUTE[resolution]: WONTFIX +ATTRIBUTE[see_also]: [] +ATTRIBUTE[severity]: medium +ATTRIBUTE[status]: CLOSED +ATTRIBUTE[sub_components]: 'DICT SCRUBBED' +ATTRIBUTE[summary]: LVM mirrored root can deadlock dmeventd if a mirror leg is lost +ATTRIBUTE[tags]: [] +ATTRIBUTE[target_milestone]: rc +ATTRIBUTE[target_release]: ['---'] +ATTRIBUTE[url]: +ATTRIBUTE[version]: ['5.8'] +ATTRIBUTE[weburl]: https:///TESTSUITEMOCK +ATTRIBUTE[whiteboard]: genericwhiteboard + + + diff --git a/tests/data/clioutput/test_query3.txt b/tests/data/clioutput/test_query3.txt new file mode 100644 index 00000000..0e223c4c --- /dev/null +++ b/tests/data/clioutput/test_query3.txt @@ -0,0 +1,23 @@ +:::genericwhiteboard:qe_test_coverage?,release?,pm_ack?,devel_ack?,qa_ack+,needinfo?:hello@example.com::?:: +* 2014-11-19T00:26:50 - example@redhat.com: +Description of problem: +Version-Release number of selected component (if applicable): +kernel-2.6.18-308.el5 +device-mapper-multipath-0.4.7-48.el5 +device-mapper-1.02.67-2.el5 +device-mapper-1.02.67-2.el5 +device-mapper-event-1.02.67-2.el5 + + +* 2014-11-19T00:47:57 - example@redhat.com: +We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond: + +crash> bt +any interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device. + +* 2014-11-19T01:53:53 - example@redhat.com: +Test text +:: +External bug: https://bugzilla.gnome.org/show_bug.cgi?id=703421 +External bug: https://bugs.launchpad.net/bugs/1203576 + diff --git a/tests/data/clioutput/test_query4.txt b/tests/data/clioutput/test_query4.txt new file mode 100644 index 00000000..7b1accb3 --- /dev/null +++ b/tests/data/clioutput/test_query4.txt @@ -0,0 +1,26 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost +Component: +CC: example@redhat.com,example2@redhat.com +Blocked: 123456 +Depends: 112233 + +* 2014-11-19T00:26:50 - example@redhat.com: +Description of problem: +Version-Release number of selected component (if applicable): +kernel-2.6.18-308.el5 +device-mapper-multipath-0.4.7-48.el5 +device-mapper-1.02.67-2.el5 +device-mapper-1.02.67-2.el5 +device-mapper-event-1.02.67-2.el5 + + +* 2014-11-19T00:47:57 - example@redhat.com: +We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond: + +crash> bt +any interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device. + +* 2014-11-19T01:53:53 - example@redhat.com: +Test text + + diff --git a/tests/data/clioutput/test_query5.txt b/tests/data/clioutput/test_query5.txt new file mode 100644 index 00000000..b82e66d2 --- /dev/null +++ b/tests/data/clioutput/test_query5.txt @@ -0,0 +1,6 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost + +Keywords: key1,keyword2,Security + +QA Whiteboard: + +Status Whiteboard: genericwhiteboard + +Devel Whiteboard: + diff --git a/tests/data/clioutput/test_query6.txt b/tests/data/clioutput/test_query6.txt new file mode 100644 index 00000000..58990a65 --- /dev/null +++ b/tests/data/clioutput/test_query6.txt @@ -0,0 +1 @@ +#1165434 CLOSED lvm-team@redhat.com [rc] qe_test_coverage?,release?,pm_ack?,devel_ack?,qa_ack+,needinfo? CVE-1234-5678 diff --git a/tests/data/clioutput/test_query7.txt b/tests/data/clioutput/test_query7.txt new file mode 100644 index 00000000..76cb0117 --- /dev/null +++ b/tests/data/clioutput/test_query7.txt @@ -0,0 +1 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost diff --git a/tests/data/mockargs/test_api_login1.txt b/tests/data/mockargs/test_api_login1.txt new file mode 100644 index 00000000..a94a371a --- /dev/null +++ b/tests/data/mockargs/test_api_login1.txt @@ -0,0 +1 @@ +{'login': None, 'password': None} \ No newline at end of file diff --git a/tests/data/mockargs/test_attach1.txt b/tests/data/mockargs/test_attach1.txt new file mode 100644 index 00000000..ec8a02ee --- /dev/null +++ b/tests/data/mockargs/test_attach1.txt @@ -0,0 +1,7 @@ +{'comment': 'some comment to go with it', + 'content_type': 'text/x-patch', + 'file_name': 'bz-attach-get1.txt', + 'ids': ['123456'], + 'is_patch': True, + 'is_private': True, + 'summary': 'bz-attach-get1.txt'} \ No newline at end of file diff --git a/tests/data/mockargs/test_attach2.txt b/tests/data/mockargs/test_attach2.txt new file mode 100644 index 00000000..aa296c52 --- /dev/null +++ b/tests/data/mockargs/test_attach2.txt @@ -0,0 +1,4 @@ +{'content_type': 'text/plain', + 'file_name': 'fake-file-name.txt', + 'ids': ['123456'], + 'summary': 'Some attachment description'} \ No newline at end of file diff --git a/tests/data/mockargs/test_attach_get1.txt b/tests/data/mockargs/test_attach_get1.txt new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/data/mockargs/test_attach_get1.txt @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/data/mockargs/test_attach_get2.txt b/tests/data/mockargs/test_attach_get2.txt new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/data/mockargs/test_attach_get2.txt @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/data/mockargs/test_info_components-active.txt b/tests/data/mockargs/test_info_components-active.txt new file mode 100644 index 00000000..92b664f6 --- /dev/null +++ b/tests/data/mockargs/test_info_components-active.txt @@ -0,0 +1,2 @@ +{'include_fields': ['name', 'id', 'components.name', 'components.is_active'], + 'names': ['test-fake-product']} \ No newline at end of file diff --git a/tests/data/mockargs/test_info_components-legalvalues.txt b/tests/data/mockargs/test_info_components-legalvalues.txt new file mode 100644 index 00000000..d6dcd341 --- /dev/null +++ b/tests/data/mockargs/test_info_components-legalvalues.txt @@ -0,0 +1 @@ +{'field': 'component', 'product_id': 7} \ No newline at end of file diff --git a/tests/data/mockargs/test_info_components-owners.txt b/tests/data/mockargs/test_info_components-owners.txt new file mode 100644 index 00000000..62b8cf45 --- /dev/null +++ b/tests/data/mockargs/test_info_components-owners.txt @@ -0,0 +1,5 @@ +{'include_fields': ['name', + 'id', + 'components.name', + 'components.default_assigned_to'], + 'names': ['test-fake-product']} \ No newline at end of file diff --git a/tests/data/mockargs/test_info_components.txt b/tests/data/mockargs/test_info_components.txt new file mode 100644 index 00000000..8f1c76ad --- /dev/null +++ b/tests/data/mockargs/test_info_components.txt @@ -0,0 +1,2 @@ +{'include_fields': ['name', 'id', 'components.name'], + 'names': ['test-fake-product']} \ No newline at end of file diff --git a/tests/data/mockargs/test_info_products.txt b/tests/data/mockargs/test_info_products.txt new file mode 100644 index 00000000..3a9690e1 --- /dev/null +++ b/tests/data/mockargs/test_info_products.txt @@ -0,0 +1 @@ +{'ids': [1, 7], 'include_fields': ['name', 'id']} \ No newline at end of file diff --git a/tests/data/mockargs/test_info_versions.txt b/tests/data/mockargs/test_info_versions.txt new file mode 100644 index 00000000..e93f737e --- /dev/null +++ b/tests/data/mockargs/test_info_versions.txt @@ -0,0 +1 @@ +{'include_fields': ['name', 'id', 'versions'], 'names': ['test-fake-product']} \ No newline at end of file diff --git a/tests/data/mockargs/test_interactive_login.txt b/tests/data/mockargs/test_interactive_login.txt new file mode 100644 index 00000000..c2b83597 --- /dev/null +++ b/tests/data/mockargs/test_interactive_login.txt @@ -0,0 +1 @@ +{'login': 'fakeuser', 'password': 'fakepass'} \ No newline at end of file diff --git a/tests/data/mockargs/test_login-restrict.txt b/tests/data/mockargs/test_login-restrict.txt new file mode 100644 index 00000000..18122767 --- /dev/null +++ b/tests/data/mockargs/test_login-restrict.txt @@ -0,0 +1 @@ +{'login': 'FOO', 'password': 'BAR', 'restrict_login': True} \ No newline at end of file diff --git a/tests/data/mockargs/test_login.txt b/tests/data/mockargs/test_login.txt new file mode 100644 index 00000000..309ed7bf --- /dev/null +++ b/tests/data/mockargs/test_login.txt @@ -0,0 +1 @@ +{'login': 'FOO', 'password': 'BAR'} \ No newline at end of file diff --git a/tests/data/mockargs/test_modify1.txt b/tests/data/mockargs/test_modify1.txt new file mode 100644 index 00000000..c0331f48 --- /dev/null +++ b/tests/data/mockargs/test_modify1.txt @@ -0,0 +1 @@ +{'component': 'NEWCOMP', 'ids': ['123456', '1234567'], 'status': 'ASSIGNED'} \ No newline at end of file diff --git a/tests/data/mockargs/test_modify2.txt b/tests/data/mockargs/test_modify2.txt new file mode 100644 index 00000000..86794afb --- /dev/null +++ b/tests/data/mockargs/test_modify2.txt @@ -0,0 +1,10 @@ +{'blocks': {'set': [123456, 445566]}, + 'component': 'NEWCOMP', + 'dupe_of': 555666, + 'flags': [{'name': '-needinfo,+somethingels', 'status': 'e'}], + 'groups': {'remove': ['BAR']}, + 'ids': ['123456'], + 'keywords': {'add': ['FOO']}, + 'resolution': 'DUPLICATE', + 'status': 'CLOSED', + 'whiteboard': 'thisone'} \ No newline at end of file diff --git a/tests/data/mockargs/test_modify3-tags.txt b/tests/data/mockargs/test_modify3-tags.txt new file mode 100644 index 00000000..7870724d --- /dev/null +++ b/tests/data/mockargs/test_modify3-tags.txt @@ -0,0 +1 @@ +{'ids': ['1165434'], 'tags': {'add': ['addtag'], 'remove': ['rmtag']}} \ No newline at end of file diff --git a/tests/data/mockargs/test_modify3.txt b/tests/data/mockargs/test_modify3.txt new file mode 100644 index 00000000..30e4ff31 --- /dev/null +++ b/tests/data/mockargs/test_modify3.txt @@ -0,0 +1,4 @@ +{'cf_devel_whiteboard': 'somedeveltag,someothertag devel-duh', + 'cf_internal_whiteboard': 'someinternal TAG internal-hey bar', + 'cf_qa_whiteboard': 'bar baz yo-qa', + 'ids': [1165434]} \ No newline at end of file diff --git a/tests/data/mockargs/test_new1.txt b/tests/data/mockargs/test_new1.txt new file mode 100644 index 00000000..50de3ec0 --- /dev/null +++ b/tests/data/mockargs/test_new1.txt @@ -0,0 +1,6 @@ +{'component': 'FOOCOMP', + 'description': 'This is the first comment!\nWith newline & stuff.', + 'groups': ['FOOGROUP', 'BARGROUP'], + 'keywords': ['ADDKEY'], + 'product': 'FOOPROD', + 'summary': 'Hey this is the title!'} \ No newline at end of file diff --git a/tests/data/mockargs/test_query1-ids.txt b/tests/data/mockargs/test_query1-ids.txt new file mode 100644 index 00000000..85288fb1 --- /dev/null +++ b/tests/data/mockargs/test_query1-ids.txt @@ -0,0 +1,4 @@ +{'component': ['foo', 'bar'], + 'id': ['1234', '2480'], + 'include_fields': ['id'], + 'product': ['foo']} \ No newline at end of file diff --git a/tests/data/mockargs/test_query1.txt b/tests/data/mockargs/test_query1.txt new file mode 100644 index 00000000..a0fcbbce --- /dev/null +++ b/tests/data/mockargs/test_query1.txt @@ -0,0 +1,4 @@ +{'component': ['foo', 'bar'], + 'id': ['1234', '2480'], + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'product': ['foo']} \ No newline at end of file diff --git a/tests/data/mockargs/test_query2.txt b/tests/data/mockargs/test_query2.txt new file mode 100644 index 00000000..2918d38f --- /dev/null +++ b/tests/data/mockargs/test_query2.txt @@ -0,0 +1 @@ +{'id': ['1165434'], 'include_fields': ['id']} \ No newline at end of file diff --git a/tests/data/mockargs/test_query3.txt b/tests/data/mockargs/test_query3.txt new file mode 100644 index 00000000..466fe072 --- /dev/null +++ b/tests/data/mockargs/test_query3.txt @@ -0,0 +1,10 @@ +{'bug_severity': ['sev1', 'sev2'], + 'include_fields': ['bar', + 'comments', + 'devel_whiteboard', + 'external_bugs', + 'flags', + 'flags_requestee', + 'foo', + 'whiteboard', + 'id']} \ No newline at end of file diff --git a/tests/data/mockargs/test_query4.txt b/tests/data/mockargs/test_query4.txt new file mode 100644 index 00000000..27565c82 --- /dev/null +++ b/tests/data/mockargs/test_query4.txt @@ -0,0 +1,16 @@ +{'bug_status': ['NEW', + 'ASSIGNED', + 'NEEDINFO', + 'ON_DEV', + 'MODIFIED', + 'POST', + 'REOPENED'], + 'include_fields': ['assigned_to', + 'blocks', + 'cc', + 'comments', + 'component', + 'depends_on', + 'id', + 'status', + 'summary']} \ No newline at end of file diff --git a/tests/data/mockargs/test_query5.txt b/tests/data/mockargs/test_query5.txt new file mode 100644 index 00000000..44710361 --- /dev/null +++ b/tests/data/mockargs/test_query5.txt @@ -0,0 +1,10 @@ +{'bug_status': ['ASSIGNED', 'ON_QA', 'FAILS_QA', 'PASSES_QA'], + 'component': ['foo', 'bar', 'baz'], + 'include_fields': ['assigned_to', + 'devel_whiteboard', + 'id', + 'keywords', + 'qa_whiteboard', + 'status', + 'summary', + 'whiteboard']} \ No newline at end of file diff --git a/tests/data/mockargs/test_query6.txt b/tests/data/mockargs/test_query6.txt new file mode 100644 index 00000000..3118a5fc --- /dev/null +++ b/tests/data/mockargs/test_query6.txt @@ -0,0 +1,11 @@ +{'BAR': 'WIBBLE', + 'FOO': '1', + 'bug_status': ['VERIFIED', 'RELEASE_PENDING', 'CLOSED'], + 'include_fields': ['assigned_to', + 'blocks', + 'component', + 'flags', + 'keywords', + 'status', + 'target_milestone', + 'id']} \ No newline at end of file diff --git a/tests/data/mockargs/test_query7.txt b/tests/data/mockargs/test_query7.txt new file mode 100644 index 00000000..c3b74a13 --- /dev/null +++ b/tests/data/mockargs/test_query7.txt @@ -0,0 +1,15 @@ +{'bug_status': ['NEW', + 'ASSIGNED', + 'MODIFIED', + 'ON_DEV', + 'ON_QA', + 'VERIFIED', + 'FAILS_QA', + 'RELEASE_PENDING', + 'POST'], + 'classification': 'Fedora', + 'component': 'virt-manager', + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'order': 'bug_status,bug_id', + 'product': 'Fedora', + 'query_format': 'advanced'} \ No newline at end of file diff --git a/tests/data/mockargs/test_query_cve_getbug.txt b/tests/data/mockargs/test_query_cve_getbug.txt new file mode 100644 index 00000000..825b67df --- /dev/null +++ b/tests/data/mockargs/test_query_cve_getbug.txt @@ -0,0 +1 @@ +{'ids': [123456]} \ No newline at end of file diff --git a/tests/data/mockreturn/test_attach_get1.txt b/tests/data/mockreturn/test_attach_get1.txt new file mode 100644 index 00000000..5a3a355a --- /dev/null +++ b/tests/data/mockreturn/test_attach_get1.txt @@ -0,0 +1,16 @@ +{'attachments': {'502352': {'bug_id': 663674, + 'content_type': 'text/plain', + 'creation_time': '2011-06-01T18:57:50Z', + 'creator': 'example', + 'data': 'SG9vcmF5IGZvciBNZXRlb3JvbG9naWNrw6kgenByw6F2eSA1XzA0LnBkZiEK', + 'file_name': 'Klíč memorial test file.txt', + 'flags': [], + 'id': 502352, + 'is_obsolete': 0, + 'is_patch': 0, + 'is_private': 0, + 'last_change_time': '2011-06-01T18:57:50Z', + 'size': 45, + 'summary': 'An empty test file with a utf-8 ' + 'filename'}}, + 'bugs': {}} diff --git a/tests/data/mockreturn/test_attach_get2.txt b/tests/data/mockreturn/test_attach_get2.txt new file mode 100644 index 00000000..e744a7aa --- /dev/null +++ b/tests/data/mockreturn/test_attach_get2.txt @@ -0,0 +1,43 @@ +{'attachments': {}, + 'bugs': {'663674': [{'bug_id': 663674, + 'content_type': 'application/octet-stream', + 'creation_time': '2010-12-16T15:28:01Z', + 'creator': 'example', + 'data': 'LS0tIGJhc2UucHkub2xkCTIwMTAtMTItMTYgMTI6MTU6MDkuOTMyMDEwNjU5ICswMTAwCisrKyBiYXNlLnB5CTIwMTAtMTItMTYgMTY6MDQ6MTguOTk1MTg1OTMzICswMTAwCkBAIC0xOSw2ICsxOSw4IEBACiBpbXBvcnQgdGVtcGZpbGUKIGltcG9ydCBsb2dnaW5nCiBpbXBvcnQgbG9jYWxlCitpbXBvcnQgZW1haWwuaGVhZGVyCitpbXBvcnQgcmUKIAogbG9nID0gbG9nZ2luZy5nZXRMb2dnZXIoJ2J1Z3ppbGxhJykKIApAQCAtNjc3LDEwICs2NzksMTcgQEAKICAgICAgICAgIyBSRkMgMjE4MyBkZWZpbmVzIHRoZSBjb250ZW50LWRpc3Bvc2l0aW9uIGhlYWRlciwgaWYgeW91J3JlIGN1cmlvdXMKICAgICAgICAgZGlzcCA9IGF0dC5oZWFkZXJzWydjb250ZW50LWRpc3Bvc2l0aW9uJ10uc3BsaXQoJzsnKQogICAgICAgICBbZmlsZW5hbWVfcGFybV0gPSBbaSBmb3IgaSBpbiBkaXNwIGlmIGkuc3RyaXAoKS5zdGFydHN3aXRoKCdmaWxlbmFtZT0nKV0KLSAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nKQotICAgICAgICAjIFJGQyAyMDQ1LzgyMiBkZWZpbmVzIHRoZSBncmFtbWFyIGZvciB0aGUgZmlsZW5hbWUgdmFsdWUsIGJ1dAotICAgICAgICAjIEkgdGhpbmsgd2UganVzdCBuZWVkIHRvIHJlbW92ZSB0aGUgcXVvdGluZy4gSSBob3BlLgotICAgICAgICBhdHQubmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nLDEpCisgICAgICAgICMgUkZDIDIwNDUvODIyIGRlZmluZXMgdGhlIGdyYW1tYXIgZm9yIHRoZSBmaWxlbmFtZSB2YWx1ZQorICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgIyBlbWFpbC5oZWFkZXIuZGVjb2RlX2hlYWRlciBjYW5ub3QgaGFuZGxlIHN0cmluZ3Mgbm90IGVuZGluZyB3aXRoICc/PScsCisgICAgICAgICMgc28gbGV0J3MgdHJhbnNmb3JtIG9uZSA9Py4uLj89IHBhcnQgYXQgYSB0aW1lCisgICAgICAgIHdoaWxlIFRydWU6CisgICAgICAgICAgICBtYXRjaCA9IHJlLnNlYXJjaCgiPVw/Lio/XD89IiwgZmlsZW5hbWUpCisgICAgICAgICAgICBpZiBtYXRjaCBpcyBOb25lOgorICAgICAgICAgICAgICAgIGJyZWFrCisgICAgICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lWzptYXRjaC5zdGFydCgpXSArIGVtYWlsLmhlYWRlci5kZWNvZGVfaGVhZGVyKG1hdGNoLmdyb3VwKDApKVswXVswXSArIGZpbGVuYW1lW21hdGNoLmVuZCgpOl0KKyAgICAgICAgYXR0Lm5hbWUgPSBmaWxlbmFtZQogICAgICAgICAjIEhvb3JheSwgbm93IHdlIGhhdmUgYSBmaWxlLWxpa2Ugb2JqZWN0IHdpdGggLnJlYWQoKSBhbmQgLm5hbWUKICAgICAgICAgcmV0dXJuIGF0dAogCg==', + 'file_name': 'bugzilla-filename.patch', + 'flags': [], + 'id': 469147, + 'is_obsolete': 1, + 'is_patch': 0, + 'is_private': 0, + 'last_change_time': '2010-12-21T17:57:36Z', + 'size': 1390, + 'summary': 'Proposed patch'}, + {'bug_id': 663674, + 'content_type': 'text/plain', + 'creation_time': '2010-12-21T17:57:36Z', + 'creator': 'example', + 'data': 'LS0tIC91c3IvbGliL3B5dGhvbjIuNy9zaXRlLXBhY2thZ2VzL2J1Z3ppbGxhL2Jhc2UucHkub3JpZwkyMDEwLTEyLTIxIDEzOjA1OjI5LjcyNzE4OTE0MSArMDEwMAorKysgL3Vzci9saWIvcHl0aG9uMi43L3NpdGUtcGFja2FnZXMvYnVnemlsbGEvYmFzZS5weQkyMDEwLTEyLTIxIDE4OjQ4OjMxLjU5NDgwMjMwNSArMDEwMApAQCAtMTksNiArMTksOCBAQAogaW1wb3J0IHRlbXBmaWxlCiBpbXBvcnQgbG9nZ2luZwogaW1wb3J0IGxvY2FsZQoraW1wb3J0IHJlCitpbXBvcnQgZW1haWwuaGVhZGVyCiAKIGxvZyA9IGxvZ2dpbmcuZ2V0TG9nZ2VyKCdidWd6aWxsYScpCiAKQEAgLTY3NywxMCArNjc5LDEzIEBACiAgICAgICAgICMgUkZDIDIxODMgZGVmaW5lcyB0aGUgY29udGVudC1kaXNwb3NpdGlvbiBoZWFkZXIsIGlmIHlvdSdyZSBjdXJpb3VzCiAgICAgICAgIGRpc3AgPSBhdHQuaGVhZGVyc1snY29udGVudC1kaXNwb3NpdGlvbiddLnNwbGl0KCc7JykKICAgICAgICAgW2ZpbGVuYW1lX3Bhcm1dID0gW2kgZm9yIGkgaW4gZGlzcCBpZiBpLnN0cmlwKCkuc3RhcnRzd2l0aCgnZmlsZW5hbWU9JyldCi0gICAgICAgIChkdW1teSxmaWxlbmFtZSkgPSBmaWxlbmFtZV9wYXJtLnNwbGl0KCc9JykKLSAgICAgICAgIyBSRkMgMjA0NS84MjIgZGVmaW5lcyB0aGUgZ3JhbW1hciBmb3IgdGhlIGZpbGVuYW1lIHZhbHVlLCBidXQKLSAgICAgICAgIyBJIHRoaW5rIHdlIGp1c3QgbmVlZCB0byByZW1vdmUgdGhlIHF1b3RpbmcuIEkgaG9wZS4KLSAgICAgICAgYXR0Lm5hbWUgPSBmaWxlbmFtZS5zdHJpcCgnIicpCisgICAgICAgIChkdW1teSxmaWxlbmFtZSkgPSBmaWxlbmFtZV9wYXJtLnNwbGl0KCc9JywgMSkKKyAgICAgICAgIyBSRkMgMjA0NS84MjIgZGVmaW5lcyB0aGUgZ3JhbW1hciBmb3IgdGhlIGZpbGVuYW1lIHZhbHVlCisgICAgICAgIGZpbGVuYW1lID0gZmlsZW5hbWUuc3RyaXAoJyInKQorICAgICAgICAjIEluIGNhc2UgdGhlIGZpbGVuYW1lIGlzIG5vdCBjb21wbGlhbnQgd2l0aCB0aGUgc3RhbmRhcmQsIGxldCdzIG1ha2UKKyAgICAgICAgIyBpdCBjb3JyZWN0LgorICAgICAgICBmaWVsZHMgPSBlbWFpbC5oZWFkZXIuZGVjb2RlX2hlYWRlcihyZS5zdWIoJyg9XD8oW15cP10qXD8pezN9PSknLCAnIFxcMSAnLCBmaWxlbmFtZSkpCisgICAgICAgIGF0dC5uYW1lID0gJycuam9pbihmaWVsZFsxXSBhbmQgZmllbGRbMF0uZGVjb2RlKGZpZWxkWzFdKSBvciBmaWVsZFswXSBmb3IgZmllbGQgaW4gZmllbGRzKQogICAgICAgICAjIEhvb3JheSwgbm93IHdlIGhhdmUgYSBmaWxlLWxpa2Ugb2JqZWN0IHdpdGggLnJlYWQoKSBhbmQgLm5hbWUKICAgICAgICAgcmV0dXJuIGF0dAogCg==', + 'file_name': 'bugzilla-filename-2.patch', + 'flags': [], + 'id': 470041, + 'is_obsolete': 0, + 'is_patch': 1, + 'is_private': 0, + 'last_change_time': '2010-12-21T17:57:36Z', + 'size': 1351, + 'summary': 'Better proposed patch'}, + {'bug_id': 663674, + 'content_type': 'text/plain', + 'creation_time': '2011-06-01T18:57:50Z', + 'creator': 'example', + 'data': 'SG9vcmF5IGZvciBNZXRlb3JvbG9naWNrw6kgenByw6F2eSA1XzA0LnBkZiEK', + 'file_name': 'Klíč memorial test file.txt', + 'flags': [], + 'id': 502352, + 'is_obsolete': 0, + 'is_patch': 0, + 'is_private': 0, + 'last_change_time': '2011-06-01T18:57:50Z', + 'size': 45, + 'summary': 'An empty test file with a utf-8 filename'}]}} diff --git a/tests/data/mockreturn/test_getbug.txt b/tests/data/mockreturn/test_getbug.txt new file mode 100644 index 00000000..7824dfa9 --- /dev/null +++ b/tests/data/mockreturn/test_getbug.txt @@ -0,0 +1,72 @@ +{'bugs': [{ + 'actual_time': 0.0, + 'alias': [], + 'assigned_to': 'crobinso@redhat.com', + 'assigned_to_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'blocks': [], + 'cc': ['crobinso@redhat.com'], + 'cc_detail': [{'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}], + 'cf_build_id': '', + 'cf_conditional_nak': [], + 'cf_cust_facing': '---', + 'cf_devel_whiteboard': '', + 'cf_doc_type': 'If docs needed, set a value', + 'cf_environment': '', + 'cf_fixed_in': '', + 'cf_internal_whiteboard': '', + 'cf_last_closed': '2019-03-29T16:39:27', + 'cf_partner': [], + 'cf_pgm_internal': '', + 'cf_pm_score': '0', + 'cf_qa_whiteboard': '', + 'cf_qe_conditional_nak': [], + 'cf_release_notes': '', + 'cf_verified': [], + 'classification': 'Fedora', + 'component': ['python-bugzilla'], + 'creation_time': '2019-03-29T16:39:01', + 'creator': 'crobinso@redhat.com', + 'creator_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'depends_on': [], + 'docs_contact': '', + 'estimated_time': 0.0, + 'groups': [], + 'id': 1694158, + 'is_cc_accessible': True, + 'is_confirmed': True, + 'is_creator_accessible': True, + 'is_open': False, + 'keywords': [], + 'last_change_time': '2019-03-29T16:57:48', + 'op_sys': 'Unspecified', + 'platform': 'Unspecified', + 'priority': 'unspecified', + 'product': 'Fedora', + 'qa_contact': 'extras-qa@fedoraproject.org', + 'qa_contact_detail': {'email': 'extras-qa@fedoraproject.org', + 'id': 171387, + 'name': 'extras-qa@fedoraproject.org', + 'real_name': 'Fedora Extras Quality ' + 'Assurance'}, + 'remaining_time': 0.0, + 'resolution': 'NOTABUG', + 'see_also': [], + 'severity': 'unspecified', + 'status': 'CLOSED', + 'summary': 'python-bugzilla test bug for API minor_update', + 'target_milestone': '---', + 'target_release': ['---'], + 'update_token': '1578493259-h3_XQLcFwQkxxRzj5fTivx_wB8OizN7dPUyU_iJ59Bc', + 'url': '', + 'version': ['30'], + 'whiteboard': ''}], +'faults': []} diff --git a/tests/data/mockreturn/test_getbug_rhel.txt b/tests/data/mockreturn/test_getbug_rhel.txt new file mode 100644 index 00000000..1e0632a2 --- /dev/null +++ b/tests/data/mockreturn/test_getbug_rhel.txt @@ -0,0 +1,196 @@ +{'faults': [], + "bugs" : [{ + 'actual_time': 0.0, + 'alias': [], + 'assigned_to': 'lvm-team@redhat.com', + 'assigned_to_detail': {'email': 'lvm-team@redhat.com', + 'id': 206817, + 'name': 'lvm-team@redhat.com', + 'real_name': 'LVM and device-mapper development team'}, + 'blocks': [123456], + 'cc': ['example@redhat.com', + 'example2@redhat.com'], + 'cc_detail': [{'email': 'example@redhat.com', + 'id': 123456, + 'name': 'example@redhat.com', + 'real_name': 'Example user'}, + {'email': 'example2@redhat.com', + 'id': 123457, + 'name': 'heinzm@redhat.com', + 'real_name': 'Example2 user'}], + 'cf_build_id': '', + 'cf_conditional_nak': [], + 'cf_cust_facing': '---', + 'cf_devel_whiteboard': 'somedeveltag,someothertag', + 'cf_doc_type': 'Bug Fix', + 'cf_environment': '', + 'cf_fixed_in': '', + 'cf_internal_whiteboard': 'someinternal TAG', + 'cf_last_closed': '2016-03-03T22:15:07', + 'cf_partner': [], + 'cf_pgm_internal': '', + 'cf_pm_score': '0', + 'cf_qa_whiteboard': 'foo bar baz', + 'cf_qe_conditional_nak': [], + 'cf_release_notes': '', + 'cf_target_upstream_version': '', + 'cf_verified': [], + 'classification': 'Red Hat', + 'comments': [{'bug_id': 1165434, + 'count': 0, + 'creation_time': '2014-11-19T00:26:50', + 'creator': 'example@redhat.com', + 'creator_id': 276776, + 'id': 7685441, + 'is_private': False, + 'tags': [], + 'text': 'Description of problem:\n' + 'Version-Release number of selected component (if ' + 'applicable):\n' + 'kernel-2.6.18-308.el5\n' + 'device-mapper-multipath-0.4.7-48.el5\n' + 'device-mapper-1.02.67-2.el5\n' + 'device-mapper-1.02.67-2.el5\n' + 'device-mapper-event-1.02.67-2.el5\n', + 'time': '2014-11-19T00:26:50'}, + {'bug_id': 1165434, + 'count': 1, + 'creation_time': '2014-11-19T00:47:57', + 'creator': 'example@redhat.com', + 'creator_id': 276776, + 'id': 7685467, + 'is_private': False, + 'tags': [], + 'text': 'We can see that there is a dmeventd task that has sent ' + 'data over a socket and is waiting for the peer to ' + 'respond:\n' + '\n' + 'crash> bt\n' + 'any interaction with the filesystem until it has ' + 'issued the suspend command to convert the mirror ' + 'device to a linear device.', + 'time': '2014-11-19T00:47:57'}, + {'bug_id': 1165434, + 'count': 2, + 'creation_time': '2014-11-19T01:53:53', + 'creator': 'example@redhat.com', + 'creator_id': 156796, + 'id': 7685595, + 'is_private': False, + 'tags': [], + 'text': 'Test text', + 'time': '2014-11-19T01:53:53'}], + 'depends_on': [112233], + 'docs_contact': '', + 'estimated_time': 0.0, + 'external_bugs': [{'bug_id': 989253, + 'ext_bz_bug_id': '703421', + 'ext_bz_id': 3, + 'ext_description': 'None', + 'ext_priority': 'None', + 'ext_status': 'None', + 'id': 115528, + 'type': {'can_get': True, + 'can_send': False, + 'description': 'GNOME Bugzilla', + 'full_url': 'https://bugzilla.gnome.org/show_bug.cgi?id=%id%', + 'id': 3, + 'must_send': False, + 'send_once': False, + 'type': 'Bugzilla', + 'url': 'https://bugzilla.gnome.org'}}, + {'bug_id': 989253, + 'ext_bz_bug_id': '1203576', + 'ext_bz_id': 29, + 'ext_description': 'None', + 'ext_priority': 'None', + 'ext_status': 'None', + 'id': 115527, + 'type': {'can_get': False, + 'can_send': False, + 'description': 'Launchpad', + 'full_url': 'https://bugs.launchpad.net/bugs/%id%', + 'id': 29, + 'must_send': False, + 'send_once': False, + 'type': 'None', + 'url': 'https://bugs.launchpad.net/bugs'}}], + 'flags': [{'creation_date': '2019-11-15T21:57:21Z', + 'id': 4302313, + 'is_active': 1, + 'modification_date': '2019-11-15T21:57:21Z', + 'name': 'qe_test_coverage', + 'setter': 'pm-rhel@redhat.com', + 'status': '?', + 'type_id': 318}, + {'creation_date': '2018-12-25T16:47:43Z', + 'id': 3883137, + 'is_active': 1, + 'modification_date': '2018-12-25T16:47:43Z', + 'name': 'release', + 'setter': 'rule-engine@redhat.com', + 'status': '?', + 'type_id': 1197}, + {'creation_date': '2018-12-25T16:47:38Z', + 'id': 3883134, + 'is_active': 1, + 'modification_date': '2018-12-25T16:47:38Z', + 'name': 'pm_ack', + 'setter': 'example3@redhat.com', + 'status': '?', + 'type_id': 11}, + {'creation_date': '2018-12-25T16:47:38Z', + 'id': 3883135, + 'is_active': 1, + 'modification_date': '2018-12-25T16:47:38Z', + 'name': 'devel_ack', + 'setter': 'example2@redhat.com', + 'status': '?', + 'type_id': 10}, + {'creation_date': '2018-12-25T16:47:38Z', + 'id': 3883136, + 'is_active': 1, + 'modification_date': '2019-04-28T02:07:03Z', + 'name': 'qa_ack', + 'setter': 'example@redhat.com', + 'status': '+', + 'type_id': 9}, + {'creation_date': '2019-03-29T06:50:01Z', + 'id': 3999302, + 'is_active': 1, + 'modification_date': '2019-03-29T06:50:01Z', + 'name': 'needinfo', + 'setter': 'example@redhat.com', + 'requestee': 'hello@example.com', + 'status': '?', + 'type_id': 1164}], + 'groups': ["somegroup"], + 'id': 1165434, + 'is_cc_accessible': True, + 'is_confirmed': True, + 'is_creator_accessible': True, + 'is_open': False, + 'keywords': ["key1", "keyword2", "Security"], + 'last_change_time': '2018-12-09T19:12:12', + 'op_sys': 'Linux', + 'platform': 'All', + 'priority': 'medium', + 'product': 'Red Hat Enterprise Linux 5', + 'qa_contact': 'mspqa-list@redhat.com', + 'qa_contact_detail': {'email': 'mspqa-list@redhat.com', + 'id': 164197, + 'name': 'mspqa-list@redhat.com', + 'real_name': 'Cluster QE'}, + 'remaining_time': 0.0, + 'resolution': 'WONTFIX', + 'see_also': [], + 'severity': 'medium', + 'status': 'CLOSED', + 'sub_components': {'lvm2': ['dmeventd (RHEL5)']}, + 'summary': 'LVM mirrored root can deadlock dmeventd if a mirror leg is lost', + 'tags': [], + 'target_milestone': 'rc', + 'target_release': ['---'], + 'url': '', + 'version': ['5.8'], + 'whiteboard': 'genericwhiteboard'}]} diff --git a/tests/data/mockreturn/test_query1.txt b/tests/data/mockreturn/test_query1.txt new file mode 100644 index 00000000..d1bdac5c --- /dev/null +++ b/tests/data/mockreturn/test_query1.txt @@ -0,0 +1 @@ +{'bugs': [{'assigned_to_detail': {'real_name': 'Libvirt Maintainers', 'email': 'libvirt-maint', 'name': 'libvirt-maint', 'id': 311982}, 'summary': 'RFE: qemu: Support a managed autoconnect mode for host USB devices', 'status': 'NEW', 'assigned_to': 'Libvirt Maintainers', 'id': 508645}, {'assigned_to_detail': {'real_name': 'Cole Robinson', 'email': 'crobinso', 'name': 'crobinso', 'id': 199727}, 'summary': 'RFE: warn users at guest start if networks/storage pools are inactive', 'status': 'NEW', 'assigned_to': 'Cole Robinson', 'id': 668543}]} diff --git a/tests/data/mockreturn/test_query_cve_getbug.txt b/tests/data/mockreturn/test_query_cve_getbug.txt new file mode 100644 index 00000000..a3c84e95 --- /dev/null +++ b/tests/data/mockreturn/test_query_cve_getbug.txt @@ -0,0 +1,71 @@ +{'bugs': [{'actual_time': 0.0, + 'alias': ["CVE-1234-5678"], + 'assigned_to': 'crobinso@redhat.com', + 'assigned_to_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'blocks': [], + 'cc': ['crobinso@redhat.com'], + 'cc_detail': [{'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}], + 'cf_build_id': '', + 'cf_conditional_nak': [], + 'cf_cust_facing': '---', + 'cf_devel_whiteboard': '', + 'cf_doc_type': 'If docs needed, set a value', + 'cf_environment': '', + 'cf_fixed_in': '', + 'cf_internal_whiteboard': '', + 'cf_last_closed': '2019-03-29T16:39:27', + 'cf_partner': [], + 'cf_pgm_internal': '', + 'cf_pm_score': '0', + 'cf_qa_whiteboard': '', + 'cf_qe_conditional_nak': [], + 'cf_release_notes': '', + 'cf_verified': [], + 'classification': 'Fedora', + 'component': ['python-bugzilla'], + 'creation_time': '2019-03-29T16:39:01', + 'creator': 'crobinso@redhat.com', + 'creator_detail': {'email': 'crobinso@redhat.com', + 'id': 199727, + 'name': 'crobinso@redhat.com', + 'real_name': 'Cole Robinson'}, + 'depends_on': [], + 'docs_contact': '', + 'estimated_time': 0.0, + 'groups': [], + 'id': 123456, + 'is_cc_accessible': True, + 'is_confirmed': True, + 'is_creator_accessible': True, + 'is_open': False, + 'keywords': [], + 'last_change_time': '2019-03-29T16:57:48', + 'op_sys': 'Unspecified', + 'platform': 'Unspecified', + 'priority': 'unspecified', + 'product': 'Fedora', + 'qa_contact': 'extras-qa@fedoraproject.org', + 'qa_contact_detail': {'email': 'extras-qa@fedoraproject.org', + 'id': 171387, + 'name': 'extras-qa@fedoraproject.org', + 'real_name': 'Fedora Extras Quality ' + 'Assurance'}, + 'remaining_time': 0.0, + 'resolution': 'NOTABUG', + 'see_also': [], + 'severity': 'unspecified', + 'status': 'CLOSED', + 'summary': 'python-bugzilla test bug for API minor_update', + 'target_milestone': '---', + 'target_release': ['---'], + 'update_token': '1578493259-h3_XQLcFwQkxxRzj5fTivx_wB8OizN7dPUyU_iJ59Bc', + 'url': '', + 'version': ['30'], + 'whiteboard': ''}], + 'faults': []} diff --git a/tests/mockbackend.py b/tests/mockbackend.py new file mode 100644 index 00000000..5ed3f90b --- /dev/null +++ b/tests/mockbackend.py @@ -0,0 +1,110 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import inspect + +import bugzilla +from bugzilla._backendbase import _BackendBase + +import tests.utils + + +# pylint: disable=abstract-method,arguments-differ + + +class BackendMock(_BackendBase): + _version = None + _extensions = None + + def bugzilla_version(self): + return {"version": self._version} + def bugzilla_extensions(self): + return self._extensions + + def __helper(self, args): + # Grab the calling function name and use it to generate + # input and output variable names for the class. So if this + # is called from bug_get, we look for: + # self._bug_get_args + # self._bug_get_return + prevfuncname = inspect.stack()[1][3] + func_args = getattr(self, "_%s_args" % prevfuncname) + func_return = getattr(self, "_%s_return" % prevfuncname) + if isinstance(func_return, BaseException): + raise func_return + + if isinstance(func_args, dict): + print(args[-1]) + assert func_args == args[-1] + elif func_args is not None: + tests.utils.diff_compare(args[-1], func_args) + + if isinstance(func_return, dict): + return func_return + + returnstr = open(tests.utils.tests_path(func_return)).read() + return eval(returnstr) # pylint: disable=eval-used + + def bug_attachment_create(self, *args): + return self.__helper(args) + def bug_attachment_get(self, *args): + return self.__helper(args) + def bug_attachment_get_all(self, *args): + return self.__helper(args) + + def bug_create(self, *args): + return self.__helper(args) + def bug_legal_values(self, *args): + return self.__helper(args) + def bug_get(self, *args): + return self.__helper(args) + def bug_search(self, *args): + return self.__helper(args) + def bug_update(self, *args): + return self.__helper(args) + def bug_update_tags(self, *args): + return self.__helper(args) + + def product_get(self, *args): + return self.__helper(args) + def product_get_accessible(self, *args): + return self.__helper(args) + + def user_get(self, *args): + return self.__helper(args) + def user_login(self, *args): + return self.__helper(args) + def user_logout(self, *args): + return self.__helper(args) + + +def _make_backend_class(version="6.0.0", extensions=None, + rhbz=False, **kwargs): + if not extensions: + extensions = {"extensions": {"foo": {"version": "0.01"}}} + if rhbz: + extensions["extensions"]['RedHat'] = {'version': '0.3'} + + class TmpBackendClass(BackendMock): + _version = version + _extensions = extensions + + for key, val in kwargs.items(): + setattr(TmpBackendClass, "_%s" % key, val) + + return TmpBackendClass + + +def make_bz(bz_kwargs=None, **kwargs): + bz_kwargs = (bz_kwargs or {}).copy() + if "url" in bz_kwargs: + raise RuntimeError("Can't set 'url' in mock make_bz, use connect()") + + if "use_creds" not in bz_kwargs: + bz_kwargs["use_creds"] = False + bz = bugzilla.Bugzilla(url=None, **bz_kwargs) + backendclass = _make_backend_class(**kwargs) + # pylint: disable=protected-access + bz._get_backend_class = lambda *a, **k: backendclass + bz.connect("https:///TESTSUITEMOCK") + return bz diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py new file mode 100644 index 00000000..3229b710 --- /dev/null +++ b/tests/test_api_bug.py @@ -0,0 +1,80 @@ +# +# Copyright Red Hat, Inc. 2014 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Unit tests for testing some bug.py magic +""" + +import pickle +import sys + +import tests +import tests.mockbackend +import tests.utils + +from bugzilla.bug import Bug + + +rhbz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + + +def testBasic(): + data = { + "bug_id": 123456, + "status": "NEW", + "assigned_to": "foo@bar.com", + "component": "foo", + "product": "bar", + "short_desc": "some short desc", + "cf_fixed_in": "nope", + "fixed_in": "1.2.3.4", + "devel_whiteboard": "some status value", + } + + bug = Bug(bugzilla=rhbz, dict=data) + + def _assert_bug(): + assert hasattr(bug, "component") is True + assert getattr(bug, "components") == ["foo"] + assert getattr(bug, "product") == "bar" + assert hasattr(bug, "short_desc") is True + assert getattr(bug, "summary") == "some short desc" + assert bool(getattr(bug, "cf_fixed_in")) is True + assert getattr(bug, "fixed_in") == "1.2.3.4" + assert bool(getattr(bug, "cf_devel_whiteboard")) is True + assert getattr(bug, "devel_whiteboard") == "some status value" + + _assert_bug() + + assert str(bug) == "#123456 NEW - foo@bar.com - some short desc" + assert repr(bug).startswith("= 3: + import io + fd = io.BytesIO() + else: + import StringIO # pylint: disable=import-error + fd = StringIO.StringIO() + + pickle.dump(bug, fd) + fd.seek(0) + bug = pickle.load(fd) + assert getattr(bug, "bugzilla") is None + bug.bugzilla = rhbz + _assert_bug() + + +def testBugNoID(): + try: + Bug(bugzilla=rhbz, dict={"component": "foo"}) + raise AssertionError("Expected lack of ID failure.") + except TypeError: + pass diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py new file mode 100644 index 00000000..76048fb1 --- /dev/null +++ b/tests/test_api_misc.py @@ -0,0 +1,312 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +from __future__ import print_function + +import os +import tempfile + +import pytest + +import bugzilla + +import tests +import tests.mockbackend + + +def test_mock_rhbz(): + fakebz = tests.mockbackend.make_bz(rhbz=True) + assert fakebz.__class__ == bugzilla.RHBugzilla + + +def testUserAgent(): + b3 = tests.mockbackend.make_bz(version="3.0.0") + assert "python-bugzilla" in b3.user_agent + + +def test_fixurl(): + assert (bugzilla.Bugzilla.fix_url("example.com") == + "https://example.com/xmlrpc.cgi") + assert (bugzilla.Bugzilla.fix_url("example.com/xmlrpc.cgi") == + "https://example.com/xmlrpc.cgi") + assert (bugzilla.Bugzilla.fix_url("http://example.com/somepath.cgi") == + "http://example.com/somepath.cgi") + + +def testCookies(): + dirname = os.path.dirname(__file__) + cookiesbad = dirname + "/data/cookies-bad.txt" + cookieslwp = dirname + "/data/cookies-lwp.txt" + cookiesmoz = dirname + "/data/cookies-moz.txt" + + # We used to convert LWP cookies, but it shouldn't matter anymore, + # so verify they fail at least + with pytest.raises(bugzilla.BugzillaError): + tests.mockbackend.make_bz(version="3.0.0", + bz_kwargs={"cookiefile": cookieslwp, "use_creds": True}) + + with pytest.raises(bugzilla.BugzillaError): + tests.mockbackend.make_bz(version="3.0.0", + bz_kwargs={"cookiefile": cookiesbad, "use_creds": True}) + + # Mozilla should 'just work' + tests.mockbackend.make_bz(version="3.0.0", + bz_kwargs={"cookiefile": cookiesmoz, "use_creds": True}) + + +def test_readconfig(): + # Testing for bugzillarc handling + bzapi = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + bzapi.url = "example.com" + temp = tempfile.NamedTemporaryFile(mode="w") + + content = """ +[example.com] +foo=1 +user=test1 +password=test2""" + temp.write(content) + temp.flush() + bzapi.readconfig(temp.name) + assert bzapi.user == "test1" + assert bzapi.password == "test2" + assert bzapi.api_key is None + + bzapi.url = "foo.example.com" + bzapi.user = None + bzapi.readconfig(temp.name) + assert bzapi.user is None + + content = """ +[foo.example.com] +user=test3 +password=test4 +api_key=123abc +""" + temp.write(content) + temp.flush() + bzapi.readconfig(temp.name) + assert bzapi.user == "test3" + assert bzapi.password == "test4" + assert bzapi.api_key == "123abc" + + bzapi.url = "bugzilla.redhat.com" + bzapi.user = None + bzapi.password = None + bzapi.api_key = None + bzapi.readconfig(temp.name) + assert bzapi.user is None + assert bzapi.password is None + assert bzapi.api_key is None + + +def testPostTranslation(): + def _testPostCompare(bz, indict, outexpect): + outdict = indict.copy() + bz.post_translation({}, outdict) + assert outdict == outexpect + + # Make sure multiple calls don't change anything + bz.post_translation({}, outdict) + assert outdict == outexpect + + bug3 = tests.mockbackend.make_bz(version="3.4.0") + rhbz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + + test1 = { + "component": ["comp1"], + "version": ["ver1", "ver2"], + + 'flags': [{ + 'is_active': 1, + 'name': 'qe_test_coverage', + 'setter': 'pm-rhel@redhat.com', + 'status': '?', + }, { + 'is_active': 1, + 'name': 'rhel-6.4.0', + 'setter': 'pm-rhel@redhat.com', + 'status': '+', + }], + + 'alias': ["FOO", "BAR"], + 'blocks': [782183, 840699, 923128], + 'keywords': ['Security'], + 'groups': ['redhat'], + } + + out_simple = test1.copy() + out_simple["components"] = out_simple["component"] + out_simple["component"] = out_simple["components"][0] + out_simple["versions"] = out_simple["version"] + out_simple["version"] = out_simple["versions"][0] + + _testPostCompare(bug3, test1, test1) + _testPostCompare(rhbz, test1, out_simple) + + +def testSubComponentFail(): + bz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + with pytest.raises(ValueError): + bz.build_update(sub_component="some sub component") + + +def testInvalidBoolean(): + bz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + with pytest.raises(RuntimeError): + bz.build_query(boolean_query="foobar") + + +def testCreatebugFieldConversion(): + bz4 = tests.mockbackend.make_bz(version="4.0.0") + vc = bz4._validate_createbug # pylint: disable=protected-access + out = vc(product="foo", component="bar", + version="12", description="foo", short_desc="bar", + check_args=False) + assert out == { + 'component': 'bar', 'description': 'foo', 'product': 'foo', + 'summary': 'bar', 'version': '12'} + + +def testURLSavedSearch(): + bz4 = tests.mockbackend.make_bz(version="4.0.0") + url = ("https://bugzilla.redhat.com/buglist.cgi?" + "cmdtype=dorem&list_id=2342312&namedcmd=" + "RHEL7%20new%20assigned%20virt-maint&remaction=run&" + "sharer_id=321167") + query = { + 'sharer_id': '321167', + 'savedsearch': 'RHEL7 new assigned virt-maint' + } + assert bz4.url_to_query(url) == query + + +def testStandardQuery(): + bz4 = tests.mockbackend.make_bz(version="4.0.0") + url = ("https://bugzilla.redhat.com/buglist.cgi?" + "component=virt-manager&query_format=advanced&classification=" + "Fedora&product=Fedora&bug_status=NEW&bug_status=ASSIGNED&" + "bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&" + "bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=" + "RELEASE_PENDING&bug_status=POST&order=bug_status%2Cbug_id") + query = { + 'product': 'Fedora', + 'query_format': 'advanced', + 'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', + 'ON_QA', 'VERIFIED', 'FAILS_QA', 'RELEASE_PENDING', 'POST'], + 'classification': 'Fedora', + 'component': 'virt-manager', + 'order': 'bug_status,bug_id' + } + assert bz4.url_to_query(url) == query + + # Test with unknown URL + assert bz4.url_to_query("https://example.com") == {} + + +def test_api_login(): + with pytest.raises(TypeError): + # Missing explicit URL + bugzilla.Bugzilla() + + with pytest.raises(Exception): + # Calling connect() with passed in URL + bugzilla.Bugzilla(url="https:///FAKEURL") + + bz = tests.mockbackend.make_bz() + + with pytest.raises(ValueError): + # Errors on missing user + bz.login() + + bz.user = "FOO" + with pytest.raises(ValueError): + # Errors on missing pass + bz.login() + + bz.password = "BAR" + bz.api_key = "WIBBLE" + with pytest.raises(ValueError): + # Errors on api_key + login() + bz.login() + + # Will log in immediately, hitting basic_auth path + bz = tests.mockbackend.make_bz( + bz_kwargs={"basic_auth": True, "user": "FOO", "password": "BAR"}, + user_login_args="data/mockargs/test_api_login1.txt", + user_login_return={}) + + # Hit default api_key code path + bz = tests.mockbackend.make_bz( + bz_kwargs={"api_key": "FAKE_KEY"}, + user_login_args="data/mockargs/test_api_login.txt", + user_login_return={}) + # Try reconnect, with RHBZ testing + bz.connect("https:///fake/bugzilla.redhat.com") + bz.connect() + + +def test_interactive_login(capsys, monkeypatch): + bz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_interactive_login.txt", + user_login_return={}, + user_logout_args=None, + user_logout_return={}, + user_get_args=None, + user_get_return={}) + + import sys + import getpass + + if sys.version_info[0] >= 3: + monkeypatch.setattr(getpass, "getpass", input) + else: + monkeypatch.setattr(getpass, "getpass", + raw_input) # pylint: disable=undefined-variable + + fakestdin = tests.utils.fake_stream("fakeuser\nfakepass\n") + monkeypatch.setattr(sys, "stdin", fakestdin) + bz.interactive_login() + bz.logout() + + out = capsys.readouterr()[0] + assert "Bugzilla Username:" in out + assert "Bugzilla Password:" in out + + # API key prompting and saving + tmp = tempfile.NamedTemporaryFile() + bz.configpath = [tmp.name] + bz.url = "https://example.com" + + fakestdin = tests.utils.fake_stream("MY-FAKE-KEY\n") + monkeypatch.setattr(sys, "stdin", fakestdin) + bz.interactive_login(use_api_key=True) + out = capsys.readouterr()[0] + assert "API Key:" in out + assert tmp.name in out + tests.utils.diff_compare(open(tmp.name).read(), + "data/clioutput/test_interactive_login_apikey_rcfile.txt") + + +def test_version_bad(): + # Hit version error handling + bz = tests.mockbackend.make_bz(version="badversion") + assert bz.bz_ver_major == 5 + assert bz.bz_ver_minor == 0 + + # pylint: disable=protected-access + assert bz._check_version(5, 0) + assert not bz._check_version(10000, 0) + + +def test_extensions_bad(): + # Hit bad extensions error handling + tests.mockbackend.make_bz(extensions="BADEXTENSIONS") diff --git a/tests/test_bug.py b/tests/test_bug.py deleted file mode 100644 index d0176ee6..00000000 --- a/tests/test_bug.py +++ /dev/null @@ -1,81 +0,0 @@ -# -# Copyright Red Hat, Inc. 2014 -# -# This work is licensed under the GNU GPLv2 or later. -# See the COPYING file in the top-level directory. -# - -""" -Unit tests for testing some bug.py magic -""" - -import pickle -import sys -import unittest - -import tests -from tests import StringIO - -from bugzilla.bug import Bug - - -rhbz = tests.make_bz("4.4.0", rhbz=True) - - -class BugTest(unittest.TestCase): - bz = rhbz - - def testBasic(self): - data = { - "bug_id": 123456, - "status": "NEW", - "assigned_to": "foo@bar.com", - "component": "foo", - "product": "bar", - "short_desc": "some short desc", - "cf_fixed_in": "nope", - "fixed_in": "1.2.3.4", - "devel_whiteboard": "some status value", - } - - bug = Bug(bugzilla=self.bz, dict=data) - - def _assert_bug(): - assert hasattr(bug, "component") is True - assert getattr(bug, "components") == ["foo"] - assert getattr(bug, "product") == "bar" - assert hasattr(bug, "short_desc") is True - assert getattr(bug, "summary") == "some short desc" - assert bool(getattr(bug, "cf_fixed_in")) is True - assert getattr(bug, "fixed_in") == "1.2.3.4" - assert bool(getattr(bug, "cf_devel_whiteboard")) is True - assert getattr(bug, "devel_whiteboard") == "some status value" - - _assert_bug() - - assert str(bug) == "#123456 NEW - foo@bar.com - some short desc" - assert repr(bug).startswith("= 3: - from io import BytesIO - fd = BytesIO() - else: - fd = StringIO() - - pickle.dump(bug, fd) - fd.seek(0) - bug = pickle.load(fd) - assert getattr(bug, "bugzilla") is None - bug.bugzilla = self.bz - _assert_bug() - - def testBugNoID(self): - try: - Bug(bugzilla=self.bz, dict={"component": "foo"}) - raise AssertionError("Expected lack of ID failure.") - except TypeError: - pass diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py new file mode 100644 index 00000000..9e20dc9b --- /dev/null +++ b/tests/test_cli_commands.py @@ -0,0 +1,407 @@ +# -*- coding: utf-8 -*- + +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import os +import re + +import pytest + +import bugzilla + +import tests +import tests.mockbackend +import tests.utils + + +################################# +# 'bugzilla login' mock testing # +################################# + +def test_login(run_cli): + cmd = "bugzilla login FOO BAR" + + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return=RuntimeError("TEST ERROR")) + out = run_cli(cmd, fakebz, expectfail=True) + assert "Login failed: TEST ERROR" in out + + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}) + out = run_cli(cmd, fakebz) + assert "Login successful" in out + + cmd = "bugzilla --restrict-login --user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login-restrict.txt", + user_login_return={}) + out = run_cli(cmd, fakebz) + assert "Login successful" in out + + cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" + # Raises raw error trying to see if we aren't logged in + with pytest.raises(NotImplementedError): + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return=NotImplementedError()) + out = run_cli(cmd, fakebz) + + # Errors with expected code + cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return=bugzilla.BugzillaError("TESTMESSAGE", code=505)) + out = run_cli(cmd, fakebz, expectfail=True) + assert "--ensure-logged-in passed but you" in out + + # Returns success for logged_in check and hits a tokenfile line + cmd = "bugzilla --ensure-logged-in " + cmd += "--user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True}, + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return={}) + out = run_cli(cmd, fakebz) + assert "token cache updated" in out + + +################################ +# 'bugzilla info' mock testing # +################################ + +def test_info(run_cli): + funcname = tests.utils.get_funcname() + argsprefix = "data/mockargs/%s_" % funcname + cliprefix = "data/clioutput/%s_" % funcname + + prod_accessible = {'ids': [1, 7]} + prod_get = {'products': [ + {'id': 1, 'name': 'Prod 1 Test'}, + {'id': 7, 'name': 'test-fake-product'} + ]} + + # info --products + fakebz = tests.mockbackend.make_bz( + product_get_accessible_args=None, + product_get_accessible_return=prod_accessible, + product_get_args=argsprefix + "products.txt", + product_get_return=prod_get) + cmd = "bugzilla info --products" + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "products.txt") + + # info --versions + prod_get_ver = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'versions': [ + {'id': 360, 'is_active': True, 'name': '7.1'}, + {'id': 123, 'is_active': True, 'name': 'fooversion!'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "versions.txt", + product_get_return=prod_get_ver) + cmd = "bugzilla info --versions test-fake-product" + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "versions.txt") + + # info --components + legal_values = {'values': ["comp1", "test-comp-2", "hey-imma-comp"]} + cmd = "bugzilla info --components test-fake-product" + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components.txt", + product_get_return=prod_get, + bug_legal_values_args=argsprefix + "components-legalvalues.txt", + bug_legal_values_return=legal_values) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components.txt") + + # info --components --active-components + cmd = "bugzilla info --components test-fake-product --active-components" + prod_get_comp_active = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'components': [ + {'is_active': True, 'name': 'backend/kernel'}, + {'is_active': True, 'name': 'client-interfaces'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components-active.txt", + product_get_return=prod_get_comp_active) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components-active.txt") + + # info --components_owners + cmd = "bugzilla info --component_owners test-fake-product" + prod_get_comp_owners = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'components': [ + {'default_assigned_to': 'Fake Guy', + 'name': 'client-interfaces'}, + {'default_assigned_to': 'ANother fake dude!', + 'name': 'configuration'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components-owners.txt", + product_get_return=prod_get_comp_owners) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components-owners.txt") + + +################################# +# 'bugzilla query' mock testing # +################################# + +def test_query(run_cli): + # query that ends up empty + cmd = "bugzilla query --ids " + fakebz = tests.mockbackend.make_bz(version="3.0.0") + out = run_cli(cmd, fakebz, expectfail=True) + assert "requires additional arguments" in out + + # bad field option + cmd = "bugzilla query --field FOO" + out = run_cli(cmd, fakebz, expectfail=True) + assert "Invalid field argument" in out + + # Simple query with some comma opts + cmd = "bugzilla query " + cmd += "--product foo --component foo,bar --bug_id 1234,2480" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query1.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1.txt") + + # Same but with --ids output + cmd = "bugzilla query --ids " + cmd += "--product foo --component foo,bar --bug_id 1234,2480" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query1-ids.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1-ids.txt") + + # Same but with --raw output + cmd = "bugzilla query --raw --bug_id 1165434" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query2.txt", + bug_search_return={"bugs": [{"id": 1165434}]}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + # Dictionary ordering is random, so scrub it from our output + out = re.sub(r"\{.*\}", r"'DICT SCRUBBED'", out, re.MULTILINE) + tests.utils.diff_compare(out, "data/clioutput/test_query2.txt") + + # Test a bunch of different combinations for code coverage + cmd = "bugzilla query --status ALL --severity sev1,sev2 " + cmd += "--outputformat='%{foo}:%{bar}::%{whiteboard}:" + cmd += "%{flags}:%{flags_requestee}%{whiteboard:devel}::" + cmd += "%{flag:needinfo}::%{comments}::%{external_bugs}'" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query3.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query3.txt") + + # Test --status DEV and --full + cmd = "bugzilla query --status DEV --full" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query4.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query4.txt") + + # Test --status QE and --extra, and components-file + compfile = os.path.dirname(__file__) + "/data/components_file.txt" + cmd = "bugzilla query --status QE --extra " + cmd += "--components_file %s" % compfile + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query5.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query5.txt") + + # Test --status EOL and --oneline, and some --field usage + cmd = "bugzilla query --status EOL --oneline " + cmd += "--field FOO=1 --field=BAR=WIBBLE " + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query6.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt", + bug_get_args="data/mockargs/test_query_cve_getbug.txt", + bug_get_return="data/mockreturn/test_query_cve_getbug.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query6.txt") + + # Test --status OPEN and --from-url + url = "https://bugzilla.redhat.com/buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=RELEASE_PENDING&bug_status=POST&classification=Fedora&component=virt-manager&order=bug_status%2Cbug_id&product=Fedora&query_format=advanced" # noqa + cmd = "bugzilla query --status OPEN --from-url %s" % url + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query7.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query7.txt") + + +############################### +# 'bugzilla new' mock testing # +############################### + +def test_new(run_cli): + # Bunch of options + cmd = "bugzilla new --product FOOPROD --component FOOCOMP " + cmd += "--summary 'Hey this is the title!' " + cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " + cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP" + + fakebz = tests.mockbackend.make_bz( + bug_create_args="data/mockargs/test_new1.txt", + bug_create_return={"id": 1694158}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_new1.txt") + + +################################## +# 'bugzilla modify' mock testing # +################################## + +def test_modify(run_cli): + # errors on missing args + cmd = "bugzilla modify 123456" + fakebz = tests.mockbackend.make_bz() + out = run_cli(cmd, fakebz, expectfail=True) + assert "additional arguments" in out + + # Modify basic + cmd = "bugzilla modify 123456 1234567 " + cmd += "--status ASSIGNED --component NEWCOMP " + fakebz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_modify1.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out + + # Modify with lots of opts + cmd = "bugzilla modify 123456 --component NEWCOMP " + cmd += "--keyword +FOO --groups=-BAR --blocked =123456,445566 " + cmd += "--flag=-needinfo,+somethingelse " + cmd += "--whiteboard =foo --whiteboard =thisone " + cmd += "--dupeid 555666 " + fakebz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_modify2.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out + + # Modify with tricky opts + cmd = "bugzilla modify 1165434 " + cmd += "--tags +addtag --tags=-rmtag " + cmd += "--qa_whiteboard +yo-qa --qa_whiteboard=-foo " + cmd += "--internal_whiteboard +internal-hey --internal_whiteboard +bar " + cmd += "--devel_whiteboard +devel-duh --devel_whiteboard=-yay " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_update_tags_args="data/mockargs/test_modify3-tags.txt", + bug_update_tags_return={}, + bug_update_args="data/mockargs/test_modify3.txt", + bug_update_return={}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + assert not out + + +################################## +# 'bugzilla attach' mock testing # +################################## + +def test_attach(run_cli): + attachfile = os.path.dirname(__file__) + "/data/bz-attach-get1.txt" + attachcontent = open(attachfile).read() + + # Hit error when no ID specified + fakebz = tests.mockbackend.make_bz() + out = run_cli("bugzilla attach", fakebz, expectfail=True) + assert "ID must be specified" in out + + # Hit error when using tty and no --file specified + out = run_cli("bugzilla attach 123456", fakebz, expectfail=True) + assert "--file must be specified" in out + + # Hit error when using stdin, but no --desc + out = run_cli("bugzilla attach 123456", fakebz, expectfail=True, + stdin=attachcontent) + assert "--description must be specified" in out + + # Basic CLI attach + cmd = "bugzilla attach 123456 --file=%s " % attachfile + cmd += "--type text/x-patch --private " + cmd += "--comment 'some comment to go with it'" + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach1.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz) + assert "Created attachment 1557949 on bug 123456" in out + + # Attach from stdin + cmd = "bugzilla attach 123456 --file=fake-file-name.txt " + cmd += "--description 'Some attachment description' " + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach2.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz, stdin=attachcontent) + assert "Created attachment 1557949 on bug 123456" in out + + +def _test_attach_get(run_cli): + # Hit error when using ids with --get* + fakebz = tests.mockbackend.make_bz() + out = run_cli("bugzilla attach 123456 --getall 123456", + fakebz, expectfail=True) + assert "not used for" in out + + # Basic --get ATTID usage + filename = u"Klíč memorial test file.txt" + cmd = "bugzilla attach --get 112233" + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_args="data/mockargs/test_attach_get1.txt", + bug_attachment_get_return="data/mockreturn/test_attach_get1.txt") + out = run_cli(cmd, fakebz) + assert filename in out + + # Basic --getall with --ignore-obsolete + cmd = "bugzilla attach --getall 663674 --ignore-obsolete" + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_all_args="data/mockargs/test_attach_get2.txt", + bug_attachment_get_all_return="data/mockreturn/test_attach_get2.txt") + out = run_cli(cmd, fakebz) + + os.system("ls %s" % os.getcwd()) + filename += ".1" + assert filename in out + assert "bugzilla-filename" in out + + +def test_attach_get(run_cli): + import tempfile + import shutil + tmpdir = tempfile.mkdtemp(dir=os.getcwd()) + origcwd = os.getcwd() + os.chdir(tmpdir) + try: + _test_attach_get(run_cli) + finally: + os.chdir(origcwd) + shutil.rmtree(tmpdir) diff --git a/tests/test_cli_misc.py b/tests/test_cli_misc.py new file mode 100644 index 00000000..39cff8a6 --- /dev/null +++ b/tests/test_cli_misc.py @@ -0,0 +1,93 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous CLI bits to get build out our code coverage +""" + +from __future__ import print_function + +import requests + +import bugzilla +import tests +import tests.mockbackend + + +def testHelp(run_cli): + out = run_cli("bugzilla --help", None) + assert len(out.splitlines()) > 18 + + +def testCmdHelp(run_cli): + out = run_cli("bugzilla query --help", None) + assert len(out.splitlines()) > 40 + + +def testVersion(run_cli): + out = run_cli("bugzilla --version", None) + assert out.strip() == bugzilla.__version__ + + +def testPositionalArgs(run_cli): + # Make sure cli correctly rejects ambiguous positional args + out = run_cli("bugzilla login --xbadarg foo", + None, expectfail=True) + assert "unrecognized arguments: --xbadarg" in out + + out = run_cli("bugzilla modify 123456 --foobar --status NEW", + None, expectfail=True) + assert "unrecognized arguments: --foobar" in out + + +def testDebug(run_cli): + # Coverage testing for debug options + run_cli("bugzilla --bugzilla https:///BADURI --verbose login", + None, expectfail=True) + run_cli("bugzilla --bugzilla https:///BADURI --debug login", + None, expectfail=True) + + +def testExceptions(run_cli): + """ + Test exception handling around main() + """ + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=KeyboardInterrupt()) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "user request" in out + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=bugzilla.BugzillaError("foo")) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "Server error:" in out + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=requests.exceptions.SSLError()) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "trust the remote server" in out + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return=requests.exceptions.ConnectionError()) + out = run_cli("bugzilla query --bug_id 1", fakebz, expectfail=True) + assert "Connection lost" in out + + +def testManualURL(run_cli): + """ + Test passing a manual URL, to hit those non-testsuite code paths + """ + try: + cmd = "bugzilla --bztype foobar " + cmd += "--bugzilla https:///FAKEURL query --bug_id 1" + run_cli(cmd, None) + except Exception as e: + assert "No host supplied" in str(e) diff --git a/tests/test_createbug.py b/tests/test_createbug.py deleted file mode 100644 index a5c3e28b..00000000 --- a/tests/test_createbug.py +++ /dev/null @@ -1,81 +0,0 @@ -# -# Copyright Red Hat, Inc. 2013 -# -# This work is licensed under the GNU GPLv2 or later. -# See the COPYING file in the top-level directory. -# - -""" -Unit tests for building createbug dictionaries with bin/bugzilla -""" - -import unittest - -import tests - - -bz4 = tests.make_bz("4.0.0") - - -class CreatebugTest(unittest.TestCase): - bz = bz4 - - def clicomm(self, argstr, out): - comm = "bugzilla new --__test-return-result " + argstr - q = tests.clicomm(comm, self.bz, returnmain=True) - assert out == q - - def testBasic(self): - self.clicomm( - "--product foo --component bar --summary baz --version 12", - {'component': 'bar', 'product': 'foo', - 'summary': 'baz', 'version': '12'} - ) - - def testOpSys(self): - self.clicomm( - "--os windowsNT --arch ia64 --comment 'youze a foo' --cc me", - {'description': 'youze a foo', 'op_sys': 'windowsNT', - 'platform': 'ia64', 'cc': ["me"]} - ) - - def testSeverity(self): - self.clicomm( - "--severity HIGH --priority Low --url http://example.com", - {'url': 'http://example.com', 'priority': 'Low', - 'severity': 'HIGH'} - ) - - def testMisc(self): - self.clicomm("--alias some-alias", - {"alias": "some-alias"} - ) - self.clicomm("--comment 'foo bar' --comment-tag tag1 " - "--comment-tag tag2", - {'comment_tags': ['tag1', 'tag2'], 'description': 'foo bar'}) - - def testMultiOpts(self): - # Test all opts that can take lists - out = {'blocks': ['3', '4'], 'cc': ['1', '2'], - 'depends_on': ['5', 'foo', 'wib'], 'groups': ['bar', '8'], - 'keywords': ['TestOnly', 'ZStream']} - self.clicomm( - "--cc 1,2 --blocked 3,4 --dependson 5,foo,wib --groups bar,8 " - "--keywords TestOnly,ZStream", - out - ) - self.clicomm( - "--cc 1 --cc 2 --blocked 3 --blocked 4 " - "--dependson 5,foo --dependson wib --groups bar --groups 8 " - "--keywords TestOnly --keywords ZStream", - out - ) - - def testFieldConversion(self): - vc = self.bz._validate_createbug # pylint: disable=protected-access - out = vc(product="foo", component="bar", - version="12", description="foo", short_desc="bar", - check_args=False) - assert out == { - 'component': 'bar', 'description': 'foo', 'product': 'foo', - 'summary': 'bar', 'version': '12'} diff --git a/tests/test_misc.py b/tests/test_misc.py deleted file mode 100644 index 9c2e7404..00000000 --- a/tests/test_misc.py +++ /dev/null @@ -1,173 +0,0 @@ -# -# Copyright Red Hat, Inc. 2012 -# -# This work is licensed under the GNU GPLv2 or later. -# See the COPYING file in the top-level directory. -# - -""" -Unit tests for building query strings with bin/bugzilla -""" - -from __future__ import print_function - -import os -import tempfile -import unittest - -import pytest - -import bugzilla -import tests - - -class MiscCLI(unittest.TestCase): - """ - Test miscellaneous CLI bits to get build out our code coverage - """ - maxDiff = None - - def testHelp(self): - out = tests.clicomm("bugzilla --help", None) - assert len(out.splitlines()) > 18 - - def testCmdHelp(self): - out = tests.clicomm("bugzilla query --help", None) - assert len(out.splitlines()) > 40 - - def testVersion(self): - out = tests.clicomm("bugzilla --version", None) - assert len(out.splitlines()) >= 2 - - def testPositionalArgs(self): - # Make sure cli correctly rejects ambiguous positional args - out = tests.clicomm("bugzilla login --xbadarg foo", - None, expectfail=True) - assert "unrecognized arguments: --xbadarg" in out - - out = tests.clicomm("bugzilla modify 123456 --foobar --status NEW", - None, expectfail=True) - assert "unrecognized arguments: --foobar" in out - - -class MiscAPI(unittest.TestCase): - """ - Test miscellaneous API bits - """ - def testUserAgent(self): - b3 = tests.make_bz("3.0.0") - assert "python-bugzilla" in b3.user_agent - - def test_fixurl(self): - assert (bugzilla.Bugzilla.fix_url("example.com") == - "https://example.com/xmlrpc.cgi") - assert (bugzilla.Bugzilla.fix_url("example.com/xmlrpc.cgi") == - "https://example.com/xmlrpc.cgi") - assert (bugzilla.Bugzilla.fix_url("http://example.com/somepath.cgi") == - "http://example.com/somepath.cgi") - - def testCookies(self): - cookiesbad = os.path.join(os.getcwd(), "tests/data/cookies-bad.txt") - cookieslwp = os.path.join(os.getcwd(), "tests/data/cookies-lwp.txt") - cookiesmoz = os.path.join(os.getcwd(), "tests/data/cookies-moz.txt") - - # We used to convert LWP cookies, but it shouldn't matter anymore, - # so verify they fail at least - with pytest.raises(bugzilla.BugzillaError): - tests.make_bz("3.0.0", cookiefile=cookieslwp) - - with pytest.raises(bugzilla.BugzillaError): - tests.make_bz("3.0.0", cookiefile=cookiesbad) - - # Mozilla should 'just work' - tests.make_bz("3.0.0", cookiefile=cookiesmoz) - - def test_readconfig(self): - # Testing for bugzillarc handling - bzapi = tests.make_bz("4.4.0", rhbz=True) - bzapi.url = "example.com" - temp = tempfile.NamedTemporaryFile(mode="w") - - content = """ -[example.com] -foo=1 -user=test1 -password=test2""" - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - assert bzapi.user == "test1" - assert bzapi.password == "test2" - assert bzapi.api_key is None - - bzapi.url = "foo.example.com" - bzapi.user = None - bzapi.readconfig(temp.name) - assert bzapi.user is None - - content = """ -[foo.example.com] -user=test3 -password=test4 -api_key=123abc -""" - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - assert bzapi.user == "test3" - assert bzapi.password == "test4" - assert bzapi.api_key == "123abc" - - bzapi.url = "bugzilla.redhat.com" - bzapi.user = None - bzapi.password = None - bzapi.api_key = None - bzapi.readconfig(temp.name) - assert bzapi.user is None - assert bzapi.password is None - assert bzapi.api_key is None - - - def testPostTranslation(self): - def _testPostCompare(bz, indict, outexpect): - outdict = indict.copy() - bz.post_translation({}, outdict) - assert outdict == outexpect - - # Make sure multiple calls don't change anything - bz.post_translation({}, outdict) - assert outdict == outexpect - - bug3 = tests.make_bz("3.4.0") - rhbz = tests.make_bz("4.4.0", rhbz=True) - - test1 = { - "component": ["comp1"], - "version": ["ver1", "ver2"], - - 'flags': [{ - 'is_active': 1, - 'name': 'qe_test_coverage', - 'setter': 'pm-rhel@redhat.com', - 'status': '?', - }, { - 'is_active': 1, - 'name': 'rhel-6.4.0', - 'setter': 'pm-rhel@redhat.com', - 'status': '+', - }], - - 'alias': ["FOO", "BAR"], - 'blocks': [782183, 840699, 923128], - 'keywords': ['Security'], - 'groups': ['redhat'], - } - - out_simple = test1.copy() - out_simple["components"] = out_simple["component"] - out_simple["component"] = out_simple["components"][0] - out_simple["versions"] = out_simple["version"] - out_simple["version"] = out_simple["versions"][0] - - _testPostCompare(bug3, test1, test1) - _testPostCompare(rhbz, test1, out_simple) diff --git a/tests/test_modify.py b/tests/test_modify.py deleted file mode 100644 index 1380b8f6..00000000 --- a/tests/test_modify.py +++ /dev/null @@ -1,197 +0,0 @@ -# -# Copyright Red Hat, Inc. 2013 -# -# This work is licensed under the GNU GPLv2 or later. -# See the COPYING file in the top-level directory. -# - -""" -Unit tests for building update dictionaries with 'bugzilla modify' -""" - -import unittest - -import pytest - -import tests - -rhbz = tests.make_bz("4.4.0", rhbz=True) - - -class ModifyTest(unittest.TestCase): - bz = rhbz - - def clicomm(self, argstr, out, wbout=None, tags_add=None, tags_rm=None): - comm = "bugzilla modify --__test-return-result 123456 224466 " + argstr - - (mdict, wdict, tagsa, tagsr) = tests.clicomm( - comm, self.bz, returnmain=True) - - if wbout: - assert wbout == wdict - if out: - assert out == mdict - if tags_add: - assert tags_add == tagsa - if tags_rm: - assert tags_rm == tagsr - - def testBasic(self): - self.clicomm( - "--component foocomp --product barprod --status ASSIGNED " - "--assignee foo@example.com --qa_contact bar@example.com " - "--comment 'hey some comment'", - {'assigned_to': 'foo@example.com', - 'comment': {'comment': 'hey some comment'}, - 'component': 'foocomp', - 'product': 'barprod', - 'qa_contact': 'bar@example.com', - 'status': 'ASSIGNED'} - ) - - def testPrivateComment(self): - self.clicomm( - "--comment 'hey private comment' --private", - {'comment': {'comment': 'hey private comment', 'is_private': True}} - ) - - def testClose(self): - self.clicomm( - "--close CANTFIX", - {'resolution': 'CANTFIX', 'status': 'CLOSED'} - ) - self.clicomm( - "--dupeid 111333", - {'dupe_of': 111333, 'resolution': 'DUPLICATE', 'status': 'CLOSED'} - ) - - def testFlags(self): - self.clicomm( - "--flag needinfoX --flag dev_ack+ --flag qa_ack-", - {"flags": [ - {'status': 'X', 'name': 'needinfo'}, - {'status': '+', 'name': 'dev_ack'}, - {'status': '-', 'name': 'qa_ack'} - ]} - ) - - def testWhiteboard(self): - self.clicomm( - "--whiteboard tagfoo --whiteboard=-tagbar", - {}, wbout={"whiteboard": (["tagfoo"], ["tagbar"])} - ) - self.clicomm( - "--whiteboard =foo --whiteboard =thisone", - {'whiteboard': 'thisone'} - ) - - self.clicomm( - "--qa_whiteboard =yo-qa --qa_whiteboard=-foo " - "--internal_whiteboard =internal-hey --internal_whiteboard +bar " - "--devel_whiteboard =devel-duh --devel_whiteboard=-yay " - "--tags foo1 --tags=-remove2", - {'cf_devel_whiteboard': 'devel-duh', - 'cf_internal_whiteboard': 'internal-hey', - 'cf_qa_whiteboard': 'yo-qa'}, wbout={ - "qa_whiteboard": ([], ["foo"]), - "internal_whiteboard": (["bar"], []), - "devel_whiteboard": ([], ["yay"]) - }, tags_add=["foo1"], tags_rm=["remove2"], - ) - - def testMisc(self): - self.clicomm( - "--fixed_in foo-bar-1.2.3 --reset-qa-contact --reset-assignee", - {"cf_fixed_in": "foo-bar-1.2.3", - 'reset_assigned_to': True, - 'reset_qa_contact': True} - ) - self.clicomm( - "--groups +foo --groups=-bar,baz --groups fribby", - {'groups': {'add': ['foo', 'fribby'], 'remove': ['bar', 'baz']}} - ) - self.clicomm( - "--target_milestone foomile --target_release relfoo", - {"target_milestone": "foomile", "target_release": "relfoo"}, - ) - self.clicomm( - "--priority medium --severity high", - {"priority": "medium", "severity": "high"}, - ) - self.clicomm( - "--os Windows --arch ia64 --version 1000 --url http://example.com " - "--summary 'foo summary'", - {"op_sys": "Windows", "platform": "ia64", "version": "1000", - "url": "http://example.com", "summary": 'foo summary'}, - ) - self.clicomm( - "--alias some-alias", - {"alias": "some-alias"} - ) - self.clicomm("--comment 'foo bar' --comment-tag tag1 ", - {'comment': {'comment': 'foo bar'}, 'comment_tags': ['tag1']}) - - - def testField(self): - self.clicomm( - "--field cf_fixed_in=foo-bar-1.2.4", - {"cf_fixed_in": "foo-bar-1.2.4"} - ) - - self.clicomm( - "--field cf_fixed_in=foo-bar-1.2.5 --field=cf_release_notes=blah", - {"cf_fixed_in": "foo-bar-1.2.5", - "cf_release_notes": "blah"} - ) - - - def testDepends(self): - self.clicomm( - "--dependson 100,200", - {'depends_on': {'add': [100, 200]}} - ) - self.clicomm( - "--dependson +100,200", - {'depends_on': {'add': [100, 200]}} - ) - self.clicomm( - "--dependson=-100,200", - {'depends_on': {'remove': [100, 200]}} - ) - self.clicomm( - "--dependson =100,200", - {'depends_on': {'set': [100, 200]}} - ) - - self.clicomm( - "--dependson 1 --dependson=-2 --dependson +3 --dependson =4", - {'depends_on': {'add': [1, 3], 'remove': [2], 'set': [4]}} - ) - self.clicomm( - "--blocked 5 --blocked -6 --blocked +7 --blocked =8,9", - {'blocks': {'add': [5, 7], 'remove': [6], 'set': [8, 9]}} - ) - self.clicomm( - "--keywords foo --keywords=-bar --keywords +baz --keywords =yay", - {'keywords': {'add': ["foo", "baz"], - 'remove': ["bar"], 'set': ["yay"]}} - ) - self.clicomm("--keywords =", {'keywords': {'set': []}}) - - - def testCC(self): - self.clicomm( - "--cc foo@example.com --cc=-minus@example.com " - "--cc =foo@example.com --cc +foo@example.com", - {'cc': {'add': ['foo@example.com', "=foo@example.com", - "+foo@example.com"], - 'remove': ["minus@example.com"]}}, - ) - - def testSubComponents(self): - self.clicomm("--component foo --sub-component 'bar baz'", - {"component": "foo", "sub_components": {"foo": ["bar baz"]}}) - - def testSubComponentFail(self): - with pytest.raises(ValueError): - self.bz.build_update(sub_component="some sub component") diff --git a/tests/test_query.py b/tests/test_query.py deleted file mode 100644 index 9b47e1eb..00000000 --- a/tests/test_query.py +++ /dev/null @@ -1,313 +0,0 @@ -# -# Copyright Red Hat, Inc. 2012 -# -# This work is licensed under the GNU GPLv2 or later. -# See the COPYING file in the top-level directory. -# - -""" -Unit tests for building query strings with bin/bugzilla -""" - -import copy -import os -import unittest - -import pytest - -import tests - -bz34 = tests.make_bz("3.4.0") -bz4 = tests.make_bz("4.0.0") -rhbz4 = tests.make_bz("4.4.0", rhbz=True) - - -class BZ34Test(unittest.TestCase): - """ - This is the base query class, but it's also functional on its - own. - """ - def clicomm(self, argstr, out): - comm = "bugzilla query --__test-return-result " + argstr - - if not out: - with pytest.raises(RuntimeError): - tests.clicomm(comm, self.bz) - - else: - q = tests.clicomm(comm, self.bz, returnmain=True) - assert out == q - - def testBasicQuery(self): - self.clicomm("--product foo --component foo,bar --bug_id 1234,2480", - self._basic_query_out) - - def testOneline(self): - self.clicomm("--product foo --oneline", self._oneline_out) - - def testOutputFormat(self): - self.clicomm("--product foo --outputformat " - "%{bug_id}:%{blockedby}:%{bug_status}:%{short_desc}:" - "%{status_whiteboard}:%{product}:%{rep_platform}", - self._output_format_out) - - def testBugStatusALL(self): - self.clicomm("--product foo --bug_status ALL", self._status_all_out) - - def testBugStatusDEV(self): - self.clicomm("--bug_status DEV", self._status_dev_out) - - def testBugStatusQE(self): - self.clicomm("--bug_status QE", self._status_qe_out) - - def testBugStatusEOL(self): - self.clicomm("--bug_status EOL", self._status_eol_out) - - def testBugStatusOPEN(self): - self.clicomm("--bug_status OPEN", self._status_open_out) - - def testBugStatusRegular(self): - self.clicomm("--bug_status POST", self._status_post_out) - - def testEmailOptions(self): - cmd = ("--cc foo1@example.com " - "--assigned_to foo2@example.com " - "--reporter foo3@example.com " - "--qa_contact foo7@example.com") - self.clicomm(cmd, self._email_out) - self.clicomm(cmd + " --emailtype notsubstring", self._email_type_out) - - def testComponentsFile(self): - self.clicomm("--components_file " + - os.getcwd() + "/tests/data/components_file.txt", - self._components_file_out) - - def testKeywords(self): - self.clicomm("--keywords Triaged " - "--url http://example.com --url_type foo", - self._keywords_out) - - def testBooleanChart(self): - self.clicomm("--boolean_query 'keywords-substring-Partner & " - "keywords-notsubstring-OtherQA' " - "--boolean_query 'foo-bar-baz | foo-bar-wee' " - "--boolean_query '! foo-bar-yargh'", None) - - def testLongDesc(self): - self.clicomm("--long_desc 'foobar'", self._longdesc_out) - - def testQuicksearch(self): - self.clicomm("--quicksearch 'foo bar baz'", self._quicksearch_out) - - def testSavedsearch(self): - self.clicomm("--savedsearch 'my saved search' " - "--savedsearch-sharer-id 123456", self._savedsearch_out) - - def testSubComponent(self): - self.clicomm("--component lvm2,kernel " - "--sub-component 'Command-line tools (RHEL5)'", - self._sub_component_out) - - # Test data. This is what subclasses need to fill in - bz = bz34 - - _basic_query_out = {'product': ['foo'], 'component': ['foo', 'bar'], - 'id': ["1234", "2480"]} - _oneline_out = {'product': ['foo']} - _output_format_out = {'product': ['foo']} - output_format_out = _output_format_out - - _status_all_out = {'product': ['foo']} - _status_dev_out = {'bug_status': ['NEW', 'ASSIGNED', 'NEEDINFO', - 'ON_DEV', 'MODIFIED', 'POST', 'REOPENED']} - _status_qe_out = {'bug_status': ['ASSIGNED', 'ON_QA', - 'FAILS_QA', 'PASSES_QA']} - _status_eol_out = {'bug_status': ['VERIFIED', 'RELEASE_PENDING', - 'CLOSED']} - _status_open_out = {'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', - 'ON_DEV', 'ON_QA', 'VERIFIED', 'RELEASE_PENDING', 'POST']} - _status_post_out = {'bug_status': ['POST']} - _email_out = {'assigned_to': 'foo2@example.com', - 'cc': ["foo1@example.com"], - 'reporter': "foo3@example.com", "qa_contact": "foo7@example.com"} - _email_type_out = { - 'email1': ['foo1@example.com'], 'email2': "foo2@example.com", - 'email3': 'foo3@example.com', 'email4': 'foo7@example.com', - 'emailtype1': 'notsubstring', 'emailtype2': 'notsubstring', - 'emailtype3': 'notsubstring', 'emailtype4': 'notsubstring', - 'emailcc1': True, 'emailassigned_to2': True, - 'emailreporter3': True, 'emailqa_contact4': True, - 'query_format': 'advanced'} - _components_file_out = {'component': ["foo", "bar", "baz"]} - _keywords_out = {'query_format': 'advanced', - 'field0-0-0': 'keywords', 'value0-0-0': 'Triaged', - 'field1-0-0': 'bug_file_loc', 'value1-0-0': 'http://example.com', - 'type0-0-0': 'substring', 'type1-0-0': 'foo'} - _longdesc_out = {'longdesc': 'foobar', 'longdesc_type': 'allwordssubstr', - 'query_format': 'advanced'} - _quicksearch_out = {'quicksearch': 'foo bar baz'} - _savedsearch_out = {'savedsearch': "my saved search", - 'sharer_id': "123456"} - _sub_component_out = {'component': ["lvm2", "kernel"], - 'sub_components': ["Command-line tools (RHEL5)"]} - - -class BZ4Test(BZ34Test): - bz = bz4 - - _default_includes = ['assigned_to', 'id', 'status', 'summary'] - - _basic_query_out = BZ34Test._basic_query_out.copy() - _basic_query_out["include_fields"] = _default_includes - - _oneline_out = BZ34Test._oneline_out.copy() - _oneline_out["include_fields"] = ['assigned_to', 'blocks', 'component', - 'flags', 'keywords', 'status', 'target_milestone', 'id'] - - _output_format_out = BZ34Test._output_format_out.copy() - _output_format_out["include_fields"] = ['product', 'summary', - 'platform', 'status', 'id', 'blocks', 'whiteboard'] - - _status_all_out = BZ34Test._status_all_out.copy() - _status_all_out["include_fields"] = _default_includes - - _status_dev_out = BZ34Test._status_dev_out.copy() - _status_dev_out["include_fields"] = _default_includes - - _status_qe_out = BZ34Test._status_qe_out.copy() - _status_qe_out["include_fields"] = _default_includes - - _status_eol_out = BZ34Test._status_eol_out.copy() - _status_eol_out["include_fields"] = _default_includes - - _status_open_out = BZ34Test._status_open_out.copy() - _status_open_out["include_fields"] = _default_includes - - _status_post_out = BZ34Test._status_post_out.copy() - _status_post_out["include_fields"] = _default_includes - - _email_out = BZ34Test._email_out.copy() - _email_out["include_fields"] = _default_includes - - _email_type_out = BZ34Test._email_type_out.copy() - _email_type_out["include_fields"] = _default_includes - - _components_file_out = BZ34Test._components_file_out.copy() - _components_file_out["include_fields"] = _default_includes - - _keywords_out = BZ34Test._keywords_out.copy() - _keywords_out["include_fields"] = _default_includes - - _longdesc_out = BZ34Test._longdesc_out.copy() - _longdesc_out["include_fields"] = _default_includes - - _quicksearch_out = BZ34Test._quicksearch_out.copy() - _quicksearch_out["include_fields"] = _default_includes - _savedsearch_out = BZ34Test._savedsearch_out.copy() - _savedsearch_out["include_fields"] = _default_includes - _sub_component_out = BZ34Test._sub_component_out.copy() - _sub_component_out["include_fields"] = _default_includes - - -class RHBZTest(BZ4Test): - bz = rhbz4 - - _output_format_out = BZ34Test.output_format_out.copy() - _output_format_out["include_fields"] = ['product', 'summary', - 'platform', 'status', 'id', 'blocks', 'whiteboard'] - _booleans_out = {} - - def testTranslation(self): - def translate(_in): - _out = copy.deepcopy(_in) - self.bz.pre_translation(_out) - return _out - - in_query = { - "fixed_in": "foo.bar", - "product": "some-product", - "cf_devel_whiteboard": "some_devel_whiteboard", - "include_fields": ["fixed_in", - "components", "cf_devel_whiteboard"], - } - out_query = translate(in_query) - - in_query["include_fields"] = [ - "cf_devel_whiteboard", "cf_fixed_in", "component", "id"] - assert in_query == out_query - - in_query = {"bug_id": "123,456", "component": "foo,bar"} - out_query = translate(in_query) - assert out_query["id"] == ["123", "456"] - assert out_query["component"] == ["foo", "bar"] - - in_query = {"bug_id": [123, 124], "column_list": ["id"]} - out_query = translate(in_query) - assert out_query["id"] == [123, 124] - assert out_query["include_fields"] == in_query["column_list"] - - def testInvalidBoolean(self): - with pytest.raises(RuntimeError): - self.bz.build_query(boolean_query="foobar") - - def testBooleans(self): - out = { - 'query_format': 'advanced', - 'type0-0-0': 'substring', - 'type1-0-0': 'substring', - 'type2-0-0': 'substring', - 'type3-0-0': 'substring', - 'value0-0-0': '123456', - 'value1-0-0': 'needinfo & devel_ack', - 'value2-0-0': '! baz foo', - 'value3-0-0': 'foobar | baz', - 'field0-0-0': 'blocked', - 'field1-0-0': 'flagtypes.name', - 'field2-0-0': 'cf_qa_whiteboard', - 'field3-0-0': 'cf_devel_whiteboard', - 'include_fields': ['assigned_to', 'id', 'status', 'summary'], - } - - import bugzilla - import logging - log = logging.getLogger(bugzilla.__name__) - handlers = log.handlers - try: - log.handlers = [] - self.clicomm("--blocked 123456 " - "--devel_whiteboard 'foobar | baz' " - "--qa_whiteboard '! baz foo' " - "--flag 'needinfo & devel_ack'", out) - finally: - log.handlers = handlers - - -class TestURLToQuery(BZ34Test): - def testSavedSearch(self): - url = ("https://bugzilla.redhat.com/buglist.cgi?" - "cmdtype=dorem&list_id=2342312&namedcmd=" - "RHEL7%20new%20assigned%20virt-maint&remaction=run&" - "sharer_id=321167") - query = { - 'sharer_id': '321167', - 'savedsearch': 'RHEL7 new assigned virt-maint' - } - assert bz4.url_to_query(url) == query - - def testStandardQuery(self): - url = ("https://bugzilla.redhat.com/buglist.cgi?" - "component=virt-manager&query_format=advanced&classification=" - "Fedora&product=Fedora&bug_status=NEW&bug_status=ASSIGNED&" - "bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&" - "bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=" - "RELEASE_PENDING&bug_status=POST&order=bug_status%2Cbug_id") - query = { - 'product': 'Fedora', - 'query_format': 'advanced', - 'bug_status': ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', - 'ON_QA', 'VERIFIED', 'FAILS_QA', 'RELEASE_PENDING', 'POST'], - 'classification': 'Fedora', - 'component': 'virt-manager', - 'order': 'bug_status,bug_id' - } - assert bz4.url_to_query(url) == query diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 81f259f7..6e45e93e 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -17,17 +17,12 @@ import sys import unittest -# pylint: disable=import-error -if sys.version_info[0] >= 3: - from io import StringIO -else: - from StringIO import StringIO -# pylint: enable=import-error - import pytest import bugzilla + import tests +import tests.utils RHURL = tests.CLICONFIG.REDHAT_URL or "partner-bugzilla.redhat.com" @@ -684,7 +679,8 @@ def fakegetpass(prompt): # bare 'login' - stdinstr = StringIO("foobar@example.com\n\rfoobar\n\r") + stdinstr = tests.utils.fake_stream( + "foobar@example.com\n\rfoobar\n\r") ret = tests.clicomm("%s login" % cmd, None, expectfail=True, stdin=stdinstr) assert "Bugzilla Username:" in ret diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..a3a7a1ce --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,90 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import difflib +import inspect +import io +import os +import pprint +import shlex +import sys + +import bugzilla._cli +from bugzilla._compatimports import IS_PY3 + +import tests + + +def get_funcname(): + # Return calling function name + return inspect.stack()[1][3] + + +def tests_path(filename): + testdir = os.path.dirname(__file__) + if testdir not in filename: + return os.path.join(testdir, filename) + return filename + + +def fake_stream(text): + if IS_PY3: + return io.StringIO(text) + else: + return io.BytesIO(text) + + +def diff_compare(inputdata, filename): + """Compare passed string output to contents of filename""" + filename = tests_path(filename) + + actual_out = inputdata + if isinstance(inputdata, dict): + actual_out = pprint.pformat(inputdata) + + if not os.path.exists(filename) or tests.CLICONFIG.REGENERATE_OUTPUT: + open(filename, "w").write(actual_out) + expect_out = open(filename).read() + + diff = "".join(difflib.unified_diff(expect_out.splitlines(1), + actual_out.splitlines(1), + fromfile=filename or '', + tofile="Generated Output")) + if diff: + raise AssertionError("Conversion outputs did not match.\n%s" % diff) + + +def do_run_cli(capsys, monkeypatch, + argvstr, bzinstance, + expectfail=False, stdin=None): + """ + Run bin/bugzilla.main() directly with passed argv + """ + argv = shlex.split(argvstr) + monkeypatch.setattr(sys, "argv", argv) + if stdin: + monkeypatch.setattr(sys, "stdin", fake_stream(stdin)) + else: + monkeypatch.setattr(sys.stdin, "isatty", lambda: True) + + ret = 0 + try: + # pylint: disable=protected-access + if bzinstance is None: + bugzilla._cli.cli() + else: + bugzilla._cli.main(unittest_bz_instance=bzinstance) + except SystemExit as sys_e: + ret = sys_e.code + + out, err = capsys.readouterr() + outstr = out + err + + if ret != 0 and not expectfail: + raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % + (ret, argvstr, outstr)) + if ret == 0 and expectfail: + raise RuntimeError("Command succeeded but we expected success\n" + "ret=%d\ncmd=%s\nout=%s" % + (ret, argvstr, outstr)) + return outstr From 7afe97dd92fb5f77352b8a1ba7b7b7cbec764697 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 18:01:44 -0500 Subject: [PATCH 161/393] tests: rw: convert to more pytest infrastructure Signed-off-by: Cole Robinson --- tests/test_api_misc.py | 10 +- tests/test_rw_functional.py | 1793 ++++++++++++++++++----------------- tests/utils.py | 9 + 3 files changed, 908 insertions(+), 904 deletions(-) diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 76048fb1..3c5a1151 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -12,6 +12,7 @@ from __future__ import print_function import os +import sys import tempfile import pytest @@ -263,14 +264,7 @@ def test_interactive_login(capsys, monkeypatch): user_get_args=None, user_get_return={}) - import sys - import getpass - - if sys.version_info[0] >= 3: - monkeypatch.setattr(getpass, "getpass", input) - else: - monkeypatch.setattr(getpass, "getpass", - raw_input) # pylint: disable=undefined-variable + tests.utils.monkeypatch_getpass(monkeypatch) fakestdin = tests.utils.fake_stream("fakeuser\nfakepass\n") monkeypatch.setattr(sys, "stdin", fakestdin) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 6e45e93e..779e85a5 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -12,16 +12,17 @@ from __future__ import print_function import datetime +import inspect import os import random import sys -import unittest import pytest import bugzilla import tests +import tests.mockbackend import tests.utils @@ -37,919 +38,919 @@ def _split_int(s): sys.exit(1) -class RHPartnerTest(unittest.TestCase): - def _check_have_admin(self, bz, funcname): - # groupnames is empty for any user if our logged in user does not - # have admin privs. - # Check a known account that likely won't ever go away - ret = bool(bz.getuser("anaconda-maint-list@redhat.com").groupnames) - if not ret: - print("\nNo admin privs, reduced testing of %s" % funcname) - return ret - - def _open_bz(self, **kwargs): - return bugzilla.RHBugzilla(url=RHURL, **kwargs) - - def test0LoggedInNoCreds(self): - bz = self._open_bz(use_creds=False) - assert not bz.logged_in - - def test2(self): - bz = bugzilla.Bugzilla(RHURL, use_creds=False) - assert bz.__class__ is bugzilla.RHBugzilla - - def _makebug(self, bz): - component = "python-bugzilla" - version = "rawhide" - summary = ("python-bugzilla test basic bug %s" % - datetime.datetime.today()) - newout = tests.clicomm("bugzilla new " - "--product Fedora --component %s --version %s " - "--summary \"%s\" " - "--comment \"Test bug from the python-bugzilla test suite\" " - "--outputformat \"%%{bug_id}\"" % - (component, version, summary), bz) - - assert len(newout.splitlines()) == 3 - bugid = int(newout.splitlines()[2]) - bug = bz.getbug(bugid) - print("\nCreated bugid: %s" % bug.id) - - assert bug.component == component - assert bug.version == version - assert bug.summary == summary - - return bug - - def test03NewBugBasic(self): +def _check_have_admin(bz): + funcname = inspect.stack()[1][3] + + # groupnames is empty for any user if our logged in user does not + # have admin privs. + # Check a known account that likely won't ever go away + ret = bool(bz.getuser("anaconda-maint-list@redhat.com").groupnames) + if not ret: + print("\nNo admin privs, reduced testing of %s" % funcname) + return ret + + +def _open_bz(**kwargs): + return bugzilla.RHBugzilla(url=RHURL, **kwargs) + + +def test0LoggedInNoCreds(): + bz = _open_bz(use_creds=False) + assert not bz.logged_in + + +def test2(): + bz = bugzilla.Bugzilla(RHURL, use_creds=False) + assert bz.__class__ is bugzilla.RHBugzilla + + +def _makebug(run_cli): + component = "python-bugzilla" + version = "rawhide" + summary = ("python-bugzilla test basic bug %s" % + datetime.datetime.today()) + newout = run_cli("bugzilla new " + "--product Fedora --component %s --version %s " + "--summary \"%s\" " + "--comment \"Test bug from the python-bugzilla test suite\" " + "--outputformat \"%%{bug_id}\"" % + (component, version, summary), bz) + + assert len(newout.splitlines()) == 1 + bugid = int(newout.splitlines()[0]) + bug = bz.getbug(bugid) + print("\nCreated bugid: %s" % bug.id) + + assert bug.component == component + assert bug.version == version + assert bug.summary == summary + + return bug + + +def test03NewBugBasic(run_cli): + """ + Create a bug with minimal amount of fields, then close it + """ + bz = _open_bz() + bug = _makebug(bz) + + # Verify hasattr works + assert hasattr(bug, "id") + assert hasattr(bug, "bug_id") + + # Close the bug + run_cli("bugzilla modify --close NOTABUG %s" % bug.id, bz) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == "NOTABUG" + + +def test04NewBugAllFields(run_cli): + """ + Create a bug using all 'new' fields, check some values, close it + """ + bz = _open_bz() + + summary = ("python-bugzilla test manyfields bug %s" % + datetime.datetime.today()) + url = "http://example.com" + osval = "Windows" + cc = "triage@lists.fedoraproject.org" + blocked = "461686,461687" + dependson = "427301" + comment = "Test bug from python-bugzilla test suite" + sub_component = "Command-line tools (RHEL6)" + alias = "pybz-%s" % datetime.datetime.today().strftime("%s") + newout = run_cli("bugzilla new " + "--product 'Red Hat Enterprise Linux 6' --version 6.0 " + "--component lvm2 --sub-component '%s' " + "--summary \"%s\" " + "--comment \"%s\" " + "--url %s --severity Urgent --priority Low --os %s " + "--arch ppc --cc %s --blocked %s --dependson %s " + "--alias %s " + "--outputformat \"%%{bug_id}\"" % + (sub_component, summary, comment, url, + osval, cc, blocked, dependson, alias), bz) + + assert len(newout.splitlines()) == 1 + + bugid = int(newout.splitlines()[0]) + bug = bz.getbug(bugid, extra_fields=["sub_components"]) + print("\nCreated bugid: %s" % bugid) + + assert bug.summary == summary + assert bug.bug_file_loc == url + assert bug.op_sys == osval + assert bug.blocks == _split_int(blocked) + assert bug.depends_on == _split_int(dependson) + assert all([e in bug.cc for e in cc.split(",")]) + assert bug.longdescs[0]["text"] == comment + assert bug.sub_components == {"lvm2": [sub_component]} + assert bug.alias == [alias] + + # Close the bug + + # RHBZ makes it difficult to provide consistent semantics for + # 'alias' update: + # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 + # alias += "-closed" + run_cli("bugzilla modify " + "--close WONTFIX %s " % + bugid, bz) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == "WONTFIX" + assert bug.alias == [alias] + + # Check bug's minimal history + ret = bug.get_history_raw() + assert len(ret["bugs"]) == 1 + assert len(ret["bugs"][0]["history"]) == 1 + + +def test05ModifyStatus(run_cli): + """ + Modify status and comment fields for an existing bug + """ + bz = _open_bz() + bugid = "663674" + cmd = "bugzilla modify %s " % bugid + + bug = bz.getbug(bugid) + + # We want to start with an open bug, so fix things + if bug.status == "CLOSED": + run_cli(cmd + "--status ASSIGNED", bz) + bug.refresh() + assert bug.status == "ASSIGNED" + + origstatus = bug.status + + # Set to ON_QA with a private comment + status = "ON_QA" + comment = ("changing status to %s at %s" % + (status, datetime.datetime.today())) + run_cli(cmd + + "--status %s --comment \"%s\" --private" % (status, comment), bz) + + bug.refresh() + assert bug.status == status + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment + + # Close bug as DEFERRED with a private comment + resolution = "DEFERRED" + comment = ("changing status to CLOSED=%s at %s" % + (resolution, datetime.datetime.today())) + run_cli(cmd + + "--close %s --comment \"%s\" --private" % + (resolution, comment), bz) + + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == resolution + assert bug.comments[-1]["is_private"] == 1 + assert bug.comments[-1]["text"] == comment + + # Close bug as dup with no comment + dupeid = "461686" + desclen = len(bug.longdescs) + run_cli(cmd + + "--close DUPLICATE --dupeid %s" % dupeid, bz) + + bug.refresh() + assert bug.dupe_of == int(dupeid) + assert len(bug.longdescs) == (desclen + 1) + assert "marked as a duplicate" in bug.longdescs[-1]["text"] + + # bz.setstatus test + comment = ("adding lone comment at %s" % datetime.datetime.today()) + bug.setstatus("POST", comment=comment, private=True) + bug.refresh() + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment + assert bug.status == "POST" + + # bz.close test + fixed_in = str(datetime.datetime.today()) + bug.close("ERRATA", fixedin=fixed_in) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == "ERRATA" + assert bug.fixed_in == fixed_in + + # bz.addcomment test + comment = ("yet another test comment %s" % datetime.datetime.today()) + bug.addcomment(comment, private=False) + bug.refresh() + assert bug.longdescs[-1]["text"] == comment + assert bug.longdescs[-1]["is_private"] == 0 + + # Confirm comments is same as getcomments + assert bug.comments == bug.getcomments() + + # Reset state + run_cli(cmd + "--status %s" % origstatus, bz) + bug.refresh() + assert bug.status == origstatus + + +def test06ModifyEmails(run_cli): + """ + Modify cc, assignee, qa_contact for existing bug + """ + bz = _open_bz() + bugid = "663674" + cmd = "bugzilla modify %s " % bugid + + bug = bz.getbug(bugid) + + origcc = bug.cc + + # Test CC list and reset it + email1 = "triage@lists.fedoraproject.org" + email2 = "crobinso@redhat.com" + bug.deletecc(origcc) + run_cli(cmd + "--cc %s --cc %s" % (email1, email2), bz) + bug.addcc(email1) + + bug.refresh() + assert email1 in bug.cc + assert email2 in bug.cc + assert len(bug.cc) == 2 + + run_cli(cmd + "--cc=-%s" % email1, bz) + bug.refresh() + assert email1 not in bug.cc + + # Test assigned target + run_cli(cmd + "--assignee %s" % email1, bz) + bug.refresh() + assert bug.assigned_to == email1 + + # Test QA target + run_cli(cmd + "--qa_contact %s" % email1, bz) + bug.refresh() + assert bug.qa_contact == email1 + + # Reset values + bug.deletecc(bug.cc) + run_cli(cmd + "--reset-qa-contact --reset-assignee", bz) + + bug.refresh() + assert bug.cc == [] + assert bug.assigned_to == "crobinso@redhat.com" + assert bug.qa_contact == "extras-qa@fedoraproject.org" + + +def test07ModifyMultiFlags(run_cli): + """ + Modify flags and fixed_in for 2 bugs + """ + bz = _open_bz() + bugid1 = "461686" + bugid2 = "461687" + cmd = "bugzilla modify %s %s " % (bugid1, bugid2) + + def flagstr(b): + ret = [] + for flag in b.flags: + ret.append(flag["name"] + flag["status"]) + return " ".join(sorted(ret)) + + def cleardict_old(b): """ - Create a bug with minimal amount of fields, then close it + Clear flag dictionary, for format meant for bug.updateflags """ - bz = self._open_bz() - bug = self._makebug(bz) + clearflags = {} + for flag in b.flags: + clearflags[flag["name"]] = "X" + return clearflags - # Verify hasattr works - assert hasattr(bug, "id") - assert hasattr(bug, "bug_id") - - # Close the bug - tests.clicomm("bugzilla modify --close NOTABUG %s" % bug.id, bz) - bug.refresh() - assert bug.status == "CLOSED" - assert bug.resolution == "NOTABUG" - - - def test04NewBugAllFields(self): - """ - Create a bug using all 'new' fields, check some values, close it - """ - bz = self._open_bz() - - summary = ("python-bugzilla test manyfields bug %s" % - datetime.datetime.today()) - url = "http://example.com" - osval = "Windows" - cc = "triage@lists.fedoraproject.org" - blocked = "461686,461687" - dependson = "427301" - comment = "Test bug from python-bugzilla test suite" - sub_component = "Command-line tools (RHEL6)" - alias = "pybz-%s" % datetime.datetime.today().strftime("%s") - newout = tests.clicomm("bugzilla new " - "--product 'Red Hat Enterprise Linux 6' --version 6.0 " - "--component lvm2 --sub-component '%s' " - "--summary \"%s\" " - "--comment \"%s\" " - "--url %s --severity Urgent --priority Low --os %s " - "--arch ppc --cc %s --blocked %s --dependson %s " - "--alias %s " - "--outputformat \"%%{bug_id}\"" % - (sub_component, summary, comment, url, - osval, cc, blocked, dependson, alias), bz) - - assert len(newout.splitlines()) == 3 - - bugid = int(newout.splitlines()[2]) - bug = bz.getbug(bugid, extra_fields=["sub_components"]) - print("\nCreated bugid: %s" % bugid) - - assert bug.summary == summary - assert bug.bug_file_loc == url - assert bug.op_sys == osval - assert bug.blocks == _split_int(blocked) - assert bug.depends_on == _split_int(dependson) - assert all([e in bug.cc for e in cc.split(",")]) - assert bug.longdescs[0]["text"] == comment - assert bug.sub_components == {"lvm2": [sub_component]} - assert bug.alias == [alias] - - # Close the bug - - # RHBZ makes it difficult to provide consistent semantics for - # 'alias' update: - # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 - # alias += "-closed" - tests.clicomm("bugzilla modify " - "--close WONTFIX %s " % - bugid, bz) - bug.refresh() - assert bug.status == "CLOSED" - assert bug.resolution == "WONTFIX" - assert bug.alias == [alias] - - # Check bug's minimal history - ret = bug.get_history_raw() - assert len(ret["bugs"]) == 1 - assert len(ret["bugs"][0]["history"]) == 1 - - - def test05ModifyStatus(self): + def cleardict_new(b): """ - Modify status and comment fields for an existing bug + Clear flag dictionary, for format meant for update_bugs """ - bz = self._open_bz() - bugid = "663674" - cmd = "bugzilla modify %s " % bugid - - bug = bz.getbug(bugid) - - # We want to start with an open bug, so fix things - if bug.status == "CLOSED": - tests.clicomm(cmd + "--status ASSIGNED", bz) - bug.refresh() - assert bug.status == "ASSIGNED" - - origstatus = bug.status - - # Set to ON_QA with a private comment - status = "ON_QA" - comment = ("changing status to %s at %s" % - (status, datetime.datetime.today())) - tests.clicomm(cmd + - "--status %s --comment \"%s\" --private" % (status, comment), bz) - - bug.refresh() - assert bug.status == status - assert bug.longdescs[-1]["is_private"] == 1 - assert bug.longdescs[-1]["text"] == comment - - # Close bug as DEFERRED with a private comment - resolution = "DEFERRED" - comment = ("changing status to CLOSED=%s at %s" % - (resolution, datetime.datetime.today())) - tests.clicomm(cmd + - "--close %s --comment \"%s\" --private" % - (resolution, comment), bz) - - bug.refresh() - assert bug.status == "CLOSED" - assert bug.resolution == resolution - assert bug.comments[-1]["is_private"] == 1 - assert bug.comments[-1]["text"] == comment - - # Close bug as dup with no comment - dupeid = "461686" - desclen = len(bug.longdescs) - tests.clicomm(cmd + - "--close DUPLICATE --dupeid %s" % dupeid, bz) - - bug.refresh() - assert bug.dupe_of == int(dupeid) - assert len(bug.longdescs) == (desclen + 1) - assert "marked as a duplicate" in bug.longdescs[-1]["text"] - - # bz.setstatus test - comment = ("adding lone comment at %s" % datetime.datetime.today()) - bug.setstatus("POST", comment=comment, private=True) - bug.refresh() - assert bug.longdescs[-1]["is_private"] == 1 - assert bug.longdescs[-1]["text"] == comment - assert bug.status == "POST" - - # bz.close test - fixed_in = str(datetime.datetime.today()) - bug.close("ERRATA", fixedin=fixed_in) - bug.refresh() - assert bug.status == "CLOSED" - assert bug.resolution == "ERRATA" - assert bug.fixed_in == fixed_in - - # bz.addcomment test - comment = ("yet another test comment %s" % datetime.datetime.today()) - bug.addcomment(comment, private=False) - bug.refresh() - assert bug.longdescs[-1]["text"] == comment - assert bug.longdescs[-1]["is_private"] == 0 - - # Confirm comments is same as getcomments - assert bug.comments == bug.getcomments() - - # Reset state - tests.clicomm(cmd + "--status %s" % origstatus, bz) - bug.refresh() - assert bug.status == origstatus - - - def test06ModifyEmails(self): - """ - Modify cc, assignee, qa_contact for existing bug - """ - bz = self._open_bz() - bugid = "663674" - cmd = "bugzilla modify %s " % bugid - - bug = bz.getbug(bugid) - - origcc = bug.cc - - # Test CC list and reset it - email1 = "triage@lists.fedoraproject.org" - email2 = "crobinso@redhat.com" - bug.deletecc(origcc) - tests.clicomm(cmd + "--cc %s --cc %s" % (email1, email2), bz) - bug.addcc(email1) - - bug.refresh() - assert email1 in bug.cc - assert email2 in bug.cc - assert len(bug.cc) == 2 - - tests.clicomm(cmd + "--cc=-%s" % email1, bz) - bug.refresh() - assert email1 not in bug.cc - - # Test assigned target - tests.clicomm(cmd + "--assignee %s" % email1, bz) - bug.refresh() - assert bug.assigned_to == email1 - - # Test QA target - tests.clicomm(cmd + "--qa_contact %s" % email1, bz) - bug.refresh() - assert bug.qa_contact == email1 - - # Reset values - bug.deletecc(bug.cc) - tests.clicomm(cmd + "--reset-qa-contact --reset-assignee", bz) - - bug.refresh() - assert bug.cc == [] - assert bug.assigned_to == "crobinso@redhat.com" - assert bug.qa_contact == "extras-qa@fedoraproject.org" - - - def test07ModifyMultiFlags(self): - """ - Modify flags and fixed_in for 2 bugs - """ - bz = self._open_bz() - bugid1 = "461686" - bugid2 = "461687" - cmd = "bugzilla modify %s %s " % (bugid1, bugid2) - - def flagstr(b): - ret = [] - for flag in b.flags: - ret.append(flag["name"] + flag["status"]) - return " ".join(sorted(ret)) - - def cleardict_old(b): - """ - Clear flag dictionary, for format meant for bug.updateflags - """ - clearflags = {} - for flag in b.flags: - clearflags[flag["name"]] = "X" - return clearflags - - def cleardict_new(b): - """ - Clear flag dictionary, for format meant for update_bugs - """ - clearflags = [] - for flag in b.flags: - clearflags.append({"name": flag["name"], "status": "X"}) - return clearflags - - bug1 = bz.getbug(bugid1) - if cleardict_old(bug1): - bug1.updateflags(cleardict_old(bug1)) - bug2 = bz.getbug(bugid2) - if cleardict_old(bug2): - bug2.updateflags(cleardict_old(bug2)) - - - # Set flags and confirm - setflags = "needinfo? requires_doc_text-" - tests.clicomm(cmd + - " ".join([(" --flag " + f) for f in setflags.split()]), bz) - - bug1.refresh() - bug2.refresh() - - assert flagstr(bug1) == setflags - assert flagstr(bug2) == setflags - assert bug1.get_flags("needinfo")[0]["status"] == "?" - assert bug1.get_flag_status("requires_doc_text") == "-" - - # Clear flags - if cleardict_new(bug1): - bz.update_flags(bug1.id, cleardict_new(bug1)) - bug1.refresh() - if cleardict_new(bug2): - bz.update_flags(bug2.id, cleardict_new(bug2)) - bug2.refresh() - - assert cleardict_old(bug1) == {} - assert cleardict_old(bug2) == {} - - # Set "Fixed In" field - origfix1 = bug1.fixed_in - origfix2 = bug2.fixed_in - - newfix = origfix1 and (origfix1 + "-new1") or "blippy1" - if newfix == origfix2: - newfix = origfix2 + "-2" - - tests.clicomm(cmd + "--fixed_in=%s" % newfix, bz) - - bug1.refresh() - bug2.refresh() - assert bug1.fixed_in == newfix - assert bug2.fixed_in == newfix - - # Reset fixed_in - tests.clicomm(cmd + "--fixed_in=\"-\"", bz) - - bug1.refresh() - bug2.refresh() - assert bug1.fixed_in == "-" - assert bug2.fixed_in == "-" - - - def test07ModifyMisc(self): - bugid = "461686" - cmd = "bugzilla modify %s " % bugid - bz = self._open_bz() - bug = bz.getbug(bugid) - - # modify --dependson - tests.clicomm(cmd + "--dependson 123456", bz) - bug.refresh() - assert 123456 in bug.depends_on - tests.clicomm(cmd + "--dependson =111222", bz) - bug.refresh() - assert [111222] == bug.depends_on - tests.clicomm(cmd + "--dependson=-111222", bz) - bug.refresh() - assert [] == bug.depends_on - - # modify --blocked - tests.clicomm(cmd + "--blocked 123,456", bz) - bug.refresh() - assert [123, 456] == bug.blocks - tests.clicomm(cmd + "--blocked =", bz) - bug.refresh() - assert [] == bug.blocks - - # modify --keywords - tests.clicomm(cmd + "--keywords +Documentation --keywords EasyFix", bz) - bug.refresh() - assert ["Documentation", "EasyFix"] == bug.keywords - tests.clicomm(cmd + "--keywords=-EasyFix --keywords=-Documentation", - bz) - bug.refresh() - assert [] == bug.keywords - - # modify --target_release - # modify --target_milestone - targetbugid = 492463 - targetbug = bz.getbug(targetbugid) - targetcmd = "bugzilla modify %s " % targetbugid - tests.clicomm(targetcmd + - "--target_milestone beta --target_release 6.2", bz) - targetbug.refresh() - assert targetbug.target_milestone == "beta" - assert targetbug.target_release == ["6.2"] - tests.clicomm(targetcmd + - "--target_milestone rc --target_release 6.10", bz) - targetbug.refresh() - assert targetbug.target_milestone == "rc" - assert targetbug.target_release == ["6.10"] - - # modify --priority - # modify --severity - tests.clicomm(cmd + "--priority low --severity high", bz) - bug.refresh() - assert bug.priority == "low" - assert bug.severity == "high" - tests.clicomm(cmd + "--priority medium --severity medium", bz) - bug.refresh() - assert bug.priority == "medium" - assert bug.severity == "medium" - - # modify --os - # modify --platform - # modify --version - tests.clicomm(cmd + "--version rawhide --os Windows --arch ppc " - "--url http://example.com", bz) - bug.refresh() - assert bug.version == "rawhide" - assert bug.op_sys == "Windows" - assert bug.platform == "ppc" - assert bug.url == "http://example.com" - tests.clicomm(cmd + "--version rawhide --os Linux --arch s390 " - "--url http://example.com/fribby", bz) - bug.refresh() - assert bug.version == "rawhide" - assert bug.op_sys == "Linux" - assert bug.platform == "s390" - assert bug.url == "http://example.com/fribby" - - # modify --field - tests.clicomm(cmd + "--field cf_fixed_in=foo-bar-1.2.3 \ - --field=cf_release_notes=baz", bz) - - bug.refresh() - assert bug.fixed_in == "foo-bar-1.2.3" - assert bug.cf_release_notes == "baz" - - - def test08Attachments(self): - tmpdir = "__test_attach_output" - if tmpdir in os.listdir("."): - os.system("rm -r %s" % tmpdir) - os.mkdir(tmpdir) - os.chdir(tmpdir) - - try: - self._test8Attachments() - finally: - os.chdir("..") - os.system("rm -r %s" % tmpdir) - - def _test8Attachments(self): - """ - Get and set attachments for a bug - """ - bz = self._open_bz() - cmd = "bugzilla attach " - testfile = "../tests/data/bz-attach-get1.txt" - - # Add attachment as CLI option - setbug = self._makebug(bz) - setbug = bz.getbug(setbug.id, extra_fields=["attachments"]) - orignumattach = len(setbug.attachments) - - # Add attachment from CLI with mime guessing - desc1 = "python-bugzilla cli upload %s" % datetime.datetime.today() - out1 = tests.clicomm(cmd + "%s --description \"%s\" --file %s" % - (setbug.id, desc1, testfile), bz, - stdin=open("/dev/tty", "rb")) - - desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() - out2 = tests.clicomm(cmd + "%s --file test --summary \"%s\"" % - (setbug.id, desc2), bz, stdin=open(testfile)) - - # Expected output format: - # Created attachment on bug - - setbug.refresh() - assert len(setbug.attachments) == (orignumattach + 2) - - att1 = setbug.attachments[-2] - attachid = att1["id"] - assert att1["summary"] == desc1 - assert att1["id"] == int(out1.splitlines()[2].split()[2]) - assert att1["content_type"] == "text/plain" - - att2 = setbug.attachments[-1] - assert att2["summary"] == desc2 - assert att2["id"] == int(out2.splitlines()[2].split()[2]) - assert att2["content_type"] == "application/octet-stream" - - # Set attachment flags - assert att1["flags"] == [] - bz.updateattachmentflags(setbug.id, att2["id"], "review", status="+") - setbug.refresh() - - assert len(setbug.attachments[-1]["flags"]) == 1 - assert setbug.attachments[-1]["flags"][0]["name"] == "review" - assert setbug.attachments[-1]["flags"][0]["status"] == "+" - - bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], - "review", status="X") - setbug.refresh() - assert setbug.attachments[-1]["flags"] == [] - - # Set attachment obsolete - bz._backend.bug_attachment_update({ # pylint: disable=protected-access - "ids": [setbug.attachments[-1]["id"]], - "is_obsolete": 1}) - setbug.refresh() - assert setbug.attachments[-1]["is_obsolete"] == 1 - - - # Get attachment, verify content - out = tests.clicomm(cmd + "--get %s" % attachid, bz).splitlines() - - # Expect format: - # Wrote - fname = out[2].split()[1].strip() - - assert len(out) == 3 - assert fname == "bz-attach-get1.txt" - assert open(fname).read() == open(testfile).read() - os.unlink(fname) - - # Get all attachments - getbug = bz.getbug(setbug.id) - getbug.autorefresh = True - numattach = len(getbug.attachments) - out = tests.clicomm(cmd + "--getall %s" % getbug.id, bz).splitlines() - - assert len(out) == (numattach + 2) - fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] - assert len(fnames) == numattach - for f in fnames: - if not os.path.exists(f): - raise AssertionError("filename '%s' not found" % f) - os.unlink(f) - - # Get all attachments, but ignore obsolete - ignorecmd = cmd + "--getall %s --ignore-obsolete" % getbug.id - out = tests.clicomm(ignorecmd, bz).splitlines() - - assert len(out) == (numattach + 1) - fnames = [l.split(" ", 1)[1].strip() for l in out[2:]] - assert len(fnames) == (numattach - 1) - for f in fnames: - if not os.path.exists(f): - raise AssertionError("filename '%s' not found" % f) - os.unlink(f) - - - def test09Whiteboards(self): - bz = self._open_bz() - bug_id = "663674" - cmd = "bugzilla modify %s " % bug_id - bug = bz.getbug(bug_id) - - # Set all whiteboards - initval = str(random.randint(1, 1024)) - tests.clicomm(cmd + - "--whiteboard =%sstatus " - "--devel_whiteboard =%sdevel " - "--internal_whiteboard '=%sinternal, security, foo security1' " - "--qa_whiteboard =%sqa " % - (initval, initval, initval, initval), bz) - - bug.refresh() - assert bug.whiteboard == (initval + "status") - assert bug.qa_whiteboard == (initval + "qa") - assert bug.devel_whiteboard == (initval + "devel") - assert (bug.internal_whiteboard == - (initval + "internal, security, foo security1")) - - # Modify whiteboards - tests.clicomm(cmd + - "--whiteboard =foobar " - "--qa_whiteboard _app " - "--devel_whiteboard =pre-%s" % bug.devel_whiteboard, bz) - - bug.refresh() - assert bug.qa_whiteboard == (initval + "qa" + " _app") - assert bug.devel_whiteboard == ("pre-" + initval + "devel") - assert bug.status_whiteboard == "foobar" - - # Verify that tag manipulation is smart about separator - tests.clicomm(cmd + - "--qa_whiteboard=-_app " - "--internal_whiteboard=-security,", bz) - bug.refresh() + clearflags = [] + for flag in b.flags: + clearflags.append({"name": flag["name"], "status": "X"}) + return clearflags + + bug1 = bz.getbug(bugid1) + if cleardict_old(bug1): + bug1.updateflags(cleardict_old(bug1)) + bug2 = bz.getbug(bugid2) + if cleardict_old(bug2): + bug2.updateflags(cleardict_old(bug2)) + + + # Set flags and confirm + setflags = "needinfo? requires_doc_text-" + run_cli(cmd + + " ".join([(" --flag " + f) for f in setflags.split()]), bz) + + bug1.refresh() + bug2.refresh() + + assert flagstr(bug1) == setflags + assert flagstr(bug2) == setflags + assert bug1.get_flags("needinfo")[0]["status"] == "?" + assert bug1.get_flag_status("requires_doc_text") == "-" + + # Clear flags + if cleardict_new(bug1): + bz.update_flags(bug1.id, cleardict_new(bug1)) + bug1.refresh() + if cleardict_new(bug2): + bz.update_flags(bug2.id, cleardict_new(bug2)) + bug2.refresh() + + assert cleardict_old(bug1) == {} + assert cleardict_old(bug2) == {} + + # Set "Fixed In" field + origfix1 = bug1.fixed_in + origfix2 = bug2.fixed_in + + newfix = origfix1 and (origfix1 + "-new1") or "blippy1" + if newfix == origfix2: + newfix = origfix2 + "-2" + + run_cli(cmd + "--fixed_in=%s" % newfix, bz) + + bug1.refresh() + bug2.refresh() + assert bug1.fixed_in == newfix + assert bug2.fixed_in == newfix + + # Reset fixed_in + run_cli(cmd + "--fixed_in=\"-\"", bz) + + bug1.refresh() + bug2.refresh() + assert bug1.fixed_in == "-" + assert bug2.fixed_in == "-" + + +def test07ModifyMisc(run_cli): + bugid = "461686" + cmd = "bugzilla modify %s " % bugid + bz = _open_bz() + bug = bz.getbug(bugid) + + # modify --dependson + run_cli(cmd + "--dependson 123456", bz) + bug.refresh() + assert 123456 in bug.depends_on + run_cli(cmd + "--dependson =111222", bz) + bug.refresh() + assert [111222] == bug.depends_on + run_cli(cmd + "--dependson=-111222", bz) + bug.refresh() + assert [] == bug.depends_on + + # modify --blocked + run_cli(cmd + "--blocked 123,456", bz) + bug.refresh() + assert [123, 456] == bug.blocks + run_cli(cmd + "--blocked =", bz) + bug.refresh() + assert [] == bug.blocks + + # modify --keywords + run_cli(cmd + "--keywords +Documentation --keywords EasyFix", bz) + bug.refresh() + assert ["Documentation", "EasyFix"] == bug.keywords + run_cli(cmd + "--keywords=-EasyFix --keywords=-Documentation", + bz) + bug.refresh() + assert [] == bug.keywords + + # modify --target_release + # modify --target_milestone + targetbugid = 492463 + targetbug = bz.getbug(targetbugid) + targetcmd = "bugzilla modify %s " % targetbugid + run_cli(targetcmd + + "--target_milestone beta --target_release 6.2", bz) + targetbug.refresh() + assert targetbug.target_milestone == "beta" + assert targetbug.target_release == ["6.2"] + run_cli(targetcmd + + "--target_milestone rc --target_release 6.10", bz) + targetbug.refresh() + assert targetbug.target_milestone == "rc" + assert targetbug.target_release == ["6.10"] + + # modify --priority + # modify --severity + run_cli(cmd + "--priority low --severity high", bz) + bug.refresh() + assert bug.priority == "low" + assert bug.severity == "high" + run_cli(cmd + "--priority medium --severity medium", bz) + bug.refresh() + assert bug.priority == "medium" + assert bug.severity == "medium" + + # modify --os + # modify --platform + # modify --version + run_cli(cmd + "--version rawhide --os Windows --arch ppc " + "--url http://example.com", bz) + bug.refresh() + assert bug.version == "rawhide" + assert bug.op_sys == "Windows" + assert bug.platform == "ppc" + assert bug.url == "http://example.com" + run_cli(cmd + "--version rawhide --os Linux --arch s390 " + "--url http://example.com/fribby", bz) + bug.refresh() + assert bug.version == "rawhide" + assert bug.op_sys == "Linux" + assert bug.platform == "s390" + assert bug.url == "http://example.com/fribby" + + # modify --field + run_cli(cmd + "--field cf_fixed_in=foo-bar-1.2.3 \ + --field=cf_release_notes=baz", bz) + + bug.refresh() + assert bug.fixed_in == "foo-bar-1.2.3" + assert bug.cf_release_notes == "baz" + + +def test08Attachments(run_cli): + tmpdir = "__test_attach_output" + if tmpdir in os.listdir("."): + os.system("rm -r %s" % tmpdir) + os.mkdir(tmpdir) + os.chdir(tmpdir) + + try: + _test8Attachments(run_cli) + finally: + os.chdir("..") + os.system("rm -r %s" % tmpdir) + + +def _test8Attachments(run_cli): + """ + Get and set attachments for a bug + """ + bz = _open_bz() + cmd = "bugzilla attach " + testfile = "../tests/data/bz-attach-get1.txt" + + # Add attachment as CLI option + setbug = _makebug(bz) + setbug = bz.getbug(setbug.id, extra_fields=["attachments"]) + orignumattach = len(setbug.attachments) + + # Add attachment from CLI with mime guessing + desc1 = "python-bugzilla cli upload %s" % datetime.datetime.today() + out1 = run_cli(cmd + "%s --description \"%s\" --file %s" % + (setbug.id, desc1, testfile), bz) + out1 = out1.splitlines()[-1] + + desc2 = "python-bugzilla cli upload %s" % datetime.datetime.today() + out2 = run_cli(cmd + "%s --file test --summary \"%s\"" % + (setbug.id, desc2), bz, stdin=open(testfile).read()) + + # Expected output format: + # Created attachment on bug + + setbug.refresh() + assert len(setbug.attachments) == (orignumattach + 2) + + att1 = setbug.attachments[-2] + attachid = att1["id"] + assert att1["summary"] == desc1 + assert att1["id"] == int(out1.splitlines()[0].split()[2]) + assert att1["content_type"] == "text/plain" + + att2 = setbug.attachments[-1] + assert att2["summary"] == desc2 + assert att2["id"] == int(out2.splitlines()[0].split()[2]) + assert att2["content_type"] == "application/octet-stream" + + # Set attachment flags + assert att1["flags"] == [] + bz.updateattachmentflags(setbug.id, att2["id"], "review", status="+") + setbug.refresh() + + assert len(setbug.attachments[-1]["flags"]) == 1 + assert setbug.attachments[-1]["flags"][0]["name"] == "review" + assert setbug.attachments[-1]["flags"][0]["status"] == "+" + + bz.updateattachmentflags(setbug.id, setbug.attachments[-1]["id"], + "review", status="X") + setbug.refresh() + assert setbug.attachments[-1]["flags"] == [] + + # Set attachment obsolete + bz._backend.bug_attachment_update({ # pylint: disable=protected-access + "ids": [setbug.attachments[-1]["id"]], + "is_obsolete": 1}) + setbug.refresh() + assert setbug.attachments[-1]["is_obsolete"] == 1 + + + # Get attachment, verify content + out = run_cli(cmd + "--get %s" % attachid, bz).splitlines() + + # Expect format: + # Wrote + fname = out[0].split()[1].strip() + + assert len(out) == 1 + assert fname == "bz-attach-get1.txt" + assert open(fname).read() == open(testfile).read() + os.unlink(fname) + + # Get all attachments + getbug = bz.getbug(setbug.id) + getbug.autorefresh = True + numattach = len(getbug.attachments) + out = run_cli(cmd + "--getall %s" % getbug.id, bz).splitlines() + + assert len(out) == numattach + fnames = [l.split(" ", 1)[1].strip() for l in out] + assert len(fnames) == numattach + for f in fnames: + if not os.path.exists(f): + raise AssertionError("filename '%s' not found" % f) + os.unlink(f) + + # Get all attachments, but ignore obsolete + ignorecmd = cmd + "--getall %s --ignore-obsolete" % getbug.id + out = run_cli(ignorecmd, bz).splitlines() + + assert len(out) == (numattach - 1) + fnames = [l.split(" ", 1)[1].strip() for l in out] + assert len(fnames) == (numattach - 1) + for f in fnames: + if not os.path.exists(f): + raise AssertionError("filename '%s' not found" % f) + os.unlink(f) + + +def test09Whiteboards(run_cli): + bz = _open_bz() + bug_id = "663674" + cmd = "bugzilla modify %s " % bug_id + bug = bz.getbug(bug_id) + + # Set all whiteboards + initval = str(random.randint(1, 1024)) + run_cli(cmd + + "--whiteboard =%sstatus " + "--devel_whiteboard =%sdevel " + "--internal_whiteboard '=%sinternal, security, foo security1' " + "--qa_whiteboard =%sqa " % + (initval, initval, initval, initval), bz) + + bug.refresh() + assert bug.whiteboard == (initval + "status") + assert bug.qa_whiteboard == (initval + "qa") + assert bug.devel_whiteboard == (initval + "devel") + assert (bug.internal_whiteboard == + (initval + "internal, security, foo security1")) + + # Modify whiteboards + run_cli(cmd + + "--whiteboard =foobar " + "--qa_whiteboard _app " + "--devel_whiteboard =pre-%s" % bug.devel_whiteboard, bz) + + bug.refresh() + assert bug.qa_whiteboard == (initval + "qa" + " _app") + assert bug.devel_whiteboard == ("pre-" + initval + "devel") + assert bug.status_whiteboard == "foobar" + + # Verify that tag manipulation is smart about separator + run_cli(cmd + + "--qa_whiteboard=-_app " + "--internal_whiteboard=-security,", bz) + bug.refresh() + + assert bug.qa_whiteboard == (initval + "qa") + assert bug.internal_whiteboard == (initval + "internal, foo security1") + + # Clear whiteboards + update = bz.build_update( + whiteboard="", devel_whiteboard="", + internal_whiteboard="", qa_whiteboard="") + bz.update_bugs(bug.id, update) + + bug.refresh() + assert bug.whiteboard == "" + assert bug.qa_whiteboard == "" + assert bug.devel_whiteboard == "" + assert bug.internal_whiteboard == "" + + +def test10Login(run_cli, monkeypatch): + """ + Failed login test, gives us a bit more coverage + """ + tests.utils.monkeypatch_getpass(monkeypatch) + + cmd = "bugzilla --no-cache-credentials --bugzilla %s" % RHURL + # Implied login with --username and --password + ret = run_cli("%s --user foobar@example.com " + "--password foobar query -b 123456" % cmd, + None, expectfail=True) + assert "Login failed: " in ret + + # 'login' with explicit options + ret = run_cli("%s --user foobar@example.com " + "--password foobar login" % cmd, + None, expectfail=True) + assert "Login failed: " in ret + + # 'login' with positional options + ret = run_cli("%s login foobar@example.com foobar" % cmd, + None, expectfail=True) + assert "Login failed: " in ret + + # bare 'login' + stdinstr = "foobar@example.com\n\rfoobar\n\r" + ret = run_cli("%s login" % cmd, + None, expectfail=True, stdin=stdinstr) + assert "Bugzilla Username:" in ret + assert "Bugzilla Password:" in ret + assert "Login failed: " in ret + + +def test11UserUpdate(): + # This won't work if run by the same user we are using + bz = _open_bz() + email = "anaconda-maint-list@redhat.com" + group = "fedora_contrib" + + have_admin = _check_have_admin(bz) + + user = bz.getuser(email) + if have_admin: + assert group in user.groupnames + origgroups = user.groupnames + + # Remove the group + try: + bz.updateperms(email, "remove", [group]) + user.refresh() + assert group not in user.groupnames + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) - assert bug.qa_whiteboard == (initval + "qa") - assert bug.internal_whiteboard == (initval + "internal, foo security1") + # Re add it + try: + bz.updateperms(email, "add", group) + user.refresh() + assert group in user.groupnames + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) - # Clear whiteboards - update = bz.build_update( - whiteboard="", devel_whiteboard="", - internal_whiteboard="", qa_whiteboard="") - bz.update_bugs(bug.id, update) + # Set groups + try: + newgroups = user.groupnames[:] + if have_admin: + newgroups.remove(group) + bz.updateperms(email, "set", newgroups) + user.refresh() + assert group not in user.groupnames + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) - bug.refresh() - assert bug.whiteboard == "" - assert bug.qa_whiteboard == "" - assert bug.devel_whiteboard == "" - assert bug.internal_whiteboard == "" + # Reset everything + try: + bz.updateperms(email, "set", origgroups) + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) + + user.refresh() + assert user.groupnames == origgroups + + +def test11ComponentEditing(): + bz = _open_bz() + component = ("python-bugzilla-testcomponent-%s" % + str(random.randint(1, 1024 * 1024 * 1024))) + basedata = { + "product": "Fedora Documentation", + "component": component, + } + + have_admin = _check_have_admin(bz) + + def compare(data, newid): + # pylint: disable=protected-access + products = bz._proxy.Product.get({"names": [basedata["product"]]}) + compdata = None + for c in products["products"][0]["components"]: + if int(c["id"]) == int(newid): + compdata = c + break + + assert bool(compdata) + assert data["component"] == compdata["name"] + assert data["description"] == compdata["description"] + assert data["initialowner"] == compdata["default_assigned_to"] + assert data["initialqacontact"] == compdata["default_qa_contact"] + assert data["is_active"] == compdata["is_active"] + + + # Create component + data = basedata.copy() + data.update({ + "description": "foo test bar", + "initialowner": "crobinso@redhat.com", + "initialqacontact": "extras-qa@fedoraproject.org", + "initialcclist": ["wwoods@redhat.com", "toshio@fedoraproject.org"], + "is_active": True, + }) + newid = None + try: + newid = bz.addcomponent(data)['id'] + print("Created product=%s component=%s" % ( + basedata["product"], basedata["component"])) + compare(data, newid) + except Exception as e: + if have_admin: + raise + assert (("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) + + + # Edit component + data = basedata.copy() + data.update({ + "description": "hey new desc!", + "initialowner": "extras-qa@fedoraproject.org", + "initialqacontact": "virt-mgr-maint@redhat.com", + "initialcclist": ["libvirt-maint@redhat.com", + "virt-maint@lists.fedoraproject.org"], + "is_active": False, + }) + try: + bz.editcomponent(data) + if newid is not None: + compare(data, newid) + except Exception as e: + if have_admin: + raise + assert (("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) - def test10Login(self): - """ - Failed login test, gives us a bit more coverage - """ - # We overwrite getpass for testing - import getpass - - def fakegetpass(prompt): - sys.stdout.write(prompt) - sys.stdout.flush() - return sys.stdin.readline() - oldgetpass = getpass.getpass - getpass.getpass = fakegetpass - - try: - cmd = "bugzilla --no-cache-credentials --bugzilla %s" % RHURL - # Implied login with --username and --password - ret = tests.clicomm("%s --user foobar@example.com " - "--password foobar query -b 123456" % cmd, - None, expectfail=True) - assert "Login failed: " in ret - - # 'login' with explicit options - ret = tests.clicomm("%s --user foobar@example.com " - "--password foobar login" % cmd, - None, expectfail=True) - assert "Login failed: " in ret - - # 'login' with positional options - ret = tests.clicomm("%s login foobar@example.com foobar" % cmd, - None, expectfail=True) - assert "Login failed: " in ret - - - # bare 'login' - stdinstr = tests.utils.fake_stream( - "foobar@example.com\n\rfoobar\n\r") - ret = tests.clicomm("%s login" % cmd, - None, expectfail=True, stdin=stdinstr) - assert "Bugzilla Username:" in ret - assert "Bugzilla Password:" in ret - assert "Login failed: " in ret - finally: - getpass.getpass = oldgetpass - - - def test11UserUpdate(self): - # This won't work if run by the same user we are using - bz = self._open_bz() - email = "anaconda-maint-list@redhat.com" - group = "fedora_contrib" - - fn = sys._getframe().f_code.co_name # pylint: disable=protected-access - have_admin = self._check_have_admin(bz, fn) - - user = bz.getuser(email) - if have_admin: - assert group in user.groupnames - origgroups = user.groupnames - - # Remove the group - try: - bz.updateperms(email, "remove", [group]) - user.refresh() - assert group not in user.groupnames - except Exception as e: - if have_admin: - raise - assert "Sorry, you aren't a member" in str(e) - - # Re add it - try: - bz.updateperms(email, "add", group) - user.refresh() - assert group in user.groupnames - except Exception as e: - if have_admin: - raise - assert "Sorry, you aren't a member" in str(e) - - # Set groups - try: - newgroups = user.groupnames[:] - if have_admin: - newgroups.remove(group) - bz.updateperms(email, "set", newgroups) - user.refresh() - assert group not in user.groupnames - except Exception as e: - if have_admin: - raise - assert "Sorry, you aren't a member" in str(e) - - # Reset everything - try: - bz.updateperms(email, "set", origgroups) - except Exception as e: - if have_admin: - raise - assert "Sorry, you aren't a member" in str(e) +def test12SetCookie(): + bz = _open_bz(cookiefile=-1, tokenfile=None, configpaths=[]) - user.refresh() - assert user.groupnames == origgroups - - - def test11ComponentEditing(self): - bz = self._open_bz() - component = ("python-bugzilla-testcomponent-%s" % - str(random.randint(1, 1024 * 1024 * 1024))) - basedata = { - "product": "Fedora Documentation", - "component": component, - } - - fn = sys._getframe().f_code.co_name # pylint: disable=protected-access - have_admin = self._check_have_admin(bz, fn) - - def compare(data, newid): - # pylint: disable=protected-access - products = bz._proxy.Product.get({"names": [basedata["product"]]}) - compdata = None - for c in products["products"][0]["components"]: - if int(c["id"]) == int(newid): - compdata = c - break - - assert bool(compdata) - assert data["component"] == compdata["name"] - assert data["description"] == compdata["description"] - assert data["initialowner"] == compdata["default_assigned_to"] - assert data["initialqacontact"] == compdata["default_qa_contact"] - assert data["is_active"] == compdata["is_active"] - - - # Create component - data = basedata.copy() - data.update({ - "description": "foo test bar", - "initialowner": "crobinso@redhat.com", - "initialqacontact": "extras-qa@fedoraproject.org", - "initialcclist": ["wwoods@redhat.com", "toshio@fedoraproject.org"], - "is_active": True, - }) - newid = None - try: - newid = bz.addcomponent(data)['id'] - print("Created product=%s component=%s" % ( - basedata["product"], basedata["component"])) - compare(data, newid) - except Exception as e: - if have_admin: - raise - assert (("Sorry, you aren't a member" in str(e)) or - # bugzilla 5 error string - ("You are not allowed" in str(e))) - - - # Edit component - data = basedata.copy() - data.update({ - "description": "hey new desc!", - "initialowner": "extras-qa@fedoraproject.org", - "initialqacontact": "virt-mgr-maint@redhat.com", - "initialcclist": ["libvirt-maint@redhat.com", - "virt-maint@lists.fedoraproject.org"], - "is_active": False, - }) - try: - bz.editcomponent(data) - if newid is not None: - compare(data, newid) - except Exception as e: - if have_admin: - raise - assert (("Sorry, you aren't a member" in str(e)) or - # bugzilla 5 error string - ("You are not allowed" in str(e))) - - def test12SetCookie(self): - bz = self._open_bz(cookiefile=-1, tokenfile=None, configpaths=[]) - - try: - bz.cookiefile = None - raise AssertionError("Setting cookiefile for active connection " - "should fail.") - except RuntimeError as e: - assert "disconnect()" in str(e) - - bz.disconnect() + try: bz.cookiefile = None - bz.connect() - assert not bz.logged_in - - def test13SubComponents(self): - bz = self._open_bz() - # Long closed RHEL5 lvm2 bug. This component has sub_components - bug = bz.getbug("185526") - bug.autorefresh = True - assert bug.component == "lvm2" - - bz.update_bugs(bug.id, bz.build_update( - component="lvm2", sub_component="Command-line tools (RHEL5)")) - bug.refresh() - assert bug.sub_components == {"lvm2": ["Command-line tools (RHEL5)"]} - - bz.update_bugs(bug.id, bz.build_update( - component="lvm2", sub_component="Default / Unclassified (RHEL5)")) - bug.refresh() - assert bug.sub_components == {"lvm2": [ - "Default / Unclassified (RHEL5)"]} - - def test13ExternalTrackerQuery(self): - bz = self._open_bz() - with pytest.raises(RuntimeError): - bz.build_external_tracker_boolean_query() - - def _deleteAllExistingExternalTrackers(self, bugid): - bz = self._open_bz() - ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] - if ids != []: - bz.remove_external_tracker(ids=ids) - - def test14ExternalTrackersAddUpdateRemoveQuery(self): - bz = self._open_bz() - bugid = 461686 - ext_bug_id = 380489 - - # Delete any existing external trackers to get to a known state - self._deleteAllExistingExternalTrackers(bugid) - - url = "https://bugzilla.mozilla.org" - if bz.bz_ver_major < 5: - url = "http://bugzilla.mozilla.org" - - # test adding tracker - kwargs = { - 'ext_type_id': 6, - 'ext_type_url': url, - 'ext_type_description': 'Mozilla Foundation', - } - bz.add_external_tracker(bugid, ext_bug_id, **kwargs) - added_bug = bz.getbug(bugid).external_bugs[0] - assert added_bug['type']['id'] == kwargs['ext_type_id'] - assert added_bug['type']['url'] == kwargs['ext_type_url'] - assert (added_bug['type']['description'] == - kwargs['ext_type_description']) - - # test updating status, description, and priority by id - kwargs = { - 'ids': bz.getbug(bugid).external_bugs[0]['id'], - 'ext_status': 'New Status', - 'ext_description': 'New Description', - 'ext_priority': 'New Priority' - } - bz.update_external_tracker(**kwargs) - updated_bug = bz.getbug(bugid).external_bugs[0] - assert updated_bug['ext_bz_bug_id'] == str(ext_bug_id) - assert updated_bug['ext_status'] == kwargs['ext_status'] - assert updated_bug['ext_description'] == kwargs['ext_description'] - assert updated_bug['ext_priority'] == kwargs['ext_priority'] - - # test removing tracker - ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] - assert len(ids) == 1 + raise AssertionError("Setting cookiefile for active connection " + "should fail.") + except RuntimeError as e: + assert "disconnect()" in str(e) + + bz.disconnect() + bz.cookiefile = None + bz.connect() + assert not bz.logged_in + + +def test13SubComponents(): + bz = _open_bz() + # Long closed RHEL5 lvm2 bug. This component has sub_components + bug = bz.getbug("185526") + bug.autorefresh = True + assert bug.component == "lvm2" + + bz.update_bugs(bug.id, bz.build_update( + component="lvm2", sub_component="Command-line tools (RHEL5)")) + bug.refresh() + assert bug.sub_components == {"lvm2": ["Command-line tools (RHEL5)"]} + + bz.update_bugs(bug.id, bz.build_update( + component="lvm2", sub_component="Default / Unclassified (RHEL5)")) + bug.refresh() + assert bug.sub_components == {"lvm2": [ + "Default / Unclassified (RHEL5)"]} + + +def test13ExternalTrackerQuery(): + bz = _open_bz() + with pytest.raises(RuntimeError): + bz.build_external_tracker_boolean_query() + + +def _deleteAllExistingExternalTrackers(bugid): + bz = _open_bz() + ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] + if ids != []: bz.remove_external_tracker(ids=ids) - ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] - assert len(ids) == 0 - - def test15EnsureLoggedIn(self): - bz = self._open_bz() - comm = "bugzilla --ensure-logged-in query --bug_id 979546" - tests.clicomm(comm, bz) - - def test16ModifyTags(self): - bugid = "461686" - cmd = "bugzilla modify %s " % bugid - bz = self._open_bz() - bug = bz.getbug(bugid) - - if bug.tags: - bz.update_tags(bug.id, tags_remove=bug.tags) - bug.refresh() - assert bug.tags == [] - - tests.clicomm(cmd + "--tags foo --tags +bar --tags baz", bz) - bug.refresh() - assert bug.tags == ["foo", "bar", "baz"] - tests.clicomm(cmd + "--tags=-bar", bz) - bug.refresh() - assert bug.tags == ["foo", "baz"] +def test14ExternalTrackersAddUpdateRemoveQuery(): + bz = _open_bz() + bugid = 461686 + ext_bug_id = 380489 + + # Delete any existing external trackers to get to a known state + _deleteAllExistingExternalTrackers(bugid) + + url = "https://bugzilla.mozilla.org" + if bz.bz_ver_major < 5: + url = "http://bugzilla.mozilla.org" + + # test adding tracker + kwargs = { + 'ext_type_id': 6, + 'ext_type_url': url, + 'ext_type_description': 'Mozilla Foundation', + } + bz.add_external_tracker(bugid, ext_bug_id, **kwargs) + added_bug = bz.getbug(bugid).external_bugs[0] + assert added_bug['type']['id'] == kwargs['ext_type_id'] + assert added_bug['type']['url'] == kwargs['ext_type_url'] + assert (added_bug['type']['description'] == + kwargs['ext_type_description']) + + # test updating status, description, and priority by id + kwargs = { + 'ids': bz.getbug(bugid).external_bugs[0]['id'], + 'ext_status': 'New Status', + 'ext_description': 'New Description', + 'ext_priority': 'New Priority' + } + bz.update_external_tracker(**kwargs) + updated_bug = bz.getbug(bugid).external_bugs[0] + assert updated_bug['ext_bz_bug_id'] == str(ext_bug_id) + assert updated_bug['ext_status'] == kwargs['ext_status'] + assert updated_bug['ext_description'] == kwargs['ext_description'] + assert updated_bug['ext_priority'] == kwargs['ext_priority'] + + # test removing tracker + ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] + assert len(ids) == 1 + bz.remove_external_tracker(ids=ids) + ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] + assert len(ids) == 0 + + +def test15EnsureLoggedIn(run_cli): + bz = _open_bz() + comm = "bugzilla --ensure-logged-in query --bug_id 979546" + run_cli(comm, bz) + + +def test16ModifyTags(run_cli): + bugid = "461686" + cmd = "bugzilla modify %s " % bugid + bz = _open_bz() + bug = bz.getbug(bugid) + + if bug.tags: bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() assert bug.tags == [] - def test17LoginAPIKey(self): - api_key = "somefakeapikey1234" - bz = self._open_bz(use_creds=False, api_key=api_key) - if bz.bz_ver_major < 5: - self.skipTest("can only test apikey on bugzilla 5+") + run_cli(cmd + "--tags foo --tags +bar --tags baz", bz) + bug.refresh() + assert bug.tags == ["foo", "bar", "baz"] + + run_cli(cmd + "--tags=-bar", bz) + bug.refresh() + assert bug.tags == ["foo", "baz"] + + bz.update_tags(bug.id, tags_remove=bug.tags) + bug.refresh() + assert bug.tags == [] + + +def test17LoginAPIKey(): + api_key = "somefakeapikey1234" + bz = _open_bz(use_creds=False, api_key=api_key) + if bz.bz_ver_major < 5: + pytest.skip("can only test apikey on bugzilla 5+") - try: - assert bz.logged_in is False + try: + assert bz.logged_in is False - # Use this to trigger a warning about api_key - bz.createbug(bz.build_createbug()) - except Exception as e: - assert "The API key you specified is invalid" in str(e) + # Use this to trigger a warning about api_key + bz.createbug(bz.build_createbug()) + except Exception as e: + assert "The API key you specified is invalid" in str(e) diff --git a/tests/utils.py b/tests/utils.py index a3a7a1ce..1e7b9658 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,6 +2,7 @@ # See the COPYING file in the top-level directory. import difflib +import getpass import inspect import io import os @@ -34,6 +35,14 @@ def fake_stream(text): return io.BytesIO(text) +def monkeypatch_getpass(monkeypatch): + if IS_PY3: + monkeypatch.setattr(getpass, "getpass", input) + else: + monkeypatch.setattr(getpass, "getpass", + raw_input) # pylint: disable=undefined-variable + + def diff_compare(inputdata, filename): """Compare passed string output to contents of filename""" filename = tests_path(filename) From 567be2a091c395be538ce2afc28991ca412039cd Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 18:32:51 -0500 Subject: [PATCH 162/393] tests: ro: convert to more pytest infrastructure Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 610 +++++++++++++++++++----------------- tests/test_rw_functional.py | 6 +- 2 files changed, 325 insertions(+), 291 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 596ba0c4..41bcd81f 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -12,321 +12,355 @@ """ import os -import unittest - -import pytest import bugzilla import tests -class BaseTest(unittest.TestCase): - url = None - bzversion = (0, 0) - closestatus = "CLOSED" - - def _open_bz(self, **kwargs): - if "use_creds" not in kwargs: - kwargs["use_creds"] = False - return bugzilla.Bugzilla(self.url, **kwargs) +REDHAT_URL = (tests.CLICONFIG.REDHAT_URL or + "https://bugzilla.redhat.com") - def clicomm(self, argstr, expectexc=False, bz=None): - comm = "bugzilla " + argstr - if not bz: - bz = self._open_bz() - if expectexc: - with pytest.raises(Exception): - tests.clicomm(comm, bz) - else: - return tests.clicomm(comm, bz) +def _open_bz(url, **kwargs): + if "use_creds" not in kwargs: + kwargs["use_creds"] = False + return bugzilla.Bugzilla(url, **kwargs) - def _testBZVersion(self): - if tests.CLICONFIG.REDHAT_URL: - return - bz = self._open_bz() - assert bz.bz_ver_major == self.bzversion[0] - assert bz.bz_ver_minor == self.bzversion[1] +def _check(out, mincount, expectstr): # Since we are running these tests against bugzilla instances in # the wild, we can't depend on certain data like product lists # remaining static. Use lax sanity checks in this case - - def _testInfoProducts(self, mincount, expectstr): - out = self.clicomm("info --products").splitlines() - assert len(out) >= mincount - assert expectstr in out - - def _testInfoComps(self, comp, mincount, expectstr): - out = self.clicomm("info --components \"%s\"" % comp).splitlines() - assert len(out) >= mincount - assert expectstr in out - - def _testInfoVers(self, comp, mincount, expectstr): - out = self.clicomm("info --versions \"%s\"" % comp).splitlines() - assert len(out) >= mincount - if expectstr: - assert expectstr in out - - def _testInfoCompOwners(self, comp, expectstr): - expectexc = (expectstr == "FAIL") - out = self.clicomm("info --component_owners \"%s\"" % - comp, expectexc=expectexc) - if expectexc: - return - - assert expectstr in out.splitlines() - - def _testQuery(self, args, mincount, expectbug): - expectexc = (expectbug == "FAIL") - cli = "query %s --bug_status %s" % (args, self.closestatus) - out = self.clicomm(cli, expectexc=expectexc) - if expectexc: - return - + if mincount is not None: assert len(out.splitlines()) >= mincount - assert bool([l1 for l1 in out.splitlines() if - l1.startswith("#" + expectbug)]) + assert expectstr in out - # Check --ids output option - out2 = self.clicomm(cli + " --ids") - assert len(out.splitlines()) == len(out2.splitlines()) - assert bool([l2 for l2 in out2.splitlines() if - l2 == expectbug]) +def _test_version(bz, bzversion): + assert bz.bz_ver_major == bzversion[0] + assert bz.bz_ver_minor == bzversion[1] - def _testQueryFull(self, bugid, mincount, expectstr): - out = self.clicomm("query --full --bug_id %s" % bugid) - assert len(out.splitlines()) >= mincount - assert expectstr in out - def _testQueryRaw(self, bugid, mincount, expectstr): - out = self.clicomm("query --raw --bug_id %s" % bugid) - assert len(out.splitlines()) >= mincount - assert expectstr in out - - def _testQueryOneline(self, bugid, expectstr): - out = self.clicomm("query --oneline --bug_id %s" % bugid) - assert len(out.splitlines()) == 3 - assert out.splitlines()[2].startswith("#%s" % bugid) - assert expectstr in out - - def _testQueryExtra(self, bugid, expectstr): - out = self.clicomm("query --extra --bug_id %s" % bugid) - assert ("#%s" % bugid) in out - assert expectstr in out - - def _testQueryFormat(self, args, expectstr): - out = self.clicomm("query %s" % args) - assert expectstr in out - - def _testQueryURL(self, querystr, count, expectstr): - url = self.url - if "/xmlrpc.cgi" in self.url: - url = url.replace("/xmlrpc.cgi", querystr) - else: - url += querystr - out = self.clicomm("query --from-url \"%s\"" % url) - assert len(out.splitlines()) == count - assert expectstr in out - - -class BZMozilla(BaseTest): +################### +# mozilla testing # +################### + +def test_mozilla(): url = "bugzilla.mozilla.org" + bz = _open_bz(url) + + # bugzilla.mozilla.org returns version values in YYYY-MM-DD + # format, so just try to confirm that + assert bz.__class__ == bugzilla.Bugzilla + assert bz.bz_ver_major >= 2016 + assert bz.bz_ver_minor in range(1, 13) - def testVersion(self): - # bugzilla.mozilla.org returns version values in YYYY-MM-DD - # format, so just try to confirm that - bz = self._open_bz() - assert bz.__class__ == bugzilla.Bugzilla - assert bz.bz_ver_major >= 2016 - assert bz.bz_ver_minor in range(1, 13) +################## +# gentoo testing # +################## -class BZGentoo(BaseTest): +def test_gentoo(): url = "bugs.gentoo.org" bzversion = (5, 0) - test0 = BaseTest._testBZVersion + bz = _open_bz(url) + _test_version(bz, bzversion) - def testURLQuery(self): - # This is a bugzilla 5.0 instance, which supports URL queries now - query_url = ("https://bugs.gentoo.org/buglist.cgi?" - "component=[CS]&product=Doc%20Translations" - "&query_format=advanced&resolution=FIXED") - bz = self._open_bz() - ret = bz.query(bz.url_to_query(query_url)) - assert len(ret) > 0 + # This is a bugzilla 5.0 instance, which supports URL queries now + query_url = ("https://bugs.gentoo.org/buglist.cgi?" + "component=[CS]&product=Doc%20Translations" + "&query_format=advanced&resolution=FIXED") + ret = bz.query(bz.url_to_query(query_url)) + assert len(ret) > 0 -class RHTest(BaseTest): - url = (tests.CLICONFIG.REDHAT_URL or - "https://bugzilla.redhat.com") - bzversion = (5, 0) +################## +# redhat testing # +################## + + +def testInfoProducts(run_cli): + bz = _open_bz(REDHAT_URL) + + out = run_cli("bugzilla info --products", bz) + _check(out, 123, "Virtualization Tools") + + +def testInfoComps(run_cli): + bz = _open_bz(REDHAT_URL) + + out = run_cli("bugzilla info --components 'Virtualization Tools'", bz) + _check(out, 8, "virtinst") + + +def testInfoVers(run_cli): + bz = _open_bz(REDHAT_URL) + + out = run_cli("bugzilla info --versions Fedora", bz) + _check(out, 17, "rawhide") + + +def testInfoCompOwners(run_cli): + bz = _open_bz(REDHAT_URL) + + out = run_cli("bugzilla info " + "--component_owners 'Virtualization Tools'", bz) + _check(out, None, "libvirt: Libvirt Maintainers") + + +def testQuery(run_cli): + bz = _open_bz(REDHAT_URL) + + args = "--product Fedora --component python-bugzilla --version 14" + cli = "bugzilla query %s --bug_status CLOSED" % args + mincount = 4 + expectbug = "621030" + out = run_cli(cli, bz) + + assert len(out.splitlines()) >= mincount + assert bool([l1 for l1 in out.splitlines() if + l1.startswith("#" + expectbug)]) + + # Check --ids output option + out2 = run_cli(cli + " --ids", bz) + assert len(out.splitlines()) == len(out2.splitlines()) + assert bool([l2 for l2 in out2.splitlines() if + l2 == expectbug]) + + +def testQueryFull(run_cli): + bz = _open_bz(REDHAT_URL) + + bugid = "621601" + out = run_cli("bugzilla query --full --bug_id %s" % bugid, bz) + _check(out, 60, "end-of-life (EOL)") + + +def testQueryRaw(run_cli): + bz = _open_bz(REDHAT_URL) + + bugid = "307471" + out = run_cli("bugzilla query --raw --bug_id %s" % bugid, bz) + _check(out, 70, "ATTRIBUTE[whiteboard]: bzcl34nup") + + +def testQueryOneline(run_cli): + bz = _open_bz(REDHAT_URL) + + bugid = "785016" + out = run_cli("bugzilla query --oneline --bug_id %s" % bugid, bz) + assert len(out.splitlines()) == 1 + assert out.splitlines()[0].startswith("#%s" % bugid) + assert "[---] fedora-review+,fedora-cvs+" in out + + bugid = "720784" + out = run_cli("bugzilla query --oneline --bug_id %s" % bugid, bz) + assert len(out.splitlines()) == 1 + assert out.splitlines()[0].startswith("#%s" % bugid) + assert " CVE-2011-2527" in out + + +def testQueryExtra(run_cli): + bz = _open_bz(REDHAT_URL) + + bugid = "307471" + out = run_cli("bugzilla query --extra --bug_id %s" % bugid, bz) + assert ("#%s" % bugid) in out + assert " +Status Whiteboard: bzcl34nup" in out + + +def testQueryFormat(run_cli): + bz = _open_bz(REDHAT_URL) + + args = ("--bug_id 307471 --outputformat=\"id=%{bug_id} " + "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " + "sum=%{summary}\"") + out = run_cli("bugzilla query %s" % args, bz) + assert "id=307471 sw= bzcl34nup needinfo= " in out + + args = ("--bug_id 785016 --outputformat=\"id=%{bug_id} " + "sw=%{whiteboard:status} flag=%{flag:fedora-review} " + "sum=%{summary}\"") + out = run_cli("bugzilla query %s" % args, bz) + assert "id=785016 sw= flag=+" in out - test0 = BaseTest._testBZVersion - test01 = lambda s: BaseTest._testInfoProducts(s, 125, - "Virtualization Tools") - test02 = lambda s: BaseTest._testInfoComps(s, "Virtualization Tools", - 10, "virtinst") - test03 = lambda s: BaseTest._testInfoVers(s, "Fedora", 19, "rawhide") - test04 = lambda s: BaseTest._testInfoCompOwners(s, "Virtualization Tools", - "libvirt: Libvirt Maintainers") - - test05 = lambda s: BaseTest._testQuery(s, - "--product Fedora --component python-bugzilla --version 14", - 6, "621030") - test06 = lambda s: BaseTest._testQueryFull(s, "621601", 60, - "end-of-life (EOL)") - test07 = lambda s: BaseTest._testQueryRaw(s, "307471", 70, - "ATTRIBUTE[whiteboard]: bzcl34nup") - test08 = lambda s: BaseTest._testQueryOneline(s, "785016", - "[---] fedora-review+,fedora-cvs+") - test09 = lambda s: BaseTest._testQueryExtra(s, "307471", - " +Status Whiteboard: bzcl34nup") - test10 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 307471 --outputformat=\"id=%{bug_id} " - "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " - "sum=%{summary}\"", - "id=307471 sw= bzcl34nup needinfo= ") - test11 = lambda s: BaseTest._testQueryURL(s, - "/buglist.cgi?f1=creation_ts" - "&list_id=973582&o1=greaterthaneq&classification=Fedora&" - "o2=lessthaneq&query_format=advanced&f2=creation_ts" - "&v1=2010-01-01&component=python-bugzilla&v2=2011-01-01" - "&product=Fedora", 26, "#553878 CLOSED") - test12 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 785016 --outputformat=\"id=%{bug_id} " - "sw=%{whiteboard:status} flag=%{flag:fedora-review} " - "sum=%{summary}\"", - "id=785016 sw= flag=+") # Unicode in this bug's summary - test13 = lambda s: BaseTest._testQueryFormat(s, - "--bug_id 522796 --outputformat \"%{summary}\"", - "V34 — system") - # CVE bug output - test14 = lambda s: BaseTest._testQueryOneline(s, "720784", - " CVE-2011-2527") - - def testDoubleConnect(self): - bz = self._open_bz() - bz.connect(self.url) - - def testQueryFlags(self): - bz = self._open_bz() - if not bz.logged_in: - print("not logged in, skipping testQueryFlags") - return - - out = self.clicomm("query --product 'Red Hat Enterprise Linux 5' " - "--component virt-manager --bug_status CLOSED " - "--flag rhel-5.4.0+", bz=bz) - assert len(out.splitlines()) > 15 - assert len(out.splitlines()) < 28 - assert "223805" in out - - def testQueryFixedIn(self): - out = self.clicomm("query --fixed_in anaconda-15.29-1") - assert len(out.splitlines()) == 6 - assert "#629311 CLOSED" in out - - def testComponentsDetails(self): - """ - Fresh call to getcomponentsdetails should properly refresh - """ - bz = self._open_bz() - assert bool(bz.getcomponentsdetails("Red Hat Developer Toolset")) - - def testGetBugAlias(self): - """ - getbug() works if passed an alias - """ - bz = self._open_bz() - bug = bz.getbug("CVE-2011-2527") - assert bug.bug_id == 720773 - - def testQuerySubComponent(self): - out = self.clicomm("query --product 'Red Hat Enterprise Linux 7' " - "--component lvm2 --sub-component 'Thin Provisioning'") - assert len(out.splitlines()) >= 5 - assert "#1060931 " in out - - def testBugFields(self): - bz = self._open_bz() - fields = bz.getbugfields(names=["product"])[:] - assert fields == ["product"] - bz.getbugfields(names=["product", "bug_status"], force_refresh=True) - assert set(bz.bugfields) == set(["product", "bug_status"]) - - def testBugAutoRefresh(self): - bz = self._open_bz() - - bz.bug_autorefresh = True - - bug = bz.query(bz.build_query(bug_id=720773, - include_fields=["summary"]))[0] - assert hasattr(bug, "component") + args = "--bug_id 522796 --outputformat \"%{summary}\"" + out = run_cli("bugzilla query %s" % args, bz) + assert "V34 — system" in out + + +def testQueryURL(run_cli): + bz = _open_bz(REDHAT_URL) + + qurl = ("/buglist.cgi?f1=creation_ts" + "&list_id=973582&o1=greaterthaneq&classification=Fedora&" + "o2=lessthaneq&query_format=advanced&f2=creation_ts" + "&v1=2010-01-01&component=python-bugzilla&v2=2011-01-01" + "&product=Fedora") + + url = REDHAT_URL + if "/xmlrpc.cgi" in url: + url = url.replace("/xmlrpc.cgi", qurl) + else: + url += qurl + out = run_cli("bugzilla query --from-url \"%s\"" % url, bz) + _check(out, 22, "#553878 CLOSED") + + +def testDoubleConnect(): + bz = _open_bz(REDHAT_URL) + bz.connect(REDHAT_URL) + + +def testQueryFlags(run_cli): + bz = _open_bz(REDHAT_URL) + + if not bz.logged_in: + print("not logged in, skipping testQueryFlags") + return + + out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 5' " + "--component virt-manager --bug_status CLOSED " + "--flag rhel-5.4.0+", bz) + assert len(out.splitlines()) > 13 + assert len(out.splitlines()) < 26 + assert "223805" in out + + +def testQueryFixedIn(run_cli): + bz = _open_bz(REDHAT_URL) + + out = run_cli("bugzilla query --fixed_in anaconda-15.29-1", bz) + assert len(out.splitlines()) == 4 + assert "#629311 CLOSED" in out + + +def testComponentsDetails(): + """ + Fresh call to getcomponentsdetails should properly refresh + """ + bz = _open_bz(REDHAT_URL) + + assert bool(bz.getcomponentsdetails("Red Hat Developer Toolset")) + + +def testGetBugAlias(): + """ + getbug() works if passed an alias + """ + bz = _open_bz(REDHAT_URL) + + bug = bz.getbug("CVE-2011-2527") + assert bug.bug_id == 720773 + + +def testQuerySubComponent(run_cli): + bz = _open_bz(REDHAT_URL) + + # Test special error wrappers in bugzilla/_cli.py + out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 7' " + "--component lvm2 --sub-component 'Thin Provisioning'", bz) + assert len(out.splitlines()) >= 3 + assert "#1060931 " in out + + +def testBugFields(): + bz = _open_bz(REDHAT_URL) + fields = bz.getbugfields(names=["product"])[:] + assert fields == ["product"] + bz.getbugfields(names=["product", "bug_status"], force_refresh=True) + assert set(bz.bugfields) == set(["product", "bug_status"]) + + +def testBugAutoRefresh(): + bz = _open_bz(REDHAT_URL) + + bz.bug_autorefresh = True + + bug = bz.query(bz.build_query(bug_id=720773, + include_fields=["summary"]))[0] + assert hasattr(bug, "component") + assert bool(bug.component) + + bz.bug_autorefresh = False + + bug = bz.query(bz.build_query(bug_id=720773, + include_fields=["summary"]))[0] + assert not hasattr(bug, "component") + try: assert bool(bug.component) + except Exception as e: + assert "adjust your include_fields" in str(e) + + +def testExtraFields(): + bz = _open_bz(REDHAT_URL) + + # Check default extra_fields will pull in comments + bug = bz.getbug(720773, exclude_fields=["product"]) + assert "comments" in dir(bug) + assert "product" not in dir(bug) + + # Ensure that include_fields overrides default extra_fields + bug = bz.getbug(720773, include_fields=["summary"]) + assert "summary" in dir(bug) + assert "comments" not in dir(bug) + + +def testExternalBugsOutput(run_cli): + bz = _open_bz(REDHAT_URL) + + out = run_cli('bugzilla query --bug_id 989253 ' + '--outputformat="%{external_bugs}"', bz) + assert "bugzilla.gnome.org/show_bug.cgi?id=703421" in out + assert "External bug: https://bugs.launchpad.net/bugs/1203576" in out + + +def testActiveComps(run_cli): + bz = _open_bz(REDHAT_URL) + + out = run_cli("bugzilla info --components 'Virtualization Tools' " + "--active-components", bz) + assert "virtinst" not in out + out = run_cli("bugzilla info --component_owners 'Virtualization Tools' " + "--active-components", bz) + assert "virtinst" not in out + + +def testFaults(run_cli): + bz = _open_bz(REDHAT_URL) + + # Test special error wrappers in bugzilla/_cli.py + out = run_cli("bugzilla query --field=IDONTEXIST=FOO", bz, + expectfail=True) + assert "Server error:" in out + + out = run_cli("bugzilla " + "--bugzilla https://example.com/xmlrpc.cgi " + "query --field=IDONTEXIST=FOO", None, expectfail=True) + assert "Connection lost/failed" in out + + out = run_cli("bugzilla " + "--bugzilla https://expired.badssl.com/ " + "query --bug_id 1234", None, expectfail=True) + assert "trust the remote server" in out + assert "--nosslverify" in out + + +def testCertFail(run_cli): + # No public setup that I know of to test cert succeeds, so + # let's give it a bogus file and ensure it fails + badcert = os.path.join(os.path.dirname(__file__), "..", "README.md") + out = run_cli( + "bugzilla --cert %s query --bug_id 123456" % badcert, + None, expectfail=True) + assert "PEM" in out + + +def test_redhat(): + bzversion = (5, 0) + bz = _open_bz(REDHAT_URL) - bz.bug_autorefresh = False - - bug = bz.query(bz.build_query(bug_id=720773, - include_fields=["summary"]))[0] - assert not hasattr(bug, "component") - try: - assert bool(bug.component) - except Exception as e: - assert "adjust your include_fields" in str(e) - - def testExtraFields(self): - bz = self._open_bz() - - # Check default extra_fields will pull in comments - bug = bz.getbug(720773, exclude_fields=["product"]) - assert "comments" in dir(bug) - assert "product" not in dir(bug) - - # Ensure that include_fields overrides default extra_fields - bug = bz.getbug(720773, include_fields=["summary"]) - assert "summary" in dir(bug) - assert "comments" not in dir(bug) - - def testExternalBugsOutput(self): - out = self.clicomm('query --bug_id 989253 ' - '--outputformat="%{external_bugs}"') - assert "bugzilla.gnome.org/show_bug.cgi?id=703421" in out - assert "External bug: https://bugs.launchpad.net/bugs/1203576" in out - - def testActiveComps(self): - out = self.clicomm("info --components 'Virtualization Tools' " - "--active-components") - assert "virtinst" not in out - out = self.clicomm("info --component_owners 'Virtualization Tools' " - "--active-components") - assert "virtinst" not in out - - def testFaults(self): - # Test special error wrappers in bugzilla/_cli.py - bz = self._open_bz() - out = tests.clicomm("bugzilla query --field=IDONTEXIST=FOO", - bz, expectfail=True) - assert "Server error:" in out - - out = tests.clicomm("bugzilla " - "--bugzilla https://example.com/xmlrpc.cgi " - "query --field=IDONTEXIST=FOO", None, expectfail=True) - assert "Connection lost/failed" in out - - out = tests.clicomm("bugzilla " - "--bugzilla https://expired.badssl.com/ " - "query --bug_id 1234", None, expectfail=True) - assert "trust the remote server" in out - assert "--nosslverify" in out - - def testCertFail(self): - # No public setup that I know of to test cert succeeds, so - # let's give it a bogus file and ensure it fails - badcert = os.path.join(os.path.dirname(__file__), "..", "README.md") - out = tests.clicomm( - "bugzilla --cert %s query --bug_id 123456" % badcert, - None, expectfail=True) - assert "PEM" in out + if not tests.CLICONFIG.REDHAT_URL: + _test_version(bz, bzversion) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 779e85a5..d9b30d56 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -64,7 +64,7 @@ def test2(): assert bz.__class__ is bugzilla.RHBugzilla -def _makebug(run_cli): +def _makebug(run_cli, bz): component = "python-bugzilla" version = "rawhide" summary = ("python-bugzilla test basic bug %s" % @@ -93,7 +93,7 @@ def test03NewBugBasic(run_cli): Create a bug with minimal amount of fields, then close it """ bz = _open_bz() - bug = _makebug(bz) + bug = _makebug(run_cli, bz) # Verify hasattr works assert hasattr(bug, "id") @@ -505,7 +505,7 @@ def _test8Attachments(run_cli): testfile = "../tests/data/bz-attach-get1.txt" # Add attachment as CLI option - setbug = _makebug(bz) + setbug = _makebug(run_cli, bz) setbug = bz.getbug(setbug.id, extra_fields=["attachments"]) orignumattach = len(setbug.attachments) From 2f567cce4ca6e9f1f62d206ed70044bf4dcabf51 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 8 Jan 2020 18:33:57 -0500 Subject: [PATCH 163/393] tests: Drop old clicomm infrastructure Signed-off-by: Cole Robinson --- tests/__init__.py | 68 ++--------------------------------------------- 1 file changed, 2 insertions(+), 66 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 35c496b7..4d1bdf97 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,18 +1,7 @@ - -from __future__ import print_function +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. import os -import shlex -import sys - -# pylint: disable=import-error -if sys.version_info[0] >= 3: - from io import StringIO -else: - from StringIO import StringIO -# pylint: enable=import-error - -from bugzilla import Bugzilla, RHBugzilla, _cli class _CLICONFIG(object): @@ -23,56 +12,3 @@ def __init__(self): CLICONFIG = _CLICONFIG() os.environ["__BUGZILLA_UNITTEST"] = "1" - - -def clicomm(argvstr, bzinstance, - returnmain=False, stdin=None, expectfail=False): - """ - Run bin/bugzilla.main() directly with passed argv - """ - - argv = shlex.split(argvstr) - - oldstdout = sys.stdout - oldstderr = sys.stderr - oldstdin = sys.stdin - oldargv = sys.argv - try: - out_io = StringIO() - sys.stdout = out_io - sys.stderr = out_io - if stdin: - sys.stdin = stdin - - sys.argv = argv - - ret = 0 - test_return = None - try: - print(" ".join(argv)) - print() - - test_return = _cli.main(unittest_bz_instance=bzinstance) - except SystemExit as sys_e: - ret = sys_e.code - - outstr = out_io.getvalue() - if outstr.endswith("\n"): - outstr = outstr[:-1] - - if ret != 0 and not expectfail: - raise RuntimeError("Command failed with %d\ncmd=%s\nout=%s" % - (ret, argvstr, outstr)) - if ret == 0 and expectfail: - raise RuntimeError("Command succeeded but we expected success\n" - "ret=%d\ncmd=%s\nout=%s" % - (ret, argvstr, outstr)) - - if returnmain: - return test_return - return outstr - finally: - sys.stdout = oldstdout - sys.stderr = oldstderr - sys.stdin = oldstdin - sys.argv = oldargv From 3aba66ea1ac6b1c3a2ceef0b20602c77f7dd341a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 10:00:04 -0500 Subject: [PATCH 164/393] Remove all the deprecated 'boolean' query handling This has been throwing warnings or errors since the 2.0.0 release in Feb 2017, which seems long enough to remove it Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 6 ------ bugzilla/base.py | 7 ------- bugzilla/rhbugzilla.py | 39 ------------------------------------- tests/test_api_misc.py | 6 ------ tests/test_rw_functional.py | 6 ------ 5 files changed, 64 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index b2e7c628..720023a0 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -330,10 +330,6 @@ def _setup_action_query_parser(subparsers): help=argparse.SUPPRESS) p.add_argument('-W', '--status_whiteboard_type', help=argparse.SUPPRESS) - p.add_argument('-B', '--booleantype', - help=argparse.SUPPRESS) - p.add_argument('--boolean_query', action="append", - help=argparse.SUPPRESS) p.add_argument('--fixed_in_type', help=argparse.SUPPRESS) @@ -562,13 +558,11 @@ def _do_query(bz, opt, parser): alias=opt.alias or None, qa_whiteboard=opt.qa_whiteboard or None, devel_whiteboard=opt.devel_whiteboard or None, - boolean_query=opt.boolean_query or None, bug_severity=opt.severity or None, priority=opt.priority or None, target_release=opt.target_release or None, target_milestone=opt.target_milestone or None, emailtype=opt.emailtype or None, - booleantype=opt.booleantype or None, include_fields=include_fields, quicksearch=opt.quicksearch or None, savedsearch=opt.savedsearch or None, diff --git a/bugzilla/base.py b/bugzilla/base.py index 097d8820..c54b3ea2 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1094,13 +1094,11 @@ def build_query(self, alias=None, qa_whiteboard=None, devel_whiteboard=None, - boolean_query=None, bug_severity=None, priority=None, target_release=None, target_milestone=None, emailtype=None, - booleantype=None, include_fields=None, quicksearch=None, savedsearch=None, @@ -1125,11 +1123,6 @@ def build_query(self, For details about the specific argument formats, see the bugzilla docs: https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs """ - if boolean_query or booleantype: - raise RuntimeError("boolean_query format is no longer supported. " - "If you need complicated URL queries, look into " - "query --from-url/url_to_query().") - query = { "alias": alias, "product": listify(product), diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 99a2727c..88786f32 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -311,42 +311,3 @@ def post_translation(self, query, bug): for vallist in val.values(): values += vallist bug['sub_component'] = " ".join(values) - - def build_external_tracker_boolean_query(self, *args, **kwargs): - ignore1 = args - ignore2 = kwargs - raise RuntimeError("Building external boolean queries is " - "no longer supported. Please build a URL query " - "via the bugzilla web UI and pass it to 'query --from-url' " - "or url_to_query()") - - - def build_query(self, **kwargs): - # pylint: disable=arguments-differ - - # We previously accepted a text format to approximate boolean - # queries, and only for RHBugzilla. Upstream bz has --from-url - # support now, so point people to that instead so we don't have - # to document and maintain this logic anymore - def _warn_bool(kwkey): - vallist = listify(kwargs.get(kwkey, None)) - for value in vallist or []: - for s in value.split(" "): - if s not in ["|", "&", "!"]: - continue - log.warning("%s value '%s' appears to use the now " - "unsupported boolean formatting, your query may " - "be incorrect. If you need complicated URL queries, " - "look into bugzilla --from-url/url_to_query().", - kwkey, value) - return - - _warn_bool("fixed_in") - _warn_bool("blocked") - _warn_bool("dependson") - _warn_bool("flag") - _warn_bool("qa_whiteboard") - _warn_bool("devel_whiteboard") - _warn_bool("alias") - - return Bugzilla.build_query(self, **kwargs) diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 3c5a1151..fad2fef0 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -160,12 +160,6 @@ def testSubComponentFail(): bz.build_update(sub_component="some sub component") -def testInvalidBoolean(): - bz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) - with pytest.raises(RuntimeError): - bz.build_query(boolean_query="foobar") - - def testCreatebugFieldConversion(): bz4 = tests.mockbackend.make_bz(version="4.0.0") vc = bz4._validate_createbug # pylint: disable=protected-access diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index d9b30d56..89f2c6ea 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -851,12 +851,6 @@ def test13SubComponents(): "Default / Unclassified (RHEL5)"]} -def test13ExternalTrackerQuery(): - bz = _open_bz() - with pytest.raises(RuntimeError): - bz.build_external_tracker_boolean_query() - - def _deleteAllExistingExternalTrackers(bugid): bz = _open_bz() ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] From 3b28e7fc8431f834f738f5e0b89c3c6fb3a30bc5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 10:11:52 -0500 Subject: [PATCH 165/393] backend: Add APIs for ExternalBugs extension via RHBugzilla Rather than calling _proxy directly Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 23 +++++++++++++++++++++++ bugzilla/_backendxmlrpc.py | 7 +++++++ bugzilla/rhbugzilla.py | 6 +++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 41b38374..8a93c7d3 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -166,6 +166,29 @@ def component_update(self, paramdict): raise NotImplementedError() + ############################### + # ExternalBugs extension APIs # + ############################### + + def externalbugs_add(self, paramdict): + """ + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#add-external-bug + """ + raise NotImplementedError() + + def externalbugs_update(self, paramdict): + """ + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#update-external-bug + """ + raise NotImplementedError() + + def externalbugs_remove(self, paramdict): + """ + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#remove-external-bug + """ + raise NotImplementedError() + + ################ # Product APIs # ################ diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index cc38f0d1..9b0e0337 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -198,6 +198,13 @@ def component_create(self, paramdict): def component_update(self, paramdict): return self._xmlrpc_proxy.Component.update(paramdict) + def externalbugs_add(self, paramdict): + return self._xmlrpc_proxy.ExternalBugs.add_external_bug(paramdict) + def externalbugs_update(self, paramdict): + return self._xmlrpc_proxy.ExternalBugs.update_external_bug(paramdict) + def externalbugs_remove(self, paramdict): + return self._xmlrpc_proxy.ExternalBugs.remove_external_bug(paramdict) + def product_get(self, paramdict): return self._xmlrpc_proxy.Product.get(paramdict) def product_get_accessible(self): diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 88786f32..52e233b0 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -153,7 +153,7 @@ def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, } log.debug("Calling ExternalBugs.add_external_bug(%s)", params) - return self._proxy.ExternalBugs.add_external_bug(params) + return self._backend.externalbugs_update(params) def update_external_tracker(self, ids=None, ext_type_id=None, ext_type_description=None, ext_type_url=None, @@ -202,7 +202,7 @@ def update_external_tracker(self, ids=None, ext_type_id=None, params['ext_priority'] = ext_priority log.debug("Calling ExternalBugs.update_external_bug(%s)", params) - return self._proxy.ExternalBugs.update_external_bug(params) + return self._backend.externalbugs_update(params) def remove_external_tracker(self, ids=None, ext_type_id=None, ext_type_description=None, ext_type_url=None, @@ -240,7 +240,7 @@ def remove_external_tracker(self, ids=None, ext_type_id=None, params['bug_ids'] = listify(bug_ids) log.debug("Calling ExternalBugs.remove_external_bug(%s)", params) - return self._proxy.ExternalBugs.remove_external_bug(params) + return self._backend.externalbugs_remove(params) ################# From 0bdf89d449ddf426dc82a6b38e3a7a2b36a59397 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 10:13:26 -0500 Subject: [PATCH 166/393] rhbugzilla: Move external_bug API wrappers to BugzillaBase While RHBugzilla is the only instance that I know of with this extension enabled, it's not strictly rhbugzilla specific behavior, and keeping it separate framents the API. Move it Signed-off-by: Cole Robinson --- bugzilla/base.py | 136 +++++++++++++++++++++++++++++++++++++++++ bugzilla/rhbugzilla.py | 131 --------------------------------------- 2 files changed, 136 insertions(+), 131 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index c54b3ea2..f5484846 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1812,3 +1812,139 @@ def updateperms(self, user, action, groups): } return self._backend.user_update(update) + + + ############################# + # ExternalBugs API wrappers # + ############################# + + def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, + ext_type_description=None, ext_type_url=None, + ext_status=None, ext_description=None, + ext_priority=None): + """ + Wrapper method to allow adding of external tracking bugs using the + ExternalBugs::WebService::add_external_bug method. + + This is documented at + https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#add_external_bug + + bug_ids: A single bug id or list of bug ids to have external trackers + added. + ext_bz_bug_id: The external bug id (ie: the bug number in the + external tracker). + ext_type_id: The external tracker id as used by Bugzilla. + ext_type_description: The external tracker description as used by + Bugzilla. + ext_type_url: The external tracker url as used by Bugzilla. + ext_status: The status of the external bug. + ext_description: The description of the external bug. + ext_priority: The priority of the external bug. + """ + param_dict = {'ext_bz_bug_id': ext_bz_bug_id} + if ext_type_id is not None: + param_dict['ext_type_id'] = ext_type_id + if ext_type_description is not None: + param_dict['ext_type_description'] = ext_type_description + if ext_type_url is not None: + param_dict['ext_type_url'] = ext_type_url + if ext_status is not None: + param_dict['ext_status'] = ext_status + if ext_description is not None: + param_dict['ext_description'] = ext_description + if ext_priority is not None: + param_dict['ext_priority'] = ext_priority + params = { + 'bug_ids': listify(bug_ids), + 'external_bugs': [param_dict], + } + + log.debug("Calling ExternalBugs.add_external_bug(%s)", params) + return self._backend.externalbugs_add(params) + + def update_external_tracker(self, ids=None, ext_type_id=None, + ext_type_description=None, ext_type_url=None, + ext_bz_bug_id=None, bug_ids=None, + ext_status=None, ext_description=None, + ext_priority=None): + """ + Wrapper method to allow adding of external tracking bugs using the + ExternalBugs::WebService::update_external_bug method. + + This is documented at + https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#update_external_bug + + ids: A single external tracker bug id or list of external tracker bug + ids. + ext_type_id: The external tracker id as used by Bugzilla. + ext_type_description: The external tracker description as used by + Bugzilla. + ext_type_url: The external tracker url as used by Bugzilla. + ext_bz_bug_id: A single external bug id or list of external bug ids + (ie: the bug number in the external tracker). + bug_ids: A single bug id or list of bug ids to have external tracker + info updated. + ext_status: The status of the external bug. + ext_description: The description of the external bug. + ext_priority: The priority of the external bug. + """ + params = {} + if ids is not None: + params['ids'] = listify(ids) + if ext_type_id is not None: + params['ext_type_id'] = ext_type_id + if ext_type_description is not None: + params['ext_type_description'] = ext_type_description + if ext_type_url is not None: + params['ext_type_url'] = ext_type_url + if ext_bz_bug_id is not None: + params['ext_bz_bug_id'] = listify(ext_bz_bug_id) + if bug_ids is not None: + params['bug_ids'] = listify(bug_ids) + if ext_status is not None: + params['ext_status'] = ext_status + if ext_description is not None: + params['ext_description'] = ext_description + if ext_priority is not None: + params['ext_priority'] = ext_priority + + log.debug("Calling ExternalBugs.update_external_bug(%s)", params) + return self._backend.externalbugs_update(params) + + def remove_external_tracker(self, ids=None, ext_type_id=None, + ext_type_description=None, ext_type_url=None, + ext_bz_bug_id=None, bug_ids=None): + """ + Wrapper method to allow removal of external tracking bugs using the + ExternalBugs::WebService::remove_external_bug method. + + This is documented at + https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#remove_external_bug + + ids: A single external tracker bug id or list of external tracker bug + ids. + ext_type_id: The external tracker id as used by Bugzilla. + ext_type_description: The external tracker description as used by + Bugzilla. + ext_type_url: The external tracker url as used by Bugzilla. + ext_bz_bug_id: A single external bug id or list of external bug ids + (ie: the bug number in the external tracker). + bug_ids: A single bug id or list of bug ids to have external tracker + info updated. + """ + params = {} + if ids is not None: + params['ids'] = listify(ids) + if ext_type_id is not None: + params['ext_type_id'] = ext_type_id + if ext_type_description is not None: + params['ext_type_description'] = ext_type_description + if ext_type_url is not None: + params['ext_type_url'] = ext_type_url + if ext_bz_bug_id is not None: + params['ext_bz_bug_id'] = listify(ext_bz_bug_id) + if bug_ids is not None: + params['bug_ids'] = listify(bug_ids) + + log.debug("Calling ExternalBugs.remove_external_bug(%s)", params) + return self._backend.externalbugs_remove(params) diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 52e233b0..94fc1a39 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -111,137 +111,6 @@ def get_alias(): return vals - def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, - ext_type_description=None, ext_type_url=None, - ext_status=None, ext_description=None, - ext_priority=None): - """ - Wrapper method to allow adding of external tracking bugs using the - ExternalBugs::WebService::add_external_bug method. - - This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#add_external_bug - - bug_ids: A single bug id or list of bug ids to have external trackers - added. - ext_bz_bug_id: The external bug id (ie: the bug number in the - external tracker). - ext_type_id: The external tracker id as used by Bugzilla. - ext_type_description: The external tracker description as used by - Bugzilla. - ext_type_url: The external tracker url as used by Bugzilla. - ext_status: The status of the external bug. - ext_description: The description of the external bug. - ext_priority: The priority of the external bug. - """ - param_dict = {'ext_bz_bug_id': ext_bz_bug_id} - if ext_type_id is not None: - param_dict['ext_type_id'] = ext_type_id - if ext_type_description is not None: - param_dict['ext_type_description'] = ext_type_description - if ext_type_url is not None: - param_dict['ext_type_url'] = ext_type_url - if ext_status is not None: - param_dict['ext_status'] = ext_status - if ext_description is not None: - param_dict['ext_description'] = ext_description - if ext_priority is not None: - param_dict['ext_priority'] = ext_priority - params = { - 'bug_ids': listify(bug_ids), - 'external_bugs': [param_dict], - } - - log.debug("Calling ExternalBugs.add_external_bug(%s)", params) - return self._backend.externalbugs_update(params) - - def update_external_tracker(self, ids=None, ext_type_id=None, - ext_type_description=None, ext_type_url=None, - ext_bz_bug_id=None, bug_ids=None, - ext_status=None, ext_description=None, - ext_priority=None): - """ - Wrapper method to allow adding of external tracking bugs using the - ExternalBugs::WebService::update_external_bug method. - - This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#update_external_bug - - ids: A single external tracker bug id or list of external tracker bug - ids. - ext_type_id: The external tracker id as used by Bugzilla. - ext_type_description: The external tracker description as used by - Bugzilla. - ext_type_url: The external tracker url as used by Bugzilla. - ext_bz_bug_id: A single external bug id or list of external bug ids - (ie: the bug number in the external tracker). - bug_ids: A single bug id or list of bug ids to have external tracker - info updated. - ext_status: The status of the external bug. - ext_description: The description of the external bug. - ext_priority: The priority of the external bug. - """ - params = {} - if ids is not None: - params['ids'] = listify(ids) - if ext_type_id is not None: - params['ext_type_id'] = ext_type_id - if ext_type_description is not None: - params['ext_type_description'] = ext_type_description - if ext_type_url is not None: - params['ext_type_url'] = ext_type_url - if ext_bz_bug_id is not None: - params['ext_bz_bug_id'] = listify(ext_bz_bug_id) - if bug_ids is not None: - params['bug_ids'] = listify(bug_ids) - if ext_status is not None: - params['ext_status'] = ext_status - if ext_description is not None: - params['ext_description'] = ext_description - if ext_priority is not None: - params['ext_priority'] = ext_priority - - log.debug("Calling ExternalBugs.update_external_bug(%s)", params) - return self._backend.externalbugs_update(params) - - def remove_external_tracker(self, ids=None, ext_type_id=None, - ext_type_description=None, ext_type_url=None, - ext_bz_bug_id=None, bug_ids=None): - """ - Wrapper method to allow removal of external tracking bugs using the - ExternalBugs::WebService::remove_external_bug method. - - This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#remove_external_bug - - ids: A single external tracker bug id or list of external tracker bug - ids. - ext_type_id: The external tracker id as used by Bugzilla. - ext_type_description: The external tracker description as used by - Bugzilla. - ext_type_url: The external tracker url as used by Bugzilla. - ext_bz_bug_id: A single external bug id or list of external bug ids - (ie: the bug number in the external tracker). - bug_ids: A single bug id or list of bug ids to have external tracker - info updated. - """ - params = {} - if ids is not None: - params['ids'] = listify(ids) - if ext_type_id is not None: - params['ext_type_id'] = ext_type_id - if ext_type_description is not None: - params['ext_type_description'] = ext_type_description - if ext_type_url is not None: - params['ext_type_url'] = ext_type_url - if ext_bz_bug_id is not None: - params['ext_bz_bug_id'] = listify(ext_bz_bug_id) - if bug_ids is not None: - params['bug_ids'] = listify(bug_ids) - - log.debug("Calling ExternalBugs.remove_external_bug(%s)", params) - return self._backend.externalbugs_remove(params) - ################# # Query methods # From e28881e74057099e99f41d1d06a3cc9f0d7ab3ef Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 10:50:20 -0500 Subject: [PATCH 167/393] rhbugzilla: Move alias handling into BugzillaBase The subclass hackery is kinda crazy, this is a step to unwinding it. Track _is_redhat_bugzilla internally in the base class, and use that to determine whether to use RHBugzilla field aliases or not Signed-off-by: Cole Robinson --- bugzilla/base.py | 116 ++++++++++++++++++++++++++--------------- bugzilla/rhbugzilla.py | 26 +-------- 2 files changed, 74 insertions(+), 68 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index f5484846..ee64f6f5 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -209,9 +209,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._sslverify = sslverify self._cache = _BugzillaAPICache() self._bug_autorefresh = False - - self._field_aliases = [] - self._init_field_aliases() + self._is_redhat_bugzilla = False self._use_creds = use_creds if not self._use_creds: @@ -234,7 +232,6 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, if url: self.connect(url) - self._init_class_state() def _init_class_from_url(self): """ @@ -262,35 +259,54 @@ def _init_class_from_url(self): return self.__class__ = c - c._init_class_state(self) # disable=protected-access + self._is_redhat_bugzilla = True - def _init_class_state(self): - """ - Hook for subclasses to do any __init__ time setup - """ - - def _init_field_aliases(self): + def _get_field_aliases(self): # List of field aliases. Maps old style RHBZ parameter # names to actual upstream values. Used for createbug() and # query include_fields at least. - self._add_field_alias('summary', 'short_desc') - self._add_field_alias('description', 'comment') - self._add_field_alias('platform', 'rep_platform') - self._add_field_alias('severity', 'bug_severity') - self._add_field_alias('status', 'bug_status') - self._add_field_alias('id', 'bug_id') - self._add_field_alias('blocks', 'blockedby') - self._add_field_alias('blocks', 'blocked') - self._add_field_alias('depends_on', 'dependson') - self._add_field_alias('creator', 'reporter') - self._add_field_alias('url', 'bug_file_loc') - self._add_field_alias('dupe_of', 'dupe_id') - self._add_field_alias('dupe_of', 'dup_id') - self._add_field_alias('comments', 'longdescs') - self._add_field_alias('creation_time', 'opendate') - self._add_field_alias('creation_time', 'creation_ts') - self._add_field_alias('whiteboard', 'status_whiteboard') - self._add_field_alias('last_change_time', 'delta_ts') + ret = [] + + def _add(*args, **kwargs): + ret.append(_FieldAlias(*args, **kwargs)) + + def _add_both(newname, origname): + _add(newname, origname, is_api=False) + _add(origname, newname, is_bug=False) + + _add('summary', 'short_desc') + _add('description', 'comment') + _add('platform', 'rep_platform') + _add('severity', 'bug_severity') + _add('status', 'bug_status') + _add('id', 'bug_id') + _add('blocks', 'blockedby') + _add('blocks', 'blocked') + _add('depends_on', 'dependson') + _add('creator', 'reporter') + _add('url', 'bug_file_loc') + _add('dupe_of', 'dupe_id') + _add('dupe_of', 'dup_id') + _add('comments', 'longdescs') + _add('creation_time', 'opendate') + _add('creation_time', 'creation_ts') + _add('whiteboard', 'status_whiteboard') + _add('last_change_time', 'delta_ts') + + if self._is_redhat_bugzilla: + _add_both('fixed_in', 'cf_fixed_in') + _add_both('qa_whiteboard', 'cf_qa_whiteboard') + _add_both('devel_whiteboard', 'cf_devel_whiteboard') + _add_both('internal_whiteboard', 'cf_internal_whiteboard') + + _add('component', 'components', is_bug=False) + _add('version', 'versions', is_bug=False) + # Yes, sub_components is the field name the API expects + _add('sub_components', 'sub_component', is_bug=False) + # flags format isn't exactly the same but it's the closest approx + _add('flags', 'flag_types') + + return ret def _get_user_agent(self): return 'python-bugzilla/%s' % __version__ @@ -311,16 +327,13 @@ def _check_version(self, major, minor): return True return False - def _add_field_alias(self, *args, **kwargs): - self._field_aliases.append(_FieldAlias(*args, **kwargs)) - def _get_bug_aliases(self): return [(f.newname, f.oldname) - for f in self._field_aliases if f.is_bug] + for f in self._get_field_aliases() if f.is_bug] def _get_api_aliases(self): return [(f.newname, f.oldname) - for f in self._field_aliases if f.is_api] + for f in self._get_field_aliases() if f.is_api] ################### @@ -945,7 +958,7 @@ def _convert_fields(_in): if exclude_fields: exclude_fields = _convert_fields(exclude_fields) ret["exclude_fields"] = exclude_fields - if self._supports_getbug_extra_fields: + if self._supports_getbug_extra_fields(): if extra_fields: ret["extra_fields"] = _convert_fields(extra_fields) return ret @@ -963,13 +976,30 @@ def _set_bug_autorefresh(self, val): bug_autorefresh = property(_get_bug_autorefresh, _set_bug_autorefresh) - # getbug_extra_fields: Extra fields that need to be explicitly - # requested from Bug.get in order for the data to be returned. - # - # As of Dec 2012 it seems like only RH bugzilla actually has behavior - # like this, for upstream bz it returns all info for every Bug.get() - _getbug_extra_fields = [] - _supports_getbug_extra_fields = False + def _getbug_extra_fields(self): + """ + Extra fields that need to be explicitly + requested from Bug.get in order for the data to be returned. + """ + rhbz_extra_fields = [ + "comments", "description", + "external_bugs", "flags", "sub_components", + "tags", + ] + if self._is_redhat_bugzilla: + return rhbz_extra_fields + return [] + + def _supports_getbug_extra_fields(self): + """ + Return True if the bugzilla instance supports passing + extra_fields to getbug + + As of Dec 2012 it seems like only RH bugzilla actually has behavior + like this, for upstream bz it returns all info for every Bug.get() + """ + return self._is_redhat_bugzilla + def _getbugs(self, idlist, permissive, include_fields=None, exclude_fields=None, extra_fields=None): @@ -987,7 +1017,7 @@ def _getbugs(self, idlist, permissive, idlist.append(i) extra_fields = listify(extra_fields or []) - extra_fields += self._getbug_extra_fields + extra_fields += self._getbug_extra_fields() getbugdata = {"ids": idlist} if permissive: diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 94fc1a39..d3ebf73b 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -29,31 +29,7 @@ class RHBugzilla(Bugzilla): This class was written using bugzilla.redhat.com's API docs: https://bugzilla.redhat.com/docs/en/html/api/ """ - def _init_class_state(self): - def _add_both_alias(newname, origname): - self._add_field_alias(newname, origname, is_api=False) - self._add_field_alias(origname, newname, is_bug=False) - - _add_both_alias('fixed_in', 'cf_fixed_in') - _add_both_alias('qa_whiteboard', 'cf_qa_whiteboard') - _add_both_alias('devel_whiteboard', 'cf_devel_whiteboard') - _add_both_alias('internal_whiteboard', 'cf_internal_whiteboard') - - self._add_field_alias('component', 'components', is_bug=False) - self._add_field_alias('version', 'versions', is_bug=False) - # Yes, sub_components is the field name the API expects - self._add_field_alias('sub_components', 'sub_component', is_bug=False) - - # flags format isn't exactly the same but it's the closest approx - self._add_field_alias('flags', 'flag_types') - - self._getbug_extra_fields = self._getbug_extra_fields + [ - "comments", "description", - "external_bugs", "flags", "sub_components", - "tags", - ] - self._supports_getbug_extra_fields = True - + _is_redhat_bugzilla = True ###################### # Bug update methods # From 1bc70c4e17efa98aa64541f943e665418f0f9a29 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 11:19:17 -0500 Subject: [PATCH 168/393] rhbugzilla: Turn converter functions into a static class This decouples the class inheritance magic and makes it more clear in Bugzilla() when exactly behavior is changing for rhbz handling Signed-off-by: Cole Robinson --- bugzilla/__init__.py | 3 +- bugzilla/base.py | 83 +++++++++++++++++++++++--------------- bugzilla/oldclasses.py | 18 ++++++++- bugzilla/rhbugzilla.py | 90 ++++++++++++++++-------------------------- 4 files changed, 102 insertions(+), 92 deletions(-) diff --git a/bugzilla/__init__.py b/bugzilla/__init__.py index 26d4de17..95a52cda 100644 --- a/bugzilla/__init__.py +++ b/bugzilla/__init__.py @@ -9,10 +9,9 @@ from .apiversion import version, __version__ from .base import Bugzilla from .exceptions import BugzillaError -from .rhbugzilla import RHBugzilla from .oldclasses import (Bugzilla3, Bugzilla32, Bugzilla34, Bugzilla36, Bugzilla4, Bugzilla42, Bugzilla44, - NovellBugzilla, RHBugzilla3, RHBugzilla4) + NovellBugzilla, RHBugzilla, RHBugzilla3, RHBugzilla4) # This is the public API. If you are explicitly instantiating any other diff --git a/bugzilla/base.py b/bugzilla/base.py index ee64f6f5..cad19057 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -23,6 +23,7 @@ from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl from .bug import Bug, User from .exceptions import BugzillaError +from .rhbugzilla import _RHBugzillaConverters from ._session import _BugzillaSession from ._util import listify @@ -233,33 +234,34 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, if url: self.connect(url) + def _detect_is_redhat_bugzilla(self): + if self._is_redhat_bugzilla: + return True + + if "bugzilla.redhat.com" in self.url: + log.info("Using RHBugzilla for URL containing bugzilla.redhat.com") + return True + + try: + extensions = self._backend.bugzilla_extensions() + if "RedHat" in extensions.get('extensions', {}): + log.info("Found RedHat bugzilla extension, " + "using RHBugzilla") + return True + except Exception: + log.debug("Failed to fetch bugzilla extensions", exc_info=True) + + return False + def _init_class_from_url(self): """ Detect if we should use RHBugzilla class, and if so, set it """ - from .rhbugzilla import RHBugzilla # pylint: disable=cyclic-import - if isinstance(self, RHBugzilla): - return - - c = None - if "bugzilla.redhat.com" in self.url: - log.info("Using RHBugzilla for URL containing bugzilla.redhat.com") - c = RHBugzilla - else: - try: - extensions = self._backend.bugzilla_extensions() - if "RedHat" in extensions.get('extensions', {}): - log.info("Found RedHat bugzilla extension, " - "using RHBugzilla") - c = RHBugzilla - except Exception: - log.debug("Failed to fetch bugzilla extensions", exc_info=True) - - if not c: - return + from .oldclasses import RHBugzilla # pylint: disable=cyclic-import - self.__class__ = c - self._is_redhat_bugzilla = True + if self._detect_is_redhat_bugzilla(): + self.__class__ = RHBugzilla + self._is_redhat_bugzilla = True def _get_field_aliases(self): # List of field aliases. Maps old style RHBZ parameter @@ -1279,12 +1281,18 @@ def pre_translation(self, query): In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function """ + if self._is_redhat_bugzilla: + _RHBugzillaConverters.pre_translation(query) + query.update(self._process_include_fields( + query["include_fields"], None, None)) def post_translation(self, query, bug): """ In order to keep the API the same, Bugzilla4 needs to process the query and the result. This also applies to the refresh() function """ + if self._is_redhat_bugzilla: + _RHBugzillaConverters.post_translation(query, bug) def bugs_history_raw(self, bug_ids): """ @@ -1396,18 +1404,28 @@ def build_update(self, https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug """ ret = {} + rhbzret = {} # These are only supported for rhbugzilla - for key, val in [ - ("fixed_in", fixed_in), - ("devel_whiteboard", devel_whiteboard), - ("qa_whiteboard", qa_whiteboard), - ("internal_whiteboard", internal_whiteboard), - ("sub_component", sub_component), - ]: - if val is not None: - raise ValueError("bugzilla instance does not support " - "updating '%s'" % key) + # + # This should not be extended any more. + # If people want to handle custom fields, manually extend the + # returned dictionary. + rhbzargs = { + "fixed_in": fixed_in, + "devel_whiteboard": devel_whiteboard, + "qa_whiteboard": qa_whiteboard, + "internal_whiteboard": internal_whiteboard, + "sub_component": sub_component, + } + if self._is_redhat_bugzilla: + rhbzret = _RHBugzillaConverters.convert_build_update( + component=component, **rhbzargs) + else: + for key, val in rhbzargs.items(): + if val is not None: + raise ValueError("bugzilla instance does not support " + "updating '%s'" % key) def s(key, val, convert=None): if val is None: @@ -1479,6 +1497,7 @@ def c(val): if comment_private: ret["comment"]["is_private"] = comment_private + ret.update(rhbzret) return ret diff --git a/bugzilla/oldclasses.py b/bugzilla/oldclasses.py index 87da3f10..e579fb9f 100644 --- a/bugzilla/oldclasses.py +++ b/bugzilla/oldclasses.py @@ -2,7 +2,6 @@ # See the COPYING file in the top-level directory. from .base import Bugzilla -from .rhbugzilla import RHBugzilla # These are old compat classes. Nothing new should be added here, # and these should not be altered @@ -40,6 +39,23 @@ class NovellBugzilla(Bugzilla): pass +class RHBugzilla(Bugzilla): + """ + Helper class for historical bugzilla.redhat.com back compat + + Historically this class used many more non-upstream methods, but + in 2012 RH started dropping most of its custom bits. By that time, + upstream BZ had most of the important functionality. + + Much of the remaining code here is just trying to keep things operating + in python-bugzilla back compatible manner. + + This class was written using bugzilla.redhat.com's API docs: + https://bugzilla.redhat.com/docs/en/html/api/ + """ + _is_redhat_bugzilla = True + + class RHBugzilla3(RHBugzilla): pass diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index d3ebf73b..8e8abba8 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -8,56 +8,26 @@ from logging import getLogger -from .base import Bugzilla from ._util import listify log = getLogger(__name__) -class RHBugzilla(Bugzilla): +class _RHBugzillaConverters(object): """ - Bugzilla class for connecting Red Hat's forked bugzilla instance, - bugzilla.redhat.com - - Historically this class used many more non-upstream methods, but - in 2012 RH started dropping most of its custom bits. By that time, - upstream BZ had most of the important functionality. - - Much of the remaining code here is just trying to keep things operating - in python-bugzilla back compatible manner. - - This class was written using bugzilla.redhat.com's API docs: - https://bugzilla.redhat.com/docs/en/html/api/ + Static class that holds functional Red Hat back compat converters. + Called inline in Bugzilla """ - _is_redhat_bugzilla = True - - ###################### - # Bug update methods # - ###################### - - def build_update(self, **kwargs): - # pylint: disable=arguments-differ + @staticmethod + def convert_build_update( + component=None, + fixed_in=None, + qa_whiteboard=None, + devel_whiteboard=None, + internal_whiteboard=None, + sub_component=None): adddict = {} - def pop(key, destkey): - val = kwargs.pop(key, None) - if val is None: - return - adddict[destkey] = val - - def get_sub_component(): - val = kwargs.pop("sub_component", None) - if val is None: - return - - if not isinstance(val, dict): - component = listify(kwargs.get("component")) - if not component: - raise ValueError("component must be specified if " - "specifying sub_component") - val = {component[0]: val} - adddict["sub_components"] = val - def get_alias(): # RHBZ has a custom extension to allow a bug to have multiple # aliases, so the format of aliases is @@ -74,25 +44,35 @@ def get_alias(): # Implementation will go here when it's available pass - pop("fixed_in", "cf_fixed_in") - pop("qa_whiteboard", "cf_qa_whiteboard") - pop("devel_whiteboard", "cf_devel_whiteboard") - pop("internal_whiteboard", "cf_internal_whiteboard") + if fixed_in is not None: + adddict["cf_fixed_in"] = fixed_in + if qa_whiteboard is not None: + adddict["cf_qa_whiteboard"] = qa_whiteboard + if devel_whiteboard is not None: + adddict["cf_devel_whiteboard"] = devel_whiteboard + if internal_whiteboard is not None: + adddict["cf_internal_whiteboard"] = internal_whiteboard + + if sub_component: + if not isinstance(sub_component, dict): + component = listify(component) + if not component: + raise ValueError("component must be specified if " + "specifying sub_component") + sub_component = {component[0]: sub_component} + adddict["sub_components"] = sub_component - get_sub_component() get_alias() - vals = Bugzilla.build_update(self, **kwargs) - vals.update(adddict) - - return vals + return adddict ################# # Query methods # ################# - def pre_translation(self, query): + @staticmethod + def pre_translation(query): """ Translates the query for possible aliases """ @@ -118,15 +98,11 @@ def pre_translation(self, query): query['include_fields'] = query['column_list'] del query['column_list'] - # We need to do this for users here for users that - # don't call build_query - query.update(self._process_include_fields(query["include_fields"], - None, None)) - if old != query: log.debug("RHBugzilla pretranslated query to: %s", query) - def post_translation(self, query, bug): + @staticmethod + def post_translation(query, bug): """ Convert the results of getbug back to the ancient RHBZ value formats From 9410d53b48e290c399821018656781a4be6cf0ca Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 11:21:06 -0500 Subject: [PATCH 169/393] base: Remove ExternalBugs param logging We get this automatically from the xml backend plumbing Signed-off-by: Cole Robinson --- bugzilla/base.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index cad19057..115ff9da 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1907,8 +1907,6 @@ def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, 'bug_ids': listify(bug_ids), 'external_bugs': [param_dict], } - - log.debug("Calling ExternalBugs.add_external_bug(%s)", params) return self._backend.externalbugs_add(params) def update_external_tracker(self, ids=None, ext_type_id=None, @@ -1956,8 +1954,6 @@ def update_external_tracker(self, ids=None, ext_type_id=None, params['ext_description'] = ext_description if ext_priority is not None: params['ext_priority'] = ext_priority - - log.debug("Calling ExternalBugs.update_external_bug(%s)", params) return self._backend.externalbugs_update(params) def remove_external_tracker(self, ids=None, ext_type_id=None, @@ -1994,6 +1990,4 @@ def remove_external_tracker(self, ids=None, ext_type_id=None, params['ext_bz_bug_id'] = listify(ext_bz_bug_id) if bug_ids is not None: params['bug_ids'] = listify(bug_ids) - - log.debug("Calling ExternalBugs.remove_external_bug(%s)", params) return self._backend.externalbugs_remove(params) From 10b7d7ff0ba861a9ba1258ad9b513a19ba6dc3b4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 11:41:35 -0500 Subject: [PATCH 170/393] util: Move _cli to_encoding, and use it in Bug() Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 21 ++------------------- bugzilla/_util.py | 20 ++++++++++++++++++++ bugzilla/bug.py | 9 ++------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 720023a0..ad0d06d1 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -24,7 +24,8 @@ import requests.exceptions import bugzilla -from bugzilla._compatimports import Fault, ProtocolError, urlparse, IS_PY3 +from bugzilla._compatimports import Fault, ProtocolError, urlparse +from bugzilla._util import to_encoding DEFAULT_BZ = 'https://bugzilla.redhat.com' @@ -42,24 +43,6 @@ def _is_unittest_debug(): return bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG")) -def to_encoding(ustring): - string = '' - if IS_PY3: - strtype = (str, bytes) - else: # pragma: no cover - strtype = basestring - if isinstance(ustring, strtype): - string = ustring - elif ustring is not None: - string = str(ustring) - - if IS_PY3: - return string - - preferred = locale.getpreferredencoding() - return string.encode(preferred, 'replace') - - def open_without_clobber(name, *args): """ Try to open the given file with the given mode; if that filename exists, diff --git a/bugzilla/_util.py b/bugzilla/_util.py index 04555779..821cb3c7 100644 --- a/bugzilla/_util.py +++ b/bugzilla/_util.py @@ -1,6 +1,10 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import locale + +from ._compatimports import IS_PY3 + def listify(val): """Ensure that value is either None or a list, converting single values @@ -10,3 +14,19 @@ def listify(val): if isinstance(val, list): return val return [val] + + +def to_encoding(ustring): + """ + Locale specific printing per python version + """ + if ustring is None: + return '' + if IS_PY3: + return str(ustring) + + strtype = basestring # pylint: disable=undefined-variable + string = ustring + if not isinstance(ustring, strtype): + string = str(ustring) + return string.encode(locale.getpreferredencoding(), 'replace') diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 58e23870..a4515ae6 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -8,10 +8,9 @@ from __future__ import unicode_literals -import locale from logging import getLogger -from ._compatimports import IS_PY3 +from ._util import to_encoding log = getLogger(__name__) @@ -52,11 +51,7 @@ def __str__(self): 'print(bug)' is not recommended because of potential encoding issues. Please use unicode(bug) where possible. """ - if IS_PY3: - return self.__unicode__() - else: - return self.__unicode__().encode( - locale.getpreferredencoding(), 'replace') + return to_encoding(self.__unicode__()) def __unicode__(self): """ From cc95e2478cd79b04a30ff0306eb1563f3ae1ce28 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 12:23:17 -0500 Subject: [PATCH 171/393] authfiles: Add _BugzillaCookieCache To wrap up all cookiejar and cookie filename handling into one class Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 63 ++++++++++++++++++++++++++++++------------ bugzilla/_session.py | 18 ++++-------- bugzilla/base.py | 29 ++++++------------- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 2b701650..1c90a7d1 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -136,20 +136,49 @@ def _save_api_key(url, api_key, configpaths): print("API key written to %s" % config_filename) -def _build_cookiejar(cookiefile): - cj = MozillaCookieJar(cookiefile) - if cookiefile is None: - return cj - if not os.path.exists(cookiefile): - # Make sure a new file has correct permissions - open(cookiefile, 'a').close() - os.chmod(cookiefile, 0o600) - cj.save() - return cj - - try: - cj.load() - return cj - except LoadError: - raise BugzillaError("cookiefile=%s not in Mozilla format" % - cookiefile) +class _BugzillaCookieCache(object): + @staticmethod + def get_default_path(): + return _default_cache_location("bugzillacookies") + + def __init__(self): + self._cookiejar = None + + def _build_cookiejar(self, cookiefile): + cj = MozillaCookieJar(cookiefile) + if cookiefile is None: + return cj + if not os.path.exists(cookiefile): + # Make sure a new file has correct permissions + open(cookiefile, 'a').close() + os.chmod(cookiefile, 0o600) + cj.save() + return cj + + try: + cj.load() + return cj + except LoadError: + raise BugzillaError("cookiefile=%s not in Mozilla format" % + cookiefile) + + def set_filename(self, cookiefile): + log.debug("Using cookiefile=%s", cookiefile) + self._cookiejar = self._build_cookiejar(cookiefile) + + def get_filename(self): + return self._cookiejar.filename + + def get_cookiejar(self): + return self._cookiejar + + def set_cookies(self, cookies): + if self._cookiejar is None: + return + + for cookie in cookies: + self._cookiejar.set_cookie(cookie) + + if self._cookiejar.filename is not None: + # Save is required only if we have a filename + self._cookiejar.save() diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 07920db1..c310c1ce 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -18,11 +18,11 @@ class _BugzillaSession(object): Class to handle the backend agnostic 'requests' setup """ def __init__(self, url, user_agent, - cookiejar=None, sslverify=True, cert=None, + cookiecache=None, sslverify=True, cert=None, tokenfile=None, api_key=None): self._user_agent = user_agent self._scheme = urlparse(url)[0] - self._cookiejar = cookiejar + self._cookiecache = cookiecache self._token_cache = _BugzillaTokenCache(url, tokenfile) self._api_key = api_key @@ -33,8 +33,8 @@ def __init__(self, url, user_agent, self._session = requests.Session() if cert: self._session.cert = cert - if self._cookiejar: - self._session.cookies = self._cookiejar + if self._cookiecache: + self._session.cookies = self._cookiecache.get_cookiejar() self._session.verify = sslverify self._session.headers["User-Agent"] = self._user_agent @@ -71,15 +71,7 @@ def set_response_cookies(self, response): """ Save any cookies received from the passed requests response """ - if self._cookiejar is None: - return - - for cookie in response.cookies: - self._cookiejar.set_cookie(cookie) - - if self._cookiejar.filename is not None: - # Save is required only if we have a filename - self._cookiejar.save() + self._cookiecache.set_cookies(response.cookies) def get_requests_session(self): return self._session diff --git a/bugzilla/base.py b/bugzilla/base.py index 115ff9da..ddb7f514 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -16,7 +16,7 @@ from io import BytesIO from ._authfiles import (DEFAULT_CONFIGPATHS, open_bugzillarc, - _build_cookiejar, _default_cache_location, + _BugzillaCookieCache, _default_cache_location, _parse_hostname, _save_api_key) from .apiversion import __version__ from ._backendxmlrpc import _BackendXMLRPC @@ -206,11 +206,12 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._backend = None self._session = None - self._cookiejar = None + self._cookiecache = None self._sslverify = sslverify self._cache = _BugzillaAPICache() self._bug_autorefresh = False self._is_redhat_bugzilla = False + self._cookiecache = _BugzillaCookieCache() self._use_creds = use_creds if not self._use_creds: @@ -219,14 +220,14 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, configpaths = [] if cookiefile == -1: - cookiefile = _default_cache_location("bugzillacookies") + cookiefile = self._cookiecache.get_default_path() if tokenfile == -1: tokenfile = _default_cache_location("bugzillatoken") if configpaths == -1: configpaths = DEFAULT_CONFIGPATHS[:] log.debug("Using tokenfile=%s", tokenfile) - self.cookiefile = cookiefile + self._setcookiefile(cookiefile) self.tokenfile = tokenfile self.configpath = configpaths self._basic_auth = basic_auth @@ -343,25 +344,13 @@ def _get_api_aliases(self): ################### def _getcookiefile(self): - """ - cookiefile is the file that bugzilla session cookies are loaded - and saved from. - """ - return self._cookiejar.filename + return self._cookiecache.get_filename() def _delcookiefile(self): - self._cookiejar = None + self._setcookiefile(None) def _setcookiefile(self, cookiefile): - if (self._cookiejar and cookiefile == self._cookiejar.filename): - return - - if self._backend is not None: - raise RuntimeError("Can't set cookies with an open connection, " - "disconnect() first.") - - log.debug("Using cookiefile=%s", cookiefile) - self._cookiejar = _build_cookiejar(cookiefile) + self._cookiecache.set_filename(cookiefile) cookiefile = property(_getcookiefile, _setcookiefile, _delcookiefile) @@ -482,7 +471,7 @@ def connect(self, url=None): self.readconfig(overwrite=False) self._session = _BugzillaSession(url, self.user_agent, - cookiejar=self._cookiejar, + cookiecache=self._cookiecache, sslverify=self._sslverify, cert=self.cert, tokenfile=self.tokenfile, From 2e3717b26a82721ec901a70c2204c45468b15f00 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 12:38:01 -0500 Subject: [PATCH 172/393] authfiles: Move more token handling into BugzillaTokenCache So that is the only API piece that internal code will interact with Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 52 +++++++++++++++++++++++++++--------------- bugzilla/_session.py | 19 +++++++-------- bugzilla/base.py | 22 +++++++++++++----- 3 files changed, 59 insertions(+), 34 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 1c90a7d1..2aa21261 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -39,38 +39,51 @@ class _BugzillaTokenCache(object): """ Class for interacting with a .bugzillatoken cache file """ - def __init__(self, uri, filename): - self._filename = filename - self._cfg = ConfigParser() - self._domain = urlparse(uri)[1] - - if self._filename: - self._cfg.read(self._filename) - - if self._domain not in self._cfg.sections(): - self._cfg.add_section(self._domain) + @staticmethod + def get_default_path(): + return _default_cache_location("bugzillatoken") - def get_value(self): - if self._cfg.has_option(self._domain, 'token'): - return self._cfg.get(self._domain, 'token') + def __init__(self): + self._filename = None + self._cfg = None + + def _get_domain(self, url): + domain = urlparse(url)[1] + if domain not in self._cfg.sections(): + self._cfg.add_section(domain) + return domain + + def get_value(self, url): + domain = self._get_domain(url) + if self._cfg.has_option(domain, 'token'): + return self._cfg.get(domain, 'token') return None - def set_value(self, value): - if self.get_value() == value: + def set_value(self, url, value): + if self.get_value(url) == value: return + domain = self._get_domain(url) if value is None: - self._cfg.remove_option(self._domain, 'token') + self._cfg.remove_option(domain, 'token') else: - self._cfg.set(self._domain, 'token', value) + self._cfg.set(domain, 'token', value) if self._filename: with open(self._filename, 'w') as _cfg: log.debug("Saving to _cfg") self._cfg.write(_cfg) - def __repr__(self): - return '' % self.get_value() + def get_filename(self): + return self._filename + + def set_filename(self, filename): + log.debug("Using tokenfile=%s", filename) + cfg = ConfigParser() + if filename: + cfg.read(filename) + self._filename = filename + self._cfg = cfg def _parse_hostname(url): @@ -112,6 +125,7 @@ def _save_api_key(url, api_key, configpaths): Save the API_KEY in the config file. If tokenfile and cookiefile are undefined, it means that the + API was called with --no-cache-credentials and no change will be made """ diff --git a/bugzilla/_session.py b/bugzilla/_session.py index c310c1ce..91218597 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -6,7 +6,6 @@ import requests -from ._authfiles import _BugzillaTokenCache from ._compatimports import urlparse @@ -19,11 +18,12 @@ class _BugzillaSession(object): """ def __init__(self, url, user_agent, cookiecache=None, sslverify=True, cert=None, - tokenfile=None, api_key=None): + tokencache=None, api_key=None): + self._url = url self._user_agent = user_agent self._scheme = urlparse(url)[0] self._cookiecache = cookiecache - self._token_cache = _BugzillaTokenCache(url, tokenfile) + self._tokencache = tokencache self._api_key = api_key if self._scheme not in ["http", "https"]: @@ -39,7 +39,7 @@ def __init__(self, url, user_agent, self._session.verify = sslverify self._session.headers["User-Agent"] = self._user_agent self._session.params["Bugzilla_api_key"] = self._api_key - self._set_token_cache_param() + self._set_tokencache_param() def get_user_agent(self): return self._user_agent @@ -48,15 +48,16 @@ def get_scheme(self): def get_api_key(self): return self._api_key def get_token_value(self): - return self._token_cache.get_value() + return self._tokencache.get_value(self._url) def set_token_value(self, value): - self._token_cache.set_value(value) - self._set_token_cache_param() + self._tokencache.set_value(self._url, value) + self._set_tokencache_param() def set_content_type(self, value): self._session.headers["Content-Type"] = value - def _set_token_cache_param(self): - self._session.params["Bugzilla_token"] = self._token_cache.get_value() + def _set_tokencache_param(self): + token = self.get_token_value() + self._session.params["Bugzilla_token"] = token def set_basic_auth(self, user, password): """ diff --git a/bugzilla/base.py b/bugzilla/base.py index ddb7f514..e21585c8 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -16,7 +16,7 @@ from io import BytesIO from ._authfiles import (DEFAULT_CONFIGPATHS, open_bugzillarc, - _BugzillaCookieCache, _default_cache_location, + _BugzillaCookieCache, _BugzillaTokenCache, _parse_hostname, _save_api_key) from .apiversion import __version__ from ._backendxmlrpc import _BackendXMLRPC @@ -211,7 +211,9 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._cache = _BugzillaAPICache() self._bug_autorefresh = False self._is_redhat_bugzilla = False + self._cookiecache = _BugzillaCookieCache() + self._tokencache = _BugzillaTokenCache() self._use_creds = use_creds if not self._use_creds: @@ -222,13 +224,13 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, if cookiefile == -1: cookiefile = self._cookiecache.get_default_path() if tokenfile == -1: - tokenfile = _default_cache_location("bugzillatoken") + tokenfile = self._tokencache.get_default_path() if configpaths == -1: configpaths = DEFAULT_CONFIGPATHS[:] - log.debug("Using tokenfile=%s", tokenfile) self._setcookiefile(cookiefile) - self.tokenfile = tokenfile + self._settokenfile(tokenfile) + self.configpath = configpaths self._basic_auth = basic_auth @@ -340,7 +342,7 @@ def _get_api_aliases(self): ################### - # Cookie handling # + # Auth handling # ################### def _getcookiefile(self): @@ -354,6 +356,14 @@ def _setcookiefile(self, cookiefile): cookiefile = property(_getcookiefile, _setcookiefile, _delcookiefile) + def _gettokenfile(self): + return self._tokencache.get_filename() + def _settokenfile(self, filename): + self._tokencache.set_filename(filename) + def _deltokenfile(self): + self._settokenfile(None) + tokenfile = property(_gettokenfile, _settokenfile, _deltokenfile) + ############################# # Login/connection handling # @@ -474,7 +484,7 @@ def connect(self, url=None): cookiecache=self._cookiecache, sslverify=self._sslverify, cert=self.cert, - tokenfile=self.tokenfile, + tokencache=self._tokencache, api_key=self.api_key) backendclass = self._get_backend_class() self._backend = backendclass(url, self._session) From efdb4f00cfee25eecff743c66eb8f0ff5077aa6c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 9 Jan 2020 13:14:04 -0500 Subject: [PATCH 173/393] authfiles: Add a BugzillaRCFile class Provides a central way to interact with the RC files. Convert a bunch of usage to use explicit APIs for it Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 149 +++++++++++++++++++++++++++-------------- bugzilla/_cli.py | 18 +---- bugzilla/base.py | 67 +++++++++--------- 3 files changed, 131 insertions(+), 103 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 2aa21261..038bd5ae 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -11,28 +11,107 @@ log = getLogger(__name__) -DEFAULT_CONFIGPATHS = [ - '/etc/bugzillarc', - '~/.bugzillarc', - '~/.config/python-bugzilla/bugzillarc', -] +def _parse_hostname(url): + # If http://example.com is passed, netloc=example.com path="" + # If just example.com is passed, netloc="" path=example.com + parsedbits = urlparse(url) + return parsedbits.netloc or parsedbits.path -def open_bugzillarc(configpaths=-1): - if configpaths == -1: - configpaths = DEFAULT_CONFIGPATHS[:] - # pylint: disable=protected-access - configpaths = [os.path.expanduser(p) for p in - listify(configpaths)] - # pylint: enable=protected-access - cfg = ConfigParser() - read_files = cfg.read(configpaths) - if not read_files: - return +def _default_location(filename, kind): + """ + Determine default location for filename, like 'bugzillacookies'. If + old style ~/.bugzillacookies exists, we use that, otherwise we + use ~/.cache/python-bugzilla/bugzillacookies. + Same for bugzillatoken and bugzillarc + """ + homepath = os.path.expanduser("~/.%s" % filename) + xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) + if os.path.exists(xdgpath): + return xdgpath + if os.path.exists(homepath): + return homepath - log.info("Found bugzillarc files: %s", read_files) - return cfg + if not os.path.exists(os.path.dirname(xdgpath)): + os.makedirs(os.path.dirname(xdgpath), 0o700) + return xdgpath + + +def _default_cache_location(filename): + return _default_location(filename, 'cache') + + +def _default_config_location(filename): + return _default_location(filename, 'config') + + +class _BugzillaRCFile(object): + """ + Helper class for interacting with bugzillarc files + """ + @staticmethod + def get_default_configpaths(): + paths = [ + '/etc/bugzillarc', + '~/.bugzillarc', + '~/.config/python-bugzilla/bugzillarc', + ] + return paths + + def __init__(self): + self._cfg = None + self._configpaths = None + self.set_configpaths(None) + + def set_configpaths(self, configpaths): + configpaths = [os.path.expanduser(p) for p in + listify(configpaths or [])] + + cfg = ConfigParser() + read_files = cfg.read(configpaths) + if read_files: + log.info("Found bugzillarc files: %s", read_files) + + self._cfg = cfg + self._configpaths = configpaths or [] + + def get_configpaths(self): + return self._configpaths[:] + + def get_default_url(self): + """ + Grab a default URL from bugzillarc [DEFAULT] url=X + """ + cfgurl = self._cfg.defaults().get("url", None) + if cfgurl is not None: + log.debug("bugzillarc: found cli url=%s", cfgurl) + return cfgurl + + def parse(self, url): + """ + Find the section for the passed URL domain, and return all the fields + """ + section = "" + log.debug("bugzillarc: Searching for config section matching %s", url) + + urlhost = _parse_hostname(url) + for sectionhost in sorted(self._cfg.sections()): + # If the section is just a hostname, make it match + # If the section has a / in it, do a substring match + if "/" not in sectionhost: + if sectionhost == urlhost: + section = sectionhost + elif sectionhost in url: + section = sectionhost + if section: + log.debug("bugzillarc: Found matching section: %s", section) + break + + if not section: + log.debug("bugzillarc: No section found") + return {} + return dict(self._cfg.items(section)) class _BugzillaTokenCache(object): @@ -86,40 +165,6 @@ def set_filename(self, filename): self._cfg = cfg -def _parse_hostname(url): - # If http://example.com is passed, netloc=example.com path="" - # If just example.com is passed, netloc="" path=example.com - parsedbits = urlparse(url) - return parsedbits.netloc or parsedbits.path - - -def _default_location(filename, kind): - """ - Determine default location for filename, like 'bugzillacookies'. If - old style ~/.bugzillacookies exists, we use that, otherwise we - use ~/.cache/python-bugzilla/bugzillacookies. - Same for bugzillatoken and bugzillarc - """ - homepath = os.path.expanduser("~/.%s" % filename) - xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) - if os.path.exists(xdgpath): - return xdgpath - if os.path.exists(homepath): - return homepath - - if not os.path.exists(os.path.dirname(xdgpath)): - os.makedirs(os.path.dirname(xdgpath), 0o700) - return xdgpath - - -def _default_cache_location(filename): - return _default_location(filename, 'cache') - - -def _default_config_location(filename): - return _default_location(filename, 'config') - - def _save_api_key(url, api_key, configpaths): """ Save the API_KEY in the config file. diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index ad0d06d1..e210cd0b 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -66,20 +66,6 @@ def open_without_clobber(name, *args): return fobj -def get_default_url(): - """ - Grab a default URL from bugzillarc [DEFAULT] url=X - """ - from bugzilla._authfiles import open_bugzillarc - cfg = open_bugzillarc() - if cfg: - cfgurl = cfg.defaults().get("url", None) - if cfgurl is not None: - log.debug("bugzillarc: found cli url=%s", cfgurl) - return cfgurl - return DEFAULT_BZ - - def setup_logging(debug, verbose): handler = StreamHandler(sys.stderr) handler.setFormatter(Formatter( @@ -106,7 +92,9 @@ def _setup_root_parser(): epilog = 'Try "bugzilla COMMAND --help" for command-specific help.' p = argparse.ArgumentParser(epilog=epilog) - default_url = get_default_url() + default_url = bugzilla.Bugzilla.get_rcfile_default_url() + if not default_url: + default_url = DEFAULT_BZ # General bugzilla connection options p.add_argument('--bugzilla', default=default_url, diff --git a/bugzilla/base.py b/bugzilla/base.py index e21585c8..59aa5b2d 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -15,9 +15,8 @@ from io import BytesIO -from ._authfiles import (DEFAULT_CONFIGPATHS, open_bugzillarc, - _BugzillaCookieCache, _BugzillaTokenCache, - _parse_hostname, _save_api_key) +from ._authfiles import (_BugzillaRCFile, + _BugzillaCookieCache, _BugzillaTokenCache, _save_api_key) from .apiversion import __version__ from ._backendxmlrpc import _BackendXMLRPC from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl @@ -161,6 +160,18 @@ def fix_url(url): log.debug("Generated fixed URL: %s", newurl) return newurl + @staticmethod + def get_rcfile_default_url(): + """ + Helper to check all the default bugzillarc file paths for + a [DEFAULT] url=X section, and if found, return it. + """ + configpaths = _BugzillaRCFile.get_default_configpaths() + rcfile = _BugzillaRCFile() + rcfile.set_configpaths(configpaths) + return rcfile.get_default_url() + + def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None, cert=None, configpaths=-1, basic_auth=False): @@ -206,12 +217,12 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._backend = None self._session = None - self._cookiecache = None self._sslverify = sslverify self._cache = _BugzillaAPICache() self._bug_autorefresh = False self._is_redhat_bugzilla = False + self._rcfile = _BugzillaRCFile() self._cookiecache = _BugzillaCookieCache() self._tokencache = _BugzillaTokenCache() @@ -226,12 +237,12 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, if tokenfile == -1: tokenfile = self._tokencache.get_default_path() if configpaths == -1: - configpaths = DEFAULT_CONFIGPATHS[:] + configpaths = _BugzillaRCFile.get_default_configpaths() self._setcookiefile(cookiefile) self._settokenfile(tokenfile) + self._setconfigpath(configpaths) - self.configpath = configpaths self._basic_auth = basic_auth if url: @@ -341,19 +352,16 @@ def _get_api_aliases(self): for f in self._get_field_aliases() if f.is_api] - ################### + ################# # Auth handling # - ################### + ################# def _getcookiefile(self): return self._cookiecache.get_filename() - def _delcookiefile(self): self._setcookiefile(None) - def _setcookiefile(self, cookiefile): self._cookiecache.set_filename(cookiefile) - cookiefile = property(_getcookiefile, _setcookiefile, _delcookiefile) def _gettokenfile(self): @@ -364,6 +372,14 @@ def _deltokenfile(self): self._settokenfile(None) tokenfile = property(_gettokenfile, _settokenfile, _deltokenfile) + def _getconfigpath(self): + return self._rcfile.get_configpaths() + def _setconfigpath(self, configpaths): + return self._rcfile.set_configpaths(configpaths) + def _delconfigpath(self): + return self._rcfile.set_configpaths(None) + configpath = property(_getconfigpath, _setconfigpath, _delconfigpath) + ############################# # Login/connection handling # @@ -402,32 +418,11 @@ def readconfig(self, configpath=None, overwrite=True): :param overwrite: If True, bugzillarc will clobber any already set self.user/password/api_key/cert value. """ - cfg = open_bugzillarc(configpath or self.configpath) - if not cfg: - return - - section = "" - log.debug("bugzillarc: Searching for config section matching %s", - self.url) - - urlhost = _parse_hostname(self.url) - for sectionhost in sorted(cfg.sections()): - # If the section is just a hostname, make it match - # If the section has a / in it, do a substring match - if "/" not in sectionhost: - if sectionhost == urlhost: - section = sectionhost - elif sectionhost in self.url: - section = sectionhost - if section: - log.debug("bugzillarc: Found matching section: %s", section) - break - - if not section: - log.debug("bugzillarc: No section found") - return + if configpath: + self._setconfigpath(configpath) + data = self._rcfile.parse(self.url) - for key, val in cfg.items(section): + for key, val in data.items(): if key == "api_key" and (overwrite or not self.api_key): log.debug("bugzillarc: setting api_key") self.api_key = val From 4e80f1ab71f7c27747581400b520e422fabec2e5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 09:29:34 -0500 Subject: [PATCH 174/393] authfiles: Drop handling of ~/.bugzillacookies and ~/.bugzillatokens We have been preferring ~/.cache locations since 2.0.0 release, and token and cookie support is on the way out in bugzilla anyways so the effects should be minimal Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 038bd5ae..a05d0cf9 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -21,18 +21,10 @@ def _parse_hostname(url): def _default_location(filename, kind): """ - Determine default location for filename, like 'bugzillacookies'. If - old style ~/.bugzillacookies exists, we use that, otherwise we - use ~/.cache/python-bugzilla/bugzillacookies. - Same for bugzillatoken and bugzillarc + Determine default location for passed filename and xdg kind, + example: ~/.cache/python-bugzilla/bugzillacookies """ - homepath = os.path.expanduser("~/.%s" % filename) xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) - if os.path.exists(xdgpath): - return xdgpath - if os.path.exists(homepath): - return homepath - if not os.path.exists(os.path.dirname(xdgpath)): os.makedirs(os.path.dirname(xdgpath), 0o700) return xdgpath From d6fd480cf62a6ee0852c2ba6ceb83fa9ffe0fdf4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 11:15:37 -0500 Subject: [PATCH 175/393] tests: Run unittests with custom HOME Signed-off-by: Cole Robinson --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 68b601f2..b002d941 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,6 +66,11 @@ def pytest_configure(config): if config.getoption("--regenerate-output"): tests.CLICONFIG.REGENERATE_OUTPUT = config.getoption( "--regenerate-output") + if not (config.getoption("--ro-functional") or + config.getoption("--rw-functional")): + # Functional tests need access to HOME cached auth. + # Unit tests shouldn't be touching any HOME files + os.environ["HOME"] = os.path.dirname(__file__) + "/data/homedir" @pytest.fixture From c10a673f691c9a841fec8f2ddc79e7ca5863a7fd Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 11:22:37 -0500 Subject: [PATCH 176/393] authfiles: Only create root directories at file save time Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index a05d0cf9..fd3c8263 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -19,14 +19,18 @@ def _parse_hostname(url): return parsedbits.netloc or parsedbits.path +def _makedirs(path): + if os.path.exists(os.path.dirname(path)): + return + os.makedirs(os.path.dirname(path), 0o700) + + def _default_location(filename, kind): """ Determine default location for passed filename and xdg kind, example: ~/.cache/python-bugzilla/bugzillacookies """ xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) - if not os.path.exists(os.path.dirname(xdgpath)): - os.makedirs(os.path.dirname(xdgpath), 0o700) return xdgpath @@ -141,6 +145,7 @@ def set_value(self, url, value): self._cfg.set(domain, 'token', value) if self._filename: + _makedirs(self._filename) with open(self._filename, 'w') as _cfg: log.debug("Saving to _cfg") self._cfg.write(_cfg) @@ -180,6 +185,7 @@ def _save_api_key(url, api_key, configpaths): cfg.set(section, 'api_key', api_key.strip()) + _makedirs(config_filename) with open(config_filename, 'w') as configfile: cfg.write(configfile) @@ -197,13 +203,8 @@ def __init__(self): def _build_cookiejar(self, cookiefile): cj = MozillaCookieJar(cookiefile) - if cookiefile is None: - return cj - if not os.path.exists(cookiefile): - # Make sure a new file has correct permissions - open(cookiefile, 'a').close() - os.chmod(cookiefile, 0o600) - cj.save() + if (cookiefile is None or + not os.path.exists(cookiefile)): return cj try: @@ -224,12 +225,17 @@ def get_cookiejar(self): return self._cookiejar def set_cookies(self, cookies): - if self._cookiejar is None: - return - for cookie in cookies: self._cookiejar.set_cookie(cookie) - if self._cookiejar.filename is not None: - # Save is required only if we have a filename - self._cookiejar.save() + cookiefile = self._cookiejar.filename + if not cookiefile: + return + + if not os.path.exists(cookiefile): + _makedirs(cookiefile) + # Make sure a new file has correct permissions + open(cookiefile, 'a').close() + os.chmod(cookiefile, 0o600) + + self._cookiejar.save() From 1d98afac0dfe32921604e2c37dd754bdd0b278d7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 11:36:22 -0500 Subject: [PATCH 177/393] tests: split apart test_cli_commands into one file per command Signed-off-by: Cole Robinson --- tests/test_cli_attach.py | 94 +++++++++ tests/test_cli_commands.py | 407 ------------------------------------- tests/test_cli_info.py | 90 ++++++++ tests/test_cli_login.py | 69 +++++++ tests/test_cli_modify.py | 55 +++++ tests/test_cli_new.py | 26 +++ tests/test_cli_query.py | 105 ++++++++++ 7 files changed, 439 insertions(+), 407 deletions(-) create mode 100644 tests/test_cli_attach.py delete mode 100644 tests/test_cli_commands.py create mode 100644 tests/test_cli_info.py create mode 100644 tests/test_cli_login.py create mode 100644 tests/test_cli_modify.py create mode 100644 tests/test_cli_new.py create mode 100644 tests/test_cli_query.py diff --git a/tests/test_cli_attach.py b/tests/test_cli_attach.py new file mode 100644 index 00000000..e0427668 --- /dev/null +++ b/tests/test_cli_attach.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import os + +import tests +import tests.mockbackend +import tests.utils + + +################################## +# 'bugzilla attach' mock testing # +################################## + +def test_attach(run_cli): + attachfile = os.path.dirname(__file__) + "/data/bz-attach-get1.txt" + attachcontent = open(attachfile).read() + + # Hit error when no ID specified + fakebz = tests.mockbackend.make_bz() + out = run_cli("bugzilla attach", fakebz, expectfail=True) + assert "ID must be specified" in out + + # Hit error when using tty and no --file specified + out = run_cli("bugzilla attach 123456", fakebz, expectfail=True) + assert "--file must be specified" in out + + # Hit error when using stdin, but no --desc + out = run_cli("bugzilla attach 123456", fakebz, expectfail=True, + stdin=attachcontent) + assert "--description must be specified" in out + + # Basic CLI attach + cmd = "bugzilla attach 123456 --file=%s " % attachfile + cmd += "--type text/x-patch --private " + cmd += "--comment 'some comment to go with it'" + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach1.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz) + assert "Created attachment 1557949 on bug 123456" in out + + # Attach from stdin + cmd = "bugzilla attach 123456 --file=fake-file-name.txt " + cmd += "--description 'Some attachment description' " + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach2.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz, stdin=attachcontent) + assert "Created attachment 1557949 on bug 123456" in out + + +def _test_attach_get(run_cli): + # Hit error when using ids with --get* + fakebz = tests.mockbackend.make_bz() + out = run_cli("bugzilla attach 123456 --getall 123456", + fakebz, expectfail=True) + assert "not used for" in out + + # Basic --get ATTID usage + filename = u"Klíč memorial test file.txt" + cmd = "bugzilla attach --get 112233" + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_args="data/mockargs/test_attach_get1.txt", + bug_attachment_get_return="data/mockreturn/test_attach_get1.txt") + out = run_cli(cmd, fakebz) + assert filename in out + + # Basic --getall with --ignore-obsolete + cmd = "bugzilla attach --getall 663674 --ignore-obsolete" + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_all_args="data/mockargs/test_attach_get2.txt", + bug_attachment_get_all_return="data/mockreturn/test_attach_get2.txt") + out = run_cli(cmd, fakebz) + + os.system("ls %s" % os.getcwd()) + filename += ".1" + assert filename in out + assert "bugzilla-filename" in out + + +def test_attach_get(run_cli): + import tempfile + import shutil + tmpdir = tempfile.mkdtemp(dir=os.getcwd()) + origcwd = os.getcwd() + os.chdir(tmpdir) + try: + _test_attach_get(run_cli) + finally: + os.chdir(origcwd) + shutil.rmtree(tmpdir) diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py deleted file mode 100644 index 9e20dc9b..00000000 --- a/tests/test_cli_commands.py +++ /dev/null @@ -1,407 +0,0 @@ -# -*- coding: utf-8 -*- - -# This work is licensed under the GNU GPLv2 or later. -# See the COPYING file in the top-level directory. - -import os -import re - -import pytest - -import bugzilla - -import tests -import tests.mockbackend -import tests.utils - - -################################# -# 'bugzilla login' mock testing # -################################# - -def test_login(run_cli): - cmd = "bugzilla login FOO BAR" - - fakebz = tests.mockbackend.make_bz( - user_login_args="data/mockargs/test_login.txt", - user_login_return=RuntimeError("TEST ERROR")) - out = run_cli(cmd, fakebz, expectfail=True) - assert "Login failed: TEST ERROR" in out - - fakebz = tests.mockbackend.make_bz( - user_login_args="data/mockargs/test_login.txt", - user_login_return={}) - out = run_cli(cmd, fakebz) - assert "Login successful" in out - - cmd = "bugzilla --restrict-login --user FOO --password BAR login" - fakebz = tests.mockbackend.make_bz( - user_login_args="data/mockargs/test_login-restrict.txt", - user_login_return={}) - out = run_cli(cmd, fakebz) - assert "Login successful" in out - - cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" - # Raises raw error trying to see if we aren't logged in - with pytest.raises(NotImplementedError): - fakebz = tests.mockbackend.make_bz( - user_login_args="data/mockargs/test_login.txt", - user_login_return={}, - user_get_args=None, - user_get_return=NotImplementedError()) - out = run_cli(cmd, fakebz) - - # Errors with expected code - cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" - fakebz = tests.mockbackend.make_bz( - user_login_args="data/mockargs/test_login.txt", - user_login_return={}, - user_get_args=None, - user_get_return=bugzilla.BugzillaError("TESTMESSAGE", code=505)) - out = run_cli(cmd, fakebz, expectfail=True) - assert "--ensure-logged-in passed but you" in out - - # Returns success for logged_in check and hits a tokenfile line - cmd = "bugzilla --ensure-logged-in " - cmd += "--user FOO --password BAR login" - fakebz = tests.mockbackend.make_bz( - bz_kwargs={"use_creds": True}, - user_login_args="data/mockargs/test_login.txt", - user_login_return={}, - user_get_args=None, - user_get_return={}) - out = run_cli(cmd, fakebz) - assert "token cache updated" in out - - -################################ -# 'bugzilla info' mock testing # -################################ - -def test_info(run_cli): - funcname = tests.utils.get_funcname() - argsprefix = "data/mockargs/%s_" % funcname - cliprefix = "data/clioutput/%s_" % funcname - - prod_accessible = {'ids': [1, 7]} - prod_get = {'products': [ - {'id': 1, 'name': 'Prod 1 Test'}, - {'id': 7, 'name': 'test-fake-product'} - ]} - - # info --products - fakebz = tests.mockbackend.make_bz( - product_get_accessible_args=None, - product_get_accessible_return=prod_accessible, - product_get_args=argsprefix + "products.txt", - product_get_return=prod_get) - cmd = "bugzilla info --products" - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, cliprefix + "products.txt") - - # info --versions - prod_get_ver = {'products': [ - {'id': 7, 'name': 'test-fake-product', - 'versions': [ - {'id': 360, 'is_active': True, 'name': '7.1'}, - {'id': 123, 'is_active': True, 'name': 'fooversion!'}, - ]}, - ]} - fakebz = tests.mockbackend.make_bz( - product_get_args=argsprefix + "versions.txt", - product_get_return=prod_get_ver) - cmd = "bugzilla info --versions test-fake-product" - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, cliprefix + "versions.txt") - - # info --components - legal_values = {'values': ["comp1", "test-comp-2", "hey-imma-comp"]} - cmd = "bugzilla info --components test-fake-product" - fakebz = tests.mockbackend.make_bz( - product_get_args=argsprefix + "components.txt", - product_get_return=prod_get, - bug_legal_values_args=argsprefix + "components-legalvalues.txt", - bug_legal_values_return=legal_values) - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, cliprefix + "components.txt") - - # info --components --active-components - cmd = "bugzilla info --components test-fake-product --active-components" - prod_get_comp_active = {'products': [ - {'id': 7, 'name': 'test-fake-product', - 'components': [ - {'is_active': True, 'name': 'backend/kernel'}, - {'is_active': True, 'name': 'client-interfaces'}, - ]}, - ]} - fakebz = tests.mockbackend.make_bz( - product_get_args=argsprefix + "components-active.txt", - product_get_return=prod_get_comp_active) - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, cliprefix + "components-active.txt") - - # info --components_owners - cmd = "bugzilla info --component_owners test-fake-product" - prod_get_comp_owners = {'products': [ - {'id': 7, 'name': 'test-fake-product', - 'components': [ - {'default_assigned_to': 'Fake Guy', - 'name': 'client-interfaces'}, - {'default_assigned_to': 'ANother fake dude!', - 'name': 'configuration'}, - ]}, - ]} - fakebz = tests.mockbackend.make_bz( - product_get_args=argsprefix + "components-owners.txt", - product_get_return=prod_get_comp_owners) - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, cliprefix + "components-owners.txt") - - -################################# -# 'bugzilla query' mock testing # -################################# - -def test_query(run_cli): - # query that ends up empty - cmd = "bugzilla query --ids " - fakebz = tests.mockbackend.make_bz(version="3.0.0") - out = run_cli(cmd, fakebz, expectfail=True) - assert "requires additional arguments" in out - - # bad field option - cmd = "bugzilla query --field FOO" - out = run_cli(cmd, fakebz, expectfail=True) - assert "Invalid field argument" in out - - # Simple query with some comma opts - cmd = "bugzilla query " - cmd += "--product foo --component foo,bar --bug_id 1234,2480" - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query1.txt", - bug_search_return="data/mockreturn/test_query1.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_query1.txt") - - # Same but with --ids output - cmd = "bugzilla query --ids " - cmd += "--product foo --component foo,bar --bug_id 1234,2480" - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query1-ids.txt", - bug_search_return="data/mockreturn/test_query1.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_query1-ids.txt") - - # Same but with --raw output - cmd = "bugzilla query --raw --bug_id 1165434" - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query2.txt", - bug_search_return={"bugs": [{"id": 1165434}]}, - bug_get_args=None, - bug_get_return="data/mockreturn/test_getbug_rhel.txt") - out = run_cli(cmd, fakebz) - # Dictionary ordering is random, so scrub it from our output - out = re.sub(r"\{.*\}", r"'DICT SCRUBBED'", out, re.MULTILINE) - tests.utils.diff_compare(out, "data/clioutput/test_query2.txt") - - # Test a bunch of different combinations for code coverage - cmd = "bugzilla query --status ALL --severity sev1,sev2 " - cmd += "--outputformat='%{foo}:%{bar}::%{whiteboard}:" - cmd += "%{flags}:%{flags_requestee}%{whiteboard:devel}::" - cmd += "%{flag:needinfo}::%{comments}::%{external_bugs}'" - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query3.txt", - bug_search_return="data/mockreturn/test_getbug_rhel.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_query3.txt") - - # Test --status DEV and --full - cmd = "bugzilla query --status DEV --full" - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query4.txt", - bug_search_return="data/mockreturn/test_getbug_rhel.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_query4.txt") - - # Test --status QE and --extra, and components-file - compfile = os.path.dirname(__file__) + "/data/components_file.txt" - cmd = "bugzilla query --status QE --extra " - cmd += "--components_file %s" % compfile - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query5.txt", - bug_search_return="data/mockreturn/test_getbug_rhel.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_query5.txt") - - # Test --status EOL and --oneline, and some --field usage - cmd = "bugzilla query --status EOL --oneline " - cmd += "--field FOO=1 --field=BAR=WIBBLE " - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query6.txt", - bug_search_return="data/mockreturn/test_getbug_rhel.txt", - bug_get_args="data/mockargs/test_query_cve_getbug.txt", - bug_get_return="data/mockreturn/test_query_cve_getbug.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_query6.txt") - - # Test --status OPEN and --from-url - url = "https://bugzilla.redhat.com/buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=RELEASE_PENDING&bug_status=POST&classification=Fedora&component=virt-manager&order=bug_status%2Cbug_id&product=Fedora&query_format=advanced" # noqa - cmd = "bugzilla query --status OPEN --from-url %s" % url - fakebz = tests.mockbackend.make_bz( - bug_search_args="data/mockargs/test_query7.txt", - bug_search_return="data/mockreturn/test_getbug_rhel.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_query7.txt") - - -############################### -# 'bugzilla new' mock testing # -############################### - -def test_new(run_cli): - # Bunch of options - cmd = "bugzilla new --product FOOPROD --component FOOCOMP " - cmd += "--summary 'Hey this is the title!' " - cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " - cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP" - - fakebz = tests.mockbackend.make_bz( - bug_create_args="data/mockargs/test_new1.txt", - bug_create_return={"id": 1694158}, - bug_get_args=None, - bug_get_return="data/mockreturn/test_getbug.txt") - out = run_cli(cmd, fakebz) - tests.utils.diff_compare(out, "data/clioutput/test_new1.txt") - - -################################## -# 'bugzilla modify' mock testing # -################################## - -def test_modify(run_cli): - # errors on missing args - cmd = "bugzilla modify 123456" - fakebz = tests.mockbackend.make_bz() - out = run_cli(cmd, fakebz, expectfail=True) - assert "additional arguments" in out - - # Modify basic - cmd = "bugzilla modify 123456 1234567 " - cmd += "--status ASSIGNED --component NEWCOMP " - fakebz = tests.mockbackend.make_bz( - bug_update_args="data/mockargs/test_modify1.txt", - bug_update_return={}) - out = run_cli(cmd, fakebz) - assert not out - - # Modify with lots of opts - cmd = "bugzilla modify 123456 --component NEWCOMP " - cmd += "--keyword +FOO --groups=-BAR --blocked =123456,445566 " - cmd += "--flag=-needinfo,+somethingelse " - cmd += "--whiteboard =foo --whiteboard =thisone " - cmd += "--dupeid 555666 " - fakebz = tests.mockbackend.make_bz( - bug_update_args="data/mockargs/test_modify2.txt", - bug_update_return={}) - out = run_cli(cmd, fakebz) - assert not out - - # Modify with tricky opts - cmd = "bugzilla modify 1165434 " - cmd += "--tags +addtag --tags=-rmtag " - cmd += "--qa_whiteboard +yo-qa --qa_whiteboard=-foo " - cmd += "--internal_whiteboard +internal-hey --internal_whiteboard +bar " - cmd += "--devel_whiteboard +devel-duh --devel_whiteboard=-yay " - fakebz = tests.mockbackend.make_bz(rhbz=True, - bug_update_tags_args="data/mockargs/test_modify3-tags.txt", - bug_update_tags_return={}, - bug_update_args="data/mockargs/test_modify3.txt", - bug_update_return={}, - bug_get_args=None, - bug_get_return="data/mockreturn/test_getbug_rhel.txt") - out = run_cli(cmd, fakebz) - assert not out - - -################################## -# 'bugzilla attach' mock testing # -################################## - -def test_attach(run_cli): - attachfile = os.path.dirname(__file__) + "/data/bz-attach-get1.txt" - attachcontent = open(attachfile).read() - - # Hit error when no ID specified - fakebz = tests.mockbackend.make_bz() - out = run_cli("bugzilla attach", fakebz, expectfail=True) - assert "ID must be specified" in out - - # Hit error when using tty and no --file specified - out = run_cli("bugzilla attach 123456", fakebz, expectfail=True) - assert "--file must be specified" in out - - # Hit error when using stdin, but no --desc - out = run_cli("bugzilla attach 123456", fakebz, expectfail=True, - stdin=attachcontent) - assert "--description must be specified" in out - - # Basic CLI attach - cmd = "bugzilla attach 123456 --file=%s " % attachfile - cmd += "--type text/x-patch --private " - cmd += "--comment 'some comment to go with it'" - fakebz = tests.mockbackend.make_bz( - bug_attachment_create_args="data/mockargs/test_attach1.txt", - bug_attachment_create_return={'ids': [1557949]}) - out = run_cli(cmd, fakebz) - assert "Created attachment 1557949 on bug 123456" in out - - # Attach from stdin - cmd = "bugzilla attach 123456 --file=fake-file-name.txt " - cmd += "--description 'Some attachment description' " - fakebz = tests.mockbackend.make_bz( - bug_attachment_create_args="data/mockargs/test_attach2.txt", - bug_attachment_create_return={'ids': [1557949]}) - out = run_cli(cmd, fakebz, stdin=attachcontent) - assert "Created attachment 1557949 on bug 123456" in out - - -def _test_attach_get(run_cli): - # Hit error when using ids with --get* - fakebz = tests.mockbackend.make_bz() - out = run_cli("bugzilla attach 123456 --getall 123456", - fakebz, expectfail=True) - assert "not used for" in out - - # Basic --get ATTID usage - filename = u"Klíč memorial test file.txt" - cmd = "bugzilla attach --get 112233" - fakebz = tests.mockbackend.make_bz( - bug_attachment_get_args="data/mockargs/test_attach_get1.txt", - bug_attachment_get_return="data/mockreturn/test_attach_get1.txt") - out = run_cli(cmd, fakebz) - assert filename in out - - # Basic --getall with --ignore-obsolete - cmd = "bugzilla attach --getall 663674 --ignore-obsolete" - fakebz = tests.mockbackend.make_bz( - bug_attachment_get_all_args="data/mockargs/test_attach_get2.txt", - bug_attachment_get_all_return="data/mockreturn/test_attach_get2.txt") - out = run_cli(cmd, fakebz) - - os.system("ls %s" % os.getcwd()) - filename += ".1" - assert filename in out - assert "bugzilla-filename" in out - - -def test_attach_get(run_cli): - import tempfile - import shutil - tmpdir = tempfile.mkdtemp(dir=os.getcwd()) - origcwd = os.getcwd() - os.chdir(tmpdir) - try: - _test_attach_get(run_cli) - finally: - os.chdir(origcwd) - shutil.rmtree(tmpdir) diff --git a/tests/test_cli_info.py b/tests/test_cli_info.py new file mode 100644 index 00000000..f7136e41 --- /dev/null +++ b/tests/test_cli_info.py @@ -0,0 +1,90 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tests +import tests.mockbackend +import tests.utils + + +################################ +# 'bugzilla info' mock testing # +################################ + +def test_info(run_cli): + funcname = tests.utils.get_funcname() + argsprefix = "data/mockargs/%s_" % funcname + cliprefix = "data/clioutput/%s_" % funcname + + prod_accessible = {'ids': [1, 7]} + prod_get = {'products': [ + {'id': 1, 'name': 'Prod 1 Test'}, + {'id': 7, 'name': 'test-fake-product'} + ]} + + # info --products + fakebz = tests.mockbackend.make_bz( + product_get_accessible_args=None, + product_get_accessible_return=prod_accessible, + product_get_args=argsprefix + "products.txt", + product_get_return=prod_get) + cmd = "bugzilla info --products" + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "products.txt") + + # info --versions + prod_get_ver = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'versions': [ + {'id': 360, 'is_active': True, 'name': '7.1'}, + {'id': 123, 'is_active': True, 'name': 'fooversion!'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "versions.txt", + product_get_return=prod_get_ver) + cmd = "bugzilla info --versions test-fake-product" + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "versions.txt") + + # info --components + legal_values = {'values': ["comp1", "test-comp-2", "hey-imma-comp"]} + cmd = "bugzilla info --components test-fake-product" + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components.txt", + product_get_return=prod_get, + bug_legal_values_args=argsprefix + "components-legalvalues.txt", + bug_legal_values_return=legal_values) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components.txt") + + # info --components --active-components + cmd = "bugzilla info --components test-fake-product --active-components" + prod_get_comp_active = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'components': [ + {'is_active': True, 'name': 'backend/kernel'}, + {'is_active': True, 'name': 'client-interfaces'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components-active.txt", + product_get_return=prod_get_comp_active) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components-active.txt") + + # info --components_owners + cmd = "bugzilla info --component_owners test-fake-product" + prod_get_comp_owners = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'components': [ + {'default_assigned_to': 'Fake Guy', + 'name': 'client-interfaces'}, + {'default_assigned_to': 'ANother fake dude!', + 'name': 'configuration'}, + ]}, + ]} + fakebz = tests.mockbackend.make_bz( + product_get_args=argsprefix + "components-owners.txt", + product_get_return=prod_get_comp_owners) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, cliprefix + "components-owners.txt") diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py new file mode 100644 index 00000000..60335878 --- /dev/null +++ b/tests/test_cli_login.py @@ -0,0 +1,69 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import pytest + +import bugzilla + +import tests +import tests.mockbackend +import tests.utils + + +################################# +# 'bugzilla login' mock testing # +################################# + +def test_login(run_cli): + cmd = "bugzilla login FOO BAR" + + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return=RuntimeError("TEST ERROR")) + out = run_cli(cmd, fakebz, expectfail=True) + assert "Login failed: TEST ERROR" in out + + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}) + out = run_cli(cmd, fakebz) + assert "Login successful" in out + + cmd = "bugzilla --restrict-login --user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login-restrict.txt", + user_login_return={}) + out = run_cli(cmd, fakebz) + assert "Login successful" in out + + cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" + # Raises raw error trying to see if we aren't logged in + with pytest.raises(NotImplementedError): + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return=NotImplementedError()) + out = run_cli(cmd, fakebz) + + # Errors with expected code + cmd = "bugzilla --ensure-logged-in --user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return=bugzilla.BugzillaError("TESTMESSAGE", code=505)) + out = run_cli(cmd, fakebz, expectfail=True) + assert "--ensure-logged-in passed but you" in out + + # Returns success for logged_in check and hits a tokenfile line + cmd = "bugzilla --ensure-logged-in " + cmd += "--user FOO --password BAR login" + fakebz = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True}, + user_login_args="data/mockargs/test_login.txt", + user_login_return={}, + user_get_args=None, + user_get_return={}) + out = run_cli(cmd, fakebz) + assert "token cache updated" in out diff --git a/tests/test_cli_modify.py b/tests/test_cli_modify.py new file mode 100644 index 00000000..21a59e58 --- /dev/null +++ b/tests/test_cli_modify.py @@ -0,0 +1,55 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tests +import tests.mockbackend +import tests.utils + + +################################## +# 'bugzilla modify' mock testing # +################################## + +def test_modify(run_cli): + # errors on missing args + cmd = "bugzilla modify 123456" + fakebz = tests.mockbackend.make_bz() + out = run_cli(cmd, fakebz, expectfail=True) + assert "additional arguments" in out + + # Modify basic + cmd = "bugzilla modify 123456 1234567 " + cmd += "--status ASSIGNED --component NEWCOMP " + fakebz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_modify1.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out + + # Modify with lots of opts + cmd = "bugzilla modify 123456 --component NEWCOMP " + cmd += "--keyword +FOO --groups=-BAR --blocked =123456,445566 " + cmd += "--flag=-needinfo,+somethingelse " + cmd += "--whiteboard =foo --whiteboard =thisone " + cmd += "--dupeid 555666 " + fakebz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_modify2.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out + + # Modify with tricky opts + cmd = "bugzilla modify 1165434 " + cmd += "--tags +addtag --tags=-rmtag " + cmd += "--qa_whiteboard +yo-qa --qa_whiteboard=-foo " + cmd += "--internal_whiteboard +internal-hey --internal_whiteboard +bar " + cmd += "--devel_whiteboard +devel-duh --devel_whiteboard=-yay " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_update_tags_args="data/mockargs/test_modify3-tags.txt", + bug_update_tags_return={}, + bug_update_args="data/mockargs/test_modify3.txt", + bug_update_return={}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + assert not out diff --git a/tests/test_cli_new.py b/tests/test_cli_new.py new file mode 100644 index 00000000..c3ea6dad --- /dev/null +++ b/tests/test_cli_new.py @@ -0,0 +1,26 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tests +import tests.mockbackend +import tests.utils + + +############################### +# 'bugzilla new' mock testing # +############################### + +def test_new(run_cli): + # Bunch of options + cmd = "bugzilla new --product FOOPROD --component FOOCOMP " + cmd += "--summary 'Hey this is the title!' " + cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " + cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP" + + fakebz = tests.mockbackend.make_bz( + bug_create_args="data/mockargs/test_new1.txt", + bug_create_return={"id": 1694158}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_new1.txt") diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py new file mode 100644 index 00000000..2deacd70 --- /dev/null +++ b/tests/test_cli_query.py @@ -0,0 +1,105 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import os +import re + +import tests +import tests.mockbackend +import tests.utils + + +################################# +# 'bugzilla query' mock testing # +################################# + +def test_query(run_cli): + # query that ends up empty + cmd = "bugzilla query --ids " + fakebz = tests.mockbackend.make_bz(version="3.0.0") + out = run_cli(cmd, fakebz, expectfail=True) + assert "requires additional arguments" in out + + # bad field option + cmd = "bugzilla query --field FOO" + out = run_cli(cmd, fakebz, expectfail=True) + assert "Invalid field argument" in out + + # Simple query with some comma opts + cmd = "bugzilla query " + cmd += "--product foo --component foo,bar --bug_id 1234,2480" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query1.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1.txt") + + # Same but with --ids output + cmd = "bugzilla query --ids " + cmd += "--product foo --component foo,bar --bug_id 1234,2480" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query1-ids.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1-ids.txt") + + # Same but with --raw output + cmd = "bugzilla query --raw --bug_id 1165434" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query2.txt", + bug_search_return={"bugs": [{"id": 1165434}]}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + # Dictionary ordering is random, so scrub it from our output + out = re.sub(r"\{.*\}", r"'DICT SCRUBBED'", out, re.MULTILINE) + tests.utils.diff_compare(out, "data/clioutput/test_query2.txt") + + # Test a bunch of different combinations for code coverage + cmd = "bugzilla query --status ALL --severity sev1,sev2 " + cmd += "--outputformat='%{foo}:%{bar}::%{whiteboard}:" + cmd += "%{flags}:%{flags_requestee}%{whiteboard:devel}::" + cmd += "%{flag:needinfo}::%{comments}::%{external_bugs}'" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query3.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query3.txt") + + # Test --status DEV and --full + cmd = "bugzilla query --status DEV --full" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query4.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query4.txt") + + # Test --status QE and --extra, and components-file + compfile = os.path.dirname(__file__) + "/data/components_file.txt" + cmd = "bugzilla query --status QE --extra " + cmd += "--components_file %s" % compfile + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query5.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query5.txt") + + # Test --status EOL and --oneline, and some --field usage + cmd = "bugzilla query --status EOL --oneline " + cmd += "--field FOO=1 --field=BAR=WIBBLE " + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query6.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt", + bug_get_args="data/mockargs/test_query_cve_getbug.txt", + bug_get_return="data/mockreturn/test_query_cve_getbug.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query6.txt") + + # Test --status OPEN and --from-url + url = "https://bugzilla.redhat.com/buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=MODIFIED&bug_status=ON_DEV&bug_status=ON_QA&bug_status=VERIFIED&bug_status=FAILS_QA&bug_status=RELEASE_PENDING&bug_status=POST&classification=Fedora&component=virt-manager&order=bug_status%2Cbug_id&product=Fedora&query_format=advanced" # noqa + cmd = "bugzilla query --status OPEN --from-url %s" % url + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query7.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query7.txt") From 36a261b8ec94e8500d842b4ec5ef123bb98931c4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 11:44:57 -0500 Subject: [PATCH 178/393] tests: split out test_api_authfiles.py Signed-off-by: Cole Robinson --- tests/test_api_authfiles.py | 87 +++++++++++++++++++++++++++++++++++++ tests/test_api_misc.py | 70 ----------------------------- 2 files changed, 87 insertions(+), 70 deletions(-) create mode 100644 tests/test_api_authfiles.py diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py new file mode 100644 index 00000000..a2d7955c --- /dev/null +++ b/tests/test_api_authfiles.py @@ -0,0 +1,87 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import os +import tempfile + +import pytest + +import bugzilla + +import tests +import tests.mockbackend + + +def testCookies(): + dirname = os.path.dirname(__file__) + cookiesbad = dirname + "/data/cookies-bad.txt" + cookieslwp = dirname + "/data/cookies-lwp.txt" + cookiesmoz = dirname + "/data/cookies-moz.txt" + + # We used to convert LWP cookies, but it shouldn't matter anymore, + # so verify they fail at least + with pytest.raises(bugzilla.BugzillaError): + tests.mockbackend.make_bz(version="3.0.0", + bz_kwargs={"cookiefile": cookieslwp, "use_creds": True}) + + with pytest.raises(bugzilla.BugzillaError): + tests.mockbackend.make_bz(version="3.0.0", + bz_kwargs={"cookiefile": cookiesbad, "use_creds": True}) + + # Mozilla should 'just work' + tests.mockbackend.make_bz(version="3.0.0", + bz_kwargs={"cookiefile": cookiesmoz, "use_creds": True}) + + +def test_readconfig(): + # Testing for bugzillarc handling + bzapi = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) + bzapi.url = "example.com" + temp = tempfile.NamedTemporaryFile(mode="w") + + content = """ +[example.com] +foo=1 +user=test1 +password=test2""" + temp.write(content) + temp.flush() + bzapi.readconfig(temp.name) + assert bzapi.user == "test1" + assert bzapi.password == "test2" + assert bzapi.api_key is None + + bzapi.url = "foo.example.com" + bzapi.user = None + bzapi.readconfig(temp.name) + assert bzapi.user is None + + content = """ +[foo.example.com] +user=test3 +password=test4 +api_key=123abc +""" + temp.write(content) + temp.flush() + bzapi.readconfig(temp.name) + assert bzapi.user == "test3" + assert bzapi.password == "test4" + assert bzapi.api_key == "123abc" + + bzapi.url = "bugzilla.redhat.com" + bzapi.user = None + bzapi.password = None + bzapi.api_key = None + bzapi.readconfig(temp.name) + assert bzapi.user is None + assert bzapi.password is None + assert bzapi.api_key is None diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index fad2fef0..db941877 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -9,9 +9,6 @@ Test miscellaneous API bits """ -from __future__ import print_function - -import os import sys import tempfile @@ -42,73 +39,6 @@ def test_fixurl(): "http://example.com/somepath.cgi") -def testCookies(): - dirname = os.path.dirname(__file__) - cookiesbad = dirname + "/data/cookies-bad.txt" - cookieslwp = dirname + "/data/cookies-lwp.txt" - cookiesmoz = dirname + "/data/cookies-moz.txt" - - # We used to convert LWP cookies, but it shouldn't matter anymore, - # so verify they fail at least - with pytest.raises(bugzilla.BugzillaError): - tests.mockbackend.make_bz(version="3.0.0", - bz_kwargs={"cookiefile": cookieslwp, "use_creds": True}) - - with pytest.raises(bugzilla.BugzillaError): - tests.mockbackend.make_bz(version="3.0.0", - bz_kwargs={"cookiefile": cookiesbad, "use_creds": True}) - - # Mozilla should 'just work' - tests.mockbackend.make_bz(version="3.0.0", - bz_kwargs={"cookiefile": cookiesmoz, "use_creds": True}) - - -def test_readconfig(): - # Testing for bugzillarc handling - bzapi = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) - bzapi.url = "example.com" - temp = tempfile.NamedTemporaryFile(mode="w") - - content = """ -[example.com] -foo=1 -user=test1 -password=test2""" - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - assert bzapi.user == "test1" - assert bzapi.password == "test2" - assert bzapi.api_key is None - - bzapi.url = "foo.example.com" - bzapi.user = None - bzapi.readconfig(temp.name) - assert bzapi.user is None - - content = """ -[foo.example.com] -user=test3 -password=test4 -api_key=123abc -""" - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - assert bzapi.user == "test3" - assert bzapi.password == "test4" - assert bzapi.api_key == "123abc" - - bzapi.url = "bugzilla.redhat.com" - bzapi.user = None - bzapi.password = None - bzapi.api_key = None - bzapi.readconfig(temp.name) - assert bzapi.user is None - assert bzapi.password is None - assert bzapi.api_key is None - - def testPostTranslation(): def _testPostCompare(bz, indict, outexpect): outdict = indict.copy() From 114862bb23b9401b2255f71425641cd2a5508b0d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 11:41:38 -0500 Subject: [PATCH 179/393] tests: Add full authfiles coverage Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 64 ++++---- bugzilla/_util.py | 15 +- bugzilla/base.py | 47 +++--- bugzilla/exceptions.py | 4 +- tests/data/authfiles/output-bugzillarc.txt | 3 + tests/data/authfiles/output-cookies.txt | 6 + tests/data/authfiles/output-token.txt | 3 + tests/test_api_authfiles.py | 169 ++++++++++++++++++--- tests/test_api_misc.py | 8 + 9 files changed, 227 insertions(+), 92 deletions(-) create mode 100644 tests/data/authfiles/output-bugzillarc.txt create mode 100644 tests/data/authfiles/output-cookies.txt create mode 100644 tests/data/authfiles/output-token.txt diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index fd3c8263..a2686c49 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -38,10 +38,6 @@ def _default_cache_location(filename): return _default_location(filename, 'cache') -def _default_config_location(filename): - return _default_location(filename, 'config') - - class _BugzillaRCFile(object): """ Helper class for interacting with bugzillarc files @@ -110,6 +106,33 @@ def parse(self, url): return dict(self._cfg.items(section)) + def save_api_key(self, url, api_key): + """ + Save the API_KEY in the config file. We use the last file + in the configpaths list, which is the one with the highest + precedence. + """ + configpaths = self.get_configpaths() + if not configpaths: + return None + + config_filename = configpaths[-1] + section = _parse_hostname(url) + cfg = ConfigParser() + cfg.read(config_filename) + + if section not in cfg.sections(): + cfg.add_section(section) + + cfg.set(section, 'api_key', api_key.strip()) + + _makedirs(config_filename) + with open(config_filename, 'w') as configfile: + cfg.write(configfile) + + return config_filename + + class _BugzillaTokenCache(object): """ Class for interacting with a .bugzillatoken cache file @@ -124,7 +147,7 @@ def __init__(self): def _get_domain(self, url): domain = urlparse(url)[1] - if domain not in self._cfg.sections(): + if domain and domain not in self._cfg.sections(): self._cfg.add_section(domain) return domain @@ -162,37 +185,6 @@ def set_filename(self, filename): self._cfg = cfg -def _save_api_key(url, api_key, configpaths): - """ - Save the API_KEY in the config file. - - If tokenfile and cookiefile are undefined, it means that the - - API was called with --no-cache-credentials and no change will be - made - """ - if configpaths: - config_filename = configpaths[0] - else: - config_filename = _default_config_location('bugzillarc') - section = _parse_hostname(url) - - cfg = ConfigParser() - cfg.read(config_filename) - - if section not in cfg.sections(): - cfg.add_section(section) - - cfg.set(section, 'api_key', api_key.strip()) - - _makedirs(config_filename) - with open(config_filename, 'w') as configfile: - cfg.write(configfile) - - log.info("API key written to %s", config_filename) - print("API key written to %s" % config_filename) - - class _BugzillaCookieCache(object): @staticmethod def get_default_path(): diff --git a/bugzilla/_util.py b/bugzilla/_util.py index 821cb3c7..82a8417d 100644 --- a/bugzilla/_util.py +++ b/bugzilla/_util.py @@ -20,13 +20,12 @@ def to_encoding(ustring): """ Locale specific printing per python version """ - if ustring is None: - return '' + ustring = ustring or '' if IS_PY3: return str(ustring) - - strtype = basestring # pylint: disable=undefined-variable - string = ustring - if not isinstance(ustring, strtype): - string = str(ustring) - return string.encode(locale.getpreferredencoding(), 'replace') + else: # pragma: no cover + strtype = basestring # pylint: disable=undefined-variable + string = ustring + if not isinstance(ustring, strtype): + string = str(ustring) + return string.encode(locale.getpreferredencoding(), 'replace') diff --git a/bugzilla/base.py b/bugzilla/base.py index 59aa5b2d..f2b69aa0 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -16,7 +16,7 @@ from io import BytesIO from ._authfiles import (_BugzillaRCFile, - _BugzillaCookieCache, _BugzillaTokenCache, _save_api_key) + _BugzillaCookieCache, _BugzillaTokenCache) from .apiversion import __version__ from ._backendxmlrpc import _BackendXMLRPC from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl @@ -212,7 +212,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self.user = user or '' self.password = password or '' self.api_key = api_key - self.cert = cert or '' + self.cert = cert or None self.url = '' self._backend = None @@ -432,7 +432,7 @@ def readconfig(self, configpath=None, overwrite=True): elif key == "password" and (overwrite or not self.password): log.debug("bugzillarc: setting password") self.password = val - elif key == "cert" and not (overwrite or not self.cert): + elif key == "cert" and (overwrite or not self.cert): log.debug("bugzillarc: setting cert") self.cert = val else: @@ -567,6 +567,28 @@ def login(self, user=None, password=None, restrict_login=None): raise BugzillaError("Login failed: %s" % BugzillaError.get_bugzilla_error_string(e)) + def _ask_api_key(self): + sys.stdout.write('API Key: ') + sys.stdout.flush() + api_key = sys.stdin.readline().strip() + + self.disconnect() + self.api_key = api_key + + log.info('Checking API key... ') + self.connect() + + if not self.logged_in: # pragma: no cover + raise BugzillaError("Login with API_KEY failed") + log.info('API Key accepted') + + wrote_filename = self._rcfile.save_api_key(self.url, self.api_key) + if wrote_filename: + log.info("API key written to %s", wrote_filename) + print("API key written to %s" % wrote_filename) + else: # pragma: no cover + log.info("API Key won't be updated because use_creds=False") + def interactive_login(self, user=None, password=None, force=False, restrict_login=None, use_api_key=False): """ @@ -582,24 +604,7 @@ def interactive_login(self, user=None, password=None, force=False, log.debug('Calling interactive_login') if use_api_key: - sys.stdout.write('API Key: ') - sys.stdout.flush() - api_key = sys.stdin.readline().strip() - - self.disconnect() - self.api_key = api_key - - log.info('Checking API key... ') - self.connect() - - if not self.logged_in: # pragma: no cover - raise BugzillaError("Login with API_KEY failed") - log.info('API Key accepted') - - if self._use_creds or self.configpath: - _save_api_key(self.url, self.api_key, self.configpath) - else: # pragma: no cover - log.info("API Key won't be updated because use_creds=False") + self._ask_api_key() return if not user: diff --git a/bugzilla/exceptions.py b/bugzilla/exceptions.py index 3060392a..d884df0a 100644 --- a/bugzilla/exceptions.py +++ b/bugzilla/exceptions.py @@ -13,9 +13,7 @@ def get_bugzilla_error_string(exc): XMLRPC Fault, or any other exception type that's raised from bugzilla interaction """ - if hasattr(exc, "faultString"): - return getattr(exc, "faultString") - return str(exc) + return getattr(exc, "faultString", str(exc)) @staticmethod def get_bugzilla_error_code(exc): diff --git a/tests/data/authfiles/output-bugzillarc.txt b/tests/data/authfiles/output-bugzillarc.txt new file mode 100644 index 00000000..5083b9b6 --- /dev/null +++ b/tests/data/authfiles/output-bugzillarc.txt @@ -0,0 +1,3 @@ +[example.com] +api_key = TEST-API-KEY + diff --git a/tests/data/authfiles/output-cookies.txt b/tests/data/authfiles/output-cookies.txt new file mode 100644 index 00000000..193bd5d5 --- /dev/null +++ b/tests/data/authfiles/output-cookies.txt @@ -0,0 +1,6 @@ +# Netscape HTTP Cookie File +# http://curl.haxx.se/rfc/cookie_spec.html +# This is a generated file! Do not edit. + +.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie +.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie diff --git a/tests/data/authfiles/output-token.txt b/tests/data/authfiles/output-token.txt new file mode 100644 index 00000000..9fd7d445 --- /dev/null +++ b/tests/data/authfiles/output-token.txt @@ -0,0 +1,3 @@ +[example.com] +token = MY-FAKE-TOKEN + diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py index a2d7955c..908705ad 100644 --- a/tests/test_api_authfiles.py +++ b/tests/test_api_authfiles.py @@ -10,14 +10,17 @@ """ import os +import shutil import tempfile import pytest +import requests import bugzilla import tests import tests.mockbackend +import tests.utils def testCookies(): @@ -37,9 +40,21 @@ def testCookies(): bz_kwargs={"cookiefile": cookiesbad, "use_creds": True}) # Mozilla should 'just work' - tests.mockbackend.make_bz(version="3.0.0", + bz = tests.mockbackend.make_bz(version="3.0.0", bz_kwargs={"cookiefile": cookiesmoz, "use_creds": True}) + # cookie/token property magic + bz = tests.mockbackend.make_bz(bz_kwargs={"use_creds": True}) + token = dirname + "/data/homedir/.cache/python-bugzilla/bugzillatoken" + cookie = dirname + "/data/homedir/.cache/python-bugzilla/bugzillacookies" + + assert token == bz.tokenfile + assert cookie == bz.cookiefile + del(bz.tokenfile) + del(bz.cookiefile) + assert bz.tokenfile is None + assert bz.cookiefile is None + def test_readconfig(): # Testing for bugzillarc handling @@ -47,41 +62,147 @@ def test_readconfig(): bzapi.url = "example.com" temp = tempfile.NamedTemporaryFile(mode="w") + def _check(user, password, api_key, cert): + assert bzapi.user == user + assert bzapi.password == password + assert bzapi.api_key == api_key + assert bzapi.cert == cert + + def _write(c): + temp.seek(0) + temp.write(c) + temp.flush() + return temp.name + + # Check readconfig normal usage content = """ [example.com] foo=1 user=test1 -password=test2""" - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - assert bzapi.user == "test1" - assert bzapi.password == "test2" - assert bzapi.api_key is None - - bzapi.url = "foo.example.com" - bzapi.user = None - bzapi.readconfig(temp.name) - assert bzapi.user is None +password=test2 +api_key=123abc +cert=/a/b/c +someunknownkey=someval +""" + bzapi.readconfig(_write(content)) + _check("test1", "test2", "123abc", "/a/b/c") + # Check loading a different URL, that values aren't overwritten content = """ [foo.example.com] user=test3 password=test4 -api_key=123abc +api_key=567abc +cert=/newpath """ - temp.write(content) - temp.flush() - bzapi.readconfig(temp.name) - assert bzapi.user == "test3" - assert bzapi.password == "test4" - assert bzapi.api_key == "123abc" + bzapi.readconfig(_write(content)) + _check("test1", "test2", "123abc", "/a/b/c") - bzapi.url = "bugzilla.redhat.com" + # Change URL, but check readconfig with overwrite=False + bzapi.url = "foo.example.com" + bzapi.readconfig(temp.name, overwrite=False) + _check("test1", "test2", "123abc", "/a/b/c") + + # With default overwrite=True, values will be updated + # Alter the config to have a / in the hostname, which hits different code + content = content.replace("example.com", "example.com/xmlrpc.cgi") + bzapi.url = "https://foo.example.com/xmlrpc.cgi" + bzapi.readconfig(_write(content)) + _check("test3", "test4", "567abc", "/newpath") + + # Confirm nothing overwritten for a totally different URL bzapi.user = None bzapi.password = None bzapi.api_key = None + bzapi.cert = None + bzapi.url = "bugzilla.redhat.com" bzapi.readconfig(temp.name) - assert bzapi.user is None - assert bzapi.password is None - assert bzapi.api_key is None + _check(None, None, None, None) + + # Test confipath overwrite + assert [temp.name] == bzapi.configpath + del(bzapi.configpath) + assert [] == bzapi.configpath + bzapi.readconfig() + _check(None, None, None, None) + + +def _get_cookiejar(): + cookiefile = os.path.dirname(__file__) + "/data/cookies-moz.txt" + inputbz = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, "cookiefile": cookiefile}) + cookiecache = inputbz._cookiecache # pylint: disable=protected-access + return cookiecache.get_cookiejar() + + +def test_authfiles_saving(monkeypatch): + tmpdir = tempfile.mkdtemp() + try: + monkeypatch.setitem(os.environ, "HOME", tmpdir) + + bzapi = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, "cert": "foo-fake-cert"}) + bzapi.connect("https://example.com/fakebz") + + bzapi.cert = "foo-fake-path" + backend = bzapi._backend # pylint: disable=protected-access + bsession = backend._bugzillasession # pylint: disable=protected-access + + response = requests.Response() + response.cookies = _get_cookiejar() + + # token testing, with repetitions to hit various code paths + bsession.set_token_value(None) + bsession.set_token_value("MY-FAKE-TOKEN") + bsession.set_token_value("MY-FAKE-TOKEN") + bsession.set_token_value(None) + bsession.set_token_value("MY-FAKE-TOKEN") + + # cookie testing + bsession.set_response_cookies(response) + + dirname = os.path.dirname(__file__) + "/data/authfiles/" + output_token = dirname + "output-token.txt" + output_cookies = dirname + "output-cookies.txt" + tests.utils.diff_compare(open(bzapi.tokenfile).read(), output_token) + tests.utils.diff_compare(open(bzapi.cookiefile).read(), output_cookies) + + # Make sure file can re-read them and not error + bzapi = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, + "cookiefile": output_cookies, + "tokenfile": output_token}) + assert bzapi.tokenfile == output_token + assert bzapi.cookiefile == output_cookies + + # Test rcfile writing for api_key + rcfile = bzapi._rcfile # pylint: disable=protected-access + bzapi.url = "https://example.com/fake" + rcfile.save_api_key(bzapi.url, "TEST-API-KEY") + rcfilepath = tmpdir + "/.config/python-bugzilla/bugzillarc" + assert rcfile.get_configpaths()[-1] == rcfilepath + tests.utils.diff_compare(open(rcfilepath).read(), + dirname + "output-bugzillarc.txt") + + # Use that generated rcfile to test default URL lookup + fakeurl = "http://foo.bar.baz/wibble" + open(rcfilepath, "w").write("\n[DEFAULT]\nurl = %s" % fakeurl) + assert bzapi.get_rcfile_default_url() == fakeurl + finally: + shutil.rmtree(tmpdir) + + +def test_authfiles_nowrite(): + # Set values when n when cookiefile is None, should be fine + bzapi = tests.mockbackend.make_bz(bz_kwargs={"use_creds": False}) + bzapi.connect("https://example.com/foo") + backend = bzapi._backend # pylint: disable=protected-access + bsession = backend._bugzillasession # pylint: disable=protected-access + rcfile = bzapi._rcfile # pylint: disable=protected-access + + response = requests.Response() + response.cookies = _get_cookiejar() + + bsession.set_token_value("NEW-TOKEN-VALUE") + bsession.set_response_cookies(response) + assert rcfile.save_api_key(bzapi.url, "fookey") is None diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index db941877..13f8a7e5 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -228,3 +228,11 @@ def test_version_bad(): def test_extensions_bad(): # Hit bad extensions error handling tests.mockbackend.make_bz(extensions="BADEXTENSIONS") + + +def test_bad_scheme(): + bz = tests.mockbackend.make_bz() + try: + bz.connect("ftp://example.com") + except Exception as e: + assert "Invalid URL scheme: ftp" in str(e) From e89676b084f12dcf89e03fb65978c313f9c686ab Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 14:15:40 -0500 Subject: [PATCH 180/393] tests: add full RHBugzilla coverage Signed-off-by: Cole Robinson --- bugzilla/rhbugzilla.py | 24 +++++++++-------------- tests/data/clioutput/test_query1-rhbz.txt | 2 ++ tests/data/mockargs/test_modify4.txt | 4 ++++ tests/data/mockargs/test_query1-rhbz.txt | 4 ++++ tests/test_api_misc.py | 18 +++++++++++++++++ tests/test_cli_modify.py | 13 +++++++++++- tests/test_cli_query.py | 9 +++++++++ 7 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 tests/data/clioutput/test_query1-rhbz.txt create mode 100644 tests/data/mockargs/test_modify4.txt create mode 100644 tests/data/mockargs/test_query1-rhbz.txt diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 8e8abba8..fb371cda 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -78,25 +78,19 @@ def pre_translation(query): """ old = query.copy() + def split_comma(_v): + if isinstance(_v, list): + return _v + return _v.split(",") + if 'bug_id' in query: - if not isinstance(query['bug_id'], list): - query['id'] = query['bug_id'].split(',') - else: - query['id'] = query['bug_id'] - del query['bug_id'] + query['id'] = split_comma(query.pop('bug_id')) if 'component' in query: - if not isinstance(query['component'], list): - query['component'] = query['component'].split(',') - - if 'include_fields' not in query and 'column_list' not in query: - return + query['component'] = split_comma(query['component']) - if 'include_fields' not in query: - query['include_fields'] = [] - if 'column_list' in query: - query['include_fields'] = query['column_list'] - del query['column_list'] + if 'include_fields' not in query and 'column_list' in query: + query['include_fields'] = query.pop('column_list') if old != query: log.debug("RHBugzilla pretranslated query to: %s", query) diff --git a/tests/data/clioutput/test_query1-rhbz.txt b/tests/data/clioutput/test_query1-rhbz.txt new file mode 100644 index 00000000..5d93087c --- /dev/null +++ b/tests/data/clioutput/test_query1-rhbz.txt @@ -0,0 +1,2 @@ +#508645 NEW - Libvirt Maintainers - RFE: qemu: Support a managed autoconnect mode for host USB devices +#668543 NEW - Cole Robinson - RFE: warn users at guest start if networks/storage pools are inactive diff --git a/tests/data/mockargs/test_modify4.txt b/tests/data/mockargs/test_modify4.txt new file mode 100644 index 00000000..31a579ee --- /dev/null +++ b/tests/data/mockargs/test_modify4.txt @@ -0,0 +1,4 @@ +{'cf_fixed_in': 'foofixedin', + 'component': 'lvm2', + 'ids': ['1165434'], + 'sub_components': {'lvm2': ['some-sub-component']}} \ No newline at end of file diff --git a/tests/data/mockargs/test_query1-rhbz.txt b/tests/data/mockargs/test_query1-rhbz.txt new file mode 100644 index 00000000..a0fcbbce --- /dev/null +++ b/tests/data/mockargs/test_query1-rhbz.txt @@ -0,0 +1,4 @@ +{'component': ['foo', 'bar'], + 'id': ['1234', '2480'], + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'product': ['foo']} \ No newline at end of file diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 13f8a7e5..60c65163 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -84,6 +84,24 @@ def _testPostCompare(bz, indict, outexpect): _testPostCompare(rhbz, test1, out_simple) +def test_rhbz_pre_translation(): + bz = tests.mockbackend.make_bz(rhbz=True) + input_query = { + "bug_id": "12345,6789", + "component": "comp1,comp2", + "column_list": ["field1", "field8"], + } + + bz.pre_translation(input_query) + output_query = { + 'component': ['comp1', 'comp2'], + 'id': ['12345', '6789'], + 'include_fields': ['field1', 'field8', 'id'], + } + + assert output_query == input_query + + def testSubComponentFail(): bz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) with pytest.raises(ValueError): diff --git a/tests/test_cli_modify.py b/tests/test_cli_modify.py index 21a59e58..2f5fee1b 100644 --- a/tests/test_cli_modify.py +++ b/tests/test_cli_modify.py @@ -38,7 +38,7 @@ def test_modify(run_cli): out = run_cli(cmd, fakebz) assert not out - # Modify with tricky opts + # Modify with tricky opts hitting other API calls cmd = "bugzilla modify 1165434 " cmd += "--tags +addtag --tags=-rmtag " cmd += "--qa_whiteboard +yo-qa --qa_whiteboard=-foo " @@ -53,3 +53,14 @@ def test_modify(run_cli): bug_get_return="data/mockreturn/test_getbug_rhel.txt") out = run_cli(cmd, fakebz) assert not out + + # Modify hitting some rhbz paths + cmd = "bugzilla modify 1165434 " + cmd += "--fixed_in foofixedin " + cmd += "--component lvm2 " + cmd += "--sub-component some-sub-component" + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_update_args="data/mockargs/test_modify4.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py index 2deacd70..21db51ca 100644 --- a/tests/test_cli_query.py +++ b/tests/test_cli_query.py @@ -34,6 +34,15 @@ def test_query(run_cli): out = run_cli(cmd, fakebz) tests.utils.diff_compare(out, "data/clioutput/test_query1.txt") + # Simple query with some comma opts + cmd = "bugzilla query " + cmd += "--product foo --component foo,bar --bug_id 1234,2480" + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_search_args="data/mockargs/test_query1-rhbz.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query1-rhbz.txt") + # Same but with --ids output cmd = "bugzilla query --ids " cmd += "--product foo --component foo,bar --bug_id 1234,2480" From ddc3b47128e6287299b5a0ed009cde3db2873bcf Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 14:29:38 -0500 Subject: [PATCH 181/393] tests: add externalbugs unit tests Signed-off-by: Cole Robinson --- tests/data/mockargs/test_externalbugs_add.txt | 8 +++ .../mockargs/test_externalbugs_remove.txt | 6 +++ .../mockargs/test_externalbugs_update.txt | 9 ++++ tests/mockbackend.py | 7 +++ tests/test_api_externalbugs.py | 53 +++++++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 tests/data/mockargs/test_externalbugs_add.txt create mode 100644 tests/data/mockargs/test_externalbugs_remove.txt create mode 100644 tests/data/mockargs/test_externalbugs_update.txt create mode 100644 tests/test_api_externalbugs.py diff --git a/tests/data/mockargs/test_externalbugs_add.txt b/tests/data/mockargs/test_externalbugs_add.txt new file mode 100644 index 00000000..c197610e --- /dev/null +++ b/tests/data/mockargs/test_externalbugs_add.txt @@ -0,0 +1,8 @@ +{'bug_ids': [1234, 5678], + 'external_bugs': [{'ext_bz_bug_id': 'externalid', + 'ext_description': 'link to launchpad', + 'ext_priority': 'bigly', + 'ext_status': 'CLOSED', + 'ext_type_description': 'some-bug-add-description', + 'ext_type_id': 'launchpad', + 'ext_type_url': 'https://example.com/launchpad/1234'}]} \ No newline at end of file diff --git a/tests/data/mockargs/test_externalbugs_remove.txt b/tests/data/mockargs/test_externalbugs_remove.txt new file mode 100644 index 00000000..28849215 --- /dev/null +++ b/tests/data/mockargs/test_externalbugs_remove.txt @@ -0,0 +1,6 @@ +{'bug_ids': ['blah'], + 'ext_bz_bug_id': ['99999'], + 'ext_type_description': 'foo-desc', + 'ext_type_id': 'footype', + 'ext_type_url': 'foo-url', + 'ids': ['remove1']} \ No newline at end of file diff --git a/tests/data/mockargs/test_externalbugs_update.txt b/tests/data/mockargs/test_externalbugs_update.txt new file mode 100644 index 00000000..de448358 --- /dev/null +++ b/tests/data/mockargs/test_externalbugs_update.txt @@ -0,0 +1,9 @@ +{'bug_ids': ['some', 'bug', 'id'], + 'ext_bz_bug_id': ['externalid-update'], + 'ext_description': 'link to mozilla', + 'ext_priority': 'like, really bigly', + 'ext_status': 'OPEN', + 'ext_type_description': 'some-bug-update', + 'ext_type_id': 'mozilla', + 'ext_type_url': 'https://mozilla.foo/bar/5678', + 'ids': ['external1', 'external2']} \ No newline at end of file diff --git a/tests/mockbackend.py b/tests/mockbackend.py index 5ed3f90b..0384ba84 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -65,6 +65,13 @@ def bug_update(self, *args): def bug_update_tags(self, *args): return self.__helper(args) + def externalbugs_add(self, *args): + return self.__helper(args) + def externalbugs_update(self, *args): + return self.__helper(args) + def externalbugs_remove(self, *args): + return self.__helper(args) + def product_get(self, *args): return self.__helper(args) def product_get_accessible(self, *args): diff --git a/tests/test_api_externalbugs.py b/tests/test_api_externalbugs.py new file mode 100644 index 00000000..c684bee6 --- /dev/null +++ b/tests/test_api_externalbugs.py @@ -0,0 +1,53 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import tests +import tests.mockbackend + + +def test_externalbugs(): + # Basic API testing of the ExternalBugs wrappers + fakebz = tests.mockbackend.make_bz( + externalbugs_add_args="data/mockargs/test_externalbugs_add.txt", + externalbugs_add_return={}, + externalbugs_update_args="data/mockargs/test_externalbugs_update.txt", + externalbugs_update_return={}, + externalbugs_remove_args="data/mockargs/test_externalbugs_remove.txt", + externalbugs_remove_return={}) + + fakebz.add_external_tracker( + bug_ids=[1234, 5678], + ext_bz_bug_id="externalid", + ext_type_id="launchpad", + ext_type_description="some-bug-add-description", + ext_type_url="https://example.com/launchpad/1234", + ext_status="CLOSED", + ext_description="link to launchpad", + ext_priority="bigly") + + fakebz.update_external_tracker( + ids=["external1", "external2"], + ext_bz_bug_id="externalid-update", + ext_type_id="mozilla", + ext_type_description="some-bug-update", + ext_type_url="https://mozilla.foo/bar/5678", + ext_status="OPEN", + bug_ids=["some", "bug", "id"], + ext_description="link to mozilla", + ext_priority="like, really bigly") + + fakebz.remove_external_tracker( + ids="remove1", + ext_bz_bug_id="99999", + ext_type_id="footype", + ext_type_description="foo-desc", + ext_type_url="foo-url", + bug_ids="blah") From f88c9bce4c3115e6c211728c50b8f1bfa1b475d0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 14:35:11 -0500 Subject: [PATCH 182/393] tests: add User API unit tests Signed-off-by: Cole Robinson --- bugzilla/base.py | 3 +- tests/data/mockargs/test_api_users_create.txt | 1 + tests/data/mockargs/test_api_users_get1.txt | 1 + tests/data/mockargs/test_api_users_get2.txt | 1 + tests/data/mockargs/test_api_users_get3.txt | 1 + .../data/mockargs/test_api_users_update1.txt | 1 + tests/mockbackend.py | 4 + tests/test_api_users.py | 75 +++++++++++++++++++ 8 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/data/mockargs/test_api_users_create.txt create mode 100644 tests/data/mockargs/test_api_users_get1.txt create mode 100644 tests/data/mockargs/test_api_users_get2.txt create mode 100644 tests/data/mockargs/test_api_users_get3.txt create mode 100644 tests/data/mockargs/test_api_users_update1.txt create mode 100644 tests/test_api_users.py diff --git a/bugzilla/base.py b/bugzilla/base.py index f2b69aa0..26092042 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1788,7 +1788,8 @@ def getusers(self, userlist): :userlist: List of usernames to lookup :returns: List of User records """ - rawusers = self._backend.user_get({"names": listify(userlist)}) + userlist = listify(userlist) + rawusers = self._backend.user_get({"names": userlist}) userobjs = [User(self, **rawuser) for rawuser in rawusers.get('users', [])] diff --git a/tests/data/mockargs/test_api_users_create.txt b/tests/data/mockargs/test_api_users_create.txt new file mode 100644 index 00000000..9415ac88 --- /dev/null +++ b/tests/data/mockargs/test_api_users_create.txt @@ -0,0 +1 @@ +{'email': 'example1@example.com', 'name': 'fooname', 'password': 'foopass'} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_users_get1.txt b/tests/data/mockargs/test_api_users_get1.txt new file mode 100644 index 00000000..eb9dd5ab --- /dev/null +++ b/tests/data/mockargs/test_api_users_get1.txt @@ -0,0 +1 @@ +{'names': ['example2@example.com']} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_users_get2.txt b/tests/data/mockargs/test_api_users_get2.txt new file mode 100644 index 00000000..93be1f65 --- /dev/null +++ b/tests/data/mockargs/test_api_users_get2.txt @@ -0,0 +1 @@ +{'names': ['example1@example.com']} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_users_get3.txt b/tests/data/mockargs/test_api_users_get3.txt new file mode 100644 index 00000000..b2e23734 --- /dev/null +++ b/tests/data/mockargs/test_api_users_get3.txt @@ -0,0 +1 @@ +{'match': ['example1@example.com']} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_users_update1.txt b/tests/data/mockargs/test_api_users_update1.txt new file mode 100644 index 00000000..3847634a --- /dev/null +++ b/tests/data/mockargs/test_api_users_update1.txt @@ -0,0 +1 @@ +{'groups': {'remove': ['fedora_contrib']}, 'names': ['example name']} \ No newline at end of file diff --git a/tests/mockbackend.py b/tests/mockbackend.py index 0384ba84..ec3f50b1 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -77,12 +77,16 @@ def product_get(self, *args): def product_get_accessible(self, *args): return self.__helper(args) + def user_create(self, *args): + return self.__helper(args) def user_get(self, *args): return self.__helper(args) def user_login(self, *args): return self.__helper(args) def user_logout(self, *args): return self.__helper(args) + def user_update(self, *args): + return self.__helper(args) def _make_backend_class(version="6.0.0", extensions=None, diff --git a/tests/test_api_users.py b/tests/test_api_users.py new file mode 100644 index 00000000..6bc5a5b1 --- /dev/null +++ b/tests/test_api_users.py @@ -0,0 +1,75 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import pytest + +import bugzilla + +import tests +import tests.mockbackend + + +def test_api_users(): + # Basic API testing of the users APIs + user_ret = {'users': [ + {'can_login': True, + 'email': 'example1@example.com', + 'id': 1010101, + 'name': 'example1@example.com', + 'real_name': 'Mr. Example Man'}, + {'can_login': False, + 'email': 'example2@example.com', + 'id': 2222333, + 'name': 'example name', + 'real_name': 'Example real name', + 'saved_reports': [], + 'saved_searches': [], + 'groups': [ + {"id": 1, "name": "testgroup", "description": "somedesc"} + ]}, + ]} + + # getusers and User testing + fakebz = tests.mockbackend.make_bz( + user_get_args="data/mockargs/test_api_users_get1.txt", + user_get_return=user_ret, + user_update_args="data/mockargs/test_api_users_update1.txt", + user_update_return={}) + userobj = fakebz.getuser("example2@example.com") + + # Some userobj testing + userobj.refresh() + assert userobj.userid == 2222333 + assert userobj.email == "example2@example.com" + assert userobj.name == "example name" + assert userobj.can_login is False + userobj.updateperms("rem", ["fedora_contrib"]) + + # Catch a validation error + with pytest.raises(bugzilla.BugzillaError): + userobj.updateperms("badaction", ["newgroup"]) + + # createuser tests + fakebz = tests.mockbackend.make_bz( + user_get_args="data/mockargs/test_api_users_get2.txt", + user_get_return=user_ret, + user_create_args="data/mockargs/test_api_users_create.txt", + user_create_return={}) + userobj = fakebz.createuser("example1@example.com", "fooname", "foopass") + assert userobj.email == "example1@example.com" + + + # searchuser tests + fakebz = tests.mockbackend.make_bz( + user_get_args="data/mockargs/test_api_users_get3.txt", + user_get_return=user_ret) + userlist = fakebz.searchusers("example1@example.com") + assert len(userlist) == 2 From 91b3569ca6b40109ad4181bdd7ceb21956e51798 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 15:07:08 -0500 Subject: [PATCH 183/393] base: Remove dead code Signed-off-by: Cole Robinson --- bugzilla/base.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 26092042..59f31298 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1504,14 +1504,6 @@ def c(val): # Methods for working with attachments # ######################################## - def _attachment_uri(self, attachid): - """ - Returns the URI for the given attachment ID. - """ - att_uri = self.url.replace('xmlrpc.cgi', 'attachment.cgi') - att_uri = att_uri + '?id=%s' % attachid - return att_uri - def attachfile(self, idlist, attachfile, description, **kwargs): """ Attach a file to the given bug IDs. Returns the ID of the attachment From d154649568080e27a9a1bf66d620114edbdf6da0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 15:15:39 -0500 Subject: [PATCH 184/393] cli: Add 'new --private' The API accepts it, it's just not exposed on the command line Signed-off-by: Cole Robinson --- bugzilla.1 | 5 +++++ bugzilla/_cli.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/bugzilla.1 b/bugzilla.1 index f4e63281..28c192af 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -197,6 +197,11 @@ Reset assignee to component default Reset QA contact to component default +.SH \[oq]new\[cq] specific options +.IP "--private" +Mark new comment as private + + .SH \[oq]attach\[cq] options .IP "--file=FILENAME, -f FILENAME" File to attach, or filename for data provided on stdin diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index e210cd0b..1bd84bf2 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -255,6 +255,9 @@ def _setup_action_new_parser(subparsers): p = subparsers.add_parser("new", description=description) _parser_add_bz_fields(p, "new") + g = p.add_argument_group("'new' specific options") + g.add_argument('--private', action='store_true', default=False, + help='Mark new comment as private') def _setup_action_query_parser(subparsers): @@ -781,6 +784,7 @@ def parse_multi(val): sub_component=opt.sub_component or None, alias=opt.alias or None, comment_tags=opt.comment_tag or None, + comment_private=opt.private or None, ) _merge_field_opts(ret, opt, parser) From 48797caede9702d8279abf4b60a16a86788aa530 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 15:16:10 -0500 Subject: [PATCH 185/393] tests: fill out API side of cli command coverage Signed-off-by: Cole Robinson --- bugzilla/base.py | 8 ++++---- tests/data/clioutput/test_query2-rhbz.txt | 2 ++ tests/data/mockargs/test_modify2.txt | 1 + tests/data/mockargs/test_new1.txt | 6 +++++- tests/data/mockargs/test_query1-rhbz.txt | 18 ++++++++++++++++-- tests/data/mockargs/test_query2-rhbz.txt | 5 +++++ tests/data/mockargs/test_update_flags.txt | 1 + tests/test_api_misc.py | 8 +++++++- tests/test_cli_modify.py | 1 + tests/test_cli_new.py | 4 +++- tests/test_cli_query.py | 16 ++++++++++++++-- 11 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 tests/data/clioutput/test_query2-rhbz.txt create mode 100644 tests/data/mockargs/test_query2-rhbz.txt create mode 100644 tests/data/mockargs/test_update_flags.txt diff --git a/bugzilla/base.py b/bugzilla/base.py index 59f31298..14e21a34 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1264,7 +1264,6 @@ def query(self, query): # Try to give a hint in the error message if url_to_query # isn't supported by this bugzilla instance if ("query_format" not in str(e) or - "RHBugzilla" in str(e.__class__) or self._check_version(5, 0)): raise raise BugzillaError("%s\nYour bugzilla instance does not " @@ -1719,14 +1718,15 @@ def _validate_createbug(self, *args, **kwargs): # Previous API required users specifying keyword args that mapped # to the XMLRPC arg names. Maintain that bad compat, but also allow # receiving a single dictionary like query() does - if kwargs and args: + if kwargs and args: # pragma: no cover raise BugzillaError("createbug: cannot specify positional " "args=%s with kwargs=%s, must be one or the " "other." % (args, kwargs)) if args: if len(args) > 1 or not isinstance(args[0], dict): - raise BugzillaError("createbug: positional arguments only " - "accept a single dictionary.") + raise BugzillaError( # pragma: no cover + "createbug: positional arguments only " + "accept a single dictionary.") data = args[0] else: data = kwargs diff --git a/tests/data/clioutput/test_query2-rhbz.txt b/tests/data/clioutput/test_query2-rhbz.txt new file mode 100644 index 00000000..5d93087c --- /dev/null +++ b/tests/data/clioutput/test_query2-rhbz.txt @@ -0,0 +1,2 @@ +#508645 NEW - Libvirt Maintainers - RFE: qemu: Support a managed autoconnect mode for host USB devices +#668543 NEW - Cole Robinson - RFE: warn users at guest start if networks/storage pools are inactive diff --git a/tests/data/mockargs/test_modify2.txt b/tests/data/mockargs/test_modify2.txt index 86794afb..13aa2f41 100644 --- a/tests/data/mockargs/test_modify2.txt +++ b/tests/data/mockargs/test_modify2.txt @@ -1,4 +1,5 @@ {'blocks': {'set': [123456, 445566]}, + 'comment': {'comment': 'some example comment', 'is_private': True}, 'component': 'NEWCOMP', 'dupe_of': 555666, 'flags': [{'name': '-needinfo,+somethingels', 'status': 'e'}], diff --git a/tests/data/mockargs/test_new1.txt b/tests/data/mockargs/test_new1.txt index 50de3ec0..aa61d7c5 100644 --- a/tests/data/mockargs/test_new1.txt +++ b/tests/data/mockargs/test_new1.txt @@ -1,4 +1,8 @@ -{'component': 'FOOCOMP', +{'blocks': ['12345', '6789'], + 'cc': ['foo@example.com', 'bar@example.com'], + 'comment_is_private': True, + 'component': 'FOOCOMP', + 'depends_on': ['dependme'], 'description': 'This is the first comment!\nWith newline & stuff.', 'groups': ['FOOGROUP', 'BARGROUP'], 'keywords': ['ADDKEY'], diff --git a/tests/data/mockargs/test_query1-rhbz.txt b/tests/data/mockargs/test_query1-rhbz.txt index a0fcbbce..b06a128d 100644 --- a/tests/data/mockargs/test_query1-rhbz.txt +++ b/tests/data/mockargs/test_query1-rhbz.txt @@ -1,4 +1,18 @@ -{'component': ['foo', 'bar'], +{'cc': ['foo@example.com'], + 'component': ['foo', 'bar'], + 'field0-0-0': 'keywords', + 'field1-0-0': 'cf_fixed_in', + 'field2-0-0': 'cf_qa_whiteboard', 'id': ['1234', '2480'], 'include_fields': ['assigned_to', 'id', 'status', 'summary'], - 'product': ['foo']} \ No newline at end of file + 'longdesc': 'some comment string', + 'longdesc_type': 'allwordssubstr', + 'product': ['foo'], + 'qa_contact': 'qa@example.com', + 'query_format': 'advanced', + 'type0-0-0': 'substring', + 'type1-0-0': 'substring', + 'type2-0-0': 'substring', + 'value0-0-0': 'fribkeyword', + 'value1-0-0': 'amifixed', + 'value2-0-0': 'some-example-whiteboard'} \ No newline at end of file diff --git a/tests/data/mockargs/test_query2-rhbz.txt b/tests/data/mockargs/test_query2-rhbz.txt new file mode 100644 index 00000000..c724f3ce --- /dev/null +++ b/tests/data/mockargs/test_query2-rhbz.txt @@ -0,0 +1,5 @@ +{'email1': ['foo@example.com'], + 'emailcc1': True, + 'emailtype1': 'BAR', + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'query_format': 'advanced'} \ No newline at end of file diff --git a/tests/data/mockargs/test_update_flags.txt b/tests/data/mockargs/test_update_flags.txt new file mode 100644 index 00000000..934e882e --- /dev/null +++ b/tests/data/mockargs/test_update_flags.txt @@ -0,0 +1 @@ +{'flags': {'name': 'needinfo', 'status': '?'}, 'ids': [12345, 6789]} \ No newline at end of file diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 60c65163..2983e609 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -102,11 +102,17 @@ def test_rhbz_pre_translation(): assert output_query == input_query -def testSubComponentFail(): +def testUpdateFailures(): + # sub_component without component also passed bz = tests.mockbackend.make_bz(version="4.4.0", rhbz=True) with pytest.raises(ValueError): bz.build_update(sub_component="some sub component") + # Trying to update value that only rhbz supports + bz = tests.mockbackend.make_bz() + with pytest.raises(ValueError): + bz.build_update(fixed_in="some fixedin value") + def testCreatebugFieldConversion(): bz4 = tests.mockbackend.make_bz(version="4.0.0") diff --git a/tests/test_cli_modify.py b/tests/test_cli_modify.py index 2f5fee1b..6f4e88f2 100644 --- a/tests/test_cli_modify.py +++ b/tests/test_cli_modify.py @@ -32,6 +32,7 @@ def test_modify(run_cli): cmd += "--flag=-needinfo,+somethingelse " cmd += "--whiteboard =foo --whiteboard =thisone " cmd += "--dupeid 555666 " + cmd += "--comment 'some example comment' --private " fakebz = tests.mockbackend.make_bz( bug_update_args="data/mockargs/test_modify2.txt", bug_update_return={}) diff --git a/tests/test_cli_new.py b/tests/test_cli_new.py index c3ea6dad..6ab25884 100644 --- a/tests/test_cli_new.py +++ b/tests/test_cli_new.py @@ -15,7 +15,9 @@ def test_new(run_cli): cmd = "bugzilla new --product FOOPROD --component FOOCOMP " cmd += "--summary 'Hey this is the title!' " cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " - cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP" + cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP " + cmd += "--blocked 12345,6789 --cc foo@example.com --cc bar@example.com " + cmd += "--dependson dependme --private " fakebz = tests.mockbackend.make_bz( bug_create_args="data/mockargs/test_new1.txt", diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py index 21db51ca..b4028c3d 100644 --- a/tests/test_cli_query.py +++ b/tests/test_cli_query.py @@ -34,15 +34,27 @@ def test_query(run_cli): out = run_cli(cmd, fakebz) tests.utils.diff_compare(out, "data/clioutput/test_query1.txt") - # Simple query with some comma opts + # RHBZ query with a ton of opts cmd = "bugzilla query " - cmd += "--product foo --component foo,bar --bug_id 1234,2480" + cmd += "--product foo --component foo,bar --bug_id 1234,2480 " + cmd += "--keywords fribkeyword --fixed_in amifixed " + cmd += "--qa_whiteboard some-example-whiteboard " + cmd += "--cc foo@example.com --qa_contact qa@example.com " + cmd += "--comment 'some comment string' " fakebz = tests.mockbackend.make_bz(rhbz=True, bug_search_args="data/mockargs/test_query1-rhbz.txt", bug_search_return="data/mockreturn/test_query1.txt") out = run_cli(cmd, fakebz) tests.utils.diff_compare(out, "data/clioutput/test_query1-rhbz.txt") + # --emailtype handling + cmd = "bugzilla query --cc foo@example.com --emailtype BAR " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_search_args="data/mockargs/test_query2-rhbz.txt", + bug_search_return="data/mockreturn/test_query1.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query2-rhbz.txt") + # Same but with --ids output cmd = "bugzilla query --ids " cmd += "--product foo --component foo,bar --bug_id 1234,2480" From f6b6725eadf8f9b5a8ea0f1f8c292af6f5a2ddc4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 16:23:20 -0500 Subject: [PATCH 186/393] tests: Add full Bugzilla attach API coverage Signed-off-by: Cole Robinson --- bugzilla/base.py | 4 +- .../mockargs/test_api_attachments_create1.txt | 5 ++ tests/data/mockargs/test_attachments_get1.txt | 1 + .../mockargs/test_attachments_getall1.txt | 1 + .../mockargs/test_attachments_update1.txt | 2 + tests/mockbackend.py | 2 + tests/test_api_attachments.py | 52 +++++++++++++++++++ 7 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/data/mockargs/test_api_attachments_create1.txt create mode 100644 tests/data/mockargs/test_attachments_get1.txt create mode 100644 tests/data/mockargs/test_attachments_getall1.txt create mode 100644 tests/data/mockargs/test_attachments_update1.txt create mode 100644 tests/test_api_attachments.py diff --git a/bugzilla/base.py b/bugzilla/base.py index 14e21a34..26cdb588 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1549,7 +1549,7 @@ def attachfile(self, idlist, attachfile, description, **kwargs): kwargs['summary'] = description data = f.read() - if not isinstance(data, bytes): + if not isinstance(data, bytes): # pragma: no cover data = data.encode(locale.getpreferredencoding()) kwargs['ids'] = listify(idlist) @@ -1586,7 +1586,7 @@ def openattachment_data(self, attachment_dict): if hasattr(data, "data"): # This is for xmlrpc Binary - content = data.data + content = data.data # pragma: no cover else: import base64 content = base64.b64decode(data) diff --git a/tests/data/mockargs/test_api_attachments_create1.txt b/tests/data/mockargs/test_api_attachments_create1.txt new file mode 100644 index 00000000..c10a7f3e --- /dev/null +++ b/tests/data/mockargs/test_api_attachments_create1.txt @@ -0,0 +1,5 @@ +{'content_type': 'text/plain', + 'file_name': 'bz-attach-get1.txt', + 'ids': [123456], + 'is_private': True, + 'summary': 'some desc'} \ No newline at end of file diff --git a/tests/data/mockargs/test_attachments_get1.txt b/tests/data/mockargs/test_attachments_get1.txt new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/data/mockargs/test_attachments_get1.txt @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/data/mockargs/test_attachments_getall1.txt b/tests/data/mockargs/test_attachments_getall1.txt new file mode 100644 index 00000000..620730f3 --- /dev/null +++ b/tests/data/mockargs/test_attachments_getall1.txt @@ -0,0 +1 @@ +{'exclude_fields': ['bar'], 'include_fields': ['foo']} \ No newline at end of file diff --git a/tests/data/mockargs/test_attachments_update1.txt b/tests/data/mockargs/test_attachments_update1.txt new file mode 100644 index 00000000..03474c4b --- /dev/null +++ b/tests/data/mockargs/test_attachments_update1.txt @@ -0,0 +1,2 @@ +{'flags': [{'is_patch': True, 'name': 'needinfo', 'value': 'foobar'}], + 'ids': [112233]} \ No newline at end of file diff --git a/tests/mockbackend.py b/tests/mockbackend.py index ec3f50b1..ff855134 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -51,6 +51,8 @@ def bug_attachment_get(self, *args): return self.__helper(args) def bug_attachment_get_all(self, *args): return self.__helper(args) + def bug_attachment_update(self, *args): + return self.__helper(args) def bug_create(self, *args): return self.__helper(args) diff --git a/tests/test_api_attachments.py b/tests/test_api_attachments.py new file mode 100644 index 00000000..b526696f --- /dev/null +++ b/tests/test_api_attachments.py @@ -0,0 +1,52 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +import os + +import pytest + +import tests +import tests.mockbackend + + +def test_api_attachments(): + # misc coverage testing for Bugzilla attachment APIs + fakebz = tests.mockbackend.make_bz( + bug_attachment_get_all_args=( + "data/mockargs/test_attachments_getall1.txt"), + bug_attachment_get_all_return={}, + bug_attachment_update_args=( + "data/mockargs/test_attachments_update1.txt"), + bug_attachment_update_return={}, + bug_attachment_get_args=( + "data/mockargs/test_attachments_get1.txt"), + bug_attachment_get_return=( + "data/mockreturn/test_attach_get1.txt"), + bug_attachment_create_args=( + "data/mockargs/test_api_attachments_create1.txt"), + bug_attachment_create_return={ + "attachments": {"123456": {}, "456789": []}}, + ) + + # coverage for include/exclude handling + fakebz.get_attachments([123456], None, + include_fields=["foo"], exclude_fields="bar") + + # coverage for updateattachment + fakebz.updateattachmentflags(None, "112233", "needinfo", + value="foobar", is_patch=True) + + # coverage for openattachment + fobj = fakebz.openattachment(502352) + assert "Hooray" in str(fobj.read()) + + # Error on bad input type + with pytest.raises(TypeError): + fakebz.attachfile([123456], None, "some desc") + + # Misc attachfile() pieces + attachfile = os.path.dirname(__file__) + "/data/bz-attach-get1.txt" + ret = fakebz.attachfile([123456], attachfile, "some desc", + isprivate=True) + assert ret == [123456, 456789] From 650f40d8195b3a864f3827488ea0a9ee503c54b0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 17:08:41 -0500 Subject: [PATCH 187/393] tests: Add full API product/component/bugfields coverage Signed-off-by: Cole Robinson --- .../mockargs/test_api_component_create1.txt | 5 + .../mockargs/test_api_component_update1.txt | 4 + .../data/mockargs/test_api_products_get1.txt | 1 + .../data/mockargs/test_api_products_get2.txt | 1 + .../data/mockargs/test_api_products_get3.txt | 1 + .../data/mockargs/test_api_products_get4.txt | 1 + .../data/mockargs/test_api_products_get5.txt | 1 + tests/data/mockargs/test_bug_fields.txt | 1 + tests/data/mockreturn/test_bug_fields.txt | 391 ++++++++++++++++++ tests/mockbackend.py | 13 + tests/test_api_products.py | 120 ++++++ tests/utils.py | 2 +- 12 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 tests/data/mockargs/test_api_component_create1.txt create mode 100644 tests/data/mockargs/test_api_component_update1.txt create mode 100644 tests/data/mockargs/test_api_products_get1.txt create mode 100644 tests/data/mockargs/test_api_products_get2.txt create mode 100644 tests/data/mockargs/test_api_products_get3.txt create mode 100644 tests/data/mockargs/test_api_products_get4.txt create mode 100644 tests/data/mockargs/test_api_products_get5.txt create mode 100644 tests/data/mockargs/test_bug_fields.txt create mode 100644 tests/data/mockreturn/test_bug_fields.txt create mode 100644 tests/test_api_products.py diff --git a/tests/data/mockargs/test_api_component_create1.txt b/tests/data/mockargs/test_api_component_create1.txt new file mode 100644 index 00000000..a55fbcd5 --- /dev/null +++ b/tests/data/mockargs/test_api_component_create1.txt @@ -0,0 +1,5 @@ +{'default_assignee': 'foo@example.com', + 'default_cc': 'foo3@example.com', + 'default_qa_contact': 'foo2@example.com', + 'is_active': 0, + 'product': 'fooproduct'} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_component_update1.txt b/tests/data/mockargs/test_api_component_update1.txt new file mode 100644 index 00000000..5f760ffe --- /dev/null +++ b/tests/data/mockargs/test_api_component_update1.txt @@ -0,0 +1,4 @@ +{'names': [{'component': 'foocomponent', 'product': 'fooproduct'}], + 'updates': {'blaharg': 'blahval', + 'default_assignee': 'foo@example.com', + 'is_active': 0}} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_products_get1.txt b/tests/data/mockargs/test_api_products_get1.txt new file mode 100644 index 00000000..e9d59ee8 --- /dev/null +++ b/tests/data/mockargs/test_api_products_get1.txt @@ -0,0 +1 @@ +{'ids': [1, 7]} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_products_get2.txt b/tests/data/mockargs/test_api_products_get2.txt new file mode 100644 index 00000000..46e1efd2 --- /dev/null +++ b/tests/data/mockargs/test_api_products_get2.txt @@ -0,0 +1 @@ +{'include_fields': ['name', 'id'], 'names': ['test-fake-product']} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_products_get3.txt b/tests/data/mockargs/test_api_products_get3.txt new file mode 100644 index 00000000..7923519a --- /dev/null +++ b/tests/data/mockargs/test_api_products_get3.txt @@ -0,0 +1 @@ +{'include_fields': ['name', 'id', 'components'], 'names': ['test-fake-product']} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_products_get4.txt b/tests/data/mockargs/test_api_products_get4.txt new file mode 100644 index 00000000..3df1d719 --- /dev/null +++ b/tests/data/mockargs/test_api_products_get4.txt @@ -0,0 +1 @@ +{'exclude_fields': ['product.foo'], 'ids': ['7']} \ No newline at end of file diff --git a/tests/data/mockargs/test_api_products_get5.txt b/tests/data/mockargs/test_api_products_get5.txt new file mode 100644 index 00000000..ee3d4f00 --- /dev/null +++ b/tests/data/mockargs/test_api_products_get5.txt @@ -0,0 +1 @@ +{'include_fields': ['name', 'id'], 'names': [0]} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_fields.txt b/tests/data/mockargs/test_bug_fields.txt new file mode 100644 index 00000000..0ff160fa --- /dev/null +++ b/tests/data/mockargs/test_bug_fields.txt @@ -0,0 +1 @@ +{'include_fields': ['name'], 'names': ['bug_status']} \ No newline at end of file diff --git a/tests/data/mockreturn/test_bug_fields.txt b/tests/data/mockreturn/test_bug_fields.txt new file mode 100644 index 00000000..72c675fa --- /dev/null +++ b/tests/data/mockreturn/test_bug_fields.txt @@ -0,0 +1,391 @@ +# bugzilla.redhat.com 2020-01-10 with {"names": "bug_status"} + +{ + "fields": [ + { + "display_name": "Status", + "id": 2, + "is_custom": False, + "is_mandatory": False, + "is_on_bug_entry": False, + "name": "bug_status", + "type": 2, + "values": [ + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + } + ], + "is_open": True, + "sort_key": 0, + "sortkey": 0, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "NEW", + "sort_key": 10, + "sortkey": 10, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "ASSIGNED", + "sort_key": 20, + "sortkey": 20, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "POST", + "sort_key": 30, + "sortkey": 30, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "MODIFIED", + "sort_key": 40, + "sortkey": 40, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "ON_DEV", + "sort_key": 50, + "sortkey": 50, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "ON_QA", + "sort_key": 60, + "sortkey": 60, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "RELEASE_PENDING" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "VERIFIED", + "sort_key": 70, + "sortkey": 70, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_DEV" + }, + { + "comment_required": False, + "name": "ON_QA" + }, + { + "comment_required": False, + "name": "VERIFIED" + }, + { + "comment_required": False, + "name": "CLOSED" + } + ], + "is_open": True, + "name": "RELEASE_PENDING", + "sort_key": 80, + "sortkey": 80, + "visibility_values": [] + }, + { + "can_change_to": [ + { + "comment_required": False, + "name": "NEW" + }, + { + "comment_required": False, + "name": "ASSIGNED" + }, + { + "comment_required": False, + "name": "POST" + }, + { + "comment_required": False, + "name": "MODIFIED" + }, + { + "comment_required": False, + "name": "ON_QA" + } + ], + "is_open": False, + "name": "CLOSED", + "sort_key": 90, + "sortkey": 90, + "visibility_values": [] + } + ], + "visibility_values": [] + } + ] +} diff --git a/tests/mockbackend.py b/tests/mockbackend.py index ff855134..0d41d199 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -60,6 +60,8 @@ def bug_legal_values(self, *args): return self.__helper(args) def bug_get(self, *args): return self.__helper(args) + def bug_fields(self, *args): + return self.__helper(args) def bug_search(self, *args): return self.__helper(args) def bug_update(self, *args): @@ -67,6 +69,13 @@ def bug_update(self, *args): def bug_update_tags(self, *args): return self.__helper(args) + def component_create(self, *args): + return self.__helper(args) + def component_get(self, *args): + return self.__helper(args) + def component_update(self, *args): + return self.__helper(args) + def externalbugs_add(self, *args): return self.__helper(args) def externalbugs_update(self, *args): @@ -78,6 +87,10 @@ def product_get(self, *args): return self.__helper(args) def product_get_accessible(self, *args): return self.__helper(args) + def product_get_enterable(self, *args): + return self.__helper(args) + def product_get_selectable(self, *args): + return self.__helper(args) def user_create(self, *args): return self.__helper(args) diff --git a/tests/test_api_products.py b/tests/test_api_products.py new file mode 100644 index 00000000..e6c7fe9f --- /dev/null +++ b/tests/test_api_products.py @@ -0,0 +1,120 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import pytest + +import bugzilla + +import tests +import tests.mockbackend + + +def test_api_component_edit(): + fakebz = tests.mockbackend.make_bz( + component_create_args="data/mockargs/test_api_component_create1.txt", + component_create_return={}, + component_update_args="data/mockargs/test_api_component_update1.txt", + component_update_return={}, + ) + + # addcomponent stub testing + fakebz.addcomponent({ + "initialowner": "foo@example.com", + "initialqacontact": "foo2@example.com", + "initialcclist": "foo3@example.com", + "product": "fooproduct", + "is_active": 0, + }) + + # editcomponent stub testing + fakebz.editcomponent({ + "initialowner": "foo@example.com", + "blaharg": "blahval", + "product": "fooproduct", + "component": "foocomponent", + "is_active": 0, + }) + + +def test_api_products(): + prod_list_return = {'ids': [1, 7]} + prod_get_return = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'foo': {"bar": "baz"}, + 'components': [ + {'default_assigned_to': 'Fake Guy', + 'name': 'client-interfaces'}, + {'default_assigned_to': 'ANother fake dude!', + 'name': 'configuration'}, + ]}, + ]} + + compnames = ["client-interfaces", "configuration"] + legal_values = {'values': compnames} + + fakebz = tests.mockbackend.make_bz( + product_get_enterable_args=None, + product_get_enterable_return=prod_list_return, + product_get_selectable_args=None, + product_get_selectable_return=prod_list_return, + product_get_args="data/mockargs/test_api_products_get1.txt", + product_get_return=prod_get_return, + ) + + # enterable products + fakebz.product_get(ptype="enterable") + fakebz.product_get(ptype="selectable") + with pytest.raises(RuntimeError): + fakebz.product_get(ptype="idontknow") + + # Double refresh things + fakebz.getproducts(force_refresh=True, ptype="enterable") + fakebz.getproducts(force_refresh=True, ptype="enterable") + + # getcomponents etc. testing + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get2.txt", + product_get_return=prod_get_return, + bug_legal_values_args=None, + bug_legal_values_return=legal_values, + ) + + # Lookup in product cache by name + ret = fakebz.getcomponents("test-fake-product") + assert ret == compnames + # Lookup in product cache by id + ret = fakebz.getcomponents(7) + assert ret == compnames + # force_refresh but its cool + ret = fakebz.getcomponents("test-fake-product", force_refresh=True) + assert ret == compnames + + # getcomponentsdetails usage + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get3.txt", + product_get_return=prod_get_return, + ) + fakebz.getcomponentdetails("test-fake-product", "configuration") + + # Some bit to test productget exclude_args + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get4.txt", + product_get_return=prod_get_return) + fakebz.product_get(ids=["7"], exclude_fields=["product.foo"]) + + # Unknown product + fakebz = tests.mockbackend.make_bz( + product_get_args="data/mockargs/test_api_products_get5.txt", + product_get_return=prod_get_return) + with pytest.raises(bugzilla.BugzillaError): + fakebz.getcomponents(0) + + + +def test_bug_fields(): + fakebz = tests.mockbackend.make_bz( + bug_fields_args="data/mockargs/test_bug_fields.txt", + bug_fields_return="data/mockreturn/test_bug_fields.txt", + ) + ret = fakebz.getbugfields(names=["bug_status"]) + assert ["bug_status"] == ret diff --git a/tests/utils.py b/tests/utils.py index 1e7b9658..cbf2db86 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,7 +49,7 @@ def diff_compare(inputdata, filename): actual_out = inputdata if isinstance(inputdata, dict): - actual_out = pprint.pformat(inputdata) + actual_out = pprint.pformat(inputdata, width=81) if not os.path.exists(filename) or tests.CLICONFIG.REGENERATE_OUTPUT: open(filename, "w").write(actual_out) From b60617ae65b0481a43de7707dde1c4d01e20144a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 15:49:41 -0500 Subject: [PATCH 188/393] tests: finish Bugzilla code coverage Signed-off-by: Cole Robinson --- bugzilla/base.py | 12 ++++---- tests/mockbackend.py | 4 +++ tests/test_api_misc.py | 65 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 26cdb588..cd9bf827 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -939,9 +939,6 @@ def _process_include_fields(self, include_fields, exclude_fields, Internal helper to process include_fields lists """ def _convert_fields(_in): - if not _in: - return _in - for newname, oldname in self._get_api_aliases(): if oldname in _in: _in.remove(oldname) @@ -1031,7 +1028,7 @@ def _getbugs(self, idlist, permissive, if self._check_version(4, 0): bugdict = dict([(b['id'], b) for b in r['bugs']]) - else: + else: # pragma: no cover bugdict = dict([(b['id'], b['internals']) for b in r['bugs']]) ret = [] @@ -1258,12 +1255,13 @@ def query(self, query): r = self._backend.bug_search(query) log.debug("bug_search returned:\n%s", str(r)) except Exception as e: - if not BugzillaError.get_bugzilla_error_code(e): - raise - # Try to give a hint in the error message if url_to_query # isn't supported by this bugzilla instance + print("query_format" in str(e)) + print(BugzillaError.get_bugzilla_error_code(e)) + print(self._check_version(5, 0)) if ("query_format" not in str(e) or + not BugzillaError.get_bugzilla_error_code(e) or self._check_version(5, 0)): raise raise BugzillaError("%s\nYour bugzilla instance does not " diff --git a/tests/mockbackend.py b/tests/mockbackend.py index 0d41d199..a97b69a4 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -54,10 +54,14 @@ def bug_attachment_get_all(self, *args): def bug_attachment_update(self, *args): return self.__helper(args) + def bug_comments(self, *args): + return self.__helper(args) def bug_create(self, *args): return self.__helper(args) def bug_legal_values(self, *args): return self.__helper(args) + def bug_history(self, *args): + return self.__helper(args) def bug_get(self, *args): return self.__helper(args) def bug_fields(self, *args): diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 2983e609..667acc5f 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -260,3 +260,68 @@ def test_bad_scheme(): bz.connect("ftp://example.com") except Exception as e: assert "Invalid URL scheme: ftp" in str(e) + + +def test_update_flags(): + # update_flags is just a compat wrapper for update_bugs + bz = tests.mockbackend.make_bz( + bug_update_args="data/mockargs/test_update_flags.txt", + bug_update_return={}) + bz.update_flags([12345, 6789], {"name": "needinfo", "status": "?"}) + + +def test_bugs_history_raw(): + # Stub test for bugs_history_raw + ids = ["12345", 567] + bz = tests.mockbackend.make_bz( + bug_history_args={"ids": ids}, + bug_history_return={}) + bz.bugs_history_raw(ids) + + +def test_get_comments(): + # Stub test for get_commands + ids = ["12345", 567] + bz = tests.mockbackend.make_bz( + bug_comments_args={"ids": ids}, + bug_comments_return={}) + bz.get_comments(ids) + + +def test_get_xmlrpc_proxy(): + # Ensure _proxy goes to a backend API + bz = tests.mockbackend.make_bz() + with pytest.raises(NotImplementedError): + dummy = bz._proxy # pylint: disable=protected-access + + +def test_query_url_fail(): + # test some handling of query from_url errors + query = {"query_format": "advanced", "product": "FOO"} + checkstr = "does not appear to support" + + exc = bugzilla.BugzillaError("FAKEERROR query_format", code=123) + bz = tests.mockbackend.make_bz(version="4.0.0", + bug_search_args=None, bug_search_return=exc) + try: + bz.query(query) + except Exception as e: + assert checkstr in str(e) + + bz = tests.mockbackend.make_bz(version="5.1.0", + bug_search_args=None, bug_search_return=exc) + try: + bz.query(query) + except Exception as e: + assert checkstr not in str(e) + + +def test_api_getbugs(): + fakebz = tests.mockbackend.make_bz( + bug_get_args="data/mockargs/test_api_getbugs1.txt", + bug_get_return="data/mockreturn/test_query_cve_getbug.txt") + + fakebz.bug_autorefresh = True + bug = fakebz.getbug("CVE-1234-5678", exclude_fields="foo") + assert bug.alias == ["CVE-1234-5678"] + assert bug.autorefresh is True From 8735749e39737f9e910f7049b03fd14380da7db8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 18:18:07 -0500 Subject: [PATCH 189/393] tests: finish Bug coverage Signed-off-by: Cole Robinson --- bugzilla/bug.py | 4 +- tests/data/mockargs/test_api_getbugs1.txt | 1 + tests/data/mockargs/test_bug_api_comments.txt | 1 + .../mockargs/test_bug_api_get_attachments.txt | 1 + tests/data/mockargs/test_bug_api_history.txt | 1 + .../mockargs/test_bug_apis_addcc_update.txt | 3 + .../test_bug_apis_addcomment_update.txt | 1 + .../mockargs/test_bug_apis_close_update.txt | 6 + .../test_bug_apis_deletecc_update.txt | 3 + .../test_bug_apis_setassignee_update.txt | 4 + .../test_bug_apis_setstatus_update.txt | 3 + .../test_bug_apis_updateflags_update.txt | 1 + tests/test_api_attachments.py | 1 + tests/test_api_bug.py | 105 ++++++++++++++++++ tests/test_api_misc.py | 11 -- 15 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 tests/data/mockargs/test_api_getbugs1.txt create mode 100644 tests/data/mockargs/test_bug_api_comments.txt create mode 100644 tests/data/mockargs/test_bug_api_get_attachments.txt create mode 100644 tests/data/mockargs/test_bug_api_history.txt create mode 100644 tests/data/mockargs/test_bug_apis_addcc_update.txt create mode 100644 tests/data/mockargs/test_bug_apis_addcomment_update.txt create mode 100644 tests/data/mockargs/test_bug_apis_close_update.txt create mode 100644 tests/data/mockargs/test_bug_apis_deletecc_update.txt create mode 100644 tests/data/mockargs/test_bug_apis_setassignee_update.txt create mode 100644 tests/data/mockargs/test_bug_apis_setstatus_update.txt create mode 100644 tests/data/mockargs/test_bug_apis_updateflags_update.txt diff --git a/bugzilla/bug.py b/bugzilla/bug.py index a4515ae6..79102007 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -6,8 +6,6 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -from __future__ import unicode_literals - from logging import getLogger from ._util import to_encoding @@ -211,7 +209,7 @@ def close(self, resolution, dupeid=None, fixedin=None, resolution=resolution, dupe_of=dupeid, fixed_in=fixedin, - status="CLOSED") + status=str("CLOSED")) log.debug("close: update=%s", vals) return self.bugzilla.update_bugs(self.bug_id, vals) diff --git a/tests/data/mockargs/test_api_getbugs1.txt b/tests/data/mockargs/test_api_getbugs1.txt new file mode 100644 index 00000000..394f7893 --- /dev/null +++ b/tests/data/mockargs/test_api_getbugs1.txt @@ -0,0 +1 @@ +{'exclude_fields': 'foo', 'ids': ['CVE-1234-5678']} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_api_comments.txt b/tests/data/mockargs/test_bug_api_comments.txt new file mode 100644 index 00000000..85b39f9a --- /dev/null +++ b/tests/data/mockargs/test_bug_api_comments.txt @@ -0,0 +1 @@ +{'ids': [1165434]} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_api_get_attachments.txt b/tests/data/mockargs/test_bug_api_get_attachments.txt new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/data/mockargs/test_bug_api_get_attachments.txt @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_api_history.txt b/tests/data/mockargs/test_bug_api_history.txt new file mode 100644 index 00000000..85b39f9a --- /dev/null +++ b/tests/data/mockargs/test_bug_api_history.txt @@ -0,0 +1 @@ +{'ids': [1165434]} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_apis_addcc_update.txt b/tests/data/mockargs/test_bug_apis_addcc_update.txt new file mode 100644 index 00000000..1a920b1b --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_addcc_update.txt @@ -0,0 +1,3 @@ +{'cc': {'add': ['foo2@example.com']}, + 'comment': {'comment': 'foocomment'}, + 'ids': [1165434]} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_apis_addcomment_update.txt b/tests/data/mockargs/test_bug_apis_addcomment_update.txt new file mode 100644 index 00000000..f67ba1f4 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_addcomment_update.txt @@ -0,0 +1 @@ +{'comment': {'comment': 'test comment', 'is_private': True}, 'ids': [1165434]} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_apis_close_update.txt b/tests/data/mockargs/test_bug_apis_close_update.txt new file mode 100644 index 00000000..cee13d91 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_close_update.txt @@ -0,0 +1,6 @@ +{'cf_fixed_in': '1.2.3.4.5', + 'comment': {'comment': 'foocomment2'}, + 'dupe_of': 123456, + 'ids': [1165434], + 'resolution': 'UPSTREAM', + 'status': 'CLOSED'} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_apis_deletecc_update.txt b/tests/data/mockargs/test_bug_apis_deletecc_update.txt new file mode 100644 index 00000000..3f2f213e --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_deletecc_update.txt @@ -0,0 +1,3 @@ +{'cc': {'remove': ['foo2@example.com']}, + 'comment': {'comment': 'foocomment'}, + 'ids': [1165434]} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_apis_setassignee_update.txt b/tests/data/mockargs/test_bug_apis_setassignee_update.txt new file mode 100644 index 00000000..44737519 --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_setassignee_update.txt @@ -0,0 +1,4 @@ +{'assigned_to': 'foo@example.com', + 'comment': {'comment': 'foocomment'}, + 'ids': [1165434], + 'qa_contact': 'bar@example.com'} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_apis_setstatus_update.txt b/tests/data/mockargs/test_bug_apis_setstatus_update.txt new file mode 100644 index 00000000..4717794b --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_setstatus_update.txt @@ -0,0 +1,3 @@ +{'comment': {'comment': 'foocomment', 'is_private': True}, + 'ids': [1165434], + 'status': 'POST'} \ No newline at end of file diff --git a/tests/data/mockargs/test_bug_apis_updateflags_update.txt b/tests/data/mockargs/test_bug_apis_updateflags_update.txt new file mode 100644 index 00000000..99e6481e --- /dev/null +++ b/tests/data/mockargs/test_bug_apis_updateflags_update.txt @@ -0,0 +1 @@ +{'flags': [{'name': 'someflag', 'status': 'someval'}], 'ids': [1165434]} diff --git a/tests/test_api_attachments.py b/tests/test_api_attachments.py index b526696f..3eb4c0d5 100644 --- a/tests/test_api_attachments.py +++ b/tests/test_api_attachments.py @@ -49,4 +49,5 @@ def test_api_attachments(): attachfile = os.path.dirname(__file__) + "/data/bz-attach-get1.txt" ret = fakebz.attachfile([123456], attachfile, "some desc", isprivate=True) + ret.sort() assert ret == [123456, 456789] diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index 3229b710..ec678908 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -12,6 +12,8 @@ import pickle import sys +import pytest + import tests import tests.mockbackend import tests.utils @@ -78,3 +80,106 @@ def testBugNoID(): raise AssertionError("Expected lack of ID failure.") except TypeError: pass + + +def test_api_getbugs(): + fakebz = tests.mockbackend.make_bz( + bug_get_args="data/mockargs/test_api_getbugs1.txt", + bug_get_return="data/mockreturn/test_query_cve_getbug.txt") + + fakebz.bug_autorefresh = True + bug = fakebz.getbug("CVE-1234-5678", exclude_fields="foo") + assert bug.alias == ["CVE-1234-5678"] + assert bug.autorefresh is True + + +def test_bug_getattr(): + fakebz = tests.mockbackend.make_bz( + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + bug = fakebz.getbug(1165434) + + with pytest.raises(AttributeError): + # Hits a specific codepath in Bug.__getattr__ + dummy = bug.__baditem__ + + bug.autorefresh = True + summary = bug.summary + del(bug.__dict__["summary"]) + # Trigger autorefresh + assert bug.summary == summary + + +def test_bug_apis(): + def _get_fake_bug(apiname): + update_args = "data/mockargs/test_bug_apis_%s_update.txt" % apiname + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt", + bug_update_args=update_args, + bug_update_return={}) + return fakebz.getbug(1165434) + + # bug.setstatus, wrapper for update_bugs + bug = _get_fake_bug("setstatus") + bug.setstatus("POST", "foocomment", private=True) + + # bug.close, wrapper for update_bugs + bug = _get_fake_bug("close") + bug.close("UPSTREAM", dupeid=123456, comment="foocomment2", + isprivate=False, fixedin="1.2.3.4.5") + + # bug.setassignee, wrapper for update_bugs + bug = _get_fake_bug("setassignee") + bug.setassignee( + assigned_to="foo@example.com", qa_contact="bar@example.com", + comment="foocomment") + with pytest.raises(ValueError): + # Hits a validation path + bug.setassignee() + + # bug.addcc test + bug = _get_fake_bug("addcc") + bug.addcc("foo2@example.com", comment="foocomment") + + # bug.deletecc test + bug = _get_fake_bug("deletecc") + bug.deletecc("foo2@example.com", comment="foocomment") + + # bug.addcomment test + bug = _get_fake_bug("addcomment") + bug.addcomment("test comment", private=True) + + # bug.updateflags test + bug = _get_fake_bug("updateflags") + bug.updateflags({"someflag": "someval"}) + + # Some minor flag API tests + assert "creation_date" in bug.get_flag_type("needinfo") + assert bug.get_flag_type("NOPE") is None + assert bug.get_flags("NOPE") is None + assert bug.get_flag_status("NOPE") is None + + # Minor get_history_raw wrapper + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_history_args="data/mockargs/test_bug_api_history.txt", + bug_history_return={}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt", + bug_comments_args="data/mockargs/test_bug_api_comments.txt", + bug_comments_return={"bugs": {"1165434": {"comments": []}}}, + bug_attachment_get_all_args=( + "data/mockargs/test_bug_api_get_attachments.txt"), + bug_attachment_get_all_return="data/mockreturn/test_attach_get2.txt", + ) + + # Stub API testing + bug = fakebz.getbug(1165434) + bug.get_history_raw() + bug.getcomments() + + # Some hackery to hit a few attachment code paths + bug.id = 663674 + attachments = bug.get_attachments() + bug.attachments = attachments + assert [469147, 470041, 502352] == bug.get_attachment_ids() diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 667acc5f..42622e93 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -314,14 +314,3 @@ def test_query_url_fail(): bz.query(query) except Exception as e: assert checkstr not in str(e) - - -def test_api_getbugs(): - fakebz = tests.mockbackend.make_bz( - bug_get_args="data/mockargs/test_api_getbugs1.txt", - bug_get_return="data/mockreturn/test_query_cve_getbug.txt") - - fakebz.bug_autorefresh = True - bug = fakebz.getbug("CVE-1234-5678", exclude_fields="foo") - assert bug.alias == ["CVE-1234-5678"] - assert bug.autorefresh is True From 225ddb213de0a9558535fc3ddb67175a15dc9d54 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 18:22:15 -0500 Subject: [PATCH 190/393] tests: Add ending newline to test data Makes diffs easier to read Signed-off-by: Cole Robinson --- tests/data/mockargs/test_api_attachments_create1.txt | 2 +- tests/data/mockargs/test_api_component_create1.txt | 2 +- tests/data/mockargs/test_api_component_update1.txt | 2 +- tests/data/mockargs/test_api_getbugs1.txt | 2 +- tests/data/mockargs/test_api_login1.txt | 2 +- tests/data/mockargs/test_api_products_get1.txt | 2 +- tests/data/mockargs/test_api_products_get2.txt | 2 +- tests/data/mockargs/test_api_products_get3.txt | 2 +- tests/data/mockargs/test_api_products_get4.txt | 2 +- tests/data/mockargs/test_api_products_get5.txt | 2 +- tests/data/mockargs/test_api_users_create.txt | 2 +- tests/data/mockargs/test_api_users_get1.txt | 2 +- tests/data/mockargs/test_api_users_get2.txt | 2 +- tests/data/mockargs/test_api_users_get3.txt | 2 +- tests/data/mockargs/test_api_users_update1.txt | 2 +- tests/data/mockargs/test_attach1.txt | 2 +- tests/data/mockargs/test_attach2.txt | 2 +- tests/data/mockargs/test_attach_get1.txt | 2 +- tests/data/mockargs/test_attach_get2.txt | 2 +- tests/data/mockargs/test_attachments_get1.txt | 2 +- tests/data/mockargs/test_attachments_getall1.txt | 2 +- tests/data/mockargs/test_attachments_update1.txt | 2 +- tests/data/mockargs/test_bug_api_comments.txt | 2 +- tests/data/mockargs/test_bug_api_get_attachments.txt | 2 +- tests/data/mockargs/test_bug_api_history.txt | 2 +- tests/data/mockargs/test_bug_apis_addcc_update.txt | 2 +- tests/data/mockargs/test_bug_apis_addcomment_update.txt | 2 +- tests/data/mockargs/test_bug_apis_close_update.txt | 2 +- tests/data/mockargs/test_bug_apis_deletecc_update.txt | 2 +- tests/data/mockargs/test_bug_apis_setassignee_update.txt | 2 +- tests/data/mockargs/test_bug_apis_setstatus_update.txt | 2 +- tests/data/mockargs/test_bug_fields.txt | 2 +- tests/data/mockargs/test_externalbugs_add.txt | 2 +- tests/data/mockargs/test_externalbugs_remove.txt | 2 +- tests/data/mockargs/test_externalbugs_update.txt | 2 +- tests/data/mockargs/test_info_components-active.txt | 2 +- tests/data/mockargs/test_info_components-legalvalues.txt | 2 +- tests/data/mockargs/test_info_components-owners.txt | 2 +- tests/data/mockargs/test_info_components.txt | 2 +- tests/data/mockargs/test_info_products.txt | 2 +- tests/data/mockargs/test_info_versions.txt | 2 +- tests/data/mockargs/test_interactive_login.txt | 2 +- tests/data/mockargs/test_login-restrict.txt | 2 +- tests/data/mockargs/test_login.txt | 2 +- tests/data/mockargs/test_modify1.txt | 2 +- tests/data/mockargs/test_modify2.txt | 2 +- tests/data/mockargs/test_modify3-tags.txt | 2 +- tests/data/mockargs/test_modify3.txt | 2 +- tests/data/mockargs/test_modify4.txt | 2 +- tests/data/mockargs/test_new1.txt | 2 +- tests/data/mockargs/test_query1-ids.txt | 2 +- tests/data/mockargs/test_query1-rhbz.txt | 2 +- tests/data/mockargs/test_query1.txt | 2 +- tests/data/mockargs/test_query2-rhbz.txt | 2 +- tests/data/mockargs/test_query2.txt | 2 +- tests/data/mockargs/test_query3.txt | 2 +- tests/data/mockargs/test_query4.txt | 2 +- tests/data/mockargs/test_query5.txt | 2 +- tests/data/mockargs/test_query6.txt | 2 +- tests/data/mockargs/test_query7.txt | 2 +- tests/data/mockargs/test_query_cve_getbug.txt | 2 +- tests/data/mockargs/test_update_flags.txt | 2 +- tests/utils.py | 2 ++ 63 files changed, 64 insertions(+), 62 deletions(-) diff --git a/tests/data/mockargs/test_api_attachments_create1.txt b/tests/data/mockargs/test_api_attachments_create1.txt index c10a7f3e..86c6e10f 100644 --- a/tests/data/mockargs/test_api_attachments_create1.txt +++ b/tests/data/mockargs/test_api_attachments_create1.txt @@ -2,4 +2,4 @@ 'file_name': 'bz-attach-get1.txt', 'ids': [123456], 'is_private': True, - 'summary': 'some desc'} \ No newline at end of file + 'summary': 'some desc'} diff --git a/tests/data/mockargs/test_api_component_create1.txt b/tests/data/mockargs/test_api_component_create1.txt index a55fbcd5..a59238a0 100644 --- a/tests/data/mockargs/test_api_component_create1.txt +++ b/tests/data/mockargs/test_api_component_create1.txt @@ -2,4 +2,4 @@ 'default_cc': 'foo3@example.com', 'default_qa_contact': 'foo2@example.com', 'is_active': 0, - 'product': 'fooproduct'} \ No newline at end of file + 'product': 'fooproduct'} diff --git a/tests/data/mockargs/test_api_component_update1.txt b/tests/data/mockargs/test_api_component_update1.txt index 5f760ffe..8234405e 100644 --- a/tests/data/mockargs/test_api_component_update1.txt +++ b/tests/data/mockargs/test_api_component_update1.txt @@ -1,4 +1,4 @@ {'names': [{'component': 'foocomponent', 'product': 'fooproduct'}], 'updates': {'blaharg': 'blahval', 'default_assignee': 'foo@example.com', - 'is_active': 0}} \ No newline at end of file + 'is_active': 0}} diff --git a/tests/data/mockargs/test_api_getbugs1.txt b/tests/data/mockargs/test_api_getbugs1.txt index 394f7893..59bff597 100644 --- a/tests/data/mockargs/test_api_getbugs1.txt +++ b/tests/data/mockargs/test_api_getbugs1.txt @@ -1 +1 @@ -{'exclude_fields': 'foo', 'ids': ['CVE-1234-5678']} \ No newline at end of file +{'exclude_fields': 'foo', 'ids': ['CVE-1234-5678']} diff --git a/tests/data/mockargs/test_api_login1.txt b/tests/data/mockargs/test_api_login1.txt index a94a371a..36ee16ac 100644 --- a/tests/data/mockargs/test_api_login1.txt +++ b/tests/data/mockargs/test_api_login1.txt @@ -1 +1 @@ -{'login': None, 'password': None} \ No newline at end of file +{'login': None, 'password': None} diff --git a/tests/data/mockargs/test_api_products_get1.txt b/tests/data/mockargs/test_api_products_get1.txt index e9d59ee8..8275b104 100644 --- a/tests/data/mockargs/test_api_products_get1.txt +++ b/tests/data/mockargs/test_api_products_get1.txt @@ -1 +1 @@ -{'ids': [1, 7]} \ No newline at end of file +{'ids': [1, 7]} diff --git a/tests/data/mockargs/test_api_products_get2.txt b/tests/data/mockargs/test_api_products_get2.txt index 46e1efd2..e2d58722 100644 --- a/tests/data/mockargs/test_api_products_get2.txt +++ b/tests/data/mockargs/test_api_products_get2.txt @@ -1 +1 @@ -{'include_fields': ['name', 'id'], 'names': ['test-fake-product']} \ No newline at end of file +{'include_fields': ['name', 'id'], 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_api_products_get3.txt b/tests/data/mockargs/test_api_products_get3.txt index 7923519a..9220969e 100644 --- a/tests/data/mockargs/test_api_products_get3.txt +++ b/tests/data/mockargs/test_api_products_get3.txt @@ -1 +1 @@ -{'include_fields': ['name', 'id', 'components'], 'names': ['test-fake-product']} \ No newline at end of file +{'include_fields': ['name', 'id', 'components'], 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_api_products_get4.txt b/tests/data/mockargs/test_api_products_get4.txt index 3df1d719..f193b73a 100644 --- a/tests/data/mockargs/test_api_products_get4.txt +++ b/tests/data/mockargs/test_api_products_get4.txt @@ -1 +1 @@ -{'exclude_fields': ['product.foo'], 'ids': ['7']} \ No newline at end of file +{'exclude_fields': ['product.foo'], 'ids': ['7']} diff --git a/tests/data/mockargs/test_api_products_get5.txt b/tests/data/mockargs/test_api_products_get5.txt index ee3d4f00..d2566d2f 100644 --- a/tests/data/mockargs/test_api_products_get5.txt +++ b/tests/data/mockargs/test_api_products_get5.txt @@ -1 +1 @@ -{'include_fields': ['name', 'id'], 'names': [0]} \ No newline at end of file +{'include_fields': ['name', 'id'], 'names': [0]} diff --git a/tests/data/mockargs/test_api_users_create.txt b/tests/data/mockargs/test_api_users_create.txt index 9415ac88..5daeae42 100644 --- a/tests/data/mockargs/test_api_users_create.txt +++ b/tests/data/mockargs/test_api_users_create.txt @@ -1 +1 @@ -{'email': 'example1@example.com', 'name': 'fooname', 'password': 'foopass'} \ No newline at end of file +{'email': 'example1@example.com', 'name': 'fooname', 'password': 'foopass'} diff --git a/tests/data/mockargs/test_api_users_get1.txt b/tests/data/mockargs/test_api_users_get1.txt index eb9dd5ab..fa10ab9f 100644 --- a/tests/data/mockargs/test_api_users_get1.txt +++ b/tests/data/mockargs/test_api_users_get1.txt @@ -1 +1 @@ -{'names': ['example2@example.com']} \ No newline at end of file +{'names': ['example2@example.com']} diff --git a/tests/data/mockargs/test_api_users_get2.txt b/tests/data/mockargs/test_api_users_get2.txt index 93be1f65..26109bc5 100644 --- a/tests/data/mockargs/test_api_users_get2.txt +++ b/tests/data/mockargs/test_api_users_get2.txt @@ -1 +1 @@ -{'names': ['example1@example.com']} \ No newline at end of file +{'names': ['example1@example.com']} diff --git a/tests/data/mockargs/test_api_users_get3.txt b/tests/data/mockargs/test_api_users_get3.txt index b2e23734..688392fc 100644 --- a/tests/data/mockargs/test_api_users_get3.txt +++ b/tests/data/mockargs/test_api_users_get3.txt @@ -1 +1 @@ -{'match': ['example1@example.com']} \ No newline at end of file +{'match': ['example1@example.com']} diff --git a/tests/data/mockargs/test_api_users_update1.txt b/tests/data/mockargs/test_api_users_update1.txt index 3847634a..cb313590 100644 --- a/tests/data/mockargs/test_api_users_update1.txt +++ b/tests/data/mockargs/test_api_users_update1.txt @@ -1 +1 @@ -{'groups': {'remove': ['fedora_contrib']}, 'names': ['example name']} \ No newline at end of file +{'groups': {'remove': ['fedora_contrib']}, 'names': ['example name']} diff --git a/tests/data/mockargs/test_attach1.txt b/tests/data/mockargs/test_attach1.txt index ec8a02ee..e16bc1b7 100644 --- a/tests/data/mockargs/test_attach1.txt +++ b/tests/data/mockargs/test_attach1.txt @@ -4,4 +4,4 @@ 'ids': ['123456'], 'is_patch': True, 'is_private': True, - 'summary': 'bz-attach-get1.txt'} \ No newline at end of file + 'summary': 'bz-attach-get1.txt'} diff --git a/tests/data/mockargs/test_attach2.txt b/tests/data/mockargs/test_attach2.txt index aa296c52..0e074bc4 100644 --- a/tests/data/mockargs/test_attach2.txt +++ b/tests/data/mockargs/test_attach2.txt @@ -1,4 +1,4 @@ {'content_type': 'text/plain', 'file_name': 'fake-file-name.txt', 'ids': ['123456'], - 'summary': 'Some attachment description'} \ No newline at end of file + 'summary': 'Some attachment description'} diff --git a/tests/data/mockargs/test_attach_get1.txt b/tests/data/mockargs/test_attach_get1.txt index 9e26dfee..0967ef42 100644 --- a/tests/data/mockargs/test_attach_get1.txt +++ b/tests/data/mockargs/test_attach_get1.txt @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/tests/data/mockargs/test_attach_get2.txt b/tests/data/mockargs/test_attach_get2.txt index 9e26dfee..0967ef42 100644 --- a/tests/data/mockargs/test_attach_get2.txt +++ b/tests/data/mockargs/test_attach_get2.txt @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/tests/data/mockargs/test_attachments_get1.txt b/tests/data/mockargs/test_attachments_get1.txt index 9e26dfee..0967ef42 100644 --- a/tests/data/mockargs/test_attachments_get1.txt +++ b/tests/data/mockargs/test_attachments_get1.txt @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/tests/data/mockargs/test_attachments_getall1.txt b/tests/data/mockargs/test_attachments_getall1.txt index 620730f3..9b5f5136 100644 --- a/tests/data/mockargs/test_attachments_getall1.txt +++ b/tests/data/mockargs/test_attachments_getall1.txt @@ -1 +1 @@ -{'exclude_fields': ['bar'], 'include_fields': ['foo']} \ No newline at end of file +{'exclude_fields': ['bar'], 'include_fields': ['foo']} diff --git a/tests/data/mockargs/test_attachments_update1.txt b/tests/data/mockargs/test_attachments_update1.txt index 03474c4b..56d69cdf 100644 --- a/tests/data/mockargs/test_attachments_update1.txt +++ b/tests/data/mockargs/test_attachments_update1.txt @@ -1,2 +1,2 @@ {'flags': [{'is_patch': True, 'name': 'needinfo', 'value': 'foobar'}], - 'ids': [112233]} \ No newline at end of file + 'ids': [112233]} diff --git a/tests/data/mockargs/test_bug_api_comments.txt b/tests/data/mockargs/test_bug_api_comments.txt index 85b39f9a..79181268 100644 --- a/tests/data/mockargs/test_bug_api_comments.txt +++ b/tests/data/mockargs/test_bug_api_comments.txt @@ -1 +1 @@ -{'ids': [1165434]} \ No newline at end of file +{'ids': [1165434]} diff --git a/tests/data/mockargs/test_bug_api_get_attachments.txt b/tests/data/mockargs/test_bug_api_get_attachments.txt index 9e26dfee..0967ef42 100644 --- a/tests/data/mockargs/test_bug_api_get_attachments.txt +++ b/tests/data/mockargs/test_bug_api_get_attachments.txt @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/tests/data/mockargs/test_bug_api_history.txt b/tests/data/mockargs/test_bug_api_history.txt index 85b39f9a..79181268 100644 --- a/tests/data/mockargs/test_bug_api_history.txt +++ b/tests/data/mockargs/test_bug_api_history.txt @@ -1 +1 @@ -{'ids': [1165434]} \ No newline at end of file +{'ids': [1165434]} diff --git a/tests/data/mockargs/test_bug_apis_addcc_update.txt b/tests/data/mockargs/test_bug_apis_addcc_update.txt index 1a920b1b..c684d228 100644 --- a/tests/data/mockargs/test_bug_apis_addcc_update.txt +++ b/tests/data/mockargs/test_bug_apis_addcc_update.txt @@ -1,3 +1,3 @@ {'cc': {'add': ['foo2@example.com']}, 'comment': {'comment': 'foocomment'}, - 'ids': [1165434]} \ No newline at end of file + 'ids': [1165434]} diff --git a/tests/data/mockargs/test_bug_apis_addcomment_update.txt b/tests/data/mockargs/test_bug_apis_addcomment_update.txt index f67ba1f4..ac00345a 100644 --- a/tests/data/mockargs/test_bug_apis_addcomment_update.txt +++ b/tests/data/mockargs/test_bug_apis_addcomment_update.txt @@ -1 +1 @@ -{'comment': {'comment': 'test comment', 'is_private': True}, 'ids': [1165434]} \ No newline at end of file +{'comment': {'comment': 'test comment', 'is_private': True}, 'ids': [1165434]} diff --git a/tests/data/mockargs/test_bug_apis_close_update.txt b/tests/data/mockargs/test_bug_apis_close_update.txt index cee13d91..d4edea5a 100644 --- a/tests/data/mockargs/test_bug_apis_close_update.txt +++ b/tests/data/mockargs/test_bug_apis_close_update.txt @@ -3,4 +3,4 @@ 'dupe_of': 123456, 'ids': [1165434], 'resolution': 'UPSTREAM', - 'status': 'CLOSED'} \ No newline at end of file + 'status': 'CLOSED'} diff --git a/tests/data/mockargs/test_bug_apis_deletecc_update.txt b/tests/data/mockargs/test_bug_apis_deletecc_update.txt index 3f2f213e..80b86d96 100644 --- a/tests/data/mockargs/test_bug_apis_deletecc_update.txt +++ b/tests/data/mockargs/test_bug_apis_deletecc_update.txt @@ -1,3 +1,3 @@ {'cc': {'remove': ['foo2@example.com']}, 'comment': {'comment': 'foocomment'}, - 'ids': [1165434]} \ No newline at end of file + 'ids': [1165434]} diff --git a/tests/data/mockargs/test_bug_apis_setassignee_update.txt b/tests/data/mockargs/test_bug_apis_setassignee_update.txt index 44737519..040cf29c 100644 --- a/tests/data/mockargs/test_bug_apis_setassignee_update.txt +++ b/tests/data/mockargs/test_bug_apis_setassignee_update.txt @@ -1,4 +1,4 @@ {'assigned_to': 'foo@example.com', 'comment': {'comment': 'foocomment'}, 'ids': [1165434], - 'qa_contact': 'bar@example.com'} \ No newline at end of file + 'qa_contact': 'bar@example.com'} diff --git a/tests/data/mockargs/test_bug_apis_setstatus_update.txt b/tests/data/mockargs/test_bug_apis_setstatus_update.txt index 4717794b..1fc937e2 100644 --- a/tests/data/mockargs/test_bug_apis_setstatus_update.txt +++ b/tests/data/mockargs/test_bug_apis_setstatus_update.txt @@ -1,3 +1,3 @@ {'comment': {'comment': 'foocomment', 'is_private': True}, 'ids': [1165434], - 'status': 'POST'} \ No newline at end of file + 'status': 'POST'} diff --git a/tests/data/mockargs/test_bug_fields.txt b/tests/data/mockargs/test_bug_fields.txt index 0ff160fa..4d770533 100644 --- a/tests/data/mockargs/test_bug_fields.txt +++ b/tests/data/mockargs/test_bug_fields.txt @@ -1 +1 @@ -{'include_fields': ['name'], 'names': ['bug_status']} \ No newline at end of file +{'include_fields': ['name'], 'names': ['bug_status']} diff --git a/tests/data/mockargs/test_externalbugs_add.txt b/tests/data/mockargs/test_externalbugs_add.txt index c197610e..fbf1cd3a 100644 --- a/tests/data/mockargs/test_externalbugs_add.txt +++ b/tests/data/mockargs/test_externalbugs_add.txt @@ -5,4 +5,4 @@ 'ext_status': 'CLOSED', 'ext_type_description': 'some-bug-add-description', 'ext_type_id': 'launchpad', - 'ext_type_url': 'https://example.com/launchpad/1234'}]} \ No newline at end of file + 'ext_type_url': 'https://example.com/launchpad/1234'}]} diff --git a/tests/data/mockargs/test_externalbugs_remove.txt b/tests/data/mockargs/test_externalbugs_remove.txt index 28849215..02930c2f 100644 --- a/tests/data/mockargs/test_externalbugs_remove.txt +++ b/tests/data/mockargs/test_externalbugs_remove.txt @@ -3,4 +3,4 @@ 'ext_type_description': 'foo-desc', 'ext_type_id': 'footype', 'ext_type_url': 'foo-url', - 'ids': ['remove1']} \ No newline at end of file + 'ids': ['remove1']} diff --git a/tests/data/mockargs/test_externalbugs_update.txt b/tests/data/mockargs/test_externalbugs_update.txt index de448358..dbc266e2 100644 --- a/tests/data/mockargs/test_externalbugs_update.txt +++ b/tests/data/mockargs/test_externalbugs_update.txt @@ -6,4 +6,4 @@ 'ext_type_description': 'some-bug-update', 'ext_type_id': 'mozilla', 'ext_type_url': 'https://mozilla.foo/bar/5678', - 'ids': ['external1', 'external2']} \ No newline at end of file + 'ids': ['external1', 'external2']} diff --git a/tests/data/mockargs/test_info_components-active.txt b/tests/data/mockargs/test_info_components-active.txt index 92b664f6..c526a897 100644 --- a/tests/data/mockargs/test_info_components-active.txt +++ b/tests/data/mockargs/test_info_components-active.txt @@ -1,2 +1,2 @@ {'include_fields': ['name', 'id', 'components.name', 'components.is_active'], - 'names': ['test-fake-product']} \ No newline at end of file + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_info_components-legalvalues.txt b/tests/data/mockargs/test_info_components-legalvalues.txt index d6dcd341..90c21939 100644 --- a/tests/data/mockargs/test_info_components-legalvalues.txt +++ b/tests/data/mockargs/test_info_components-legalvalues.txt @@ -1 +1 @@ -{'field': 'component', 'product_id': 7} \ No newline at end of file +{'field': 'component', 'product_id': 7} diff --git a/tests/data/mockargs/test_info_components-owners.txt b/tests/data/mockargs/test_info_components-owners.txt index 62b8cf45..4523e133 100644 --- a/tests/data/mockargs/test_info_components-owners.txt +++ b/tests/data/mockargs/test_info_components-owners.txt @@ -2,4 +2,4 @@ 'id', 'components.name', 'components.default_assigned_to'], - 'names': ['test-fake-product']} \ No newline at end of file + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_info_components.txt b/tests/data/mockargs/test_info_components.txt index 8f1c76ad..e90b8640 100644 --- a/tests/data/mockargs/test_info_components.txt +++ b/tests/data/mockargs/test_info_components.txt @@ -1,2 +1,2 @@ {'include_fields': ['name', 'id', 'components.name'], - 'names': ['test-fake-product']} \ No newline at end of file + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_info_products.txt b/tests/data/mockargs/test_info_products.txt index 3a9690e1..fd9d9c8b 100644 --- a/tests/data/mockargs/test_info_products.txt +++ b/tests/data/mockargs/test_info_products.txt @@ -1 +1 @@ -{'ids': [1, 7], 'include_fields': ['name', 'id']} \ No newline at end of file +{'ids': [1, 7], 'include_fields': ['name', 'id']} diff --git a/tests/data/mockargs/test_info_versions.txt b/tests/data/mockargs/test_info_versions.txt index e93f737e..50dc6e00 100644 --- a/tests/data/mockargs/test_info_versions.txt +++ b/tests/data/mockargs/test_info_versions.txt @@ -1 +1 @@ -{'include_fields': ['name', 'id', 'versions'], 'names': ['test-fake-product']} \ No newline at end of file +{'include_fields': ['name', 'id', 'versions'], 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_interactive_login.txt b/tests/data/mockargs/test_interactive_login.txt index c2b83597..24933de8 100644 --- a/tests/data/mockargs/test_interactive_login.txt +++ b/tests/data/mockargs/test_interactive_login.txt @@ -1 +1 @@ -{'login': 'fakeuser', 'password': 'fakepass'} \ No newline at end of file +{'login': 'fakeuser', 'password': 'fakepass'} diff --git a/tests/data/mockargs/test_login-restrict.txt b/tests/data/mockargs/test_login-restrict.txt index 18122767..a6d82c48 100644 --- a/tests/data/mockargs/test_login-restrict.txt +++ b/tests/data/mockargs/test_login-restrict.txt @@ -1 +1 @@ -{'login': 'FOO', 'password': 'BAR', 'restrict_login': True} \ No newline at end of file +{'login': 'FOO', 'password': 'BAR', 'restrict_login': True} diff --git a/tests/data/mockargs/test_login.txt b/tests/data/mockargs/test_login.txt index 309ed7bf..a1334c12 100644 --- a/tests/data/mockargs/test_login.txt +++ b/tests/data/mockargs/test_login.txt @@ -1 +1 @@ -{'login': 'FOO', 'password': 'BAR'} \ No newline at end of file +{'login': 'FOO', 'password': 'BAR'} diff --git a/tests/data/mockargs/test_modify1.txt b/tests/data/mockargs/test_modify1.txt index c0331f48..905b037e 100644 --- a/tests/data/mockargs/test_modify1.txt +++ b/tests/data/mockargs/test_modify1.txt @@ -1 +1 @@ -{'component': 'NEWCOMP', 'ids': ['123456', '1234567'], 'status': 'ASSIGNED'} \ No newline at end of file +{'component': 'NEWCOMP', 'ids': ['123456', '1234567'], 'status': 'ASSIGNED'} diff --git a/tests/data/mockargs/test_modify2.txt b/tests/data/mockargs/test_modify2.txt index 13aa2f41..66c279ee 100644 --- a/tests/data/mockargs/test_modify2.txt +++ b/tests/data/mockargs/test_modify2.txt @@ -8,4 +8,4 @@ 'keywords': {'add': ['FOO']}, 'resolution': 'DUPLICATE', 'status': 'CLOSED', - 'whiteboard': 'thisone'} \ No newline at end of file + 'whiteboard': 'thisone'} diff --git a/tests/data/mockargs/test_modify3-tags.txt b/tests/data/mockargs/test_modify3-tags.txt index 7870724d..cfbd5d7a 100644 --- a/tests/data/mockargs/test_modify3-tags.txt +++ b/tests/data/mockargs/test_modify3-tags.txt @@ -1 +1 @@ -{'ids': ['1165434'], 'tags': {'add': ['addtag'], 'remove': ['rmtag']}} \ No newline at end of file +{'ids': ['1165434'], 'tags': {'add': ['addtag'], 'remove': ['rmtag']}} diff --git a/tests/data/mockargs/test_modify3.txt b/tests/data/mockargs/test_modify3.txt index 30e4ff31..f17e5a87 100644 --- a/tests/data/mockargs/test_modify3.txt +++ b/tests/data/mockargs/test_modify3.txt @@ -1,4 +1,4 @@ {'cf_devel_whiteboard': 'somedeveltag,someothertag devel-duh', 'cf_internal_whiteboard': 'someinternal TAG internal-hey bar', 'cf_qa_whiteboard': 'bar baz yo-qa', - 'ids': [1165434]} \ No newline at end of file + 'ids': [1165434]} diff --git a/tests/data/mockargs/test_modify4.txt b/tests/data/mockargs/test_modify4.txt index 31a579ee..b8271f91 100644 --- a/tests/data/mockargs/test_modify4.txt +++ b/tests/data/mockargs/test_modify4.txt @@ -1,4 +1,4 @@ {'cf_fixed_in': 'foofixedin', 'component': 'lvm2', 'ids': ['1165434'], - 'sub_components': {'lvm2': ['some-sub-component']}} \ No newline at end of file + 'sub_components': {'lvm2': ['some-sub-component']}} diff --git a/tests/data/mockargs/test_new1.txt b/tests/data/mockargs/test_new1.txt index aa61d7c5..78f518b7 100644 --- a/tests/data/mockargs/test_new1.txt +++ b/tests/data/mockargs/test_new1.txt @@ -7,4 +7,4 @@ 'groups': ['FOOGROUP', 'BARGROUP'], 'keywords': ['ADDKEY'], 'product': 'FOOPROD', - 'summary': 'Hey this is the title!'} \ No newline at end of file + 'summary': 'Hey this is the title!'} diff --git a/tests/data/mockargs/test_query1-ids.txt b/tests/data/mockargs/test_query1-ids.txt index 85288fb1..1f2bb4b3 100644 --- a/tests/data/mockargs/test_query1-ids.txt +++ b/tests/data/mockargs/test_query1-ids.txt @@ -1,4 +1,4 @@ {'component': ['foo', 'bar'], 'id': ['1234', '2480'], 'include_fields': ['id'], - 'product': ['foo']} \ No newline at end of file + 'product': ['foo']} diff --git a/tests/data/mockargs/test_query1-rhbz.txt b/tests/data/mockargs/test_query1-rhbz.txt index b06a128d..76d342e0 100644 --- a/tests/data/mockargs/test_query1-rhbz.txt +++ b/tests/data/mockargs/test_query1-rhbz.txt @@ -15,4 +15,4 @@ 'type2-0-0': 'substring', 'value0-0-0': 'fribkeyword', 'value1-0-0': 'amifixed', - 'value2-0-0': 'some-example-whiteboard'} \ No newline at end of file + 'value2-0-0': 'some-example-whiteboard'} diff --git a/tests/data/mockargs/test_query1.txt b/tests/data/mockargs/test_query1.txt index a0fcbbce..7b37ed0f 100644 --- a/tests/data/mockargs/test_query1.txt +++ b/tests/data/mockargs/test_query1.txt @@ -1,4 +1,4 @@ {'component': ['foo', 'bar'], 'id': ['1234', '2480'], 'include_fields': ['assigned_to', 'id', 'status', 'summary'], - 'product': ['foo']} \ No newline at end of file + 'product': ['foo']} diff --git a/tests/data/mockargs/test_query2-rhbz.txt b/tests/data/mockargs/test_query2-rhbz.txt index c724f3ce..8889d7f7 100644 --- a/tests/data/mockargs/test_query2-rhbz.txt +++ b/tests/data/mockargs/test_query2-rhbz.txt @@ -2,4 +2,4 @@ 'emailcc1': True, 'emailtype1': 'BAR', 'include_fields': ['assigned_to', 'id', 'status', 'summary'], - 'query_format': 'advanced'} \ No newline at end of file + 'query_format': 'advanced'} diff --git a/tests/data/mockargs/test_query2.txt b/tests/data/mockargs/test_query2.txt index 2918d38f..d499186e 100644 --- a/tests/data/mockargs/test_query2.txt +++ b/tests/data/mockargs/test_query2.txt @@ -1 +1 @@ -{'id': ['1165434'], 'include_fields': ['id']} \ No newline at end of file +{'id': ['1165434'], 'include_fields': ['id']} diff --git a/tests/data/mockargs/test_query3.txt b/tests/data/mockargs/test_query3.txt index 466fe072..a1e7c651 100644 --- a/tests/data/mockargs/test_query3.txt +++ b/tests/data/mockargs/test_query3.txt @@ -7,4 +7,4 @@ 'flags_requestee', 'foo', 'whiteboard', - 'id']} \ No newline at end of file + 'id']} diff --git a/tests/data/mockargs/test_query4.txt b/tests/data/mockargs/test_query4.txt index 27565c82..6bcd62e6 100644 --- a/tests/data/mockargs/test_query4.txt +++ b/tests/data/mockargs/test_query4.txt @@ -13,4 +13,4 @@ 'depends_on', 'id', 'status', - 'summary']} \ No newline at end of file + 'summary']} diff --git a/tests/data/mockargs/test_query5.txt b/tests/data/mockargs/test_query5.txt index 44710361..8551539f 100644 --- a/tests/data/mockargs/test_query5.txt +++ b/tests/data/mockargs/test_query5.txt @@ -7,4 +7,4 @@ 'qa_whiteboard', 'status', 'summary', - 'whiteboard']} \ No newline at end of file + 'whiteboard']} diff --git a/tests/data/mockargs/test_query6.txt b/tests/data/mockargs/test_query6.txt index 3118a5fc..78b18f48 100644 --- a/tests/data/mockargs/test_query6.txt +++ b/tests/data/mockargs/test_query6.txt @@ -8,4 +8,4 @@ 'keywords', 'status', 'target_milestone', - 'id']} \ No newline at end of file + 'id']} diff --git a/tests/data/mockargs/test_query7.txt b/tests/data/mockargs/test_query7.txt index c3b74a13..56c5fe3e 100644 --- a/tests/data/mockargs/test_query7.txt +++ b/tests/data/mockargs/test_query7.txt @@ -12,4 +12,4 @@ 'include_fields': ['assigned_to', 'id', 'status', 'summary'], 'order': 'bug_status,bug_id', 'product': 'Fedora', - 'query_format': 'advanced'} \ No newline at end of file + 'query_format': 'advanced'} diff --git a/tests/data/mockargs/test_query_cve_getbug.txt b/tests/data/mockargs/test_query_cve_getbug.txt index 825b67df..6a2485b1 100644 --- a/tests/data/mockargs/test_query_cve_getbug.txt +++ b/tests/data/mockargs/test_query_cve_getbug.txt @@ -1 +1 @@ -{'ids': [123456]} \ No newline at end of file +{'ids': [123456]} diff --git a/tests/data/mockargs/test_update_flags.txt b/tests/data/mockargs/test_update_flags.txt index 934e882e..458ae42f 100644 --- a/tests/data/mockargs/test_update_flags.txt +++ b/tests/data/mockargs/test_update_flags.txt @@ -1 +1 @@ -{'flags': {'name': 'needinfo', 'status': '?'}, 'ids': [12345, 6789]} \ No newline at end of file +{'flags': {'name': 'needinfo', 'status': '?'}, 'ids': [12345, 6789]} diff --git a/tests/utils.py b/tests/utils.py index cbf2db86..2853e638 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -50,6 +50,8 @@ def diff_compare(inputdata, filename): actual_out = inputdata if isinstance(inputdata, dict): actual_out = pprint.pformat(inputdata, width=81) + if not actual_out.endswith("\n"): + actual_out += "\n" if not os.path.exists(filename) or tests.CLICONFIG.REGENERATE_OUTPUT: open(filename, "w").write(actual_out) From 8fb61c09a3323ff3af94bba96bcc8fee650e9c5c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 19:13:24 -0500 Subject: [PATCH 191/393] conftest: Default to --verbosity=2 with functional tests Signed-off-by: Cole Robinson --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index b002d941..e6832013 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,7 @@ def pytest_ignore_collect(path, config): if is_rw and not has_rw: return True elif skip_rest: + config.option.verbose = 2 return True From 6b8840f3a0608a5ad7688da718bc5f3b5701cfd0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 19:13:37 -0500 Subject: [PATCH 192/393] tests: Some python2 functional test fixes Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 2 +- tests/test_rw_functional.py | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 41bcd81f..88dcb5e5 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -187,7 +187,7 @@ def testQueryFormat(run_cli): # Unicode in this bug's summary args = "--bug_id 522796 --outputformat \"%{summary}\"" out = run_cli("bugzilla query %s" % args, bz) - assert "V34 — system" in out + assert u"V34 — system" in out def testQueryURL(run_cli): diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 89f2c6ea..975fde3b 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -167,7 +167,7 @@ def test04NewBugAllFields(run_cli): # Check bug's minimal history ret = bug.get_history_raw() assert len(ret["bugs"]) == 1 - assert len(ret["bugs"][0]["history"]) == 1 + assert len(ret["bugs"][0]["history"]) def test05ModifyStatus(run_cli): @@ -816,22 +816,6 @@ def compare(data, newid): ("You are not allowed" in str(e))) -def test12SetCookie(): - bz = _open_bz(cookiefile=-1, tokenfile=None, configpaths=[]) - - try: - bz.cookiefile = None - raise AssertionError("Setting cookiefile for active connection " - "should fail.") - except RuntimeError as e: - assert "disconnect()" in str(e) - - bz.disconnect() - bz.cookiefile = None - bz.connect() - assert not bz.logged_in - - def test13SubComponents(): bz = _open_bz() # Long closed RHEL5 lvm2 bug. This component has sub_components From 0fbfe82b7376dd48f8f6c8c9a5f29f1f0af92cbb Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 19:14:52 -0500 Subject: [PATCH 193/393] tox: Add latest py3 versions to env list Signed-off-by: Cole Robinson --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 11cb1d7e..7fa3c94c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36 +envlist = py27,py34,py35,py36,py37,py38 [testenv] sitepackages = True From 82d3d45f295ee79fe1a828a78d41feddc9a0da6d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 19:34:07 -0500 Subject: [PATCH 194/393] base: Pass value to user_get for logged_in check The XMLRPC backend doesn't care if a value is passed or not, and will reject auth with the current empty list. The REST API does care though. So try to resolve id=1 Signed-off-by: Cole Robinson --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index cd9bf827..99cdd60d 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -648,7 +648,7 @@ def logged_in(self): http://bugzilla.readthedocs.org/en/latest/api/core/v1/user.html#valid-login """ try: - self._backend.user_get({"ids": []}) + self._backend.user_get({"ids": [1]}) return True except Exception as e: code = BugzillaError.get_bugzilla_error_code(e) From b8a237769edc7c81a403db7e57c3f24e11eaa4a6 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 20:00:44 -0500 Subject: [PATCH 195/393] Remove some debug print()s Signed-off-by: Cole Robinson --- bugzilla/base.py | 3 --- tests/mockbackend.py | 1 - 2 files changed, 4 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 99cdd60d..8fc05b34 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1257,9 +1257,6 @@ def query(self, query): except Exception as e: # Try to give a hint in the error message if url_to_query # isn't supported by this bugzilla instance - print("query_format" in str(e)) - print(BugzillaError.get_bugzilla_error_code(e)) - print(self._check_version(5, 0)) if ("query_format" not in str(e) or not BugzillaError.get_bugzilla_error_code(e) or self._check_version(5, 0)): diff --git a/tests/mockbackend.py b/tests/mockbackend.py index a97b69a4..6c4a8a28 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -34,7 +34,6 @@ def __helper(self, args): raise func_return if isinstance(func_args, dict): - print(args[-1]) assert func_args == args[-1] elif func_args is not None: tests.utils.diff_compare(args[-1], func_args) From 59ded916864d11ec845b5861f5e05a212ffb1556 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 10 Jan 2020 20:04:02 -0500 Subject: [PATCH 196/393] Bugzilla: clarify that use_creds is a one time init operation And stop carrying self._use_creds which isn't being used Signed-off-by: Cole Robinson --- bugzilla/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 8fc05b34..9c3c1b28 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -196,8 +196,9 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, If -1, use the default path. If None, don't use or save any tokenfile. :param use_creds: If False, this disables cookiefile, tokenfile, - and any bugzillarc reading. This overwrites any tokenfile - or cookiefile settings + and configpaths by default. This is a convenience option to + unset those values at init time. If those values are later + changed, they may be used for future operations. :param sslverify: Maps to 'requests' sslverify parameter. Set to False to disable SSL verification, but it can also be a path to file or directory for custom certs. @@ -226,8 +227,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._cookiecache = _BugzillaCookieCache() self._tokencache = _BugzillaTokenCache() - self._use_creds = use_creds - if not self._use_creds: + if not use_creds: cookiefile = None tokenfile = None configpaths = [] From c19a862825e0253cf449c26ccb6f87cd41dbe67c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 09:16:03 -0500 Subject: [PATCH 197/393] backendbase: Save the url to self._url The REST backend will want this Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 8a93c7d3..57c56f63 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -9,7 +9,7 @@ class _BackendBase(object): the code, but this is all internal to the library. """ def __init__(self, url, bugzillasession): - dummy = url + self._url = url self._bugzillasession = bugzillasession ################# From 6c734d1728943d62ec721b6a9bcc741ba84bcf6c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 09:19:11 -0500 Subject: [PATCH 198/393] Bugzilla: Add is_rest() and is_xmlrpc() public APIs This allows API users to inspect what backend was chosen Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 12 ++++++++++++ bugzilla/_backendxmlrpc.py | 2 ++ bugzilla/base.py | 12 ++++++++++++ tests/test_api_misc.py | 3 +++ 4 files changed, 29 insertions(+) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 57c56f63..e042da88 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -22,6 +22,18 @@ def get_xmlrpc_proxy(self): """ raise NotImplementedError() + def is_rest(self): + """ + :returns: True if this is the REST backend + """ + return False + + def is_xmlrpc(self): + """ + :returns: True if this is the XMLRPC backend + """ + return False + ###################### # Bugzilla info APIs # diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 9b0e0337..d8128562 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -153,6 +153,8 @@ def __init__(self, url, bugzillasession): def get_xmlrpc_proxy(self): return self._xmlrpc_proxy + def is_xmlrpc(self): + return True def bugzilla_version(self): return self._xmlrpc_proxy.Bugzilla.version() diff --git a/bugzilla/base.py b/bugzilla/base.py index 9c3c1b28..7984de5c 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -508,6 +508,18 @@ def _proxy(self): """ return self._backend.get_xmlrpc_proxy() + def is_xmlrpc(self): + """ + :returns: True if using the XMLRPC API + """ + return self._backend.is_xmlrpc() + + def is_rest(self): + """ + :returns: True if using the REST API + """ + return self._backend.is_rest() + def disconnect(self): """ Disconnect from the given bugzilla instance. diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 42622e93..3dfb4360 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -294,6 +294,9 @@ def test_get_xmlrpc_proxy(): with pytest.raises(NotImplementedError): dummy = bz._proxy # pylint: disable=protected-access + assert bz.is_xmlrpc() is False + assert bz.is_rest() is False + def test_query_url_fail(): # test some handling of query from_url errors From 7fcdd2f3985deda7594f7be11ea87cc8548506d9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 09:31:44 -0500 Subject: [PATCH 199/393] Add stub _BackendREST This only implements 'version' and 'extensions' APIs for now. Nothing wires it up yet Signed-off-by: Cole Robinson --- bugzilla/_backendrest.py | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 bugzilla/_backendrest.py diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py new file mode 100644 index 00000000..e6c52570 --- /dev/null +++ b/bugzilla/_backendrest.py @@ -0,0 +1,82 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import json +import logging +import os + +from ._backendbase import _BackendBase +from .exceptions import BugzillaError + + +log = logging.getLogger(__name__) + + +# XXX remove this pylint disable +# pylint: disable=abstract-method + + +class _BackendREST(_BackendBase): + """ + Internal interface for direct calls to bugzilla's REST API + """ + def __init__(self, url, bugzillasession): + _BackendBase.__init__(self, url, bugzillasession) + self._bugzillasession.set_content_type("application/json") + + + ######################### + # Internal REST helpers # + ######################### + + def _handle_response(self, response): + response.raise_for_status() + text = response.text.encode("utf-8") + + try: + ret = dict(json.loads(text)) + except Exception: + log.debug("Failed to parse REST response. Output is:\n%s", text) + raise + + if ret.get("error", False): + raise BugzillaError(ret["message"], code=ret["code"]) + return ret + + def _op(self, optype, apiurl, paramdict=None): + fullurl = os.path.join(self._url, apiurl.lstrip("/")) + log.debug("Bugzilla REST %s %s params=%s", optype, fullurl, paramdict) + session = self._bugzillasession.get_requests_session() + data = json.dumps(paramdict or {}) + + if optype == "POST": + response = session.post(fullurl, data=data) + elif optype == "PUT": + response = session.put(fullurl, data=data) + else: + response = session.get(fullurl, params=paramdict) + + return self._handle_response(response) + + def _get(self, *args, **kwargs): + return self._op("GET", *args, **kwargs) + def _put(self, *args, **kwargs): + return self._op("PUT", *args, **kwargs) + def _post(self, *args, **kwargs): + return self._op("POST", *args, **kwargs) + + + ####################### + # API implementations # + ####################### + + def get_xmlrpc_proxy(self): + raise BugzillaError("You are using the bugzilla REST API, " + "so raw XMLRPC access is not provided.") + def is_rest(self): + return True + + def bugzilla_version(self): + return self._get("/version") + def bugzilla_extensions(self): + return self._get("/extensions") From 3a5eebc7e5169476dd79485076f25c6f618d5865 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 15:24:15 -0500 Subject: [PATCH 200/393] Bugzilla: fix pre_translation when no include_fields are specified Signed-off-by: Cole Robinson --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 7984de5c..97b4c6e7 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1289,7 +1289,7 @@ def pre_translation(self, query): if self._is_redhat_bugzilla: _RHBugzillaConverters.pre_translation(query) query.update(self._process_include_fields( - query["include_fields"], None, None)) + query.get("include_fields", []), None, None)) def post_translation(self, query, bug): """ From 1b0c95a544dbdeedaee426f558cedae02bc13b58 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 09:33:19 -0500 Subject: [PATCH 201/393] Bugzilla: Add force_xmlrpc and force_rest params We continue to default to XMLRPC, but this allows forcing one way or the other. Signed-off-by: Cole Robinson --- bugzilla/base.py | 31 +++++++++++++++++++++++++------ tests/test_api_misc.py | 2 ++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 97b4c6e7..ecb18b57 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -18,6 +18,7 @@ from ._authfiles import (_BugzillaRCFile, _BugzillaCookieCache, _BugzillaTokenCache) from .apiversion import __version__ +from ._backendrest import _BackendREST from ._backendxmlrpc import _BackendXMLRPC from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl from .bug import Bug, User @@ -138,9 +139,11 @@ def url_to_query(url): return q @staticmethod - def fix_url(url): + def fix_url(url, force_rest=False): """ Turn passed url into a bugzilla XMLRPC web url + + :param force_rest: If True, generate a REST API url """ scheme, netloc, path, params, query, fragment = urlparse(url) if not scheme: @@ -152,8 +155,10 @@ def fix_url(url): path = "/".join(path.split("/")[1:]) or None if not path: - log.debug('No path given for url, assuming /xmlrpc.cgi') path = 'xmlrpc.cgi' + if force_rest: + path = "rest/" + log.debug('No path given for url, assuming /%s', path) newurl = urlunparse((scheme, netloc, path, params, query, fragment)) if newurl != url: @@ -174,7 +179,8 @@ def get_rcfile_default_url(): def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None, - cert=None, configpaths=-1, basic_auth=False): + cert=None, configpaths=-1, basic_auth=False, + force_rest=False, force_xmlrpc=False): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at @@ -205,6 +211,10 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, :param api_key: A bugzilla5+ API key :param configpaths: A list of possible bugzillarc locations. :param basic_auth: Use headers with HTTP Basic authentication + :param force_rest: Force use of the REST API + :param force_xmlrpc: Force use of the XMLRPC API. If neither force_X + parameter are specified, heuristics will be used to determine + which API to use, with XMLRPC preferred for back compatability. """ if url == -1: raise TypeError("Specify a valid bugzilla url, or pass url=None") @@ -227,6 +237,9 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._cookiecache = _BugzillaCookieCache() self._tokencache = _BugzillaTokenCache() + self._force_rest = force_rest + self._force_xmlrpc = force_xmlrpc + if not use_creds: cookiefile = None tokenfile = None @@ -448,9 +461,15 @@ def _set_bz_version(self, version): self.bz_ver_major = 5 self.bz_ver_minor = 0 - def _get_backend_class(self): + def _get_backend_class(self): # pragma: no cover # This is a hook for the test suite to do some mock hackery - return _BackendXMLRPC # pragma: no cover + if self._force_rest: + return _BackendREST + elif self._force_xmlrpc: + return _BackendXMLRPC + # default to XMLRPC, like before + # FIXME: guess backend? + return _BackendXMLRPC def connect(self, url=None): """ @@ -469,7 +488,7 @@ def connect(self, url=None): if url is None and self.url: url = self.url - url = self.fix_url(url) + url = self.fix_url(url, force_rest=self._force_rest) self.url = url # we've changed URLs - reload config diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 3dfb4360..a1749fb7 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -33,6 +33,8 @@ def testUserAgent(): def test_fixurl(): assert (bugzilla.Bugzilla.fix_url("example.com") == "https://example.com/xmlrpc.cgi") + assert (bugzilla.Bugzilla.fix_url("example.com", force_rest=True) == + "https://example.com/rest/") assert (bugzilla.Bugzilla.fix_url("example.com/xmlrpc.cgi") == "https://example.com/xmlrpc.cgi") assert (bugzilla.Bugzilla.fix_url("http://example.com/somepath.cgi") == From b4602d5cb1d8bf7ddcb3e25439caedf2eccd5d1f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 09:40:33 -0500 Subject: [PATCH 202/393] tests: Add 'backends' fixture to run test with both REST and XMLRPC Add one example for bugs.mozilla.org Signed-off-by: Cole Robinson --- tests/conftest.py | 11 +++++++++++ tests/test_ro_functional.py | 12 +++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e6832013..123b4569 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,6 +74,17 @@ def pytest_configure(config): os.environ["HOME"] = os.path.dirname(__file__) + "/data/homedir" +def pytest_generate_tests(metafunc): + """ + If a test requests the 'backends' fixture, run that test with both + force_rest=True and force_xmlrpc=True Bugzilla options + """ + if 'backends' in metafunc.fixturenames: + values = [{"force_xmlrpc": True}, {"force_rest": True}] + ids = ["XMLRPC", "REST"] + metafunc.parametrize("backends", values, ids=ids, scope="session") + + @pytest.fixture def run_cli(capsys, monkeypatch): """ diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 88dcb5e5..3b1d5c00 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -24,7 +24,13 @@ def _open_bz(url, **kwargs): if "use_creds" not in kwargs: kwargs["use_creds"] = False - return bugzilla.Bugzilla(url, **kwargs) + bz = bugzilla.Bugzilla(url, **kwargs) + + if kwargs.get("force_rest", False): + assert bz.is_rest() is True + if kwargs.get("force_xmlrpc", False): + assert bz.is_xmlrpc() is True + return bz def _check(out, mincount, expectstr): @@ -45,9 +51,9 @@ def _test_version(bz, bzversion): # mozilla testing # ################### -def test_mozilla(): +def test_mozilla(backends): url = "bugzilla.mozilla.org" - bz = _open_bz(url) + bz = _open_bz(url, **backends) # bugzilla.mozilla.org returns version values in YYYY-MM-DD # format, so just try to confirm that From 465f1cd8f0ea8f008d16a8e66bfde432c7f3253f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 09:42:58 -0500 Subject: [PATCH 203/393] tests: Add --only-rest and --only-xmlrpc options To limit the 'backends' parameterized tests to use only one method Signed-off-by: Cole Robinson --- tests/__init__.py | 2 ++ tests/conftest.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 4d1bdf97..df9650b9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,6 +8,8 @@ class _CLICONFIG(object): def __init__(self): self.REDHAT_URL = None self.REGENERATE_OUTPUT = False + self.ONLY_REST = False + self.ONLY_XMLRPC = False CLICONFIG = _CLICONFIG() diff --git a/tests/conftest.py b/tests/conftest.py index 123b4569..c0c467e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,6 +38,8 @@ def pytest_addoption(parser): parser.addoption("--regenerate-output", action="store_true", default=False, help=("Force regeneration of generated test output")) + parser.addoption("--only-rest", action="store_true", default=False) + parser.addoption("--only-xmlrpc", action="store_true", default=False) def pytest_ignore_collect(path, config): @@ -72,6 +74,10 @@ def pytest_configure(config): # Functional tests need access to HOME cached auth. # Unit tests shouldn't be touching any HOME files os.environ["HOME"] = os.path.dirname(__file__) + "/data/homedir" + if config.getoption("--only-rest"): + tests.CLICONFIG.ONLY_REST = True + if config.getoption("--only-xmlrpc"): + tests.CLICONFIG.ONLY_XMLRPC = True def pytest_generate_tests(metafunc): @@ -80,8 +86,14 @@ def pytest_generate_tests(metafunc): force_rest=True and force_xmlrpc=True Bugzilla options """ if 'backends' in metafunc.fixturenames: - values = [{"force_xmlrpc": True}, {"force_rest": True}] - ids = ["XMLRPC", "REST"] + values = [] + ids = [] + if not tests.CLICONFIG.ONLY_REST: + values.append({"force_xmlrpc": True}) + ids.append("XMLRPC") + if not tests.CLICONFIG.ONLY_XMLRPC: + values.append({"force_rest": True}) + ids.append("REST") metafunc.parametrize("backends", values, ids=ids, scope="session") From b1d6b228cda5df365f523a655df032563d13d450 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 11:39:15 -0500 Subject: [PATCH 204/393] Bugzilla: add get_requests_session() API Return the backing requests.Session object to the user, incase they need to specify any special config to communicate with bugzilla Signed-off-by: Cole Robinson --- bugzilla/base.py | 9 +++++++++ tests/test_api_misc.py | 1 + 2 files changed, 10 insertions(+) diff --git a/bugzilla/base.py b/bugzilla/base.py index ecb18b57..3f6a3fc8 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -539,6 +539,15 @@ def is_rest(self): """ return self._backend.is_rest() + def get_requests_session(self): + """ + Give API users access to the Requests.session object we use for + talking to the remote bugzilla instance. + + :returns: The Requests.session object backing the open connection. + """ + return self._session.get_requests_session() + def disconnect(self): """ Disconnect from the given bugzilla instance. diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index a1749fb7..e55c75c0 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -298,6 +298,7 @@ def test_get_xmlrpc_proxy(): assert bz.is_xmlrpc() is False assert bz.is_rest() is False + assert hasattr(bz.get_requests_session(), "request") def test_query_url_fail(): From 304e39016e1dc243b8f6ccf5effe040ce7013861 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 11:38:12 -0500 Subject: [PATCH 205/393] tests: Add requests timeout hackery Timeout requests after 60 seconds Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 8 +------- tests/test_rw_functional.py | 10 +++++----- tests/utils.py | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 3b1d5c00..16c7d9a5 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -24,13 +24,7 @@ def _open_bz(url, **kwargs): if "use_creds" not in kwargs: kwargs["use_creds"] = False - bz = bugzilla.Bugzilla(url, **kwargs) - - if kwargs.get("force_rest", False): - assert bz.is_rest() is True - if kwargs.get("force_xmlrpc", False): - assert bz.is_xmlrpc() is True - return bz + return tests.utils.open_functional_bz(bugzilla.Bugzilla, url, kwargs) def _check(out, mincount, expectstr): diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 975fde3b..0517f8e5 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -33,7 +33,11 @@ def _split_int(s): return [int(i) for i in s.split(",")] -if not bugzilla.RHBugzilla(url=RHURL).logged_in: +def _open_bz(**kwargs): + return tests.utils.open_functional_bz(bugzilla.RHBugzilla, RHURL, kwargs) + + +if not _open_bz().logged_in: print("\nR/W tests require cached login credentials for url=%s\n" % RHURL) sys.exit(1) @@ -50,10 +54,6 @@ def _check_have_admin(bz): return ret -def _open_bz(**kwargs): - return bugzilla.RHBugzilla(url=RHURL, **kwargs) - - def test0LoggedInNoCreds(): bz = _open_bz(use_creds=False) assert not bz.logged_in diff --git a/tests/utils.py b/tests/utils.py index 2853e638..3a454831 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -43,6 +43,27 @@ def monkeypatch_getpass(monkeypatch): raw_input) # pylint: disable=undefined-variable +def open_functional_bz(bzclass, url, kwargs): + bz = bzclass(url, **kwargs) + + if kwargs.get("force_rest", False): + assert bz.is_rest() is True + if kwargs.get("force_xmlrpc", False): + assert bz.is_xmlrpc() is True + + # Set a session timeout of 30 seconds + session = bz.get_requests_session() + origrequest = session.request + + def fake_request(*args, **kwargs): + if "timeout" not in kwargs: + kwargs["timeout"] = 60 + return origrequest(*args, **kwargs) + + session.request = fake_request + return bz + + def diff_compare(inputdata, filename): """Compare passed string output to contents of filename""" filename = tests_path(filename) From 45fa1df11452b3c4f74fe370ea7a5e12ef49abf5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 12:20:35 -0500 Subject: [PATCH 206/393] tests: ro: Remove some redundant tests * double connect is unittestable * the cert fail bit is unittestable * the query --flags bit uses privileged data so it's tough to maintain Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 16c7d9a5..ffd63d2a 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -11,8 +11,6 @@ Unit tests that do readonly functional tests against real bugzilla instances. """ -import os - import bugzilla import tests @@ -208,26 +206,6 @@ def testQueryURL(run_cli): _check(out, 22, "#553878 CLOSED") -def testDoubleConnect(): - bz = _open_bz(REDHAT_URL) - bz.connect(REDHAT_URL) - - -def testQueryFlags(run_cli): - bz = _open_bz(REDHAT_URL) - - if not bz.logged_in: - print("not logged in, skipping testQueryFlags") - return - - out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 5' " - "--component virt-manager --bug_status CLOSED " - "--flag rhel-5.4.0+", bz) - assert len(out.splitlines()) > 13 - assert len(out.splitlines()) < 26 - assert "223805" in out - - def testQueryFixedIn(run_cli): bz = _open_bz(REDHAT_URL) @@ -348,17 +326,7 @@ def testFaults(run_cli): assert "--nosslverify" in out -def testCertFail(run_cli): - # No public setup that I know of to test cert succeeds, so - # let's give it a bogus file and ensure it fails - badcert = os.path.join(os.path.dirname(__file__), "..", "README.md") - out = run_cli( - "bugzilla --cert %s query --bug_id 123456" % badcert, - None, expectfail=True) - assert "PEM" in out - - -def test_redhat(): +def test_redhat_version(): bzversion = (5, 0) bz = _open_bz(REDHAT_URL) From 6b3be48f1e5fd3fb2e9f30e68d6501a7da5df7ec Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 12:24:27 -0500 Subject: [PATCH 207/393] tests: rw: Rename ambiguous test Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 0517f8e5..52409b5a 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -59,7 +59,7 @@ def test0LoggedInNoCreds(): assert not bz.logged_in -def test2(): +def test0ClassDetection(): bz = bugzilla.Bugzilla(RHURL, use_creds=False) assert bz.__class__ is bugzilla.RHBugzilla From 813f56475748f4a2d787bf624b47f4f2cd9dedfe Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 12:24:36 -0500 Subject: [PATCH 208/393] tests: rw: Remove function with only one user Just open code it Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 52409b5a..75aa19f5 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -835,20 +835,15 @@ def test13SubComponents(): "Default / Unclassified (RHEL5)"]} -def _deleteAllExistingExternalTrackers(bugid): - bz = _open_bz() - ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] - if ids != []: - bz.remove_external_tracker(ids=ids) - - def test14ExternalTrackersAddUpdateRemoveQuery(): bz = _open_bz() bugid = 461686 ext_bug_id = 380489 # Delete any existing external trackers to get to a known state - _deleteAllExistingExternalTrackers(bugid) + ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] + if ids != []: + bz.remove_external_tracker(ids=ids) url = "https://bugzilla.mozilla.org" if bz.bz_ver_major < 5: From c8c2bee2eb45cb059df1a750b1a61bbc423e9abc Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 12:45:22 -0500 Subject: [PATCH 209/393] Bugzilla: drop 'extensions' checking for redhat detection This inserts an API call into class startup for all non-rh bugzilla instances, which isn't very nice. The other detection metric, of the url containing bugzilla.redhat.com, should catch all RH bugzilla instances I know about. If someone needs to trick things beyond that, they can use URL + "?bugzilla.redhat.com" which shouldn't interfere with correct operation of the API Signed-off-by: Cole Robinson --- bugzilla/base.py | 9 --------- tests/mockbackend.py | 19 +++++++------------ 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 3f6a3fc8..f692410b 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -269,15 +269,6 @@ def _detect_is_redhat_bugzilla(self): log.info("Using RHBugzilla for URL containing bugzilla.redhat.com") return True - try: - extensions = self._backend.bugzilla_extensions() - if "RedHat" in extensions.get('extensions', {}): - log.info("Found RedHat bugzilla extension, " - "using RHBugzilla") - return True - except Exception: - log.debug("Failed to fetch bugzilla extensions", exc_info=True) - return False def _init_class_from_url(self): diff --git a/tests/mockbackend.py b/tests/mockbackend.py index 6c4a8a28..587d88f9 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -14,12 +14,9 @@ class BackendMock(_BackendBase): _version = None - _extensions = None def bugzilla_version(self): return {"version": self._version} - def bugzilla_extensions(self): - return self._extensions def __helper(self, args): # Grab the calling function name and use it to generate @@ -107,16 +104,10 @@ def user_update(self, *args): return self.__helper(args) -def _make_backend_class(version="6.0.0", extensions=None, - rhbz=False, **kwargs): - if not extensions: - extensions = {"extensions": {"foo": {"version": "0.01"}}} - if rhbz: - extensions["extensions"]['RedHat'] = {'version': '0.3'} +def _make_backend_class(version="6.0.0", **kwargs): class TmpBackendClass(BackendMock): _version = version - _extensions = extensions for key, val in kwargs.items(): setattr(TmpBackendClass, "_%s" % key, val) @@ -124,7 +115,7 @@ class TmpBackendClass(BackendMock): return TmpBackendClass -def make_bz(bz_kwargs=None, **kwargs): +def make_bz(bz_kwargs=None, rhbz=False, **kwargs): bz_kwargs = (bz_kwargs or {}).copy() if "url" in bz_kwargs: raise RuntimeError("Can't set 'url' in mock make_bz, use connect()") @@ -135,5 +126,9 @@ def make_bz(bz_kwargs=None, **kwargs): backendclass = _make_backend_class(**kwargs) # pylint: disable=protected-access bz._get_backend_class = lambda *a, **k: backendclass - bz.connect("https:///TESTSUITEMOCK") + + url = "https:///TESTSUITEMOCK" + if rhbz: + url += "?fakeredhat=bugzilla.redhat.com" + bz.connect(url) return bz From 0f5058477cc99210de0d2347c3050003e49ceea0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 12:48:36 -0500 Subject: [PATCH 210/393] backend: Remove bugzilla_extensions We no longer use it internally Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 7 ------- bugzilla/_backendrest.py | 2 -- bugzilla/_backendxmlrpc.py | 2 -- 3 files changed, 11 deletions(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index e042da88..03724044 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -46,13 +46,6 @@ def bugzilla_version(self): """ raise NotImplementedError() - def bugzilla_extensions(self): - """ - Return info about Bugzilla extensions - http://bugzilla.readthedocs.io/en/latest/api/core/v1/bugzilla.html#extensions - """ - raise NotImplementedError() - ####################### # Bug attachment APIs # diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index e6c52570..3d6c93a0 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -78,5 +78,3 @@ def is_rest(self): def bugzilla_version(self): return self._get("/version") - def bugzilla_extensions(self): - return self._get("/extensions") diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index d8128562..699c923c 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -158,8 +158,6 @@ def is_xmlrpc(self): def bugzilla_version(self): return self._xmlrpc_proxy.Bugzilla.version() - def bugzilla_extensions(self): - return self._xmlrpc_proxy.Bugzilla.extensions() def bug_attachment_get(self, attachment_ids, paramdict): data = paramdict.copy() From 33c970e628d4a77616d78dd4b1a57bef8dad28a3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 12:59:08 -0500 Subject: [PATCH 211/393] Bugzilla: drop some bugzilla < 4.0 handling The last release of the bugzilla 3 series was Feb 2013. bugzilla 4 was first released Feb 2011. I don't know of any bugzilla 3 instances still in the wild. Drop the old version checks. Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 2 +- bugzilla/base.py | 23 +++++++++-------------- tests/test_cli_query.py | 7 +------ 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 1bd84bf2..982280d0 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -548,7 +548,7 @@ def _do_query(bz, opt, parser): built_query.update(q) q = built_query - if not q: + if not q: # pragma: no cover parser.error("'query' command requires additional arguments") return bz.query(q) diff --git a/bugzilla/base.py b/bugzilla/base.py index f692410b..f33a3077 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -978,15 +978,14 @@ def _convert_fields(_in): return _in ret = {} - if self._check_version(4, 0): - if include_fields: - include_fields = _convert_fields(include_fields) - if "id" not in include_fields: - include_fields.append("id") - ret["include_fields"] = include_fields - if exclude_fields: - exclude_fields = _convert_fields(exclude_fields) - ret["exclude_fields"] = exclude_fields + if include_fields: + include_fields = _convert_fields(include_fields) + if "id" not in include_fields: + include_fields.append("id") + ret["include_fields"] = include_fields + if exclude_fields: + exclude_fields = _convert_fields(exclude_fields) + ret["exclude_fields"] = exclude_fields if self._supports_getbug_extra_fields(): if extra_fields: ret["extra_fields"] = _convert_fields(extra_fields) @@ -1057,11 +1056,7 @@ def _getbugs(self, idlist, permissive, r = self._backend.bug_get(getbugdata) - if self._check_version(4, 0): - bugdict = dict([(b['id'], b) for b in r['bugs']]) - else: # pragma: no cover - bugdict = dict([(b['id'], b['internals']) for b in r['bugs']]) - + bugdict = dict([(b['id'], b) for b in r['bugs']]) ret = [] for i in idlist: found = None diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py index b4028c3d..b094c121 100644 --- a/tests/test_cli_query.py +++ b/tests/test_cli_query.py @@ -14,13 +14,8 @@ ################################# def test_query(run_cli): - # query that ends up empty - cmd = "bugzilla query --ids " - fakebz = tests.mockbackend.make_bz(version="3.0.0") - out = run_cli(cmd, fakebz, expectfail=True) - assert "requires additional arguments" in out - # bad field option + fakebz = tests.mockbackend.make_bz() cmd = "bugzilla query --field FOO" out = run_cli(cmd, fakebz, expectfail=True) assert "Invalid field argument" in out From b1e4d4533f3c5fc1a1254f3ba3f85562fabd2ae1 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 13:33:09 -0500 Subject: [PATCH 212/393] Bugzilla: Rework version caching slightly Add _get_version() which returns the version as a float, which makes comparisons more explicit. Use the Cache object to track the handled version values Signed-off-by: Cole Robinson --- bugzilla/base.py | 37 +++++++++++++++++++------------------ tests/test_api_misc.py | 3 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index f33a3077..6eabd8e1 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -72,6 +72,8 @@ def __init__(self): self.products = [] self.component_names = {} self.bugfields = [] + self.version_raw = None + self.version_parsed = (0, 0) class Bugzilla(object): @@ -98,12 +100,6 @@ class Bugzilla(object): Another way to specify auth credentials is via a 'bugzillarc' file. See readconfig() documentation for details. """ - - # bugzilla version that the class is targeting. filled in by - # subclasses - bz_ver_major = 0 - bz_ver_minor = 0 - @staticmethod def url_to_query(url): """ @@ -332,20 +328,24 @@ def _get_user_agent(self): return 'python-bugzilla/%s' % __version__ user_agent = property(_get_user_agent) + @property + def bz_ver_major(self): + return self._cache.version_parsed[0] + + @property + def bz_ver_minor(self): + return self._cache.version_parsed[1] + ################### # Private helpers # ################### - def _check_version(self, major, minor): + def _get_version(self): """ - Check if the detected bugzilla version is >= passed major/minor pair. + Return version number as a float """ - if major < self.bz_ver_major: - return True - if (major == self.bz_ver_major and minor <= self.bz_ver_minor): - return True - return False + return float("%d.%d" % (self.bz_ver_major, self.bz_ver_minor)) def _get_bug_aliases(self): return [(f.newname, f.oldname) @@ -443,14 +443,15 @@ def readconfig(self, configpath=None, overwrite=True): log.debug("bugzillarc: unknown key=%s", key) def _set_bz_version(self, version): + self._cache.version_raw = version try: - self.bz_ver_major, self.bz_ver_minor = [ - int(i) for i in version.split(".")[0:2]] + major, minor = [int(i) for i in version.split(".")[0:2]] except Exception: log.debug("version doesn't match expected format X.Y.Z, " "assuming 5.0", exc_info=True) - self.bz_ver_major = 5 - self.bz_ver_minor = 0 + major = 5 + minor = 0 + self._cache.version_parsed = (major, minor) def _get_backend_class(self): # pragma: no cover # This is a hook for the test suite to do some mock hackery @@ -1285,7 +1286,7 @@ def query(self, query): # isn't supported by this bugzilla instance if ("query_format" not in str(e) or not BugzillaError.get_bugzilla_error_code(e) or - self._check_version(5, 0)): + self._get_version() >= 5.0): raise raise BugzillaError("%s\nYour bugzilla instance does not " "appear to support API queries derived from bugzilla " diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index e55c75c0..f22c69d1 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -247,8 +247,7 @@ def test_version_bad(): assert bz.bz_ver_minor == 0 # pylint: disable=protected-access - assert bz._check_version(5, 0) - assert not bz._check_version(10000, 0) + assert bz._get_version() == 5.0 def test_extensions_bad(): From 061a4ec9be87406b7c464d69b6c40d0d5984daeb Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 14:53:40 -0500 Subject: [PATCH 213/393] Bugzilla: Add requests_session __init__ parameter This allows users to pass in a requests_session we will use for talking to remote bugzilla, which saves us having to expose various requests bits via the official API Signed-off-by: Cole Robinson --- bugzilla/_session.py | 13 ++++++++----- bugzilla/base.py | 9 +++++++-- tests/test_api_misc.py | 10 ++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 91218597..89db265e 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -17,8 +17,8 @@ class _BugzillaSession(object): Class to handle the backend agnostic 'requests' setup """ def __init__(self, url, user_agent, - cookiecache=None, sslverify=True, cert=None, - tokencache=None, api_key=None): + cookiecache, sslverify, cert, + tokencache, api_key, requests_session=None): self._url = url self._user_agent = user_agent self._scheme = urlparse(url)[0] @@ -30,13 +30,16 @@ def __init__(self, url, user_agent, raise Exception("Invalid URL scheme: %s (%s)" % ( self._scheme, url)) - self._session = requests.Session() + self._session = requests_session + if not self._session: + self._session = requests.Session() + if cert: self._session.cert = cert if self._cookiecache: self._session.cookies = self._cookiecache.get_cookiejar() - - self._session.verify = sslverify + if sslverify is False: + self._session.verify = False self._session.headers["User-Agent"] = self._user_agent self._session.params["Bugzilla_api_key"] = self._api_key self._set_tokencache_param() diff --git a/bugzilla/base.py b/bugzilla/base.py index 6eabd8e1..77dd35f0 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -176,7 +176,7 @@ def get_rcfile_default_url(): def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None, cert=None, configpaths=-1, basic_auth=False, - force_rest=False, force_xmlrpc=False): + force_rest=False, force_xmlrpc=False, requests_session=None): """ :param url: The bugzilla instance URL, which we will connect to immediately. Most users will want to specify this at @@ -211,6 +211,9 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, :param force_xmlrpc: Force use of the XMLRPC API. If neither force_X parameter are specified, heuristics will be used to determine which API to use, with XMLRPC preferred for back compatability. + :param requests_session: An optional requests.Session object the + API will use to contact the remote bugzilla instance. This + way the API user can set up whatever auth bits they may need. """ if url == -1: raise TypeError("Specify a valid bugzilla url, or pass url=None") @@ -224,6 +227,7 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._backend = None self._session = None + self._user_requests_session = requests_session self._sslverify = sslverify self._cache = _BugzillaAPICache() self._bug_autorefresh = False @@ -491,7 +495,8 @@ def connect(self, url=None): sslverify=self._sslverify, cert=self.cert, tokencache=self._tokencache, - api_key=self.api_key) + api_key=self.api_key, + requests_session=self._user_requests_session) backendclass = self._get_backend_class() self._backend = backendclass(url, self._session) diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index f22c69d1..33b5abdc 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -300,6 +300,16 @@ def test_get_xmlrpc_proxy(): assert hasattr(bz.get_requests_session(), "request") +def test_requests_session_passthrough(): + import requests + session = requests.Session() + + bz = tests.mockbackend.make_bz( + bz_kwargs={"requests_session": session, "sslverify": False}) + assert bz.get_requests_session() is session + assert session.verify is False + + def test_query_url_fail(): # test some handling of query from_url errors query = {"query_format": "advanced", "product": "FOO"} From 737e15b12e0a45543dccca7c8aae19da1cd191c7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 14:55:51 -0500 Subject: [PATCH 214/393] Bugzilla: Remove explicit basic_auth support This was added during this dev cycle, but it's no longer needed with the request_session passthrough support Signed-off-by: Cole Robinson --- bugzilla/_session.py | 10 ---------- bugzilla/base.py | 8 +------- tests/test_api_misc.py | 6 ------ 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 89db265e..129d1441 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -1,7 +1,6 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -import base64 from logging import getLogger import requests @@ -62,15 +61,6 @@ def _set_tokencache_param(self): token = self.get_token_value() self._session.params["Bugzilla_token"] = token - def set_basic_auth(self, user, password): - """ - Set basic authentication method. - """ - formatstr = "{}:{}".format(user, password).encode("utf-8") - b64str = base64.b64encode(formatstr).decode("utf-8") - authstr = "Basic {}".format(b64str) - self._session.headers["Authorization"] = authstr - def set_response_cookies(self, response): """ Save any cookies received from the passed requests response diff --git a/bugzilla/base.py b/bugzilla/base.py index 77dd35f0..41abbed7 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -175,7 +175,7 @@ def get_rcfile_default_url(): def __init__(self, url=-1, user=None, password=None, cookiefile=-1, sslverify=True, tokenfile=-1, use_creds=True, api_key=None, - cert=None, configpaths=-1, basic_auth=False, + cert=None, configpaths=-1, force_rest=False, force_xmlrpc=False, requests_session=None): """ :param url: The bugzilla instance URL, which we will connect @@ -206,7 +206,6 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, to file or directory for custom certs. :param api_key: A bugzilla5+ API key :param configpaths: A list of possible bugzillarc locations. - :param basic_auth: Use headers with HTTP Basic authentication :param force_rest: Force use of the REST API :param force_xmlrpc: Force use of the XMLRPC API. If neither force_X parameter are specified, heuristics will be used to determine @@ -256,8 +255,6 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._settokenfile(tokenfile) self._setconfigpath(configpaths) - self._basic_auth = basic_auth - if url: self.connect(url) @@ -588,9 +585,6 @@ def login(self, user=None, password=None, restrict_login=None): log.info("logging in with restrict_login=True") try: - if self._basic_auth: - self._session.set_basic_auth(user, password) - payload = {'login': user, 'password': password} if restrict_login: payload['restrict_login'] = True diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 33b5abdc..afc99ae9 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -189,12 +189,6 @@ def test_api_login(): # Errors on api_key + login() bz.login() - # Will log in immediately, hitting basic_auth path - bz = tests.mockbackend.make_bz( - bz_kwargs={"basic_auth": True, "user": "FOO", "password": "BAR"}, - user_login_args="data/mockargs/test_api_login1.txt", - user_login_return={}) - # Hit default api_key code path bz = tests.mockbackend.make_bz( bz_kwargs={"api_key": "FAKE_KEY"}, From 33e930f25d5f37cf4f2e2fda6b466dfcd5a47336 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 17:37:16 -0500 Subject: [PATCH 215/393] tests: do more thorough API arg comparisons Don't just compare the last dict(), compart all of args tuple() Signed-off-by: Cole Robinson --- .../mockargs/test_api_attachments_create1.txt | 11 +++--- tests/data/mockargs/test_attach1.txt | 15 ++++---- tests/data/mockargs/test_attach2.txt | 9 ++--- tests/data/mockargs/test_attach_get1.txt | 2 +- tests/data/mockargs/test_attach_get2.txt | 2 +- tests/data/mockargs/test_attachments_get1.txt | 2 +- .../mockargs/test_attachments_getall1.txt | 2 +- .../mockargs/test_bug_api_get_attachments.txt | 2 +- tests/mockbackend.py | 20 ++++++++--- tests/utils.py | 35 ++++++++++++------- 10 files changed, 62 insertions(+), 38 deletions(-) diff --git a/tests/data/mockargs/test_api_attachments_create1.txt b/tests/data/mockargs/test_api_attachments_create1.txt index 86c6e10f..2a65f5c6 100644 --- a/tests/data/mockargs/test_api_attachments_create1.txt +++ b/tests/data/mockargs/test_api_attachments_create1.txt @@ -1,5 +1,6 @@ -{'content_type': 'text/plain', - 'file_name': 'bz-attach-get1.txt', - 'ids': [123456], - 'is_private': True, - 'summary': 'some desc'} +('STRIPPED-BY-TESTSUITE', + {'content_type': 'text/plain', + 'file_name': 'bz-attach-get1.txt', + 'ids': [123456], + 'is_private': True, + 'summary': 'some desc'}) diff --git a/tests/data/mockargs/test_attach1.txt b/tests/data/mockargs/test_attach1.txt index e16bc1b7..1c2f6cef 100644 --- a/tests/data/mockargs/test_attach1.txt +++ b/tests/data/mockargs/test_attach1.txt @@ -1,7 +1,8 @@ -{'comment': 'some comment to go with it', - 'content_type': 'text/x-patch', - 'file_name': 'bz-attach-get1.txt', - 'ids': ['123456'], - 'is_patch': True, - 'is_private': True, - 'summary': 'bz-attach-get1.txt'} +('STRIPPED-BY-TESTSUITE', + {'comment': 'some comment to go with it', + 'content_type': 'text/x-patch', + 'file_name': 'bz-attach-get1.txt', + 'ids': ['123456'], + 'is_patch': True, + 'is_private': True, + 'summary': 'bz-attach-get1.txt'}) diff --git a/tests/data/mockargs/test_attach2.txt b/tests/data/mockargs/test_attach2.txt index 0e074bc4..a49e7de7 100644 --- a/tests/data/mockargs/test_attach2.txt +++ b/tests/data/mockargs/test_attach2.txt @@ -1,4 +1,5 @@ -{'content_type': 'text/plain', - 'file_name': 'fake-file-name.txt', - 'ids': ['123456'], - 'summary': 'Some attachment description'} +('STRIPPED-BY-TESTSUITE', + {'content_type': 'text/plain', + 'file_name': 'fake-file-name.txt', + 'ids': ['123456'], + 'summary': 'Some attachment description'}) diff --git a/tests/data/mockargs/test_attach_get1.txt b/tests/data/mockargs/test_attach_get1.txt index 0967ef42..25381b70 100644 --- a/tests/data/mockargs/test_attach_get1.txt +++ b/tests/data/mockargs/test_attach_get1.txt @@ -1 +1 @@ -{} +(['112233'], {}) diff --git a/tests/data/mockargs/test_attach_get2.txt b/tests/data/mockargs/test_attach_get2.txt index 0967ef42..558c8feb 100644 --- a/tests/data/mockargs/test_attach_get2.txt +++ b/tests/data/mockargs/test_attach_get2.txt @@ -1 +1 @@ -{} +(['663674'], {}) diff --git a/tests/data/mockargs/test_attachments_get1.txt b/tests/data/mockargs/test_attachments_get1.txt index 0967ef42..22469fd3 100644 --- a/tests/data/mockargs/test_attachments_get1.txt +++ b/tests/data/mockargs/test_attachments_get1.txt @@ -1 +1 @@ -{} +(502352, {}) diff --git a/tests/data/mockargs/test_attachments_getall1.txt b/tests/data/mockargs/test_attachments_getall1.txt index 9b5f5136..0614705e 100644 --- a/tests/data/mockargs/test_attachments_getall1.txt +++ b/tests/data/mockargs/test_attachments_getall1.txt @@ -1 +1 @@ -{'exclude_fields': ['bar'], 'include_fields': ['foo']} +([123456], {'exclude_fields': ['bar'], 'include_fields': ['foo']}) diff --git a/tests/data/mockargs/test_bug_api_get_attachments.txt b/tests/data/mockargs/test_bug_api_get_attachments.txt index 0967ef42..20c730e9 100644 --- a/tests/data/mockargs/test_bug_api_get_attachments.txt +++ b/tests/data/mockargs/test_bug_api_get_attachments.txt @@ -1 +1 @@ -{} +([663674], {}) diff --git a/tests/mockbackend.py b/tests/mockbackend.py index 587d88f9..ad6dc757 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -30,10 +30,22 @@ def __helper(self, args): if isinstance(func_return, BaseException): raise func_return - if isinstance(func_args, dict): - assert func_args == args[-1] - elif func_args is not None: - tests.utils.diff_compare(args[-1], func_args) + filename = None + expect_out = func_args + if isinstance(func_args, str): + filename = func_args + expect_out = None + + # Hack to strip out attachment content from the generated + # test output, because it doesn't play well with the test + # suite running on python2 + if "content-disposition" in str(args): + largs = list(args) + largs[0] = "STRIPPED-BY-TESTSUITE" + args = tuple(largs) + + if filename or expect_out: + tests.utils.diff_compare(args, filename, expect_out) if isinstance(func_return, dict): return func_return diff --git a/tests/utils.py b/tests/utils.py index 3a454831..75f96a22 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -64,23 +64,32 @@ def fake_request(*args, **kwargs): return bz -def diff_compare(inputdata, filename): +def diff_compare(inputdata, filename, expect_out=None): """Compare passed string output to contents of filename""" - filename = tests_path(filename) - - actual_out = inputdata - if isinstance(inputdata, dict): - actual_out = pprint.pformat(inputdata, width=81) - if not actual_out.endswith("\n"): - actual_out += "\n" - - if not os.path.exists(filename) or tests.CLICONFIG.REGENERATE_OUTPUT: - open(filename, "w").write(actual_out) - expect_out = open(filename).read() + def _process(data): + if isinstance(data, tuple) and len(data) == 1: + data = data[0] + if isinstance(data, (dict, tuple)): + out = pprint.pformat(data, width=81) + else: + out = str(data) + if not out.endswith("\n"): + out += "\n" + return out + + actual_out = _process(inputdata) + + if filename: + filename = tests_path(filename) + if not os.path.exists(filename) or tests.CLICONFIG.REGENERATE_OUTPUT: + open(filename, "w").write(actual_out) + expect_out = open(filename).read() + else: + expect_out = _process(expect_out) diff = "".join(difflib.unified_diff(expect_out.splitlines(1), actual_out.splitlines(1), - fromfile=filename or '', + fromfile=filename or "Manual input", tofile="Generated Output")) if diff: raise AssertionError("Conversion outputs did not match.\n%s" % diff) From 89b57cf394312048f19e5149875c4ef416cf18ac Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 17:23:34 -0500 Subject: [PATCH 216/393] backend: Tweak some API signatures to better fit the REST API Break out the relevant id parameters into explicit options, and adjust Bugzilla usage and XMLRPC impls to match Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 14 ++-- bugzilla/_backendxmlrpc.py | 45 ++++++++---- bugzilla/base.py | 72 ++++++++++--------- .../mockargs/test_api_attachments_create1.txt | 4 +- tests/data/mockargs/test_api_getbugs1.txt | 2 +- tests/data/mockargs/test_attach1.txt | 4 +- tests/data/mockargs/test_attach2.txt | 4 +- .../mockargs/test_attachments_update1.txt | 4 +- tests/data/mockargs/test_bug_api_comments.txt | 2 +- tests/data/mockargs/test_bug_api_history.txt | 2 +- .../mockargs/test_bug_apis_addcc_update.txt | 5 +- .../test_bug_apis_addcomment_update.txt | 2 +- .../mockargs/test_bug_apis_close_update.txt | 12 ++-- .../test_bug_apis_deletecc_update.txt | 5 +- .../test_bug_apis_setassignee_update.txt | 8 +-- .../test_bug_apis_setstatus_update.txt | 5 +- .../test_bug_apis_updateflags_update.txt | 2 +- tests/data/mockargs/test_modify1.txt | 2 +- tests/data/mockargs/test_modify2.txt | 22 +++--- tests/data/mockargs/test_modify3-tags.txt | 2 +- tests/data/mockargs/test_modify3.txt | 8 +-- tests/data/mockargs/test_modify4.txt | 8 +-- tests/data/mockargs/test_query_cve_getbug.txt | 2 +- tests/data/mockargs/test_update_flags.txt | 2 +- tests/mockbackend.py | 2 +- tests/test_api_misc.py | 4 +- tests/test_rw_functional.py | 6 +- 27 files changed, 132 insertions(+), 118 deletions(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 03724044..8bf77f6e 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -65,7 +65,7 @@ def bug_attachment_get_all(self, bug_ids, paramdict): """ raise NotImplementedError() - def bug_attachment_create(self, data, paramdict): + def bug_attachment_create(self, bug_ids, data, paramdict): """ Create a bug attachment http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#create-attachment @@ -76,7 +76,7 @@ def bug_attachment_create(self, data, paramdict): """ raise NotImplementedError() - def bug_attachment_update(self, paramdict): + def bug_attachment_update(self, attachment_ids, paramdict): """ Update a bug attachment http://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#update-attachment @@ -88,7 +88,7 @@ def bug_attachment_update(self, paramdict): # bug APIs # ############ - def bug_comments(self, paramdict): + def bug_comments(self, bug_ids, paramdict): """ Fetch bug comments http://bugzilla.readthedocs.io/en/latest/api/core/v1/comment.html#get-comments @@ -109,14 +109,14 @@ def bug_fields(self, paramdict): """ raise NotImplementedError() - def bug_get(self, paramdict): + def bug_get(self, bug_ids, aliases, paramdict): """ Lookup bug data by ID http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#get-bug """ raise NotImplementedError() - def bug_history(self, paramdict): + def bug_history(self, bug_ids, paramdict): """ Lookup bug history http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#bug-history @@ -137,14 +137,14 @@ def bug_search(self, paramdict): """ raise NotImplementedError() - def bug_update(self, paramdict): + def bug_update(self, bug_ids, paramdict): """ Update bugs http://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug """ raise NotImplementedError() - def bug_update_tags(self, paramdict): + def bug_update_tags(self, bug_ids, paramdict): """ Update bug tags https://www.bugzilla.org/docs/4.4/en/html/api/Bugzilla/WebService/Bug.html#update_tags diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 699c923c..abae9c5b 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -167,31 +167,46 @@ def bug_attachment_get_all(self, bug_ids, paramdict): data = paramdict.copy() data["ids"] = listify(bug_ids) return self._xmlrpc_proxy.Bug.attachments(data) - def bug_attachment_create(self, data, paramdict): + def bug_attachment_create(self, bug_ids, data, paramdict): + pdata = paramdict.copy() + pdata["ids"] = listify(bug_ids) if data is not None and "data" not in paramdict: - paramdict["data"] = Binary(data) - return self._xmlrpc_proxy.Bug.add_attachment(paramdict) - def bug_attachment_update(self, paramdict): - return self._xmlrpc_proxy.Bug.update_attachment(paramdict) + pdata["data"] = Binary(data) + return self._xmlrpc_proxy.Bug.add_attachment(pdata) + def bug_attachment_update(self, attachment_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(attachment_ids) + return self._xmlrpc_proxy.Bug.update_attachment(data) - def bug_comments(self, paramdict): - return self._xmlrpc_proxy.Bug.comments(paramdict) + def bug_comments(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.comments(data) def bug_create(self, paramdict): return self._xmlrpc_proxy.Bug.create(paramdict) def bug_fields(self, paramdict): return self._xmlrpc_proxy.Bug.fields(paramdict) - def bug_get(self, paramdict): - return self._xmlrpc_proxy.Bug.get(paramdict) - def bug_history(self, paramdict): - return self._xmlrpc_proxy.Bug.history(paramdict) + def bug_get(self, bug_ids, aliases, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) or [] + data["ids"] += listify(aliases) or [] + return self._xmlrpc_proxy.Bug.get(data) + def bug_history(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.history(data) def bug_legal_values(self, paramdict): return self._xmlrpc_proxy.Bug.legal_values(paramdict) def bug_search(self, paramdict): return self._xmlrpc_proxy.Bug.search(paramdict) - def bug_update(self, paramdict): - return self._xmlrpc_proxy.Bug.update(paramdict) - def bug_update_tags(self, paramdict): - return self._xmlrpc_proxy.Bug.update_tags(paramdict) + def bug_update(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.update(data) + def bug_update_tags(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._xmlrpc_proxy.Bug.update_tags(data) def component_create(self, paramdict): return self._xmlrpc_proxy.Component.create(paramdict) diff --git a/bugzilla/base.py b/bugzilla/base.py index 41abbed7..0e80b0bd 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1035,42 +1035,47 @@ def _getbugs(self, idlist, permissive, Return a list of dicts of full bug info for each given bug id. bug ids that couldn't be found will return None instead of a dict. """ - oldidlist = idlist - idlist = [] - for i in oldidlist: - try: - idlist.append(int(i)) - except ValueError: - # String aliases can be passed as well - idlist.append(i) + ids = [] + aliases = [] + + def _alias_or_int(_v): + if str(_v).isdigit(): + return int(_v), None + return None, str(_v) + + for idstr in idlist: + idint, alias = _alias_or_int(idstr) + if alias: + aliases.append(alias) + else: + ids.append(idstr) extra_fields = listify(extra_fields or []) extra_fields += self._getbug_extra_fields() - getbugdata = {"ids": idlist} + getbugdata = {} if permissive: getbugdata["permissive"] = 1 getbugdata.update(self._process_include_fields( include_fields, exclude_fields, extra_fields)) - r = self._backend.bug_get(getbugdata) + r = self._backend.bug_get(ids, aliases, getbugdata) - bugdict = dict([(b['id'], b) for b in r['bugs']]) + # Do some wrangling to ensure we return bugs in the same order + # the were passed in, for historical reasons ret = [] - for i in idlist: - found = None - if i in bugdict: - found = bugdict[i] - else: - # Need to map an alias - for valdict in bugdict.values(): - if i in listify(valdict.get("alias", None) or []): - found = valdict - break - - ret.append(found) + for idval in idlist: + idint, alias = _alias_or_int(idval) + for bugdict in r["bugs"]: + if idint and idint != bugdict.get("id", None): + continue + aliaslist = listify(bugdict.get("alias", None) or []) + if alias and alias not in aliaslist: + continue + ret.append(bugdict) + break return ret def _getbug(self, objid, **kwargs): @@ -1115,7 +1120,7 @@ def get_comments(self, idlist): Returns a dictionary of bugs and comments. The comments key will be empty. See bugzilla docs for details """ - return self._backend.bug_comments({'ids': idlist}) + return self._backend.bug_comments(idlist, {}) ################# @@ -1318,7 +1323,7 @@ def bugs_history_raw(self, bug_ids): Experimental. Gets the history of changes for particular bugs in the database. """ - return self._backend.bug_history({'ids': bug_ids}) + return self._backend.bug_history(bug_ids, {}) ####################################### @@ -1336,9 +1341,7 @@ def update_bugs(self, ids, updates): build_update(), otherwise we cannot guarantee back compatibility. """ tmp = updates.copy() - tmp["ids"] = listify(ids) - - return self._backend.bug_update(tmp) + return self._backend.bug_update(listify(ids), tmp) def update_tags(self, idlist, tags_add=None, tags_remove=None): """ @@ -1351,11 +1354,10 @@ def update_tags(self, idlist, tags_add=None, tags_remove=None): tags["remove"] = listify(tags_remove) d = { - "ids": listify(idlist), "tags": tags, } - return self._backend.bug_update_tags(d) + return self._backend.bug_update_tags(listify(idlist), d) def update_flags(self, idlist, flags): """ @@ -1573,8 +1575,6 @@ def attachfile(self, idlist, attachfile, description, **kwargs): if not isinstance(data, bytes): # pragma: no cover data = data.encode(locale.getpreferredencoding()) - kwargs['ids'] = listify(idlist) - if 'file_name' not in kwargs and hasattr(f, "name"): kwargs['file_name'] = os.path.basename(f.name) if 'content_type' not in kwargs: @@ -1584,7 +1584,8 @@ def attachfile(self, idlist, attachfile, description, **kwargs): kwargs['file_name'], strict=False)[0] kwargs['content_type'] = ctype or 'application/octet-stream' - ret = self._backend.bug_attachment_create(data, kwargs) + ret = self._backend.bug_attachment_create( + listify(idlist), data, kwargs) if "attachments" in ret: # Up to BZ 4.2 @@ -1639,9 +1640,10 @@ def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): flags = {"name": flagname} flags.update(kwargs) - update = {'ids': [int(attachid)], 'flags': [flags]} + attachment_ids = [int(attachid)] + update = {'flags': [flags]} - return self._backend.bug_attachment_update(update) + return self._backend.bug_attachment_update(attachment_ids, update) def get_attachments(self, ids, attachment_ids, include_fields=None, exclude_fields=None): diff --git a/tests/data/mockargs/test_api_attachments_create1.txt b/tests/data/mockargs/test_api_attachments_create1.txt index 2a65f5c6..49fa401b 100644 --- a/tests/data/mockargs/test_api_attachments_create1.txt +++ b/tests/data/mockargs/test_api_attachments_create1.txt @@ -1,6 +1,6 @@ -('STRIPPED-BY-TESTSUITE', +([123456], + 'STRIPPED-BY-TESTSUITE', {'content_type': 'text/plain', 'file_name': 'bz-attach-get1.txt', - 'ids': [123456], 'is_private': True, 'summary': 'some desc'}) diff --git a/tests/data/mockargs/test_api_getbugs1.txt b/tests/data/mockargs/test_api_getbugs1.txt index 59bff597..f31bf44e 100644 --- a/tests/data/mockargs/test_api_getbugs1.txt +++ b/tests/data/mockargs/test_api_getbugs1.txt @@ -1 +1 @@ -{'exclude_fields': 'foo', 'ids': ['CVE-1234-5678']} +([], ['CVE-1234-5678'], {'exclude_fields': 'foo'}) diff --git a/tests/data/mockargs/test_attach1.txt b/tests/data/mockargs/test_attach1.txt index 1c2f6cef..34292bbd 100644 --- a/tests/data/mockargs/test_attach1.txt +++ b/tests/data/mockargs/test_attach1.txt @@ -1,8 +1,8 @@ -('STRIPPED-BY-TESTSUITE', +(['123456'], + 'STRIPPED-BY-TESTSUITE', {'comment': 'some comment to go with it', 'content_type': 'text/x-patch', 'file_name': 'bz-attach-get1.txt', - 'ids': ['123456'], 'is_patch': True, 'is_private': True, 'summary': 'bz-attach-get1.txt'}) diff --git a/tests/data/mockargs/test_attach2.txt b/tests/data/mockargs/test_attach2.txt index a49e7de7..002998b7 100644 --- a/tests/data/mockargs/test_attach2.txt +++ b/tests/data/mockargs/test_attach2.txt @@ -1,5 +1,5 @@ -('STRIPPED-BY-TESTSUITE', +(['123456'], + 'STRIPPED-BY-TESTSUITE', {'content_type': 'text/plain', 'file_name': 'fake-file-name.txt', - 'ids': ['123456'], 'summary': 'Some attachment description'}) diff --git a/tests/data/mockargs/test_attachments_update1.txt b/tests/data/mockargs/test_attachments_update1.txt index 56d69cdf..726b5e76 100644 --- a/tests/data/mockargs/test_attachments_update1.txt +++ b/tests/data/mockargs/test_attachments_update1.txt @@ -1,2 +1,2 @@ -{'flags': [{'is_patch': True, 'name': 'needinfo', 'value': 'foobar'}], - 'ids': [112233]} +([112233], + {'flags': [{'is_patch': True, 'name': 'needinfo', 'value': 'foobar'}]}) diff --git a/tests/data/mockargs/test_bug_api_comments.txt b/tests/data/mockargs/test_bug_api_comments.txt index 79181268..8c0dc078 100644 --- a/tests/data/mockargs/test_bug_api_comments.txt +++ b/tests/data/mockargs/test_bug_api_comments.txt @@ -1 +1 @@ -{'ids': [1165434]} +([1165434], {}) diff --git a/tests/data/mockargs/test_bug_api_history.txt b/tests/data/mockargs/test_bug_api_history.txt index 79181268..8c0dc078 100644 --- a/tests/data/mockargs/test_bug_api_history.txt +++ b/tests/data/mockargs/test_bug_api_history.txt @@ -1 +1 @@ -{'ids': [1165434]} +([1165434], {}) diff --git a/tests/data/mockargs/test_bug_apis_addcc_update.txt b/tests/data/mockargs/test_bug_apis_addcc_update.txt index c684d228..3eea2c90 100644 --- a/tests/data/mockargs/test_bug_apis_addcc_update.txt +++ b/tests/data/mockargs/test_bug_apis_addcc_update.txt @@ -1,3 +1,2 @@ -{'cc': {'add': ['foo2@example.com']}, - 'comment': {'comment': 'foocomment'}, - 'ids': [1165434]} +([1165434], + {'cc': {'add': ['foo2@example.com']}, 'comment': {'comment': 'foocomment'}}) diff --git a/tests/data/mockargs/test_bug_apis_addcomment_update.txt b/tests/data/mockargs/test_bug_apis_addcomment_update.txt index ac00345a..3396f824 100644 --- a/tests/data/mockargs/test_bug_apis_addcomment_update.txt +++ b/tests/data/mockargs/test_bug_apis_addcomment_update.txt @@ -1 +1 @@ -{'comment': {'comment': 'test comment', 'is_private': True}, 'ids': [1165434]} +([1165434], {'comment': {'comment': 'test comment', 'is_private': True}}) diff --git a/tests/data/mockargs/test_bug_apis_close_update.txt b/tests/data/mockargs/test_bug_apis_close_update.txt index d4edea5a..1931f05a 100644 --- a/tests/data/mockargs/test_bug_apis_close_update.txt +++ b/tests/data/mockargs/test_bug_apis_close_update.txt @@ -1,6 +1,6 @@ -{'cf_fixed_in': '1.2.3.4.5', - 'comment': {'comment': 'foocomment2'}, - 'dupe_of': 123456, - 'ids': [1165434], - 'resolution': 'UPSTREAM', - 'status': 'CLOSED'} +([1165434], + {'cf_fixed_in': '1.2.3.4.5', + 'comment': {'comment': 'foocomment2'}, + 'dupe_of': 123456, + 'resolution': 'UPSTREAM', + 'status': 'CLOSED'}) diff --git a/tests/data/mockargs/test_bug_apis_deletecc_update.txt b/tests/data/mockargs/test_bug_apis_deletecc_update.txt index 80b86d96..25281146 100644 --- a/tests/data/mockargs/test_bug_apis_deletecc_update.txt +++ b/tests/data/mockargs/test_bug_apis_deletecc_update.txt @@ -1,3 +1,2 @@ -{'cc': {'remove': ['foo2@example.com']}, - 'comment': {'comment': 'foocomment'}, - 'ids': [1165434]} +([1165434], + {'cc': {'remove': ['foo2@example.com']}, 'comment': {'comment': 'foocomment'}}) diff --git a/tests/data/mockargs/test_bug_apis_setassignee_update.txt b/tests/data/mockargs/test_bug_apis_setassignee_update.txt index 040cf29c..f07c6672 100644 --- a/tests/data/mockargs/test_bug_apis_setassignee_update.txt +++ b/tests/data/mockargs/test_bug_apis_setassignee_update.txt @@ -1,4 +1,4 @@ -{'assigned_to': 'foo@example.com', - 'comment': {'comment': 'foocomment'}, - 'ids': [1165434], - 'qa_contact': 'bar@example.com'} +([1165434], + {'assigned_to': 'foo@example.com', + 'comment': {'comment': 'foocomment'}, + 'qa_contact': 'bar@example.com'}) diff --git a/tests/data/mockargs/test_bug_apis_setstatus_update.txt b/tests/data/mockargs/test_bug_apis_setstatus_update.txt index 1fc937e2..6e2abf92 100644 --- a/tests/data/mockargs/test_bug_apis_setstatus_update.txt +++ b/tests/data/mockargs/test_bug_apis_setstatus_update.txt @@ -1,3 +1,2 @@ -{'comment': {'comment': 'foocomment', 'is_private': True}, - 'ids': [1165434], - 'status': 'POST'} +([1165434], + {'comment': {'comment': 'foocomment', 'is_private': True}, 'status': 'POST'}) diff --git a/tests/data/mockargs/test_bug_apis_updateflags_update.txt b/tests/data/mockargs/test_bug_apis_updateflags_update.txt index 99e6481e..04a79656 100644 --- a/tests/data/mockargs/test_bug_apis_updateflags_update.txt +++ b/tests/data/mockargs/test_bug_apis_updateflags_update.txt @@ -1 +1 @@ -{'flags': [{'name': 'someflag', 'status': 'someval'}], 'ids': [1165434]} +([1165434], {'flags': [{'name': 'someflag', 'status': 'someval'}]}) diff --git a/tests/data/mockargs/test_modify1.txt b/tests/data/mockargs/test_modify1.txt index 905b037e..8d28eb0d 100644 --- a/tests/data/mockargs/test_modify1.txt +++ b/tests/data/mockargs/test_modify1.txt @@ -1 +1 @@ -{'component': 'NEWCOMP', 'ids': ['123456', '1234567'], 'status': 'ASSIGNED'} +(['123456', '1234567'], {'component': 'NEWCOMP', 'status': 'ASSIGNED'}) diff --git a/tests/data/mockargs/test_modify2.txt b/tests/data/mockargs/test_modify2.txt index 66c279ee..5ee27d14 100644 --- a/tests/data/mockargs/test_modify2.txt +++ b/tests/data/mockargs/test_modify2.txt @@ -1,11 +1,11 @@ -{'blocks': {'set': [123456, 445566]}, - 'comment': {'comment': 'some example comment', 'is_private': True}, - 'component': 'NEWCOMP', - 'dupe_of': 555666, - 'flags': [{'name': '-needinfo,+somethingels', 'status': 'e'}], - 'groups': {'remove': ['BAR']}, - 'ids': ['123456'], - 'keywords': {'add': ['FOO']}, - 'resolution': 'DUPLICATE', - 'status': 'CLOSED', - 'whiteboard': 'thisone'} +(['123456'], + {'blocks': {'set': [123456, 445566]}, + 'comment': {'comment': 'some example comment', 'is_private': True}, + 'component': 'NEWCOMP', + 'dupe_of': 555666, + 'flags': [{'name': '-needinfo,+somethingels', 'status': 'e'}], + 'groups': {'remove': ['BAR']}, + 'keywords': {'add': ['FOO']}, + 'resolution': 'DUPLICATE', + 'status': 'CLOSED', + 'whiteboard': 'thisone'}) diff --git a/tests/data/mockargs/test_modify3-tags.txt b/tests/data/mockargs/test_modify3-tags.txt index cfbd5d7a..e0b3ffdc 100644 --- a/tests/data/mockargs/test_modify3-tags.txt +++ b/tests/data/mockargs/test_modify3-tags.txt @@ -1 +1 @@ -{'ids': ['1165434'], 'tags': {'add': ['addtag'], 'remove': ['rmtag']}} +(['1165434'], {'tags': {'add': ['addtag'], 'remove': ['rmtag']}}) diff --git a/tests/data/mockargs/test_modify3.txt b/tests/data/mockargs/test_modify3.txt index f17e5a87..d3cee364 100644 --- a/tests/data/mockargs/test_modify3.txt +++ b/tests/data/mockargs/test_modify3.txt @@ -1,4 +1,4 @@ -{'cf_devel_whiteboard': 'somedeveltag,someothertag devel-duh', - 'cf_internal_whiteboard': 'someinternal TAG internal-hey bar', - 'cf_qa_whiteboard': 'bar baz yo-qa', - 'ids': [1165434]} +([1165434], + {'cf_devel_whiteboard': 'somedeveltag,someothertag devel-duh', + 'cf_internal_whiteboard': 'someinternal TAG internal-hey bar', + 'cf_qa_whiteboard': 'bar baz yo-qa'}) diff --git a/tests/data/mockargs/test_modify4.txt b/tests/data/mockargs/test_modify4.txt index b8271f91..7bf1e1c3 100644 --- a/tests/data/mockargs/test_modify4.txt +++ b/tests/data/mockargs/test_modify4.txt @@ -1,4 +1,4 @@ -{'cf_fixed_in': 'foofixedin', - 'component': 'lvm2', - 'ids': ['1165434'], - 'sub_components': {'lvm2': ['some-sub-component']}} +(['1165434'], + {'cf_fixed_in': 'foofixedin', + 'component': 'lvm2', + 'sub_components': {'lvm2': ['some-sub-component']}}) diff --git a/tests/data/mockargs/test_query_cve_getbug.txt b/tests/data/mockargs/test_query_cve_getbug.txt index 6a2485b1..9f6ec7d2 100644 --- a/tests/data/mockargs/test_query_cve_getbug.txt +++ b/tests/data/mockargs/test_query_cve_getbug.txt @@ -1 +1 @@ -{'ids': [123456]} +([123456], [], {}) diff --git a/tests/data/mockargs/test_update_flags.txt b/tests/data/mockargs/test_update_flags.txt index 458ae42f..64d864ab 100644 --- a/tests/data/mockargs/test_update_flags.txt +++ b/tests/data/mockargs/test_update_flags.txt @@ -1 +1 @@ -{'flags': {'name': 'needinfo', 'status': '?'}, 'ids': [12345, 6789]} +([12345, 6789], {'flags': {'name': 'needinfo', 'status': '?'}}) diff --git a/tests/mockbackend.py b/tests/mockbackend.py index ad6dc757..b724b02b 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -41,7 +41,7 @@ def __helper(self, args): # suite running on python2 if "content-disposition" in str(args): largs = list(args) - largs[0] = "STRIPPED-BY-TESTSUITE" + largs[1] = "STRIPPED-BY-TESTSUITE" args = tuple(largs) if filename or expect_out: diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index afc99ae9..357ef823 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -269,7 +269,7 @@ def test_bugs_history_raw(): # Stub test for bugs_history_raw ids = ["12345", 567] bz = tests.mockbackend.make_bz( - bug_history_args={"ids": ids}, + bug_history_args=(ids, {}), bug_history_return={}) bz.bugs_history_raw(ids) @@ -278,7 +278,7 @@ def test_get_comments(): # Stub test for get_commands ids = ["12345", 567] bz = tests.mockbackend.make_bz( - bug_comments_args={"ids": ids}, + bug_comments_args=(ids, {}), bug_comments_return={}) bz.get_comments(ids) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 75aa19f5..3ced7190 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -551,9 +551,9 @@ def _test8Attachments(run_cli): assert setbug.attachments[-1]["flags"] == [] # Set attachment obsolete - bz._backend.bug_attachment_update({ # pylint: disable=protected-access - "ids": [setbug.attachments[-1]["id"]], - "is_obsolete": 1}) + bz._backend.bug_attachment_update( # pylint: disable=protected-access + [setbug.attachments[-1]["id"]], + {"is_obsolete": 1}) setbug.refresh() assert setbug.attachments[-1]["is_obsolete"] == 1 From f5079ac2e003253be5aeaa9620a055079b50f0a8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 18:58:27 -0500 Subject: [PATCH 217/393] Bugzilla: Drop use of legal_values in getcomponents It's deprecated and will be removed eventually, and product_get isn't too much slower these days Remove traces of legal_values, as it's no longer used Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 7 ------- bugzilla/_backendxmlrpc.py | 2 -- bugzilla/base.py | 21 +++++++++---------- tests/data/clioutput/test_info_components.txt | 5 ++--- .../data/mockargs/test_api_products_get2.txt | 3 ++- .../data/mockargs/test_api_products_get5.txt | 2 +- .../test_info_components-legalvalues.txt | 1 - tests/mockbackend.py | 2 -- tests/test_api_products.py | 4 ---- tests/test_cli_info.py | 19 +++++++---------- 10 files changed, 23 insertions(+), 43 deletions(-) delete mode 100644 tests/data/mockargs/test_info_components-legalvalues.txt diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 8bf77f6e..9032feb1 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -123,13 +123,6 @@ def bug_history(self, bug_ids, paramdict): """ raise NotImplementedError() - def bug_legal_values(self, paramdict): - """ - Old style fields querying - http://bugzilla.readthedocs.io/en/latest/api/core/v1/field.html#legal-values - """ - raise NotImplementedError() - def bug_search(self, paramdict): """ Search/query bugs diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index abae9c5b..23eefb39 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -195,8 +195,6 @@ def bug_history(self, bug_ids, paramdict): data = paramdict.copy() data["ids"] = listify(bug_ids) return self._xmlrpc_proxy.Bug.history(data) - def bug_legal_values(self, paramdict): - return self._xmlrpc_proxy.Bug.legal_values(paramdict) def bug_search(self, paramdict): return self._xmlrpc_proxy.Bug.search(paramdict) def bug_update(self, bug_ids, paramdict): diff --git a/bugzilla/base.py b/bugzilla/base.py index 0e80b0bd..edc8ecb8 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -870,12 +870,6 @@ def getcomponents(self, product, force_refresh=False): """ Return a list of component names for the passed product. - This can be implemented with Product.get, but behind the - scenes it uses Bug.legal_values. Reason being that on bugzilla - instances with tons of components, like bugzilla.redhat.com - Product=Fedora for example, there's a 10x speed difference - even with properly limited Product.get calls. - On first invocation the value is cached, and subsequent calls will return the cached data. @@ -885,17 +879,22 @@ def getcomponents(self, product, force_refresh=False): proddict = self._lookup_product_in_cache(product) product_id = proddict.get("id", None) - if force_refresh or product_id is None: - self.refresh_products(names=[product], - include_fields=["name", "id"]) + if (force_refresh or product_id is None or + "components" not in proddict): + self.refresh_products( + names=[product], + include_fields=["name", "id", "components.name"]) proddict = self._lookup_product_in_cache(product) if "id" not in proddict: raise BugzillaError("Product '%s' not found" % product) product_id = proddict["id"] if product_id not in self._cache.component_names: - opts = {'product_id': product_id, 'field': 'component'} - names = self._backend.bug_legal_values(opts)["values"] + names = [] + for comp in proddict.get("components", []): + name = comp.get("name") + if name: + names.append(name) self._cache.component_names[product_id] = names return self._cache.component_names[product_id] diff --git a/tests/data/clioutput/test_info_components.txt b/tests/data/clioutput/test_info_components.txt index 7f2c49bc..265e86d7 100644 --- a/tests/data/clioutput/test_info_components.txt +++ b/tests/data/clioutput/test_info_components.txt @@ -1,3 +1,2 @@ -comp1 -hey-imma-comp -test-comp-2 +backend/kernel +client-interfaces diff --git a/tests/data/mockargs/test_api_products_get2.txt b/tests/data/mockargs/test_api_products_get2.txt index e2d58722..e90b8640 100644 --- a/tests/data/mockargs/test_api_products_get2.txt +++ b/tests/data/mockargs/test_api_products_get2.txt @@ -1 +1,2 @@ -{'include_fields': ['name', 'id'], 'names': ['test-fake-product']} +{'include_fields': ['name', 'id', 'components.name'], + 'names': ['test-fake-product']} diff --git a/tests/data/mockargs/test_api_products_get5.txt b/tests/data/mockargs/test_api_products_get5.txt index d2566d2f..995f6507 100644 --- a/tests/data/mockargs/test_api_products_get5.txt +++ b/tests/data/mockargs/test_api_products_get5.txt @@ -1 +1 @@ -{'include_fields': ['name', 'id'], 'names': [0]} +{'include_fields': ['name', 'id', 'components.name'], 'names': [0]} diff --git a/tests/data/mockargs/test_info_components-legalvalues.txt b/tests/data/mockargs/test_info_components-legalvalues.txt deleted file mode 100644 index 90c21939..00000000 --- a/tests/data/mockargs/test_info_components-legalvalues.txt +++ /dev/null @@ -1 +0,0 @@ -{'field': 'component', 'product_id': 7} diff --git a/tests/mockbackend.py b/tests/mockbackend.py index b724b02b..dfd440ce 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -66,8 +66,6 @@ def bug_comments(self, *args): return self.__helper(args) def bug_create(self, *args): return self.__helper(args) - def bug_legal_values(self, *args): - return self.__helper(args) def bug_history(self, *args): return self.__helper(args) def bug_get(self, *args): diff --git a/tests/test_api_products.py b/tests/test_api_products.py index e6c7fe9f..41991ae1 100644 --- a/tests/test_api_products.py +++ b/tests/test_api_products.py @@ -50,8 +50,6 @@ def test_api_products(): ]} compnames = ["client-interfaces", "configuration"] - legal_values = {'values': compnames} - fakebz = tests.mockbackend.make_bz( product_get_enterable_args=None, product_get_enterable_return=prod_list_return, @@ -75,8 +73,6 @@ def test_api_products(): fakebz = tests.mockbackend.make_bz( product_get_args="data/mockargs/test_api_products_get2.txt", product_get_return=prod_get_return, - bug_legal_values_args=None, - bug_legal_values_return=legal_values, ) # Lookup in product cache by name diff --git a/tests/test_cli_info.py b/tests/test_cli_info.py index f7136e41..fe91e03d 100644 --- a/tests/test_cli_info.py +++ b/tests/test_cli_info.py @@ -47,25 +47,22 @@ def test_info(run_cli): tests.utils.diff_compare(out, cliprefix + "versions.txt") # info --components - legal_values = {'values': ["comp1", "test-comp-2", "hey-imma-comp"]} + prod_get_comp_active = {'products': [ + {'id': 7, 'name': 'test-fake-product', + 'components': [ + {'is_active': True, 'name': 'backend/kernel'}, + {'is_active': True, 'name': 'client-interfaces'}, + ]}, + ]} cmd = "bugzilla info --components test-fake-product" fakebz = tests.mockbackend.make_bz( product_get_args=argsprefix + "components.txt", - product_get_return=prod_get, - bug_legal_values_args=argsprefix + "components-legalvalues.txt", - bug_legal_values_return=legal_values) + product_get_return=prod_get_comp_active) out = run_cli(cmd, fakebz) tests.utils.diff_compare(out, cliprefix + "components.txt") # info --components --active-components cmd = "bugzilla info --components test-fake-product --active-components" - prod_get_comp_active = {'products': [ - {'id': 7, 'name': 'test-fake-product', - 'components': [ - {'is_active': True, 'name': 'backend/kernel'}, - {'is_active': True, 'name': 'client-interfaces'}, - ]}, - ]} fakebz = tests.mockbackend.make_bz( product_get_args=argsprefix + "components-active.txt", product_get_return=prod_get_comp_active) From 5ab112338535c04ef506287af1523d51689b1dcd Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 19:08:20 -0500 Subject: [PATCH 218/393] Bugzilla: Add some tests for regressed coverage pieces Signed-off-by: Cole Robinson --- tests/data/mockargs/test_api_getbugs2.txt | 1 + tests/data/mockargs/test_api_login2.txt | 1 + tests/test_api_bug.py | 5 +++++ tests/test_api_misc.py | 6 ++++++ 4 files changed, 13 insertions(+) create mode 100644 tests/data/mockargs/test_api_getbugs2.txt create mode 100644 tests/data/mockargs/test_api_login2.txt diff --git a/tests/data/mockargs/test_api_getbugs2.txt b/tests/data/mockargs/test_api_getbugs2.txt new file mode 100644 index 00000000..80d8cb73 --- /dev/null +++ b/tests/data/mockargs/test_api_getbugs2.txt @@ -0,0 +1 @@ +(['123456'], ['CVE-1234-FAKE'], {'permissive': 1}) diff --git a/tests/data/mockargs/test_api_login2.txt b/tests/data/mockargs/test_api_login2.txt new file mode 100644 index 00000000..36ee16ac --- /dev/null +++ b/tests/data/mockargs/test_api_login2.txt @@ -0,0 +1 @@ +{'login': None, 'password': None} diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index ec678908..bb76aaea 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -92,6 +92,11 @@ def test_api_getbugs(): assert bug.alias == ["CVE-1234-5678"] assert bug.autorefresh is True + fakebz = tests.mockbackend.make_bz( + bug_get_args="data/mockargs/test_api_getbugs2.txt", + bug_get_return={"bugs": [{}, {}]}) + assert fakebz.getbugs(["123456", "CVE-1234-FAKE"]) == [] + def test_bug_getattr(): fakebz = tests.mockbackend.make_bz( diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 357ef823..3a1e35d4 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -198,6 +198,12 @@ def test_api_login(): bz.connect("https:///fake/bugzilla.redhat.com") bz.connect() + # Test auto login if user/password is set + bz = tests.mockbackend.make_bz( + bz_kwargs={"user": "FOO", "password": "BAR"}, + user_login_args="data/mockargs/test_api_login2.txt", + user_login_return={}) + def test_interactive_login(capsys, monkeypatch): bz = tests.mockbackend.make_bz( From 49a865d1f033982f781d599b088155f52ab14923 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 11 Jan 2020 09:48:13 -0500 Subject: [PATCH 219/393] Finish _BackendREST implementation and Bugzilla probing The REST API is mostly working. Some test suite bits are skipped, but this mostly seems due to some Red Hat specific APIs that I can't find implementations for. The URL probing logic is: * If URL contains '/xmlrpc', assume XMLRPC * If URL contains '/rest', assume REST * Otherwise try the expected xmlrpc.cgi URL and if it exists, use it * Otherwise tru the expected /rest URL and if it exists, use it * Otherwise just attempt to initialize the XMLRPC backend Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 17 ++++++ bugzilla/_backendrest.py | 117 +++++++++++++++++++++++++++++++++++- bugzilla/base.py | 40 ++++++++---- tests/mockbackend.py | 7 ++- tests/test_ro_functional.py | 110 +++++++++++++++++++-------------- tests/test_rw_functional.py | 77 ++++++++++++------------ tests/utils.py | 7 +++ 7 files changed, 277 insertions(+), 98 deletions(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index 9032feb1..c65d739e 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -1,6 +1,12 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +from logging import getLogger + +import requests + +log = getLogger(__name__) + class _BackendBase(object): """ @@ -12,6 +18,17 @@ def __init__(self, url, bugzillasession): self._url = url self._bugzillasession = bugzillasession + + @staticmethod + def probe(url): + try: + requests.head(url).raise_for_status() + return True # pragma: no cover + except Exception as e: + log.debug("Failed to probe url=%s : %s", url, str(e)) + return False + + ################# # Internal APIs # ################# diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index 3d6c93a0..7d7ce92b 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -1,19 +1,23 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import base64 import json import logging import os from ._backendbase import _BackendBase from .exceptions import BugzillaError +from ._util import listify log = logging.getLogger(__name__) -# XXX remove this pylint disable -# pylint: disable=abstract-method +def _update_key(indict, updict, key): + if key not in indict: + indict[key] = {} + indict[key].update(updict.get(key, {})) class _BackendREST(_BackendBase): @@ -78,3 +82,112 @@ def is_rest(self): def bugzilla_version(self): return self._get("/version") + + def bug_create(self, paramdict): + return self._post("/bug", paramdict) + def bug_fields(self, paramdict): + return self._get("/field/bug", paramdict) + def bug_get(self, bug_ids, aliases, paramdict): + data = paramdict.copy() + data["id"] = listify(bug_ids) + data["alias"] = listify(aliases) + ret = self._get("/bug", data) + return ret + + def bug_attachment_get(self, attachment_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {} + for attid in listify(attachment_ids): + out = self._get("/bug/attachment/%s" % attid, paramdict) + _update_key(ret, out, "attachments") + _update_key(ret, out, "bugs") + return ret + + def bug_attachment_get_all(self, bug_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {} + for bugid in listify(bug_ids): + out = self._get("/bug/%s/attachment" % bugid, paramdict) + _update_key(ret, out, "attachments") + _update_key(ret, out, "bugs") + return ret + + def bug_attachment_create(self, bug_ids, data, paramdict): + if data is not None and "data" not in paramdict: + paramdict["data"] = base64.b64encode(data).decode("utf-8") + paramdict["ids"] = listify(bug_ids) + return self._post("/bug/%s/attachment" % paramdict["ids"][0], + paramdict) + + def bug_attachment_update(self, attachment_ids, paramdict): + paramdict["ids"] = listify(attachment_ids) + return self._put("/bug/attachment/%s" % paramdict["ids"][0], paramdict) + + def bug_comments(self, bug_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {} + for bugid in bug_ids: + out = self._get("/bug/%s/comment" % bugid, paramdict) + _update_key(ret, out, "bugs") + return ret + def bug_history(self, bug_ids, paramdict): + # XMLRPC supported mutiple fetch at once, but not REST + ret = {"bugs": []} + for bugid in bug_ids: + out = self._get("/bug/%s/history" % bugid, paramdict) + ret["bugs"].extend(out.get("bugs", [])) + return ret + + def bug_search(self, paramdict): + return self._get("/bug", paramdict) + def bug_update(self, bug_ids, paramdict): + data = paramdict.copy() + data["ids"] = listify(bug_ids) + return self._put("/bug/%s" % data["ids"][0], data) + def bug_update_tags(self, bug_ids, paramdict): + raise BugzillaError("No REST API available for bug_update_tags") + + def component_create(self, paramdict): + return self._post("/component", paramdict) + def component_update(self, paramdict): + if "ids" in paramdict: + apiurl = str(listify(paramdict["ids"])[0]) + if "names" in paramdict: + apiurl = ("%(product)s/%(component)s" % + listify(paramdict["names"])[0]) + return self._put("/component/%s" % apiurl, paramdict) + + def externalbugs_add(self, paramdict): + raise BugzillaError( + "No REST API available yet for externalbugs_add") + def externalbugs_remove(self, paramdict): + raise BugzillaError( + "No REST API available yet for externalbugs_remove") + def externalbugs_update(self, paramdict): + raise BugzillaError( + "No REST API available yet for externalbugs_update") + + def product_get(self, paramdict): + return self._get("/product/get", paramdict) + def product_get_accessible(self): + return self._get("/product_accessible") + def product_get_enterable(self): + return self._get("/product_enterable") + def product_get_selectable(self): + return self._get("/product_selectable") + + def user_create(self, paramdict): + return self._post("/user", paramdict) + def user_get(self, paramdict): + return self._get("/user", paramdict) + def user_login(self, paramdict): + return self._get("/login", paramdict) + def user_logout(self): + return self._get("/logout") + def user_update(self, paramdict): + urlid = None + if "ids" in paramdict: + urlid = listify(paramdict["ids"])[0] + if "names" in paramdict: + urlid = listify(paramdict["names"])[0] + return self._put("/user/%s" % urlid, paramdict) diff --git a/bugzilla/base.py b/bugzilla/base.py index edc8ecb8..2e0423da 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -454,15 +454,35 @@ def _set_bz_version(self, version): minor = 0 self._cache.version_parsed = (major, minor) - def _get_backend_class(self): # pragma: no cover + def _get_backend_class(self, url): # pragma: no cover # This is a hook for the test suite to do some mock hackery + if self._force_rest and self._force_xmlrpc: + raise BugzillaError( + "Cannot specify both force_rest and force_xmlrpc") + + xmlurl = self.fix_url(url) + if self._force_xmlrpc: + return _BackendXMLRPC, xmlurl + + resturl = self.fix_url(url, force_rest=self._force_rest) if self._force_rest: - return _BackendREST - elif self._force_xmlrpc: - return _BackendXMLRPC - # default to XMLRPC, like before - # FIXME: guess backend? - return _BackendXMLRPC + return _BackendREST, resturl + + # Simple heuristic if the original url has a path in it + if "/xmlrpc" in url: + return _BackendXMLRPC, xmlurl + if "/rest" in url: + return _BackendREST, resturl + + # We were passed something like bugzilla.example.com but we + # aren't sure which method to use, try probing + if _BackendXMLRPC.probe(xmlurl): + return _BackendXMLRPC, xmlurl + if _BackendREST.probe(resturl): + return _BackendREST, resturl + + # Otherwise fallback to XMLRPC default and let it fail + return _BackendXMLRPC, xmlurl def connect(self, url=None): """ @@ -479,10 +499,7 @@ def connect(self, url=None): if self._session: self.disconnect() - if url is None and self.url: - url = self.url - url = self.fix_url(url, force_rest=self._force_rest) - + backendclass, url = self._get_backend_class(url or self.url) self.url = url # we've changed URLs - reload config self.readconfig(overwrite=False) @@ -494,7 +511,6 @@ def connect(self, url=None): tokencache=self._tokencache, api_key=self.api_key, requests_session=self._user_requests_session) - backendclass = self._get_backend_class() self._backend = backendclass(url, self._session) if (self.user and self.password): diff --git a/tests/mockbackend.py b/tests/mockbackend.py index dfd440ce..0b64943b 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -132,10 +132,15 @@ def make_bz(bz_kwargs=None, rhbz=False, **kwargs): if "use_creds" not in bz_kwargs: bz_kwargs["use_creds"] = False + bz = bugzilla.Bugzilla(url=None, **bz_kwargs) backendclass = _make_backend_class(**kwargs) + + def _get_backend_class(url): + return backendclass, bugzilla.Bugzilla.fix_url(url) + # pylint: disable=protected-access - bz._get_backend_class = lambda *a, **k: backendclass + bz._get_backend_class = _get_backend_class url = "https:///TESTSUITEMOCK" if rhbz: diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index ffd63d2a..a5837fef 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -39,6 +39,21 @@ def _test_version(bz, bzversion): assert bz.bz_ver_minor == bzversion[1] +def test_rest_xmlrpc_detection(): + # The default: use XMLRPC + bz = _open_bz("bugzilla.redhat.com") + assert bz.is_xmlrpc() + assert "/xmlrpc.cgi" in bz.url + + # See /rest in the URL, so use REST + bz = _open_bz("bugzilla.redhat.com/rest") + assert bz.is_rest() + + # See /xmlrpc.cgi in the URL, so use XMLRPC + bz = _open_bz("bugzilla.redhat.com/xmlrpc.cgi") + assert bz.is_xmlrpc() + + ################### # mozilla testing # ################### @@ -58,10 +73,10 @@ def test_mozilla(backends): # gentoo testing # ################## -def test_gentoo(): +def test_gentoo(backends): url = "bugs.gentoo.org" bzversion = (5, 0) - bz = _open_bz(url) + bz = _open_bz(url, **backends) _test_version(bz, bzversion) # This is a bugzilla 5.0 instance, which supports URL queries now @@ -77,37 +92,37 @@ def test_gentoo(): ################## -def testInfoProducts(run_cli): - bz = _open_bz(REDHAT_URL) +def testInfoProducts(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) out = run_cli("bugzilla info --products", bz) _check(out, 123, "Virtualization Tools") -def testInfoComps(run_cli): - bz = _open_bz(REDHAT_URL) +def testInfoComps(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) out = run_cli("bugzilla info --components 'Virtualization Tools'", bz) _check(out, 8, "virtinst") -def testInfoVers(run_cli): - bz = _open_bz(REDHAT_URL) +def testInfoVers(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) out = run_cli("bugzilla info --versions Fedora", bz) _check(out, 17, "rawhide") -def testInfoCompOwners(run_cli): - bz = _open_bz(REDHAT_URL) +def testInfoCompOwners(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) out = run_cli("bugzilla info " "--component_owners 'Virtualization Tools'", bz) _check(out, None, "libvirt: Libvirt Maintainers") -def testQuery(run_cli): - bz = _open_bz(REDHAT_URL) +def testQuery(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) args = "--product Fedora --component python-bugzilla --version 14" cli = "bugzilla query %s --bug_status CLOSED" % args @@ -126,24 +141,24 @@ def testQuery(run_cli): l2 == expectbug]) -def testQueryFull(run_cli): - bz = _open_bz(REDHAT_URL) +def testQueryFull(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) bugid = "621601" out = run_cli("bugzilla query --full --bug_id %s" % bugid, bz) _check(out, 60, "end-of-life (EOL)") -def testQueryRaw(run_cli): - bz = _open_bz(REDHAT_URL) +def testQueryRaw(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) bugid = "307471" out = run_cli("bugzilla query --raw --bug_id %s" % bugid, bz) _check(out, 70, "ATTRIBUTE[whiteboard]: bzcl34nup") -def testQueryOneline(run_cli): - bz = _open_bz(REDHAT_URL) +def testQueryOneline(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) bugid = "785016" out = run_cli("bugzilla query --oneline --bug_id %s" % bugid, bz) @@ -158,8 +173,8 @@ def testQueryOneline(run_cli): assert " CVE-2011-2527" in out -def testQueryExtra(run_cli): - bz = _open_bz(REDHAT_URL) +def testQueryExtra(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) bugid = "307471" out = run_cli("bugzilla query --extra --bug_id %s" % bugid, bz) @@ -167,8 +182,8 @@ def testQueryExtra(run_cli): assert " +Status Whiteboard: bzcl34nup" in out -def testQueryFormat(run_cli): - bz = _open_bz(REDHAT_URL) +def testQueryFormat(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) args = ("--bug_id 307471 --outputformat=\"id=%{bug_id} " "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " @@ -188,8 +203,8 @@ def testQueryFormat(run_cli): assert u"V34 — system" in out -def testQueryURL(run_cli): - bz = _open_bz(REDHAT_URL) +def testQueryURL(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) qurl = ("/buglist.cgi?f1=creation_ts" "&list_id=973582&o1=greaterthaneq&classification=Fedora&" @@ -206,35 +221,37 @@ def testQueryURL(run_cli): _check(out, 22, "#553878 CLOSED") -def testQueryFixedIn(run_cli): - bz = _open_bz(REDHAT_URL) +def testQueryFixedIn(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) out = run_cli("bugzilla query --fixed_in anaconda-15.29-1", bz) assert len(out.splitlines()) == 4 assert "#629311 CLOSED" in out -def testComponentsDetails(): +def testComponentsDetails(backends): """ Fresh call to getcomponentsdetails should properly refresh """ - bz = _open_bz(REDHAT_URL) + bz = _open_bz(REDHAT_URL, **backends) assert bool(bz.getcomponentsdetails("Red Hat Developer Toolset")) -def testGetBugAlias(): +def testGetBugAlias(backends): """ getbug() works if passed an alias """ - bz = _open_bz(REDHAT_URL) + bz = _open_bz(REDHAT_URL, **backends) bug = bz.getbug("CVE-2011-2527") assert bug.bug_id == 720773 -def testQuerySubComponent(run_cli): - bz = _open_bz(REDHAT_URL) +def testQuerySubComponent(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) + + tests.utils.skip_if_rest(bz, "Not working on REST, not sure why yet") # Test special error wrappers in bugzilla/_cli.py out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 7' " @@ -243,16 +260,17 @@ def testQuerySubComponent(run_cli): assert "#1060931 " in out -def testBugFields(): - bz = _open_bz(REDHAT_URL) +def testBugFields(backends): + bz = _open_bz(REDHAT_URL, **backends) + fields = bz.getbugfields(names=["product"])[:] assert fields == ["product"] bz.getbugfields(names=["product", "bug_status"], force_refresh=True) assert set(bz.bugfields) == set(["product", "bug_status"]) -def testBugAutoRefresh(): - bz = _open_bz(REDHAT_URL) +def testBugAutoRefresh(backends): + bz = _open_bz(REDHAT_URL, **backends) bz.bug_autorefresh = True @@ -272,8 +290,8 @@ def testBugAutoRefresh(): assert "adjust your include_fields" in str(e) -def testExtraFields(): - bz = _open_bz(REDHAT_URL) +def testExtraFields(backends): + bz = _open_bz(REDHAT_URL, **backends) # Check default extra_fields will pull in comments bug = bz.getbug(720773, exclude_fields=["product"]) @@ -286,8 +304,8 @@ def testExtraFields(): assert "comments" not in dir(bug) -def testExternalBugsOutput(run_cli): - bz = _open_bz(REDHAT_URL) +def testExternalBugsOutput(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) out = run_cli('bugzilla query --bug_id 989253 ' '--outputformat="%{external_bugs}"', bz) @@ -295,8 +313,8 @@ def testExternalBugsOutput(run_cli): assert "External bug: https://bugs.launchpad.net/bugs/1203576" in out -def testActiveComps(run_cli): - bz = _open_bz(REDHAT_URL) +def testActiveComps(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) out = run_cli("bugzilla info --components 'Virtualization Tools' " "--active-components", bz) @@ -306,8 +324,8 @@ def testActiveComps(run_cli): assert "virtinst" not in out -def testFaults(run_cli): - bz = _open_bz(REDHAT_URL) +def testFaults(run_cli, backends): + bz = _open_bz(REDHAT_URL, **backends) # Test special error wrappers in bugzilla/_cli.py out = run_cli("bugzilla query --field=IDONTEXIST=FOO", bz, @@ -326,9 +344,9 @@ def testFaults(run_cli): assert "--nosslverify" in out -def test_redhat_version(): +def test_redhat_version(backends): bzversion = (5, 0) - bz = _open_bz(REDHAT_URL) + bz = _open_bz(REDHAT_URL, **backends) if not tests.CLICONFIG.REDHAT_URL: _test_version(bz, bzversion) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 3ced7190..ccaf93ae 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -17,8 +17,6 @@ import random import sys -import pytest - import bugzilla import tests @@ -88,11 +86,11 @@ def _makebug(run_cli, bz): return bug -def test03NewBugBasic(run_cli): +def test03NewBugBasic(run_cli, backends): """ Create a bug with minimal amount of fields, then close it """ - bz = _open_bz() + bz = _open_bz(**backends) bug = _makebug(run_cli, bz) # Verify hasattr works @@ -106,11 +104,11 @@ def test03NewBugBasic(run_cli): assert bug.resolution == "NOTABUG" -def test04NewBugAllFields(run_cli): +def test04NewBugAllFields(run_cli, backends): """ Create a bug using all 'new' fields, check some values, close it """ - bz = _open_bz() + bz = _open_bz(**backends) summary = ("python-bugzilla test manyfields bug %s" % datetime.datetime.today()) @@ -170,11 +168,11 @@ def test04NewBugAllFields(run_cli): assert len(ret["bugs"][0]["history"]) -def test05ModifyStatus(run_cli): +def test05ModifyStatus(run_cli, backends): """ Modify status and comment fields for an existing bug """ - bz = _open_bz() + bz = _open_bz(**backends) bugid = "663674" cmd = "bugzilla modify %s " % bugid @@ -257,11 +255,11 @@ def test05ModifyStatus(run_cli): assert bug.status == origstatus -def test06ModifyEmails(run_cli): +def test06ModifyEmails(run_cli, backends): """ Modify cc, assignee, qa_contact for existing bug """ - bz = _open_bz() + bz = _open_bz(**backends) bugid = "663674" cmd = "bugzilla modify %s " % bugid @@ -305,11 +303,11 @@ def test06ModifyEmails(run_cli): assert bug.qa_contact == "extras-qa@fedoraproject.org" -def test07ModifyMultiFlags(run_cli): +def test07ModifyMultiFlags(run_cli, backends): """ Modify flags and fixed_in for 2 bugs """ - bz = _open_bz() + bz = _open_bz(**backends) bugid1 = "461686" bugid2 = "461687" cmd = "bugzilla modify %s %s " % (bugid1, bugid2) @@ -394,10 +392,10 @@ def cleardict_new(b): assert bug2.fixed_in == "-" -def test07ModifyMisc(run_cli): +def test07ModifyMisc(run_cli, backends): bugid = "461686" cmd = "bugzilla modify %s " % bugid - bz = _open_bz() + bz = _open_bz(**backends) bug = bz.getbug(bugid) # modify --dependson @@ -482,7 +480,7 @@ def test07ModifyMisc(run_cli): assert bug.cf_release_notes == "baz" -def test08Attachments(run_cli): +def test08Attachments(run_cli, backends): tmpdir = "__test_attach_output" if tmpdir in os.listdir("."): os.system("rm -r %s" % tmpdir) @@ -490,17 +488,17 @@ def test08Attachments(run_cli): os.chdir(tmpdir) try: - _test8Attachments(run_cli) + _test8Attachments(run_cli, backends) finally: os.chdir("..") os.system("rm -r %s" % tmpdir) -def _test8Attachments(run_cli): +def _test8Attachments(run_cli, backends): """ Get and set attachments for a bug """ - bz = _open_bz() + bz = _open_bz(**backends) cmd = "bugzilla attach " testfile = "../tests/data/bz-attach-get1.txt" @@ -597,8 +595,8 @@ def _test8Attachments(run_cli): os.unlink(f) -def test09Whiteboards(run_cli): - bz = _open_bz() +def test09Whiteboards(run_cli, backends): + bz = _open_bz(**backends) bug_id = "663674" cmd = "bugzilla modify %s " % bug_id bug = bz.getbug(bug_id) @@ -685,9 +683,9 @@ def test10Login(run_cli, monkeypatch): assert "Login failed: " in ret -def test11UserUpdate(): +def test11UserUpdate(backends): # This won't work if run by the same user we are using - bz = _open_bz() + bz = _open_bz(**backends) email = "anaconda-maint-list@redhat.com" group = "fedora_contrib" @@ -743,8 +741,8 @@ def test11UserUpdate(): assert user.groupnames == origgroups -def test11ComponentEditing(): - bz = _open_bz() +def test11ComponentEditing(backends): + bz = _open_bz(**backends) component = ("python-bugzilla-testcomponent-%s" % str(random.randint(1, 1024 * 1024 * 1024))) basedata = { @@ -793,6 +791,9 @@ def compare(data, newid): # bugzilla 5 error string ("You are not allowed" in str(e))) + # bugzilla.redhat.com doesn't have REST editcomponent yet + tests.utils.skip_if_rest( + bz, "editcomponent not supported for redhat REST API") # Edit component data = basedata.copy() @@ -816,8 +817,8 @@ def compare(data, newid): ("You are not allowed" in str(e))) -def test13SubComponents(): - bz = _open_bz() +def test13SubComponents(backends): + bz = _open_bz(**backends) # Long closed RHEL5 lvm2 bug. This component has sub_components bug = bz.getbug("185526") bug.autorefresh = True @@ -835,11 +836,14 @@ def test13SubComponents(): "Default / Unclassified (RHEL5)"]} -def test14ExternalTrackersAddUpdateRemoveQuery(): - bz = _open_bz() +def test14ExternalTrackersAddUpdateRemoveQuery(backends): + bz = _open_bz(**backends) bugid = 461686 ext_bug_id = 380489 + tests.utils.skip_if_rest( + bz, "unknown if REST API has externaltrackers support") + # Delete any existing external trackers to get to a known state ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] if ids != []: @@ -884,18 +888,20 @@ def test14ExternalTrackersAddUpdateRemoveQuery(): assert len(ids) == 0 -def test15EnsureLoggedIn(run_cli): - bz = _open_bz() +def test15EnsureLoggedIn(run_cli, backends): + bz = _open_bz(**backends) comm = "bugzilla --ensure-logged-in query --bug_id 979546" run_cli(comm, bz) -def test16ModifyTags(run_cli): +def test16ModifyTags(run_cli, backends): bugid = "461686" cmd = "bugzilla modify %s " % bugid - bz = _open_bz() + bz = _open_bz(**backends) bug = bz.getbug(bugid) + tests.utils.skip_if_rest(bz, "update_tags not supported for REST API") + if bug.tags: bz.update_tags(bug.id, tags_remove=bug.tags) bug.refresh() @@ -914,12 +920,9 @@ def test16ModifyTags(run_cli): assert bug.tags == [] -def test17LoginAPIKey(): +def test17LoginAPIKey(backends): api_key = "somefakeapikey1234" - bz = _open_bz(use_creds=False, api_key=api_key) - if bz.bz_ver_major < 5: - pytest.skip("can only test apikey on bugzilla 5+") - + bz = _open_bz(use_creds=False, api_key=api_key, **backends) try: assert bz.logged_in is False diff --git a/tests/utils.py b/tests/utils.py index 75f96a22..630ee253 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,6 +10,8 @@ import shlex import sys +import pytest + import bugzilla._cli from bugzilla._compatimports import IS_PY3 @@ -64,6 +66,11 @@ def fake_request(*args, **kwargs): return bz +def skip_if_rest(bz, msg): + if bz.is_rest(): + pytest.skip(msg) + + def diff_compare(inputdata, filename, expect_out=None): """Compare passed string output to contents of filename""" def _process(data): From 6da20650b2a0ca7a2cba50aab1b9d5b8e5bf27b4 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Fri, 29 Nov 2019 15:28:29 +0100 Subject: [PATCH 220/393] Add some support for bugzilla groups With this commit one can now search/retrieve a specific group and its members. It also stores the list of emails of its members for convenience. Fixes https://github.com/python-bugzilla/python-bugzilla/issues/111 Signed-off-by: Pierre-Yves Chibon --- bugzilla/base.py | 61 +++++++++++++++++++++++++++++++++++++++++++- bugzilla/bug.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 2e0423da..a6b76117 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -21,7 +21,7 @@ from ._backendrest import _BackendREST from ._backendxmlrpc import _BackendXMLRPC from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl -from .bug import Bug, User +from .bug import Bug, Group, User from .exceptions import BugzillaError from .rhbugzilla import _RHBugzillaConverters from ._session import _BugzillaSession @@ -1893,6 +1893,65 @@ def updateperms(self, user, action, groups): return self._backend.user_update(update) + ############################### + # Methods for handling Groups # + ############################### + + def _getgroups(self, names, membership=False): + """ + Return a list of users that match criteria. + + :kwarg ids: list of user ids to return data on + :kwarg membership: boolean specifying wether to query the members + of the group or not. + :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the + names array. + Code 304: if the user was not authorized to see user they + requested. + Code 505: user is logged out and can't use the match or ids + parameter. + """ + params = {"membership": membership} + params['names'] = listify(names) + return self._proxy.Group.get(params) + + def getgroup(self, name, membership=False): + """ + Return a bugzilla Group for the given name + + :arg name: The name used in bugzilla. + :raises XMLRPC Fault: Code 51 if the name does not exist + :returns: Group record for the name + """ + ret = self.getgroups(name, membership=membership) + return ret and ret[0] + + def getgroups(self, grouplist, membership=False): + """ + Return a list of Groups from . + + :userlist: List of group names to lookup + :returns: List of Group records + """ + grouplist = listify(grouplist) + groupobjs = [ + Group(self, **rawgroup) + for rawgroup in self._getgroups( + names=grouplist, membership=membership).get('groups', []) + ] + + # Return in same order they were passed in + ret = [] + for g in grouplist: + for gobj in groupobjs[:]: + if gobj.name == g: + groupobjs.remove(gobj) + ret.append(gobj) + break + ret += groupobjs + return ret + + ############################# # ExternalBugs API wrappers # ############################# diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 79102007..91369ea4 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -445,3 +445,69 @@ def updateperms(self, action, groups): :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) """ self.bugzilla.updateperms(self.name, action, groups) + + +class Group(object): + """ + Container object for a bugzilla Group. + + :arg bugzilla: Bugzilla instance that this Group belongs to. + Rest of the params come straight from Group.get() + """ + def __init__(self, bugzilla, **kwargs): + self.bugzilla = bugzilla + self.__groupid = kwargs.get('id') + + self.name = kwargs.get('name') + self.description = kwargs.get('description', self.name) + self.is_active = kwargs.get('is_active', False) + self.icon_url = kwargs.get('icon_url', None) + self.is_active_bug_group = kwargs.get('is_active_bug_group', None) + + self.membership = kwargs.get('membership', []) + self.__member_emails = set() + self._refresh_member_emails_list() + + ######################## + # Read-only attributes # + ######################## + + # We make these properties so that the user cannot set them. They are + # unaffected by the update() method so it would be misleading to let them + # be changed. + @property + def groupid(self): + return self.__groupid + + @property + def member_emails(self): + return sorted(self.__member_emails) + + def _refresh_member_emails_list(self): + """ + Refresh the list of emails of the members of the group. + """ + if self.membership: + for m in self.membership: + if "email" in m: + self.__member_emails.add(m["email"]) + + def refresh(self, membership=False): + """ + Update Group object with latest info from bugzilla + """ + newgroup = self.bugzilla.getgroup( + self.name, membership=membership) + self.__dict__.update(newgroup.__dict__) + self._refresh_member_emails_list() + + def members(self): + """ + Retrieve the members of this Group from bugzilla + """ + if self.membership: + return self.membership + + self.refresh(membership=True) + + return self.membership From 4d6c31e794ab9fc62738d10c6ef29cae7ec7b24d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 13:52:31 -0500 Subject: [PATCH 221/393] Convert groups API to _Backend * Add REST imple * Add unittest coverage * Add functional test coverage * Use new listify Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 11 ++++ bugzilla/_backendrest.py | 3 + bugzilla/_backendxmlrpc.py | 3 + bugzilla/base.py | 2 +- bugzilla/bug.py | 7 +-- tests/data/mockargs/test_api_groups_get1.txt | 1 + tests/data/mockargs/test_api_groups_get2.txt | 1 + tests/mockbackend.py | 3 + tests/test_api_groups.py | 60 ++++++++++++++++++++ tests/test_rw_functional.py | 9 +++ 10 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 tests/data/mockargs/test_api_groups_get1.txt create mode 100644 tests/data/mockargs/test_api_groups_get2.txt create mode 100644 tests/test_api_groups.py diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index c65d739e..b81e1082 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -204,6 +204,17 @@ def externalbugs_remove(self, paramdict): raise NotImplementedError() + ############## + # Group APIs # + ############## + + def group_get(self, paramdict): + """ + https://bugzilla.readthedocs.io/en/latest/api/core/v1/group.html#get-group + """ + raise NotImplementedError() + + ################ # Product APIs # ################ diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index 7d7ce92b..d2090d25 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -167,6 +167,9 @@ def externalbugs_update(self, paramdict): raise BugzillaError( "No REST API available yet for externalbugs_update") + def group_get(self, paramdict): + return self._get("/group", paramdict) + def product_get(self, paramdict): return self._get("/product/get", paramdict) def product_get_accessible(self): diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 23eefb39..e2182cd7 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -218,6 +218,9 @@ def externalbugs_update(self, paramdict): def externalbugs_remove(self, paramdict): return self._xmlrpc_proxy.ExternalBugs.remove_external_bug(paramdict) + def group_get(self, paramdict): + return self._xmlrpc_proxy.Group.get(paramdict) + def product_get(self, paramdict): return self._xmlrpc_proxy.Product.get(paramdict) def product_get_accessible(self): diff --git a/bugzilla/base.py b/bugzilla/base.py index a6b76117..304e3807 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1913,7 +1913,7 @@ def _getgroups(self, names, membership=False): """ params = {"membership": membership} params['names'] = listify(names) - return self._proxy.Group.get(params) + return self._backend.group_get(params) def getgroup(self, name, membership=False): """ diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 91369ea4..72f8e508 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -505,9 +505,6 @@ def members(self): """ Retrieve the members of this Group from bugzilla """ - if self.membership: - return self.membership - - self.refresh(membership=True) - + if not self.membership: + self.refresh(membership=True) return self.membership diff --git a/tests/data/mockargs/test_api_groups_get1.txt b/tests/data/mockargs/test_api_groups_get1.txt new file mode 100644 index 00000000..b750e91b --- /dev/null +++ b/tests/data/mockargs/test_api_groups_get1.txt @@ -0,0 +1 @@ +{'membership': False, 'names': ['TestGroups']} diff --git a/tests/data/mockargs/test_api_groups_get2.txt b/tests/data/mockargs/test_api_groups_get2.txt new file mode 100644 index 00000000..70138e42 --- /dev/null +++ b/tests/data/mockargs/test_api_groups_get2.txt @@ -0,0 +1 @@ +{'membership': True, 'names': ['TestGroup']} diff --git a/tests/mockbackend.py b/tests/mockbackend.py index 0b64943b..3dcfc014 100644 --- a/tests/mockbackend.py +++ b/tests/mockbackend.py @@ -86,6 +86,9 @@ def component_get(self, *args): def component_update(self, *args): return self.__helper(args) + def group_get(self, *args): + return self.__helper(args) + def externalbugs_add(self, *args): return self.__helper(args) def externalbugs_update(self, *args): diff --git a/tests/test_api_groups.py b/tests/test_api_groups.py new file mode 100644 index 00000000..2f1f425c --- /dev/null +++ b/tests/test_api_groups.py @@ -0,0 +1,60 @@ +# +# Copyright Red Hat, Inc. 2012 +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. +# + +""" +Test miscellaneous API bits +""" + +import tests +import tests.mockbackend + + +def test_api_groups(): + # Basic API testing of the users APIs + group_ret = {"groups": [{ + "membership": [ + {"real_name": "Bugzilla User", + "can_login": 1, + "name": "user@bugzilla.org", + "login_denied_text": "", + "id": 85, + "email_enabled": 1, + "email": "user@bugzilla.org"}, + {"real_name": "Bugzilla User2", + "can_login": 0, + "name": "user2@bugzilla.org", + "login_denied_text": "", + "id": 77, + "email_enabled": 0, + "email": "user2@bugzilla.org"}, + ], + "is_active": 1, + "description": "Test Group", + "user_regexp": "", + "is_bug_group": 1, + "name": "TestGroup", + "id": 9 + }]} + + fakebz = tests.mockbackend.make_bz( + group_get_args="data/mockargs/test_api_groups_get1.txt", + group_get_return=group_ret) + + # getgroups testing + groupobj = fakebz.getgroups("TestGroups")[0] + assert groupobj.groupid == 9 + assert groupobj.member_emails == [ + "user2@bugzilla.org", "user@bugzilla.org"] + assert groupobj.name == "TestGroup" + + # getgroup testing + fakebz = tests.mockbackend.make_bz( + group_get_args="data/mockargs/test_api_groups_get2.txt", + group_get_return=group_ret) + groupobj = fakebz.getgroup("TestGroup", membership=True) + groupobj.membership = [] + assert groupobj.members() == group_ret["groups"][0]["membership"] diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index ccaf93ae..f00c76fc 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -696,6 +696,15 @@ def test11UserUpdate(backends): assert group in user.groupnames origgroups = user.groupnames + # Test group_get + try: + group = bz.getgroup("fedora_contrib") + group.refresh() + except Exception as e: + if have_admin: + raise + assert bugzilla.BugzillaError.get_bugzilla_error_code(e) == 805 + # Remove the group try: bz.updateperms(email, "remove", [group]) From 52d925510d442c71b1d4fe830d77c0c68d70d7c0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 14:24:20 -0500 Subject: [PATCH 222/393] Bugzilla: Simplify some login() logging Signed-off-by: Cole Robinson --- bugzilla/base.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 304e3807..aa73cee6 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -597,17 +597,16 @@ def login(self, user=None, password=None, restrict_login=None): if not self.password: raise ValueError("missing password") + payload = {"login": user} if restrict_login: - log.info("logging in with restrict_login=True") + payload['restrict_login'] = True + log.debug("logging in with options %s", str(payload)) + payload['password'] = password try: - payload = {'login': user, 'password': password} - if restrict_login: - payload['restrict_login'] = True - ret = self._backend.user_login(payload) self.password = '' - log.info("login successful for user=%s", self.user) + log.info("login succeeded for user=%s", self.user) return ret except Exception as e: log.debug("Login exception: %s", str(e), exc_info=True) @@ -663,7 +662,6 @@ def interactive_login(self, user=None, password=None, force=False, log.info('Logging in... ') self.login(user, password, restrict_login) - log.info('Authorization cookie received.') def logout(self): """ From 20c480ba4db439947dc8ef26b5065117879af039 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 14:55:59 -0500 Subject: [PATCH 223/393] Bugzilla: Move login success printing into interactive_login We already do some printing there, this unifies it. Extend the printing to report when we actually save the token to disk, and where we save it. Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 5 ----- bugzilla/base.py | 15 ++++++++++----- tests/test_cli_login.py | 7 ++++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 982280d0..27b1913b 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1053,11 +1053,6 @@ def _handle_login(opt, action, bz): sys.exit(1) if is_login_command: - msg = "Login successful." - if (bz.cookiefile or bz.tokenfile) and not use_key: - msg = "Login successful, token cache updated." - - print(msg) sys.exit(0) diff --git a/bugzilla/base.py b/bugzilla/base.py index aa73cee6..42a659da 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -629,11 +629,12 @@ def _ask_api_key(self): log.info('API Key accepted') wrote_filename = self._rcfile.save_api_key(self.url, self.api_key) + log.info("API key written to filename=%s", wrote_filename) + + msg = "Login successful." if wrote_filename: - log.info("API key written to %s", wrote_filename) - print("API key written to %s" % wrote_filename) - else: # pragma: no cover - log.info("API Key won't be updated because use_creds=False") + msg += " API key written to %s" % wrote_filename + print(msg) def interactive_login(self, user=None, password=None, force=False, restrict_login=None, use_api_key=False): @@ -661,7 +662,11 @@ def interactive_login(self, user=None, password=None, force=False, password = getpass.getpass('Bugzilla Password: ') log.info('Logging in... ') - self.login(user, password, restrict_login) + out = self.login(user, password, restrict_login) + msg = "Login successful." + if "token" in out and self.tokenfile: + msg += " Token cache saved to %s" % self.tokenfile + print(msg) def logout(self): """ diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py index 60335878..a140181f 100644 --- a/tests/test_cli_login.py +++ b/tests/test_cli_login.py @@ -58,12 +58,13 @@ def test_login(run_cli): # Returns success for logged_in check and hits a tokenfile line cmd = "bugzilla --ensure-logged-in " - cmd += "--user FOO --password BAR login" + cmd += "login FOO BAR" fakebz = tests.mockbackend.make_bz( bz_kwargs={"use_creds": True}, user_login_args="data/mockargs/test_login.txt", - user_login_return={}, + user_login_return={'id': 1234, 'token': 'my-fake-token'}, user_get_args=None, user_get_return={}) out = run_cli(cmd, fakebz) - assert "token cache updated" in out + assert "Token cache saved" in out + assert fakebz.tokenfile in out From 22fc599feffedf7554033ad0e2d5caf68ae3c894 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 15:10:57 -0500 Subject: [PATCH 224/393] tests: Do interactive login tests through the cli Signed-off-by: Cole Robinson --- tests/test_api_misc.py | 37 ++----------------------------------- tests/test_cli_login.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 3a1e35d4..f63cc5ae 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -9,9 +9,6 @@ Test miscellaneous API bits """ -import sys -import tempfile - import pytest import bugzilla @@ -202,43 +199,13 @@ def test_api_login(): bz = tests.mockbackend.make_bz( bz_kwargs={"user": "FOO", "password": "BAR"}, user_login_args="data/mockargs/test_api_login2.txt", - user_login_return={}) - - -def test_interactive_login(capsys, monkeypatch): - bz = tests.mockbackend.make_bz( - user_login_args="data/mockargs/test_interactive_login.txt", user_login_return={}, user_logout_args=None, - user_logout_return={}, - user_get_args=None, - user_get_return={}) - - tests.utils.monkeypatch_getpass(monkeypatch) + user_logout_return={}) - fakestdin = tests.utils.fake_stream("fakeuser\nfakepass\n") - monkeypatch.setattr(sys, "stdin", fakestdin) - bz.interactive_login() + # Test logout bz.logout() - out = capsys.readouterr()[0] - assert "Bugzilla Username:" in out - assert "Bugzilla Password:" in out - - # API key prompting and saving - tmp = tempfile.NamedTemporaryFile() - bz.configpath = [tmp.name] - bz.url = "https://example.com" - - fakestdin = tests.utils.fake_stream("MY-FAKE-KEY\n") - monkeypatch.setattr(sys, "stdin", fakestdin) - bz.interactive_login(use_api_key=True) - out = capsys.readouterr()[0] - assert "API Key:" in out - assert tmp.name in out - tests.utils.diff_compare(open(tmp.name).read(), - "data/clioutput/test_interactive_login_apikey_rcfile.txt") - def test_version_bad(): # Hit version error handling diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py index a140181f..ef6441fe 100644 --- a/tests/test_cli_login.py +++ b/tests/test_cli_login.py @@ -1,6 +1,8 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import tempfile + import pytest import bugzilla @@ -68,3 +70,33 @@ def test_login(run_cli): out = run_cli(cmd, fakebz) assert "Token cache saved" in out assert fakebz.tokenfile in out + + +def test_interactive_login(monkeypatch, run_cli): + bz = tests.mockbackend.make_bz( + user_login_args="data/mockargs/test_interactive_login.txt", + user_login_return={}, + user_logout_args=None, + user_logout_return={}, + user_get_args=None, + user_get_return={}) + + tests.utils.monkeypatch_getpass(monkeypatch) + cmd = "bugzilla login" + fakestdin = "fakeuser\nfakepass\n" + out = run_cli(cmd, bz, stdin=fakestdin) + assert "Bugzilla Username:" in out + assert "Bugzilla Password:" in out + + # API key prompting and saving + tmp = tempfile.NamedTemporaryFile() + bz.configpath = [tmp.name] + bz.url = "https://example.com" + + cmd = "bugzilla login --api-key" + fakestdin = "MY-FAKE-KEY\n" + out = run_cli(cmd, bz, stdin=fakestdin) + assert "API Key:" in out + assert tmp.name in out + tests.utils.diff_compare(open(tmp.name).read(), + "data/clioutput/test_interactive_login_apikey_rcfile.txt") From 42150863cf3104c57e4f35e3b50f42b3806da30a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 15:11:25 -0500 Subject: [PATCH 225/393] bugzilla: Add interactive_save_api_key Split out from interactive_login(use_api_key=True), because they are really distinct operations Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 8 ++++---- bugzilla/base.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 27b1913b..f1c473dd 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1037,11 +1037,11 @@ def _handle_login(opt, action, bz): use_key = getattr(opt, "api_key", False) try: - if do_interactive_login or use_key: - if bz.url and not use_key: - print("Logging into %s" % urlparse(bz.url)[1]) + if use_key: + bz.interactive_save_api_key() + elif do_interactive_login: + print("Logging into %s" % urlparse(bz.url)[1]) bz.interactive_login(username, password, - use_api_key=use_key, restrict_login=opt.restrict_login) except bugzilla.BugzillaError as e: print(str(e)) diff --git a/bugzilla/base.py b/bugzilla/base.py index 42a659da..b22e31c2 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -613,7 +613,12 @@ def login(self, user=None, password=None, restrict_login=None): raise BugzillaError("Login failed: %s" % BugzillaError.get_bugzilla_error_string(e)) - def _ask_api_key(self): + def interactive_save_api_key(self): + """ + Helper method to interactively ask for an API key, verify it + is valid, and save it to a bugzillarc file referenced via + self.configpaths + """ sys.stdout.write('API Key: ') sys.stdout.flush() api_key = sys.stdin.readline().strip() @@ -637,7 +642,7 @@ def _ask_api_key(self): print(msg) def interactive_login(self, user=None, password=None, force=False, - restrict_login=None, use_api_key=False): + restrict_login=None): """ Helper method to handle login for this bugzilla instance. @@ -645,15 +650,10 @@ def interactive_login(self, user=None, password=None, force=False, :param password: bugzilla password. If not specified, prompt for it. :param force: Unused :param restrict_login: restricts session to IP address - :param use_api_key: If True, prompt for an api_key instead """ ignore = force log.debug('Calling interactive_login') - if use_api_key: - self._ask_api_key() - return - if not user: sys.stdout.write('Bugzilla Username: ') sys.stdout.flush() From 500479e14bb0529b6f444425069fc738111b21db Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 15:20:57 -0500 Subject: [PATCH 226/393] cli: If 'login' called but we have an API key, print and exit Otherwise the API throws an unfriendly error Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 4 ++++ tests/test_cli_login.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index f1c473dd..3f82f968 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1040,6 +1040,10 @@ def _handle_login(opt, action, bz): if use_key: bz.interactive_save_api_key() elif do_interactive_login: + if bz.api_key: + print("You already have an API key configured for %s" % bz.url) + print("There is no need to cache a login token. Exiting.") + sys.exit(0) print("Logging into %s" % urlparse(bz.url)[1]) bz.interactive_login(username, password, restrict_login=opt.restrict_login) diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py index ef6441fe..f9a8a7ba 100644 --- a/tests/test_cli_login.py +++ b/tests/test_cli_login.py @@ -100,3 +100,9 @@ def test_interactive_login(monkeypatch, run_cli): assert tmp.name in out tests.utils.diff_compare(open(tmp.name).read(), "data/clioutput/test_interactive_login_apikey_rcfile.txt") + + # Check that we don't attempt to log in if API key is configured + assert bz.api_key + cmd = "bugzilla login" + out = run_cli(cmd, bz) + assert "already have an API" in out From 5184517e1f5f9fddaa1dd18111a7a67f42a15b87 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 15:42:01 -0500 Subject: [PATCH 227/393] session: Don't pass both api_key and token to the URL Passing just API key will make some situations less ambiguous Signed-off-by: Cole Robinson --- bugzilla/_session.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 129d1441..70631e66 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -58,6 +58,12 @@ def set_content_type(self, value): self._session.headers["Content-Type"] = value def _set_tokencache_param(self): + if self._api_key: + # Don't add a token to the params list if an API key is set. + # Keeping API key solo means bugzilla will definitely fail + # if the key expires. Passing in a token could hide that + # fact, which could make it confusing to pinpoint the issue. + return token = self.get_token_value() self._session.params["Bugzilla_token"] = token From 4823ef9e7a4d8b9edd12e8af597284bcec83216c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 16:13:33 -0500 Subject: [PATCH 228/393] Bug: Track all API data internally as _rawdata This simplifies things compared to just tracking the field names, at the expense of some memory, but will help implement things like CLI JSON output Signed-off-by: Cole Robinson --- bugzilla/bug.py | 61 +++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 72f8e508..d4a2a809 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -29,7 +29,7 @@ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): # API had pre-existing issue that we can't change ('dict' usage) self.bugzilla = bugzilla - self._bug_fields = [] + self._rawdata = {} self.autorefresh = autorefresh if not dict: @@ -111,41 +111,45 @@ def refresh(self, include_fields=None, exclude_fields=None, Refresh the bug with the latest data from bugzilla """ # pylint: disable=protected-access + extra_fields = list(self._rawdata.keys()) + (extra_fields or []) r = self.bugzilla._getbug(self.bug_id, include_fields=include_fields, exclude_fields=exclude_fields, - extra_fields=self._bug_fields + (extra_fields or [])) + extra_fields=extra_fields) # pylint: enable=protected-access self._update_dict(r) reload = refresh + def _translate_dict(self, newdict): + if not self.bugzilla: + return + + self.bugzilla.post_translation({}, newdict) + + # pylint: disable=protected-access + aliases = self.bugzilla._get_bug_aliases() + # pylint: enable=protected-access + + for newname, oldname in aliases: + if oldname not in newdict: + continue + + if newname not in newdict: + newdict[newname] = newdict[oldname] + elif newdict[newname] != newdict[oldname]: + log.debug("Update dict contained differing alias values " + "d[%s]=%s and d[%s]=%s , dropping the value " + "d[%s]", newname, newdict[newname], oldname, + newdict[oldname], oldname) + del(newdict[oldname]) + + def _update_dict(self, newdict): """ Update internal dictionary, in a way that ensures no duplicate entries are stored WRT field aliases """ - if self.bugzilla: - self.bugzilla.post_translation({}, newdict) - - # pylint: disable=protected-access - aliases = self.bugzilla._get_bug_aliases() - # pylint: enable=protected-access - - for newname, oldname in aliases: - if oldname not in newdict: - continue - - if newname not in newdict: - newdict[newname] = newdict[oldname] - elif newdict[newname] != newdict[oldname]: - log.debug("Update dict contained differing alias values " - "d[%s]=%s and d[%s]=%s , dropping the value " - "d[%s]", newname, newdict[newname], oldname, - newdict[oldname], oldname) - del(newdict[oldname]) - - for key in newdict.keys(): - if key not in self._bug_fields: - self._bug_fields.append(key) + self._translate_dict(newdict) + self._rawdata.update(newdict) self.__dict__.update(newdict) if 'id' not in self.__dict__ and 'bug_id' not in self.__dict__: @@ -157,13 +161,10 @@ def _update_dict(self, newdict): ################## def __getstate__(self): - ret = {} - for key in self._bug_fields: - ret[key] = self.__dict__[key] - return ret + return self._rawdata def __setstate__(self, vals): - self._bug_fields = [] + self._rawdata = {} self.bugzilla = None self._update_dict(vals) From 3337328685641db2d2f987b0d5b89dfa5fb4dd8e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 16:23:16 -0500 Subject: [PATCH 229/393] bug: Fix several issues with pickling * Cache bugzilla aliases internally, so we can restore them * Handle bugzilla == None in several places Closes: #105 Signed-off-by: Cole Robinson --- bugzilla/bug.py | 34 +++++++++++++++++----------------- tests/test_api_bug.py | 2 ++ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index d4a2a809..83f39f93 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -32,6 +32,10 @@ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): self._rawdata = {} self.autorefresh = autorefresh + # pylint: disable=protected-access + self._aliases = self.bugzilla._get_bug_aliases() + # pylint: enable=protected-access + if not dict: dict = {} if bug_id: @@ -59,8 +63,10 @@ def __unicode__(self): self.assigned_to, self.summary) def __repr__(self): - return '' % (self.bug_id, self.bugzilla.url, - id(self)) + url = "" + if self.bugzilla: + url = self.bugzilla.url + return '' % (self.bug_id, url, id(self)) def __getattr__(self, name): refreshed = False @@ -70,11 +76,7 @@ def __getattr__(self, name): # have never been called. return self.__dict__[name] - # pylint: disable=protected-access - aliases = self.bugzilla._get_bug_aliases() - # pylint: enable=protected-access - - for newname, oldname in aliases: + for newname, oldname in self._aliases: if name == oldname and newname in self.__dict__: return self.__dict__[newname] @@ -120,16 +122,10 @@ def refresh(self, include_fields=None, exclude_fields=None, reload = refresh def _translate_dict(self, newdict): - if not self.bugzilla: - return - - self.bugzilla.post_translation({}, newdict) - - # pylint: disable=protected-access - aliases = self.bugzilla._get_bug_aliases() - # pylint: enable=protected-access + if self.bugzilla: + self.bugzilla.post_translation({}, newdict) - for newname, oldname in aliases: + for newname, oldname in self._aliases: if oldname not in newdict: continue @@ -161,11 +157,15 @@ def _update_dict(self, newdict): ################## def __getstate__(self): - return self._rawdata + ret = self._rawdata.copy() + ret["_aliases"] = self._aliases + return ret def __setstate__(self, vals): self._rawdata = {} self.bugzilla = None + self._aliases = vals.get("_aliases", []) + self.autorefresh = False self._update_dict(vals) diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index bb76aaea..54372269 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -70,6 +70,8 @@ def _assert_bug(): fd.seek(0) bug = pickle.load(fd) assert getattr(bug, "bugzilla") is None + assert str(bug) + assert repr(bug) bug.bugzilla = rhbz _assert_bug() From 0fc4eaec3e6c3ef81c3ace493be600e742c8d0e8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 12 Jan 2020 17:09:40 -0500 Subject: [PATCH 230/393] Bug: Add get_raw_data API Returns the raw API data that populated the bug Signed-off-by: Cole Robinson --- bugzilla/bug.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 83f39f93..f25609dd 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -6,6 +6,7 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import copy from logging import getLogger from ._util import to_encoding @@ -107,6 +108,13 @@ def __getattr__(self, name): "to adjust your include_fields for getbug/query." % name) raise AttributeError(msg) + def get_raw_data(self): + """ + Return the raw API dictionary data that has been used to + populate this bug + """ + return copy.deepcopy(self._rawdata) + def refresh(self, include_fields=None, exclude_fields=None, extra_fields=None): """ From f2a2351b8ead2cdaf6f99d9d21982d30645e766f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 10:43:57 -0500 Subject: [PATCH 231/393] cli: Split apart _format_output No functional change, just a clarity cleanup Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 164 +++++++++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 78 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 3f82f968..9762e57f 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -642,90 +642,98 @@ def _convert_to_outputformat(output): return fmt -def _format_output(bz, opt, buglist): - if opt.output == 'raw': - buglist = bz.getbugs([b.bug_id for b in buglist]) - for b in buglist: - print("Bugzilla %s: " % b.bug_id) - SKIP_NAMES = ["bugzilla"] - for attrname in sorted(b.__dict__): - if attrname in SKIP_NAMES: - continue - if attrname.startswith("_"): +def _format_output_raw(buglist): + for b in buglist: + print("Bugzilla %s: " % b.bug_id) + SKIP_NAMES = ["bugzilla"] + for attrname in sorted(b.__dict__): + if attrname in SKIP_NAMES: + continue + if attrname.startswith("_"): + continue + print(to_encoding(u"ATTRIBUTE[%s]: %s" % + (attrname, b.__dict__[attrname]))) + print("\n\n") + + +def _bug_field_repl_cb(bz, b, matchobj): + # whiteboard and flag allow doing + # %{whiteboard:devel} and %{flag:needinfo} + # That's what 'rest' matches + (fieldname, rest) = matchobj.groups() + + if fieldname == "whiteboard" and rest: + fieldname = rest + "_" + fieldname + + if fieldname == "flag" and rest: + val = b.get_flag_status(rest) + + elif fieldname in ["flags", "flags_requestee"]: + tmpstr = [] + for f in getattr(b, "flags", []): + requestee = f.get('requestee', "") + if fieldname == "flags": + requestee = "" + if fieldname == "flags_requestee": + if requestee == "": continue - print(to_encoding(u"ATTRIBUTE[%s]: %s" % - (attrname, b.__dict__[attrname]))) - print("\n\n") - return + tmpstr.append("%s" % requestee) + else: + tmpstr.append("%s%s%s" % + (f['name'], f['status'], requestee)) + + val = ",".join(tmpstr) + + elif fieldname == "cve": + cves = [] + for key in getattr(b, "keywords", []): + # grab CVE from keywords and blockers + if key.find("Security") == -1: + continue + for bl in b.blocks: + cvebug = bz.getbug(bl) + for cb in cvebug.alias: + if (cb.find("CVE") != -1 and + cb.strip() not in cves): + cves.append(cb) + val = ",".join(cves) + + elif fieldname == "comments": + val = "" + for c in getattr(b, "comments", []): + val += ("\n* %s - %s:\n%s\n" % (c['time'], + c.get("creator", c.get("author", "")), c['text'])) + + elif fieldname == "external_bugs": + val = "" + for e in getattr(b, "external_bugs", []): + url = e["type"]["full_url"].replace("%id%", e["ext_bz_bug_id"]) + if not val: + val += "\n" + val += "External bug: %s\n" % url + + elif fieldname == "__unicode__": + val = b.__unicode__() + else: + val = getattr(b, fieldname, "") - def bug_field(matchobj): - # whiteboard and flag allow doing - # %{whiteboard:devel} and %{flag:needinfo} - # That's what 'rest' matches - (fieldname, rest) = matchobj.groups() - - if fieldname == "whiteboard" and rest: - fieldname = rest + "_" + fieldname - - if fieldname == "flag" and rest: - val = b.get_flag_status(rest) - - elif fieldname in ["flags", "flags_requestee"]: - tmpstr = [] - for f in getattr(b, "flags", []): - requestee = f.get('requestee', "") - if fieldname == "flags": - requestee = "" - if fieldname == "flags_requestee": - if requestee == "": - continue - tmpstr.append("%s" % requestee) - else: - tmpstr.append("%s%s%s" % - (f['name'], f['status'], requestee)) - - val = ",".join(tmpstr) - - elif fieldname == "cve": - cves = [] - for key in getattr(b, "keywords", []): - # grab CVE from keywords and blockers - if key.find("Security") == -1: - continue - for bl in b.blocks: - cvebug = bz.getbug(bl) - for cb in cvebug.alias: - if (cb.find("CVE") != -1 and - cb.strip() not in cves): - cves.append(cb) - val = ",".join(cves) - - elif fieldname == "comments": - val = "" - for c in getattr(b, "comments", []): - val += ("\n* %s - %s:\n%s\n" % (c['time'], - c.get("creator", c.get("author", "")), c['text'])) - - elif fieldname == "external_bugs": - val = "" - for e in getattr(b, "external_bugs", []): - url = e["type"]["full_url"].replace("%id%", e["ext_bz_bug_id"]) - if not val: - val += "\n" - val += "External bug: %s\n" % url - - elif fieldname == "__unicode__": - val = b.__unicode__() - else: - val = getattr(b, fieldname, "") + vallist = isinstance(val, list) and val or [val] + val = ','.join([to_encoding(v) for v in vallist]) - vallist = isinstance(val, list) and val or [val] - val = ','.join([to_encoding(v) for v in vallist]) + return val - return val + +def _format_output(bz, opt, buglist): + if opt.output == 'raw': + buglist = bz.getbugs([b.bug_id for b in buglist]) + _format_output_raw(buglist) + return for b in buglist: - print(format_field_re.sub(bug_field, opt.outputformat)) + # pylint: disable=cell-var-from-loop + def cb(matchobj): + return _bug_field_repl_cb(bz, b, matchobj) + print(format_field_re.sub(cb, opt.outputformat)) def _parse_triset(vallist, checkplus=True, checkminus=True, checkequal=True, From 583481bbe9a17fa832d4f17b25e15cb6e11d4fd3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 10:49:32 -0500 Subject: [PATCH 232/393] cli: Add --json output Add a json output method to the cli, which is much more standard and flexible than any other output method Closes: #72 Signed-off-by: Cole Robinson --- bugzilla.1 | 2 + bugzilla/_cli.py | 37 +++- bugzilla/_compatimports.py | 5 +- tests/data/clioutput/test_json_xmlrpc.txt | 9 + tests/data/clioutput/test_query8.txt | 248 ++++++++++++++++++++++ tests/data/mockargs/test_query8.txt | 1 + tests/test_cli_misc.py | 50 +++++ tests/test_cli_query.py | 13 ++ tests/utils.py | 6 + 9 files changed, 364 insertions(+), 7 deletions(-) create mode 100644 tests/data/clioutput/test_json_xmlrpc.txt create mode 100644 tests/data/clioutput/test_query8.txt create mode 100644 tests/data/mockargs/test_query8.txt diff --git a/bugzilla.1 b/bugzilla.1 index 28c192af..7cf2e51d 100644 --- a/bugzilla.1 +++ b/bugzilla.1 @@ -138,6 +138,8 @@ output additional bug information (keywords, Whiteboards, etc.) one line summary of the bug (useful for scripts) .IP "--raw" raw output of the bugzilla contents +.IP "--json" +output bug contents in JSON format .IP "--outputformat=OUTPUTFORMAT" Print output in the form given. You can use RPM-style tags that match bug fields, e.g.: '%{id}: %{summary}'. diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 9762e57f..fec1e806 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -11,10 +11,13 @@ from __future__ import print_function +import argparse +import base64 +import datetime import errno +import json import locale from logging import getLogger, DEBUG, INFO, WARN, StreamHandler, Formatter -import argparse import os import re import socket @@ -160,6 +163,8 @@ def _parser_add_output_options(p): help="one line summary of the bug (useful for scripts)") outg.add_argument('--raw', action='store_const', dest='output', const='raw', help="raw output of the bugzilla contents") + outg.add_argument('--json', action='store_const', dest='output', + const='json', help="output contents in json format") outg.add_argument('--outputformat', help="Print output in the form given. " "You can use RPM-style tags that match bug " @@ -480,7 +485,7 @@ def _do_query(bz, opt, parser): setattr(opt, optname, val.split(",")) include_fields = None - if opt.output == 'raw': + if opt.output in ['raw', 'json']: # 'raw' always does a getbug() call anyways, so just ask for ID back include_fields = ['id'] @@ -642,6 +647,25 @@ def _convert_to_outputformat(output): return fmt +def _xmlrpc_converter(obj): + if "DateTime" in str(obj.__class__): + # xmlrpc DateTime object. Convert to date format that + # bugzilla REST API outputs + dobj = datetime.datetime.strptime(str(obj), '%Y%m%dT%H:%M:%S') + return dobj.isoformat() + "Z" + if "Binary" in str(obj.__class__): + # xmlrpc Binary object. Convert to base64 + return base64.b64encode(obj.data).decode("utf-8") + raise RuntimeError( + "Unexpected JSON conversion class=%s" % obj.__class__) + + +def _format_output_json(buglist): + out = {"bugs": [b.get_raw_data() for b in buglist]} + s = json.dumps(out, default=_xmlrpc_converter, indent=2, sort_keys=True) + print(s) + + def _format_output_raw(buglist): for b in buglist: print("Bugzilla %s: " % b.bug_id) @@ -724,9 +748,12 @@ def _bug_field_repl_cb(bz, b, matchobj): def _format_output(bz, opt, buglist): - if opt.output == 'raw': + if opt.output in ['raw', 'json']: buglist = bz.getbugs([b.bug_id for b in buglist]) - _format_output_raw(buglist) + if opt.output == 'json': + _format_output_json(buglist) + if opt.output == 'raw': + _format_output_raw(buglist) return for b in buglist: @@ -1094,7 +1121,7 @@ def _main(unittest_bz_instance): ########################### if hasattr(opt, "outputformat"): - if not opt.outputformat and opt.output not in ['raw', None]: + if not opt.outputformat and opt.output not in ['raw', 'json', None]: opt.outputformat = _convert_to_outputformat(opt.output) buglist = [] diff --git a/bugzilla/_compatimports.py b/bugzilla/_compatimports.py index 11e93f1f..b531566d 100644 --- a/bugzilla/_compatimports.py +++ b/bugzilla/_compatimports.py @@ -12,12 +12,13 @@ from configparser import ConfigParser from http.cookiejar import LoadError, MozillaCookieJar from urllib.parse import urlparse, urlunparse, parse_qsl - from xmlrpc.client import (Binary, Fault, ProtocolError, + from xmlrpc.client import (Binary, DateTime, Fault, ProtocolError, ServerProxy, Transport) else: # pragma: no cover from collections import Mapping from ConfigParser import SafeConfigParser as ConfigParser from cookielib import LoadError, MozillaCookieJar from urlparse import urlparse - from xmlrpclib import Binary, Fault, ProtocolError, ServerProxy, Transport + from xmlrpclib import (Binary, DateTime, Fault, ProtocolError, + ServerProxy, Transport) from urlparse import urlparse, urlunparse, parse_qsl diff --git a/tests/data/clioutput/test_json_xmlrpc.txt b/tests/data/clioutput/test_json_xmlrpc.txt new file mode 100644 index 00000000..262b9487 --- /dev/null +++ b/tests/data/clioutput/test_json_xmlrpc.txt @@ -0,0 +1,9 @@ +{ + "bugs": [ + { + "binarytest": "LS0tIGJhc2UucHkub2xkCTIwMTAtMTItMTYgMTI6MTU6MDkuOTMyMDEwNjU5ICswMTAwCisrKyBiYXNlLnB5CTIwMTAtMTItMTYgMTY6MDQ6MTguOTk1MTg1OTMzICswMTAwCkBAIC0xOSw2ICsxOSw4IEBACiBpbXBvcnQgdGVtcGZpbGUKIGltcG9ydCBsb2dnaW5nCiBpbXBvcnQgbG9jYWxlCitpbXBvcnQgZW1haWwuaGVhZGVyCitpbXBvcnQgcmUKIAogbG9nID0gbG9nZ2luZy5nZXRMb2dnZXIoJ2J1Z3ppbGxhJykKIApAQCAtNjc3LDEwICs2NzksMTcgQEAKICAgICAgICAgIyBSRkMgMjE4MyBkZWZpbmVzIHRoZSBjb250ZW50LWRpc3Bvc2l0aW9uIGhlYWRlciwgaWYgeW91J3JlIGN1cmlvdXMKICAgICAgICAgZGlzcCA9IGF0dC5oZWFkZXJzWydjb250ZW50LWRpc3Bvc2l0aW9uJ10uc3BsaXQoJzsnKQogICAgICAgICBbZmlsZW5hbWVfcGFybV0gPSBbaSBmb3IgaSBpbiBkaXNwIGlmIGkuc3RyaXAoKS5zdGFydHN3aXRoKCdmaWxlbmFtZT0nKV0KLSAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nKQotICAgICAgICAjIFJGQyAyMDQ1LzgyMiBkZWZpbmVzIHRoZSBncmFtbWFyIGZvciB0aGUgZmlsZW5hbWUgdmFsdWUsIGJ1dAotICAgICAgICAjIEkgdGhpbmsgd2UganVzdCBuZWVkIHRvIHJlbW92ZSB0aGUgcXVvdGluZy4gSSBob3BlLgotICAgICAgICBhdHQubmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgKGR1bW15LGZpbGVuYW1lKSA9IGZpbGVuYW1lX3Bhcm0uc3BsaXQoJz0nLDEpCisgICAgICAgICMgUkZDIDIwNDUvODIyIGRlZmluZXMgdGhlIGdyYW1tYXIgZm9yIHRoZSBmaWxlbmFtZSB2YWx1ZQorICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lLnN0cmlwKCciJykKKyAgICAgICAgIyBlbWFpbC5oZWFkZXIuZGVjb2RlX2hlYWRlciBjYW5ub3QgaGFuZGxlIHN0cmluZ3Mgbm90IGVuZGluZyB3aXRoICc/PScsCisgICAgICAgICMgc28gbGV0J3MgdHJhbnNmb3JtIG9uZSA9Py4uLj89IHBhcnQgYXQgYSB0aW1lCisgICAgICAgIHdoaWxlIFRydWU6CisgICAgICAgICAgICBtYXRjaCA9IHJlLnNlYXJjaCgiPVw/Lio/XD89IiwgZmlsZW5hbWUpCisgICAgICAgICAgICBpZiBtYXRjaCBpcyBOb25lOgorICAgICAgICAgICAgICAgIGJyZWFrCisgICAgICAgICAgICBmaWxlbmFtZSA9IGZpbGVuYW1lWzptYXRjaC5zdGFydCgpXSArIGVtYWlsLmhlYWRlci5kZWNvZGVfaGVhZGVyKG1hdGNoLmdyb3VwKDApKVswXVswXSArIGZpbGVuYW1lW21hdGNoLmVuZCgpOl0KKyAgICAgICAgYXR0Lm5hbWUgPSBmaWxlbmFtZQogICAgICAgICAjIEhvb3JheSwgbm93IHdlIGhhdmUgYSBmaWxlLWxpa2Ugb2JqZWN0IHdpdGggLnJlYWQoKSBhbmQgLm5hbWUKICAgICAgICAgcmV0dXJuIGF0dAogCg==", + "id": 1165434, + "timetest": "2018-12-09T19:12:12Z" + } + ] +} diff --git a/tests/data/clioutput/test_query8.txt b/tests/data/clioutput/test_query8.txt new file mode 100644 index 00000000..b8d29c56 --- /dev/null +++ b/tests/data/clioutput/test_query8.txt @@ -0,0 +1,248 @@ +{ + "bugs": [ + { + "actual_time": 0.0, + "alias": [], + "assigned_to": "lvm-team@redhat.com", + "assigned_to_detail": { + "email": "lvm-team@redhat.com", + "id": 206817, + "name": "lvm-team@redhat.com", + "real_name": "LVM and device-mapper development team" + }, + "blocks": [ + 123456 + ], + "cc": [ + "example@redhat.com", + "example2@redhat.com" + ], + "cc_detail": [ + { + "email": "example@redhat.com", + "id": 123456, + "name": "example@redhat.com", + "real_name": "Example user" + }, + { + "email": "example2@redhat.com", + "id": 123457, + "name": "heinzm@redhat.com", + "real_name": "Example2 user" + } + ], + "cf_build_id": "", + "cf_conditional_nak": [], + "cf_cust_facing": "---", + "cf_devel_whiteboard": "somedeveltag,someothertag", + "cf_doc_type": "Bug Fix", + "cf_environment": "", + "cf_fixed_in": "", + "cf_internal_whiteboard": "someinternal TAG", + "cf_last_closed": "2016-03-03T22:15:07", + "cf_partner": [], + "cf_pgm_internal": "", + "cf_pm_score": "0", + "cf_qa_whiteboard": "foo bar baz", + "cf_qe_conditional_nak": [], + "cf_release_notes": "", + "cf_target_upstream_version": "", + "cf_verified": [], + "classification": "Red Hat", + "comments": [ + { + "bug_id": 1165434, + "count": 0, + "creation_time": "2014-11-19T00:26:50", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685441, + "is_private": false, + "tags": [], + "text": "Description of problem:\nVersion-Release number of selected component (if applicable):\nkernel-2.6.18-308.el5\ndevice-mapper-multipath-0.4.7-48.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-event-1.02.67-2.el5\n", + "time": "2014-11-19T00:26:50" + }, + { + "bug_id": 1165434, + "count": 1, + "creation_time": "2014-11-19T00:47:57", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685467, + "is_private": false, + "tags": [], + "text": "We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond:\n\ncrash> bt\nany interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device.", + "time": "2014-11-19T00:47:57" + }, + { + "bug_id": 1165434, + "count": 2, + "creation_time": "2014-11-19T01:53:53", + "creator": "example@redhat.com", + "creator_id": 156796, + "id": 7685595, + "is_private": false, + "tags": [], + "text": "Test text", + "time": "2014-11-19T01:53:53" + } + ], + "depends_on": [ + 112233 + ], + "docs_contact": "", + "estimated_time": 0.0, + "external_bugs": [ + { + "bug_id": 989253, + "ext_bz_bug_id": "703421", + "ext_bz_id": 3, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115528, + "type": { + "can_get": true, + "can_send": false, + "description": "GNOME Bugzilla", + "full_url": "https://bugzilla.gnome.org/show_bug.cgi?id=%id%", + "id": 3, + "must_send": false, + "send_once": false, + "type": "Bugzilla", + "url": "https://bugzilla.gnome.org" + } + }, + { + "bug_id": 989253, + "ext_bz_bug_id": "1203576", + "ext_bz_id": 29, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115527, + "type": { + "can_get": false, + "can_send": false, + "description": "Launchpad", + "full_url": "https://bugs.launchpad.net/bugs/%id%", + "id": 29, + "must_send": false, + "send_once": false, + "type": "None", + "url": "https://bugs.launchpad.net/bugs" + } + } + ], + "flags": [ + { + "creation_date": "2019-11-15T21:57:21Z", + "id": 4302313, + "is_active": 1, + "modification_date": "2019-11-15T21:57:21Z", + "name": "qe_test_coverage", + "setter": "pm-rhel@redhat.com", + "status": "?", + "type_id": 318 + }, + { + "creation_date": "2018-12-25T16:47:43Z", + "id": 3883137, + "is_active": 1, + "modification_date": "2018-12-25T16:47:43Z", + "name": "release", + "setter": "rule-engine@redhat.com", + "status": "?", + "type_id": 1197 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883134, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "pm_ack", + "setter": "example3@redhat.com", + "status": "?", + "type_id": 11 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883135, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "devel_ack", + "setter": "example2@redhat.com", + "status": "?", + "type_id": 10 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883136, + "is_active": 1, + "modification_date": "2019-04-28T02:07:03Z", + "name": "qa_ack", + "setter": "example@redhat.com", + "status": "+", + "type_id": 9 + }, + { + "creation_date": "2019-03-29T06:50:01Z", + "id": 3999302, + "is_active": 1, + "modification_date": "2019-03-29T06:50:01Z", + "name": "needinfo", + "requestee": "hello@example.com", + "setter": "example@redhat.com", + "status": "?", + "type_id": 1164 + } + ], + "groups": [ + "somegroup" + ], + "id": 1165434, + "is_cc_accessible": true, + "is_confirmed": true, + "is_creator_accessible": true, + "is_open": false, + "keywords": [ + "key1", + "keyword2", + "Security" + ], + "last_change_time": "2018-12-09T19:12:12", + "op_sys": "Linux", + "platform": "All", + "priority": "medium", + "product": "Red Hat Enterprise Linux 5", + "qa_contact": "mspqa-list@redhat.com", + "qa_contact_detail": { + "email": "mspqa-list@redhat.com", + "id": 164197, + "name": "mspqa-list@redhat.com", + "real_name": "Cluster QE" + }, + "remaining_time": 0.0, + "resolution": "WONTFIX", + "see_also": [], + "severity": "medium", + "status": "CLOSED", + "sub_components": { + "lvm2": [ + "dmeventd (RHEL5)" + ] + }, + "summary": "LVM mirrored root can deadlock dmeventd if a mirror leg is lost", + "tags": [], + "target_milestone": "rc", + "target_release": [ + "---" + ], + "url": "", + "version": [ + "5.8" + ], + "whiteboard": "genericwhiteboard" + } + ] +} diff --git a/tests/data/mockargs/test_query8.txt b/tests/data/mockargs/test_query8.txt new file mode 100644 index 00000000..d499186e --- /dev/null +++ b/tests/data/mockargs/test_query8.txt @@ -0,0 +1 @@ +{'id': ['1165434'], 'include_fields': ['id']} diff --git a/tests/test_cli_misc.py b/tests/test_cli_misc.py index 39cff8a6..fd042cfe 100644 --- a/tests/test_cli_misc.py +++ b/tests/test_cli_misc.py @@ -11,9 +11,16 @@ from __future__ import print_function +import base64 +import datetime +import json + +import pytest import requests import bugzilla +from bugzilla._compatimports import Binary, DateTime + import tests import tests.mockbackend @@ -91,3 +98,46 @@ def testManualURL(run_cli): run_cli(cmd, None) except Exception as e: assert "No host supplied" in str(e) + + +def test_json_xmlrpc(run_cli): + # Test --json output with XMLRPC type conversion + cmd = "bugzilla query --json --id 1165434" + + timestr = '20181209T19:12:12' + dateobj = datetime.datetime.strptime(timestr, '%Y%m%dT%H:%M:%S') + + attachfile = tests.utils.tests_path("data/bz-attach-get1.txt") + attachdata = open(attachfile, "rb").read() + + bugid = 1165434 + data = {"bugs": [{ + 'id': bugid, + 'timetest': DateTime(dateobj), + 'binarytest': Binary(attachdata), + }]} + + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return={"bugs": [{"id": bugid}]}, + bug_get_args=None, + bug_get_return=data) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(tests.utils.sanitize_json(out), + "data/clioutput/test_json_xmlrpc.txt") + + retdata = json.loads(out)["bugs"][0] + assert (base64.b64decode(retdata["binarytest"]) == + attachdata) + assert retdata["timetest"] == dateobj.isoformat() + "Z" + + + # Test an error case, json converter can't handle Exception class + data["bugs"][0]["foo"] = Exception("foo") + fakebz = tests.mockbackend.make_bz( + bug_search_args=None, + bug_search_return={"bugs": [{"id": bugid}]}, + bug_get_args=None, + bug_get_return=data) + with pytest.raises(RuntimeError): + run_cli(cmd, fakebz, expectfail=True) diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py index b094c121..47c9dd42 100644 --- a/tests/test_cli_query.py +++ b/tests/test_cli_query.py @@ -1,6 +1,7 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import json import os import re @@ -119,3 +120,15 @@ def test_query(run_cli): bug_search_return="data/mockreturn/test_getbug_rhel.txt") out = run_cli(cmd, fakebz) tests.utils.diff_compare(out, "data/clioutput/test_query7.txt") + + # Test --json output + cmd = "bugzilla query --json --id 1165434" + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query8.txt", + bug_search_return={"bugs": [{"id": 1165434}]}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(tests.utils.sanitize_json(out), + "data/clioutput/test_query8.txt") + assert json.loads(out) diff --git a/tests/utils.py b/tests/utils.py index 630ee253..7f0850a5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -45,6 +45,12 @@ def monkeypatch_getpass(monkeypatch): raw_input) # pylint: disable=undefined-variable +def sanitize_json(rawout): + # py2.7 leaves trailing whitespace after commas. strip it so + # tests pass on both python versions + return "\n".join([l.rstrip() for l in rawout.splitlines()]) + + def open_functional_bz(bzclass, url, kwargs): bz = bzclass(url, **kwargs) From 5db4ea21bef75a69ceb4d245a309322810a9ee92 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 11:00:05 -0500 Subject: [PATCH 233/393] Bugzilla: Tweak URL logging a bit Because we call fix_url multiple times internally, the logging there is noisy. Instead centralize relevant logging in connect() Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 3 --- bugzilla/base.py | 17 +++++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index fec1e806..30088d9e 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1104,9 +1104,6 @@ def _main(unittest_bz_instance): log.debug("Launched with command line: %s", " ".join(sys.argv)) log.debug("Bugzilla module: %s", bugzilla) - # Connect to bugzilla - log.info('Connecting to %s', opt.bugzilla) - if unittest_bz_instance: bz = unittest_bz_instance else: diff --git a/bugzilla/base.py b/bugzilla/base.py index b22e31c2..fe38df65 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -143,7 +143,6 @@ def fix_url(url, force_rest=False): """ scheme, netloc, path, params, query, fragment = urlparse(url) if not scheme: - log.debug('No scheme given for url, assuming https') scheme = 'https' if path and not netloc: @@ -154,11 +153,8 @@ def fix_url(url, force_rest=False): path = 'xmlrpc.cgi' if force_rest: path = "rest/" - log.debug('No path given for url, assuming /%s', path) newurl = urlunparse((scheme, netloc, path, params, query, fragment)) - if newurl != url: - log.debug("Generated fixed URL: %s", newurl) return newurl @staticmethod @@ -499,19 +495,24 @@ def connect(self, url=None): if self._session: self.disconnect() - backendclass, url = self._get_backend_class(url or self.url) - self.url = url + url = url or self.url + backendclass, newurl = self._get_backend_class(url) + if url != newurl: + log.debug("Converted url=%s to fixed url=%s", url, newurl) + self.url = newurl + log.debug("Connecting with URL %s", self.url) + # we've changed URLs - reload config self.readconfig(overwrite=False) - self._session = _BugzillaSession(url, self.user_agent, + self._session = _BugzillaSession(self.url, self.user_agent, cookiecache=self._cookiecache, sslverify=self._sslverify, cert=self.cert, tokencache=self._tokencache, api_key=self.api_key, requests_session=self._user_requests_session) - self._backend = backendclass(url, self._session) + self._backend = backendclass(self.url, self._session) if (self.user and self.password): log.info("user and password present - doing login()") From 495e7ccf91391fecd8a8b34f514c58a9eefc36eb Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 11:04:52 -0500 Subject: [PATCH 234/393] Bugzilla: Print interactive_login warning when API keys are available Point users explicitly to API keys, because tokens will be removed in future bugzilla releases Closes: #93 Signed-off-by: Cole Robinson --- bugzilla/base.py | 3 +++ tests/test_cli_login.py | 1 + 2 files changed, 4 insertions(+) diff --git a/bugzilla/base.py b/bugzilla/base.py index fe38df65..17dc529a 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -667,6 +667,9 @@ def interactive_login(self, user=None, password=None, force=False, msg = "Login successful." if "token" in out and self.tokenfile: msg += " Token cache saved to %s" % self.tokenfile + if self._get_version() >= 5.0: + msg += "\nToken usage is deprecated. " + msg += "Consider using bugzilla API keys instead." print(msg) def logout(self): diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py index f9a8a7ba..967a92c2 100644 --- a/tests/test_cli_login.py +++ b/tests/test_cli_login.py @@ -70,6 +70,7 @@ def test_login(run_cli): out = run_cli(cmd, fakebz) assert "Token cache saved" in out assert fakebz.tokenfile in out + assert "Consider using bugzilla API" in out def test_interactive_login(monkeypatch, run_cli): From 0fa3c5db9f65d43b7310afb9b747918d5619e918 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 14:48:52 -0500 Subject: [PATCH 235/393] spec: Fix RPM warnings about trailing comments Signed-off-by: Cole Robinson --- python-bugzilla.spec | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 1c36678c..5f0eaa5e 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -29,14 +29,14 @@ BuildRequires: python2-devel BuildRequires: python2-requests BuildRequires: python2-setuptools BuildRequires: python2-pytest -%endif # with python2 +%endif %if %{with python3} BuildRequires: python3-devel BuildRequires: python3-requests BuildRequires: python3-setuptools BuildRequires: python3-pytest -%endif # if with_python3 +%endif %global _description\ python-bugzilla is a python library for interacting with bugzilla instances\ @@ -56,7 +56,7 @@ Requires: python-bugzilla-cli %description -n python2-bugzilla %_description -%endif # with python2 +%endif %if %{with python3} @@ -68,10 +68,10 @@ Requires: python3-requests %if %{without python2} Obsoletes: python-bugzilla < %{version}-%{release} Obsoletes: python2-bugzilla < %{version}-%{release} -%endif # without python2 +%endif %description -n python3-bugzilla %_description -%endif # if with_python3 +%endif %package cli @@ -93,7 +93,7 @@ This package includes the 'bugzilla' command-line tool for interacting with bugz %if %{with python3} rm -rf %{py3dir} cp -a . %{py3dir} -%endif # with_python3 +%endif @@ -102,11 +102,11 @@ cp -a . %{py3dir} pushd %{py3dir} %{__python3} setup.py build popd -%endif # with_python3 +%endif %if %{with python2} %{__python2} setup.py build -%endif # with python2 +%endif @@ -120,11 +120,11 @@ rm %{buildroot}/usr/bin/bugzilla %endif popd -%endif # with_python3 +%endif %if %{with python2} %{__python2} setup.py install -O1 --skip-build --root %{buildroot} -%endif # with python2 +%endif # Replace '#!/usr/bin/env python' with '#!/usr/bin/python2' # The format is ideal for upstream, but not a distro. See: @@ -144,10 +144,10 @@ done %if %{with python2} # py.test naming is needed for RHEL7 compat, works fine with Fedora py.test -%endif # with python2 +%endif %if %{with python3} pytest-3 -%endif # with python3 +%endif @@ -155,13 +155,13 @@ pytest-3 %files -n python2-bugzilla %doc COPYING README.md NEWS.md %{python2_sitelib}/* -%endif # with python2 +%endif %if %{with python3} %files -n python3-bugzilla %doc COPYING README.md NEWS.md %{python3_sitelib}/* -%endif # with_python3 +%endif %files cli %{_bindir}/bugzilla From d4b6b6730c7bfc6d1ba6e0c4c9fcafa72bbf21ea Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 14:56:28 -0500 Subject: [PATCH 236/393] tests: Don't fail if we can't set locale Instead just print an error. This can happen in Fedora buildroots Signed-off-by: Cole Robinson --- tests/conftest.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c0c467e4..cdba933e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,10 +13,6 @@ import bugzilla -# Use consistent locale for tests -locale.setlocale(locale.LC_CTYPE, 'en_US.UTF-8') - - # pytest plugin adding custom options. Hooks are documented here: # https://docs.pytest.org/en/latest/writing_plugins.html @@ -61,6 +57,13 @@ def pytest_ignore_collect(path, config): def pytest_configure(config): + try: + # Needed for test reproducibility on systems not using a UTF-8 locale + locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_CTYPE, 'en_US.UTF-8') + except Exception as e: + print("Error setting locale: %s" % str(e)) + if config.getoption("--redhat-url"): tests.CLICONFIG.REDHAT_URL = config.getoption("--redhat-url") if config.getoption("--pybz-debug"): From 7f3cfb1a3962931daded36762f46c8c76642f9d0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 14:44:50 -0500 Subject: [PATCH 237/393] Convert bugzilla.1 man page to rst Generate the man page at build time with rst2man Adjust buildsystem and RPM spec to match Signed-off-by: Cole Robinson --- .gitignore | 1 + MANIFEST.in | 2 +- bugzilla.1 | 311 -------------------------- man/bugzilla.rst | 507 +++++++++++++++++++++++++++++++++++++++++++ python-bugzilla.spec | 2 + setup.py | 22 +- 6 files changed, 532 insertions(+), 313 deletions(-) delete mode 100644 bugzilla.1 create mode 100644 man/bugzilla.rst diff --git a/.gitignore b/.gitignore index a3f16c2f..1f832a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build .coverage .tox .pytest_cache +man/bugzilla.1 diff --git a/MANIFEST.in b/MANIFEST.in index 43b14525..b8f64d7b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ include COPYING CONTRIBUTING.md MANIFEST.in README.md NEWS.md -include bugzilla.1 include xmlrpc-api-notes.txt include python-bugzilla.spec include *requirements.txt +include man/bugzilla.rst recursive-include examples *.py recursive-include tests *.py *.txt *.cfg diff --git a/bugzilla.1 b/bugzilla.1 deleted file mode 100644 index 7cf2e51d..00000000 --- a/bugzilla.1 +++ /dev/null @@ -1,311 +0,0 @@ -.TH bugzilla 1 "Mar 30, 2017" "version 2.1.0" "User Commands" -.SH NAME -bugzilla \- command-line interface to Bugzilla over XML-RPC -.SH SYNOPSIS -.B bugzilla -[\fIoptions\fR] [\fIcommand\fR] [\fIcommand-options\fR] -.SH DESCRIPTION -.PP -.BR bugzilla -is a command-line utility that allows access to the XML-RPC interface provided -by Bugzilla. -.PP -\fIcommand\fP is one of: -.br -.I \fR * login - log into the given bugzilla instance -.br -.I \fR * new - create a new bug -.br -.I \fR * query - search for bugs matching given criteria -.br -.I \fR * modify - modify existing bugs -.br -.I \fR * attach - attach files to existing bugs, or get attachments -.br -.I \fR * info - get info about the given bugzilla instance -.SH GLOBAL OPTIONS -.IP "--version" -show program's version number and exit -.IP "--help, -h" -show this help message and exit -.IP "--bugzilla=BUGZILLA" -bugzilla XMLRPC URI. default: https://bugzilla.redhat.com/xmlrpc.cgi -.IP "--nosslverify" -Don't error on invalid bugzilla SSL certificate -.IP "--cert=CERTFILE" -client side certificate file needed by the webserver. -.IP "--login" -Run interactive "login" before performing the specified command. -.IP "--username=USERNAME" -Log in with this username -.IP "--password=PASSWORD" -Log in with this password -.IP "--restrict-login" -The session (login token) will be restricted to the current IP address. -.IP "--ensure-logged-in" -Raise an error if we aren't logged in to bugzilla. Consider using this if you are depending on cached credentials, to ensure that when they expire the tool errors, rather than subtly change output. -.IP "--no-cache-credentials" -Don't save any bugzilla cookies or tokens to disk, and don't use any pre-existing credentials. -.IP "--cookiefile=COOKIEFILE" -cookie file to use for bugzilla authentication -.IP "--tokenfile=TOKENFILE" -token file to use for bugzilla authentication -.IP "--verbose" -give more info about what's going on -.IP "--debug" -output bunches of debugging info -.IP "--version" -show program's version number and exit - -.SH Standard bugzilla options -.PP -These options are shared by some combination of the 'new', 'query', and 'modify' sub commands. Not every option works for each command though. - -.IP "--product=PRODUCT, -p PRODUCT" -Product name -.IP "--version=VERSION, -v VERSION" -Product version -.IP "--component=COMPONENT, -c COMPONENT" -Component name -.IP "--summary=SUMMARY, -s SUMMARY, --short_desc=SUMMARY" -Bug summary -.IP "--comment=DESCRIPTION, -l DESCRIPTION" -Set initial bug comment/description -.IP "--comment-tag=TAG" -Comment tag for the new comment -.IP "--sub-component=SUB_COMPONENT" -RHBZ sub component name -.IP "--os=OS, -o OS" -Operating system -.IP "--arch=ARCH" -Arch this bug occurs on -.IP "--severity=SEVERITY, -x SEVERITY" -Bug severity -.IP "--priority=PRIORITY, -z PRIORITY" -Bug priority -.IP "--alias=ALIAS" -Bug alias (name) -.IP "--status=STATUS, -s STATUS, --bug_status=STATUS" -Bug status (NEW, ASSIGNED, etc.) -.IP "--url=URL, -u URL" -URL for further bug info -.IP "--target_milestone=TARGET_MILESTONE, -m TARGET_MILESTONE" -Target milestone -.IP "--target_release=TARGET_RELEASE" -RHBZ Target release -.IP "--blocked=BUGID[, BUGID, ...]" -Bug IDs that this bug blocks -.IP "--dependson=BUGID[, BUGID, ...]" -Bug IDs that this bug depends on -.IP "--keywords=KEYWORD[, KEYWORD, ...]" -Bug keywords -.IP "--groups=GROUP[, GROUP, ...]" -Which user groups can view this bug -.IP "--cc=CC[, CC, ...]" -CC list -.IP "--assigned_to=ASSIGNED_TO, -a ASSIGNED_TO, --assignee ASSIGNED_TO" -Bug assignee -.IP "--qa_contact=QA_CONTACT, -q QA_CONTACT" -QA contact -.IP "--flag=FLAG" -Set or unset a flag. For example, to set a flag named devel_ack, do --flag devel_ack+ Unset a flag with the 'X' value, like --flag needinfoX -.IP "--tags=TAG" -Set (personal) tags field -.IP "--whiteboard WHITEBOARD, -w WHITEBOARD, --status_whiteboard WHITEBOARD" -Whiteboard field -.IP "--devel_whiteboard DEVEL_WHITEBOARD" -RHBZ devel whiteboard field -.IP "--internal_whiteboard INTERNAL_WHITEBOARD" -RHBZ internal whiteboard field -.IP "--qa_whiteboard QA_WHITEBOARD" -RHBZ QA whiteboard field -.IP "--fixed_in FIXED_IN, -F FIXED_IN -RHBZ 'Fixed in version' field -.IP "--field=FIELD=VALUE" -Manually specify a bugzilla XMLRPC field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE - - -.SH Output options -.PP -These options are shared by several commands, for tweaking the text output of the command results. -.IP "--full, -f" -output detailed bug info -.IP "--ids, -i" -output only bug IDs -.IP "--extra, -e" -output additional bug information (keywords, Whiteboards, etc.) -.IP "--oneline" -one line summary of the bug (useful for scripts) -.IP "--raw" -raw output of the bugzilla contents -.IP "--json" -output bug contents in JSON format -.IP "--outputformat=OUTPUTFORMAT" -Print output in the form given. You can use RPM-style tags that match bug fields, e.g.: '%{id}: %{summary}'. - -The output of the bugzilla tool should NEVER BE PARSED unless you are using a -custom --outputformat. For everything else, just don't parse it, the formats -are not stable and are subject to change. - ---outputformat allows printing arbitrary bug data in a user preferred format. -For example, to print a returned bug ID, component, and product, separated -with ::, do: - ---outputformat "%{id}::%{component}::%{product}" - -The fields (like 'id', 'component', etc.) are the names of the values returned -by bugzilla's XMLRPC interface. To see a list of all fields, check the API -documentation in the 'SEE ALSO' section. Alternatively, run a 'bugzilla ---debug query ...' and look at the key names returned in the query results. -Also, in most cases, using the name of the associated command line switch -should work, like --bug_status becomes %{bug_status}, etc. - - -.SH \[oq]query\[cq] specific options -Certain options can accept a comma separated list to query multiple values, including --status, --component, --product, --version, --id. - -Note: querying via explicit command line options will only get you so far. See the --from-url option for a way to use powerful Web UI queries from the command line. -.IP "--id ID, -b ID, --bug_id ID" -specify individual bugs by IDs, separated with commas -.IP "--reporter REPORTER, -r REPORTER" -Email: search reporter email for given address -.IP "--quicksearch QUICKSEARCH" -Search using bugzilla's quicksearch functionality. -.IP "--savedsearch SAVEDSEARCH" -Name of a bugzilla saved search. If you don't own this saved search, you must passed --savedsearch_sharer_id. -.IP "--savedsearch-sharer-id SAVEDSEARCH_SHARER_ID" -Owner ID of the --savedsearch. You can get this ID from the URL bugzilla generates when running the saved search from the web UI. -.IP "--from-url WEB_QUERY_URL" -Make a working query via bugzilla's 'Advanced search' web UI, grab the url from your browser (the string with query.cgi or buglist.cgi in it), and --from-url will run it via the bugzilla API. Don't forget to quote the string! This only works for Bugzilla 5 and Red Hat bugzilla - - -.SH \[oq]modify\[cq] specific options -Fields that take multiple values have a special input format. - - Append: --cc=foo@example.com - Overwrite: --cc==foo@example.com - Remove: --cc=-foo@example.com - -Options that accept this format: --cc, --blocked, --dependson, --groups, --tags, whiteboard fields. -.IP "--close RESOLUTION, -k RESOLUTION" -Close with the given resolution (WONTFIX, NOTABUG, etc.) -.IP "--dupeid ORIGINAL, -d ORIGINAL" -ID of original bug. Implies --close DUPLICATE -.IP "--private" -Mark new comment as private -.IP "--reset-assignee" -Reset assignee to component default -.IP "--reset-qa-contact" -Reset QA contact to component default - - -.SH \[oq]new\[cq] specific options -.IP "--private" -Mark new comment as private - - -.SH \[oq]attach\[cq] options -.IP "--file=FILENAME, -f FILENAME" -File to attach, or filename for data provided on stdin -.IP "--description=DESCRIPTION, -d DESCRIPTION" -A short description of the file being attached -.IP "--type=MIMETYPE, -t MIMETYPE" -Mime-type for the file being attached -.IP "--get=ATTACHID, -g ATTACHID" -Download the attachment with the given ID -.IP "--getall=BUGID, --get-all=BUGID" -Download all attachments on the given bug -.IP "--ignore-obsolete" -Do not download attachments marked as obsolete. -.IP "--comment=COMMENT, -l COMMENT" -Add comment with attachment - - -.SH \[oq]info\[cq] options -.IP "--products, -p" -Get a list of products -.IP "--components=PRODUCT, -c PRODUCT" -List the components in the given product -.IP "--component_owners=PRODUCT, -o PRODUCT" -List components (and their owners) -.IP "--versions=PRODUCT, -v PRODUCT" -List the versions for the given product -.IP "--active-components" -Only show active components. Combine with --components* - - -.SH AUTHENTICATION CACHE AND API KEYS - -Some command usage will require an active login to the bugzilla -instance. For example, if the bugzilla instance has some private -bugs, those bugs will be missing from 'query' output if you do -not have an active login. - -If you are connecting to a bugzilla 5.0 or later instance, the -best option is to use bugzilla API keys. From the bugzilla -web UI, log in, navigate to Preferences->API Keys, and generate -a key (it will be a long string of characters and numbers). -Then create a ~/.config/python-bugzilla/bugzillarc like this: - - $ cat ~/.config/python-bugzilla/bugzillarc - [bugzilla.example.com] - api_key=YOUR_API_KEY - -Replace 'bugzilla.example.com' with your bugzilla host name, -and YOUR_API_KEY with the generated API Key from the Web UI. - -Alternatively, you can use 'bugzilla login --api-key', which -will ask for the API key, and save it to bugzillarc for you. - -For older bugzilla instances, you will need to cache a login -cookie or token with the "login" subcommand or the "--login" -argument. - -Additionally, the --no-cache-credentials option will tell the -bugzilla tool to _not_ save or use any authentication cache, -including the bugzillarc file. - -.SH EXAMPLES -.PP -.RS 0 -bugzilla query --bug_id 62037 - -bugzilla query --version 15 --component python-bugzilla - -# All boolean options can be formatted like this -.br -bugzilla query --blocked "123456 | 224466" - -bugzilla login - -bugzilla new -p Fedora -v rawhide -c python-bugzilla \\ - --summary "python-bugzilla causes headaches" \\ - --comment "python-bugzilla made my brain hurt when I used it." - -bugzilla attach --file ~/Pictures/cam1.jpg --desc "me, in pain" $BUGID - -bugzilla attach --getall $BUGID - -bugzilla modify --close NOTABUG --comment "Actually, you're hungover." $BUGID - - -.SH EXIT STATUS -.BR bugzilla -normally returns 0 if the requested command was successful. -Otherwise, exit status is 1 if -.BR bugzilla -is interrupted by the user (or a login attempt fails), 2 if a -socket error occurs (e.g. TCP connection timeout), and 3 if the server returns -an XML-RPC fault. -.SH BUGS -Please report any bugs as github issues at -.br -https://github.com/python-bugzilla/python-bugzilla -.br -to the mailing list at -.br -https://fedorahosted.org/mailman/listinfo/python-bugzilla -.SH SEE ALSO -.nf -https://bugzilla.readthedocs.io/en/latest/api/index.html -https://bugzilla.redhat.com/docs/en/html/api/Bugzilla/WebService/Bug.html diff --git a/man/bugzilla.rst b/man/bugzilla.rst new file mode 100644 index 00000000..177b260a --- /dev/null +++ b/man/bugzilla.rst @@ -0,0 +1,507 @@ +======== +bugzilla +======== + +----------------------------------------------- +command-line interface to Bugzilla over XML-RPC +----------------------------------------------- + +:Manual section: 1 +:Manual group: User Commands + + +SYNOPSIS +======== + +**bugzilla** [*options*] [*command*] [*command-options*] + + +DESCRIPTION +=========== + +**bugzilla** is a command-line utility that allows access to the XML-RPC +interface provided by Bugzilla. + +| +| *command* is one of: +| * login - log into the given bugzilla instance +| * new - create a new bug +| * query - search for bugs matching given criteria +| * modify - modify existing bugs +| * attach - attach files to existing bugs, or get attachments +| * info - get info about the given bugzilla instance + + +GLOBAL OPTIONS +-------------- + +- ``--help, -h`` + +show this help message and exit + +- ``--bugzilla=BUGZILLA`` + +bugzilla XMLRPC URI. default: https://bugzilla.redhat.com/xmlrpc.cgi + +- ``--nosslverify`` + +Don't error on invalid bugzilla SSL certificate + +- ``--cert=CERTFILE`` + +client side certificate file needed by the webserver. + +- ``--login`` + +Run interactive "login" before performing the specified command. + +- ``--username=USERNAME`` + +Log in with this username + +- ``--password=PASSWORD`` + +Log in with this password + +- ``--restrict-login`` + +The session (login token) will be restricted to the current IP +address. + +- ``--ensure-logged-in`` + +Raise an error if we aren't logged in to bugzilla. Consider using +this if you are depending on cached credentials, to ensure that when +they expire the tool errors, rather than subtly change output. + +- ``--no-cache-credentials`` + +Don't save any bugzilla cookies or tokens to disk, and don't use any +pre-existing credentials. + +- ``--cookiefile=COOKIEFILE`` + +cookie file to use for bugzilla authentication + +- ``--tokenfile=TOKENFILE`` + +token file to use for bugzilla authentication + +- ``--verbose`` + +give more info about what's going on + +- ``--debug`` + +output bunches of debugging info + +- ``--version`` + +show program's version number and exit + + +Standard bugzilla options +========================= + +These options are shared by some combination of the 'new', 'query', and +'modify' sub commands. Not every option works for each command though. + +- ``--product=PRODUCT, -p PRODUCT`` + +Product name + +- ``--version=VERSION, -v VERSION`` + +Product version + +- ``--component=COMPONENT, -c COMPONENT`` + +Component name + +- ``--summary=SUMMARY, -s SUMMARY, --short_desc=SUMMARY`` + +Bug summary + +- ``--comment=DESCRIPTION, -l DESCRIPTION`` + +Set initial bug comment/description + +- ``--comment-tag=TAG`` + +Comment tag for the new comment + +- ``--sub-component=SUB_COMPONENT`` + +RHBZ sub component name + +- ``--os=OS, -o OS`` + +Operating system + +- ``--arch=ARCH`` + +Arch this bug occurs on + +- ``--severity=SEVERITY, -x SEVERITY`` + +Bug severity + +- ``--priority=PRIORITY, -z PRIORITY`` + +Bug priority + +- ``--alias=ALIAS`` + +Bug alias (name) + +- ``--status=STATUS, -s STATUS, --bug_status=STATUS`` + +Bug status (NEW, ASSIGNED, etc.) + +- ``--url=URL, -u URL`` + +URL for further bug info + +- ``--target_milestone=TARGET_MILESTONE, -m TARGET_MILESTONE`` + +Target milestone + +- ``--target_release=TARGET_RELEASE`` + +RHBZ Target release + +- ``--blocked=BUGID[, BUGID, ...]`` + +Bug IDs that this bug blocks + +- ``--dependson=BUGID[, BUGID, ...]`` + +Bug IDs that this bug depends on + +- ``--keywords=KEYWORD[, KEYWORD, ...]`` + +Bug keywords + +- ``--groups=GROUP[, GROUP, ...]`` + +Which user groups can view this bug + +- ``--cc=CC[, CC, ...]`` + +CC list + +- ``--assigned_to=ASSIGNED_TO, -a ASSIGNED_TO, --assignee ASSIGNED_TO`` + +Bug assignee + +- ``--qa_contact=QA_CONTACT, -q QA_CONTACT`` + +QA contact + +- ``--flag=FLAG`` + +Set or unset a flag. For example, to set a flag named devel_ack, do +--flag devel_ack+ Unset a flag with the 'X' value, like --flag +needinfoX + +- ``--tags=TAG`` + +Set (personal) tags field + +- ``--whiteboard WHITEBOARD, -w WHITEBOARD, --status_whiteboard WHITEBOARD`` + +Whiteboard field + +- ``--devel_whiteboard DEVEL_WHITEBOARD`` + +RHBZ devel whiteboard field + +- ``--internal_whiteboard INTERNAL_WHITEBOARD`` + +RHBZ internal whiteboard field + +- ``--qa_whiteboard QA_WHITEBOARD`` + +RHBZ QA whiteboard field + +- ``--fixed_in FIXED_IN, -F FIXED_IN`` + +RHBZ 'Fixed in version' field + +- ``--field=FIELD=VALUE`` + +Manually specify a bugzilla XMLRPC field. FIELD is the raw name used +by the bugzilla instance. For example if your bugzilla instance has a +custom field cf_my_field, do: --field cf_my_field=VALUE + + +Output options +============== + +These options are shared by several commands, for tweaking the text +output of the command results. + +- ``--full, -f`` + +output detailed bug info + +- ``--ids, -i`` + +output only bug IDs + +- ``--extra, -e`` + +output additional bug information (keywords, Whiteboards, etc.) + +- ``--oneline`` + +one line summary of the bug (useful for scripts) + +- ``--raw`` + +raw output of the bugzilla contents + +- ``--json`` + +output bug contents in JSON format + +- ``--outputformat=OUTPUTFORMAT`` + +Print output in the form given. You can use RPM-style tags that match +bug fields, e.g.: '%{id}: %{summary}'. + +The output of the bugzilla tool should NEVER BE PARSED unless you are +using a custom --outputformat. For everything else, just don't parse it, +the formats are not stable and are subject to change. + +--outputformat allows printing arbitrary bug data in a user preferred +format. For example, to print a returned bug ID, component, and product, +separated with ::, do: + +--outputformat "%{id}::%{component}::%{product}" + +The fields (like 'id', 'component', etc.) are the names of the values +returned by bugzilla's XMLRPC interface. To see a list of all fields, +check the API documentation in the 'SEE ALSO' section. Alternatively, +run a 'bugzilla --debug query ...' and look at the key names returned in +the query results. Also, in most cases, using the name of the associated +command line switch should work, like --bug_status becomes +%{bug_status}, etc. + + +‘query’ specific options +======================== + +Certain options can accept a comma separated list to query multiple +values, including --status, --component, --product, --version, --id. + +Note: querying via explicit command line options will only get you so +far. See the --from-url option for a way to use powerful Web UI queries +from the command line. + +- ``--id ID, -b ID, --bug_id ID`` + +specify individual bugs by IDs, separated with commas + +- ``--reporter REPORTER, -r REPORTER`` + +Email: search reporter email for given address + +- ``--quicksearch QUICKSEARCH`` + +Search using bugzilla's quicksearch functionality. + +- ``--savedsearch SAVEDSEARCH`` + +Name of a bugzilla saved search. If you don't own this saved search, +you must passed --savedsearch_sharer_id. + +- ``--savedsearch-sharer-id SAVEDSEARCH_SHARER_ID`` + +Owner ID of the --savedsearch. You can get this ID from the URL +bugzilla generates when running the saved search from the web UI. + +- ``--from-url WEB_QUERY_URL`` + +Make a working query via bugzilla's 'Advanced search' web UI, grab +the url from your browser (the string with query.cgi or buglist.cgi +in it), and --from-url will run it via the bugzilla API. Don't forget +to quote the string! This only works for Bugzilla 5 and Red Hat +bugzilla + + +‘modify’ specific options +========================= + +Fields that take multiple values have a special input format. + +| Append: --cc=foo@example.com +| Overwrite: --cc==foo@example.com +| Remove: --cc=-foo@example.com + +Options that accept this format: --cc, --blocked, --dependson, --groups, +--tags, whiteboard fields. + +- ``--close RESOLUTION, -k RESOLUTION`` + +Close with the given resolution (WONTFIX, NOTABUG, etc.) + +- ``--dupeid ORIGINAL, -d ORIGINAL`` + +ID of original bug. Implies --close DUPLICATE + +- ``--private`` + +Mark new comment as private + +- ``--reset-assignee`` + +Reset assignee to component default + +- ``--reset-qa-contact`` + +Reset QA contact to component default + + +‘new’ specific options +====================== + +- ``--private`` + +Mark new comment as private + + +‘attach’ options +================ + +- ``--file=FILENAME, -f FILENAME`` + +File to attach, or filename for data provided on stdin + +- ``--description=DESCRIPTION, -d DESCRIPTION`` + +A short description of the file being attached + +- ``--type=MIMETYPE, -t MIMETYPE`` + +Mime-type for the file being attached + +- ``--get=ATTACHID, -g ATTACHID`` + +Download the attachment with the given ID + +- ``--getall=BUGID, --get-all=BUGID`` + +Download all attachments on the given bug + +- ``--ignore-obsolete`` + +Do not download attachments marked as obsolete. + +- ``--comment=COMMENT, -l COMMENT`` + +Add comment with attachment + + +‘info’ options +============== + +- ``--products, -p`` + +Get a list of products + +- ``--components=PRODUCT, -c PRODUCT`` + +List the components in the given product + +- ``--component_owners=PRODUCT, -o PRODUCT`` + +List components (and their owners) + +- ``--versions=PRODUCT, -v PRODUCT`` + +List the versions for the given product + +- ``--active-components`` + +Only show active components. Combine with --components* + + +AUTHENTICATION CACHE AND API KEYS +================================= + +Some command usage will require an active login to the bugzilla +instance. For example, if the bugzilla instance has some private bugs, +those bugs will be missing from 'query' output if you do not have an +active login. + +If you are connecting to a bugzilla 5.0 or later instance, the best +option is to use bugzilla API keys. From the bugzilla web UI, log in, +navigate to Preferences->API Keys, and generate a key (it will be a long +string of characters and numbers). Then create a +~/.config/python-bugzilla/bugzillarc like this: + +:: + + $ cat ~/.config/python-bugzilla/bugzillarc + + [bugzilla.example.com] + api_key=YOUR_API_KEY + +Replace 'bugzilla.example.com' with your bugzilla host name, and +YOUR_API_KEY with the generated API Key from the Web UI. + +Alternatively, you can use 'bugzilla login --api-key', which will ask +for the API key, and save it to bugzillarc for you. + +For older bugzilla instances, you will need to cache a login cookie or +token with the "login" subcommand or the "--login" argument. + +Additionally, the --no-cache-credentials option will tell the bugzilla +tool to *not* save or use any authentication cache, including the +bugzillarc file. + + +EXAMPLES +======== + +| bugzilla query --bug_id 62037 +| +| bugzilla query --version 15 --component python-bugzilla +| +| bugzilla login +| +| bugzilla new -p Fedora -v rawhide -c python-bugzilla \\ +| --summary "python-bugzilla causes headaches" \\ +| --comment "python-bugzilla made my brain hurt when I used it." +| +| bugzilla attach --file ~/Pictures/cam1.jpg --desc "me, in pain" +| $BUGID +| +| bugzilla attach --getall $BUGID +| +| bugzilla modify --close NOTABUG --comment "Actually, you're +| hungover." $BUGID + + +EXIT STATUS +=========== + +**bugzilla** normally returns 0 if the requested command was successful. +Otherwise, exit status is 1 if **bugzilla** is interrupted by the user +(or a login attempt fails), 2 if a socket error occurs (e.g. TCP +connection timeout), and 3 if the server returns an XML-RPC fault. + + +BUGS +==== + +Please report any bugs as github issues at +https://github.com/python-bugzilla/python-bugzilla + + +SEE ALSO +======== + +https://bugzilla.readthedocs.io/en/latest/api/index.html +https://bugzilla.redhat.com/docs/en/html/api/Bugzilla/WebService/Bug.html diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 5f0eaa5e..3a7739b8 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -26,6 +26,7 @@ BuildArch: noarch %if %{with python2} BuildRequires: python2-devel +BuildRequires: python2-docutils BuildRequires: python2-requests BuildRequires: python2-setuptools BuildRequires: python2-pytest @@ -33,6 +34,7 @@ BuildRequires: python2-pytest %if %{with python3} BuildRequires: python3-devel +BuildRequires: python3-docutils BuildRequires: python3-requests BuildRequires: python3-setuptools BuildRequires: python3-pytest diff --git a/setup.py b/setup.py index e62c9dd6..4c812f69 100755 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ import os import sys +import distutils.command.build from distutils.core import Command from setuptools import setup @@ -97,6 +98,24 @@ def run(self): os.system("mv /tmp/python-bugzilla.spec .") +class BuildCommand(distutils.command.build.build): + def _make_man_pages(self): + for path in glob.glob("man/*.rst"): + base = os.path.basename(path) + appname = os.path.splitext(base)[0] + newpath = os.path.join(os.path.dirname(path), + appname + ".1") + + print("Generating %s" % newpath) + ret = os.system('rst2man %s > %s' % (path, newpath)) + if ret != 0: + raise RuntimeError("Generating '%s' failed." % newpath) + + def run(self): + self._make_man_pages() + distutils.command.build.build.run(self) + + def _parse_requirements(fname): ret = [] for line in open(fname).readlines(): @@ -130,12 +149,13 @@ def _parse_requirements(fname): ], packages=['bugzilla'], entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, - data_files=[('share/man/man1', ['bugzilla.1'])], + data_files=[('share/man/man1', ['man/bugzilla.1'])], install_requires=_parse_requirements("requirements.txt"), tests_require=_parse_requirements("test-requirements.txt"), cmdclass={ + "build": BuildCommand, "pylint": PylintCommand, "rpm": RPMCommand, "test": TestCommand, From e9e03fd074928158f891a81e1b4d6df9b5c3060e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 15:13:45 -0500 Subject: [PATCH 238/393] man: Adjust references to XMLRPC Extend references to also mention REST, or drop any mention of the API type entirely Signed-off-by: Cole Robinson --- man/bugzilla.rst | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index 177b260a..d43d2ba7 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -19,8 +19,8 @@ SYNOPSIS DESCRIPTION =========== -**bugzilla** is a command-line utility that allows access to the XML-RPC -interface provided by Bugzilla. +**bugzilla** is a command-line utility for interacting with a Bugzilla +instance over REST or XMLRPC. | | *command* is one of: @@ -41,7 +41,18 @@ show this help message and exit - ``--bugzilla=BUGZILLA`` -bugzilla XMLRPC URI. default: https://bugzilla.redhat.com/xmlrpc.cgi +The bugzilla URL. Full API URLs are typically like: + +| +| * https://bugzilla.example.com/xmlrpc.cgi # XMLRPC API +| * https://bugzilla.example.com/rest/ # REST API +| + +If a non-specific URL is passed, like 'bugzilla.redhat.com', **bugzilla** +will try to probe whether the expected XMLRPC or REST path is available, +preferring XMLRPC for backwards compatibility. + +The default URL https://bugzilla.redhat.com - ``--nosslverify`` @@ -230,7 +241,7 @@ RHBZ 'Fixed in version' field - ``--field=FIELD=VALUE`` -Manually specify a bugzilla XMLRPC field. FIELD is the raw name used +Manually specify a bugzilla API field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE @@ -281,7 +292,7 @@ separated with ::, do: --outputformat "%{id}::%{component}::%{product}" The fields (like 'id', 'component', etc.) are the names of the values -returned by bugzilla's XMLRPC interface. To see a list of all fields, +returned by bugzilla's API. To see a list of all fields, check the API documentation in the 'SEE ALSO' section. Alternatively, run a 'bugzilla --debug query ...' and look at the key names returned in the query results. Also, in most cases, using the name of the associated From e2e264c02a54811000566b4f6574f9285310f5e9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 15:16:02 -0500 Subject: [PATCH 239/393] cli: Mention that --raw is unstable and suggest --json Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 5 +++-- man/bugzilla.rst | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 30088d9e..47daeb89 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -161,10 +161,11 @@ def _parser_add_output_options(p): outg.add_argument('--oneline', action='store_const', dest='output', const='oneline', help="one line summary of the bug (useful for scripts)") - outg.add_argument('--raw', action='store_const', dest='output', - const='raw', help="raw output of the bugzilla contents") outg.add_argument('--json', action='store_const', dest='output', const='json', help="output contents in json format") + outg.add_argument('--raw', action='store_const', dest='output', + const='raw', help="raw output of the bugzilla contents. This " + "format is unstable and difficult to parse. Use --json instead.") outg.add_argument('--outputformat', help="Print output in the form given. " "You can use RPM-style tags that match bug " diff --git a/man/bugzilla.rst b/man/bugzilla.rst index d43d2ba7..cc05cd96 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -268,14 +268,16 @@ output additional bug information (keywords, Whiteboards, etc.) one line summary of the bug (useful for scripts) -- ``--raw`` - -raw output of the bugzilla contents - - ``--json`` output bug contents in JSON format +- ``--raw`` + +raw output of the bugzilla contents. This format is unstable and +difficult to parse. Please use the ``--json`` instead if you want +maximum output from the `bugzilla` + - ``--outputformat=OUTPUTFORMAT`` Print output in the form given. You can use RPM-style tags that match From 527fe33526b6ba2801599fed9c9caaec405daf8c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 15:20:23 -0500 Subject: [PATCH 240/393] man: Minor tweaks Signed-off-by: Cole Robinson --- man/bugzilla.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index cc05cd96..bd58c7cd 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -3,7 +3,7 @@ bugzilla ======== ----------------------------------------------- -command-line interface to Bugzilla over XML-RPC +command line tool for interacting with Bugzilla ----------------------------------------------- :Manual section: 1 @@ -19,7 +19,7 @@ SYNOPSIS DESCRIPTION =========== -**bugzilla** is a command-line utility for interacting with a Bugzilla +**bugzilla** is a command line tool for interacting with a Bugzilla instance over REST or XMLRPC. | @@ -503,7 +503,7 @@ EXIT STATUS **bugzilla** normally returns 0 if the requested command was successful. Otherwise, exit status is 1 if **bugzilla** is interrupted by the user (or a login attempt fails), 2 if a socket error occurs (e.g. TCP -connection timeout), and 3 if the server returns an XML-RPC fault. +connection timeout), and 3 if the Bugzilla server throws an error. BUGS From 3b8dfb8cbeb55708f38525c51deb3a75b792c8d8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 13 Jan 2020 19:46:59 -0500 Subject: [PATCH 241/393] Add docutils to requirements.txt, for man page building Signed-off-by: Cole Robinson --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f2293605..0ec150d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +docutils requests From 16058dee4d48ab390e0651d1fc8bc6df3ace75e8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 14 Jan 2020 09:41:46 -0500 Subject: [PATCH 242/393] tests: Drop './setup.py test' stub command We've used pytest-3 for a while. Make default pytest-3 invocation a bit safer Signed-off-by: Cole Robinson --- setup.py | 14 -------------- tox.ini | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 4c812f69..f981d883 100755 --- a/setup.py +++ b/setup.py @@ -27,19 +27,6 @@ def get_version(): return eval(line.split('=')[-1]) # pylint: disable=eval-used -class TestCommand(Command): - user_options = [] - - def initialize_options(self): - pass - def finalize_options(self): - pass - - def run(self): - print("\n* Tests are now run with the 'pytest' tool.\n" - "* See CONTRIBUTING.md for details.") - - class PylintCommand(Command): user_options = [] @@ -158,6 +145,5 @@ def _parse_requirements(fname): "build": BuildCommand, "pylint": PylintCommand, "rpm": RPMCommand, - "test": TestCommand, }, ) diff --git a/tox.ini b/tox.ini index 7fa3c94c..00a1a0aa 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ commands = [pytest] -addopts = -q --tb=native +addopts = -q --tb=native tests/ [coverage:run] From 80c451e2f846c976575f35115295a5b8872f36ba Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 14 Jan 2020 09:23:27 -0500 Subject: [PATCH 243/393] setup: Make man page generation optional rst2man doesn't appear to be in /usr/bin via pip packages, so make manpage building optional Signed-off-by: Cole Robinson --- python-bugzilla.spec | 17 ++--------------- requirements.txt | 1 - setup.py | 7 +++++-- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 3a7739b8..d21f206f 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -99,23 +99,10 @@ cp -a . %{py3dir} -%build -%if %{with python3} -pushd %{py3dir} -%{__python3} setup.py build -popd -%endif - -%if %{with python2} -%{__python2} setup.py build -%endif - - - %install %if %{with python3} pushd %{py3dir} -%{__python3} setup.py install -O1 --skip-build --root %{buildroot} +%{__python3} setup.py install -O1 --root %{buildroot} %if %{with python2} rm %{buildroot}/usr/bin/bugzilla @@ -125,7 +112,7 @@ popd %endif %if %{with python2} -%{__python2} setup.py install -O1 --skip-build --root %{buildroot} +%{__python2} setup.py install -O1 --root %{buildroot} %endif # Replace '#!/usr/bin/env python' with '#!/usr/bin/python2' diff --git a/requirements.txt b/requirements.txt index 0ec150d5..f2293605 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -docutils requests diff --git a/setup.py b/setup.py index f981d883..f5870405 100755 --- a/setup.py +++ b/setup.py @@ -96,7 +96,10 @@ def _make_man_pages(self): print("Generating %s" % newpath) ret = os.system('rst2man %s > %s' % (path, newpath)) if ret != 0: - raise RuntimeError("Generating '%s' failed." % newpath) + print("Generating '%s' failed." % newpath) + continue + self.distribution.data_files.append( + ('share/man/man1', (newpath,))) def run(self): self._make_man_pages() @@ -135,8 +138,8 @@ def _parse_requirements(fname): 'Programming Language :: Python :: 3.7', ], packages=['bugzilla'], + data_files=[], entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, - data_files=[('share/man/man1', ['man/bugzilla.1'])], install_requires=_parse_requirements("requirements.txt"), tests_require=_parse_requirements("test-requirements.txt"), From 323dc55207b9853603247c6f6d7dab276c7136e6 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 14 Jan 2020 10:14:37 -0500 Subject: [PATCH 244/393] setup: Fix license classifier We are GPLv2+ Signed-off-by: Cole Robinson --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f5870405..0ad8ed8b 100755 --- a/setup.py +++ b/setup.py @@ -126,7 +126,8 @@ def _parse_requirements(fname): classifiers=[ 'Topic :: Software Development :: Libraries :: Python Modules', 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', + 'License :: OSI Approved :: ' + 'GNU General Public License v2 or later (GPLv2+)', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', From ff04c6d15fa2a27c401c291a25726ce974409b18 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 18 Jan 2020 14:47:19 -0500 Subject: [PATCH 245/393] base: Fix manually passed user/password Fix: #115 Signed-off-by: Cole Robinson --- bugzilla/base.py | 4 ++-- tests/data/mockargs/test_api_login2.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 17dc529a..0abe2554 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -598,11 +598,11 @@ def login(self, user=None, password=None, restrict_login=None): if not self.password: raise ValueError("missing password") - payload = {"login": user} + payload = {"login": self.user} if restrict_login: payload['restrict_login'] = True log.debug("logging in with options %s", str(payload)) - payload['password'] = password + payload['password'] = self.password try: ret = self._backend.user_login(payload) diff --git a/tests/data/mockargs/test_api_login2.txt b/tests/data/mockargs/test_api_login2.txt index 36ee16ac..a1334c12 100644 --- a/tests/data/mockargs/test_api_login2.txt +++ b/tests/data/mockargs/test_api_login2.txt @@ -1 +1 @@ -{'login': None, 'password': None} +{'login': 'FOO', 'password': 'BAR'} From 671e5b7ccd57d5fbea16abe4dc0f3f35050c90f2 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Fri, 17 Jan 2020 17:42:24 +0100 Subject: [PATCH 246/393] Little doc string adjustments in the API for bugzilla groups Signed-off-by: Pierre-Yves Chibon --- bugzilla/base.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 0abe2554..873c4a96 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1906,9 +1906,9 @@ def updateperms(self, user, action, groups): def _getgroups(self, names, membership=False): """ - Return a list of users that match criteria. + Return a list of groups that match criteria. - :kwarg ids: list of user ids to return data on + :kwarg ids: list of group ids to return data on :kwarg membership: boolean specifying wether to query the members of the group or not. :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the @@ -1917,6 +1917,8 @@ def _getgroups(self, names, membership=False): requested. Code 505: user is logged out and can't use the match or ids parameter. + Code 805: logged in user do not have enough priviledges to view + groups. """ params = {"membership": membership} params['names'] = listify(names) @@ -1926,8 +1928,10 @@ def getgroup(self, name, membership=False): """ Return a bugzilla Group for the given name - :arg name: The name used in bugzilla. + :arg name: The group name used in bugzilla. :raises XMLRPC Fault: Code 51 if the name does not exist + :raises XMLRPC Fault: Code 805 if the user does not have enough + permissions to view groups :returns: Group record for the name """ ret = self.getgroups(name, membership=membership) From f8ccfcda49aa5ecedf5d69542f1848570e9fff82 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 25 Feb 2020 16:10:37 -0500 Subject: [PATCH 247/393] Rename xmlrpc-api-notes -> bugzilla-api.txt Signed-off-by: Cole Robinson --- xmlrpc-api-notes.txt => bugzilla-api.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename xmlrpc-api-notes.txt => bugzilla-api.txt (100%) diff --git a/xmlrpc-api-notes.txt b/bugzilla-api.txt similarity index 100% rename from xmlrpc-api-notes.txt rename to bugzilla-api.txt From 8b342696d4b228b937501eeb9de0247aab31ba2a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 25 Feb 2020 16:14:28 -0500 Subject: [PATCH 248/393] bugzilla-api.txt: Updates Signed-off-by: Cole Robinson --- bugzilla-api.txt | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/bugzilla-api.txt b/bugzilla-api.txt index ac38684f..110ceffb 100644 --- a/bugzilla-api.txt +++ b/bugzilla-api.txt @@ -1,30 +1,9 @@ - -Fedora infrastructure depends on python-bugzilla in various ways: -http://lists.fedorahosted.org/pipermail/python-bugzilla/2012-June/000001.html - -Red Hat bugzilla originally had a totally custom API. Much of that is -being dropped in 2013, API conversions outlined here: -https://bugzilla.redhat.com/show_bug.cgi?id=822007 - -Externally facing RH bugzilla instance that doesn't send email and is -refreshed periodically. This is what is used in the functional test suite: -http://partner-bugzilla.redhat.com - -Some trackers in the wild to use for API testing: - bugzilla.redhat.com - bugzilla.mozilla.org - bugzilla.kernel.org - bugzilla.gnome.org - bugzilla.novell.com - bugzilla.zimbra.com - bugzilla.samba.org - bugs.gentoo.org - +This document tracks upstream bugzilla API changes, and related info. Upstream timeline ================= -Here's a timeline of the evolution of the upstream bugzilla XMLRPC API: +Here's a timeline of the evolution of the upstream bugzilla XMLRPC/REST API: Bugzilla 2.*: No XMLRPC API that I can tell @@ -109,10 +88,18 @@ Bugzilla 5.0: (July 2015) Component.create User.valid_login + Bugzilla latest/tip: https://bugzilla.readthedocs.io/en/latest/api/index.html + Misc info + ========= + +Bugzilla REST API code link + https://github.com/bugzilla/bugzilla/tree/5.2/Bugzilla/WebService/Server/REST/Resources + + Redhat Bugzilla: 5.0 based with extensions https://bugzilla.redhat.com/docs/en/html/api/ Bug.search has --from-url extension From d6e7f61fc5e2fd8ccba4edae4339edb96727beb0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 26 Jun 2020 17:55:35 -0400 Subject: [PATCH 249/393] Add examples/getbug_restapi.py Just a little example for using force_rest=True Signed-off-by: Cole Robinson --- examples/getbug_restapi.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 examples/getbug_restapi.py diff --git a/examples/getbug_restapi.py b/examples/getbug_restapi.py new file mode 100644 index 00000000..cc09721b --- /dev/null +++ b/examples/getbug_restapi.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +# getbug_restapi.py: +# Simple demonstration of connecting to bugzilla over the REST +# API and printing some bug details. + +from __future__ import print_function + +import bugzilla + +# public test instance of bugzilla.redhat.com. It's okay to make changes +URL = "partner-bugzilla.redhat.com" + +# By default, if plain Bugzilla(URL) is invoked, the Bugzilla class will +# attempt to determine if XMLRPC or REST API is available, with a preference +# for XMLRPC for back compatability. But you can for use for the REST API +# with force_rest=True +bzapi = bugzilla.Bugzilla(URL, force_rest=True) + +# After that, the bugzilla API can be used as normal. See getbug.py for +# some more info here. +bug = bzapi.getbug(427301) +print("Fetched bug #%s:" % bug.id) +print(" Product = %s" % bug.product) +print(" Component = %s" % bug.component) +print(" Status = %s" % bug.status) +print(" Resolution= %s" % bug.resolution) +print(" Summary = %s" % bug.summary) From 9663e9fcc4048868fb11c26d17b68ce71382e5c0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 26 Jun 2020 19:50:19 -0400 Subject: [PATCH 250/393] Fix tests on RHEL7 Signed-off-by: Cole Robinson --- tests/conftest.py | 2 +- tests/test_api_authfiles.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index cdba933e..7481f0fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -108,4 +108,4 @@ def run_cli(capsys, monkeypatch): """ def _do_run(*args, **kwargs): return tests.utils.do_run_cli(capsys, monkeypatch, *args, **kwargs) - yield _do_run + return _do_run diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py index 908705ad..222a9ea2 100644 --- a/tests/test_api_authfiles.py +++ b/tests/test_api_authfiles.py @@ -165,7 +165,15 @@ def test_authfiles_saving(monkeypatch): output_token = dirname + "output-token.txt" output_cookies = dirname + "output-cookies.txt" tests.utils.diff_compare(open(bzapi.tokenfile).read(), output_token) - tests.utils.diff_compare(open(bzapi.cookiefile).read(), output_cookies) + + # On RHEL7 the cookie comment header is different. Strip off leading + # comments + def strip_comments(f): + return "".join([l for l in open(f).readlines() if + not l.startswith("#")]) + + tests.utils.diff_compare(strip_comments(bzapi.cookiefile), + None, expect_out=strip_comments(output_cookies)) # Make sure file can re-read them and not error bzapi = tests.mockbackend.make_bz( From 983a64bd710e0b4aa72d0f08e70fa5b7ca56c72f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 26 Jun 2020 19:57:21 -0400 Subject: [PATCH 251/393] Fix some pylint Signed-off-by: Cole Robinson --- bugzilla/base.py | 4 ++-- pylintrc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 873c4a96..07d5503e 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -109,8 +109,8 @@ def url_to_query(url): q = {} # pylint: disable=unpacking-non-sequence - (ignore, ignore, path, - ignore, query, ignore) = urlparse(url) + (ignore1, ignore2, path, + ignore, query, ignore3) = urlparse(url) base = os.path.basename(path) if base not in ('buglist.cgi', 'query.cgi'): diff --git a/pylintrc b/pylintrc index 28a9a6fe..23122933 100644 --- a/pylintrc +++ b/pylintrc @@ -7,7 +7,7 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel enable=fixme From 4f77f96c96401a55147409d0267361f686760838 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 26 Jun 2020 20:23:01 -0400 Subject: [PATCH 252/393] Update some project metadata * Drop references to the fedorahosted mailing list, it will be closed * Doc tweaks * Drop XMLRPC reference from setup.py, make it more generic Signed-off-by: Cole Robinson --- CONTRIBUTING.md | 8 -------- README.md | 6 ++---- setup.py | 5 ++--- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe304abb..8f8f24f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,11 +63,3 @@ If you are submitting a patch, ensure the following: Running any of the functional tests is not a requirement for patch submission, but please give them a go if you are interested. - -Patches can be submitted via github pull-request, or via the mailing list -at python-bugzilla@lists.fedorahosted.org using 'git send-email'. - - -# Bug reports - -Bug reports should be submitted as github issues, or sent to the mailing list diff --git a/README.md b/README.md index 4742be92..3d947da8 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ This was originally written specifically for Red Hat's Bugzilla instance and is used heavily at Red Hat and in Fedora, but it should still be generically useful. -You can find some code examples in the [examples](examples) directory +You can find some code examples in the [examples](examples) directory. -For questions about submitting bug reports or patches, see [CONTRIBUTING.md](CONTRIBUTING.md) - -Questions, comments, and discussions should go to our mailing: http://lists.fedorahosted.org/mailman/listinfo/python-bugzilla +For questions about submitting patches, see [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/setup.py b/setup.py index 0ad8ed8b..20eafff7 100755 --- a/setup.py +++ b/setup.py @@ -118,9 +118,7 @@ def _parse_requirements(fname): setup( name='python-bugzilla', version=get_version(), - description='Bugzilla XMLRPC access module', - author='Cole Robinson', - author_email='python-bugzilla@lists.fedorahosted.org', + description='Library and command line tool for interacting with Bugzilla', license="GPLv2", url='https://github.com/python-bugzilla/python-bugzilla', classifiers=[ @@ -137,6 +135,7 @@ def _parse_requirements(fname): 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], packages=['bugzilla'], data_files=[], From 37659196800562ea9264f904b7f2f64eeb65d85f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 29 Jun 2020 14:05:25 -0400 Subject: [PATCH 253/393] Prep for release 2.4.0 --- NEWS.md | 12 ++++++++++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 510e7a8d..b9731ced 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,17 @@ # python-bugzilla release news +## Release 2.4.0 (June 29, 2020) +- Bugzilla REST API support +- Add --json command line output option +- Add APIs for Bugzilla Groups (Pierre-Yves Chibon) +- Add `Bugzilla.get_requests_session()` API to access raw requests Session +- Add `Bugzilla.get_xmlrpc_proxy()` API to access raw ServerProxy +- Add `Bugzilla requests_session=` init parameter to pass in auth, etc. +- Add `bugzilla attach --ignore-obsolete` (Čestmír Kalina) +- Add `bugzilla login --api-key` for API key prompting (Danilo C. L. de + Paula) +- Add `bugzilla new --private` + ## Release 2.3.0 (August 26, 2019) - restrict-login suppot (Viliam Krizan) - cli: Add support for private attachments (Brian 'Redbeard' Harrington) diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 7d6a67b2..afb66674 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "2.3.0" +version = "2.4.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index d21f206f..520604b9 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -15,7 +15,7 @@ %endif Name: python-bugzilla -Version: 2.3.0 +Version: 2.4.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From 829a6ad1f3e7498a7bc4a5e16fa7a81b9a70c146 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 15:55:30 -0400 Subject: [PATCH 254/393] Fix flake8 errors with py2 imports Signed-off-by: Cole Robinson --- bugzilla/_util.py | 4 +++- examples/apikey.py | 7 +++---- tests/utils.py | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/bugzilla/_util.py b/bugzilla/_util.py index 82a8417d..8c137c16 100644 --- a/bugzilla/_util.py +++ b/bugzilla/_util.py @@ -20,11 +20,13 @@ def to_encoding(ustring): """ Locale specific printing per python version """ + # pylint: disable=undefined-variable + ustring = ustring or '' if IS_PY3: return str(ustring) else: # pragma: no cover - strtype = basestring # pylint: disable=undefined-variable + strtype = basestring # noqa string = ustring if not isinstance(ustring, strtype): string = str(ustring) diff --git a/examples/apikey.py b/examples/apikey.py index ee8a43c9..8e7d5ac6 100644 --- a/examples/apikey.py +++ b/examples/apikey.py @@ -4,6 +4,7 @@ # See the COPYING file in the top-level directory. # create.py: Create a new bug report +# pylint: disable=undefined-variable from __future__ import print_function import sys @@ -18,12 +19,10 @@ " https://landfill.bugzilla.org/bugzilla-5.0-branch/userprefs.cgi") print("This is a test site, so no harm will come!\n") -# pylint: disable=undefined-variable if sys.version_info[0] >= 3: - api_key = input("Enter Bugzilla API Key: ") + api_key = input("Enter Bugzilla API Key: ") # noqa else: - api_key = raw_input("Enter Bugzilla API Key: ") -# pylint: enable=undefined-variable + api_key = raw_input("Enter Bugzilla API Key: ") # noqa # API key usage assumes the API caller is storing the API key; if you would # like to use one of the login options that stores credentials on-disk for diff --git a/tests/utils.py b/tests/utils.py index 7f0850a5..339d7169 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -38,11 +38,12 @@ def fake_stream(text): def monkeypatch_getpass(monkeypatch): + # pylint: disable=undefined-variable if IS_PY3: - monkeypatch.setattr(getpass, "getpass", input) + use_input = input # noqa else: - monkeypatch.setattr(getpass, "getpass", - raw_input) # pylint: disable=undefined-variable + use_input = raw_input # noqa + monkeypatch.setattr(getpass, "getpass", use_input) def sanitize_json(rawout): From c4f523a867596b43dcab6e41176e70f00457b24d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 15:56:08 -0400 Subject: [PATCH 255/393] examples/apikey: Fix header comment Signed-off-by: Cole Robinson --- examples/apikey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/apikey.py b/examples/apikey.py index 8e7d5ac6..4522129c 100644 --- a/examples/apikey.py +++ b/examples/apikey.py @@ -3,7 +3,7 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -# create.py: Create a new bug report +# apikey.py: Demostrate prompting for API key and passing it to Bugzilla # pylint: disable=undefined-variable from __future__ import print_function From 55781449d0f85ac99ee6b0bb90d073cbbf3b16c3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 15:38:52 -0400 Subject: [PATCH 256/393] Add github actions build+test CI Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..1b5f34e4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +# Derived from stock python-package.yml +name: Build + test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest From 14af87b7a74bc659b1bac4a10f217c675ab4b545 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 16:07:45 -0400 Subject: [PATCH 257/393] Add codecov.io integration Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 17 ++++++++++++++--- codecov.yml | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 codecov.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b5f34e4..fbfc5ed0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,21 +15,32 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest + pip install flake8 pytest pytest-cov if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest + + - name: Test with pytest and generate coverage report run: | - pytest + pytest --cov --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..fbd9242a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,4 @@ +# The files aren't interesting for the unit tests run in CI +ignore: + - "bugzilla/_backendrest.py" + - "bugzilla/_backendxmlrpc.py" From bc4c2ae21e8b1d314c24c1bb7d25bb8be5e26362 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 16:57:53 -0400 Subject: [PATCH 258/393] Remove travis.yml We are using github actions now Signed-off-by: Cole Robinson --- .travis.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 26c8166c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: python - -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 - -install: - - pip install tox - - pip install tox-travis - -script: - # --ro-functional can be flakey, so drop it - - tox - -notifications: - email: true - on_success: never - on_failure: always From 68a1dc628ac21a24018710ac3fdba2709aa80441 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 17:20:32 -0400 Subject: [PATCH 259/393] setup: Check for rst2man.py as well This is what docutils from pip installs. Compared to fedora packages which rename it rst2man. This should fix the issue that prompted 80c451e2f846, where we made man page building non-fatal, so undo that to ensure CI is hitting this build path. Signed-off-by: Cole Robinson --- requirements.txt | 1 + setup.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index f2293605..f6bdb7bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests +docutils diff --git a/setup.py b/setup.py index 20eafff7..a5c01da7 100755 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ import glob import os +import subprocess import sys import distutils.command.build @@ -87,6 +88,13 @@ def run(self): class BuildCommand(distutils.command.build.build): def _make_man_pages(self): + from distutils.spawn import find_executable + rstbin = find_executable("rst2man") + if not rstbin: + rstbin = find_executable("rst2man.py") + if not rstbin: + sys.exit("Didn't find rst2man or rst2man.py") + for path in glob.glob("man/*.rst"): base = os.path.basename(path) appname = os.path.splitext(base)[0] @@ -94,10 +102,9 @@ def _make_man_pages(self): appname + ".1") print("Generating %s" % newpath) - ret = os.system('rst2man %s > %s' % (path, newpath)) - if ret != 0: - print("Generating '%s' failed." % newpath) - continue + out = subprocess.check_output([rstbin, path]) + open(newpath, "wb").write(out) + self.distribution.data_files.append( ('share/man/man1', (newpath,))) From 0b9e12b7c3cb6f3e489df67556ce67f156ebeacf Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 18:04:08 -0400 Subject: [PATCH 260/393] Revive bugzilla.rhbugzilla.RHBugzilla import path I've had a few reports about apps breaking without it. We can't easily move RHBugzilla to the existing rhbugzilla.py due to cyclic dependency issues. Rename rhbugzilla.py to _rhconverters.py, and make rhbugzilla.py just import RHBugzilla from oldclasses.py. Add tests to ensure we don't regress in the future Signed-off-by: Cole Robinson --- bugzilla/_rhconverters.py | 128 ++++++++++++++++++++++++++++++++++++++ bugzilla/base.py | 2 +- bugzilla/rhbugzilla.py | 127 +------------------------------------ tests/test_api_misc.py | 8 +++ 4 files changed, 140 insertions(+), 125 deletions(-) create mode 100644 bugzilla/_rhconverters.py diff --git a/bugzilla/_rhconverters.py b/bugzilla/_rhconverters.py new file mode 100644 index 00000000..fb371cda --- /dev/null +++ b/bugzilla/_rhconverters.py @@ -0,0 +1,128 @@ +# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib. +# +# Copyright (C) 2008-2012 Red Hat Inc. +# Author: Will Woods +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +from logging import getLogger + +from ._util import listify + +log = getLogger(__name__) + + +class _RHBugzillaConverters(object): + """ + Static class that holds functional Red Hat back compat converters. + Called inline in Bugzilla + """ + @staticmethod + def convert_build_update( + component=None, + fixed_in=None, + qa_whiteboard=None, + devel_whiteboard=None, + internal_whiteboard=None, + sub_component=None): + adddict = {} + + def get_alias(): + # RHBZ has a custom extension to allow a bug to have multiple + # aliases, so the format of aliases is + # {"add": [...], "remove": [...]} + # But that means in order to approximate upstream, behavior + # which just overwrites the existing alias, we need to read + # the bug's state first to know what string to remove. Which + # we can't do, since we don't know the bug numbers at this point. + # So fail for now. + # + # The API should provide {"set": [...]} + # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 + # + # Implementation will go here when it's available + pass + + if fixed_in is not None: + adddict["cf_fixed_in"] = fixed_in + if qa_whiteboard is not None: + adddict["cf_qa_whiteboard"] = qa_whiteboard + if devel_whiteboard is not None: + adddict["cf_devel_whiteboard"] = devel_whiteboard + if internal_whiteboard is not None: + adddict["cf_internal_whiteboard"] = internal_whiteboard + + if sub_component: + if not isinstance(sub_component, dict): + component = listify(component) + if not component: + raise ValueError("component must be specified if " + "specifying sub_component") + sub_component = {component[0]: sub_component} + adddict["sub_components"] = sub_component + + get_alias() + + return adddict + + + ################# + # Query methods # + ################# + + @staticmethod + def pre_translation(query): + """ + Translates the query for possible aliases + """ + old = query.copy() + + def split_comma(_v): + if isinstance(_v, list): + return _v + return _v.split(",") + + if 'bug_id' in query: + query['id'] = split_comma(query.pop('bug_id')) + + if 'component' in query: + query['component'] = split_comma(query['component']) + + if 'include_fields' not in query and 'column_list' in query: + query['include_fields'] = query.pop('column_list') + + if old != query: + log.debug("RHBugzilla pretranslated query to: %s", query) + + @staticmethod + def post_translation(query, bug): + """ + Convert the results of getbug back to the ancient RHBZ value + formats + """ + ignore = query + + # RHBZ _still_ returns component and version as lists, which + # deviates from upstream. Copy the list values to components + # and versions respectively. + if 'component' in bug and "components" not in bug: + val = bug['component'] + bug['components'] = isinstance(val, list) and val or [val] + bug['component'] = bug['components'][0] + + if 'version' in bug and "versions" not in bug: + val = bug['version'] + bug['versions'] = isinstance(val, list) and val or [val] + bug['version'] = bug['versions'][0] + + # sub_components isn't too friendly of a format, add a simpler + # sub_component value + if 'sub_components' in bug and 'sub_component' not in bug: + val = bug['sub_components'] + bug['sub_component'] = "" + if isinstance(val, dict): + values = [] + for vallist in val.values(): + values += vallist + bug['sub_component'] = " ".join(values) diff --git a/bugzilla/base.py b/bugzilla/base.py index 07d5503e..362e4c32 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -23,7 +23,7 @@ from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl from .bug import Bug, Group, User from .exceptions import BugzillaError -from .rhbugzilla import _RHBugzillaConverters +from ._rhconverters import _RHBugzillaConverters from ._session import _BugzillaSession from ._util import listify diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index fb371cda..10b45946 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -1,128 +1,7 @@ -# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib. -# -# Copyright (C) 2008-2012 Red Hat Inc. -# Author: Will Woods -# # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -from logging import getLogger +# This class needs to live in rhbugzilla.py to preserve historical +# 'bugzilla.rhbugzilla' import compat -from ._util import listify - -log = getLogger(__name__) - - -class _RHBugzillaConverters(object): - """ - Static class that holds functional Red Hat back compat converters. - Called inline in Bugzilla - """ - @staticmethod - def convert_build_update( - component=None, - fixed_in=None, - qa_whiteboard=None, - devel_whiteboard=None, - internal_whiteboard=None, - sub_component=None): - adddict = {} - - def get_alias(): - # RHBZ has a custom extension to allow a bug to have multiple - # aliases, so the format of aliases is - # {"add": [...], "remove": [...]} - # But that means in order to approximate upstream, behavior - # which just overwrites the existing alias, we need to read - # the bug's state first to know what string to remove. Which - # we can't do, since we don't know the bug numbers at this point. - # So fail for now. - # - # The API should provide {"set": [...]} - # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 - # - # Implementation will go here when it's available - pass - - if fixed_in is not None: - adddict["cf_fixed_in"] = fixed_in - if qa_whiteboard is not None: - adddict["cf_qa_whiteboard"] = qa_whiteboard - if devel_whiteboard is not None: - adddict["cf_devel_whiteboard"] = devel_whiteboard - if internal_whiteboard is not None: - adddict["cf_internal_whiteboard"] = internal_whiteboard - - if sub_component: - if not isinstance(sub_component, dict): - component = listify(component) - if not component: - raise ValueError("component must be specified if " - "specifying sub_component") - sub_component = {component[0]: sub_component} - adddict["sub_components"] = sub_component - - get_alias() - - return adddict - - - ################# - # Query methods # - ################# - - @staticmethod - def pre_translation(query): - """ - Translates the query for possible aliases - """ - old = query.copy() - - def split_comma(_v): - if isinstance(_v, list): - return _v - return _v.split(",") - - if 'bug_id' in query: - query['id'] = split_comma(query.pop('bug_id')) - - if 'component' in query: - query['component'] = split_comma(query['component']) - - if 'include_fields' not in query and 'column_list' in query: - query['include_fields'] = query.pop('column_list') - - if old != query: - log.debug("RHBugzilla pretranslated query to: %s", query) - - @staticmethod - def post_translation(query, bug): - """ - Convert the results of getbug back to the ancient RHBZ value - formats - """ - ignore = query - - # RHBZ _still_ returns component and version as lists, which - # deviates from upstream. Copy the list values to components - # and versions respectively. - if 'component' in bug and "components" not in bug: - val = bug['component'] - bug['components'] = isinstance(val, list) and val or [val] - bug['component'] = bug['components'][0] - - if 'version' in bug and "versions" not in bug: - val = bug['version'] - bug['versions'] = isinstance(val, list) and val or [val] - bug['version'] = bug['versions'][0] - - # sub_components isn't too friendly of a format, add a simpler - # sub_component value - if 'sub_components' in bug and 'sub_component' not in bug: - val = bug['sub_components'] - bug['sub_component'] = "" - if isinstance(val, dict): - values = [] - for vallist in val.values(): - values += vallist - bug['sub_component'] = " ".join(values) +from .oldclasses import RHBugzilla # pylint: disable=unused-import diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index f63cc5ae..75814e35 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -22,6 +22,14 @@ def test_mock_rhbz(): assert fakebz.__class__ == bugzilla.RHBugzilla +def test_file_imports(): + # Ensure historically stable import paths continue to work + # pylint: disable=unused-import + from bugzilla.rhbugzilla import RHBugzilla + from bugzilla.bug import Bug + from bugzilla.base import Bugzilla + + def testUserAgent(): b3 = tests.mockbackend.make_bz(version="3.0.0") assert "python-bugzilla" in b3.user_agent From 3f81d2db40efe09e0989fa04c81a25b360bb0bb9 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 18:10:34 -0400 Subject: [PATCH 261/393] ci: Drop codecov env_vars, not supported with v1 version yet Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbfc5ed0..34f350ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,4 +43,3 @@ jobs: with: file: ./coverage.xml flags: unittests - env_vars: OS,PYTHON From 292ebeb3e1eb202626c758bc188b58e5887895f7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 18:49:54 -0400 Subject: [PATCH 262/393] cli: Add query --extrafield, --includefield, --excludefield Allow users to pass these extra/include/exclude values straight through to the remote server. For most of the output format options these don't add anything, but for --json and --raw which do a getbug() call, these can be useful for tweaking the output, like reducing the total returned data, or requesting the server return a non-standard field. Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 31 ++- man/bugzilla.rst | 21 ++ tests/data/clioutput/test_query9.txt | 250 +++++++++++++++++++++ tests/data/mockargs/test_getbug_query9.txt | 13 ++ tests/data/mockargs/test_query9.txt | 1 + tests/test_cli_query.py | 15 ++ tests/test_ro_functional.py | 11 + 7 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 tests/data/clioutput/test_query9.txt create mode 100644 tests/data/mockargs/test_getbug_query9.txt create mode 100644 tests/data/mockargs/test_query9.txt diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 47daeb89..93ee6def 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -163,6 +163,21 @@ def _parser_add_output_options(p): help="one line summary of the bug (useful for scripts)") outg.add_argument('--json', action='store_const', dest='output', const='json', help="output contents in json format") + outg.add_argument("--includefield", action="append", + help="Pass the field name to bugzilla include_fields list. " + "Only the fields passed to include_fields are returned " + "by the bugzilla server. " + "This can be specified multiple times.") + outg.add_argument("--extrafield", action="append", + help="Pass the field name to bugzilla extra_fields list. " + "When used with --json this can be used to request " + "bugzilla to return values for non-default fields. " + "This can be specified multiple times.") + outg.add_argument("--excludefield", action="append", + help="Pass the field name to bugzilla exclude_fields list. " + "When used with --json this can be used to request " + "bugzilla to not return values for a field. " + "This can be specified multiple times.") outg.add_argument('--raw', action='store_const', dest='output', const='raw', help="raw output of the bugzilla contents. This " "format is unstable and difficult to parse. Use --json instead.") @@ -750,7 +765,21 @@ def _bug_field_repl_cb(bz, b, matchobj): def _format_output(bz, opt, buglist): if opt.output in ['raw', 'json']: - buglist = bz.getbugs([b.bug_id for b in buglist]) + include_fields = None + exclude_fields = None + extra_fields = None + + if opt.includefield: + include_fields = opt.includefield + if opt.excludefield: + exclude_fields = opt.excludefield + if opt.extrafield: + extra_fields = opt.extrafield + + buglist = bz.getbugs([b.bug_id for b in buglist], + include_fields=include_fields, + exclude_fields=exclude_fields, + extra_fields=extra_fields) if opt.output == 'json': _format_output_json(buglist) if opt.output == 'raw': diff --git a/man/bugzilla.rst b/man/bugzilla.rst index bd58c7cd..c456d6da 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -272,6 +272,27 @@ one line summary of the bug (useful for scripts) output bug contents in JSON format +- ``--includefield`` + +Pass the field name to bugzilla include_fields list. +Only the fields passed to include_fields are returned +by the bugzilla server. +This can be specified multiple times. + +- ``--extrafield`` + +Pass the field name to bugzilla extra_fields list. +When used with --json this can be used to request +bugzilla to return values for non-default fields. +This can be specified multiple times. + +- ``--excludefield`` + +Pass the field name to bugzilla exclude_fields list. +When used with --json this can be used to request +bugzilla to not return values for a field. +This can be specified multiple times. + - ``--raw`` raw output of the bugzilla contents. This format is unstable and diff --git a/tests/data/clioutput/test_query9.txt b/tests/data/clioutput/test_query9.txt new file mode 100644 index 00000000..2e698205 --- /dev/null +++ b/tests/data/clioutput/test_query9.txt @@ -0,0 +1,250 @@ +{ + "bugs": [ + { + "actual_time": 0.0, + "alias": [], + "assigned_to": "lvm-team@redhat.com", + "assigned_to_detail": { + "email": "lvm-team@redhat.com", + "id": 206817, + "name": "lvm-team@redhat.com", + "real_name": "LVM and device-mapper development team" + }, + "blocks": [ + 123456 + ], + "cc": [ + "example@redhat.com", + "example2@redhat.com" + ], + "cc_detail": [ + { + "email": "example@redhat.com", + "id": 123456, + "name": "example@redhat.com", + "real_name": "Example user" + }, + { + "email": "example2@redhat.com", + "id": 123457, + "name": "heinzm@redhat.com", + "real_name": "Example2 user" + } + ], + "cf_build_id": "", + "cf_conditional_nak": [], + "cf_cust_facing": "---", + "cf_doc_type": "Bug Fix", + "cf_environment": "", + "cf_last_closed": "2016-03-03T22:15:07", + "cf_partner": [], + "cf_pgm_internal": "", + "cf_pm_score": "0", + "cf_qe_conditional_nak": [], + "cf_release_notes": "", + "cf_target_upstream_version": "", + "cf_verified": [], + "classification": "Red Hat", + "comments": [ + { + "bug_id": 1165434, + "count": 0, + "creation_time": "2014-11-19T00:26:50", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685441, + "is_private": false, + "tags": [], + "text": "Description of problem:\nVersion-Release number of selected component (if applicable):\nkernel-2.6.18-308.el5\ndevice-mapper-multipath-0.4.7-48.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-1.02.67-2.el5\ndevice-mapper-event-1.02.67-2.el5\n", + "time": "2014-11-19T00:26:50" + }, + { + "bug_id": 1165434, + "count": 1, + "creation_time": "2014-11-19T00:47:57", + "creator": "example@redhat.com", + "creator_id": 276776, + "id": 7685467, + "is_private": false, + "tags": [], + "text": "We can see that there is a dmeventd task that has sent data over a socket and is waiting for the peer to respond:\n\ncrash> bt\nany interaction with the filesystem until it has issued the suspend command to convert the mirror device to a linear device.", + "time": "2014-11-19T00:47:57" + }, + { + "bug_id": 1165434, + "count": 2, + "creation_time": "2014-11-19T01:53:53", + "creator": "example@redhat.com", + "creator_id": 156796, + "id": 7685595, + "is_private": false, + "tags": [], + "text": "Test text", + "time": "2014-11-19T01:53:53" + } + ], + "depends_on": [ + 112233 + ], + "devel_whiteboard": "somedeveltag,someothertag", + "docs_contact": "", + "estimated_time": 0.0, + "external_bugs": [ + { + "bug_id": 989253, + "ext_bz_bug_id": "703421", + "ext_bz_id": 3, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115528, + "type": { + "can_get": true, + "can_send": false, + "description": "GNOME Bugzilla", + "full_url": "https://bugzilla.gnome.org/show_bug.cgi?id=%id%", + "id": 3, + "must_send": false, + "send_once": false, + "type": "Bugzilla", + "url": "https://bugzilla.gnome.org" + } + }, + { + "bug_id": 989253, + "ext_bz_bug_id": "1203576", + "ext_bz_id": 29, + "ext_description": "None", + "ext_priority": "None", + "ext_status": "None", + "id": 115527, + "type": { + "can_get": false, + "can_send": false, + "description": "Launchpad", + "full_url": "https://bugs.launchpad.net/bugs/%id%", + "id": 29, + "must_send": false, + "send_once": false, + "type": "None", + "url": "https://bugs.launchpad.net/bugs" + } + } + ], + "fixed_in": "", + "flags": [ + { + "creation_date": "2019-11-15T21:57:21Z", + "id": 4302313, + "is_active": 1, + "modification_date": "2019-11-15T21:57:21Z", + "name": "qe_test_coverage", + "setter": "pm-rhel@redhat.com", + "status": "?", + "type_id": 318 + }, + { + "creation_date": "2018-12-25T16:47:43Z", + "id": 3883137, + "is_active": 1, + "modification_date": "2018-12-25T16:47:43Z", + "name": "release", + "setter": "rule-engine@redhat.com", + "status": "?", + "type_id": 1197 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883134, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "pm_ack", + "setter": "example3@redhat.com", + "status": "?", + "type_id": 11 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883135, + "is_active": 1, + "modification_date": "2018-12-25T16:47:38Z", + "name": "devel_ack", + "setter": "example2@redhat.com", + "status": "?", + "type_id": 10 + }, + { + "creation_date": "2018-12-25T16:47:38Z", + "id": 3883136, + "is_active": 1, + "modification_date": "2019-04-28T02:07:03Z", + "name": "qa_ack", + "setter": "example@redhat.com", + "status": "+", + "type_id": 9 + }, + { + "creation_date": "2019-03-29T06:50:01Z", + "id": 3999302, + "is_active": 1, + "modification_date": "2019-03-29T06:50:01Z", + "name": "needinfo", + "requestee": "hello@example.com", + "setter": "example@redhat.com", + "status": "?", + "type_id": 1164 + } + ], + "groups": [ + "somegroup" + ], + "id": 1165434, + "internal_whiteboard": "someinternal TAG", + "is_cc_accessible": true, + "is_confirmed": true, + "is_creator_accessible": true, + "is_open": false, + "keywords": [ + "key1", + "keyword2", + "Security" + ], + "last_change_time": "2018-12-09T19:12:12", + "op_sys": "Linux", + "platform": "All", + "priority": "medium", + "product": "Red Hat Enterprise Linux 5", + "qa_contact": "mspqa-list@redhat.com", + "qa_contact_detail": { + "email": "mspqa-list@redhat.com", + "id": 164197, + "name": "mspqa-list@redhat.com", + "real_name": "Cluster QE" + }, + "qa_whiteboard": "foo bar baz", + "remaining_time": 0.0, + "resolution": "WONTFIX", + "see_also": [], + "severity": "medium", + "status": "CLOSED", + "sub_component": "dmeventd (RHEL5)", + "sub_components": { + "lvm2": [ + "dmeventd (RHEL5)" + ] + }, + "summary": "LVM mirrored root can deadlock dmeventd if a mirror leg is lost", + "tags": [], + "target_milestone": "rc", + "target_release": [ + "---" + ], + "url": "", + "version": "5.8", + "versions": [ + "5.8" + ], + "whiteboard": "genericwhiteboard" + } + ] +} diff --git a/tests/data/mockargs/test_getbug_query9.txt b/tests/data/mockargs/test_getbug_query9.txt new file mode 100644 index 00000000..1905d96e --- /dev/null +++ b/tests/data/mockargs/test_getbug_query9.txt @@ -0,0 +1,13 @@ +([1165434], + [], + {'exclude_fields': ['excludeme'], + 'extra_fields': ['extrame1', + 'extrame2', + 'comments', + 'description', + 'external_bugs', + 'flags', + 'sub_components', + 'tags'], + 'include_fields': ['foo', 'bar', 'id'], + 'permissive': 1}) diff --git a/tests/data/mockargs/test_query9.txt b/tests/data/mockargs/test_query9.txt new file mode 100644 index 00000000..d499186e --- /dev/null +++ b/tests/data/mockargs/test_query9.txt @@ -0,0 +1 @@ +{'id': ['1165434'], 'include_fields': ['id']} diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py index 47c9dd42..3fa4cc85 100644 --- a/tests/test_cli_query.py +++ b/tests/test_cli_query.py @@ -132,3 +132,18 @@ def test_query(run_cli): tests.utils.diff_compare(tests.utils.sanitize_json(out), "data/clioutput/test_query8.txt") assert json.loads(out) + + # Test --json output + cmd = ("bugzilla query --json --id 1165434 " + "--includefield foo --includefield bar " + "--excludefield excludeme " + "--extrafield extrame1 --extrafield extrame2 ") + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_search_args="data/mockargs/test_query9.txt", + bug_search_return={"bugs": [{"id": 1165434}]}, + bug_get_args="data/mockargs/test_getbug_query9.txt", + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(tests.utils.sanitize_json(out), + "data/clioutput/test_query9.txt") + assert json.loads(out) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index a5837fef..03761bdc 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -229,6 +229,17 @@ def testQueryFixedIn(run_cli, backends): assert "#629311 CLOSED" in out +def testQueryExtrafieldPool(run_cli, backends): + # rhbz has an agile 'pool' extension but doesn't return the field + # by default. Check that '-extrafield pool' returns it for --json output + bz = _open_bz(REDHAT_URL, **backends) + + out1 = run_cli("bugzilla query --id 1717616 --json", bz) + out2 = run_cli("bugzilla query --id 1717616 --json --extrafield pool", bz) + assert "current_sprint_id" not in out1 + assert "current_sprint_id" in out2 + + def testComponentsDetails(backends): """ Fresh call to getcomponentsdetails should properly refresh From d899472b12aae8ac801f1805094b4509c63ff090 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 3 Jul 2020 19:19:01 -0400 Subject: [PATCH 263/393] tox: Drop sitepackages usage It hasn't been needed for a long time Signed-off-by: Cole Robinson --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 00a1a0aa..7fa6266b 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ envlist = py27,py34,py35,py36,py37,py38 [testenv] -sitepackages = True deps = -rrequirements.txt -rtest-requirements.txt From 9c2880e164b8d9267ef5defebfaa47b30b4ef911 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jul 2020 11:07:18 -0400 Subject: [PATCH 264/393] setup: Rework 'rpm' command slightly - Use subprocess - Return error on failure - Force output in the source directory - Drop the /tmp spec handling which was an artifact of my local config Signed-off-by: Cole Robinson --- setup.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index a5c01da7..2fadede1 100755 --- a/setup.py +++ b/setup.py @@ -64,7 +64,8 @@ def run(self): class RPMCommand(Command): - description = "Build src and binary rpms." + description = ("Build src and binary rpms and output them " + "in the source directory") user_options = [] def initialize_options(self): @@ -73,17 +74,15 @@ def finalize_options(self): pass def run(self): - """ - Run sdist, then 'rpmbuild' the tar.gz - """ - os.system("cp python-bugzilla.spec /tmp") - try: - os.system("rm -rf python-bugzilla-%s" % get_version()) - self.run_command('sdist') - os.system('rpmbuild -ta --clean dist/python-bugzilla-%s.tar.gz' % - get_version()) - finally: - os.system("mv /tmp/python-bugzilla.spec .") + self.run_command('sdist') + srcdir = os.path.dirname(__file__) + cmd = [ + "rpmbuild", "-ta", + "--define", "_rpmdir %s" % srcdir, + "--define", "_srcrpmdir %s" % srcdir, + "dist/python-bugzilla-%s.tar.gz" % get_version(), + ] + subprocess.check_call(cmd) class BuildCommand(distutils.command.build.build): From 88f51f2647c712b413af30731b9c7f4ef6d23c89 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jul 2020 12:52:17 -0400 Subject: [PATCH 265/393] ci: Rework build.yml naming a bit Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34f350ab..10bde702 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions # Derived from stock python-package.yml -name: Build + test +name: CI on: [push, pull_request] @@ -25,7 +25,7 @@ jobs: run: | python -m pip install --upgrade pip pip install flake8 pytest pytest-cov - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r requirements.txt -r test-requirements.txt - name: Lint with flake8 run: | From 84da9ad60e770fa5889020180c39266e3b018c4e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jul 2020 12:52:41 -0400 Subject: [PATCH 266/393] ci: Add RPM building for fedora, centos7 and centos8 Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10bde702..005b6a2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,3 +43,33 @@ jobs: with: file: ./coverage.xml flags: unittests + + # Build the RPM on latest fedora, centos7 and centos8 + rpm: + runs-on: ubuntu-latest + + container: + image: fedora:latest + # All this is needed to ensure 'mock' works in docker + options: --cap-add=SYS_ADMIN --security-opt label:disable --security-opt seccomp=unconfined --security-opt apparmor:unconfined + + steps: + - uses: actions/checkout@v2 + + - name: Install deps + run: | + # glibc-langpacks-en needed to work around python locale issues + dnf install -y \ + python3-pip \ + rpm-build \ + mock \ + dnf-plugins-core \ + glibc-langpack-en + dnf builddep -y ./*.spec + + - name: Build RPM + SRPM + run: | + ./setup.py rpm + + - run: mock --root epel-7-x86_64 *.src.rpm + - run: mock --root epel-8-x86_64 *.src.rpm From b695caca41fea11ab1194e05d68a56f0b1b99555 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jul 2020 13:35:27 -0400 Subject: [PATCH 267/393] README: Add PyPI and CI badges Signed-off-by: Cole Robinson --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3d947da8..0bceec99 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![CI](https://github.com/python-bugzilla/python-bugzilla/workflows/CI/badge.svg)](https://github.com/python-bugzilla/python-bugzilla/actions?query=workflow%3ACI) +[![PyPI](https://img.shields.io/pypi/v/python-bugzilla)](https://pypi.org/project/python-bugzilla/) + # python-bugzilla This package provides two bits: From c9e2cf76bae180b14df82eed7bfe5096b9ca7ac4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jul 2020 13:39:57 -0400 Subject: [PATCH 268/393] Adjust references to XMLRPC to mention REST With REST in the mix we need to make these more generic Signed-off-by: Cole Robinson --- README.md | 2 +- bugzilla/_cli.py | 6 +++--- examples/bug_autorefresh.py | 2 +- examples/create.py | 2 +- examples/getbug_restapi.py | 2 +- examples/query.py | 2 +- examples/update.py | 2 +- python-bugzilla.spec | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0bceec99..5943aa3e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This package provides two bits: -* 'bugzilla' python module for talking to a [Bugzilla](https://www.bugzilla.org/) instance over XMLRPC +* 'bugzilla' python module for talking to a [Bugzilla](https://www.bugzilla.org/) instance over XMLRPC or REST * /usr/bin/bugzilla command line tool for performing actions from the command line: create or edit bugs, various queries, etc. This was originally written specifically for Red Hat's Bugzilla instance diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 93ee6def..26a48ad2 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -101,7 +101,7 @@ def _setup_root_parser(): # General bugzilla connection options p.add_argument('--bugzilla', default=default_url, - help="bugzilla XMLRPC URI. default: %s" % default_url) + help="bugzilla URI. default: %s" % default_url) p.add_argument("--nosslverify", dest="sslverify", action="store_false", default=True, help="Don't error on invalid bugzilla SSL certificate") @@ -258,7 +258,7 @@ def _parser_add_bz_fields(rootp, command): # Put this at the end, so it sticks out more p.add_argument('--field', metavar="FIELD=VALUE", action="append", dest="fields", - help="Manually specify a bugzilla XMLRPC field. FIELD is " + help="Manually specify a bugzilla API field. FIELD is " "the raw name used by the bugzilla instance. For example, if your " "bugzilla instance has a custom field cf_my_field, do:\n" " --field cf_my_field=VALUE") @@ -969,7 +969,7 @@ def _do_modify(bz, parser, opt): # Now for the things we can't blindly batch. # Being able to prepend/append to whiteboards, which are just # plain string values, is an old rhbz semantic that we try to maintain - # here. This is a bit weird for traditional bugzilla XMLRPC + # here. This is a bit weird for traditional bugzilla API log.debug("Adjusting whiteboard fields one by one") for bug in bz.getbugs(bugid_list): update_kwargs = {} diff --git a/examples/bug_autorefresh.py b/examples/bug_autorefresh.py index b95ff584..20f90b17 100644 --- a/examples/bug_autorefresh.py +++ b/examples/bug_autorefresh.py @@ -51,6 +51,6 @@ # Why does this matter? Some scripts are implicitly depending on this # auto-refresh behavior, because their include_fields specification doesn't # cover all attributes they actually use. Your script will work, sure, but -# it's likely doing many more XML-RPC calls than needed, possibly 1 per bug. +# it's likely doing many more API calls than needed, possibly 1 per bug. # So if after upgrading python-bugzilla you start hitting issues, the # recommendation is to fix your include_fields. diff --git a/examples/create.py b/examples/create.py index 067a0942..cf417fa1 100644 --- a/examples/create.py +++ b/examples/create.py @@ -25,7 +25,7 @@ # Similar to build_query, build_createbug is a helper function that handles # some bugzilla version incompatibility issues. All it does is return a # properly formatted dict(), and provide friendly parameter names. -# The argument names map to those accepted by XMLRPC Bug.create: +# The argument names map to those accepted by Bugzilla Bug.create: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug # # The arguments specified here are mandatory, but there are many other diff --git a/examples/getbug_restapi.py b/examples/getbug_restapi.py index cc09721b..6b1b5e29 100644 --- a/examples/getbug_restapi.py +++ b/examples/getbug_restapi.py @@ -16,7 +16,7 @@ # By default, if plain Bugzilla(URL) is invoked, the Bugzilla class will # attempt to determine if XMLRPC or REST API is available, with a preference -# for XMLRPC for back compatability. But you can for use for the REST API +# for XMLRPC for back compatability. But you can use the REST API # with force_rest=True bzapi = bugzilla.Bugzilla(URL, force_rest=True) diff --git a/examples/query.py b/examples/query.py index 468d586a..856f64f5 100644 --- a/examples/query.py +++ b/examples/query.py @@ -20,7 +20,7 @@ # build_query is a helper function that handles some bugzilla version # incompatibility issues. All it does is return a properly formatted # dict(), and provide friendly parameter names. The param names map -# to those accepted by XMLRPC Bug.search: +# to those accepted by Bugzilla Bug.search: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs query = bzapi.build_query( product="Fedora", diff --git a/examples/update.py b/examples/update.py index 7de33a34..1da11470 100644 --- a/examples/update.py +++ b/examples/update.py @@ -22,7 +22,7 @@ # Similar to build_query, build_update is a helper function that handles # some bugzilla version incompatibility issues. All it does is return a # properly formatted dict(), and provide friendly parameter names. -# The param names map to those accepted by XMLRPC Bug.update: +# The param names map to those accepted by Bugzilla Bug.update: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug # # Example bug: https://partner-bugzilla.redhat.com/show_bug.cgi?id=427301 diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 520604b9..2e945902 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -42,7 +42,7 @@ BuildRequires: python3-pytest %global _description\ python-bugzilla is a python library for interacting with bugzilla instances\ -over XML-RPC.\ +over XMLRPC or REST.\ %description %_description From 7aa70edcfea9b524cd8ac51a891b6395ca40dc87 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 4 Jul 2020 14:27:25 -0400 Subject: [PATCH 269/393] Prep for release 2.5.0 --- NEWS.md | 4 ++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index b9731ced..78190215 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # python-bugzilla release news +## Release 2.5.0 (July 04, 2020) +- cli: Add query --extrafield, --includefield, --excludefield +- Revive bugzilla.rhbugzilla.RHBugzilla import path + ## Release 2.4.0 (June 29, 2020) - Bugzilla REST API support - Add --json command line output option diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index afb66674..6ff41ed4 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "2.4.0" +version = "2.5.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 2e945902..404a8a4a 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -15,7 +15,7 @@ %endif Name: python-bugzilla -Version: 2.4.0 +Version: 2.5.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From 7b9df009dc4914d09835eb92df694565650e5bbf Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 16 Jul 2020 18:19:44 -0400 Subject: [PATCH 270/393] Bugzilla: Don't overwrite subclass with RHBugzilla If a user subclasses Bugzilla, we don't want to overwrite __class__ with the RHBugzilla detection back compat Closes: #129 Signed-off-by: Cole Robinson --- bugzilla/base.py | 10 ++++++++-- tests/test_ro_functional.py | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 362e4c32..751b9828 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -270,9 +270,15 @@ def _init_class_from_url(self): """ from .oldclasses import RHBugzilla # pylint: disable=cyclic-import - if self._detect_is_redhat_bugzilla(): + if not self._detect_is_redhat_bugzilla(): + return + + self._is_redhat_bugzilla = True + if self.__class__ == Bugzilla: + # Overriding the class doesn't have any functional effect, + # but we continue to do it for API back compat incase anyone + # is doing any class comparison. We should drop this in the future self.__class__ = RHBugzilla - self._is_redhat_bugzilla = True def _get_field_aliases(self): # List of field aliases. Maps old style RHBZ parameter diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 03761bdc..f423d4a3 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -19,10 +19,11 @@ "https://bugzilla.redhat.com") -def _open_bz(url, **kwargs): +def _open_bz(url, bzclass=None, **kwargs): if "use_creds" not in kwargs: kwargs["use_creds"] = False - return tests.utils.open_functional_bz(bugzilla.Bugzilla, url, kwargs) + return tests.utils.open_functional_bz(bzclass or bugzilla.Bugzilla, + url, kwargs) def _check(out, mincount, expectstr): @@ -39,11 +40,21 @@ def _test_version(bz, bzversion): assert bz.bz_ver_minor == bzversion[1] +def test_bugzilla_override(): + class MyBugzilla(bugzilla.Bugzilla): + pass + + bz = _open_bz("bugzilla.redhat.com", bzclass=MyBugzilla) + assert bz.__class__ is MyBugzilla + assert bz._is_redhat_bugzilla is True # pylint: disable=protected-access + + def test_rest_xmlrpc_detection(): # The default: use XMLRPC bz = _open_bz("bugzilla.redhat.com") assert bz.is_xmlrpc() assert "/xmlrpc.cgi" in bz.url + assert bz.__class__ is bugzilla.RHBugzilla # See /rest in the URL, so use REST bz = _open_bz("bugzilla.redhat.com/rest") From d6186e8b6024fa1e9ee17f46b3463cf894c24a2e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 17 Jul 2020 14:55:15 -0400 Subject: [PATCH 271/393] Move pylintrc -> .pylintrc pylint picks up the latter as well Signed-off-by: Cole Robinson --- pylintrc => .pylintrc | 0 setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename pylintrc => .pylintrc (100%) diff --git a/pylintrc b/.pylintrc similarity index 100% rename from pylintrc rename to .pylintrc diff --git a/setup.py b/setup.py index 2fadede1..938567fd 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ def run(self): print("running pylint") pylint_opts = [ - "--rcfile", "pylintrc", + "--rcfile", ".pylintrc", "--output-format=%s" % output_format, ] pylint.lint.Run(files + pylint_opts) From a3893e636b9c589a84489631c499607749ac6882 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 17 Jul 2020 15:22:44 -0400 Subject: [PATCH 272/393] setup.py: Don't remove the .spec file after rpm build Signed-off-by: Cole Robinson --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 938567fd..cd852469 100755 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ def run(self): "rpmbuild", "-ta", "--define", "_rpmdir %s" % srcdir, "--define", "_srcrpmdir %s" % srcdir, + "--define", "_specdir /tmp", "dist/python-bugzilla-%s.tar.gz" % get_version(), ] subprocess.check_call(cmd) From 674f43928c5abdb91389e3860e9505c0aa290915 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 07:16:54 -0400 Subject: [PATCH 273/393] Fix pylint on Fedora 33 Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 4 ++-- bugzilla/_backendxmlrpc.py | 1 + bugzilla/_cli.py | 2 +- bugzilla/base.py | 4 ++-- tests/test_api_authfiles.py | 5 +++-- tests/test_rw_functional.py | 4 ++-- tests/utils.py | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index a2686c49..7d746c3f 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -203,8 +203,8 @@ def _build_cookiejar(self, cookiefile): cj.load() return cj except LoadError: - raise BugzillaError("cookiefile=%s not in Mozilla format" % - cookiefile) + msg = "cookiefile=%s not in Mozilla format" % cookiefile + raise BugzillaError(msg) from None def set_filename(self, cookiefile): log.debug("Using cookiefile=%s", cookiefile) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index e2182cd7..59c47c99 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -39,6 +39,7 @@ def __request_helper(self, url, request_body): """ response = None # pylint: disable=try-except-raise + # pylint: disable=raise-missing-from try: session = self.__bugzillasession.get_requests_session() response = session.post(url, data=request_body) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 26a48ad2..b8313e76 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -62,7 +62,7 @@ def open_without_clobber(name, *args): name = "%s.%i" % (orig_name, count) count += 1 else: # pragma: no cover - raise IOError(err.errno, err.strerror, err.filename) + raise IOError(err.errno, err.strerror, err.filename) from None fobj = open(name, *args) if fd != fobj.fileno(): os.close(fd) diff --git a/bugzilla/base.py b/bugzilla/base.py index 751b9828..76664d90 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -618,7 +618,7 @@ def login(self, user=None, password=None, restrict_login=None): except Exception as e: log.debug("Login exception: %s", str(e), exc_info=True) raise BugzillaError("Login failed: %s" % - BugzillaError.get_bugzilla_error_string(e)) + BugzillaError.get_bugzilla_error_string(e)) from None def interactive_save_api_key(self): """ @@ -1322,7 +1322,7 @@ def query(self, query): raise raise BugzillaError("%s\nYour bugzilla instance does not " "appear to support API queries derived from bugzilla " - "web URL queries." % e) + "web URL queries." % e) from None log.debug("Query returned %s bugs", len(r['bugs'])) return [Bug(self, dict=b, diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py index 222a9ea2..2ea411a0 100644 --- a/tests/test_api_authfiles.py +++ b/tests/test_api_authfiles.py @@ -169,8 +169,9 @@ def test_authfiles_saving(monkeypatch): # On RHEL7 the cookie comment header is different. Strip off leading # comments def strip_comments(f): - return "".join([l for l in open(f).readlines() if - not l.startswith("#")]) + return "".join([ + line for line in open(f).readlines() if + not line.startswith("#")]) tests.utils.diff_compare(strip_comments(bzapi.cookiefile), None, expect_out=strip_comments(output_cookies)) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index f00c76fc..3415e321 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -575,7 +575,7 @@ def _test8Attachments(run_cli, backends): out = run_cli(cmd + "--getall %s" % getbug.id, bz).splitlines() assert len(out) == numattach - fnames = [l.split(" ", 1)[1].strip() for l in out] + fnames = [line.split(" ", 1)[1].strip() for line in out] assert len(fnames) == numattach for f in fnames: if not os.path.exists(f): @@ -587,7 +587,7 @@ def _test8Attachments(run_cli, backends): out = run_cli(ignorecmd, bz).splitlines() assert len(out) == (numattach - 1) - fnames = [l.split(" ", 1)[1].strip() for l in out] + fnames = [line.split(" ", 1)[1].strip() for line in out] assert len(fnames) == (numattach - 1) for f in fnames: if not os.path.exists(f): diff --git a/tests/utils.py b/tests/utils.py index 339d7169..d75f21a9 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,7 +49,7 @@ def monkeypatch_getpass(monkeypatch): def sanitize_json(rawout): # py2.7 leaves trailing whitespace after commas. strip it so # tests pass on both python versions - return "\n".join([l.rstrip() for l in rawout.splitlines()]) + return "\n".join([line.rstrip() for line in rawout.splitlines()]) def open_functional_bz(bzclass, url, kwargs): From 56c09549fb3bb2640a7632f38ba143a4ed52f480 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 08:03:13 -0400 Subject: [PATCH 274/393] Drop python2 support Python 2 is end-of-life since Jan 1 2020. The 2.5.0 release can still be used if python2 support is needed. Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 13 ++---- bugzilla/_compatimports.py | 25 +++-------- bugzilla/_util.py | 21 ---------- bugzilla/bug.py | 10 +---- examples/apikey.py | 8 +--- examples/bug_autorefresh.py | 2 - examples/create.py | 2 - examples/getbug.py | 2 - examples/getbug_restapi.py | 2 - examples/query.py | 2 - examples/update.py | 2 - python-bugzilla.spec | 84 ------------------------------------- setup.py | 14 +------ tests/test_api_bug.py | 10 +---- tests/test_cli_misc.py | 2 - tests/test_rw_functional.py | 2 - tests/utils.py | 17 +------- tox.ini | 2 +- 18 files changed, 19 insertions(+), 201 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index b8313e76..e529004b 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -9,8 +9,6 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -from __future__ import print_function - import argparse import base64 import datetime @@ -28,7 +26,6 @@ import bugzilla from bugzilla._compatimports import Fault, ProtocolError, urlparse -from bugzilla._util import to_encoding DEFAULT_BZ = 'https://bugzilla.redhat.com' @@ -620,13 +617,12 @@ def _filter_components(compdetails): elif opt.versions: proddict = bz.getproducts()[0] for v in proddict['versions']: - print(to_encoding(v["name"])) + print(str(v["name"] or '')) elif opt.component_owners: details = bz.getcomponentsdetails(productname) for c in sorted(_filter_components(details)): - print(to_encoding(u"%s: %s" % (c, - details[c]['default_assigned_to']))) + print(u"%s: %s" % (c, details[c]['default_assigned_to'])) def _convert_to_outputformat(output): @@ -691,8 +687,7 @@ def _format_output_raw(buglist): continue if attrname.startswith("_"): continue - print(to_encoding(u"ATTRIBUTE[%s]: %s" % - (attrname, b.__dict__[attrname]))) + print("ATTRIBUTE[%s]: %s" % (attrname, b.__dict__[attrname])) print("\n\n") @@ -758,7 +753,7 @@ def _bug_field_repl_cb(bz, b, matchobj): val = getattr(b, fieldname, "") vallist = isinstance(val, list) and val or [val] - val = ','.join([to_encoding(v) for v in vallist]) + val = ','.join([str(v or '') for v in vallist]) return val diff --git a/bugzilla/_compatimports.py b/bugzilla/_compatimports.py index b531566d..47fcaf79 100644 --- a/bugzilla/_compatimports.py +++ b/bugzilla/_compatimports.py @@ -1,24 +1,11 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -import sys +# pylint: disable=unused-import -IS_PY3 = sys.version_info[0] >= 3 - -# pylint: disable=import-error,unused-import,ungrouped-imports -# pylint: disable=no-name-in-module -if IS_PY3: - from collections.abc import Mapping - from configparser import ConfigParser - from http.cookiejar import LoadError, MozillaCookieJar - from urllib.parse import urlparse, urlunparse, parse_qsl - from xmlrpc.client import (Binary, DateTime, Fault, ProtocolError, - ServerProxy, Transport) -else: # pragma: no cover - from collections import Mapping - from ConfigParser import SafeConfigParser as ConfigParser - from cookielib import LoadError, MozillaCookieJar - from urlparse import urlparse - from xmlrpclib import (Binary, DateTime, Fault, ProtocolError, +from collections.abc import Mapping +from configparser import ConfigParser +from http.cookiejar import LoadError, MozillaCookieJar +from urllib.parse import urlparse, urlunparse, parse_qsl +from xmlrpc.client import (Binary, DateTime, Fault, ProtocolError, ServerProxy, Transport) - from urlparse import urlparse, urlunparse, parse_qsl diff --git a/bugzilla/_util.py b/bugzilla/_util.py index 8c137c16..04555779 100644 --- a/bugzilla/_util.py +++ b/bugzilla/_util.py @@ -1,10 +1,6 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -import locale - -from ._compatimports import IS_PY3 - def listify(val): """Ensure that value is either None or a list, converting single values @@ -14,20 +10,3 @@ def listify(val): if isinstance(val, list): return val return [val] - - -def to_encoding(ustring): - """ - Locale specific printing per python version - """ - # pylint: disable=undefined-variable - - ustring = ustring or '' - if IS_PY3: - return str(ustring) - else: # pragma: no cover - strtype = basestring # noqa - string = ustring - if not isinstance(ustring, strtype): - string = str(ustring) - return string.encode(locale.getpreferredencoding(), 'replace') diff --git a/bugzilla/bug.py b/bugzilla/bug.py index f25609dd..5faa28b8 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -9,8 +9,6 @@ import copy from logging import getLogger -from ._util import to_encoding - log = getLogger(__name__) @@ -49,17 +47,13 @@ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): def __str__(self): """ Return a simple string representation of this bug - - This is available only for compatibility. Using 'str(bug)' and - 'print(bug)' is not recommended because of potential encoding issues. - Please use unicode(bug) where possible. """ - return to_encoding(self.__unicode__()) + return self.__unicode__() def __unicode__(self): """ Return a simple unicode string representation of this bug - """ + """ return "#%-6s %-10s - %s - %s" % (self.bug_id, self.bug_status, self.assigned_to, self.summary) diff --git a/examples/apikey.py b/examples/apikey.py index 4522129c..8b2c90b7 100644 --- a/examples/apikey.py +++ b/examples/apikey.py @@ -6,9 +6,6 @@ # apikey.py: Demostrate prompting for API key and passing it to Bugzilla # pylint: disable=undefined-variable -from __future__ import print_function -import sys - import bugzilla # Don't worry, changing things here is fine, and won't send any email to @@ -19,10 +16,7 @@ " https://landfill.bugzilla.org/bugzilla-5.0-branch/userprefs.cgi") print("This is a test site, so no harm will come!\n") -if sys.version_info[0] >= 3: - api_key = input("Enter Bugzilla API Key: ") # noqa -else: - api_key = raw_input("Enter Bugzilla API Key: ") # noqa +api_key = input("Enter Bugzilla API Key: ") # API key usage assumes the API caller is storing the API key; if you would # like to use one of the login options that stores credentials on-disk for diff --git a/examples/bug_autorefresh.py b/examples/bug_autorefresh.py index 20f90b17..09c1c533 100644 --- a/examples/bug_autorefresh.py +++ b/examples/bug_autorefresh.py @@ -6,8 +6,6 @@ # bug_autorefresh.py: Show what bug_autorefresh is all about, and explain # how to handle the default change via python-bugzilla in 2016 -from __future__ import print_function - import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes diff --git a/examples/create.py b/examples/create.py index cf417fa1..be1c75dc 100644 --- a/examples/create.py +++ b/examples/create.py @@ -5,8 +5,6 @@ # create.py: Create a new bug report -from __future__ import print_function - import time import bugzilla diff --git a/examples/getbug.py b/examples/getbug.py index e532a289..866b0d6d 100644 --- a/examples/getbug.py +++ b/examples/getbug.py @@ -6,8 +6,6 @@ # getbug.py: Simple demonstration of connecting to bugzilla, fetching # a bug, and printing some details. -from __future__ import print_function - import pprint import bugzilla diff --git a/examples/getbug_restapi.py b/examples/getbug_restapi.py index 6b1b5e29..94723884 100644 --- a/examples/getbug_restapi.py +++ b/examples/getbug_restapi.py @@ -7,8 +7,6 @@ # Simple demonstration of connecting to bugzilla over the REST # API and printing some bug details. -from __future__ import print_function - import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes diff --git a/examples/query.py b/examples/query.py index 856f64f5..ac285118 100644 --- a/examples/query.py +++ b/examples/query.py @@ -5,8 +5,6 @@ # query.py: Perform a few varieties of queries -from __future__ import print_function - import time import bugzilla diff --git a/examples/update.py b/examples/update.py index 1da11470..ff13bdf3 100644 --- a/examples/update.py +++ b/examples/update.py @@ -5,8 +5,6 @@ # update.py: Make changes to an existing bug -from __future__ import print_function - import time import bugzilla diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 404a8a4a..4da607a3 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,19 +1,3 @@ -%if 0%{?fedora} || 0%{?rhel} > 7 -# Enable python3 by default -%bcond_without python3 -%else -%bcond_with python3 -%endif - -%if 0%{?fedora} > 31 || 0%{?rhel} > 7 -# Disable python2 build by default -%bcond_with python2 -%else -%bcond_without python2 -%{!?__python2: %global __python2 /usr/bin/python2} -%{!?python2_sitelib: %global python2_sitelib %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print (get_python_lib())")} -%endif - Name: python-bugzilla Version: 2.5.0 Release: 1%{?dist} @@ -24,21 +8,11 @@ URL: https://github.com/python-bugzilla/python-bugzilla Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{version}/%{name}-%{version}.tar.gz BuildArch: noarch -%if %{with python2} -BuildRequires: python2-devel -BuildRequires: python2-docutils -BuildRequires: python2-requests -BuildRequires: python2-setuptools -BuildRequires: python2-pytest -%endif - -%if %{with python3} BuildRequires: python3-devel BuildRequires: python3-docutils BuildRequires: python3-requests BuildRequires: python3-setuptools BuildRequires: python3-pytest -%endif %global _description\ python-bugzilla is a python library for interacting with bugzilla instances\ @@ -47,42 +21,20 @@ over XMLRPC or REST.\ %description %_description -%if %{with python2} -%package -n python2-bugzilla -Summary: %summary -Requires: python2-requests -# This dep is for back compat, so that installing python-bugzilla continues -# to give the cli tool -Requires: python-bugzilla-cli -%{?python_provide:%python_provide python2-bugzilla} - -%description -n python2-bugzilla %_description - -%endif - - -%if %{with python3} %package -n python3-bugzilla Summary: %summary Requires: python3-requests %{?python_provide:%python_provide python3-bugzilla} -%if %{without python2} Obsoletes: python-bugzilla < %{version}-%{release} Obsoletes: python2-bugzilla < %{version}-%{release} -%endif %description -n python3-bugzilla %_description -%endif %package cli Summary: Command line tool for interacting with Bugzilla -%if %{with python3} Requires: python3-bugzilla = %{version}-%{release} -%else -Requires: python2-bugzilla = %{version}-%{release} -%endif %description cli This package includes the 'bugzilla' command-line tool for interacting with bugzilla. Uses the python-bugzilla API @@ -92,37 +44,15 @@ This package includes the 'bugzilla' command-line tool for interacting with bugz %prep %setup -q -%if %{with python3} -rm -rf %{py3dir} -cp -a . %{py3dir} -%endif - %install -%if %{with python3} -pushd %{py3dir} %{__python3} setup.py install -O1 --root %{buildroot} -%if %{with python2} -rm %{buildroot}/usr/bin/bugzilla -%endif - -popd -%endif - -%if %{with python2} -%{__python2} setup.py install -O1 --root %{buildroot} -%endif - # Replace '#!/usr/bin/env python' with '#!/usr/bin/python2' # The format is ideal for upstream, but not a distro. See: # https://fedoraproject.org/wiki/Features/SystemPythonExecutablesUseSystemPython -%if %{with python3} %global python_env_path %{__python3} -%else -%global python_env_path %{__python2} -%endif for f in $(find %{buildroot} -type f -executable -print); do sed -i "1 s|^#!/usr/bin/.*|#!%{python_env_path}|" $f || : done @@ -130,27 +60,13 @@ done %check -%if %{with python2} -# py.test naming is needed for RHEL7 compat, works fine with Fedora -py.test -%endif -%if %{with python3} pytest-3 -%endif - -%if %{with python2} -%files -n python2-bugzilla -%doc COPYING README.md NEWS.md -%{python2_sitelib}/* -%endif -%if %{with python3} %files -n python3-bugzilla %doc COPYING README.md NEWS.md %{python3_sitelib}/* -%endif %files cli %{_bindir}/bugzilla diff --git a/setup.py b/setup.py index cd852469..50f2b522 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -from __future__ import print_function - import glob import os import subprocess @@ -12,15 +10,6 @@ from setuptools import setup -def unsupported_python_version(): - return sys.version_info < (2, 7) \ - or (sys.version_info > (3,) and sys.version_info < (3, 4)) - - -if unsupported_python_version(): - raise ImportError("python-bugzilla does not support this python version") - - def get_version(): f = open("bugzilla/apiversion.py") for line in f: @@ -135,14 +124,13 @@ def _parse_requirements(fname): 'GNU General Public License v2 or later (GPLv2+)', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], packages=['bugzilla'], data_files=[], diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index 54372269..2f762453 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -9,8 +9,8 @@ Unit tests for testing some bug.py magic """ +import io import pickle -import sys import pytest @@ -59,13 +59,7 @@ def _assert_bug(): dir(bug) # Test special pickle support - if sys.version_info[0] >= 3: - import io - fd = io.BytesIO() - else: - import StringIO # pylint: disable=import-error - fd = StringIO.StringIO() - + fd = io.BytesIO() pickle.dump(bug, fd) fd.seek(0) bug = pickle.load(fd) diff --git a/tests/test_cli_misc.py b/tests/test_cli_misc.py index fd042cfe..2955a82e 100644 --- a/tests/test_cli_misc.py +++ b/tests/test_cli_misc.py @@ -9,8 +9,6 @@ Test miscellaneous CLI bits to get build out our code coverage """ -from __future__ import print_function - import base64 import datetime import json diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 3415e321..c3f90bf8 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -9,8 +9,6 @@ Unit tests that do permanent functional against a real bugzilla instances. """ -from __future__ import print_function - import datetime import inspect import os diff --git a/tests/utils.py b/tests/utils.py index d75f21a9..cc738fc7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,7 +13,6 @@ import pytest import bugzilla._cli -from bugzilla._compatimports import IS_PY3 import tests @@ -30,20 +29,8 @@ def tests_path(filename): return filename -def fake_stream(text): - if IS_PY3: - return io.StringIO(text) - else: - return io.BytesIO(text) - - def monkeypatch_getpass(monkeypatch): - # pylint: disable=undefined-variable - if IS_PY3: - use_input = input # noqa - else: - use_input = raw_input # noqa - monkeypatch.setattr(getpass, "getpass", use_input) + monkeypatch.setattr(getpass, "getpass", input) def sanitize_json(rawout): @@ -118,7 +105,7 @@ def do_run_cli(capsys, monkeypatch, argv = shlex.split(argvstr) monkeypatch.setattr(sys, "argv", argv) if stdin: - monkeypatch.setattr(sys, "stdin", fake_stream(stdin)) + monkeypatch.setattr(sys, "stdin", io.StringIO(stdin)) else: monkeypatch.setattr(sys.stdin, "isatty", lambda: True) diff --git a/tox.ini b/tox.ini index 7fa6266b..1a6ced78 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,py37,py38 +envlist = py34,py35,py36,py37,py38,py39 [testenv] deps = From 7010174ccfba5b62855f93657017314806606329 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 08:39:41 -0400 Subject: [PATCH 275/393] xmlrpc: Don't add api key to passed in user dictionary We shouldn't pollute the passed in user data with any auth parameters Resolves: #132 Signed-off-by: Cole Robinson --- bugzilla/_backendxmlrpc.py | 16 ++++++++-------- tests/test_rw_functional.py | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 59c47c99..721619b5 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -121,22 +121,22 @@ def _ServerProxy__request(self, methodname, params): """ Overrides ServerProxy _request method """ - if len(params) == 0: - params = ({}, ) + # params is a singleton tuple, enforced by xmlrpc.client.dumps + newparams = params and params[0].copy() or {} - log.debug("XMLRPC call: %s(%s)", methodname, params[0]) + log.debug("XMLRPC call: %s(%s)", methodname, newparams) api_key = self.__bugzillasession.get_api_key() token_value = self.__bugzillasession.get_token_value() if api_key is not None: - if 'Bugzilla_api_key' not in params[0]: - params[0]['Bugzilla_api_key'] = api_key + if 'Bugzilla_api_key' not in newparams: + newparams['Bugzilla_api_key'] = api_key elif token_value is not None: - if 'Bugzilla_token' not in params[0]: - params[0]['Bugzilla_token'] = token_value + if 'Bugzilla_token' not in newparams: + newparams['Bugzilla_token'] = token_value # pylint: disable=no-member - ret = ServerProxy._ServerProxy__request(self, methodname, params) + ret = ServerProxy._ServerProxy__request(self, methodname, (newparams,)) # pylint: enable=no-member if isinstance(ret, dict) and 'token' in ret.keys(): diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index c3f90bf8..59e0cf90 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -900,6 +900,12 @@ def test15EnsureLoggedIn(run_cli, backends): comm = "bugzilla --ensure-logged-in query --bug_id 979546" run_cli(comm, bz) + # Test that we don't pollute the query dict with auth info + query = {"id": [1234567]} + origquery = query.copy() + bz.query(query) + assert query == origquery + def test16ModifyTags(run_cli, backends): bugid = "461686" From e1bd6a70f9c6fc1034985278861575c158c96197 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 08:53:34 -0400 Subject: [PATCH 276/393] session: Convert callers to use request() wrapper This converts to a single entry point for passing options to requests, which we will use in upcoming patches Signed-off-by: Cole Robinson --- bugzilla/_backendrest.py | 20 ++++++++++---------- bugzilla/_backendxmlrpc.py | 4 ++-- bugzilla/_session.py | 3 +++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index d2090d25..ae9592fe 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -47,19 +47,19 @@ def _handle_response(self, response): raise BugzillaError(ret["message"], code=ret["code"]) return ret - def _op(self, optype, apiurl, paramdict=None): + def _op(self, method, apiurl, paramdict=None): fullurl = os.path.join(self._url, apiurl.lstrip("/")) - log.debug("Bugzilla REST %s %s params=%s", optype, fullurl, paramdict) - session = self._bugzillasession.get_requests_session() - data = json.dumps(paramdict or {}) - - if optype == "POST": - response = session.post(fullurl, data=data) - elif optype == "PUT": - response = session.put(fullurl, data=data) + log.debug("Bugzilla REST %s %s params=%s", method, fullurl, paramdict) + + data = None + params = None + if method == "GET": + params = paramdict else: - response = session.get(fullurl, params=paramdict) + data = json.dumps(paramdict or {}) + response = self._bugzillasession.request(method, fullurl, data=data, + params=params) return self._handle_response(response) def _get(self, *args, **kwargs): diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 721619b5..db905541 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -41,8 +41,8 @@ def __request_helper(self, url, request_body): # pylint: disable=try-except-raise # pylint: disable=raise-missing-from try: - session = self.__bugzillasession.get_requests_session() - response = session.post(url, data=request_body) + response = self.__bugzillasession.request( + "POST", url, data=request_body) # We expect utf-8 from the server response.encoding = 'UTF-8' diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 70631e66..1b9a4627 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -75,3 +75,6 @@ def set_response_cookies(self, response): def get_requests_session(self): return self._session + + def request(self, *args, **kwargs): + return self._session.request(*args, **kwargs) From ff9868426e4513450622a328ee1c3f0049933214 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 09:45:35 -0400 Subject: [PATCH 277/393] requests: use PYTHONBUGZILLA_REQUESTS_TIMEOUT env variable Add a default timeout of 5 minutes, but let users override it with PYTHONBUGZILLA_REQUESTS_TIMEOUT environment variable Resolves: #135 Signed-off-by: Cole Robinson --- bugzilla/_session.py | 12 ++++++++++++ tests/utils.py | 12 ++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 1b9a4627..26ff8684 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -3,6 +3,7 @@ from logging import getLogger +import os import requests from ._compatimports import urlparse @@ -43,6 +44,14 @@ def __init__(self, url, user_agent, self._session.params["Bugzilla_api_key"] = self._api_key self._set_tokencache_param() + def _get_timeout(self): + # Default to 5 minutes. This is longer than bugzilla.redhat.com's + # apparent 3 minute timeout so shouldn't affect legitimate usage, + # but saves us from indefinite hangs + DEFAULT_TIMEOUT = 300 + envtimeout = os.environ.get("PYTHONBUGZILLA_REQUESTS_TIMEOUT") + return float(envtimeout or DEFAULT_TIMEOUT) + def get_user_agent(self): return self._user_agent def get_scheme(self): @@ -77,4 +86,7 @@ def get_requests_session(self): return self._session def request(self, *args, **kwargs): + timeout = self._get_timeout() + if "timeout" not in kwargs: + kwargs["timeout"] = timeout return self._session.request(*args, **kwargs) diff --git a/tests/utils.py b/tests/utils.py index cc738fc7..a9dd98dc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -47,16 +47,8 @@ def open_functional_bz(bzclass, url, kwargs): if kwargs.get("force_xmlrpc", False): assert bz.is_xmlrpc() is True - # Set a session timeout of 30 seconds - session = bz.get_requests_session() - origrequest = session.request - - def fake_request(*args, **kwargs): - if "timeout" not in kwargs: - kwargs["timeout"] = 60 - return origrequest(*args, **kwargs) - - session.request = fake_request + # Set a request timeout of 60 seconds + os.environ["PYTHONBUGZILLA_REQUESTS_TIMEOUT"] = "60" return bz From b247c9d8178176c9f3c3599b38874d59ef831244 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 10:20:19 -0400 Subject: [PATCH 278/393] tests: Add unit tests for more cli sub options Signed-off-by: Cole Robinson --- tests/data/clioutput/test_new2.txt | 1 + tests/data/clioutput/test_query10.txt | 1 + tests/data/mockargs/test_modify5.txt | 26 ++++++++++++++++++ tests/data/mockargs/test_new2.txt | 22 +++++++++++++++ tests/data/mockargs/test_query10.txt | 39 +++++++++++++++++++++++++++ tests/test_cli_modify.py | 25 +++++++++++++++++ tests/test_cli_new.py | 26 +++++++++++++++++- tests/test_cli_query.py | 24 +++++++++++++++++ 8 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 tests/data/clioutput/test_new2.txt create mode 100644 tests/data/clioutput/test_query10.txt create mode 100644 tests/data/mockargs/test_modify5.txt create mode 100644 tests/data/mockargs/test_new2.txt create mode 100644 tests/data/mockargs/test_query10.txt diff --git a/tests/data/clioutput/test_new2.txt b/tests/data/clioutput/test_new2.txt new file mode 100644 index 00000000..0e1e2bde --- /dev/null +++ b/tests/data/clioutput/test_new2.txt @@ -0,0 +1 @@ +#1694158 CLOSED - crobinso@redhat.com - python-bugzilla test bug for API minor_update diff --git a/tests/data/clioutput/test_query10.txt b/tests/data/clioutput/test_query10.txt new file mode 100644 index 00000000..76cb0117 --- /dev/null +++ b/tests/data/clioutput/test_query10.txt @@ -0,0 +1 @@ +#1165434 CLOSED - lvm-team@redhat.com - LVM mirrored root can deadlock dmeventd if a mirror leg is lost diff --git a/tests/data/mockargs/test_modify5.txt b/tests/data/mockargs/test_modify5.txt new file mode 100644 index 00000000..98d02d0a --- /dev/null +++ b/tests/data/mockargs/test_modify5.txt @@ -0,0 +1,26 @@ +(['1165434'], + {'alias': 'fooalias', + 'assigned_to': 'foo@example.com', + 'bar': 'foo', + 'blocks': {'add': [1234], 'remove': [1235], 'set': []}, + 'cc': {'add': ['+bar@example.com'], 'remove': ['steve@example.com']}, + 'cf_devel_whiteboard': 'DEVBOARD', + 'cf_internal_whiteboard': 'INTBOARD', + 'cf_qa_whiteboard': 'QABOARD', + 'comment_tags': ['FOOTAG'], + 'depends_on': {'add': [2234], 'remove': [2235], 'set': []}, + 'groups': {'add': ['foogroup']}, + 'keywords': {'add': ['newkeyword'], 'remove': ['byekeyword'], 'set': []}, + 'op_sys': 'windows', + 'platform': 'mips', + 'priority': 'high', + 'product': 'newproduct', + 'qa_contact': 'qa@example.com', + 'reset_assigned_to': True, + 'reset_qa_contact': True, + 'severity': 'low', + 'summary': 'newsummary', + 'target_milestone': 'beta', + 'target_release': '1.2.4', + 'url': 'https://example.com', + 'version': '1.2.3'}) diff --git a/tests/data/mockargs/test_new2.txt b/tests/data/mockargs/test_new2.txt new file mode 100644 index 00000000..5bd147e2 --- /dev/null +++ b/tests/data/mockargs/test_new2.txt @@ -0,0 +1,22 @@ +{'alias': 'somealias', + 'assigned_to': 'foo@example.com', + 'blocks': ['12345', '6789'], + 'cc': ['foo@example.com', 'bar@example.com'], + 'comment_is_private': True, + 'comment_tags': ['FOO'], + 'component': 'FOOCOMP', + 'depends_on': ['dependme'], + 'description': 'This is the first comment!\nWith newline & stuff.', + 'foo': 'bar', + 'groups': ['FOOGROUP', 'BARGROUP'], + 'keywords': ['ADDKEY'], + 'op_sys': 'linux', + 'platform': 'mips', + 'priority': 'low', + 'product': 'FOOPROD', + 'qa_contact': 'qa@example.com', + 'severity': 'high', + 'sub_components': {'FOOCOMP': ['FOOCOMP']}, + 'summary': 'Hey this is the title!', + 'url': 'https://some.example.com', + 'version': '5.6.7'} diff --git a/tests/data/mockargs/test_query10.txt b/tests/data/mockargs/test_query10.txt new file mode 100644 index 00000000..42ae8b14 --- /dev/null +++ b/tests/data/mockargs/test_query10.txt @@ -0,0 +1,39 @@ +{'alias': 'somealias', + 'assigned_to': 'bar@example.com', + 'field0-0-0': 'keywords', + 'field1-0-0': 'blocked', + 'field2-0-0': 'dependson', + 'field3-0-0': 'bug_file_loc', + 'field4-0-0': 'cf_fixed_in', + 'field5-0-0': 'flagtypes.name', + 'field6-0-0': 'status_whiteboard', + 'field7-0-0': 'cf_devel_whiteboard', + 'include_fields': ['assigned_to', 'id', 'status', 'summary'], + 'priority': ['wibble'], + 'query_format': 'advanced', + 'quicksearch': '1', + 'reporter': 'me@example.com', + 'savedsearch': '2', + 'sharer_id': '3', + 'short_desc': 'search summary', + 'sub_components': ['FOOCOMP'], + 'tag': ['+foo'], + 'target_milestone': 'bar', + 'target_release': 'foo', + 'type0-0-0': 'substring', + 'type1-0-0': 'substring', + 'type2-0-0': 'substring', + 'type3-0-0': 'sometype', + 'type4-0-0': 'substring', + 'type5-0-0': 'substring', + 'type6-0-0': 'substring', + 'type7-0-0': 'substring', + 'value0-0-0': 'FOO', + 'value1-0-0': '12345', + 'value2-0-0': '23456', + 'value3-0-0': 'https://example.com', + 'value4-0-0': '5.5.5', + 'value5-0-0': 'needinfo', + 'value6-0-0': 'FOO', + 'value7-0-0': 'DEVBOARD', + 'version': ['5.6.7']} diff --git a/tests/test_cli_modify.py b/tests/test_cli_modify.py index 6f4e88f2..1ad6328b 100644 --- a/tests/test_cli_modify.py +++ b/tests/test_cli_modify.py @@ -65,3 +65,28 @@ def test_modify(run_cli): bug_update_return={}) out = run_cli(cmd, fakebz) assert not out + + # Modify with a slew of misc opt coverage + cmd = "bugzilla modify 1165434 " + cmd += "--assigned_to foo@example.com --qa_contact qa@example.com " + cmd += "--product newproduct " + cmd += "--blocked +1234 --blocked -1235 --blocked = " + cmd += "--url https://example.com " + cmd += "--cc=+bar@example.com --cc=-steve@example.com " + cmd += "--dependson=+2234 --dependson=-2235 --dependson = " + cmd += "--groups +foogroup " + cmd += "--keywords +newkeyword --keywords=-byekeyword --keywords = " + cmd += "--os windows --arch mips " + cmd += "--priority high --severity low " + cmd += "--summary newsummary --version 1.2.3 " + cmd += "--reset-assignee --reset-qa-contact " + cmd += "--alias fooalias " + cmd += "--target_release 1.2.4 --target_milestone beta " + cmd += "--devel_whiteboard =DEVBOARD --internal_whiteboard =INTBOARD " + cmd += "--qa_whiteboard =QABOARD " + cmd += "--comment-tag FOOTAG --field bar=foo " + fakebz = tests.mockbackend.make_bz(rhbz=True, + bug_update_args="data/mockargs/test_modify5.txt", + bug_update_return={}) + out = run_cli(cmd, fakebz) + assert not out diff --git a/tests/test_cli_new.py b/tests/test_cli_new.py index 6ab25884..5e4742be 100644 --- a/tests/test_cli_new.py +++ b/tests/test_cli_new.py @@ -11,7 +11,7 @@ ############################### def test_new(run_cli): - # Bunch of options + # Test a simpler creation cmd = "bugzilla new --product FOOPROD --component FOOCOMP " cmd += "--summary 'Hey this is the title!' " cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " @@ -26,3 +26,27 @@ def test_new(run_cli): bug_get_return="data/mockreturn/test_getbug.txt") out = run_cli(cmd, fakebz) tests.utils.diff_compare(out, "data/clioutput/test_new1.txt") + + # Test every option + cmd = "bugzilla new --product FOOPROD --component FOOCOMP " + cmd += "--summary 'Hey this is the title!' " + cmd += "--comment 'This is the first comment!\nWith newline & stuff.' " + cmd += "--keywords ADDKEY --groups FOOGROUP,BARGROUP " + cmd += "--blocked 12345,6789 --cc foo@example.com --cc bar@example.com " + cmd += "--dependson dependme --private " + cmd += "--os linux --arch mips --severity high --priority low " + cmd += "--url https://some.example.com " + cmd += "--version 5.6.7 --alias somealias " + cmd += "--sub-component FOOCOMP " + cmd += "--assignee foo@example.com --qa_contact qa@example.com " + cmd += "--comment-tag FOO " + cmd += "--field foo=bar " + + fakebz = tests.mockbackend.make_bz( + bug_create_args="data/mockargs/test_new2.txt", + bug_create_return={"id": 1694158}, + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug.txt", + rhbz=True) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_new2.txt") diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py index 3fa4cc85..98c6028b 100644 --- a/tests/test_cli_query.py +++ b/tests/test_cli_query.py @@ -147,3 +147,27 @@ def test_query(run_cli): tests.utils.diff_compare(tests.utils.sanitize_json(out), "data/clioutput/test_query9.txt") assert json.loads(out) + + + # Test every remaining option + cmd = "bugzilla query " + cmd += "--sub-component FOOCOMP " + cmd += "--version 5.6.7 --reporter me@example.com " + cmd += "--summary 'search summary' " + cmd += "--assignee bar@example.com " + cmd += "--blocked 12345 --dependson 23456 " + cmd += "--keywords FOO --keywords_type substring " + cmd += "--url https://example.com --url_type sometype " + cmd += "--target_release foo --target_milestone bar " + cmd += "--quicksearch 1 --savedsearch 2 --savedsearch-sharer-id 3 " + cmd += "--tags +foo --flag needinfo --alias somealias " + cmd += "--devel_whiteboard DEVBOARD " + cmd += "--priority wibble " + cmd += "--fixed_in 5.5.5 --fixed_in_type substring " + cmd += "--whiteboard FOO --status_whiteboard_type substring " + fakebz = tests.mockbackend.make_bz( + bug_search_args="data/mockargs/test_query10.txt", + bug_search_return="data/mockreturn/test_getbug_rhel.txt", + rhbz=True) + out = run_cli(cmd, fakebz) + tests.utils.diff_compare(out, "data/clioutput/test_query10.txt") From af77a3ef5ca302d9c8eace5b993b85f054e04732 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 11:06:59 -0400 Subject: [PATCH 279/393] cli: Reorg code to make it easier to check cli test coverage Using a conditional for each option makes it clear in coverage annotations whether we are testing the option or not Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 330 +++++++++++++++++++++++++++++++---------------- 1 file changed, 216 insertions(+), 114 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index e529004b..1fd0767e 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -432,12 +432,9 @@ def setup_parser(): # Command routines # #################### -def _merge_field_opts(query, opt, parser): +def _merge_field_opts(query, fields, parser): # Add any custom fields if specified - if opt.fields is None: - return - - for f in opt.fields: + for f in fields: try: f, v = f.split('=', 1) query[f] = v @@ -523,45 +520,83 @@ def _do_query(bz, opt, parser): if include_fields is not None: include_fields.sort() - built_query = bz.build_query( - product=opt.product or None, - component=opt.component or None, - sub_component=opt.sub_component or None, - version=opt.version or None, - reporter=opt.reporter or None, - bug_id=opt.id or None, - short_desc=opt.summary or None, - long_desc=opt.comment or None, - cc=opt.cc or None, - assigned_to=opt.assigned_to or None, - qa_contact=opt.qa_contact or None, - status=opt.status or None, - blocked=opt.blocked or None, - dependson=opt.dependson or None, - keywords=opt.keywords or None, - keywords_type=opt.keywords_type or None, - url=opt.url or None, - url_type=opt.url_type or None, - status_whiteboard=opt.whiteboard or None, - status_whiteboard_type=opt.status_whiteboard_type or None, - fixed_in=opt.fixed_in or None, - fixed_in_type=opt.fixed_in_type or None, - flag=opt.flag or None, - alias=opt.alias or None, - qa_whiteboard=opt.qa_whiteboard or None, - devel_whiteboard=opt.devel_whiteboard or None, - bug_severity=opt.severity or None, - priority=opt.priority or None, - target_release=opt.target_release or None, - target_milestone=opt.target_milestone or None, - emailtype=opt.emailtype or None, - include_fields=include_fields, - quicksearch=opt.quicksearch or None, - savedsearch=opt.savedsearch or None, - savedsearch_sharer_id=opt.savedsearch_sharer_id or None, - tags=opt.tags or None) - - _merge_field_opts(built_query, opt, parser) + kwopts = {} + if opt.product: + kwopts["product"] = opt.product + if opt.component: + kwopts["component"] = opt.component + if opt.sub_component: + kwopts["sub_component"] = opt.sub_component + if opt.version: + kwopts["version"] = opt.version + if opt.reporter: + kwopts["reporter"] = opt.reporter + if opt.id: + kwopts["bug_id"] = opt.id + if opt.summary: + kwopts["short_desc"] = opt.summary + if opt.comment: + kwopts["long_desc"] = opt.comment + if opt.cc: + kwopts["cc"] = opt.cc + if opt.assigned_to: + kwopts["assigned_to"] = opt.assigned_to + if opt.qa_contact: + kwopts["qa_contact"] = opt.qa_contact + if opt.status: + kwopts["status"] = opt.status + if opt.blocked: + kwopts["blocked"] = opt.blocked + if opt.dependson: + kwopts["dependson"] = opt.dependson + if opt.keywords: + kwopts["keywords"] = opt.keywords + if opt.keywords_type: + kwopts["keywords_type"] = opt.keywords_type + if opt.url: + kwopts["url"] = opt.url + if opt.url_type: + kwopts["url_type"] = opt.url_type + if opt.whiteboard: + kwopts["status_whiteboard"] = opt.whiteboard + if opt.status_whiteboard_type: + kwopts["status_whiteboard_type"] = opt.status_whiteboard_type + if opt.fixed_in: + kwopts["fixed_in"] = opt.fixed_in + if opt.fixed_in_type: + kwopts["fixed_in_type"] = opt.fixed_in_type + if opt.flag: + kwopts["flag"] = opt.flag + if opt.alias: + kwopts["alias"] = opt.alias + if opt.qa_whiteboard: + kwopts["qa_whiteboard"] = opt.qa_whiteboard + if opt.devel_whiteboard: + kwopts["devel_whiteboard"] = opt.devel_whiteboard + if opt.severity: + kwopts["bug_severity"] = opt.severity + if opt.priority: + kwopts["priority"] = opt.priority + if opt.target_release: + kwopts["target_release"] = opt.target_release + if opt.target_milestone: + kwopts["target_milestone"] = opt.target_milestone + if opt.emailtype: + kwopts["emailtype"] = opt.emailtype + if include_fields: + kwopts["include_fields"] = include_fields + if opt.quicksearch: + kwopts["quicksearch"] = opt.quicksearch + if opt.savedsearch: + kwopts["savedsearch"] = opt.savedsearch + if opt.savedsearch_sharer_id: + kwopts["savedsearch_sharer_id"] = opt.savedsearch_sharer_id + if opt.tags: + kwopts["tags"] = opt.tags + + built_query = bz.build_query(**kwopts) + if opt.fields: + _merge_field_opts(built_query, opt.fields, parser) built_query.update(q) q = built_query @@ -823,31 +858,53 @@ def parse_multi(val): return _parse_triset(val, checkplus=False, checkminus=False, checkequal=False, splitcomma=True)[0] - ret = bz.build_createbug( - blocks=parse_multi(opt.blocked) or None, - cc=parse_multi(opt.cc) or None, - component=opt.component or None, - depends_on=parse_multi(opt.dependson) or None, - description=opt.comment or None, - groups=parse_multi(opt.groups) or None, - keywords=parse_multi(opt.keywords) or None, - op_sys=opt.os or None, - platform=opt.arch or None, - priority=opt.priority or None, - product=opt.product or None, - severity=opt.severity or None, - summary=opt.summary or None, - url=opt.url or None, - version=opt.version or None, - assigned_to=opt.assigned_to or None, - qa_contact=opt.qa_contact or None, - sub_component=opt.sub_component or None, - alias=opt.alias or None, - comment_tags=opt.comment_tag or None, - comment_private=opt.private or None, - ) - - _merge_field_opts(ret, opt, parser) + kwopts = {} + if opt.blocked: + kwopts["blocks"] = parse_multi(opt.blocked) + if opt.cc: + kwopts["cc"] = parse_multi(opt.cc) + if opt.component: + kwopts["component"] = opt.component + if opt.dependson: + kwopts["depends_on"] = parse_multi(opt.dependson) + if opt.comment: + kwopts["description"] = opt.comment + if opt.groups: + kwopts["groups"] = parse_multi(opt.groups) + if opt.keywords: + kwopts["keywords"] = parse_multi(opt.keywords) + if opt.os: + kwopts["op_sys"] = opt.os + if opt.arch: + kwopts["platform"] = opt.arch + if opt.priority: + kwopts["priority"] = opt.priority + if opt.product: + kwopts["product"] = opt.product + if opt.severity: + kwopts["severity"] = opt.severity + if opt.summary: + kwopts["summary"] = opt.summary + if opt.url: + kwopts["url"] = opt.url + if opt.version: + kwopts["version"] = opt.version + if opt.assigned_to: + kwopts["assigned_to"] = opt.assigned_to + if opt.qa_contact: + kwopts["qa_contact"] = opt.qa_contact + if opt.sub_component: + kwopts["sub_component"] = opt.sub_component + if opt.alias: + kwopts["alias"] = opt.alias + if opt.comment_tag: + kwopts["comment_tags"] = opt.comment_tag + if opt.private: + kwopts["comment_private"] = opt.private + + ret = bz.build_createbug(**kwopts) + if opt.fields: + _merge_field_opts(ret, opt.fields, parser) b = bz.createbug(ret) b.refresh() @@ -885,50 +942,94 @@ def _do_modify(bz, parser, opt): for f in opt.flag: flags.append({"name": f[:-1], "status": f[-1]}) - update = bz.build_update( - assigned_to=opt.assigned_to or None, - comment=opt.comment or None, - comment_private=opt.private or None, - component=opt.component or None, - product=opt.product or None, - blocks_add=add_blk or None, - blocks_remove=rm_blk or None, - blocks_set=set_blk, - url=opt.url or None, - cc_add=add_cc or None, - cc_remove=rm_cc or None, - depends_on_add=add_deps or None, - depends_on_remove=rm_deps or None, - depends_on_set=set_deps, - groups_add=add_groups or None, - groups_remove=rm_groups or None, - keywords_add=add_key or None, - keywords_remove=rm_key or None, - keywords_set=set_key, - op_sys=opt.os or None, - platform=opt.arch or None, - priority=opt.priority or None, - qa_contact=opt.qa_contact or None, - severity=opt.severity or None, - status=status, - summary=opt.summary or None, - version=opt.version or None, - reset_assigned_to=opt.reset_assignee or None, - reset_qa_contact=opt.reset_qa_contact or None, - resolution=opt.close or None, - target_release=opt.target_release or None, - target_milestone=opt.target_milestone or None, - dupe_of=opt.dupeid or None, - fixed_in=opt.fixed_in or None, - whiteboard=set_wb and set_wb[0] or None, - devel_whiteboard=set_devwb and set_devwb[0] or None, - internal_whiteboard=set_intwb and set_intwb[0] or None, - qa_whiteboard=set_qawb and set_qawb[0] or None, - sub_component=opt.sub_component or None, - alias=opt.alias or None, - flags=flags or None, - comment_tags=opt.comment_tag or None, - ) + update_opts = {} + + if opt.assigned_to: + update_opts["assigned_to"] = opt.assigned_to + if opt.comment: + update_opts["comment"] = opt.comment + if opt.private: + update_opts["comment_private"] = opt.private + if opt.component: + update_opts["component"] = opt.component + if opt.product: + update_opts["product"] = opt.product + if add_blk: + update_opts["blocks_add"] = add_blk + if rm_blk: + update_opts["blocks_remove"] = rm_blk + if set_blk is not None: + update_opts["blocks_set"] = set_blk + if opt.url: + update_opts["url"] = opt.url + if add_cc: + update_opts["cc_add"] = add_cc + if rm_cc: + update_opts["cc_remove"] = rm_cc + if add_deps: + update_opts["depends_on_add"] = add_deps + if rm_deps: + update_opts["depends_on_remove"] = rm_deps + if set_deps is not None: + update_opts["depends_on_set"] = set_deps + if add_groups: + update_opts["groups_add"] = add_groups + if rm_groups: + update_opts["groups_remove"] = rm_groups + if add_key: + update_opts["keywords_add"] = add_key + if rm_key: + update_opts["keywords_remove"] = rm_key + if set_key is not None: + update_opts["keywords_set"] = set_key + if opt.os: + update_opts["op_sys"] = opt.os + if opt.arch: + update_opts["platform"] = opt.arch + if opt.priority: + update_opts["priority"] = opt.priority + if opt.qa_contact: + update_opts["qa_contact"] = opt.qa_contact + if opt.severity: + update_opts["severity"] = opt.severity + if status: + update_opts["status"] = status + if opt.summary: + update_opts["summary"] = opt.summary + if opt.version: + update_opts["version"] = opt.version + if opt.reset_assignee: + update_opts["reset_assigned_to"] = opt.reset_assignee + if opt.reset_qa_contact: + update_opts["reset_qa_contact"] = opt.reset_qa_contact + if opt.close: + update_opts["resolution"] = opt.close + if opt.target_release: + update_opts["target_release"] = opt.target_release + if opt.target_milestone: + update_opts["target_milestone"] = opt.target_milestone + if opt.dupeid: + update_opts["dupe_of"] = opt.dupeid + if opt.fixed_in: + update_opts["fixed_in"] = opt.fixed_in + if set_wb and set_wb[0]: + update_opts["whiteboard"] = set_wb and set_wb[0] + if set_devwb and set_devwb[0]: + update_opts["devel_whiteboard"] = set_devwb and set_devwb[0] + if set_intwb and set_intwb[0]: + update_opts["internal_whiteboard"] = set_intwb and set_intwb[0] + if set_qawb and set_qawb[0]: + update_opts["qa_whiteboard"] = set_qawb and set_qawb[0] + if opt.sub_component: + update_opts["sub_component"] = opt.sub_component + if opt.alias: + update_opts["alias"] = opt.alias + if flags: + update_opts["flags"] = flags + if opt.comment_tag: + update_opts["comment_tags"] = opt.comment_tag + + update = bz.build_update(**update_opts) # We make this a little convoluted to facilitate unit testing wbmap = { @@ -942,7 +1043,8 @@ def _do_modify(bz, parser, opt): if not v[0] and not v[1]: del(wbmap[k]) - _merge_field_opts(update, opt, parser) + if opt.fields: + _merge_field_opts(update, opt.fields, parser) log.debug("update bug dict=%s", update) log.debug("update whiteboard dict=%s", wbmap) From 60b42ae924c9514e04a83628521d4e4db3eded47 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 12:06:16 -0400 Subject: [PATCH 280/393] tests: Run regular test suite alongside functional tests This lets us check the whole code coverage more easily Signed-off-by: Cole Robinson --- tests/conftest.py | 20 +++++++------------- tests/test_api_authfiles.py | 5 ++++- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7481f0fc..0ea42d39 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,18 +41,13 @@ def pytest_addoption(parser): def pytest_ignore_collect(path, config): has_ro = config.getoption("--ro-functional") has_rw = config.getoption("--rw-functional") - skip_rest = has_ro or has_rw base = os.path.basename(str(path)) is_ro = base == "test_ro_functional.py" is_rw = base == "test_rw_functional.py" - if is_ro or is_rw: - if is_ro and not has_ro: - return True - if is_rw and not has_rw: - return True - elif skip_rest: - config.option.verbose = 2 + if is_ro and not has_ro: + return True + if is_rw and not has_rw: return True @@ -72,16 +67,15 @@ def pytest_configure(config): if config.getoption("--regenerate-output"): tests.CLICONFIG.REGENERATE_OUTPUT = config.getoption( "--regenerate-output") - if not (config.getoption("--ro-functional") or - config.getoption("--rw-functional")): - # Functional tests need access to HOME cached auth. - # Unit tests shouldn't be touching any HOME files - os.environ["HOME"] = os.path.dirname(__file__) + "/data/homedir" if config.getoption("--only-rest"): tests.CLICONFIG.ONLY_REST = True if config.getoption("--only-xmlrpc"): tests.CLICONFIG.ONLY_XMLRPC = True + if (config.getoption("--ro-functional") or + config.getoption("--rw-functional")): + config.option.verbose = 2 + def pytest_generate_tests(metafunc): """ diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py index 2ea411a0..00318613 100644 --- a/tests/test_api_authfiles.py +++ b/tests/test_api_authfiles.py @@ -23,7 +23,10 @@ import tests.utils -def testCookies(): +def testCookies(monkeypatch): + monkeypatch.setitem(os.environ, "HOME", + os.path.dirname(__file__) + "/data/homedir") + dirname = os.path.dirname(__file__) cookiesbad = dirname + "/data/cookies-bad.txt" cookieslwp = dirname + "/data/cookies-lwp.txt" From 14849bde351d6c6c83cec8394a4b41fed7695a4e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 12:51:12 -0400 Subject: [PATCH 281/393] cli: Add `modify --minor-update` option This adds support for the minor_update bugzilla option that is in bugzilla.git: https://github.com/bugzilla/bugzilla/commit/1d96fa1 It disables sending email notifications for the bug change Resolves: #94 Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 6 ++++++ bugzilla/base.py | 4 +++- man/bugzilla.rst | 4 ++++ tests/data/mockargs/test_modify5.txt | 1 + tests/test_cli_modify.py | 1 + tests/test_rw_functional.py | 2 +- 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 1fd0767e..84d516be 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -231,6 +231,10 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('--cc', action="append", help="CC list") p.add_argument('-a', '--assigned_to', '--assignee', help="Bug assignee") p.add_argument('-q', '--qa_contact', help='QA contact') + if cmd_modify: + p.add_argument("--minor-update", action="store_true", + help="Request bugzilla to not send any " + "email about this change") if not cmd_new: p.add_argument('-f', '--flag', action='append', @@ -1028,6 +1032,8 @@ def _do_modify(bz, parser, opt): update_opts["flags"] = flags if opt.comment_tag: update_opts["comment_tags"] = opt.comment_tag + if opt.minor_update: + update_opts["minor_update"] = opt.minor_update update = bz.build_update(**update_opts) diff --git a/bugzilla/base.py b/bugzilla/base.py index 76664d90..924b230a 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1444,7 +1444,8 @@ def build_update(self, internal_whiteboard=None, sub_component=None, flags=None, - comment_tags=None): + comment_tags=None, + minor_update=None): """ Returns a python dict() with properly formatted parameters to pass to update_bugs(). See bugzilla documentation for the format @@ -1531,6 +1532,7 @@ def c(val): s("work_time", work_time, float) s("flags", flags) s("comment_tags", comment_tags, listify) + s("minor_update", minor_update, bool) add_dict("blocks", blocks_add, blocks_remove, blocks_set, convert=int) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index c456d6da..b7850954 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -396,6 +396,10 @@ Reset assignee to component default Reset QA contact to component default +- ``--minor-update`` + +Request bugzilla to not send any email about this change + ‘new’ specific options ====================== diff --git a/tests/data/mockargs/test_modify5.txt b/tests/data/mockargs/test_modify5.txt index 98d02d0a..972c2765 100644 --- a/tests/data/mockargs/test_modify5.txt +++ b/tests/data/mockargs/test_modify5.txt @@ -11,6 +11,7 @@ 'depends_on': {'add': [2234], 'remove': [2235], 'set': []}, 'groups': {'add': ['foogroup']}, 'keywords': {'add': ['newkeyword'], 'remove': ['byekeyword'], 'set': []}, + 'minor_update': True, 'op_sys': 'windows', 'platform': 'mips', 'priority': 'high', diff --git a/tests/test_cli_modify.py b/tests/test_cli_modify.py index 1ad6328b..ed5519c3 100644 --- a/tests/test_cli_modify.py +++ b/tests/test_cli_modify.py @@ -85,6 +85,7 @@ def test_modify(run_cli): cmd += "--devel_whiteboard =DEVBOARD --internal_whiteboard =INTBOARD " cmd += "--qa_whiteboard =QABOARD " cmd += "--comment-tag FOOTAG --field bar=foo " + cmd += "--minor-update " fakebz = tests.mockbackend.make_bz(rhbz=True, bug_update_args="data/mockargs/test_modify5.txt", bug_update_return={}) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 59e0cf90..6bd62d31 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -96,7 +96,7 @@ def test03NewBugBasic(run_cli, backends): assert hasattr(bug, "bug_id") # Close the bug - run_cli("bugzilla modify --close NOTABUG %s" % bug.id, bz) + run_cli("bugzilla modify --close NOTABUG %s --minor-update" % bug.id, bz) bug.refresh() assert bug.status == "CLOSED" assert bug.resolution == "NOTABUG" From 82972796cf04f1ac06525670c272465a66d77da1 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 13:04:44 -0400 Subject: [PATCH 282/393] man: Make .rst options linkable on github Signed-off-by: Cole Robinson --- man/bugzilla.rst | 489 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 407 insertions(+), 82 deletions(-) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index b7850954..08c14dab 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -32,14 +32,22 @@ instance over REST or XMLRPC. | * info - get info about the given bugzilla instance + GLOBAL OPTIONS --------------- +============== + +``--help, -h`` +^^^^^^^^^^^^^^ -- ``--help, -h`` +**Syntax:** ``-h`` show this help message and exit -- ``--bugzilla=BUGZILLA`` + +``--bugzilla`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--bugzilla`` BUGZILLA The bugzilla URL. Full API URLs are typically like: @@ -54,252 +62,470 @@ preferring XMLRPC for backwards compatibility. The default URL https://bugzilla.redhat.com -- ``--nosslverify`` + +``--nosslverify`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--nosslverify`` Don't error on invalid bugzilla SSL certificate -- ``--cert=CERTFILE`` + +``--cert`` +^^^^^^^^^^ + +**Syntax:** ``--cert`` CERTFILE client side certificate file needed by the webserver. -- ``--login`` + +``--login`` +^^^^^^^^^^^ + +**Syntax:** ``--login`` Run interactive "login" before performing the specified command. -- ``--username=USERNAME`` + +``--username`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--username`` USERNAME Log in with this username -- ``--password=PASSWORD`` + +``--password`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--password`` PASSWORD Log in with this password -- ``--restrict-login`` + +``--restrict-login`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--restrict-login`` The session (login token) will be restricted to the current IP address. -- ``--ensure-logged-in`` + +``--ensure-logged-in`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--ensure-logged-in`` Raise an error if we aren't logged in to bugzilla. Consider using this if you are depending on cached credentials, to ensure that when they expire the tool errors, rather than subtly change output. -- ``--no-cache-credentials`` + +``--no-cache-credentials`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--no-cache-credentials`` Don't save any bugzilla cookies or tokens to disk, and don't use any pre-existing credentials. -- ``--cookiefile=COOKIEFILE`` + +``--cookiefile`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--cookiefile`` COOKIEFILE cookie file to use for bugzilla authentication -- ``--tokenfile=TOKENFILE`` + +``--tokenfile`` +^^^^^^^^^^^^^^^ + +**Syntax:** ``--tokenfile`` TOKENFILE token file to use for bugzilla authentication -- ``--verbose`` + +``--verbose`` +^^^^^^^^^^^^^ + +**Syntax:** ``--verbose`` give more info about what's going on -- ``--debug`` + +``--debug`` +^^^^^^^^^^^ + +**Syntax:** ``--debug`` output bunches of debugging info -- ``--version`` + +``--version`` +^^^^^^^^^^^^^ + +**Syntax:** ``--version`` show program's version number and exit + Standard bugzilla options ========================= These options are shared by some combination of the 'new', 'query', and 'modify' sub commands. Not every option works for each command though. -- ``--product=PRODUCT, -p PRODUCT`` + +``-p, --product`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--product`` PRODUCT Product name -- ``--version=VERSION, -v VERSION`` + +``-v, --version`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--version`` VERSION Product version -- ``--component=COMPONENT, -c COMPONENT`` + +``-c, --component`` +^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--component`` COMPONENT Component name -- ``--summary=SUMMARY, -s SUMMARY, --short_desc=SUMMARY`` + +``-s, --summary`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--summary`` SUMMARY Bug summary -- ``--comment=DESCRIPTION, -l DESCRIPTION`` + +``-l, --comment`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--comment`` DESCRIPTION Set initial bug comment/description -- ``--comment-tag=TAG`` + +``--comment-tag`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--comment-tag`` TAG Comment tag for the new comment -- ``--sub-component=SUB_COMPONENT`` + +``--sub-component`` +^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--sub-component`` SUB_COMPONENT RHBZ sub component name -- ``--os=OS, -o OS`` + +``-o, --os`` +^^^^^^^^^^^^ + +**Syntax:** ``--os`` OS Operating system -- ``--arch=ARCH`` + +``--arch`` +^^^^^^^^^^ + +**Syntax:** ``--arch`` ARCH Arch this bug occurs on -- ``--severity=SEVERITY, -x SEVERITY`` + +``-x, --severity`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--severity`` SEVERITY Bug severity -- ``--priority=PRIORITY, -z PRIORITY`` + +``-z, --priority`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--priority`` PRIORITY Bug priority -- ``--alias=ALIAS`` + +``--alias`` +^^^^^^^^^^^ + +**Syntax:** ``--alias`` ALIAS Bug alias (name) -- ``--status=STATUS, -s STATUS, --bug_status=STATUS`` + +``-s, --status`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--status`` STATUS Bug status (NEW, ASSIGNED, etc.) -- ``--url=URL, -u URL`` + +``-u, --url`` +^^^^^^^^^^^^^ + +**Syntax:** ``--url`` URL URL for further bug info -- ``--target_milestone=TARGET_MILESTONE, -m TARGET_MILESTONE`` + +``-m --target_milestone`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--target_milestone`` TARGET_MILESTONE Target milestone -- ``--target_release=TARGET_RELEASE`` + +``--target_release`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--target_release`` TARGET_RELEASE RHBZ Target release -- ``--blocked=BUGID[, BUGID, ...]`` + +``--blocked`` +^^^^^^^^^^^^^ + +**Syntax:** ``...]`` Bug IDs that this bug blocks -- ``--dependson=BUGID[, BUGID, ...]`` + +``--dependson`` +^^^^^^^^^^^^^^^ + +**Syntax:** ``...]`` Bug IDs that this bug depends on -- ``--keywords=KEYWORD[, KEYWORD, ...]`` + +``--keywords`` +^^^^^^^^^^^^^^ + +**Syntax:** ``...]`` Bug keywords -- ``--groups=GROUP[, GROUP, ...]`` + +``--groups`` +^^^^^^^^^^^^ + +**Syntax:** ``...]`` Which user groups can view this bug -- ``--cc=CC[, CC, ...]`` + +``--cc`` +^^^^^^^^ + +**Syntax:** ``...]`` CC list -- ``--assigned_to=ASSIGNED_TO, -a ASSIGNED_TO, --assignee ASSIGNED_TO`` + +``-a, --assignee, --assigned_to`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--assigned_to`` ASSIGNED_TO Bug assignee -- ``--qa_contact=QA_CONTACT, -q QA_CONTACT`` + +``-q, --qa_contact`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--qa_contact`` QA_CONTACT QA contact -- ``--flag=FLAG`` + +``--flag`` +^^^^^^^^^^ + +**Syntax:** ``--flag`` FLAG Set or unset a flag. For example, to set a flag named devel_ack, do --flag devel_ack+ Unset a flag with the 'X' value, like --flag needinfoX -- ``--tags=TAG`` + +``--tags`` +^^^^^^^^^^ + +**Syntax:** ``--tags`` TAG Set (personal) tags field -- ``--whiteboard WHITEBOARD, -w WHITEBOARD, --status_whiteboard WHITEBOARD`` + +``-w, --whiteboard`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--whiteboard`` WHITEBOARD Whiteboard field -- ``--devel_whiteboard DEVEL_WHITEBOARD`` + +``--devel_whiteboard`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--devel_whiteboard`` DEVEL_WHITEBOARD RHBZ devel whiteboard field -- ``--internal_whiteboard INTERNAL_WHITEBOARD`` + +``--internal_whiteboard`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--internal_whiteboard`` INTERNAL_WHITEBOARD RHBZ internal whiteboard field -- ``--qa_whiteboard QA_WHITEBOARD`` + +``--qa_whiteboard`` +^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--qa_whiteboard`` QA_WHITEBOARD RHBZ QA whiteboard field -- ``--fixed_in FIXED_IN, -F FIXED_IN`` + +``-F, --fixed_in`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--fixed_in`` FIXED_IN RHBZ 'Fixed in version' field -- ``--field=FIELD=VALUE`` + +``--field`` +^^^^^^^^^^^ + +**Syntax:** ``--field`` FIELD`` VALUE Manually specify a bugzilla API field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE + Output options ============== These options are shared by several commands, for tweaking the text output of the command results. -- ``--full, -f`` + +``-f, --full`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--full`` output detailed bug info -- ``--ids, -i`` + +``-i, --ids`` +^^^^^^^^^^^^^ + +**Syntax:** ``--ids`` output only bug IDs -- ``--extra, -e`` + +``-e, --extra`` +^^^^^^^^^^^^^^^ + +**Syntax:** ``--extra`` output additional bug information (keywords, Whiteboards, etc.) -- ``--oneline`` + +``--oneline`` +^^^^^^^^^^^^^ + +**Syntax:** ``--oneline`` one line summary of the bug (useful for scripts) -- ``--json`` + +``--json`` +^^^^^^^^^^ + +**Syntax:** ``--json`` output bug contents in JSON format -- ``--includefield`` + +``--includefield`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--includefield`` Pass the field name to bugzilla include_fields list. Only the fields passed to include_fields are returned by the bugzilla server. This can be specified multiple times. -- ``--extrafield`` + +``--extrafield`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--extrafield`` Pass the field name to bugzilla extra_fields list. When used with --json this can be used to request bugzilla to return values for non-default fields. This can be specified multiple times. -- ``--excludefield`` + +``--excludefield`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--excludefield`` Pass the field name to bugzilla exclude_fields list. When used with --json this can be used to request bugzilla to not return values for a field. This can be specified multiple times. -- ``--raw`` + +``--raw`` +^^^^^^^^^ + +**Syntax:** ``--raw`` raw output of the bugzilla contents. This format is unstable and difficult to parse. Please use the ``--json`` instead if you want maximum output from the `bugzilla` -- ``--outputformat=OUTPUTFORMAT`` + +``--outputformat`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--outputformat`` OUTPUTFORMAT Print output in the form given. You can use RPM-style tags that match bug fields, e.g.: '%{id}: %{summary}'. @@ -333,29 +559,53 @@ Note: querying via explicit command line options will only get you so far. See the --from-url option for a way to use powerful Web UI queries from the command line. -- ``--id ID, -b ID, --bug_id ID`` + +``-b, --bug_id, --id`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--id`` ID specify individual bugs by IDs, separated with commas -- ``--reporter REPORTER, -r REPORTER`` + +``-r, --reporter`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--reporter`` REPORTER Email: search reporter email for given address -- ``--quicksearch QUICKSEARCH`` + +``--quicksearch`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--quicksearch`` QUICKSEARCH Search using bugzilla's quicksearch functionality. -- ``--savedsearch SAVEDSEARCH`` + +``--savedsearch`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--savedsearch`` SAVEDSEARCH Name of a bugzilla saved search. If you don't own this saved search, you must passed --savedsearch_sharer_id. -- ``--savedsearch-sharer-id SAVEDSEARCH_SHARER_ID`` + +``--savedsearch-sharer-id`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--savedsearch-sharer-id`` SAVEDSEARCH_SHARER_ID Owner ID of the --savedsearch. You can get this ID from the URL bugzilla generates when running the saved search from the web UI. -- ``--from-url WEB_QUERY_URL`` + +``--from-url`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--from-url`` WEB_QUERY_URL Make a working query via bugzilla's 'Advanced search' web UI, grab the url from your browser (the string with query.cgi or buglist.cgi @@ -376,67 +626,123 @@ Fields that take multiple values have a special input format. Options that accept this format: --cc, --blocked, --dependson, --groups, --tags, whiteboard fields. -- ``--close RESOLUTION, -k RESOLUTION`` + +``-k, --close RESOLUTION`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``RESOLUTION`` Close with the given resolution (WONTFIX, NOTABUG, etc.) -- ``--dupeid ORIGINAL, -d ORIGINAL`` + +``-d, --dupeid`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--dupeid`` ORIGINAL ID of original bug. Implies --close DUPLICATE -- ``--private`` + +``--private`` +^^^^^^^^^^^^^ + +**Syntax:** ``--private`` Mark new comment as private -- ``--reset-assignee`` + +``--reset-assignee`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--reset-assignee`` Reset assignee to component default -- ``--reset-qa-contact`` + +``--reset-qa-contact`` +^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--reset-qa-contact`` Reset QA contact to component default -- ``--minor-update`` + +``--minor-update`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--minor-update`` Request bugzilla to not send any email about this change + ‘new’ specific options ====================== -- ``--private`` +``--private`` +^^^^^^^^^^^^^ + +**Syntax:** ``--private`` Mark new comment as private + ‘attach’ options ================ -- ``--file=FILENAME, -f FILENAME`` +``-f, --file`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--file`` FILENAME File to attach, or filename for data provided on stdin -- ``--description=DESCRIPTION, -d DESCRIPTION`` + +``-d, --description`` +^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--description`` DESCRIPTION A short description of the file being attached -- ``--type=MIMETYPE, -t MIMETYPE`` + +``-t, --type`` +^^^^^^^^^^^^^^ + +**Syntax:** ``--type`` MIMETYPE Mime-type for the file being attached -- ``--get=ATTACHID, -g ATTACHID`` + +``-g, --get`` +^^^^^^^^^^^^^ + +**Syntax:** ``--get`` ATTACHID Download the attachment with the given ID -- ``--getall=BUGID, --get-all=BUGID`` + +``--getall`` +^^^^^^^^^^^^ + +**Syntax:** ``--getall`` BUGID Download all attachments on the given bug -- ``--ignore-obsolete`` + +``--ignore-obsolete`` +^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--ignore-obsolete`` Do not download attachments marked as obsolete. -- ``--comment=COMMENT, -l COMMENT`` + +``-l, --comment`` +^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--comment`` COMMENT Add comment with attachment @@ -444,23 +750,42 @@ Add comment with attachment ‘info’ options ============== -- ``--products, -p`` +``-p, --products`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--products`` Get a list of products -- ``--components=PRODUCT, -c PRODUCT`` + +``-c, --components`` +^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--components`` PRODUCT List the components in the given product -- ``--component_owners=PRODUCT, -o PRODUCT`` + +``-o, --component_owners`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--component_owners`` PRODUCT List components (and their owners) -- ``--versions=PRODUCT, -v PRODUCT`` + +``-v, --versions`` +^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--versions`` PRODUCT List the versions for the given product -- ``--active-components`` + +``--active-components`` +^^^^^^^^^^^^^^^^^^^^^^^ + +**Syntax:** ``--active-components`` Only show active components. Combine with --components* From 0e2885424bc3f6c44dba526963bd9610069f6472 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 15:31:26 -0400 Subject: [PATCH 283/393] ci: Drop python2 and epel7 test, add py3.9 Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 005b6a2c..a55fe894 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -71,5 +71,4 @@ jobs: run: | ./setup.py rpm - - run: mock --root epel-7-x86_64 *.src.rpm - run: mock --root epel-8-x86_64 *.src.rpm From 716101c6c8300ee9e0bec21172f3c49c65a5c359 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 16:50:21 -0400 Subject: [PATCH 284/393] ci: Drop 3.9, it isn't GA yet Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a55fe894..e300acb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.5, 3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 From fb75633440e85a5d46d6d91c9a6ad90eabf64933 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 17:06:48 -0400 Subject: [PATCH 285/393] tests: Drop now redundant utf-8 file encodings No longer needed after dropping python2 support Signed-off-by: Cole Robinson --- tests/test_cli_attach.py | 2 -- tests/test_ro_functional.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/test_cli_attach.py b/tests/test_cli_attach.py index e0427668..2975d106 100644 --- a/tests/test_cli_attach.py +++ b/tests/test_cli_attach.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index f423d4a3..4c3ec476 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- - # # Copyright Red Hat, Inc. 2012 # From 24dea740bbb830ba3f2dd6cb604e553b4e8bb1f8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 17:07:10 -0400 Subject: [PATCH 286/393] _backendrest: Fix json parsing on python 3.4 encoding() is unnecessary here and bytes() is rejected on python3.4 Signed-off-by: Cole Robinson --- bugzilla/_backendrest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index ae9592fe..43a503f6 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -35,7 +35,7 @@ def __init__(self, url, bugzillasession): def _handle_response(self, response): response.raise_for_status() - text = response.text.encode("utf-8") + text = response.text try: ret = dict(json.loads(text)) From 8c62f1498378d15d8b06bf2c86f0fe654e952d2e Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sat, 3 Oct 2020 18:50:28 -0400 Subject: [PATCH 287/393] Prep for release 3.0.0 Signed-off-by: Cole Robinson --- NEWS.md | 6 ++++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 78190215..90f5ba77 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # python-bugzilla release news +## Release 3.0.0 (October 03, 2020) +- Drop python2 support +- New option `bugzilla modify --minor-update option` +- requests: use PYTHONBUGZILLA_REQUESTS_TIMEOUT env variable +- xmlrpc: Don't add api key to passed in user dictionary + ## Release 2.5.0 (July 04, 2020) - cli: Add query --extrafield, --includefield, --excludefield - Revive bugzilla.rhbugzilla.RHBugzilla import path diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 6ff41ed4..34b7f6f0 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "2.5.0" +version = "3.0.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 4da607a3..cb79ddfd 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,5 +1,5 @@ Name: python-bugzilla -Version: 2.5.0 +Version: 3.0.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From a07c1ac63d6d4a86b9db3e13fe096bb3fb48a2cc Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Wed, 7 Oct 2020 12:48:56 +0300 Subject: [PATCH 288/393] Add CI job to install on Windows. Refs #134 will fail with: running install running build Didn't find rst2man or rst2man.py ERROR: Command errored out with exit status 1: ... --- .github/workflows/build.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e300acb9..3478536f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,3 +72,24 @@ jobs: ./setup.py rpm - run: mock --root epel-8-x86_64 *.src.rpm + + # Build and install on Windows + windows: + runs-on: windows-latest + strategy: + matrix: + python-version: [3.8] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Build tarball & install + run: | + python setup.py sdist + + pip install --find-links dist python-bugzilla From 301ae4e61f1a180a6350869fa41ede48045e3bd2 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Wed, 7 Oct 2020 20:07:45 +0300 Subject: [PATCH 289/393] Use shutil.which() instead of find_executable(). Refs #134 unfortunately this doesn't seem to work --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 50f2b522..e7e4ca22 100755 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ import glob import os +import shutil import subprocess import sys @@ -77,10 +78,9 @@ def run(self): class BuildCommand(distutils.command.build.build): def _make_man_pages(self): - from distutils.spawn import find_executable - rstbin = find_executable("rst2man") + rstbin = shutil.which("rst2man") if not rstbin: - rstbin = find_executable("rst2man.py") + rstbin = shutil.which("rst2man.py") if not rstbin: sys.exit("Didn't find rst2man or rst2man.py") From b3767190cd79d9ff25ed123e027322e83e46e1c0 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Wed, 7 Oct 2020 13:58:30 +0300 Subject: [PATCH 290/393] Install without man pages if rst2man not found. Fixes #134 Prior to 68a1dc628ac21a24018710ac3fdba2709aa80441 the man page generation was skipped if it failed, which worked fine for users on both Windows and Linux where rst2man was missing. This commit brings back the same behavior. --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e7e4ca22..9ba486bb 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ import shutil import subprocess import sys +import warnings import distutils.command.build from distutils.core import Command @@ -82,7 +83,8 @@ def _make_man_pages(self): if not rstbin: rstbin = shutil.which("rst2man.py") if not rstbin: - sys.exit("Didn't find rst2man or rst2man.py") + warnings.warn("Didn't find rst2man or rst2man.py. Installing without man pages") + return for path in glob.glob("man/*.rst"): base = os.path.basename(path) From 119be9df3f04c6ea62eaf7d4b5ce348cc900f60f Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Oct 2020 13:28:39 -0400 Subject: [PATCH 291/393] Prep for release 3.0.1 Signed-off-by: Cole Robinson --- NEWS.md | 3 +++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 90f5ba77..cfd01b0b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # python-bugzilla release news +## Release 3.0.1 (October 07, 2020) +- Skip man page generation to fix build on Windows (Alexander Todorov) + ## Release 3.0.0 (October 03, 2020) - Drop python2 support - New option `bugzilla modify --minor-update option` diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 34b7f6f0..f5e5ba64 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "3.0.0" +version = "3.0.1" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index cb79ddfd..f3f36de6 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,5 +1,5 @@ Name: python-bugzilla -Version: 3.0.0 +Version: 3.0.1 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From 29cc511e455e08927c810e0ae9883f23a6c48c56 Mon Sep 17 00:00:00 2001 From: Jiri Popelka Date: Mon, 12 Oct 2020 14:24:38 +0200 Subject: [PATCH 292/393] Packit config file (.packit.yml) [Packit](https://packit.dev) is a [Github App](https://github.com/marketplace/packit-as-a-service) which helps you integrate this project/package downstream (Fedora). Just add a config (i.e. merge this PR) and [install the Github App](https://packit.dev/packit-as-a-service) in this repo. There are two [jobs](https://packit.dev/docs/configuration/#supported-jobs) in the config file, which tell the service what to do: - `[copr_build]` anytime a PR is created/updated, Packit builds RPM packages in [copr](https://copr.fedorainfracloud.org) and lets you know whether everything's still ok - `[tests]` run tests in [Testing Farm](https://packit.dev/testing-farm), by default a simple installation test is performed For more job types or configuration options, see [documentation](https://packit.dev/docs/configuration/#supported-jobs). --- .packit.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .packit.yml diff --git a/.packit.yml b/.packit.yml new file mode 100644 index 00000000..4140e4a9 --- /dev/null +++ b/.packit.yml @@ -0,0 +1,14 @@ +--- +upstream_project_url: https://github.com/python-bugzilla/python-bugzilla + +jobs: + - job: copr_build + trigger: pull_request + metadata: + targets: + - fedora-all + - job: tests + trigger: pull_request + metadata: + targets: + - fedora-all From 12f314fe9df78e88ea48859a37bc0e2f224a1191 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 12 Oct 2020 09:13:55 -0400 Subject: [PATCH 293/393] packit: build on epel-8 too Signed-off-by: Cole Robinson --- .packit.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.packit.yml b/.packit.yml index 4140e4a9..af3a2e0f 100644 --- a/.packit.yml +++ b/.packit.yml @@ -7,8 +7,10 @@ jobs: metadata: targets: - fedora-all + - epel-8-x86_64 - job: tests trigger: pull_request metadata: targets: - fedora-all + - epel-8-x86_64 From 06dcf606d8f6b8d0c5faf51afe8b8d1d26a445ad Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 12 Oct 2020 09:15:04 -0400 Subject: [PATCH 294/393] ci: Drop github RPM build step, packit has us covered now Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3478536f..f4a643ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,35 +44,6 @@ jobs: file: ./coverage.xml flags: unittests - # Build the RPM on latest fedora, centos7 and centos8 - rpm: - runs-on: ubuntu-latest - - container: - image: fedora:latest - # All this is needed to ensure 'mock' works in docker - options: --cap-add=SYS_ADMIN --security-opt label:disable --security-opt seccomp=unconfined --security-opt apparmor:unconfined - - steps: - - uses: actions/checkout@v2 - - - name: Install deps - run: | - # glibc-langpacks-en needed to work around python locale issues - dnf install -y \ - python3-pip \ - rpm-build \ - mock \ - dnf-plugins-core \ - glibc-langpack-en - dnf builddep -y ./*.spec - - - name: Build RPM + SRPM - run: | - ./setup.py rpm - - - run: mock --root epel-8-x86_64 *.src.rpm - # Build and install on Windows windows: runs-on: windows-latest From 4189f04c6c2aa1dc49146251491a99c00755a54b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 12 Oct 2020 09:40:12 -0400 Subject: [PATCH 295/393] packit: enable RPM tests on commits too Signed-off-by: Cole Robinson --- .packit.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.packit.yml b/.packit.yml index af3a2e0f..52ee1416 100644 --- a/.packit.yml +++ b/.packit.yml @@ -3,7 +3,9 @@ upstream_project_url: https://github.com/python-bugzilla/python-bugzilla jobs: - job: copr_build - trigger: pull_request + trigger: + - pull_request + - commit metadata: targets: - fedora-all From c4476b79022919e1e92fc112b411ad890d6f92c6 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 12 Oct 2020 09:53:58 -0400 Subject: [PATCH 296/393] packit: fix syntax for running against 'commit' Signed-off-by: Cole Robinson --- .packit.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.packit.yml b/.packit.yml index 52ee1416..7d6b64c9 100644 --- a/.packit.yml +++ b/.packit.yml @@ -3,9 +3,13 @@ upstream_project_url: https://github.com/python-bugzilla/python-bugzilla jobs: - job: copr_build - trigger: - - pull_request - - commit + trigger: pull_request + metadata: + targets: + - fedora-all + - epel-8-x86_64 + - job: copr_build + trigger: commit metadata: targets: - fedora-all From efa0990a2e89c8c97a83803487fafb2052e94e8a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 3 Nov 2020 11:00:33 -0500 Subject: [PATCH 297/393] Re-add man/bugzilla.1 We will explicitly regenerate this at release time. Dealing with it at build time is a pain Signed-off-by: Cole Robinson --- .gitignore | 1 - man/bugzilla.1 | 621 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 man/bugzilla.1 diff --git a/.gitignore b/.gitignore index 1f832a6b..a3f16c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ build .coverage .tox .pytest_cache -man/bugzilla.1 diff --git a/man/bugzilla.1 b/man/bugzilla.1 new file mode 100644 index 00000000..fd83d516 --- /dev/null +++ b/man/bugzilla.1 @@ -0,0 +1,621 @@ +.\" Man page generated from reStructuredText. +. +.TH BUGZILLA 1 "" "" "User Commands" +.SH NAME +bugzilla \- command line tool for interacting with Bugzilla +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +\fBbugzilla\fP [\fIoptions\fP] [\fIcommand\fP] [\fIcommand\-options\fP] +.SH DESCRIPTION +.sp +\fBbugzilla\fP is a command line tool for interacting with a Bugzilla +instance over REST or XMLRPC. +.nf + +\fIcommand\fP is one of: +* login \- log into the given bugzilla instance +* new \- create a new bug +* query \- search for bugs matching given criteria +* modify \- modify existing bugs +* attach \- attach files to existing bugs, or get attachments +* info \- get info about the given bugzilla instance +.fi +.sp +.SH GLOBAL OPTIONS +.SS \fB\-\-help, \-h\fP +.sp +\fBSyntax:\fP \fB\-h\fP +.sp +show this help message and exit +.SS \fB\-\-bugzilla\fP +.sp +\fBSyntax:\fP \fB\-\-bugzilla\fP BUGZILLA +.sp +The bugzilla URL. Full API URLs are typically like: +.nf + +* \fI\%https://bugzilla.example.com/xmlrpc.cgi\fP # XMLRPC API +* \fI\%https://bugzilla.example.com/rest/\fP # REST API + +.fi +.sp +.sp +If a non\-specific URL is passed, like \(aqbugzilla.redhat.com\(aq, \fBbugzilla\fP +will try to probe whether the expected XMLRPC or REST path is available, +preferring XMLRPC for backwards compatibility. +.sp +The default URL \fI\%https://bugzilla.redhat.com\fP +.SS \fB\-\-nosslverify\fP +.sp +\fBSyntax:\fP \fB\-\-nosslverify\fP +.sp +Don\(aqt error on invalid bugzilla SSL certificate +.SS \fB\-\-cert\fP +.sp +\fBSyntax:\fP \fB\-\-cert\fP CERTFILE +.sp +client side certificate file needed by the webserver. +.SS \fB\-\-login\fP +.sp +\fBSyntax:\fP \fB\-\-login\fP +.sp +Run interactive "login" before performing the specified command. +.SS \fB\-\-username\fP +.sp +\fBSyntax:\fP \fB\-\-username\fP USERNAME +.sp +Log in with this username +.SS \fB\-\-password\fP +.sp +\fBSyntax:\fP \fB\-\-password\fP PASSWORD +.sp +Log in with this password +.SS \fB\-\-restrict\-login\fP +.sp +\fBSyntax:\fP \fB\-\-restrict\-login\fP +.sp +The session (login token) will be restricted to the current IP +address. +.SS \fB\-\-ensure\-logged\-in\fP +.sp +\fBSyntax:\fP \fB\-\-ensure\-logged\-in\fP +.sp +Raise an error if we aren\(aqt logged in to bugzilla. Consider using +this if you are depending on cached credentials, to ensure that when +they expire the tool errors, rather than subtly change output. +.SS \fB\-\-no\-cache\-credentials\fP +.sp +\fBSyntax:\fP \fB\-\-no\-cache\-credentials\fP +.sp +Don\(aqt save any bugzilla cookies or tokens to disk, and don\(aqt use any +pre\-existing credentials. +.SS \fB\-\-cookiefile\fP +.sp +\fBSyntax:\fP \fB\-\-cookiefile\fP COOKIEFILE +.sp +cookie file to use for bugzilla authentication +.SS \fB\-\-tokenfile\fP +.sp +\fBSyntax:\fP \fB\-\-tokenfile\fP TOKENFILE +.sp +token file to use for bugzilla authentication +.SS \fB\-\-verbose\fP +.sp +\fBSyntax:\fP \fB\-\-verbose\fP +.sp +give more info about what\(aqs going on +.SS \fB\-\-debug\fP +.sp +\fBSyntax:\fP \fB\-\-debug\fP +.sp +output bunches of debugging info +.SS \fB\-\-version\fP +.sp +\fBSyntax:\fP \fB\-\-version\fP +.sp +show program\(aqs version number and exit +.SH STANDARD BUGZILLA OPTIONS +.sp +These options are shared by some combination of the \(aqnew\(aq, \(aqquery\(aq, and +\(aqmodify\(aq sub commands. Not every option works for each command though. +.SS \fB\-p, \-\-product\fP +.sp +\fBSyntax:\fP \fB\-\-product\fP PRODUCT +.sp +Product name +.SS \fB\-v, \-\-version\fP +.sp +\fBSyntax:\fP \fB\-\-version\fP VERSION +.sp +Product version +.SS \fB\-c, \-\-component\fP +.sp +\fBSyntax:\fP \fB\-\-component\fP COMPONENT +.sp +Component name +.SS \fB\-s, \-\-summary\fP +.sp +\fBSyntax:\fP \fB\-\-summary\fP SUMMARY +.sp +Bug summary +.SS \fB\-l, \-\-comment\fP +.sp +\fBSyntax:\fP \fB\-\-comment\fP DESCRIPTION +.sp +Set initial bug comment/description +.SS \fB\-\-comment\-tag\fP +.sp +\fBSyntax:\fP \fB\-\-comment\-tag\fP TAG +.sp +Comment tag for the new comment +.SS \fB\-\-sub\-component\fP +.sp +\fBSyntax:\fP \fB\-\-sub\-component\fP SUB_COMPONENT +.sp +RHBZ sub component name +.SS \fB\-o, \-\-os\fP +.sp +\fBSyntax:\fP \fB\-\-os\fP OS +.sp +Operating system +.SS \fB\-\-arch\fP +.sp +\fBSyntax:\fP \fB\-\-arch\fP ARCH +.sp +Arch this bug occurs on +.SS \fB\-x, \-\-severity\fP +.sp +\fBSyntax:\fP \fB\-\-severity\fP SEVERITY +.sp +Bug severity +.SS \fB\-z, \-\-priority\fP +.sp +\fBSyntax:\fP \fB\-\-priority\fP PRIORITY +.sp +Bug priority +.SS \fB\-\-alias\fP +.sp +\fBSyntax:\fP \fB\-\-alias\fP ALIAS +.sp +Bug alias (name) +.SS \fB\-s, \-\-status\fP +.sp +\fBSyntax:\fP \fB\-\-status\fP STATUS +.sp +Bug status (NEW, ASSIGNED, etc.) +.SS \fB\-u, \-\-url\fP +.sp +\fBSyntax:\fP \fB\-\-url\fP URL +.sp +URL for further bug info +.SS \fB\-m \-\-target_milestone\fP +.sp +\fBSyntax:\fP \fB\-\-target_milestone\fP TARGET_MILESTONE +.sp +Target milestone +.SS \fB\-\-target_release\fP +.sp +\fBSyntax:\fP \fB\-\-target_release\fP TARGET_RELEASE +.sp +RHBZ Target release +.SS \fB\-\-blocked\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Bug IDs that this bug blocks +.SS \fB\-\-dependson\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Bug IDs that this bug depends on +.SS \fB\-\-keywords\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Bug keywords +.SS \fB\-\-groups\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +Which user groups can view this bug +.SS \fB\-\-cc\fP +.sp +\fBSyntax:\fP \fB\&...]\fP +.sp +CC list +.SS \fB\-a, \-\-assignee, \-\-assigned_to\fP +.sp +\fBSyntax:\fP \fB\-\-assigned_to\fP ASSIGNED_TO +.sp +Bug assignee +.SS \fB\-q, \-\-qa_contact\fP +.sp +\fBSyntax:\fP \fB\-\-qa_contact\fP QA_CONTACT +.sp +QA contact +.SS \fB\-\-flag\fP +.sp +\fBSyntax:\fP \fB\-\-flag\fP FLAG +.sp +Set or unset a flag. For example, to set a flag named devel_ack, do +\-\-flag devel_ack+ Unset a flag with the \(aqX\(aq value, like \-\-flag +needinfoX +.SS \fB\-\-tags\fP +.sp +\fBSyntax:\fP \fB\-\-tags\fP TAG +.sp +Set (personal) tags field +.SS \fB\-w, \-\-whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-whiteboard\fP WHITEBOARD +.sp +Whiteboard field +.SS \fB\-\-devel_whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-devel_whiteboard\fP DEVEL_WHITEBOARD +.sp +RHBZ devel whiteboard field +.SS \fB\-\-internal_whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-internal_whiteboard\fP INTERNAL_WHITEBOARD +.sp +RHBZ internal whiteboard field +.SS \fB\-\-qa_whiteboard\fP +.sp +\fBSyntax:\fP \fB\-\-qa_whiteboard\fP QA_WHITEBOARD +.sp +RHBZ QA whiteboard field +.SS \fB\-F, \-\-fixed_in\fP +.sp +\fBSyntax:\fP \fB\-\-fixed_in\fP FIXED_IN +.sp +RHBZ \(aqFixed in version\(aq field +.SS \fB\-\-field\fP +.sp +\fBSyntax:\fP \fB\-\-field\fP FIELD\(ga\(ga VALUE +.sp +Manually specify a bugzilla API field. FIELD is the raw name used +by the bugzilla instance. For example if your bugzilla instance has a +custom field cf_my_field, do: \-\-field cf_my_field=VALUE +.SH OUTPUT OPTIONS +.sp +These options are shared by several commands, for tweaking the text +output of the command results. +.SS \fB\-f, \-\-full\fP +.sp +\fBSyntax:\fP \fB\-\-full\fP +.sp +output detailed bug info +.SS \fB\-i, \-\-ids\fP +.sp +\fBSyntax:\fP \fB\-\-ids\fP +.sp +output only bug IDs +.SS \fB\-e, \-\-extra\fP +.sp +\fBSyntax:\fP \fB\-\-extra\fP +.sp +output additional bug information (keywords, Whiteboards, etc.) +.SS \fB\-\-oneline\fP +.sp +\fBSyntax:\fP \fB\-\-oneline\fP +.sp +one line summary of the bug (useful for scripts) +.SS \fB\-\-json\fP +.sp +\fBSyntax:\fP \fB\-\-json\fP +.sp +output bug contents in JSON format +.SS \fB\-\-includefield\fP +.sp +\fBSyntax:\fP \fB\-\-includefield\fP +.sp +Pass the field name to bugzilla include_fields list. +Only the fields passed to include_fields are returned +by the bugzilla server. +This can be specified multiple times. +.SS \fB\-\-extrafield\fP +.sp +\fBSyntax:\fP \fB\-\-extrafield\fP +.sp +Pass the field name to bugzilla extra_fields list. +When used with \-\-json this can be used to request +bugzilla to return values for non\-default fields. +This can be specified multiple times. +.SS \fB\-\-excludefield\fP +.sp +\fBSyntax:\fP \fB\-\-excludefield\fP +.sp +Pass the field name to bugzilla exclude_fields list. +When used with \-\-json this can be used to request +bugzilla to not return values for a field. +This can be specified multiple times. +.SS \fB\-\-raw\fP +.sp +\fBSyntax:\fP \fB\-\-raw\fP +.sp +raw output of the bugzilla contents. This format is unstable and +difficult to parse. Please use the \fB\-\-json\fP instead if you want +maximum output from the \fIbugzilla\fP +.SS \fB\-\-outputformat\fP +.sp +\fBSyntax:\fP \fB\-\-outputformat\fP OUTPUTFORMAT +.sp +Print output in the form given. You can use RPM\-style tags that match +bug fields, e.g.: \(aq%{id}: %{summary}\(aq. +.sp +The output of the bugzilla tool should NEVER BE PARSED unless you are +using a custom \-\-outputformat. For everything else, just don\(aqt parse it, +the formats are not stable and are subject to change. +.sp +\-\-outputformat allows printing arbitrary bug data in a user preferred +format. For example, to print a returned bug ID, component, and product, +separated with ::, do: +.sp +\-\-outputformat "%{id}::%{component}::%{product}" +.sp +The fields (like \(aqid\(aq, \(aqcomponent\(aq, etc.) are the names of the values +returned by bugzilla\(aqs API. To see a list of all fields, +check the API documentation in the \(aqSEE ALSO\(aq section. Alternatively, +run a \(aqbugzilla \-\-debug query ...\(aq and look at the key names returned in +the query results. Also, in most cases, using the name of the associated +command line switch should work, like \-\-bug_status becomes +%{bug_status}, etc. +.SH ‘QUERY’ SPECIFIC OPTIONS +.sp +Certain options can accept a comma separated list to query multiple +values, including \-\-status, \-\-component, \-\-product, \-\-version, \-\-id. +.sp +Note: querying via explicit command line options will only get you so +far. See the \-\-from\-url option for a way to use powerful Web UI queries +from the command line. +.SS \fB\-b, \-\-bug_id, \-\-id\fP +.sp +\fBSyntax:\fP \fB\-\-id\fP ID +.sp +specify individual bugs by IDs, separated with commas +.SS \fB\-r, \-\-reporter\fP +.sp +\fBSyntax:\fP \fB\-\-reporter\fP REPORTER +.sp +Email: search reporter email for given address +.SS \fB\-\-quicksearch\fP +.sp +\fBSyntax:\fP \fB\-\-quicksearch\fP QUICKSEARCH +.sp +Search using bugzilla\(aqs quicksearch functionality. +.SS \fB\-\-savedsearch\fP +.sp +\fBSyntax:\fP \fB\-\-savedsearch\fP SAVEDSEARCH +.sp +Name of a bugzilla saved search. If you don\(aqt own this saved search, +you must passed \-\-savedsearch_sharer_id. +.SS \fB\-\-savedsearch\-sharer\-id\fP +.sp +\fBSyntax:\fP \fB\-\-savedsearch\-sharer\-id\fP SAVEDSEARCH_SHARER_ID +.sp +Owner ID of the \-\-savedsearch. You can get this ID from the URL +bugzilla generates when running the saved search from the web UI. +.SS \fB\-\-from\-url\fP +.sp +\fBSyntax:\fP \fB\-\-from\-url\fP WEB_QUERY_URL +.sp +Make a working query via bugzilla\(aqs \(aqAdvanced search\(aq web UI, grab +the url from your browser (the string with query.cgi or buglist.cgi +in it), and \-\-from\-url will run it via the bugzilla API. Don\(aqt forget +to quote the string! This only works for Bugzilla 5 and Red Hat +bugzilla +.SH ‘MODIFY’ SPECIFIC OPTIONS +.sp +Fields that take multiple values have a special input format. +.nf +Append: \fI\%\-\-cc=foo@example.com\fP +Overwrite: \fI\%\-\-cc==foo@example.com\fP +Remove: \fI\%\-\-cc=\-foo@example.com\fP +.fi +.sp +.sp +Options that accept this format: \-\-cc, \-\-blocked, \-\-dependson, \-\-groups, +\-\-tags, whiteboard fields. +.SS \fB\-k, \-\-close RESOLUTION\fP +.sp +\fBSyntax:\fP \fBRESOLUTION\fP +.sp +Close with the given resolution (WONTFIX, NOTABUG, etc.) +.SS \fB\-d, \-\-dupeid\fP +.sp +\fBSyntax:\fP \fB\-\-dupeid\fP ORIGINAL +.sp +ID of original bug. Implies \-\-close DUPLICATE +.SS \fB\-\-private\fP +.sp +\fBSyntax:\fP \fB\-\-private\fP +.sp +Mark new comment as private +.SS \fB\-\-reset\-assignee\fP +.sp +\fBSyntax:\fP \fB\-\-reset\-assignee\fP +.sp +Reset assignee to component default +.SS \fB\-\-reset\-qa\-contact\fP +.sp +\fBSyntax:\fP \fB\-\-reset\-qa\-contact\fP +.sp +Reset QA contact to component default +.SS \fB\-\-minor\-update\fP +.sp +\fBSyntax:\fP \fB\-\-minor\-update\fP +.sp +Request bugzilla to not send any email about this change +.SH ‘NEW’ SPECIFIC OPTIONS +.SS \fB\-\-private\fP +.sp +\fBSyntax:\fP \fB\-\-private\fP +.sp +Mark new comment as private +.SH ‘ATTACH’ OPTIONS +.SS \fB\-f, \-\-file\fP +.sp +\fBSyntax:\fP \fB\-\-file\fP FILENAME +.sp +File to attach, or filename for data provided on stdin +.SS \fB\-d, \-\-description\fP +.sp +\fBSyntax:\fP \fB\-\-description\fP DESCRIPTION +.sp +A short description of the file being attached +.SS \fB\-t, \-\-type\fP +.sp +\fBSyntax:\fP \fB\-\-type\fP MIMETYPE +.sp +Mime\-type for the file being attached +.SS \fB\-g, \-\-get\fP +.sp +\fBSyntax:\fP \fB\-\-get\fP ATTACHID +.sp +Download the attachment with the given ID +.SS \fB\-\-getall\fP +.sp +\fBSyntax:\fP \fB\-\-getall\fP BUGID +.sp +Download all attachments on the given bug +.SS \fB\-\-ignore\-obsolete\fP +.sp +\fBSyntax:\fP \fB\-\-ignore\-obsolete\fP +.sp +Do not download attachments marked as obsolete. +.SS \fB\-l, \-\-comment\fP +.sp +\fBSyntax:\fP \fB\-\-comment\fP COMMENT +.sp +Add comment with attachment +.SH ‘INFO’ OPTIONS +.SS \fB\-p, \-\-products\fP +.sp +\fBSyntax:\fP \fB\-\-products\fP +.sp +Get a list of products +.SS \fB\-c, \-\-components\fP +.sp +\fBSyntax:\fP \fB\-\-components\fP PRODUCT +.sp +List the components in the given product +.SS \fB\-o, \-\-component_owners\fP +.sp +\fBSyntax:\fP \fB\-\-component_owners\fP PRODUCT +.sp +List components (and their owners) +.SS \fB\-v, \-\-versions\fP +.sp +\fBSyntax:\fP \fB\-\-versions\fP PRODUCT +.sp +List the versions for the given product +.SS \fB\-\-active\-components\fP +.sp +\fBSyntax:\fP \fB\-\-active\-components\fP +.sp +Only show active components. Combine with \-\-components* +.SH AUTHENTICATION CACHE AND API KEYS +.sp +Some command usage will require an active login to the bugzilla +instance. For example, if the bugzilla instance has some private bugs, +those bugs will be missing from \(aqquery\(aq output if you do not have an +active login. +.sp +If you are connecting to a bugzilla 5.0 or later instance, the best +option is to use bugzilla API keys. From the bugzilla web UI, log in, +navigate to Preferences\->API Keys, and generate a key (it will be a long +string of characters and numbers). Then create a +~/.config/python\-bugzilla/bugzillarc like this: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ cat ~/.config/python\-bugzilla/bugzillarc + +[bugzilla.example.com] +api_key=YOUR_API_KEY +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Replace \(aqbugzilla.example.com\(aq with your bugzilla host name, and +YOUR_API_KEY with the generated API Key from the Web UI. +.sp +Alternatively, you can use \(aqbugzilla login \-\-api\-key\(aq, which will ask +for the API key, and save it to bugzillarc for you. +.sp +For older bugzilla instances, you will need to cache a login cookie or +token with the "login" subcommand or the "\-\-login" argument. +.sp +Additionally, the \-\-no\-cache\-credentials option will tell the bugzilla +tool to \fInot\fP save or use any authentication cache, including the +bugzillarc file. +.SH EXAMPLES +.nf +bugzilla query \-\-bug_id 62037 + +bugzilla query \-\-version 15 \-\-component python\-bugzilla + +bugzilla login + +bugzilla new \-p Fedora \-v rawhide \-c python\-bugzilla \e +.in +2 +\-\-summary "python\-bugzilla causes headaches" \e +\-\-comment "python\-bugzilla made my brain hurt when I used it." + +.in -2 +bugzilla attach \-\-file ~/Pictures/cam1.jpg \-\-desc "me, in pain" +$BUGID + +bugzilla attach \-\-getall $BUGID + +bugzilla modify \-\-close NOTABUG \-\-comment "Actually, you\(aqre +hungover." $BUGID +.fi +.sp +.SH EXIT STATUS +.sp +\fBbugzilla\fP normally returns 0 if the requested command was successful. +Otherwise, exit status is 1 if \fBbugzilla\fP is interrupted by the user +(or a login attempt fails), 2 if a socket error occurs (e.g. TCP +connection timeout), and 3 if the Bugzilla server throws an error. +.SH BUGS +.sp +Please report any bugs as github issues at +\fI\%https://github.com/python\-bugzilla/python\-bugzilla\fP +.SH SEE ALSO +.sp +\fI\%https://bugzilla.readthedocs.io/en/latest/api/index.html\fP +\fI\%https://bugzilla.redhat.com/docs/en/html/api/Bugzilla/WebService/Bug.html\fP +.\" Generated by docutils manpage writer. +. From 948f020a796c88c0b5e5dfdd435ce75a51c98960 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 3 Nov 2020 11:01:46 -0500 Subject: [PATCH 298/393] Don't generate man page at build time Instead add an explicit 'regenerate_manpages' command that I will do before release. Dealing with docutils both at build time, and as part of the setuptools dep chain has proven to be a pain in a variety of ways. Signed-off-by: Cole Robinson --- python-bugzilla.spec | 1 - requirements.txt | 1 - setup.py | 20 ++++++++++++-------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index f3f36de6..51a7d585 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -9,7 +9,6 @@ Source0: https://github.com/python-bugzilla/python-bugzilla/archive/v%{ve BuildArch: noarch BuildRequires: python3-devel -BuildRequires: python3-docutils BuildRequires: python3-requests BuildRequires: python3-setuptools BuildRequires: python3-pytest diff --git a/requirements.txt b/requirements.txt index f6bdb7bc..f2293605 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ requests -docutils diff --git a/setup.py b/setup.py index 9ba486bb..e32c1c9f 100755 --- a/setup.py +++ b/setup.py @@ -5,9 +5,7 @@ import shutil import subprocess import sys -import warnings -import distutils.command.build from distutils.core import Command from setuptools import setup @@ -77,14 +75,21 @@ def run(self): subprocess.check_call(cmd) -class BuildCommand(distutils.command.build.build): +class ManCommand(Command): + description = ("Regenerate manpages from rst") + user_options = [] + + def initialize_options(self): + pass + def finalize_options(self): + pass + def _make_man_pages(self): rstbin = shutil.which("rst2man") if not rstbin: rstbin = shutil.which("rst2man.py") if not rstbin: - warnings.warn("Didn't find rst2man or rst2man.py. Installing without man pages") - return + raise RuntimeError("Didn't find rst2man or rst2man.py") for path in glob.glob("man/*.rst"): base = os.path.basename(path) @@ -101,7 +106,6 @@ def _make_man_pages(self): def run(self): self._make_man_pages() - distutils.command.build.build.run(self) def _parse_requirements(fname): @@ -135,14 +139,14 @@ def _parse_requirements(fname): 'Programming Language :: Python :: 3.9', ], packages=['bugzilla'], - data_files=[], + data_files=[('share/man/man1', ['man/bugzilla.1'])], entry_points={'console_scripts': ['bugzilla = bugzilla._cli:cli']}, install_requires=_parse_requirements("requirements.txt"), tests_require=_parse_requirements("test-requirements.txt"), cmdclass={ - "build": BuildCommand, + "regenerate_manpages": ManCommand, "pylint": PylintCommand, "rpm": RPMCommand, }, From 4c5a66b3ad34507eeb8c6a1fb16125c300054eb8 Mon Sep 17 00:00:00 2001 From: Ken Dreyer Date: Thu, 5 Nov 2020 13:12:43 -0700 Subject: [PATCH 299/393] bug: remove copied comment from base.py This comment was an accidental copy & paste from commit 72ba03a3dc3a69d4d09db51237f71aa6e6917b3e. --- bugzilla/bug.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 5faa28b8..e6c457fc 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -1,5 +1,3 @@ -# base.py - the base classes etc. for a Python interface to bugzilla -# # Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. # Author: Will Woods # From 4fc3636e4a1cfa27510d0f0e1a2833c4f8d57c31 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 11:08:00 -0500 Subject: [PATCH 300/393] xmlrpc: Drop explicit UTF-8 encoding setup I don't think this is required anymore Signed-off-by: Cole Robinson --- bugzilla/_backendxmlrpc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index db905541..523a9243 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -44,9 +44,6 @@ def __request_helper(self, url, request_body): response = self.__bugzillasession.request( "POST", url, data=request_body) - # We expect utf-8 from the server - response.encoding = 'UTF-8' - # update/set any cookies self.__bugzillasession.set_response_cookies(response) From 260e4e3ea8d8931d58c2ab10dad832e4207553e4 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 11:08:32 -0500 Subject: [PATCH 301/393] xmlrpc: Only set cookies if request didn't error There shouldn't be anything to set in that case Signed-off-by: Cole Robinson --- bugzilla/_backendxmlrpc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 523a9243..af430aa8 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -43,11 +43,9 @@ def __request_helper(self, url, request_body): try: response = self.__bugzillasession.request( "POST", url, data=request_body) + response.raise_for_status() - # update/set any cookies self.__bugzillasession.set_response_cookies(response) - - response.raise_for_status() return self.parse_response(response) except RequestException as e: if not response: From 3f974a6402bc11e0d6d31fc25f17d10c61aa999d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 12:11:19 -0500 Subject: [PATCH 302/393] session: Centralize xmlrpc vs rest session setup Move it all to the session class, because we need more differences than we currently have Signed-off-by: Cole Robinson --- bugzilla/_backendrest.py | 9 +++------ bugzilla/_backendxmlrpc.py | 4 +--- bugzilla/_session.py | 21 ++++++++++++++++++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index 43a503f6..275c40da 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -26,17 +26,14 @@ class _BackendREST(_BackendBase): """ def __init__(self, url, bugzillasession): _BackendBase.__init__(self, url, bugzillasession) - self._bugzillasession.set_content_type("application/json") + self._bugzillasession.set_rest_defaults() ######################### # Internal REST helpers # ######################### - def _handle_response(self, response): - response.raise_for_status() - text = response.text - + def _handle_response(self, text): try: ret = dict(json.loads(text)) except Exception: @@ -60,7 +57,7 @@ def _op(self, method, apiurl, paramdict=None): response = self._bugzillasession.request(method, fullurl, data=data, params=params) - return self._handle_response(response) + return self._handle_response(response.text) def _get(self, *args, **kwargs): return self._op("GET", *args, **kwargs) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index af430aa8..500a99c6 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -22,7 +22,7 @@ def __init__(self, bugzillasession): Transport.__init__(self, use_datetime=False) self.__bugzillasession = bugzillasession - self.__bugzillasession.set_content_type("text/xml") + self.__bugzillasession.set_xmlrpc_defaults() self.__seen_valid_xml = False # Override Transport.user_agent @@ -43,9 +43,7 @@ def __request_helper(self, url, request_body): try: response = self.__bugzillasession.request( "POST", url, data=request_body) - response.raise_for_status() - self.__bugzillasession.set_response_cookies(response) return self.parse_response(response) except RequestException as e: if not response: diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 26ff8684..8b76ac16 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -25,6 +25,7 @@ def __init__(self, url, user_agent, self._cookiecache = cookiecache self._tokencache = tokencache self._api_key = api_key + self._is_xmlrpc = False if self._scheme not in ["http", "https"]: raise Exception("Invalid URL scheme: %s (%s)" % ( @@ -52,6 +53,12 @@ def _get_timeout(self): envtimeout = os.environ.get("PYTHONBUGZILLA_REQUESTS_TIMEOUT") return float(envtimeout or DEFAULT_TIMEOUT) + def set_rest_defaults(self): + self._session.headers["Content-Type"] = "application/json" + def set_xmlrpc_defaults(self): + self._is_xmlrpc = True + self._session.headers["Content-Type"] = "text/xml" + def get_user_agent(self): return self._user_agent def get_scheme(self): @@ -63,8 +70,6 @@ def get_token_value(self): def set_token_value(self, value): self._tokencache.set_value(self._url, value) self._set_tokencache_param() - def set_content_type(self, value): - self._session.headers["Content-Type"] = value def _set_tokencache_param(self): if self._api_key: @@ -89,4 +94,14 @@ def request(self, *args, **kwargs): timeout = self._get_timeout() if "timeout" not in kwargs: kwargs["timeout"] = timeout - return self._session.request(*args, **kwargs) + response = self._session.request(*args, **kwargs) + + if self._is_xmlrpc: + # Yes this still appears to matter for properly decoding unicode + # code points in bugzilla.redhat.com content + response.encoding = "UTF-8" + # Set response cookies + self.set_response_cookies(response) + + response.raise_for_status() + return response From 13b8a058400a6413042eeaf0e1081e7835c421e2 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 12:31:37 -0500 Subject: [PATCH 303/393] session: Only set api_key query param for REST XMLRPC sends it in the POST payload. This makes it less likely that the API key will leak into error messages: https://bugzilla.redhat.com/show_bug.cgi?id=1896791 Signed-off-by: Cole Robinson --- bugzilla/_session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 8b76ac16..c9daa318 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -42,7 +42,6 @@ def __init__(self, url, user_agent, if sslverify is False: self._session.verify = False self._session.headers["User-Agent"] = self._user_agent - self._session.params["Bugzilla_api_key"] = self._api_key self._set_tokencache_param() def _get_timeout(self): @@ -55,6 +54,10 @@ def _get_timeout(self): def set_rest_defaults(self): self._session.headers["Content-Type"] = "application/json" + # Bugzilla 5.0 only supports api_key as a query parameter. + # Bugzilla 5.1+ takes it as a X-BUGZILLA-API-KEY header as well, + # with query param taking preference. + self._session.params["Bugzilla_api_key"] = self._api_key def set_xmlrpc_defaults(self): self._is_xmlrpc = True self._session.headers["Content-Type"] = "text/xml" From 209ef83486316957793698e66339c07999828012 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 12:59:30 -0500 Subject: [PATCH 304/393] packit: Remove the pull_request trigger On PRs, these jobs fail by default unless I explicitly trigger it with `/packit build`, which is mildly annoying. It's not really important for us to test RPM build + install on the occasional PR, so just do it at commit time. Signed-off-by: Cole Robinson --- .packit.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.packit.yml b/.packit.yml index 7d6b64c9..aefa13b9 100644 --- a/.packit.yml +++ b/.packit.yml @@ -2,12 +2,6 @@ upstream_project_url: https://github.com/python-bugzilla/python-bugzilla jobs: - - job: copr_build - trigger: pull_request - metadata: - targets: - - fedora-all - - epel-8-x86_64 - job: copr_build trigger: commit metadata: @@ -15,7 +9,7 @@ jobs: - fedora-all - epel-8-x86_64 - job: tests - trigger: pull_request + trigger: commit metadata: targets: - fedora-all From f4e980577d05855acbbf48c30dbcf0af8bc26be2 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 19:18:02 -0500 Subject: [PATCH 305/393] session: Scrape API key out of requests exceptions Via the params dictionary this can leak into requests errors, depending on the way it fails. Perform a straight string replacement on the exception string and re-raise the exception https://bugzilla.redhat.com/show_bug.cgi?id=1896791 Signed-off-by: Cole Robinson --- bugzilla/_session.py | 11 ++++++++++- tests/test_ro_functional.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index c9daa318..d8529200 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -4,6 +4,8 @@ from logging import getLogger import os +import sys + import requests from ._compatimports import urlparse @@ -97,6 +99,7 @@ def request(self, *args, **kwargs): timeout = self._get_timeout() if "timeout" not in kwargs: kwargs["timeout"] = timeout + response = self._session.request(*args, **kwargs) if self._is_xmlrpc: @@ -106,5 +109,11 @@ def request(self, *args, **kwargs): # Set response cookies self.set_response_cookies(response) - response.raise_for_status() + try: + response.raise_for_status() + except Exception as e: + # Scrape the api key out of the returned exception string + message = str(e).replace(self._api_key or "", "") + raise type(e)(message).with_traceback(sys.exc_info()[2]) + return response diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 4c3ec476..4c153396 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -8,6 +8,7 @@ """ Unit tests that do readonly functional tests against real bugzilla instances. """ +import pytest import bugzilla import tests @@ -63,6 +64,22 @@ def test_rest_xmlrpc_detection(): assert bz.is_xmlrpc() +def test_apikey_error_scraping(): + # Ensure the API key does not leak into any requests exceptions + fakekey = "FOOBARMYKEY" + with pytest.raises(Exception) as e: + _open_bz("https://httpstat.us/502&foo", + force_xmlrpc=True, api_key=fakekey) + assert "400 Client Error" in str(e.value) + assert fakekey not in str(e.value) + + with pytest.raises(Exception) as e: + _open_bz("https://httpstat.us/502&foo", + force_rest=True, api_key=fakekey) + assert "400 Client Error" in str(e.value) + assert fakekey not in str(e.value) + + ################### # mozilla testing # ################### From 6cfe8c5b6fd069a96f9a0f5e405bd40a0bef734c Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 19:39:05 -0500 Subject: [PATCH 306/393] Remove compatimports file We don't need this after dropping python2 support Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 19 ++++++++++--------- bugzilla/_backendxmlrpc.py | 4 ++-- bugzilla/_cli.py | 9 +++++---- bugzilla/_compatimports.py | 11 ----------- bugzilla/_session.py | 5 ++--- bugzilla/base.py | 15 +++++++++------ tests/test_cli_misc.py | 6 +++--- 7 files changed, 31 insertions(+), 38 deletions(-) delete mode 100644 bugzilla/_compatimports.py diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 7d746c3f..609b9651 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -1,11 +1,12 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import configparser +import http.cookiejar import os from logging import getLogger +import urllib.parse -from ._compatimports import (ConfigParser, LoadError, - MozillaCookieJar, urlparse) from .exceptions import BugzillaError from ._util import listify @@ -15,7 +16,7 @@ def _parse_hostname(url): # If http://example.com is passed, netloc=example.com path="" # If just example.com is passed, netloc="" path=example.com - parsedbits = urlparse(url) + parsedbits = urllib.parse.urlparse(url) return parsedbits.netloc or parsedbits.path @@ -60,7 +61,7 @@ def set_configpaths(self, configpaths): configpaths = [os.path.expanduser(p) for p in listify(configpaths or [])] - cfg = ConfigParser() + cfg = configparser.ConfigParser() read_files = cfg.read(configpaths) if read_files: log.info("Found bugzillarc files: %s", read_files) @@ -118,7 +119,7 @@ def save_api_key(self, url, api_key): config_filename = configpaths[-1] section = _parse_hostname(url) - cfg = ConfigParser() + cfg = configparser.ConfigParser() cfg.read(config_filename) if section not in cfg.sections(): @@ -146,7 +147,7 @@ def __init__(self): self._cfg = None def _get_domain(self, url): - domain = urlparse(url)[1] + domain = urllib.parse.urlparse(url)[1] if domain and domain not in self._cfg.sections(): self._cfg.add_section(domain) return domain @@ -178,7 +179,7 @@ def get_filename(self): def set_filename(self, filename): log.debug("Using tokenfile=%s", filename) - cfg = ConfigParser() + cfg = configparser.ConfigParser() if filename: cfg.read(filename) self._filename = filename @@ -194,7 +195,7 @@ def __init__(self): self._cookiejar = None def _build_cookiejar(self, cookiefile): - cj = MozillaCookieJar(cookiefile) + cj = http.cookiejar.MozillaCookieJar(cookiefile) if (cookiefile is None or not os.path.exists(cookiefile)): return cj @@ -202,7 +203,7 @@ def _build_cookiejar(self, cookiefile): try: cj.load() return cj - except LoadError: + except http.cookiejar.LoadError: msg = "cookiefile=%s not in Mozilla format" % cookiefile raise BugzillaError(msg) from None diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 500a99c6..029dee52 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -3,12 +3,12 @@ from logging import getLogger import sys +from xmlrpc.client import (Binary, Fault, ProtocolError, + ServerProxy, Transport) from requests import RequestException from ._backendbase import _BackendBase -from ._compatimports import (Binary, Fault, ProtocolError, - ServerProxy, Transport) from .exceptions import BugzillaError from ._util import listify diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 84d516be..22e05ea6 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -21,11 +21,12 @@ import socket import sys import tempfile +import urllib.parse +import xmlrpc.client import requests.exceptions import bugzilla -from bugzilla._compatimports import Fault, ProtocolError, urlparse DEFAULT_BZ = 'https://bugzilla.redhat.com' @@ -1212,7 +1213,7 @@ def _handle_login(opt, action, bz): print("You already have an API key configured for %s" % bz.url) print("There is no need to cache a login token. Exiting.") sys.exit(0) - print("Logging into %s" % urlparse(bz.url)[1]) + print("Logging into %s" % urllib.parse.urlparse(bz.url)[1]) bz.interactive_login(username, password, restrict_login=opt.restrict_login) except bugzilla.BugzillaError as e: @@ -1293,7 +1294,7 @@ def main(unittest_bz_instance=None): except KeyboardInterrupt: print("\nExited at user request.") sys.exit(1) - except (Fault, bugzilla.BugzillaError) as e: + except (xmlrpc.client.Fault, bugzilla.BugzillaError) as e: print("\nServer error: %s" % str(e)) sys.exit(3) except requests.exceptions.SSLError as e: @@ -1307,7 +1308,7 @@ def main(unittest_bz_instance=None): requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.InvalidURL, - ProtocolError) as e: + xmlrpc.client.ProtocolError) as e: print("\nConnection lost/failed: %s" % str(e)) sys.exit(2) diff --git a/bugzilla/_compatimports.py b/bugzilla/_compatimports.py deleted file mode 100644 index 47fcaf79..00000000 --- a/bugzilla/_compatimports.py +++ /dev/null @@ -1,11 +0,0 @@ -# This work is licensed under the GNU GPLv2 or later. -# See the COPYING file in the top-level directory. - -# pylint: disable=unused-import - -from collections.abc import Mapping -from configparser import ConfigParser -from http.cookiejar import LoadError, MozillaCookieJar -from urllib.parse import urlparse, urlunparse, parse_qsl -from xmlrpc.client import (Binary, DateTime, Fault, ProtocolError, - ServerProxy, Transport) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index d8529200..9bc56835 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -5,11 +5,10 @@ import os import sys +import urllib.parse import requests -from ._compatimports import urlparse - log = getLogger(__name__) @@ -23,7 +22,7 @@ def __init__(self, url, user_agent, tokencache, api_key, requests_session=None): self._url = url self._user_agent = user_agent - self._scheme = urlparse(url)[0] + self._scheme = urllib.parse.urlparse(url)[0] self._cookiecache = cookiecache self._tokencache = tokencache self._api_key = api_key diff --git a/bugzilla/base.py b/bugzilla/base.py index 924b230a..5d469ca8 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -6,12 +6,14 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +import collections import getpass import locale from logging import getLogger import mimetypes import os import sys +import urllib.parse from io import BytesIO @@ -20,7 +22,6 @@ from .apiversion import __version__ from ._backendrest import _BackendREST from ._backendxmlrpc import _BackendXMLRPC -from ._compatimports import Mapping, urlparse, urlunparse, parse_qsl from .bug import Bug, Group, User from .exceptions import BugzillaError from ._rhconverters import _RHBugzillaConverters @@ -34,7 +35,7 @@ def _nested_update(d, u): # Helper for nested dict update() for k, v in list(u.items()): - if isinstance(v, Mapping): + if isinstance(v, collections.abc.Mapping): d[k] = _nested_update(d.get(k, {}), v) else: d[k] = v @@ -110,13 +111,13 @@ def url_to_query(url): # pylint: disable=unpacking-non-sequence (ignore1, ignore2, path, - ignore, query, ignore3) = urlparse(url) + ignore, query, ignore3) = urllib.parse.urlparse(url) base = os.path.basename(path) if base not in ('buglist.cgi', 'query.cgi'): return {} - for (k, v) in parse_qsl(query): + for (k, v) in urllib.parse.parse_qsl(query): if k not in q: q[k] = v elif isinstance(q[k], list): @@ -141,7 +142,8 @@ def fix_url(url, force_rest=False): :param force_rest: If True, generate a REST API url """ - scheme, netloc, path, params, query, fragment = urlparse(url) + (scheme, netloc, path, + params, query, fragment) = urllib.parse.urlparse(url) if not scheme: scheme = 'https' @@ -154,7 +156,8 @@ def fix_url(url, force_rest=False): if force_rest: path = "rest/" - newurl = urlunparse((scheme, netloc, path, params, query, fragment)) + newurl = urllib.parse.urlunparse( + (scheme, netloc, path, params, query, fragment)) return newurl @staticmethod diff --git a/tests/test_cli_misc.py b/tests/test_cli_misc.py index 2955a82e..70f49bf6 100644 --- a/tests/test_cli_misc.py +++ b/tests/test_cli_misc.py @@ -12,12 +12,12 @@ import base64 import datetime import json +import xmlrpc.client import pytest import requests import bugzilla -from bugzilla._compatimports import Binary, DateTime import tests import tests.mockbackend @@ -111,8 +111,8 @@ def test_json_xmlrpc(run_cli): bugid = 1165434 data = {"bugs": [{ 'id': bugid, - 'timetest': DateTime(dateobj), - 'binarytest': Binary(attachdata), + 'timetest': xmlrpc.client.DateTime(dateobj), + 'binarytest': xmlrpc.client.Binary(attachdata), }]} fakebz = tests.mockbackend.make_bz( From 1852b7712f84fb9d499df389c1f06c12ffa69e3b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 12 Nov 2020 20:28:20 -0500 Subject: [PATCH 307/393] Prep for release 3.0.2 Signed-off-by: Cole Robinson --- NEWS.md | 3 +++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index cfd01b0b..8f070b90 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # python-bugzilla release news +## Release 3.0.2 (November 12, 2020) +- Fix API key leaking into requests exceptions + ## Release 3.0.1 (October 07, 2020) - Skip man page generation to fix build on Windows (Alexander Todorov) diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index f5e5ba64..26bfecb5 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "3.0.1" +version = "3.0.2" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 51a7d585..236fb9f4 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,5 +1,5 @@ Name: python-bugzilla -Version: 3.0.1 +Version: 3.0.2 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From 051b2a0e3b7a7a7cae71e136c378cb93d6818248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Mon, 8 Feb 2021 12:29:34 +0100 Subject: [PATCH 308/393] Reference the manual page from the token deprecation message Related to https://github.com/python-bugzilla/python-bugzilla/issues/93#issuecomment-774097863 --- bugzilla/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 5d469ca8..76d9b4e5 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -678,7 +678,8 @@ def interactive_login(self, user=None, password=None, force=False, msg += " Token cache saved to %s" % self.tokenfile if self._get_version() >= 5.0: msg += "\nToken usage is deprecated. " - msg += "Consider using bugzilla API keys instead." + msg += "Consider using bugzilla API keys instead. " + msg += "See `man bugzilla` for more details." print(msg) def logout(self): From d4b1861156b06074ae50c7b249a9e91052443547 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Apr 2021 12:46:27 -0400 Subject: [PATCH 309/393] pylint: Exclude use-a-generator Signed-off-by: Cole Robinson --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 23122933..db2f2c95 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator enable=fixme From 370eddbe175bb24a4abb37d2fd45fe75219b6c44 Mon Sep 17 00:00:00 2001 From: Ivan Lausuch Date: Mon, 22 Feb 2021 16:57:33 +0100 Subject: [PATCH 310/393] Add limit as option to build_query Problem: In queries where the number of bugs are too large, usually we get a timeout Solution: bugzilla API provides a limit option in queries. This commit adds this option to the build_query method --- bugzilla/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 76d9b4e5..43d7a323 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1197,7 +1197,8 @@ def build_query(self, sub_component=None, tags=None, exclude_fields=None, - extra_fields=None): + extra_fields=None, + limit=None): """ Build a query string from passed arguments. Will handle query parameter differences between various bugzilla versions. @@ -1230,6 +1231,7 @@ def build_query(self, "quicksearch": quicksearch, "savedsearch": savedsearch, "sharer_id": savedsearch_sharer_id, + "limit": limit, # RH extensions... don't add any more. See comment below "sub_components": listify(sub_component), From 4bb6f156288d82c9dc6d8d56e330705f4806f176 Mon Sep 17 00:00:00 2001 From: Nikolas Komonen <32624665+NikolasKomonen@users.noreply.github.com> Date: Thu, 13 May 2021 18:35:05 -0400 Subject: [PATCH 311/393] Update Documentation Links for External Bugs --- bugzilla/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 43d7a323..8c1ea590 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1990,7 +1990,7 @@ def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None, ExternalBugs::WebService::add_external_bug method. This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#add_external_bug + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#add-external-bug bug_ids: A single bug id or list of bug ids to have external trackers added. @@ -2033,7 +2033,7 @@ def update_external_tracker(self, ids=None, ext_type_id=None, ExternalBugs::WebService::update_external_bug method. This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#update_external_bug + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#update-external-bug ids: A single external tracker bug id or list of external tracker bug ids. @@ -2078,7 +2078,7 @@ def remove_external_tracker(self, ids=None, ext_type_id=None, ExternalBugs::WebService::remove_external_bug method. This is documented at - https://bugzilla.redhat.com/docs/en/html/api/extensions/ExternalBugs/lib/WebService.html#remove_external_bug + https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#remove-external-bug ids: A single external tracker bug id or list of external tracker bug ids. From d2803aa680735ff3df8cf8ac2a48098b61387aed Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 23 May 2021 16:46:43 -0400 Subject: [PATCH 312/393] setup: Drop distutils direct usage Signed-off-by: Cole Robinson --- setup.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index e32c1c9f..38ce974d 100755 --- a/setup.py +++ b/setup.py @@ -6,8 +6,7 @@ import subprocess import sys -from distutils.core import Command -from setuptools import setup +import setuptools def get_version(): @@ -17,7 +16,7 @@ def get_version(): return eval(line.split('=')[-1]) # pylint: disable=eval-used -class PylintCommand(Command): +class PylintCommand(setuptools.Command): user_options = [] def initialize_options(self): @@ -52,7 +51,7 @@ def run(self): pylint.lint.Run(files + pylint_opts) -class RPMCommand(Command): +class RPMCommand(setuptools.Command): description = ("Build src and binary rpms and output them " "in the source directory") user_options = [] @@ -75,7 +74,7 @@ def run(self): subprocess.check_call(cmd) -class ManCommand(Command): +class ManCommand(setuptools.Command): description = ("Regenerate manpages from rst") user_options = [] @@ -117,7 +116,7 @@ def _parse_requirements(fname): return ret -setup( +setuptools.setup( name='python-bugzilla', version=get_version(), description='Library and command line tool for interacting with Bugzilla', From 77e9094e6cb7f5b2de01b2de3a82b7e561000a81 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 23 May 2021 16:47:00 -0400 Subject: [PATCH 313/393] pylint: Disable consider-using-with Signed-off-by: Cole Robinson --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index db2f2c95..182a9a87 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with enable=fixme From 3fd0566a395dc9f33bad6498c9d84bb95034266a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 22 Jun 2021 19:47:28 -0400 Subject: [PATCH 314/393] spec: Remove shebang fixup redhat-rpm-config brp-mangle-shebangs added in 2018 will do /usr/bin/env python3 -> /usr/bin/python3 automatically for us Signed-off-by: Cole Robinson --- python-bugzilla.spec | 8 -------- 1 file changed, 8 deletions(-) diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 236fb9f4..88fc6a76 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -48,14 +48,6 @@ This package includes the 'bugzilla' command-line tool for interacting with bugz %install %{__python3} setup.py install -O1 --root %{buildroot} -# Replace '#!/usr/bin/env python' with '#!/usr/bin/python2' -# The format is ideal for upstream, but not a distro. See: -# https://fedoraproject.org/wiki/Features/SystemPythonExecutablesUseSystemPython -%global python_env_path %{__python3} -for f in $(find %{buildroot} -type f -executable -print); do - sed -i "1 s|^#!/usr/bin/.*|#!%{python_env_path}|" $f || : -done - %check From 9ed2bea7c74f9fc9262b8979e85432807510c901 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 20 Jul 2021 09:20:42 -0400 Subject: [PATCH 315/393] base: Relax redhat bugzilla detection Internall RH hosts staging versions of bugzilla at URLs that don't match bugzilla.redhat.com, so loosen the restriction Signed-off-by: Cole Robinson --- bugzilla/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 8c1ea590..97ce8a3b 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -261,8 +261,9 @@ def _detect_is_redhat_bugzilla(self): if self._is_redhat_bugzilla: return True - if "bugzilla.redhat.com" in self.url: - log.info("Using RHBugzilla for URL containing bugzilla.redhat.com") + match = ".redhat.com" + if match in self.url: + log.info("Using RHBugzilla for URL containing %s", match) return True return False From 250b6dbfd09b2ac2209f3fb38f585fbb9284ad9a Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 20 Jul 2021 09:24:14 -0400 Subject: [PATCH 316/393] Remove partner-bugzilla.redhat.com references Replace with bugzilla.stage.redhat.com, the new URL Signed-off-by: Cole Robinson --- CONTRIBUTING.md | 6 +++--- examples/bug_autorefresh.py | 2 +- examples/create.py | 4 ++-- examples/getbug.py | 4 ++-- examples/getbug_restapi.py | 2 +- examples/query.py | 6 +++--- examples/update.py | 6 +++--- tests/conftest.py | 2 +- tests/data/authfiles/output-cookies.txt | 4 ++-- tests/data/cookies-lwp.txt | 4 ++-- tests/data/cookies-moz.txt | 4 ++-- tests/test_rw_functional.py | 2 +- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f8f24f0..c3943334 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,17 +24,17 @@ login account is required. Run them with: ## Read/Write Functional Tests. -Read/Write functional tests use partner-bugzilla.redhat.com, which is a +Read/Write functional tests use bugzilla.stage.redhat.com, which is a bugzilla instance specifically for this type of testing. Data is occasionally hard synced with regular bugzilla.redhat.com, and all local edits are removed. Login accounts are also synced. If you want access to -partner-bugzilla.redhat.com, sign up for a regular bugzilla.redhat.com login +bugzilla.stage.redhat.com, sign up for a regular bugzilla.redhat.com login and wait for the next sync period. Before running these tests, you'll need to cache login credentials. Example: - ./bugzilla-cli --bugzilla=partner-bugzilla.redhat.com --username=$USER login + ./bugzilla-cli --bugzilla=bugzilla.stage.redhat.com --username=$USER login pytest --rw-functional ## Testing across python versions diff --git a/examples/bug_autorefresh.py b/examples/bug_autorefresh.py index 09c1c533..a8aa6728 100644 --- a/examples/bug_autorefresh.py +++ b/examples/bug_autorefresh.py @@ -9,7 +9,7 @@ import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) # The Bugzilla.bug_autorefresh setting controls whether bugs will diff --git a/examples/create.py b/examples/create.py index be1c75dc..124a93b0 100644 --- a/examples/create.py +++ b/examples/create.py @@ -12,8 +12,8 @@ # public test instance of bugzilla.redhat.com. # # Don't worry, changing things here is fine, and won't send any email to -# users or anything. It's what partner-bugzilla.redhat.com is for! -URL = "partner-bugzilla.redhat.com" +# users or anything. It's what bugzilla.stage.redhat.com is for! +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) if not bzapi.logged_in: print("This example requires cached login credentials for %s" % URL) diff --git a/examples/getbug.py b/examples/getbug.py index 866b0d6d..faf4c30f 100644 --- a/examples/getbug.py +++ b/examples/getbug.py @@ -11,14 +11,14 @@ import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) # getbug() is just a simple wrapper around getbugs(), which takes a list # IDs, if you need to fetch multiple # -# Example bug: https://partner-bugzilla.redhat.com/show_bug.cgi?id=427301 +# Example bug: https://bugzilla.stage.redhat.com/show_bug.cgi?id=427301 bug = bzapi.getbug(427301) print("Fetched bug #%s:" % bug.id) print(" Product = %s" % bug.product) diff --git a/examples/getbug_restapi.py b/examples/getbug_restapi.py index 94723884..1cb4e797 100644 --- a/examples/getbug_restapi.py +++ b/examples/getbug_restapi.py @@ -10,7 +10,7 @@ import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" # By default, if plain Bugzilla(URL) is invoked, the Bugzilla class will # attempt to determine if XMLRPC or REST API is available, with a preference diff --git a/examples/query.py b/examples/query.py index ac285118..05ad7026 100644 --- a/examples/query.py +++ b/examples/query.py @@ -10,7 +10,7 @@ import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) @@ -55,7 +55,7 @@ # bugzilla.redhat.com, and bugzilla >= 5.0 support queries using the same # format as is used for 'advanced' search URLs via the Web UI. For example, -# I go to partner-bugzilla.redhat.com -> Search -> Advanced Search, select +# I go to bugzilla.stage.redhat.com -> Search -> Advanced Search, select # Classification=Fedora # Product=Fedora # Component=python-bugzilla @@ -65,7 +65,7 @@ # # Run that, copy the URL and bring it here, pass it to url_to_query to # convert it to a dict(), and query as usual -query = bzapi.url_to_query("https://partner-bugzilla.redhat.com/" +query = bzapi.url_to_query("https://bugzilla.stage.redhat.com/" "buglist.cgi?classification=Fedora&component=python-bugzilla&" "f1=creation_ts&o1=lessthaneq&order=Importance&product=Fedora&" "query_format=advanced&v1=2010-01-01") diff --git a/examples/update.py b/examples/update.py index ff13bdf3..cd76992e 100644 --- a/examples/update.py +++ b/examples/update.py @@ -10,7 +10,7 @@ import bugzilla # public test instance of bugzilla.redhat.com. It's okay to make changes -URL = "partner-bugzilla.redhat.com" +URL = "bugzilla.stage.redhat.com" bzapi = bugzilla.Bugzilla(URL) if not bzapi.logged_in: print("This example requires cached login credentials for %s" % URL) @@ -23,9 +23,9 @@ # The param names map to those accepted by Bugzilla Bug.update: # https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug # -# Example bug: https://partner-bugzilla.redhat.com/show_bug.cgi?id=427301 +# Example bug: https://bugzilla.stage.redhat.com/show_bug.cgi?id=427301 # Don't worry, changing things here is fine, and won't send any email to -# users or anything. It's what partner-bugzilla.redhat.com is for! +# users or anything. It's what bugzilla.stage.redhat.com is for! bug = bzapi.getbug(427301) print("Bug id=%s original summary=%s" % (bug.id, bug.summary)) diff --git a/tests/conftest.py b/tests/conftest.py index 0ea42d39..a90dfbb9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def pytest_addoption(parser): parser.addoption("--rw-functional", action="store_true", default=False, help=("Run read/write functional tests against actual bugzilla " "instances. As of now this only runs against " - "partner-bugzilla.redhat.com, which requires an RH " + "bugzilla.stage.redhat.com, which requires an RH " "bugzilla account with cached login creds. This will " "also be very slow.")) parser.addoption("--redhat-url", diff --git a/tests/data/authfiles/output-cookies.txt b/tests/data/authfiles/output-cookies.txt index 193bd5d5..e4970061 100644 --- a/tests/data/authfiles/output-cookies.txt +++ b/tests/data/authfiles/output-cookies.txt @@ -2,5 +2,5 @@ # http://curl.haxx.se/rfc/cookie_spec.html # This is a generated file! Do not edit. -.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie -.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie +.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie +.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie diff --git a/tests/data/cookies-lwp.txt b/tests/data/cookies-lwp.txt index b8818ef3..d3795485 100644 --- a/tests/data/cookies-lwp.txt +++ b/tests/data/cookies-lwp.txt @@ -1,3 +1,3 @@ #LWP-Cookies-2.0 -Set-Cookie3: Bugzilla_login=notacookie; path="/"; domain=".partner-bugzilla.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 -Set-Cookie3: Bugzilla_logincookie=notacookie; path="/"; domain=".partner-bugzilla.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 +Set-Cookie3: Bugzilla_login=notacookie; path="/"; domain=".bugzilla.stage.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 +Set-Cookie3: Bugzilla_logincookie=notacookie; path="/"; domain=".bugzilla.stage.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 diff --git a/tests/data/cookies-moz.txt b/tests/data/cookies-moz.txt index 6a16c9db..316e4d96 100644 --- a/tests/data/cookies-moz.txt +++ b/tests/data/cookies-moz.txt @@ -2,5 +2,5 @@ # http://www.netscape.com/newsref/std/cookie_spec.html # This is a generated file! Do not edit. -.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie -.partner-bugzilla.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie +.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie +.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 6bd62d31..b3d211bc 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -22,7 +22,7 @@ import tests.utils -RHURL = tests.CLICONFIG.REDHAT_URL or "partner-bugzilla.redhat.com" +RHURL = tests.CLICONFIG.REDHAT_URL or "bugzilla.stage.redhat.com" def _split_int(s): From 55e55cc1e5f6a702e84f8c91da3d18f4496c4f18 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 27 Jul 2021 13:24:39 -0400 Subject: [PATCH 317/393] tox: Add py310 Signed-off-by: Cole Robinson --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1a6ced78..0d51036b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34,py35,py36,py37,py38,py39 +envlist = py34,py35,py36,py37,py38,py39,py310 [testenv] deps = From e7d1d595af72213b9448267bafc7395ad65f09c8 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 27 Jul 2021 13:29:56 -0400 Subject: [PATCH 318/393] Prep for release 3.1.0 Signed-off-by: Cole Robinson --- NEWS.md | 4 ++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 8f070b90..fba19ea3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # python-bugzilla release news +## Release 3.1.0 (July 27, 2021) +- Detect bugzilla.stage.redhat.com as RHBugzilla +- Add limit as option to build_query (Ivan Lausuch) + ## Release 3.0.2 (November 12, 2020) - Fix API key leaking into requests exceptions diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 26bfecb5..38fdb64b 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "3.0.2" +version = "3.1.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 88fc6a76..660f8109 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,5 +1,5 @@ Name: python-bugzilla -Version: 3.0.2 +Version: 3.1.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From 17ad10afd5055990f8437afddc9ebb141f5eb084 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 30 Jul 2021 12:04:19 -0400 Subject: [PATCH 319/393] README.md: some tweaks --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5943aa3e..be52d437 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ This package provides two bits: -* 'bugzilla' python module for talking to a [Bugzilla](https://www.bugzilla.org/) instance over XMLRPC or REST -* /usr/bin/bugzilla command line tool for performing actions from the command line: create or edit bugs, various queries, etc. +* `bugzilla` python module for talking to a [Bugzilla](https://www.bugzilla.org/) instance over XMLRPC or REST +* `/usr/bin/bugzilla` command line tool for performing actions from the command line: create or edit bugs, various queries, etc. -This was originally written specifically for Red Hat's Bugzilla instance -and is used heavily at Red Hat and in Fedora, but it should still be +This was originally written specifically for [Red Hat's Bugzilla instance](https://bugzilla.redhat.com) +and is used heavily at Red Hat and in Fedora, but it should be generically useful. You can find some code examples in the [examples](examples) directory. From be67e6df822a82501be3577e1a4eef0e5ecf14e0 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 4 Oct 2021 12:56:50 -0400 Subject: [PATCH 320/393] tests: Fix after rhbz query limits Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 4c153396..577e8203 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -235,7 +235,7 @@ def testQueryURL(run_cli, backends): qurl = ("/buglist.cgi?f1=creation_ts" "&list_id=973582&o1=greaterthaneq&classification=Fedora&" "o2=lessthaneq&query_format=advanced&f2=creation_ts" - "&v1=2010-01-01&component=python-bugzilla&v2=2011-01-01" + "&v1=2010-01-01&component=python-bugzilla&v2=2010-06-01" "&product=Fedora") url = REDHAT_URL @@ -244,7 +244,7 @@ def testQueryURL(run_cli, backends): else: url += qurl out = run_cli("bugzilla query --from-url \"%s\"" % url, bz) - _check(out, 22, "#553878 CLOSED") + _check(out, 10, "#553878 CLOSED") def testQueryFixedIn(run_cli, backends): From 1e42224b4b83037ea78a4a293d346b17b22f1ddf Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 4 Oct 2021 15:34:30 -0400 Subject: [PATCH 321/393] Fix some pylint Signed-off-by: Cole Robinson --- .pylintrc | 2 +- tests/test_cli_attach.py | 2 +- tests/test_ro_functional.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 182a9a87..efdb0d1f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with,consider-using-f-string,unspecified-encoding enable=fixme diff --git a/tests/test_cli_attach.py b/tests/test_cli_attach.py index 2975d106..d287bc54 100644 --- a/tests/test_cli_attach.py +++ b/tests/test_cli_attach.py @@ -58,7 +58,7 @@ def _test_attach_get(run_cli): assert "not used for" in out # Basic --get ATTID usage - filename = u"Klíč memorial test file.txt" + filename = "Klíč memorial test file.txt" cmd = "bugzilla attach --get 112233" fakebz = tests.mockbackend.make_bz( bug_attachment_get_args="data/mockargs/test_attach_get1.txt", diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 577e8203..c2dd75c1 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -226,7 +226,7 @@ def testQueryFormat(run_cli, backends): # Unicode in this bug's summary args = "--bug_id 522796 --outputformat \"%{summary}\"" out = run_cli("bugzilla query %s" % args, bz) - assert u"V34 — system" in out + assert "V34 — system" in out def testQueryURL(run_cli, backends): From 017a9311783fce4529a320b293c89fa2002cfc30 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 5 Oct 2021 12:34:21 -0400 Subject: [PATCH 322/393] cli; Drop unnecessary u"" usage Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 22e05ea6..5fc28652 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -662,7 +662,7 @@ def _filter_components(compdetails): elif opt.component_owners: details = bz.getcomponentsdetails(productname) for c in sorted(_filter_components(details)): - print(u"%s: %s" % (c, details[c]['default_assigned_to'])) + print("%s: %s" % (c, details[c]['default_assigned_to'])) def _convert_to_outputformat(output): From 380fc34c9db049f5cf66098f9b705e7e293243df Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 5 Oct 2021 12:29:04 -0400 Subject: [PATCH 323/393] Remove cookie auth support Bugzilla dropped cookie auth support in 4.4 version, released in 1. I think that's long enough to drop support here. If anyone still needs this feature, they can continue to use old python-bugzilla versions Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 63 ++----------------------- bugzilla/_cli.py | 3 +- bugzilla/_session.py | 13 +---- bugzilla/base.py | 32 ++++--------- examples/apikey.py | 2 +- man/bugzilla.rst | 14 ++---- tests/data/authfiles/output-cookies.txt | 6 --- tests/data/cookies-bad.txt | 1 - tests/data/cookies-lwp.txt | 3 -- tests/data/cookies-moz.txt | 6 --- tests/test_api_authfiles.py | 63 ++----------------------- tests/test_cli_misc.py | 7 +++ 12 files changed, 30 insertions(+), 183 deletions(-) delete mode 100644 tests/data/authfiles/output-cookies.txt delete mode 100644 tests/data/cookies-bad.txt delete mode 100644 tests/data/cookies-lwp.txt delete mode 100644 tests/data/cookies-moz.txt diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index 609b9651..ae31797d 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -2,12 +2,10 @@ # See the COPYING file in the top-level directory. import configparser -import http.cookiejar import os from logging import getLogger import urllib.parse -from .exceptions import BugzillaError from ._util import listify log = getLogger(__name__) @@ -26,17 +24,12 @@ def _makedirs(path): os.makedirs(os.path.dirname(path), 0o700) -def _default_location(filename, kind): +def _default_cache_location(filename): """ - Determine default location for passed filename and xdg kind, - example: ~/.cache/python-bugzilla/bugzillacookies + Determine default location for passed xdg filename. + example: ~/.cache/python-bugzilla/bugzillarc """ - xdgpath = os.path.expanduser("~/.%s/python-bugzilla/%s" % (kind, filename)) - return xdgpath - - -def _default_cache_location(filename): - return _default_location(filename, 'cache') + return os.path.expanduser("~/.cache/python-bugzilla/%s" % filename) class _BugzillaRCFile(object): @@ -184,51 +177,3 @@ def set_filename(self, filename): cfg.read(filename) self._filename = filename self._cfg = cfg - - -class _BugzillaCookieCache(object): - @staticmethod - def get_default_path(): - return _default_cache_location("bugzillacookies") - - def __init__(self): - self._cookiejar = None - - def _build_cookiejar(self, cookiefile): - cj = http.cookiejar.MozillaCookieJar(cookiefile) - if (cookiefile is None or - not os.path.exists(cookiefile)): - return cj - - try: - cj.load() - return cj - except http.cookiejar.LoadError: - msg = "cookiefile=%s not in Mozilla format" % cookiefile - raise BugzillaError(msg) from None - - def set_filename(self, cookiefile): - log.debug("Using cookiefile=%s", cookiefile) - self._cookiejar = self._build_cookiejar(cookiefile) - - def get_filename(self): - return self._cookiejar.filename - - def get_cookiejar(self): - return self._cookiejar - - def set_cookies(self, cookies): - for cookie in cookies: - self._cookiejar.set_cookie(cookie) - - cookiefile = self._cookiejar.filename - if not cookiefile: - return - - if not os.path.exists(cookiefile): - _makedirs(cookiefile) - # Make sure a new file has correct permissions - open(cookiefile, 'a').close() - os.chmod(cookiefile, 0o600) - - self._cookiejar.save() diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index 5fc28652..d5035cc2 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -125,8 +125,7 @@ def _setup_root_parser(): help="Don't save any bugzilla cookies or tokens to disk, and " "don't use any pre-existing credentials.") - p.add_argument('--cookiefile', default=None, - help="cookie file to use for bugzilla authentication") + p.add_argument('--cookiefile', default=None, help=argparse.SUPPRESS) p.add_argument('--tokenfile', default=None, help="token file to use for bugzilla authentication") diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 9bc56835..9288ec51 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -18,12 +18,11 @@ class _BugzillaSession(object): Class to handle the backend agnostic 'requests' setup """ def __init__(self, url, user_agent, - cookiecache, sslverify, cert, + sslverify, cert, tokencache, api_key, requests_session=None): self._url = url self._user_agent = user_agent self._scheme = urllib.parse.urlparse(url)[0] - self._cookiecache = cookiecache self._tokencache = tokencache self._api_key = api_key self._is_xmlrpc = False @@ -38,8 +37,6 @@ def __init__(self, url, user_agent, if cert: self._session.cert = cert - if self._cookiecache: - self._session.cookies = self._cookiecache.get_cookiejar() if sslverify is False: self._session.verify = False self._session.headers["User-Agent"] = self._user_agent @@ -85,12 +82,6 @@ def _set_tokencache_param(self): token = self.get_token_value() self._session.params["Bugzilla_token"] = token - def set_response_cookies(self, response): - """ - Save any cookies received from the passed requests response - """ - self._cookiecache.set_cookies(response.cookies) - def get_requests_session(self): return self._session @@ -105,8 +96,6 @@ def request(self, *args, **kwargs): # Yes this still appears to matter for properly decoding unicode # code points in bugzilla.redhat.com content response.encoding = "UTF-8" - # Set response cookies - self.set_response_cookies(response) try: response.raise_for_status() diff --git a/bugzilla/base.py b/bugzilla/base.py index 97ce8a3b..997ea3c8 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -17,8 +17,7 @@ from io import BytesIO -from ._authfiles import (_BugzillaRCFile, - _BugzillaCookieCache, _BugzillaTokenCache) +from ._authfiles import _BugzillaRCFile, _BugzillaTokenCache from .apiversion import __version__ from ._backendrest import _BackendREST from ._backendxmlrpc import _BackendXMLRPC @@ -87,7 +86,7 @@ class Bugzilla(object): bzapi = Bugzilla("http://bugzilla.example.com") If you have previously logged into that URL, and have cached login - cookies/tokens, you will automatically be logged in. Otherwise to + tokens, you will automatically be logged in. Otherwise to log in, you can either pass auth options to __init__, or call a login helper like interactive_login(). @@ -185,18 +184,14 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, :param password: optional password for the connecting user :param cert: optional certificate file for client side certificate authentication - :param cookiefile: Location to cache the login session cookies so you - don't have to keep specifying username/password. Bugzilla 5+ will - use tokens instead of cookies. - If -1, use the default path. If None, don't use or save - any cookiefile. + :param cookiefile: Deprecated, raises an error if not -1 or None :param sslverify: Set this to False to skip SSL hostname and CA validation checks, like out of date certificate :param tokenfile: Location to cache the API login token so youi don't have to keep specifying username/password. If -1, use the default path. If None, don't use or save any tokenfile. - :param use_creds: If False, this disables cookiefile, tokenfile, + :param use_creds: If False, this disables tokenfile and configpaths by default. This is a convenience option to unset those values at init time. If those values are later changed, they may be used for future operations. @@ -232,25 +227,23 @@ def __init__(self, url=-1, user=None, password=None, cookiefile=-1, self._is_redhat_bugzilla = False self._rcfile = _BugzillaRCFile() - self._cookiecache = _BugzillaCookieCache() self._tokencache = _BugzillaTokenCache() self._force_rest = force_rest self._force_xmlrpc = force_xmlrpc + if cookiefile not in [None, -1]: + raise TypeError("cookiefile is deprecated, don't pass any value.") + if not use_creds: - cookiefile = None tokenfile = None configpaths = [] - if cookiefile == -1: - cookiefile = self._cookiecache.get_default_path() if tokenfile == -1: tokenfile = self._tokencache.get_default_path() if configpaths == -1: configpaths = _BugzillaRCFile.get_default_configpaths() - self._setcookiefile(cookiefile) self._settokenfile(tokenfile) self._setconfigpath(configpaths) @@ -368,12 +361,8 @@ def _get_api_aliases(self): ################# def _getcookiefile(self): - return self._cookiecache.get_filename() - def _delcookiefile(self): - self._setcookiefile(None) - def _setcookiefile(self, cookiefile): - self._cookiecache.set_filename(cookiefile) - cookiefile = property(_getcookiefile, _setcookiefile, _delcookiefile) + return None + cookiefile = property(_getcookiefile) def _gettokenfile(self): return self._tokencache.get_filename() @@ -516,7 +505,6 @@ def connect(self, url=None): self.readconfig(overwrite=False) self._session = _BugzillaSession(self.url, self.user_agent, - cookiecache=self._cookiecache, sslverify=self._sslverify, cert=self.cert, tokencache=self._tokencache, @@ -686,7 +674,7 @@ def interactive_login(self, user=None, password=None, force=False, def logout(self): """ Log out of bugzilla. Drops server connection and user info, and - destroys authentication cookies. + destroys authentication cache """ self._backend.user_logout() self.disconnect() diff --git a/examples/apikey.py b/examples/apikey.py index 8b2c90b7..021d70fb 100644 --- a/examples/apikey.py +++ b/examples/apikey.py @@ -20,6 +20,6 @@ # API key usage assumes the API caller is storing the API key; if you would # like to use one of the login options that stores credentials on-disk for -# command-line usage, use tokens or cookies. +# command-line usage, use login tokens. bzapi = bugzilla.Bugzilla(URL, api_key=api_key) assert bzapi.logged_in diff --git a/man/bugzilla.rst b/man/bugzilla.rst index 08c14dab..5d790eca 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -127,18 +127,10 @@ they expire the tool errors, rather than subtly change output. **Syntax:** ``--no-cache-credentials`` -Don't save any bugzilla cookies or tokens to disk, and don't use any +Don't save any bugzilla tokens to disk, and don't use any pre-existing credentials. -``--cookiefile`` -^^^^^^^^^^^^^^^^ - -**Syntax:** ``--cookiefile`` COOKIEFILE - -cookie file to use for bugzilla authentication - - ``--tokenfile`` ^^^^^^^^^^^^^^^ @@ -817,8 +809,8 @@ YOUR_API_KEY with the generated API Key from the Web UI. Alternatively, you can use 'bugzilla login --api-key', which will ask for the API key, and save it to bugzillarc for you. -For older bugzilla instances, you will need to cache a login cookie or -token with the "login" subcommand or the "--login" argument. +For older bugzilla instances, you will need to cache a login token +with the "login" subcommand or the "--login" argument. Additionally, the --no-cache-credentials option will tell the bugzilla tool to *not* save or use any authentication cache, including the diff --git a/tests/data/authfiles/output-cookies.txt b/tests/data/authfiles/output-cookies.txt deleted file mode 100644 index e4970061..00000000 --- a/tests/data/authfiles/output-cookies.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Netscape HTTP Cookie File -# http://curl.haxx.se/rfc/cookie_spec.html -# This is a generated file! Do not edit. - -.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie -.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie diff --git a/tests/data/cookies-bad.txt b/tests/data/cookies-bad.txt deleted file mode 100644 index 0928f036..00000000 --- a/tests/data/cookies-bad.txt +++ /dev/null @@ -1 +0,0 @@ -foo this is invalid cookies diff --git a/tests/data/cookies-lwp.txt b/tests/data/cookies-lwp.txt deleted file mode 100644 index d3795485..00000000 --- a/tests/data/cookies-lwp.txt +++ /dev/null @@ -1,3 +0,0 @@ -#LWP-Cookies-2.0 -Set-Cookie3: Bugzilla_login=notacookie; path="/"; domain=".bugzilla.stage.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 -Set-Cookie3: Bugzilla_logincookie=notacookie; path="/"; domain=".bugzilla.stage.redhat.com"; domain_dot; expires="2038-01-01 00:00:00Z"; version=0 diff --git a/tests/data/cookies-moz.txt b/tests/data/cookies-moz.txt deleted file mode 100644 index 316e4d96..00000000 --- a/tests/data/cookies-moz.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Netscape HTTP Cookie File -# http://www.netscape.com/newsref/std/cookie_spec.html -# This is a generated file! Do not edit. - -.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_login notacookie -.bugzilla.stage.redhat.com TRUE / FALSE 2145916800 Bugzilla_logincookie notacookie diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py index 00318613..1452f6af 100644 --- a/tests/test_api_authfiles.py +++ b/tests/test_api_authfiles.py @@ -13,48 +13,22 @@ import shutil import tempfile -import pytest import requests -import bugzilla - import tests import tests.mockbackend import tests.utils -def testCookies(monkeypatch): - monkeypatch.setitem(os.environ, "HOME", - os.path.dirname(__file__) + "/data/homedir") - +def test_tokenfile(monkeypatch): dirname = os.path.dirname(__file__) - cookiesbad = dirname + "/data/cookies-bad.txt" - cookieslwp = dirname + "/data/cookies-lwp.txt" - cookiesmoz = dirname + "/data/cookies-moz.txt" - - # We used to convert LWP cookies, but it shouldn't matter anymore, - # so verify they fail at least - with pytest.raises(bugzilla.BugzillaError): - tests.mockbackend.make_bz(version="3.0.0", - bz_kwargs={"cookiefile": cookieslwp, "use_creds": True}) + monkeypatch.setitem(os.environ, "HOME", dirname + "/data/homedir") - with pytest.raises(bugzilla.BugzillaError): - tests.mockbackend.make_bz(version="3.0.0", - bz_kwargs={"cookiefile": cookiesbad, "use_creds": True}) - - # Mozilla should 'just work' - bz = tests.mockbackend.make_bz(version="3.0.0", - bz_kwargs={"cookiefile": cookiesmoz, "use_creds": True}) - - # cookie/token property magic bz = tests.mockbackend.make_bz(bz_kwargs={"use_creds": True}) token = dirname + "/data/homedir/.cache/python-bugzilla/bugzillatoken" - cookie = dirname + "/data/homedir/.cache/python-bugzilla/bugzillacookies" assert token == bz.tokenfile - assert cookie == bz.cookiefile del(bz.tokenfile) - del(bz.cookiefile) assert bz.tokenfile is None assert bz.cookiefile is None @@ -130,14 +104,6 @@ def _write(c): _check(None, None, None, None) -def _get_cookiejar(): - cookiefile = os.path.dirname(__file__) + "/data/cookies-moz.txt" - inputbz = tests.mockbackend.make_bz( - bz_kwargs={"use_creds": True, "cookiefile": cookiefile}) - cookiecache = inputbz._cookiecache # pylint: disable=protected-access - return cookiecache.get_cookiejar() - - def test_authfiles_saving(monkeypatch): tmpdir = tempfile.mkdtemp() try: @@ -151,9 +117,6 @@ def test_authfiles_saving(monkeypatch): backend = bzapi._backend # pylint: disable=protected-access bsession = backend._bugzillasession # pylint: disable=protected-access - response = requests.Response() - response.cookies = _get_cookiejar() - # token testing, with repetitions to hit various code paths bsession.set_token_value(None) bsession.set_token_value("MY-FAKE-TOKEN") @@ -161,31 +124,15 @@ def test_authfiles_saving(monkeypatch): bsession.set_token_value(None) bsession.set_token_value("MY-FAKE-TOKEN") - # cookie testing - bsession.set_response_cookies(response) - dirname = os.path.dirname(__file__) + "/data/authfiles/" output_token = dirname + "output-token.txt" - output_cookies = dirname + "output-cookies.txt" tests.utils.diff_compare(open(bzapi.tokenfile).read(), output_token) - # On RHEL7 the cookie comment header is different. Strip off leading - # comments - def strip_comments(f): - return "".join([ - line for line in open(f).readlines() if - not line.startswith("#")]) - - tests.utils.diff_compare(strip_comments(bzapi.cookiefile), - None, expect_out=strip_comments(output_cookies)) - # Make sure file can re-read them and not error bzapi = tests.mockbackend.make_bz( bz_kwargs={"use_creds": True, - "cookiefile": output_cookies, "tokenfile": output_token}) assert bzapi.tokenfile == output_token - assert bzapi.cookiefile == output_cookies # Test rcfile writing for api_key rcfile = bzapi._rcfile # pylint: disable=protected-access @@ -205,16 +152,12 @@ def strip_comments(f): def test_authfiles_nowrite(): - # Set values when n when cookiefile is None, should be fine + # Setting values tokenfile is None, should be fine bzapi = tests.mockbackend.make_bz(bz_kwargs={"use_creds": False}) bzapi.connect("https://example.com/foo") backend = bzapi._backend # pylint: disable=protected-access bsession = backend._bugzillasession # pylint: disable=protected-access rcfile = bzapi._rcfile # pylint: disable=protected-access - response = requests.Response() - response.cookies = _get_cookiejar() - bsession.set_token_value("NEW-TOKEN-VALUE") - bsession.set_response_cookies(response) assert rcfile.save_api_key(bzapi.url, "fookey") is None diff --git a/tests/test_cli_misc.py b/tests/test_cli_misc.py index 70f49bf6..5a898bef 100644 --- a/tests/test_cli_misc.py +++ b/tests/test_cli_misc.py @@ -38,6 +38,13 @@ def testVersion(run_cli): assert out.strip() == bugzilla.__version__ +def testCookiefileDeprecated(run_cli): + with pytest.raises(TypeError) as e: + run_cli("bugzilla --cookiefile foobar login", + None, expectfail=True) + assert "cookiefile is deprecated" in str(e) + + def testPositionalArgs(run_cli): # Make sure cli correctly rejects ambiguous positional args out = run_cli("bugzilla login --xbadarg foo", From 6573d90abefe26234120f603d1ce27874c882b94 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 5 Oct 2021 14:18:06 -0400 Subject: [PATCH 324/393] session: Rework how auth params are passed to backends REST backend relies on session to set the params, but XMLRPC does it in the backend. Switch it so session hands a param dictionary off to the backend. This will help with upcoming changes that make param values more conditional, and a possible future where bugzilla doesn't accept auth via URL params Signed-off-by: Cole Robinson --- bugzilla/_backendrest.py | 6 +++--- bugzilla/_backendxmlrpc.py | 14 ++++---------- bugzilla/_session.py | 33 +++++++++++++++------------------ 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index 275c40da..f27fa12a 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -49,14 +49,14 @@ def _op(self, method, apiurl, paramdict=None): log.debug("Bugzilla REST %s %s params=%s", method, fullurl, paramdict) data = None - params = None + authparams = self._bugzillasession.get_auth_params() if method == "GET": - params = paramdict + authparams.update(paramdict or {}) else: data = json.dumps(paramdict or {}) response = self._bugzillasession.request(method, fullurl, data=data, - params=params) + params=authparams) return self._handle_response(response.text) def _get(self, *args, **kwargs): diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 029dee52..0d47694f 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -118,18 +118,12 @@ def _ServerProxy__request(self, methodname, params): newparams = params and params[0].copy() or {} log.debug("XMLRPC call: %s(%s)", methodname, newparams) - api_key = self.__bugzillasession.get_api_key() - token_value = self.__bugzillasession.get_token_value() - - if api_key is not None: - if 'Bugzilla_api_key' not in newparams: - newparams['Bugzilla_api_key'] = api_key - elif token_value is not None: - if 'Bugzilla_token' not in newparams: - newparams['Bugzilla_token'] = token_value + authparams = self.__bugzillasession.get_auth_params() + authparams.update(newparams) # pylint: disable=no-member - ret = ServerProxy._ServerProxy__request(self, methodname, (newparams,)) + ret = ServerProxy._ServerProxy__request( + self, methodname, (authparams,)) # pylint: enable=no-member if isinstance(ret, dict) and 'token' in ret.keys(): diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 9288ec51..1960ba50 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -40,7 +40,6 @@ def __init__(self, url, user_agent, if sslverify is False: self._session.verify = False self._session.headers["User-Agent"] = self._user_agent - self._set_tokencache_param() def _get_timeout(self): # Default to 5 minutes. This is longer than bugzilla.redhat.com's @@ -52,10 +51,6 @@ def _get_timeout(self): def set_rest_defaults(self): self._session.headers["Content-Type"] = "application/json" - # Bugzilla 5.0 only supports api_key as a query parameter. - # Bugzilla 5.1+ takes it as a X-BUGZILLA-API-KEY header as well, - # with query param taking preference. - self._session.params["Bugzilla_api_key"] = self._api_key def set_xmlrpc_defaults(self): self._is_xmlrpc = True self._session.headers["Content-Type"] = "text/xml" @@ -64,23 +59,25 @@ def get_user_agent(self): return self._user_agent def get_scheme(self): return self._scheme - def get_api_key(self): - return self._api_key - def get_token_value(self): - return self._tokencache.get_value(self._url) def set_token_value(self, value): self._tokencache.set_value(self._url, value) - self._set_tokencache_param() - def _set_tokencache_param(self): + def get_auth_params(self): + # Don't add a token to the params list if an API key is set. + # Keeping API key solo means bugzilla will definitely fail + # if the key expires. Passing in a token could hide that + # fact, which could make it confusing to pinpoint the issue. if self._api_key: - # Don't add a token to the params list if an API key is set. - # Keeping API key solo means bugzilla will definitely fail - # if the key expires. Passing in a token could hide that - # fact, which could make it confusing to pinpoint the issue. - return - token = self.get_token_value() - self._session.params["Bugzilla_token"] = token + # Bugzilla 5.0 only supports api_key as a query parameter. + # Bugzilla 5.1+ takes it as a X-BUGZILLA-API-KEY header as well, + # with query param taking preference. + return {"Bugzilla_api_key": self._api_key} + + token = self._tokencache.get_value(self._url) + if token: + return {"Bugzilla_token": token} + + return {} def get_requests_session(self): return self._session From ddfbc168f647690b37bcef9e8bc9343b806ef8b2 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 5 Oct 2021 14:45:07 -0400 Subject: [PATCH 325/393] session: Use Authorization header for RH bugzilla See https://bugzilla.redhat.com/show_bug.cgi?id=1833585 bugzilla.redhat.com has added support for non-standard 'Authorization: Bearer $APIKEY' header for authenticating. Other auth methods may eventually be removed. So let's start using this for bugzilla.redhat.com One caveat is that we need to stop sending token/apikey values as query parameters when this header is used Signed-off-by: Cole Robinson --- bugzilla/_session.py | 16 ++++++++++++++-- bugzilla/base.py | 5 ++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 1960ba50..c83fcf15 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -18,14 +18,16 @@ class _BugzillaSession(object): Class to handle the backend agnostic 'requests' setup """ def __init__(self, url, user_agent, - sslverify, cert, - tokencache, api_key, requests_session=None): + sslverify, cert, tokencache, api_key, + is_redhat_bugzilla, + requests_session=None): self._url = url self._user_agent = user_agent self._scheme = urllib.parse.urlparse(url)[0] self._tokencache = tokencache self._api_key = api_key self._is_xmlrpc = False + self._use_auth_bearer = False if self._scheme not in ["http", "https"]: raise Exception("Invalid URL scheme: %s (%s)" % ( @@ -41,6 +43,11 @@ def __init__(self, url, user_agent, self._session.verify = False self._session.headers["User-Agent"] = self._user_agent + if is_redhat_bugzilla and self._api_key: + self._use_auth_bearer = True + self._session.headers["Authorization"] = ( + "Bearer %s" % self._api_key) + def _get_timeout(self): # Default to 5 minutes. This is longer than bugzilla.redhat.com's # apparent 3 minute timeout so shouldn't affect legitimate usage, @@ -63,6 +70,11 @@ def set_token_value(self, value): self._tokencache.set_value(self._url, value) def get_auth_params(self): + # bugzilla.redhat.com will error if there's auth bits in params + # when Authorization header is used + if self._use_auth_bearer: + return {} + # Don't add a token to the params list if an API key is set. # Keeping API key solo means bugzilla will definitely fail # if the key expires. Passing in a token could hide that diff --git a/bugzilla/base.py b/bugzilla/base.py index 997ea3c8..6eb70eb6 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -504,11 +504,15 @@ def connect(self, url=None): # we've changed URLs - reload config self.readconfig(overwrite=False) + # Detect if connecting to redhat bugzilla + self._init_class_from_url() + self._session = _BugzillaSession(self.url, self.user_agent, sslverify=self._sslverify, cert=self.cert, tokencache=self._tokencache, api_key=self.api_key, + is_redhat_bugzilla=self._is_redhat_bugzilla, requests_session=self._user_requests_session) self._backend = backendclass(self.url, self._session) @@ -522,7 +526,6 @@ def connect(self, url=None): version = self._backend.bugzilla_version()["version"] log.debug("Bugzilla version string: %s", version) self._set_bz_version(version) - self._init_class_from_url() @property From 50acd2e97e8561766313ae894cb6771906940045 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 5 Oct 2021 15:25:32 -0400 Subject: [PATCH 326/393] tests: Fill out some more backend test coverage Signed-off-by: Cole Robinson --- bugzilla/_backendrest.py | 12 +++---- bugzilla/_backendxmlrpc.py | 4 +-- tests/test_ro_functional.py | 31 ++++++++++++++-- tests/test_rw_functional.py | 72 +++++++++++++++++++++++-------------- tests/utils.py | 7 ---- 5 files changed, 82 insertions(+), 44 deletions(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index f27fa12a..3abe49c7 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -36,7 +36,7 @@ def __init__(self, url, bugzillasession): def _handle_response(self, text): try: ret = dict(json.loads(text)) - except Exception: + except Exception: # pragma: no cover log.debug("Failed to parse REST response. Output is:\n%s", text) raise @@ -148,19 +148,19 @@ def component_create(self, paramdict): return self._post("/component", paramdict) def component_update(self, paramdict): if "ids" in paramdict: - apiurl = str(listify(paramdict["ids"])[0]) + apiurl = str(listify(paramdict["ids"])[0]) # pragma: no cover if "names" in paramdict: apiurl = ("%(product)s/%(component)s" % listify(paramdict["names"])[0]) return self._put("/component/%s" % apiurl, paramdict) - def externalbugs_add(self, paramdict): + def externalbugs_add(self, paramdict): # pragma: no cover raise BugzillaError( "No REST API available yet for externalbugs_add") - def externalbugs_remove(self, paramdict): + def externalbugs_remove(self, paramdict): # pragma: no cover raise BugzillaError( "No REST API available yet for externalbugs_remove") - def externalbugs_update(self, paramdict): + def externalbugs_update(self, paramdict): # pragma: no cover raise BugzillaError( "No REST API available yet for externalbugs_update") @@ -187,7 +187,7 @@ def user_logout(self): def user_update(self, paramdict): urlid = None if "ids" in paramdict: - urlid = listify(paramdict["ids"])[0] + urlid = listify(paramdict["ids"])[0] # pragma: no cover if "names" in paramdict: urlid = listify(paramdict["names"])[0] return self._put("/user/%s" % urlid, paramdict) diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index 0d47694f..feb7b2f9 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -48,7 +48,7 @@ def __request_helper(self, url, request_body): except RequestException as e: if not response: raise - raise ProtocolError( + raise ProtocolError( # pragma: no cover url, response.status_code, str(e), response.headers) except Fault: raise @@ -75,7 +75,7 @@ def parse_response(self, response): msg = response.text.encode('utf-8') try: parser.feed(msg) - except Exception: + except Exception: # pragma: no cover log.debug("Failed to parse this XMLRPC response:\n%s", msg) raise diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index c2dd75c1..9e0cf9a0 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -58,10 +58,14 @@ def test_rest_xmlrpc_detection(): # See /rest in the URL, so use REST bz = _open_bz("bugzilla.redhat.com/rest") assert bz.is_rest() + with pytest.raises(bugzilla.BugzillaError) as e: + dummy = bz._proxy # pylint: disable=protected-access + assert "raw XMLRPC access is not provided" in str(e) # See /xmlrpc.cgi in the URL, so use XMLRPC bz = _open_bz("bugzilla.redhat.com/xmlrpc.cgi") assert bz.is_xmlrpc() + assert bz._proxy # pylint: disable=protected-access def test_apikey_error_scraping(): @@ -80,6 +84,12 @@ def test_apikey_error_scraping(): assert fakekey not in str(e.value) +def test_xmlrpc_bad_url(): + with pytest.raises(bugzilla.BugzillaError) as e: + _open_bz("https://example.com/#xmlrpc") + assert "URL may not be an XMLRPC URL" in str(e) + + ################### # mozilla testing # ################### @@ -288,8 +298,6 @@ def testGetBugAlias(backends): def testQuerySubComponent(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) - tests.utils.skip_if_rest(bz, "Not working on REST, not sure why yet") - # Test special error wrappers in bugzilla/_cli.py out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 7' " "--component lvm2 --sub-component 'Thin Provisioning'", bz) @@ -306,6 +314,13 @@ def testBugFields(backends): assert set(bz.bugfields) == set(["product", "bug_status"]) +def testProductGetMisc(backends): + bz = _open_bz(REDHAT_URL, **backends) + + assert bz.product_get(ptype="enterable", include_fields=["id"]) + assert bz.product_get(ptype="selectable", include_fields=["name"]) + + def testBugAutoRefresh(backends): bz = _open_bz(REDHAT_URL, **backends) @@ -381,6 +396,18 @@ def testFaults(run_cli, backends): assert "--nosslverify" in out +def test_login_stubs(backends): + bz = _open_bz(REDHAT_URL, **backends) + + # Failed login, verifies our backends are calling the correct API + with pytest.raises(bugzilla.BugzillaError) as e: + bz.login("foo", "bar") + assert "Login failed" in str(e) + + # Works fine when not logged in + bz.logout() + + def test_redhat_version(backends): bzversion = (5, 0) bz = _open_bz(REDHAT_URL, **backends) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index b3d211bc..b7d9cfff 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -747,6 +747,15 @@ def test11UserUpdate(backends): user.refresh() assert user.groupnames == origgroups + # Try user create + try: + name = "pythonbugzilla-%s" % datetime.datetime.today() + bz.createuser(name + "@example.com", name, name) + except Exception as e: + if have_admin: + raise + assert "Sorry, you aren't a member" in str(e) + def test11ComponentEditing(backends): bz = _open_bz(**backends) @@ -798,10 +807,6 @@ def compare(data, newid): # bugzilla 5 error string ("You are not allowed" in str(e))) - # bugzilla.redhat.com doesn't have REST editcomponent yet - tests.utils.skip_if_rest( - bz, "editcomponent not supported for redhat REST API") - # Edit component data = basedata.copy() data.update({ @@ -817,11 +822,15 @@ def compare(data, newid): if newid is not None: compare(data, newid) except Exception as e: - if have_admin: + if bz.is_rest(): + # redhat REST does not support component editing + assert "A REST API resource was not found" in str(e) + elif have_admin: raise - assert (("Sorry, you aren't a member" in str(e)) or - # bugzilla 5 error string - ("You are not allowed" in str(e))) + else: + assert (("Sorry, you aren't a member" in str(e)) or + # bugzilla 5 error string + ("You are not allowed" in str(e))) def test13SubComponents(backends): @@ -843,14 +852,10 @@ def test13SubComponents(backends): "Default / Unclassified (RHEL5)"]} -def test14ExternalTrackersAddUpdateRemoveQuery(backends): - bz = _open_bz(**backends) +def _testExternalTrackers(bz): bugid = 461686 ext_bug_id = 380489 - tests.utils.skip_if_rest( - bz, "unknown if REST API has externaltrackers support") - # Delete any existing external trackers to get to a known state ids = [bug['id'] for bug in bz.getbug(bugid).external_bugs] if ids != []: @@ -895,6 +900,16 @@ def test14ExternalTrackersAddUpdateRemoveQuery(backends): assert len(ids) == 0 +def test14ExternalTrackersAddUpdateRemoveQuery(backends): + bz = _open_bz(**backends) + try: + _testExternalTrackers(bz) + except Exception as e: + if not bz.is_rest(): + raise + assert "No REST API available" in str(e) + + def test15EnsureLoggedIn(run_cli, backends): bz = _open_bz(**backends) comm = "bugzilla --ensure-logged-in query --bug_id 979546" @@ -913,24 +928,27 @@ def test16ModifyTags(run_cli, backends): bz = _open_bz(**backends) bug = bz.getbug(bugid) - tests.utils.skip_if_rest(bz, "update_tags not supported for REST API") + try: + if bug.tags: + bz.update_tags(bug.id, tags_remove=bug.tags) + bug.refresh() + assert bug.tags == [] - if bug.tags: - bz.update_tags(bug.id, tags_remove=bug.tags) + run_cli(cmd + "--tags foo --tags +bar --tags baz", bz) bug.refresh() - assert bug.tags == [] - - run_cli(cmd + "--tags foo --tags +bar --tags baz", bz) - bug.refresh() - assert bug.tags == ["foo", "bar", "baz"] + assert bug.tags == ["foo", "bar", "baz"] - run_cli(cmd + "--tags=-bar", bz) - bug.refresh() - assert bug.tags == ["foo", "baz"] + run_cli(cmd + "--tags=-bar", bz) + bug.refresh() + assert bug.tags == ["foo", "baz"] - bz.update_tags(bug.id, tags_remove=bug.tags) - bug.refresh() - assert bug.tags == [] + bz.update_tags(bug.id, tags_remove=bug.tags) + bug.refresh() + assert bug.tags == [] + except Exception as e: + if not bz.is_rest(): + raise + assert "No REST API available" in str(e) def test17LoginAPIKey(backends): diff --git a/tests/utils.py b/tests/utils.py index a9dd98dc..cfa1b424 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,8 +10,6 @@ import shlex import sys -import pytest - import bugzilla._cli import tests @@ -52,11 +50,6 @@ def open_functional_bz(bzclass, url, kwargs): return bz -def skip_if_rest(bz, msg): - if bz.is_rest(): - pytest.skip(msg) - - def diff_compare(inputdata, filename, expect_out=None): """Compare passed string output to contents of filename""" def _process(data): From ac853a5f9fd815dc91ad5b33aff1e66bb3d1c2e5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 5 Oct 2021 16:12:14 -0400 Subject: [PATCH 327/393] session: Move tokencache updating into Bugzilla class This is wired deep into the backend layer, but it really should just be done after a successful login() call. Move the tokencache.set_value() call to Bugzilla.login() and adjust everything to match. Fill in some more descriptive interactive_login text while we are here Signed-off-by: Cole Robinson --- bugzilla/_authfiles.py | 2 +- bugzilla/_backendxmlrpc.py | 2 -- bugzilla/_session.py | 2 -- bugzilla/base.py | 11 +++++++++-- tests/data/clioutput/tokenfile.txt | 3 +++ tests/test_api_authfiles.py | 21 +++++++++++---------- tests/test_cli_login.py | 18 +++++++++++++++++- 7 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 tests/data/clioutput/tokenfile.txt diff --git a/bugzilla/_authfiles.py b/bugzilla/_authfiles.py index ae31797d..bdb977e7 100644 --- a/bugzilla/_authfiles.py +++ b/bugzilla/_authfiles.py @@ -147,7 +147,7 @@ def _get_domain(self, url): def get_value(self, url): domain = self._get_domain(url) - if self._cfg.has_option(domain, 'token'): + if domain and self._cfg.has_option(domain, 'token'): return self._cfg.get(domain, 'token') return None diff --git a/bugzilla/_backendxmlrpc.py b/bugzilla/_backendxmlrpc.py index feb7b2f9..0558b350 100644 --- a/bugzilla/_backendxmlrpc.py +++ b/bugzilla/_backendxmlrpc.py @@ -126,8 +126,6 @@ def _ServerProxy__request(self, methodname, params): self, methodname, (authparams,)) # pylint: enable=no-member - if isinstance(ret, dict) and 'token' in ret.keys(): - self.__bugzillasession.set_token_value(ret.get('token')) return ret diff --git a/bugzilla/_session.py b/bugzilla/_session.py index c83fcf15..ce030514 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -66,8 +66,6 @@ def get_user_agent(self): return self._user_agent def get_scheme(self): return self._scheme - def set_token_value(self, value): - self._tokencache.set_value(self._url, value) def get_auth_params(self): # bugzilla.redhat.com will error if there's auth bits in params diff --git a/bugzilla/base.py b/bugzilla/base.py index 6eb70eb6..68b36833 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -609,6 +609,8 @@ def login(self, user=None, password=None, restrict_login=None): ret = self._backend.user_login(payload) self.password = '' log.info("login succeeded for user=%s", self.user) + if "token" in ret: + self._tokencache.set_value(self.url, ret["token"]) return ret except Exception as e: log.debug("Login exception: %s", str(e), exc_info=True) @@ -666,8 +668,13 @@ def interactive_login(self, user=None, password=None, force=False, log.info('Logging in... ') out = self.login(user, password, restrict_login) msg = "Login successful." - if "token" in out and self.tokenfile: - msg += " Token cache saved to %s" % self.tokenfile + if "token" not in out: + msg += " However no token was returned." + else: + if not self.tokenfile: + msg += " Token not saved to disk." + else: + msg += " Token cache saved to %s" % self.tokenfile if self._get_version() >= 5.0: msg += "\nToken usage is deprecated. " msg += "Consider using bugzilla API keys instead. " diff --git a/tests/data/clioutput/tokenfile.txt b/tests/data/clioutput/tokenfile.txt new file mode 100644 index 00000000..3f4ff578 --- /dev/null +++ b/tests/data/clioutput/tokenfile.txt @@ -0,0 +1,3 @@ +[example.com] +token = my-fake-token + diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py index 1452f6af..6717385f 100644 --- a/tests/test_api_authfiles.py +++ b/tests/test_api_authfiles.py @@ -13,8 +13,6 @@ import shutil import tempfile -import requests - import tests import tests.mockbackend import tests.utils @@ -116,13 +114,17 @@ def test_authfiles_saving(monkeypatch): bzapi.cert = "foo-fake-path" backend = bzapi._backend # pylint: disable=protected-access bsession = backend._bugzillasession # pylint: disable=protected-access + btokencache = bzapi._tokencache # pylint: disable=protected-access # token testing, with repetitions to hit various code paths - bsession.set_token_value(None) - bsession.set_token_value("MY-FAKE-TOKEN") - bsession.set_token_value("MY-FAKE-TOKEN") - bsession.set_token_value(None) - bsession.set_token_value("MY-FAKE-TOKEN") + btokencache.set_value(bzapi.url, None) + assert "Bugzilla_token" not in bsession.get_auth_params() + btokencache.set_value(bzapi.url, "MY-FAKE-TOKEN") + assert bsession.get_auth_params()["Bugzilla_token"] == "MY-FAKE-TOKEN" + btokencache.set_value(bzapi.url, "MY-FAKE-TOKEN") + btokencache.set_value(bzapi.url, None) + assert "Bugzilla_token" not in bsession.get_auth_params() + btokencache.set_value(bzapi.url, "MY-FAKE-TOKEN") dirname = os.path.dirname(__file__) + "/data/authfiles/" output_token = dirname + "output-token.txt" @@ -155,9 +157,8 @@ def test_authfiles_nowrite(): # Setting values tokenfile is None, should be fine bzapi = tests.mockbackend.make_bz(bz_kwargs={"use_creds": False}) bzapi.connect("https://example.com/foo") - backend = bzapi._backend # pylint: disable=protected-access - bsession = backend._bugzillasession # pylint: disable=protected-access + btokencache = bzapi._tokencache # pylint: disable=protected-access rcfile = bzapi._rcfile # pylint: disable=protected-access - bsession.set_token_value("NEW-TOKEN-VALUE") + btokencache.set_value(bzapi.url, "NEW-TOKEN-VALUE") assert rcfile.save_api_key(bzapi.url, "fookey") is None diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py index 967a92c2..fad2b7dd 100644 --- a/tests/test_cli_login.py +++ b/tests/test_cli_login.py @@ -61,16 +61,32 @@ def test_login(run_cli): # Returns success for logged_in check and hits a tokenfile line cmd = "bugzilla --ensure-logged-in " cmd += "login FOO BAR" + tmp = tempfile.NamedTemporaryFile() fakebz = tests.mockbackend.make_bz( - bz_kwargs={"use_creds": True}, + bz_kwargs={"use_creds": True, "tokenfile": tmp.name}, user_login_args="data/mockargs/test_login.txt", user_login_return={'id': 1234, 'token': 'my-fake-token'}, user_get_args=None, user_get_return={}) + fakebz.connect("https://example.com") out = run_cli(cmd, fakebz) assert "Token cache saved" in out assert fakebz.tokenfile in out assert "Consider using bugzilla API" in out + tests.utils.diff_compare(open(tmp.name).read(), + "data/clioutput/tokenfile.txt") + + # Returns success for logged_in check and hits another tokenfile line + cmd = "bugzilla --ensure-logged-in " + cmd += "login FOO BAR" + fakebz = tests.mockbackend.make_bz( + bz_kwargs={"use_creds": True, "tokenfile": None}, + user_login_args="data/mockargs/test_login.txt", + user_login_return={'id': 1234, 'token': 'my-fake-token'}, + user_get_args=None, + user_get_return={}) + out = run_cli(cmd, fakebz) + assert "Token not saved" in out def test_interactive_login(monkeypatch, run_cli): From acd5e43e09ea79b0476612e06a617d65ec55a6f7 Mon Sep 17 00:00:00 2001 From: Jan Macku Date: Thu, 7 Oct 2021 18:23:36 +0200 Subject: [PATCH 328/393] codecov: Add badge to show current test coverage --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index be52d437..bd2d6d82 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![CI](https://github.com/python-bugzilla/python-bugzilla/workflows/CI/badge.svg)](https://github.com/python-bugzilla/python-bugzilla/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/python-bugzilla/python-bugzilla/branch/master/graph/badge.svg?token=4R3FR4RVH4)](https://codecov.io/gh/python-bugzilla/python-bugzilla) [![PyPI](https://img.shields.io/pypi/v/python-bugzilla)](https://pypi.org/project/python-bugzilla/) # python-bugzilla From 79585d67c9824203b6388c9c661f9ff6a0f12ec7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 12 Jan 2022 13:06:13 -0500 Subject: [PATCH 329/393] tests: Fix API key scraping test Error code changed so we need to relax our string check Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 9e0cf9a0..a533863a 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -74,13 +74,13 @@ def test_apikey_error_scraping(): with pytest.raises(Exception) as e: _open_bz("https://httpstat.us/502&foo", force_xmlrpc=True, api_key=fakekey) - assert "400 Client Error" in str(e.value) + assert "Client Error" in str(e.value) assert fakekey not in str(e.value) with pytest.raises(Exception) as e: _open_bz("https://httpstat.us/502&foo", force_rest=True, api_key=fakekey) - assert "400 Client Error" in str(e.value) + assert "Client Error" in str(e.value) assert fakekey not in str(e.value) From cd701f6879e7868c0b6b4b37857994b01b88eda1 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 12 Jan 2022 13:09:08 -0500 Subject: [PATCH 330/393] Prep for release 3.2.0 Signed-off-by: Cole Robinson --- NEWS.md | 4 ++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index fba19ea3..e642fbf1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # python-bugzilla release news +## Release 3.2.0 (January 12, 2022) +- Use soon-to-be-required Authorization header for RH bugzilla +- Remove cookie auth support + ## Release 3.1.0 (July 27, 2021) - Detect bugzilla.stage.redhat.com as RHBugzilla - Add limit as option to build_query (Ivan Lausuch) diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 38fdb64b..3a6d3e83 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "3.1.0" +version = "3.2.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index 660f8109..fe4e459f 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,5 +1,5 @@ Name: python-bugzilla -Version: 3.1.0 +Version: 3.2.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From a7c324041175a4157823bc2332a046cc2a54d105 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 27 Jan 2022 11:22:28 -0500 Subject: [PATCH 331/393] README.md: Rename 'master' branch to 'main' Signed-off-by: Cole Robinson --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd2d6d82..4c40be66 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![CI](https://github.com/python-bugzilla/python-bugzilla/workflows/CI/badge.svg)](https://github.com/python-bugzilla/python-bugzilla/actions?query=workflow%3ACI) -[![codecov](https://codecov.io/gh/python-bugzilla/python-bugzilla/branch/master/graph/badge.svg?token=4R3FR4RVH4)](https://codecov.io/gh/python-bugzilla/python-bugzilla) +[![codecov](https://codecov.io/gh/python-bugzilla/python-bugzilla/branch/main/graph/badge.svg?token=4R3FR4RVH4)](https://codecov.io/gh/python-bugzilla/python-bugzilla) [![PyPI](https://img.shields.io/pypi/v/python-bugzilla)](https://pypi.org/project/python-bugzilla/) # python-bugzilla From 6de112f4851b2fe0ff93aee39ee4f16a1c8fc983 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 9 Aug 2023 10:48:04 -0400 Subject: [PATCH 332/393] ci: modernize a bit - Use newer github action versions - Build against latest python 3.x version - Pin ubuntu-20.04 so we can get python 3.6 testing for rhel/centos8 - Reduce the testing matrix a bit - Drop redundant codecov params Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4a643ec..2af21f8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,16 +8,19 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + # We stick with 20.04 to get access to python 3.6 + # https://github.com/actions/setup-python/issues/544 + runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + # python 3.6 is for rhel/centos8 compat + python-version: ["3.6", "3.x"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -39,23 +42,21 @@ jobs: pytest --cov --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - file: ./coverage.xml - flags: unittests + uses: codecov/codecov-action@v3 + # Build and install on Windows windows: runs-on: windows-latest strategy: matrix: - python-version: [3.8] + python-version: ["3.x"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} From 138caf8aa72757329aa92c1acc66b4302486ac35 Mon Sep 17 00:00:00 2001 From: Stanislav Levin Date: Fri, 21 Oct 2022 15:38:51 +0300 Subject: [PATCH 333/393] rest api: Post process bugzilla code on HTTP error Bugzilla REST API map result codes to HTTP status codes: https://github.com/bugzilla/bugzilla/blob/7581e08f9136ec32219af6c3192e42ff1c8e9691/Bugzilla/WebService/Constants.pm#L262-L287 But python-bugzilla don't propagate those Bugzilla codes. Fixes: https://github.com/python-bugzilla/python-bugzilla/issues/171 Signed-off-by: Stanislav Levin --- bugzilla/_backendrest.py | 28 +++++++++++++++++++++++++--- bugzilla/_session.py | 8 ++++++-- bugzilla/exceptions.py | 5 +++++ tests/test_rw_functional.py | 4 ++-- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index 3abe49c7..d90da35f 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -7,7 +7,7 @@ import os from ._backendbase import _BackendBase -from .exceptions import BugzillaError +from .exceptions import BugzillaError, BugzillaHTTPError from ._util import listify @@ -32,6 +32,23 @@ def __init__(self, url, bugzillasession): ######################### # Internal REST helpers # ######################### + def _handle_error(self, e): + response = getattr(e, "response", None) + if response is None: + raise e + + if response.status_code in [400, 401, 404]: + self._handle_error_response(response.text) + raise e + + def _handle_error_response(self, text): + try: + result = json.loads(text) + except json.JSONDecodeError: + return + + if result.get("error"): + raise BugzillaError(result["message"], code=result["code"]) def _handle_response(self, text): try: @@ -55,8 +72,13 @@ def _op(self, method, apiurl, paramdict=None): else: data = json.dumps(paramdict or {}) - response = self._bugzillasession.request(method, fullurl, data=data, - params=authparams) + try: + response = self._bugzillasession.request( + method, fullurl, data=data, params=authparams + ) + except BugzillaHTTPError as e: + self._handle_error(e) + return self._handle_response(response.text) def _get(self, *args, **kwargs): diff --git a/bugzilla/_session.py b/bugzilla/_session.py index ce030514..8006bbde 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -9,6 +9,7 @@ import requests +from .exceptions import BugzillaHTTPError log = getLogger(__name__) @@ -106,9 +107,12 @@ def request(self, *args, **kwargs): try: response.raise_for_status() - except Exception as e: + except requests.HTTPError as e: # Scrape the api key out of the returned exception string message = str(e).replace(self._api_key or "", "") - raise type(e)(message).with_traceback(sys.exc_info()[2]) + response = getattr(e, "response", None) + raise BugzillaHTTPError(message, response=response).with_traceback( + sys.exc_info()[2] + ) return response diff --git a/bugzilla/exceptions.py b/bugzilla/exceptions.py index d884df0a..2562dc45 100644 --- a/bugzilla/exceptions.py +++ b/bugzilla/exceptions.py @@ -1,5 +1,6 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. +from requests import HTTPError class BugzillaError(Exception): @@ -36,3 +37,7 @@ def __init__(self, message, code=None): if self.code: message += " (code=%s)" % self.code Exception.__init__(self, message) + + +class BugzillaHTTPError(HTTPError): + """Error raised in the Bugzilla session""" diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index b7d9cfff..ce01ac37 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -50,8 +50,8 @@ def _check_have_admin(bz): return ret -def test0LoggedInNoCreds(): - bz = _open_bz(use_creds=False) +def test0LoggedInNoCreds(backends): + bz = _open_bz(**backends, use_creds=False) assert not bz.logged_in From 0135f97336d24300d47b89c38caeff1f4f2f58f7 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 20 Aug 2023 07:38:42 -0400 Subject: [PATCH 334/393] tests: Fix updateperms test This had no chance of working since 4d6c31e7, but I've never bothered getting admin permissions back from RHBZ admins to confirm :/ Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index ce01ac37..4e600393 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -696,8 +696,8 @@ def test11UserUpdate(backends): # Test group_get try: - group = bz.getgroup("fedora_contrib") - group.refresh() + groupobj = bz.getgroup(group) + groupobj.refresh() except Exception as e: if have_admin: raise From f3c019c4920a4f3206915e6c8a98fa951a861dd2 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 20 Aug 2023 09:03:07 -0400 Subject: [PATCH 335/393] Use timeout=10 for probe() URL detection method Signed-off-by: Cole Robinson --- bugzilla/_backendbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/_backendbase.py b/bugzilla/_backendbase.py index b81e1082..8fd9a80e 100644 --- a/bugzilla/_backendbase.py +++ b/bugzilla/_backendbase.py @@ -22,7 +22,7 @@ def __init__(self, url, bugzillasession): @staticmethod def probe(url): try: - requests.head(url).raise_for_status() + requests.head(url, timeout=10).raise_for_status() return True # pragma: no cover except Exception as e: log.debug("Failed to probe url=%s : %s", url, str(e)) From 5e8b2c86cc3d5dfe949b34195336ecf24f61afb6 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 20 Aug 2023 09:04:54 -0400 Subject: [PATCH 336/393] backendrest: Add coverage annotations Signed-off-by: Cole Robinson --- bugzilla/_backendrest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index d90da35f..a14c4c03 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -35,7 +35,7 @@ def __init__(self, url, bugzillasession): def _handle_error(self, e): response = getattr(e, "response", None) if response is None: - raise e + raise e # pragma: no cover if response.status_code in [400, 401, 404]: self._handle_error_response(response.text) @@ -57,7 +57,7 @@ def _handle_response(self, text): log.debug("Failed to parse REST response. Output is:\n%s", text) raise - if ret.get("error", False): + if ret.get("error", False): # pragma: no cover raise BugzillaError(ret["message"], code=ret["code"]) return ret From 51d6fd0728338e98502bfb2e6faccfbed3afb991 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 20 Aug 2023 09:07:26 -0400 Subject: [PATCH 337/393] Fix pylint and pep8 issues Signed-off-by: Cole Robinson --- .pylintrc | 2 +- bugzilla/_cli.py | 2 +- bugzilla/_session.py | 2 +- bugzilla/base.py | 4 ++-- bugzilla/bug.py | 2 +- tests/test_api_authfiles.py | 4 ++-- tests/test_api_bug.py | 2 +- tests/test_api_misc.py | 1 + tests/test_rw_functional.py | 1 + 9 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.pylintrc b/.pylintrc index efdb0d1f..43ac9c2c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,star-args,fixme,global-statement,broad-except,no-self-use,bare-except,locally-enabled,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with,consider-using-f-string,unspecified-encoding +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,fixme,global-statement,broad-except,bare-except,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with,consider-using-f-string,unspecified-encoding enable=fixme diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index d5035cc2..d0688809 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -1047,7 +1047,7 @@ def _do_modify(bz, parser, opt): for k, v in wbmap.copy().items(): if not v[0] and not v[1]: - del(wbmap[k]) + del wbmap[k] if opt.fields: _merge_field_opts(update, opt.fields, parser) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 8006bbde..98064671 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -31,7 +31,7 @@ def __init__(self, url, user_agent, self._use_auth_bearer = False if self._scheme not in ["http", "https"]: - raise Exception("Invalid URL scheme: %s (%s)" % ( + raise ValueError("Invalid URL scheme: %s (%s)" % ( self._scheme, url)) self._session = requests_session diff --git a/bugzilla/base.py b/bugzilla/base.py index 68b36833..ca638f53 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1302,7 +1302,7 @@ def add_email(key, value, count): # Strip out None elements in the dict for k, v in query.copy().items(): if v is None: - del(query[k]) + del query[k] self.pre_translation(query) return query @@ -1799,7 +1799,7 @@ def _validate_createbug(self, *args, **kwargs): # Back compat handling for check_args if "check_args" in data: - del(data["check_args"]) + del data["check_args"] return data diff --git a/bugzilla/bug.py b/bugzilla/bug.py index e6c457fc..282e6052 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -136,7 +136,7 @@ def _translate_dict(self, newdict): "d[%s]=%s and d[%s]=%s , dropping the value " "d[%s]", newname, newdict[newname], oldname, newdict[oldname], oldname) - del(newdict[oldname]) + del newdict[oldname] def _update_dict(self, newdict): diff --git a/tests/test_api_authfiles.py b/tests/test_api_authfiles.py index 6717385f..fcd6fbd8 100644 --- a/tests/test_api_authfiles.py +++ b/tests/test_api_authfiles.py @@ -26,7 +26,7 @@ def test_tokenfile(monkeypatch): token = dirname + "/data/homedir/.cache/python-bugzilla/bugzillatoken" assert token == bz.tokenfile - del(bz.tokenfile) + del bz.tokenfile assert bz.tokenfile is None assert bz.cookiefile is None @@ -96,7 +96,7 @@ def _write(c): # Test confipath overwrite assert [temp.name] == bzapi.configpath - del(bzapi.configpath) + del bzapi.configpath assert [] == bzapi.configpath bzapi.readconfig() _check(None, None, None, None) diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index 2f762453..e4f88b35 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -106,7 +106,7 @@ def test_bug_getattr(): bug.autorefresh = True summary = bug.summary - del(bug.__dict__["summary"]) + del bug.__dict__["summary"] # Trigger autorefresh assert bug.summary == summary diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index 75814e35..dfb0e923 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -164,6 +164,7 @@ def testStandardQuery(): } assert bz4.url_to_query(url) == query + # pylint: disable=use-implicit-booleaness-not-comparison # Test with unknown URL assert bz4.url_to_query("https://example.com") == {} diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 4e600393..5e40b004 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -363,6 +363,7 @@ def cleardict_new(b): bz.update_flags(bug2.id, cleardict_new(bug2)) bug2.refresh() + # pylint: disable=use-implicit-booleaness-not-comparison assert cleardict_old(bug1) == {} assert cleardict_old(bug2) == {} From b591b4ef90ed06b550b87503b98d19094fb8c1de Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 20 Aug 2023 09:44:49 -0400 Subject: [PATCH 338/393] codecov: Exclude _session.py, which won't be covered by github CI Signed-off-by: Cole Robinson --- codecov.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index fbd9242a..4aa11c0f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,6 @@ -# The files aren't interesting for the unit tests run in CI +# These files will only get full coverage from running the functional +# tests, but those aren't run from CI ignore: - "bugzilla/_backendrest.py" - "bugzilla/_backendxmlrpc.py" + - "bugzilla/_session.py" From c6bfae507c79a66a062fca65a0b5e770a568803f Mon Sep 17 00:00:00 2001 From: "Danilo C. L. de Paula" Date: Thu, 2 Jun 2022 10:01:53 -0400 Subject: [PATCH 339/393] docs: fixing wrong documentation link This solves the problem found in #161 The old link referred to the deprecated APIs. --- man/bugzilla.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index 5d790eca..38f484bb 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -859,4 +859,4 @@ SEE ALSO ======== https://bugzilla.readthedocs.io/en/latest/api/index.html -https://bugzilla.redhat.com/docs/en/html/api/Bugzilla/WebService/Bug.html +https://bugzilla.redhat.com/docs/en/html/api/core/v1/bug.html From 7798f8176b6575575ec00172ec34c87aa577ad4f Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Mon, 3 Jul 2023 16:51:53 +0200 Subject: [PATCH 340/393] Set `Bug.weburl` that is compatible with the REST API (fixes #178) Instead of replacing the substring 'xmlrpc.cgi' in the Bugzilla URL, the URL is now constructed by explicitly using the `netloc` of the Bugzilla URL. --- bugzilla/bug.py | 13 +++++++++++-- tests/data/clioutput/test_query2.txt | 2 +- tests/test_api_bug.py | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 282e6052..ec0e9c00 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -6,6 +6,7 @@ import copy from logging import getLogger +from urllib.parse import urlparse, urlunparse log = getLogger(__name__) @@ -39,8 +40,16 @@ def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False): dict["id"] = bug_id self._update_dict(dict) - self.weburl = bugzilla.url.replace('xmlrpc.cgi', - 'show_bug.cgi?id=%i' % self.bug_id) + self.weburl = self._generate_weburl() + + def _generate_weburl(self): + """ + Generate the URL to the bug in the web UI + """ + parsed = urlparse(self.bugzilla.url) + return urlunparse((parsed.scheme, parsed.netloc, + 'show_bug.cgi', '', 'id=%s' % self.bug_id, + '')) def __str__(self): """ diff --git a/tests/data/clioutput/test_query2.txt b/tests/data/clioutput/test_query2.txt index f7a3b723..d959145f 100644 --- a/tests/data/clioutput/test_query2.txt +++ b/tests/data/clioutput/test_query2.txt @@ -57,7 +57,7 @@ ATTRIBUTE[target_milestone]: rc ATTRIBUTE[target_release]: ['---'] ATTRIBUTE[url]: ATTRIBUTE[version]: ['5.8'] -ATTRIBUTE[weburl]: https:///TESTSUITEMOCK +ATTRIBUTE[weburl]: https:///show_bug.cgi?id=1165434 ATTRIBUTE[whiteboard]: genericwhiteboard diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index e4f88b35..47391d46 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -184,3 +184,12 @@ def _get_fake_bug(apiname): attachments = bug.get_attachments() bug.attachments = attachments assert [469147, 470041, 502352] == bug.get_attachment_ids() + + +def test_bug_weburl(): + fakebz = tests.mockbackend.make_bz( + bug_get_args=None, + bug_get_return="data/mockreturn/test_getbug_rhel.txt") + bug_id = 1165434 + bug = fakebz.getbug(bug_id) + assert bug.weburl == f"https:///show_bug.cgi?id={bug_id}" From 9363b2dce55077bcac4aef560ffc9bb15805445d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 6 Sep 2023 10:46:21 -0400 Subject: [PATCH 341/393] tests: Add functional test for bug.weburl generation Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index a533863a..4bc31dff 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -414,3 +414,12 @@ def test_redhat_version(backends): if not tests.CLICONFIG.REDHAT_URL: _test_version(bz, bzversion) + + +def test_bug_misc(backends): + bz = _open_bz(REDHAT_URL, **backends) + + # Ensure weburl is generated consistently whether + # we are using XMLRPC or REST + bug = bz.getbug(720773) + assert bug.weburl == "https://bugzilla.redhat.com/show_bug.cgi?id=720773" From 55279176be97f6041af4ee86c5346ebf4a1ebdbc Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 20 Aug 2023 09:44:49 -0400 Subject: [PATCH 342/393] codecov: Exclude _session.py, which won't be covered by github CI Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 5e40b004..af240e76 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -116,18 +116,19 @@ def test04NewBugAllFields(run_cli, backends): blocked = "461686,461687" dependson = "427301" comment = "Test bug from python-bugzilla test suite" - sub_component = "Command-line tools (RHEL6)" + component = "Extensions" + sub_component = "AgileTools" alias = "pybz-%s" % datetime.datetime.today().strftime("%s") newout = run_cli("bugzilla new " - "--product 'Red Hat Enterprise Linux 6' --version 6.0 " - "--component lvm2 --sub-component '%s' " + "--product 'Bugzilla' --version 5.0 " + "--component %s --sub-component '%s' " "--summary \"%s\" " "--comment \"%s\" " "--url %s --severity Urgent --priority Low --os %s " "--arch ppc --cc %s --blocked %s --dependson %s " "--alias %s " "--outputformat \"%%{bug_id}\"" % - (sub_component, summary, comment, url, + (component, sub_component, summary, comment, url, osval, cc, blocked, dependson, alias), bz) assert len(newout.splitlines()) == 1 @@ -139,11 +140,12 @@ def test04NewBugAllFields(run_cli, backends): assert bug.summary == summary assert bug.bug_file_loc == url assert bug.op_sys == osval - assert bug.blocks == _split_int(blocked) - assert bug.depends_on == _split_int(dependson) + # Using a non-RH account seems to fail to set these at bug create time + # assert bug.blocks == _split_int(blocked) + # assert bug.depends_on == _split_int(dependson) assert all([e in bug.cc for e in cc.split(",")]) assert bug.longdescs[0]["text"] == comment - assert bug.sub_components == {"lvm2": [sub_component]} + assert bug.sub_components == {component: [sub_component]} assert bug.alias == [alias] # Close the bug From 4b521037c2ab7258e9c1e7d1df7d755c3e596fa5 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Thu, 7 Sep 2023 11:34:28 -0400 Subject: [PATCH 343/393] tests: rw: Make things pass without rhbz dev permissions All these tests were written with implicit Fedora and RHEL developer permissions, since that's what my bugzilla.redhat.com account has. This is a big rework to make things pass and get maximum test coverage with an unprivileged account. The main bit is we need to use more self created bugs, vs pre-existing bugs. This necessitates a bunch of messy changes scattered everywhere Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 509 +++++++++++++++++++++--------------- 1 file changed, 301 insertions(+), 208 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index af240e76..3d49688e 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -25,6 +25,10 @@ RHURL = tests.CLICONFIG.REDHAT_URL or "bugzilla.stage.redhat.com" +################## +# helper methods # +################## + def _split_int(s): return [int(i) for i in s.split(",")] @@ -50,30 +54,41 @@ def _check_have_admin(bz): return ret -def test0LoggedInNoCreds(backends): - bz = _open_bz(**backends, use_creds=False) - assert not bz.logged_in +def _set_have_dev(bug, assigned_to): + # This will only take effect if the logged in user has fedora dev perms + have_dev = bug.assigned_to == assigned_to + bug._testsuite_have_dev = have_dev # pylint: disable=protected-access -def test0ClassDetection(): - bz = bugzilla.Bugzilla(RHURL, use_creds=False) - assert bz.__class__ is bugzilla.RHBugzilla +def _bug_close(run_cli, bug): + # Pre-close it + bz = bug.bugzilla + run_cli("bugzilla modify --close NOTABUG %s --minor-update" % bug.id, bz) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == "NOTABUG" def _makebug(run_cli, bz): + """ + Make a basic bug that the logged in user can maximally manipulate + """ + product = "Fedora" component = "python-bugzilla" version = "rawhide" + assigned_to = "triage@lists.fedoraproject.org" summary = ("python-bugzilla test basic bug %s" % datetime.datetime.today()) newout = run_cli("bugzilla new " - "--product Fedora --component %s --version %s " - "--summary \"%s\" " + f"--product '{product}' " + f"--component '{component}' " + f"--version '{version}' " + f"--assigned_to '{assigned_to}' " + f"--summary \"{summary}\" " "--comment \"Test bug from the python-bugzilla test suite\" " - "--outputformat \"%%{bug_id}\"" % - (component, version, summary), bz) + "--outputformat \"%{bug_id}\"", bz) - assert len(newout.splitlines()) == 1 - bugid = int(newout.splitlines()[0]) + bugid = int(newout.splitlines()[-1]) bug = bz.getbug(bugid) print("\nCreated bugid: %s" % bug.id) @@ -81,86 +96,129 @@ def _makebug(run_cli, bz): assert bug.version == version assert bug.summary == summary + _set_have_dev(bug, assigned_to) + _bug_close(run_cli, bug) + return bug -def test03NewBugBasic(run_cli, backends): - """ - Create a bug with minimal amount of fields, then close it - """ - bz = _open_bz(**backends) - bug = _makebug(run_cli, bz) +def _check_have_dev(bug): + funcname = inspect.stack()[1][3] + have_dev = bug._testsuite_have_dev # pylint: disable=protected-access - # Verify hasattr works - assert hasattr(bug, "id") - assert hasattr(bug, "bug_id") + if not have_dev: + print("\nNo dev privs, reduced testing of %s" % funcname) + return have_dev - # Close the bug - run_cli("bugzilla modify --close NOTABUG %s --minor-update" % bug.id, bz) - bug.refresh() - assert bug.status == "CLOSED" - assert bug.resolution == "NOTABUG" +class _BugCache: + cache = {} -def test04NewBugAllFields(run_cli, backends): + @classmethod + def get_bug(cls, run_cli, bz): + key = bz.is_xmlrpc() and "xmlrpc" or "rest" + if key not in cls.cache: + cls.cache[key] = _makebug(run_cli, bz) + return cls.cache[key] + + +def _make_subcomponent_bug(run_cli, bz): """ - Create a bug using all 'new' fields, check some values, close it + Helper for creating a bug that can handle rhbz sub components """ - bz = _open_bz(**backends) - summary = ("python-bugzilla test manyfields bug %s" % datetime.datetime.today()) + assigned_to = "triage@lists.fedoraproject.org" url = "http://example.com" osval = "Windows" cc = "triage@lists.fedoraproject.org" + assigned_to = "triage@lists.fedoraproject.org" blocked = "461686,461687" dependson = "427301" comment = "Test bug from python-bugzilla test suite" + # We use this product+component to test sub_component + product = "Bugzilla" component = "Extensions" + version = "5.0" sub_component = "AgileTools" alias = "pybz-%s" % datetime.datetime.today().strftime("%s") newout = run_cli("bugzilla new " - "--product 'Bugzilla' --version 5.0 " - "--component %s --sub-component '%s' " - "--summary \"%s\" " - "--comment \"%s\" " - "--url %s --severity Urgent --priority Low --os %s " - "--arch ppc --cc %s --blocked %s --dependson %s " - "--alias %s " - "--outputformat \"%%{bug_id}\"" % - (component, sub_component, summary, comment, url, - osval, cc, blocked, dependson, alias), bz) - - assert len(newout.splitlines()) == 1 - - bugid = int(newout.splitlines()[0]) + f"--product '{product}' " + f"--version '{version}' " + f"--component '{component}' " + f"--sub-component '{sub_component}' " + f"--summary \"{summary}\" " + f"--comment \"{comment}\" " + f"--url {url} " + f"--os {osval} " + f"--cc {cc} " + f"--assigned_to {assigned_to} " + f"--blocked {blocked} " + f"--dependson {dependson} " + f"--alias {alias} " + "--arch ppc --severity Urgent --priority Low " + "--outputformat \"%{bug_id}\"", bz) + + bugid = int(newout.splitlines()[-1]) bug = bz.getbug(bugid, extra_fields=["sub_components"]) print("\nCreated bugid: %s" % bugid) + _set_have_dev(bug, assigned_to) + have_dev = _check_have_dev(bug) + assert bug.summary == summary assert bug.bug_file_loc == url assert bug.op_sys == osval - # Using a non-RH account seems to fail to set these at bug create time - # assert bug.blocks == _split_int(blocked) - # assert bug.depends_on == _split_int(dependson) assert all([e in bug.cc for e in cc.split(",")]) assert bug.longdescs[0]["text"] == comment assert bug.sub_components == {component: [sub_component]} assert bug.alias == [alias] - # Close the bug + if have_dev: + assert bug.blocks == _split_int(blocked) + assert bug.depends_on == _split_int(dependson) + else: + # Using a non-dev account seems to fail to set these at bug create time + assert bug.blocks == [] + assert bug.depends_on == [] + + _bug_close(run_cli, bug) + + return bug + + +############## +# test cases # +############## + +def test0LoggedInNoCreds(backends): + bz = _open_bz(**backends, use_creds=False) + assert not bz.logged_in - # RHBZ makes it difficult to provide consistent semantics for - # 'alias' update: - # https://bugzilla.redhat.com/show_bug.cgi?id=1173114 - # alias += "-closed" + +def test0ClassDetection(): + bz = bugzilla.Bugzilla(RHURL, use_creds=False) + assert bz.__class__ is bugzilla.RHBugzilla + + +def test04NewBugAllFields(run_cli, backends): + """ + Create a bug using all 'new' fields, check some values, close it + """ + bz = _open_bz(**backends) + bug = _make_subcomponent_bug(run_cli, bz) + + # Verify hasattr works + assert hasattr(bug, "id") + assert hasattr(bug, "bug_id") + + # Close the bug run_cli("bugzilla modify " "--close WONTFIX %s " % - bugid, bz) + bug.id, bz) bug.refresh() assert bug.status == "CLOSED" assert bug.resolution == "WONTFIX" - assert bug.alias == [alias] # Check bug's minimal history ret = bug.get_history_raw() @@ -173,44 +231,53 @@ def test05ModifyStatus(run_cli, backends): Modify status and comment fields for an existing bug """ bz = _open_bz(**backends) - bugid = "663674" - cmd = "bugzilla modify %s " % bugid - - bug = bz.getbug(bugid) - - # We want to start with an open bug, so fix things - if bug.status == "CLOSED": - run_cli(cmd + "--status ASSIGNED", bz) - bug.refresh() - assert bug.status == "ASSIGNED" + bug = _BugCache.get_bug(run_cli, bz) + have_dev = _check_have_dev(bug) + cmd = "bugzilla modify %s " % bug.id origstatus = bug.status + perm_error = "not allowed to (un)mark comments" # Set to ON_QA with a private comment - status = "ON_QA" - comment = ("changing status to %s at %s" % - (status, datetime.datetime.today())) - run_cli(cmd + - "--status %s --comment \"%s\" --private" % (status, comment), bz) + try: + status = "ON_QA" + comment = ("changing status to %s at %s" % + (status, datetime.datetime.today())) + run_cli(cmd + + "--status %s --comment \"%s\" --private" % (status, comment), bz) - bug.refresh() - assert bug.status == status - assert bug.longdescs[-1]["is_private"] == 1 - assert bug.longdescs[-1]["text"] == comment + bug.refresh() + assert bug.status == status + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) # Close bug as DEFERRED with a private comment - resolution = "DEFERRED" - comment = ("changing status to CLOSED=%s at %s" % - (resolution, datetime.datetime.today())) - run_cli(cmd + - "--close %s --comment \"%s\" --private" % - (resolution, comment), bz) + try: + resolution = "DEFERRED" + comment = ("changing status to CLOSED=%s at %s" % + (resolution, datetime.datetime.today())) + run_cli(cmd + + "--close %s --comment \"%s\" --private" % + (resolution, comment), bz) + bug.refresh() + assert bug.status == "CLOSED" + assert bug.resolution == resolution + assert bug.comments[-1]["is_private"] == 1 + assert bug.comments[-1]["text"] == comment + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + # Set to assigned + run_cli(cmd + "--status ASSIGNED", bz) bug.refresh() - assert bug.status == "CLOSED" - assert bug.resolution == resolution - assert bug.comments[-1]["is_private"] == 1 - assert bug.comments[-1]["text"] == comment + assert bug.status == "ASSIGNED" # Close bug as dup with no comment dupeid = "461686" @@ -224,12 +291,17 @@ def test05ModifyStatus(run_cli, backends): assert "marked as a duplicate" in bug.longdescs[-1]["text"] # bz.setstatus test - comment = ("adding lone comment at %s" % datetime.datetime.today()) - bug.setstatus("POST", comment=comment, private=True) - bug.refresh() - assert bug.longdescs[-1]["is_private"] == 1 - assert bug.longdescs[-1]["text"] == comment - assert bug.status == "POST" + try: + comment = ("adding lone comment at %s" % datetime.datetime.today()) + bug.setstatus("POST", comment=comment, private=True) + bug.refresh() + assert bug.longdescs[-1]["is_private"] == 1 + assert bug.longdescs[-1]["text"] == comment + assert bug.status == "POST" + except Exception as e: + if have_dev: + raise + assert perm_error in str(e) # bz.close test fixed_in = str(datetime.datetime.today()) @@ -260,56 +332,69 @@ def test06ModifyEmails(run_cli, backends): Modify cc, assignee, qa_contact for existing bug """ bz = _open_bz(**backends) - bugid = "663674" - cmd = "bugzilla modify %s " % bugid - - bug = bz.getbug(bugid) + bug = _BugCache.get_bug(run_cli, bz) + user = bug.creator + have_dev = _check_have_dev(bug) - origcc = bug.cc + cmd = "bugzilla modify %s " % bug.id # Test CC list and reset it email1 = "triage@lists.fedoraproject.org" - email2 = "crobinso@redhat.com" - bug.deletecc(origcc) - run_cli(cmd + "--cc %s --cc %s" % (email1, email2), bz) - bug.addcc(email1) - + run_cli(cmd + "--cc %s --cc %s" % (email1, user), bz) bug.refresh() assert email1 in bug.cc - assert email2 in bug.cc - assert len(bug.cc) == 2 + assert user in bug.cc - run_cli(cmd + "--cc=-%s" % email1, bz) + # Remove CC via command line + # Unprivileged user can only add/remove their own CC value + run_cli(cmd + "--cc=-%s" % user, bz) bug.refresh() - assert email1 not in bug.cc + assert user not in bug.cc - # Test assigned target - run_cli(cmd + "--assignee %s" % email1, bz) + # Re-add CC via API + bug.addcc(user) bug.refresh() - assert bug.assigned_to == email1 + assert user in bug.cc - # Test QA target - run_cli(cmd + "--qa_contact %s" % email1, bz) + # Remove it again, via API + bug.deletecc(user) bug.refresh() - assert bug.qa_contact == email1 + assert user not in bug.cc + assert bug.cc - # Reset values - bug.deletecc(bug.cc) - run_cli(cmd + "--reset-qa-contact --reset-assignee", bz) + perm_error = "required permissions may change that field" - bug.refresh() - assert bug.cc == [] - assert bug.assigned_to == "crobinso@redhat.com" - assert bug.qa_contact == "extras-qa@fedoraproject.org" + # Test assigned and QA target + try: + run_cli(cmd + "--assignee %s --qa_contact %s" % (email1, email1), bz) + bug.refresh() + assert bug.assigned_to == email1 + assert bug.qa_contact == email1 + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + + # Test --reset options + try: + run_cli(cmd + "--reset-qa-contact --reset-assignee", bz) + bug.refresh() + assert bug.assigned_to != email1 + assert bug.qa_contact != email1 + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) -def test07ModifyMultiFlags(run_cli, backends): +def test070ModifyMultiFlags(run_cli, backends): """ Modify flags and fixed_in for 2 bugs """ bz = _open_bz(**backends) - bugid1 = "461686" - bugid2 = "461687" + bugid1 = _BugCache.get_bug(run_cli, bz).id + bugid2 = _makebug(run_cli, bz).id cmd = "bugzilla modify %s %s " % (bugid1, bugid2) def flagstr(b): @@ -345,7 +430,7 @@ def cleardict_new(b): # Set flags and confirm - setflags = "needinfo? requires_doc_text-" + setflags = "fedora_prioritized_bug? needinfo+" run_cli(cmd + " ".join([(" --flag " + f) for f in setflags.split()]), bz) @@ -354,8 +439,8 @@ def cleardict_new(b): assert flagstr(bug1) == setflags assert flagstr(bug2) == setflags - assert bug1.get_flags("needinfo")[0]["status"] == "?" - assert bug1.get_flag_status("requires_doc_text") == "-" + assert bug1.get_flags("needinfo")[0]["status"] == "+" + assert bug1.get_flag_status("fedora_prioritized_bug") == "?" # Clear flags if cleardict_new(bug1): @@ -377,7 +462,7 @@ def cleardict_new(b): if newfix == origfix2: newfix = origfix2 + "-2" - run_cli(cmd + "--fixed_in=%s" % newfix, bz) + run_cli(cmd + "--fixed_in '%s'" % newfix, bz) bug1.refresh() bug2.refresh() @@ -385,7 +470,7 @@ def cleardict_new(b): assert bug2.fixed_in == newfix # Reset fixed_in - run_cli(cmd + "--fixed_in=\"-\"", bz) + run_cli(cmd + "--fixed_in \"-\"", bz) bug1.refresh() bug2.refresh() @@ -393,11 +478,11 @@ def cleardict_new(b): assert bug2.fixed_in == "-" -def test07ModifyMisc(run_cli, backends): - bugid = "461686" - cmd = "bugzilla modify %s " % bugid +def test071ModifyMisc(run_cli, backends): bz = _open_bz(**backends) - bug = bz.getbug(bugid) + bug = _BugCache.get_bug(run_cli, bz) + have_dev = _check_have_dev(bug) + cmd = "bugzilla modify %s " % bug.id # modify --dependson run_cli(cmd + "--dependson 123456", bz) @@ -419,40 +504,52 @@ def test07ModifyMisc(run_cli, backends): assert [] == bug.blocks # modify --keywords + origkw = bug.keywords run_cli(cmd + "--keywords +Documentation --keywords EasyFix", bz) bug.refresh() - assert ["Documentation", "EasyFix"] == bug.keywords - run_cli(cmd + "--keywords=-EasyFix --keywords=-Documentation", - bz) + assert set(["Documentation", "EasyFix"] + origkw) == set(bug.keywords) + run_cli(cmd + "--keywords=-EasyFix --keywords=-Documentation", bz) bug.refresh() - assert [] == bug.keywords - - # modify --target_release - # modify --target_milestone - targetbugid = 492463 - targetbug = bz.getbug(targetbugid) - targetcmd = "bugzilla modify %s " % targetbugid - run_cli(targetcmd + - "--target_milestone beta --target_release 6.2", bz) - targetbug.refresh() - assert targetbug.target_milestone == "beta" - assert targetbug.target_release == ["6.2"] - run_cli(targetcmd + - "--target_milestone rc --target_release 6.10", bz) - targetbug.refresh() - assert targetbug.target_milestone == "rc" - assert targetbug.target_release == ["6.10"] - - # modify --priority - # modify --severity - run_cli(cmd + "--priority low --severity high", bz) - bug.refresh() - assert bug.priority == "low" - assert bug.severity == "high" - run_cli(cmd + "--priority medium --severity medium", bz) - bug.refresh() - assert bug.priority == "medium" - assert bug.severity == "medium" + assert origkw == bug.keywords + + perm_error = "user with the required permissions" + + try: + # modify --target_release + # modify --target_milestone + targetbugid = 492463 + targetbug = bz.getbug(targetbugid) + targetcmd = "bugzilla modify %s " % targetbugid + run_cli(targetcmd + + "--target_milestone beta --target_release 6.2", bz) + targetbug.refresh() + assert targetbug.target_milestone == "beta" + assert targetbug.target_release == ["6.2"] + run_cli(targetcmd + + "--target_milestone rc --target_release 6.10", bz) + targetbug.refresh() + assert targetbug.target_milestone == "rc" + assert targetbug.target_release == ["6.10"] + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) + + try: + # modify --priority + # modify --severity + run_cli(cmd + "--priority low --severity high", bz) + bug.refresh() + assert bug.priority == "low" + assert bug.severity == "high" + run_cli(cmd + "--priority medium --severity medium", bz) + bug.refresh() + assert bug.priority == "medium" + assert bug.severity == "medium" + except RuntimeError as e: + if have_dev: + raise + assert perm_error in str(e) # modify --os # modify --platform @@ -504,7 +601,7 @@ def _test8Attachments(run_cli, backends): testfile = "../tests/data/bz-attach-get1.txt" # Add attachment as CLI option - setbug = _makebug(run_cli, bz) + setbug = _BugCache.get_bug(run_cli, bz) setbug = bz.getbug(setbug.id, extra_fields=["attachments"]) orignumattach = len(setbug.attachments) @@ -598,45 +695,42 @@ def _test8Attachments(run_cli, backends): def test09Whiteboards(run_cli, backends): bz = _open_bz(**backends) - bug_id = "663674" - cmd = "bugzilla modify %s " % bug_id - bug = bz.getbug(bug_id) + bug = _BugCache.get_bug(run_cli, bz) + have_dev = _check_have_dev(bug) + cmd = "bugzilla modify %s " % bug.id # Set all whiteboards initval = str(random.randint(1, 1024)) - run_cli(cmd + - "--whiteboard =%sstatus " - "--devel_whiteboard =%sdevel " - "--internal_whiteboard '=%sinternal, security, foo security1' " - "--qa_whiteboard =%sqa " % - (initval, initval, initval, initval), bz) + statusstr = initval + "foo, bar, baz bar1" + devstr = initval + "devel" + internalstr = initval + "internal" + qastr = initval + "qa" + run_cmd = (cmd + f"--whiteboard '{statusstr}' ") + if have_dev: + run_cmd += ( + f"--devel_whiteboard '{devstr}' " + f"--internal_whiteboard '{internalstr}' " + f"--qa_whiteboard '{qastr}' ") + run_cli(run_cmd, bz) bug.refresh() - assert bug.whiteboard == (initval + "status") - assert bug.qa_whiteboard == (initval + "qa") - assert bug.devel_whiteboard == (initval + "devel") - assert (bug.internal_whiteboard == - (initval + "internal, security, foo security1")) + assert bug.whiteboard == statusstr - # Modify whiteboards - run_cli(cmd + - "--whiteboard =foobar " - "--qa_whiteboard _app " - "--devel_whiteboard =pre-%s" % bug.devel_whiteboard, bz) + if have_dev: + assert bug.qa_whiteboard == qastr + assert bug.devel_whiteboard == devstr + assert bug.internal_whiteboard == internalstr + # Remove a tag + run_cli(cmd + "--whiteboard=-bar, ", bz) bug.refresh() - assert bug.qa_whiteboard == (initval + "qa" + " _app") - assert bug.devel_whiteboard == ("pre-" + initval + "devel") - assert bug.status_whiteboard == "foobar" + statusstr = statusstr.replace("bar, ", "") + assert bug.status_whiteboard == statusstr - # Verify that tag manipulation is smart about separator - run_cli(cmd + - "--qa_whiteboard=-_app " - "--internal_whiteboard=-security,", bz) + run_cli(cmd + "--whiteboard NEWBIT", bz) bug.refresh() - - assert bug.qa_whiteboard == (initval + "qa") - assert bug.internal_whiteboard == (initval + "internal, foo security1") + statusstr += " NEWBIT" + assert bug.whiteboard == statusstr # Clear whiteboards update = bz.build_update( @@ -646,9 +740,10 @@ def test09Whiteboards(run_cli, backends): bug.refresh() assert bug.whiteboard == "" - assert bug.qa_whiteboard == "" - assert bug.devel_whiteboard == "" - assert bug.internal_whiteboard == "" + if have_dev: + assert bug.qa_whiteboard == "" + assert bug.devel_whiteboard == "" + assert bug.internal_whiteboard == "" def test10Login(run_cli, monkeypatch): @@ -836,27 +931,26 @@ def compare(data, newid): ("You are not allowed" in str(e))) -def test13SubComponents(backends): +def test13SubComponents(run_cli, backends): bz = _open_bz(**backends) - # Long closed RHEL5 lvm2 bug. This component has sub_components - bug = bz.getbug("185526") + bug = _make_subcomponent_bug(run_cli, bz) + bug.autorefresh = True - assert bug.component == "lvm2" + assert bug.component == "Extensions" bz.update_bugs(bug.id, bz.build_update( - component="lvm2", sub_component="Command-line tools (RHEL5)")) + component="Extensions", sub_component="RedHat")) bug.refresh() - assert bug.sub_components == {"lvm2": ["Command-line tools (RHEL5)"]} + assert bug.sub_components == {"Extensions": ["RedHat"]} bz.update_bugs(bug.id, bz.build_update( - component="lvm2", sub_component="Default / Unclassified (RHEL5)")) + component="Extensions", sub_component="AgileTools")) bug.refresh() - assert bug.sub_components == {"lvm2": [ - "Default / Unclassified (RHEL5)"]} + assert bug.sub_components == {"Extensions": ["AgileTools"]} -def _testExternalTrackers(bz): - bugid = 461686 +def _testExternalTrackers(run_cli, bz): + bugid = _BugCache.get_bug(run_cli, bz).id ext_bug_id = 380489 # Delete any existing external trackers to get to a known state @@ -903,10 +997,10 @@ def _testExternalTrackers(bz): assert len(ids) == 0 -def test14ExternalTrackersAddUpdateRemoveQuery(backends): +def test14ExternalTrackersAddUpdateRemoveQuery(run_cli, backends): bz = _open_bz(**backends) try: - _testExternalTrackers(bz) + _testExternalTrackers(run_cli, bz) except Exception as e: if not bz.is_rest(): raise @@ -926,10 +1020,9 @@ def test15EnsureLoggedIn(run_cli, backends): def test16ModifyTags(run_cli, backends): - bugid = "461686" - cmd = "bugzilla modify %s " % bugid bz = _open_bz(**backends) - bug = bz.getbug(bugid) + bug = _BugCache.get_bug(run_cli, bz) + cmd = "bugzilla modify %s " % bug.id try: if bug.tags: From a890ad06e63fb92ce16dbfcf9acca694ee3dd900 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Fri, 8 Sep 2023 11:58:54 -0400 Subject: [PATCH 344/393] Add a pylint exclusion Signed-off-by: Cole Robinson --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 43ac9c2c..747933e0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ persistent=no # can either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). -disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,fixme,global-statement,broad-except,bare-except,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with,consider-using-f-string,unspecified-encoding +disable=Design,Format,Similarities,invalid-name,missing-docstring,locally-disabled,unnecessary-lambda,fixme,global-statement,broad-except,bare-except,wrong-import-position,consider-using-ternary,len-as-condition,no-else-return,useless-object-inheritance,inconsistent-return-statements,consider-using-dict-comprehension,import-outside-toplevel,use-a-generator,consider-using-with,consider-using-f-string,unspecified-encoding,use-implicit-booleaness-not-comparison enable=fixme From 5ffa1b216cd791175e2c7d46880d5277832ce9a1 Mon Sep 17 00:00:00 2001 From: Adam Williamson Date: Fri, 22 Sep 2023 14:17:30 -0700 Subject: [PATCH 345/393] build_update: don't convert 'blocks' or 'depends' to int This is not necessary - Bugzilla, at least the current version on bugzilla.redhat.com, is happy to accept IDs as a string. It also causes a problem: this prevents you from using aliases when setting blocks or depends, which is often very convenient when bugs have predictable aliases (like "F40Changes" or "F39BetaBlocker"). Without this change, you have to do an extra query just to find the ID of the bug. Signed-off-by: Adam Williamson --- bugzilla/base.py | 18 +++++------------- tests/data/mockargs/test_modify2.txt | 2 +- tests/data/mockargs/test_modify5.txt | 4 ++-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index ca638f53..15ccc4f8 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1493,19 +1493,13 @@ def add_dict(key, add, remove, _set=None, convert=None): if add is remove is _set is None: return - def c(val): - val = listify(val) - if convert: - val = [convert(v) for v in val] - return val - newdict = {} if add is not None: - newdict["add"] = c(add) + newdict["add"] = listify(add) if remove is not None: - newdict["remove"] = c(remove) + newdict["remove"] = listify(remove) if _set is not None: - newdict["set"] = c(_set) + newdict["set"] = listify(_set) ret[key] = newdict @@ -1539,10 +1533,8 @@ def c(val): s("comment_tags", comment_tags, listify) s("minor_update", minor_update, bool) - add_dict("blocks", blocks_add, blocks_remove, blocks_set, - convert=int) - add_dict("depends_on", depends_on_add, depends_on_remove, - depends_on_set, convert=int) + add_dict("blocks", blocks_add, blocks_remove, blocks_set) + add_dict("depends_on", depends_on_add, depends_on_remove, depends_on_set) add_dict("cc", cc_add, cc_remove) add_dict("groups", groups_add, groups_remove) add_dict("keywords", keywords_add, keywords_remove, keywords_set) diff --git a/tests/data/mockargs/test_modify2.txt b/tests/data/mockargs/test_modify2.txt index 5ee27d14..3c0315af 100644 --- a/tests/data/mockargs/test_modify2.txt +++ b/tests/data/mockargs/test_modify2.txt @@ -1,5 +1,5 @@ (['123456'], - {'blocks': {'set': [123456, 445566]}, + {'blocks': {'set': ['123456', '445566']}, 'comment': {'comment': 'some example comment', 'is_private': True}, 'component': 'NEWCOMP', 'dupe_of': 555666, diff --git a/tests/data/mockargs/test_modify5.txt b/tests/data/mockargs/test_modify5.txt index 972c2765..5b9b14a3 100644 --- a/tests/data/mockargs/test_modify5.txt +++ b/tests/data/mockargs/test_modify5.txt @@ -2,13 +2,13 @@ {'alias': 'fooalias', 'assigned_to': 'foo@example.com', 'bar': 'foo', - 'blocks': {'add': [1234], 'remove': [1235], 'set': []}, + 'blocks': {'add': ['1234'], 'remove': ['1235'], 'set': []}, 'cc': {'add': ['+bar@example.com'], 'remove': ['steve@example.com']}, 'cf_devel_whiteboard': 'DEVBOARD', 'cf_internal_whiteboard': 'INTBOARD', 'cf_qa_whiteboard': 'QABOARD', 'comment_tags': ['FOOTAG'], - 'depends_on': {'add': [2234], 'remove': [2235], 'set': []}, + 'depends_on': {'add': ['2234'], 'remove': ['2235'], 'set': []}, 'groups': {'add': ['foogroup']}, 'keywords': {'add': ['newkeyword'], 'remove': ['byekeyword'], 'set': []}, 'minor_update': True, From 343f15ebfbe8a942ee028e2c10f6ea132b44b776 Mon Sep 17 00:00:00 2001 From: Pino Toscano Date: Tue, 12 Sep 2023 20:31:39 +0200 Subject: [PATCH 346/393] ci: add dependabot config for GitHub Actions Enable dependabot for the "main" branch, letting it scan for outdated GitHub Actions used in workflows on a weekly base. --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..13e4e05b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: "main" + commit-message: + prefix: "ci" From 2b1281f04f536d4c42c0c2cf3256831cbc0560ef Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Tue, 24 Oct 2023 14:54:34 +0200 Subject: [PATCH 347/393] Use proper REST API route for getting a single bug (fixes #174) (#183) This avoids an `IndexError` in Bugzilla._getbug` and ensures that a `BugzillaError` gets raised e.g. if the bug ID does not exist or the client is not authorized. closes #174 --- bugzilla/_backendrest.py | 17 +++++++++++++++-- tests/test_backend_rest.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_ro_functional.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 tests/test_backend_rest.py diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index a14c4c03..00b5563a 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -107,9 +107,22 @@ def bug_create(self, paramdict): def bug_fields(self, paramdict): return self._get("/field/bug", paramdict) def bug_get(self, bug_ids, aliases, paramdict): + bug_list = listify(bug_ids) + alias_list = listify(aliases) data = paramdict.copy() - data["id"] = listify(bug_ids) - data["alias"] = listify(aliases) + + # FYI: The high-level API expects the backends to raise an exception + # when retrieval of a single bug fails (default behavior of the XMLRPC + # API), but the REST API simply returns an empty search result set. + # To ensure compliant behavior, the REST backend needs to use the + # explicit URL to get a single bug. + if len(bug_list or []) + len(alias_list or []) == 1: + for id_list in (bug_list, alias_list): + if id_list: + return self._get("/bug/%s" % id_list[0], data) + + data["id"] = bug_list + data["alias"] = alias_list ret = self._get("/bug", data) return ret diff --git a/tests/test_backend_rest.py b/tests/test_backend_rest.py new file mode 100644 index 00000000..fdfbd05b --- /dev/null +++ b/tests/test_backend_rest.py @@ -0,0 +1,35 @@ +from types import MethodType + +from bugzilla._backendrest import _BackendREST +from bugzilla._session import _BugzillaSession + + +def test_getbug(): + session = _BugzillaSession(url="http://example.com", + user_agent="py-bugzilla-test", + sslverify=False, + cert=None, + tokencache={}, + api_key="", + is_redhat_bugzilla=False) + backend = _BackendREST(url="http://example.com", + bugzillasession=session) + + def _assertion(self, *args): + self.assertion_called = True + assert args and args[0] == url + + setattr(backend, "_get", MethodType(_assertion, backend)) + + for _ids, aliases, url in ( + (1, None, "/bug/1"), + ([1], [], "/bug/1"), + (None, "CVE-1999-0001", "/bug/CVE-1999-0001"), + ([], ["CVE-1999-0001"], "/bug/CVE-1999-0001"), + (1, "CVE-1999-0001", "/bug"), + ): + backend.assertion_called = False + + backend.bug_get(_ids, aliases, {}) + + assert backend.assertion_called is True diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 4bc31dff..24ffcf8b 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -8,9 +8,12 @@ """ Unit tests that do readonly functional tests against real bugzilla instances. """ +from xmlrpc.client import Fault + import pytest import bugzilla +from bugzilla.exceptions import BugzillaError import tests @@ -295,6 +298,38 @@ def testGetBugAlias(backends): assert bug.bug_id == 720773 +def testGetBug404(backends): + """ + getbug() is expected to raise an error, if a bug ID or alias does not exist + """ + bz = _open_bz(REDHAT_URL, **backends) + + try: + bz.getbug(100000000) + except Fault as error: # XMLRPC API + assert error.faultCode == 101 + except BugzillaError as error: # REST API + assert error.code == 101 + else: + raise AssertionError("No exception raised") + + +def testGetBugAlias404(backends): + """ + getbug() is expected to raise an error, if a bug ID or alias does not exist + """ + bz = _open_bz(REDHAT_URL, **backends) + + try: + bz.getbug("CVE-1234-4321") + except Fault as error: # XMLRPC API + assert error.faultCode == 100 + except BugzillaError as error: # REST API + assert error.code == 100 + else: + raise AssertionError("No exception raised") + + def testQuerySubComponent(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) From 0001f6e4e328c80dabd61116dbfddcdcc12cd413 Mon Sep 17 00:00:00 2001 From: Ricardo Branco Date: Mon, 25 Sep 2023 21:24:02 +0200 Subject: [PATCH 348/393] Avoid duplicate entries when one id is 0 --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 15ccc4f8..4ea036ed 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1099,7 +1099,7 @@ def _alias_or_int(_v): for idval in idlist: idint, alias = _alias_or_int(idval) for bugdict in r["bugs"]: - if idint and idint != bugdict.get("id", None): + if idint is not None and idint != bugdict.get("id", None): continue aliaslist = listify(bugdict.get("alias", None) or []) if alias and alias not in aliaslist: From 0a0bddb3aa542d8b548d32974c60d476dfd47e4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:23:08 +0000 Subject: [PATCH 349/393] ci: bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2af21f8c..f51c690a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: python-version: ["3.6", "3.x"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -53,7 +53,7 @@ jobs: python-version: ["3.x"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 From 80a0316a643d47fe5f769d57c738795e0c2de90b Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Tue, 24 Oct 2023 15:15:33 +0200 Subject: [PATCH 350/393] Removed unused argument from `Bugzilla.add_dict` --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 4ea036ed..b310242a 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1489,7 +1489,7 @@ def s(key, val, convert=None): val = convert(val) ret[key] = val - def add_dict(key, add, remove, _set=None, convert=None): + def add_dict(key, add, remove, _set=None): if add is remove is _set is None: return From 182e0b0ba05c393f2a6ab81e02a1de8c2e04b7a3 Mon Sep 17 00:00:00 2001 From: Ricardo Branco Date: Wed, 20 Sep 2023 18:07:54 +0200 Subject: [PATCH 351/393] Fix API key leak --- bugzilla/_session.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 98064671..017c6d91 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -98,14 +98,14 @@ def request(self, *args, **kwargs): if "timeout" not in kwargs: kwargs["timeout"] = timeout - response = self._session.request(*args, **kwargs) + try: + response = self._session.request(*args, **kwargs) - if self._is_xmlrpc: - # Yes this still appears to matter for properly decoding unicode - # code points in bugzilla.redhat.com content - response.encoding = "UTF-8" + if self._is_xmlrpc: + # This still appears to matter for properly decoding unicode + # code points in bugzilla.redhat.com content + response.encoding = "UTF-8" - try: response.raise_for_status() except requests.HTTPError as e: # Scrape the api key out of the returned exception string From c756f55593f9e37f1a7150ac884884fdeccbd8c2 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 25 Oct 2023 05:56:32 -0400 Subject: [PATCH 352/393] ci: Fix packit RPM build locale errors (#196) Be more thorough trying to find a UTF-8 locale in minimal fedora buildroot Lifted from https://gitlab.com/libosinfo/osinfo-db Signed-off-by: Cole Robinson --- tests/conftest.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a90dfbb9..cfac4671 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,12 +52,16 @@ def pytest_ignore_collect(path, config): def pytest_configure(config): - try: - # Needed for test reproducibility on systems not using a UTF-8 locale - locale.setlocale(locale.LC_ALL, 'C') - locale.setlocale(locale.LC_CTYPE, 'en_US.UTF-8') - except Exception as e: - print("Error setting locale: %s" % str(e)) + # Needed for test reproducibility on any system not using a UTF-8 locale + locale.setlocale(locale.LC_ALL, "C") + for loc in ["C.UTF-8", "C.utf8", "UTF-8", "en_US.UTF-8"]: + try: + locale.setlocale(locale.LC_CTYPE, loc) + break + except locale.Error: + pass + else: + raise locale.Error("No UTF-8 locale found") if config.getoption("--redhat-url"): tests.CLICONFIG.REDHAT_URL = config.getoption("--redhat-url") From def2d28782286c668d473da478f51a263f01f784 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 08:45:03 +0100 Subject: [PATCH 353/393] ci: bump actions/setup-python from 4 to 5 (#198) Bumps actions/setup-python from 4 to 5. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f51c690a..e671b6ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -56,7 +56,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From 629248f036b7624fd59064192afd864fe5ab6499 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Feb 2024 02:58:10 -0500 Subject: [PATCH 354/393] A couple doc fixes (#201) Fix a couple doc issues, as reported here: https://github.com/python-bugzilla/python-bugzilla/issues/176 --------- Signed-off-by: Cole Robinson --- man/bugzilla.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index 38f484bb..54e3b10f 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -355,8 +355,8 @@ Bug assignee QA contact -``--flag`` -^^^^^^^^^^ +``-f, --flag`` +^^^^^^^^^^^^^^ **Syntax:** ``--flag`` FLAG @@ -431,8 +431,8 @@ These options are shared by several commands, for tweaking the text output of the command results. -``-f, --full`` -^^^^^^^^^^^^^^ +``--full`` +^^^^^^^^^^ **Syntax:** ``--full`` @@ -859,4 +859,5 @@ SEE ALSO ======== https://bugzilla.readthedocs.io/en/latest/api/index.html + https://bugzilla.redhat.com/docs/en/html/api/core/v1/bug.html From e5f9d93e57792056a91590603972142f5a176600 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:03:02 +0100 Subject: [PATCH 355/393] ci: bump codecov/codecov-action from 3 to 4 (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
Release notes

Sourced from codecov/codecov-action's releases.

v4.0.0

v4 of the Codecov Action uses the CLI as the underlying upload. The CLI has helped to power new features including local upload, the global upload token, and new upcoming features.

Breaking Changes

  • The Codecov Action runs as a node20 action due to node16 deprecation. See this post from GitHub on how to migrate.
  • Tokenless uploading is unsupported. However, PRs made from forks to the upstream public repos will support tokenless (e.g. contributors to OS projects do not need the upstream repo's Codecov token). This doc shows instructions on how to add the Codecov token.
  • OS platforms have been added, though some may not be automatically detected. To see a list of platforms, see our CLI download page
  • Various arguments to the Action have been changed. Please be aware that the arguments match with the CLI's needs

v3 versions and below will not have access to CLI features (e.g. global upload token, ATS).

What's Changed

... (truncated)

Changelog

Sourced from codecov/codecov-action's changelog.

4.0.0-beta.2

Fixes

  • #1085 not adding -n if empty to do-upload command

4.0.0-beta.1

v4 represents a move from the universal uploader to the Codecov CLI. Although this will unlock new features for our users, the CLI is not yet at feature parity with the universal uploader.

Breaking Changes

  • No current support for aarch64 and alpine architectures.
  • Tokenless uploading is unsuported
  • Various arguments to the Action have been removed

3.1.4

Fixes

  • #967 Fix typo in README.md
  • #971 fix: add back in working dir
  • #969 fix: CLI option names for uploader

Dependencies

  • #970 build(deps-dev): bump @​types/node from 18.15.12 to 18.16.3
  • #979 build(deps-dev): bump @​types/node from 20.1.0 to 20.1.2
  • #981 build(deps-dev): bump @​types/node from 20.1.2 to 20.1.4

3.1.3

Fixes

  • #960 fix: allow for aarch64 build

Dependencies

  • #957 build(deps-dev): bump jest-junit from 15.0.0 to 16.0.0
  • #958 build(deps): bump openpgp from 5.7.0 to 5.8.0
  • #959 build(deps-dev): bump @​types/node from 18.15.10 to 18.15.12

3.1.2

Fixes

  • #718 Update README.md
  • #851 Remove unsupported path_to_write_report argument
  • #898 codeql-analysis.yml
  • #901 Update README to contain correct information - inputs and negate feature
  • #955 fix: add in all the extra arguments for uploader

Dependencies

  • #819 build(deps): bump openpgp from 5.4.0 to 5.5.0
  • #835 build(deps): bump node-fetch from 3.2.4 to 3.2.10
  • #840 build(deps): bump ossf/scorecard-action from 1.1.1 to 2.0.4
  • #841 build(deps): bump @​actions/core from 1.9.1 to 1.10.0
  • #843 build(deps): bump @​actions/github from 5.0.3 to 5.1.1
  • #869 build(deps): bump node-fetch from 3.2.10 to 3.3.0
  • #872 build(deps-dev): bump jest-junit from 13.2.0 to 15.0.0
  • #879 build(deps): bump decode-uri-component from 0.2.0 to 0.2.2

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e671b6ab..cd5c13bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: pytest --cov --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 # Build and install on Windows From e15878007438b9df818beadd3f32c2d5f6130044 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 6 Feb 2024 10:17:23 -0500 Subject: [PATCH 356/393] codecov: workflow v4 requires an upload token (#202) Signed-off-by: Cole Robinson --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd5c13bd..0b617085 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,8 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} # Build and install on Windows From 0955d2e423f1f31c693c97b18a1278422e89df85 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Tue, 6 Feb 2024 16:29:20 +0100 Subject: [PATCH 357/393] Include `alias` in `include_fields` (closes #170) (#186) Include `alias` in `include_fields`, when the parameter for `getbug` is an alias (closes #170) Because the `_getbugs` method tries to return bug data in the same order as IDs and aliases are provided, the `alias` needs to be explicitly added to `include_fields`. --- bugzilla/base.py | 5 +++++ tests/test_api_bug.py | 23 +++++++++++++++++++++++ tests/test_ro_functional.py | 7 +++++++ 3 files changed, 35 insertions(+) diff --git a/bugzilla/base.py b/bugzilla/base.py index b310242a..8d6af295 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1081,6 +1081,11 @@ def _alias_or_int(_v): else: ids.append(idstr) + if (include_fields is not None and aliases + and "alias" not in include_fields): + # Extra field to prevent sorting (see below) from causing an error + include_fields.append("alias") + extra_fields = listify(extra_fields or []) extra_fields += self._getbug_extra_fields() diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index 47391d46..61572fc5 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -94,6 +94,29 @@ def test_api_getbugs(): assert fakebz.getbugs(["123456", "CVE-1234-FAKE"]) == [] +def test_getbug_alias(): + """ + Test that `getbug()` includes the alias in `include_fields` + """ + fakebz = tests.mockbackend.make_bz( + bug_get_args=None, + bug_get_return="data/mockreturn/test_query_cve_getbug.txt") + bug = fakebz.getbug("CVE-1234-5678", include_fields=["id"]) + assert bug.alias == ["CVE-1234-5678"] + assert bug.id == 123456 + + def mock_bug_get(bug_ids, aliases, paramdict): + assert bug_ids == [] + assert aliases == ["CVE-1234-5678"] + assert "alias" in paramdict.get("include_fields", []) + return {"bugs": [bug.get_raw_data()]} + + backend = getattr(fakebz, "_backend") + setattr(backend, "bug_get", mock_bug_get) + + fakebz.getbug("CVE-1234-5678", include_fields=["id"]) + + def test_bug_getattr(): fakebz = tests.mockbackend.make_bz( bug_get_args=None, diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 24ffcf8b..9c6f75d0 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -330,6 +330,13 @@ def testGetBugAlias404(backends): raise AssertionError("No exception raised") +def testGetBugAliasIncludedField(backends): + bz = _open_bz(REDHAT_URL, **backends) + + bug = bz.getbug("CVE-2011-2527", include_fields=["id"]) + assert bug.bug_id == 720773 + + def testQuerySubComponent(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) From 473da017066ab960d4a87d1c25092e2c40c626c1 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Thu, 8 Feb 2024 17:44:36 +0100 Subject: [PATCH 358/393] Run pylint as separate GH action (#193) ... and allow lines to be 100 characters long. This closes python-bugzilla#185 --- .github/workflows/build.yml | 20 ++++++++++++++++++++ .pylintrc | 2 +- test-requirements.txt | 2 ++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b617085..6369f0be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,26 @@ name: CI on: [push, pull_request] jobs: + linter: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.x"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r test-requirements.txt setuptools + - name: Lint + run: | + pylint --output-format colorized --rcfile .pylintrc \ + bugzilla-cli setup.py bugzilla examples tests + build: # We stick with 20.04 to get access to python 3.6 # https://github.com/actions/setup-python/issues/544 diff --git a/.pylintrc b/.pylintrc index 747933e0..378199f1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -19,7 +19,7 @@ score=no [FORMAT] # Maximum number of characters on a single line. -max-line-length=80 +max-line-length=100 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). diff --git a/test-requirements.txt b/test-requirements.txt index c588a62a..6abde2bb 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,4 @@ # additional packages needed for testing pytest +pylint<3.1 +pycodestyle<2.12 From 5f0727b33fac1f0428ef1c6f568a9702bcd9ba07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:46:56 +0000 Subject: [PATCH 359/393] ci: bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6369f0be..8c8d6cf8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: matrix: python-version: ["3.x"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: From 41f357030cbbc923580de086133d6eebe44f48fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:47:00 +0000 Subject: [PATCH 360/393] ci: bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c8d6cf8..cd92d9d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From f95ff31d76463fbb05d49fe624f63d3b27c3ef72 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 5 Feb 2024 16:04:15 -0500 Subject: [PATCH 361/393] cli: Add `--field-json=JSONSTRING` This is similar to the `--field` option for interacting with custom bugzilla fields, but it takes a full JSON string as input. This lets users set complex values like arrays, dict mappings, that otherwise were not settable with plain `--field` Fixes: https://github.com/python-bugzilla/python-bugzilla/issues/163 Signed-off-by: Cole Robinson --- bugzilla/_cli.py | 32 ++++++++++++++++++++-------- man/bugzilla.rst | 11 +++++++++- tests/data/mockargs/test_modify5.txt | 2 ++ tests/data/mockargs/test_new2.txt | 2 ++ tests/data/mockargs/test_query6.txt | 2 ++ tests/test_cli_modify.py | 1 + tests/test_cli_new.py | 1 + tests/test_cli_query.py | 9 +++++++- 8 files changed, 49 insertions(+), 11 deletions(-) diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index d0688809..e14c1df4 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -263,6 +263,10 @@ def _parser_add_bz_fields(rootp, command): "the raw name used by the bugzilla instance. For example, if your " "bugzilla instance has a custom field cf_my_field, do:\n" " --field cf_my_field=VALUE") + p.add_argument('--field-json', + metavar="JSONSTRING", action="append", dest="field_jsons", + help="Specify --field data as a JSON string. Example: --field-json " + '\'{"cf_my_field": "VALUE", "cf_array_field": [1, 2]}\'') if not cmd_modify: _parser_add_output_options(rootp) @@ -436,15 +440,28 @@ def setup_parser(): # Command routines # #################### -def _merge_field_opts(query, fields, parser): +def _merge_field_opts(query, fields, field_jsons, parser): + values = {} + # Add any custom fields if specified - for f in fields: + for f in (fields or []): try: f, v = f.split('=', 1) - query[f] = v + values[f] = v except Exception: parser.error("Invalid field argument provided: %s" % (f)) + for j in (field_jsons or []): + try: + jvalues = json.loads(j) + values.update(jvalues) + except Exception as e: + parser.error("Invalid field-json value=%s: %s" % (j, e)) + + if values: + log.debug("parsed --field* values: %s", values) + query.update(values) + def _do_query(bz, opt, parser): q = {} @@ -599,8 +616,7 @@ def _do_query(bz, opt, parser): kwopts["tags"] = opt.tags built_query = bz.build_query(**kwopts) - if opt.fields: - _merge_field_opts(built_query, opt.fields, parser) + _merge_field_opts(built_query, opt.fields, opt.field_jsons, parser) built_query.update(q) q = built_query @@ -907,8 +923,7 @@ def parse_multi(val): kwopts["comment_private"] = opt.private ret = bz.build_createbug(**kwopts) - if opt.fields: - _merge_field_opts(ret, opt.fields, parser) + _merge_field_opts(ret, opt.fields, opt.field_jsons, parser) b = bz.createbug(ret) b.refresh() @@ -1049,8 +1064,7 @@ def _do_modify(bz, parser, opt): if not v[0] and not v[1]: del wbmap[k] - if opt.fields: - _merge_field_opts(update, opt.fields, parser) + _merge_field_opts(update, opt.fields, opt.field_jsons, parser) log.debug("update bug dict=%s", update) log.debug("update whiteboard dict=%s", wbmap) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index 54e3b10f..c814e519 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -416,13 +416,22 @@ RHBZ 'Fixed in version' field ``--field`` ^^^^^^^^^^^ -**Syntax:** ``--field`` FIELD`` VALUE +**Syntax:** ``--field`` FIELD=VALUE Manually specify a bugzilla API field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: --field cf_my_field=VALUE +``--field-json`` +^^^^^^^^^^^^^^^^ + +**Syntax:** ``--field-json`` JSONSTRING + +Specify --field data as a JSON string. Example: +--field-json '{"cf_my_field": "VALUE", "cf_array_field": [1, 2]}' + + Output options ============== diff --git a/tests/data/mockargs/test_modify5.txt b/tests/data/mockargs/test_modify5.txt index 5b9b14a3..b21a6ee2 100644 --- a/tests/data/mockargs/test_modify5.txt +++ b/tests/data/mockargs/test_modify5.txt @@ -4,9 +4,11 @@ 'bar': 'foo', 'blocks': {'add': ['1234'], 'remove': ['1235'], 'set': []}, 'cc': {'add': ['+bar@example.com'], 'remove': ['steve@example.com']}, + 'cf_blah': {'1': 2}, 'cf_devel_whiteboard': 'DEVBOARD', 'cf_internal_whiteboard': 'INTBOARD', 'cf_qa_whiteboard': 'QABOARD', + 'cf_verified': ['Tested'], 'comment_tags': ['FOOTAG'], 'depends_on': {'add': ['2234'], 'remove': ['2235'], 'set': []}, 'groups': {'add': ['foogroup']}, diff --git a/tests/data/mockargs/test_new2.txt b/tests/data/mockargs/test_new2.txt index 5bd147e2..a3636ead 100644 --- a/tests/data/mockargs/test_new2.txt +++ b/tests/data/mockargs/test_new2.txt @@ -2,6 +2,8 @@ 'assigned_to': 'foo@example.com', 'blocks': ['12345', '6789'], 'cc': ['foo@example.com', 'bar@example.com'], + 'cf_blah': {'1': 2}, + 'cf_verified': ['Tested'], 'comment_is_private': True, 'comment_tags': ['FOO'], 'component': 'FOOCOMP', diff --git a/tests/data/mockargs/test_query6.txt b/tests/data/mockargs/test_query6.txt index 78b18f48..b3a21252 100644 --- a/tests/data/mockargs/test_query6.txt +++ b/tests/data/mockargs/test_query6.txt @@ -1,6 +1,8 @@ {'BAR': 'WIBBLE', 'FOO': '1', 'bug_status': ['VERIFIED', 'RELEASE_PENDING', 'CLOSED'], + 'cf_blah': {'1': 2}, + 'cf_verified': ['Tested'], 'include_fields': ['assigned_to', 'blocks', 'component', diff --git a/tests/test_cli_modify.py b/tests/test_cli_modify.py index ed5519c3..e3731451 100644 --- a/tests/test_cli_modify.py +++ b/tests/test_cli_modify.py @@ -85,6 +85,7 @@ def test_modify(run_cli): cmd += "--devel_whiteboard =DEVBOARD --internal_whiteboard =INTBOARD " cmd += "--qa_whiteboard =QABOARD " cmd += "--comment-tag FOOTAG --field bar=foo " + cmd += '--field-json \'{"cf_verified": ["Tested"], "cf_blah": {"1": 2}}\' ' cmd += "--minor-update " fakebz = tests.mockbackend.make_bz(rhbz=True, bug_update_args="data/mockargs/test_modify5.txt", diff --git a/tests/test_cli_new.py b/tests/test_cli_new.py index 5e4742be..86e153ab 100644 --- a/tests/test_cli_new.py +++ b/tests/test_cli_new.py @@ -41,6 +41,7 @@ def test_new(run_cli): cmd += "--assignee foo@example.com --qa_contact qa@example.com " cmd += "--comment-tag FOO " cmd += "--field foo=bar " + cmd += '--field-json \'{"cf_verified": ["Tested"], "cf_blah": {"1": 2}}\' ' fakebz = tests.mockbackend.make_bz( bug_create_args="data/mockargs/test_new2.txt", diff --git a/tests/test_cli_query.py b/tests/test_cli_query.py index 98c6028b..5a751a53 100644 --- a/tests/test_cli_query.py +++ b/tests/test_cli_query.py @@ -15,12 +15,18 @@ ################################# def test_query(run_cli): - # bad field option + # bad --field option fakebz = tests.mockbackend.make_bz() cmd = "bugzilla query --field FOO" out = run_cli(cmd, fakebz, expectfail=True) assert "Invalid field argument" in out + # bad --field-json option + fakebz = tests.mockbackend.make_bz() + cmd = "bugzilla query --field-json='{1: 2}'" + out = run_cli(cmd, fakebz, expectfail=True) + assert "Invalid field-json" in out + # Simple query with some comma opts cmd = "bugzilla query " cmd += "--product foo --component foo,bar --bug_id 1234,2480" @@ -104,6 +110,7 @@ def test_query(run_cli): # Test --status EOL and --oneline, and some --field usage cmd = "bugzilla query --status EOL --oneline " cmd += "--field FOO=1 --field=BAR=WIBBLE " + cmd += '--field-json \'{"cf_verified": ["Tested"], "cf_blah": {"1": 2}}\' ' fakebz = tests.mockbackend.make_bz( bug_search_args="data/mockargs/test_query6.txt", bug_search_return="data/mockreturn/test_getbug_rhel.txt", From b9eaa598a82d1b00ff041af3119855a0dbdd1264 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 24 Oct 2023 10:47:32 -0400 Subject: [PATCH 362/393] session: Fix API leak pt2 Between the time 182e0b0ba0 was written, 138caf8aa72 was committed which inadvertently loosened up the api_key scraping. Go back to catching `Exception` so we try harder to scrape api_key from error messages. Signed-off-by: Cole Robinson --- bugzilla/_session.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bugzilla/_session.py b/bugzilla/_session.py index 017c6d91..ff00f3f5 100644 --- a/bugzilla/_session.py +++ b/bugzilla/_session.py @@ -107,12 +107,15 @@ def request(self, *args, **kwargs): response.encoding = "UTF-8" response.raise_for_status() - except requests.HTTPError as e: + except Exception as e: # Scrape the api key out of the returned exception string message = str(e).replace(self._api_key or "", "") - response = getattr(e, "response", None) - raise BugzillaHTTPError(message, response=response).with_traceback( - sys.exc_info()[2] - ) + if isinstance(e, requests.HTTPError): + response = getattr(e, "response", None) + raise BugzillaHTTPError( + message, response=response).with_traceback( + sys.exc_info()[2]) + raise type(e)(message).with_traceback(sys.exc_info()[2]) + return response From 70c6ad300b9e87dbed5be40837a14a0eff2e9791 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 24 Oct 2023 10:26:29 -0400 Subject: [PATCH 363/393] tests: confirm API key doesn't leak on connection error Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 9c6f75d0..df7f9e5d 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -74,6 +74,17 @@ def test_rest_xmlrpc_detection(): def test_apikey_error_scraping(): # Ensure the API key does not leak into any requests exceptions fakekey = "FOOBARMYKEY" + + with pytest.raises(Exception) as e: + _open_bz("https://bugzilla.redhat.nopedontexist", + force_rest=True, api_key=fakekey) + assert fakekey not in str(e.value) + + with pytest.raises(Exception) as e: + _open_bz("https://bugzilla.redhat.nopedontexist", + force_xmlrpc=True, api_key=fakekey) + assert fakekey not in str(e.value) + with pytest.raises(Exception) as e: _open_bz("https://httpstat.us/502&foo", force_xmlrpc=True, api_key=fakekey) From 7359b339c4395b5e8c6e7a63e189d60f595e89ab Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 14 Feb 2024 11:13:24 -0500 Subject: [PATCH 364/393] base: Tweak `query()` docs - Drop outdated references - point to `build_query` Signed-off-by: Cole Robinson --- bugzilla/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 8d6af295..23cdc7ba 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1314,11 +1314,11 @@ def add_email(key, value, count): def query(self, query): """ - Query bugzilla and return a list of matching bugs. - query must be a dict with fields like those in in querydata['fields']. - Returns a list of Bug objects. - Also see the _query() method for details about the underlying - implementation. + Pass search terms to bugzilla and and return a list of matching + Bug objects. + + See `build_query` for more details about constructing the + `query` dict parameter. """ try: r = self._backend.bug_search(query) From 794865f9c45d53b4e92252ca2777ad5dd83b7d48 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 14 Feb 2024 10:46:41 -0500 Subject: [PATCH 365/393] base: Add query_return_extra This is like `query()`, but the return value is altered to be (buglist, values), where `values` is raw dictionary output from the API call, excluding the bug content. For example this may include a `limit` value if the bugzilla instance puts an implied limit on returned result numbers. bugzilla.redhat.com also has a custom extension to report `total_matches` for a query, which lets user know if the returned results are complete or not Signed-off-by: Cole Robinson --- bugzilla/base.py | 33 +++++++++++++++++++-------- tests/data/mockreturn/test_query1.txt | 2 +- tests/test_api_misc.py | 9 ++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index 23cdc7ba..a30ec2cb 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1312,13 +1312,14 @@ def add_email(key, value, count): self.pre_translation(query) return query - def query(self, query): - """ - Pass search terms to bugzilla and and return a list of matching - Bug objects. - See `build_query` for more details about constructing the - `query` dict parameter. + def query_return_extra(self, query): + """ + Same as `query()`, but the return value is altered to be + (buglist, values), where `values` is raw dictionary output from + the API call, excluding the bug content. For example this may + include a `limit` value if the bugzilla instance puts an implied + limit on returned result numbers. """ try: r = self._backend.bug_search(query) @@ -1334,9 +1335,23 @@ def query(self, query): "appear to support API queries derived from bugzilla " "web URL queries." % e) from None - log.debug("Query returned %s bugs", len(r['bugs'])) - return [Bug(self, dict=b, - autorefresh=self.bug_autorefresh) for b in r['bugs']] + rawbugs = r.pop("bugs") + log.debug("Query returned %s bugs", len(rawbugs)) + bugs = [Bug(self, dict=b, + autorefresh=self.bug_autorefresh) for b in rawbugs] + + return bugs, r + + def query(self, query): + """ + Pass search terms to bugzilla and and return a list of matching + Bug objects. + + See `build_query` for more details about constructing the + `query` dict parameter. + """ + bugs, dummy = self.query_return_extra(query) + return bugs def pre_translation(self, query): """ diff --git a/tests/data/mockreturn/test_query1.txt b/tests/data/mockreturn/test_query1.txt index d1bdac5c..4f7019b2 100644 --- a/tests/data/mockreturn/test_query1.txt +++ b/tests/data/mockreturn/test_query1.txt @@ -1 +1 @@ -{'bugs': [{'assigned_to_detail': {'real_name': 'Libvirt Maintainers', 'email': 'libvirt-maint', 'name': 'libvirt-maint', 'id': 311982}, 'summary': 'RFE: qemu: Support a managed autoconnect mode for host USB devices', 'status': 'NEW', 'assigned_to': 'Libvirt Maintainers', 'id': 508645}, {'assigned_to_detail': {'real_name': 'Cole Robinson', 'email': 'crobinso', 'name': 'crobinso', 'id': 199727}, 'summary': 'RFE: warn users at guest start if networks/storage pools are inactive', 'status': 'NEW', 'assigned_to': 'Cole Robinson', 'id': 668543}]} +{'bugs': [{'assigned_to_detail': {'real_name': 'Libvirt Maintainers', 'email': 'libvirt-maint', 'name': 'libvirt-maint', 'id': 311982}, 'summary': 'RFE: qemu: Support a managed autoconnect mode for host USB devices', 'status': 'NEW', 'assigned_to': 'Libvirt Maintainers', 'id': 508645}, {'assigned_to_detail': {'real_name': 'Cole Robinson', 'email': 'crobinso', 'name': 'crobinso', 'id': 199727}, 'summary': 'RFE: warn users at guest start if networks/storage pools are inactive', 'status': 'NEW', 'assigned_to': 'Cole Robinson', 'id': 668543}], 'limit': 0, 'FOOFAKEVALUE': 'hello'} diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index dfb0e923..ea1f2e47 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -305,3 +305,12 @@ def test_query_url_fail(): bz.query(query) except Exception as e: assert checkstr not in str(e) + + +def test_query_return_extra(): + bz = tests.mockbackend.make_bz(version="5.1.0", + bug_search_args=None, + bug_search_return="data/mockreturn/test_query1.txt") + dummy, extra = bz.query_return_extra({}) + assert extra['limit'] == 0 + assert extra['FOOFAKEVALUE'] == "hello" From 46f6cc38ecbd541c887accd571d074d41086351b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Tue, 13 Feb 2024 16:01:22 -0500 Subject: [PATCH 366/393] examples: add redhat_query_all.py In late 2021, bugzilla.redhat.com changed query() results to default returning only 20 bugs. If the user passes in limit=0, that number changes to 1000, but is still capped if the query would return more than that. There's a discussion here with multiple proposed workarounds: https://github.com/python-bugzilla/python-bugzilla/issues/149 This demonstrates the one that takes the least amount of code IMO. It uses `ids_only=True`, which is a custom bugzilla.redhat.com query feature to bypass the query limit by only returning matching bug IDs. Signed-off-by: Cole Robinson --- examples/redhat_query_all.py | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 examples/redhat_query_all.py diff --git a/examples/redhat_query_all.py b/examples/redhat_query_all.py new file mode 100644 index 00000000..9ec5f26f --- /dev/null +++ b/examples/redhat_query_all.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +# redhat_query_all.py: Perform a few varieties of queries + +import bugzilla + +# public test instance of bugzilla.redhat.com. It's okay to make changes +URL = "bugzilla.stage.redhat.com" + +bzapi = bugzilla.Bugzilla(URL) + + +# In late 2021, bugzilla.redhat.com changed query() results to default to +# returning only 20 bugs. If the user passes in limit=0, that number changes +# to 1000, but is still capped if the query would return more than that. +# +# There's a discussion here with multiple proposed ways to work around it: +# https://github.com/python-bugzilla/python-bugzilla/issues/149 +# +# This method uses ids_only=True, which is a custom bugzilla.redhat.com +# query feature to bypass the query limit by only returning matching bug IDs. +# rhbz feature bug: https://bugzilla.redhat.com/show_bug.cgi?id=2005153 + + +# As of Feb 2024 this 1300+ bugs, which would have hit the query limit of 1000 +query = bzapi.build_query( + product="Fedora", + component="virt-manager") +# Request the bugzilla.redhat.com extension ids_only=True to bypass limit +query["ids_only"] = True + +queried_bugs = bzapi.query(query) +ids = [bug.id for bug in queried_bugs] +print(f"Queried {len(ids)} ids") + + +# Use getbugs to fetch the full list. getbugs is not affected by +# default RHBZ limits. However, requesting too much data via getbugs +# will timeout. This paginates the lookup to query 1000 bugs at a time. +# +# We also limit the returned data to just give us the `summary`. +# You should always limit your queries with include_fields` to only return +# the data you need. +count = 0 +pagesize = 1000 +include_fields = ["summary"] +while count < len(ids): + idslice = ids[count:(count + pagesize)] + print(f"Fetching data for bugs {count}-{count+len(idslice)-1}") + bugs = bzapi.getbugs(idslice, include_fields=include_fields) + print(f"Fetched {len(bugs)} bugs") + count += pagesize From c2b4fedda606ef91202941de70916fb1a32d8add Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 25 Feb 2024 15:44:41 -0500 Subject: [PATCH 367/393] cli: Support `--field` and `--field-json` for `bugzilla attach` (#206) Enables option passthrough for adding attachments too Resolves: https://github.com/python-bugzilla/python-bugzilla/issues/169 --------- Signed-off-by: Cole Robinson Co-authored-by: Andreas Hasenkopf --- bugzilla/_cli.py | 29 +++++++++++++++++----------- tests/data/mockargs/test_attach3.txt | 9 +++++++++ tests/test_cli_attach.py | 12 ++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 tests/data/mockargs/test_attach3.txt diff --git a/bugzilla/_cli.py b/bugzilla/_cli.py index e14c1df4..02d6a367 100755 --- a/bugzilla/_cli.py +++ b/bugzilla/_cli.py @@ -185,6 +185,19 @@ def _parser_add_output_options(p): "section 'Output options' for more details.") +def _parser_add_field_passthrough_opts(p): + p.add_argument('--field', + metavar="FIELD=VALUE", action="append", dest="fields", + help="Manually specify a bugzilla API field. FIELD is " + "the raw name used by the bugzilla instance. For example, if your " + "bugzilla instance has a custom field cf_my_field, do:\n" + " --field cf_my_field=VALUE") + p.add_argument('--field-json', + metavar="JSONSTRING", action="append", dest="field_jsons", + help="Specify --field data as a JSON string. Example: --field-json " + '\'{"cf_my_field": "VALUE", "cf_array_field": [1, 2]}\'') + + def _parser_add_bz_fields(rootp, command): cmd_new = (command == "new") cmd_query = (command == "query") @@ -256,17 +269,7 @@ def _parser_add_bz_fields(rootp, command): p.add_argument('-F', '--fixed_in', help="RHBZ 'Fixed in version' field") - # Put this at the end, so it sticks out more - p.add_argument('--field', - metavar="FIELD=VALUE", action="append", dest="fields", - help="Manually specify a bugzilla API field. FIELD is " - "the raw name used by the bugzilla instance. For example, if your " - "bugzilla instance has a custom field cf_my_field, do:\n" - " --field cf_my_field=VALUE") - p.add_argument('--field-json', - metavar="JSONSTRING", action="append", dest="field_jsons", - help="Specify --field data as a JSON string. Example: --field-json " - '\'{"cf_my_field": "VALUE", "cf_array_field": [1, 2]}\'') + _parser_add_field_passthrough_opts(p) if not cmd_modify: _parser_add_output_options(rootp) @@ -405,6 +408,8 @@ def _setup_action_attach_parser(subparsers): p.add_argument('--private', action='store_true', default=False, help='Mark new comment as private') + _parser_add_field_passthrough_opts(p) + def _setup_action_login_parser(subparsers): usage = 'bugzilla login [--api-key] [username [password]]' @@ -1172,6 +1177,8 @@ def _do_set_attach(bz, opt, parser): kwargs["is_private"] = True desc = opt.desc or os.path.basename(fileobj.name) + _merge_field_opts(kwargs, opt.fields, opt.field_jsons, parser) + # Upload attachments for bugid in opt.ids: attid = bz.attachfile(bugid, fileobj, desc, **kwargs) diff --git a/tests/data/mockargs/test_attach3.txt b/tests/data/mockargs/test_attach3.txt new file mode 100644 index 00000000..48da7d7a --- /dev/null +++ b/tests/data/mockargs/test_attach3.txt @@ -0,0 +1,9 @@ +(['123456'], + 'STRIPPED-BY-TESTSUITE', + {'content_type': 'text/plain', + 'file_name': 'bz-attach-get1.txt', + 'flags': [{'name': 'review', + 'requestee': 'crobinso@redhat.com', + 'status': '-'}], + 'is_obsolete': '1', + 'summary': 'bz-attach-get1.txt'}) diff --git a/tests/test_cli_attach.py b/tests/test_cli_attach.py index d287bc54..584858f1 100644 --- a/tests/test_cli_attach.py +++ b/tests/test_cli_attach.py @@ -49,6 +49,18 @@ def test_attach(run_cli): out = run_cli(cmd, fakebz, stdin=attachcontent) assert "Created attachment 1557949 on bug 123456" in out + # Test --field passthrough + cmd = "bugzilla attach 123456 --file=%s " % attachfile + cmd += "--field=is_obsolete=1 " + cmd += "--field-json " + cmd += ('\'{"flags": [{"name": "review"' + ', "requestee": "crobinso@redhat.com", "status": "-"}]}\'') + fakebz = tests.mockbackend.make_bz( + bug_attachment_create_args="data/mockargs/test_attach3.txt", + bug_attachment_create_return={'ids': [1557949]}) + out = run_cli(cmd, fakebz) + assert "Created attachment 1557949 on bug 123456" in out + def _test_attach_get(run_cli): # Hit error when using ids with --get* From 9e9d39bf8f76079887cfee6a5336555c3578eb00 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Thu, 13 Jun 2024 14:16:20 +0200 Subject: [PATCH 368/393] Allow bug creation with an explicitly empty list of groups (closes #210) This allows the caller of this method to create a bug with no groups assigned, even if server-side default group assignments are configured. --- bugzilla/base.py | 2 +- tests/test_base.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/test_base.py diff --git a/bugzilla/base.py b/bugzilla/base.py index a30ec2cb..eef84aba 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1761,7 +1761,7 @@ def build_createbug(self, localdict["cc"] = listify(cc) if depends_on: localdict["depends_on"] = listify(depends_on) - if groups: + if groups is not None: localdict["groups"] = listify(groups) if keywords: localdict["keywords"] = listify(keywords) diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 00000000..d62428e5 --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,20 @@ +from bugzilla.base import Bugzilla + + +def test_build_createbug(): + bz = Bugzilla(url=None) + + args = {"product": "Ubuntu 33⅓", "summary": "Hello World", "alias": "CVE-2024-0000"} + result = bz.build_createbug(**args) + assert result == args + + result = bz.build_createbug(groups=None, **args) + assert result == args + + args["groups"] = [] + result = bz.build_createbug(**args) + assert result == args + + args["groups"] += ["the-group"] + result = bz.build_createbug(**args) + assert result == args From efd2c174bd03dc2e1cecb889b0c2bf4f10ce7f02 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 7 Feb 2024 08:59:16 -0500 Subject: [PATCH 369/393] man: Add section about `bugzillarc` Fixes: https://github.com/python-bugzilla/python-bugzilla/issues/175 Signed-off-by: Cole Robinson --- man/bugzilla.rst | 59 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/man/bugzilla.rst b/man/bugzilla.rst index c814e519..09ff4132 100644 --- a/man/bugzilla.rst +++ b/man/bugzilla.rst @@ -791,6 +791,46 @@ List the versions for the given product Only show active components. Combine with --components* +``bugzillarc`` CONFIG FILE +========================== + +Both ``bugzilla`` and the python-bugzilla library will read +a ``bugzillarc`` config file if it is present in the following +locations: + +- /etc/bugzillarc +- ~/.bugzillarc +- ~/.config/python-bugzilla/bugzillarc + +The contents of the files are processed and merged together +in the order they are listed above. + +The main usage for ``bugzillarc`` is to store API keys for your +bugzilla URLs: + +:: + + [bugzilla.example.com] + api_key=INSERT-YOUR-API-KEY-HERE + + [bugzilla.redhat.com] + api_key=MY-REDHAT-API-KEY-BLAH + + +The sections must be hostnames. Other values that can be +set per hostname section are + +- ``user``: default auth username +- ``password``: default auth password +- ``cert``: default client side certificate + + +A ``[DEFAULTS]`` section is also accepted, which takes the following +values: + +- ``url``: default bugzilla URL + + AUTHENTICATION CACHE AND API KEYS ================================= @@ -802,28 +842,17 @@ active login. If you are connecting to a bugzilla 5.0 or later instance, the best option is to use bugzilla API keys. From the bugzilla web UI, log in, navigate to Preferences->API Keys, and generate a key (it will be a long -string of characters and numbers). Then create a -~/.config/python-bugzilla/bugzillarc like this: - -:: - - $ cat ~/.config/python-bugzilla/bugzillarc - - [bugzilla.example.com] - api_key=YOUR_API_KEY - -Replace 'bugzilla.example.com' with your bugzilla host name, and -YOUR_API_KEY with the generated API Key from the Web UI. +string of characters and numbers). -Alternatively, you can use 'bugzilla login --api-key', which will ask -for the API key, and save it to bugzillarc for you. +Then use 'bugzilla --bugzilla URL login --api-key', which will ask +for the API key, and save it to ``bugzillarc`` for you. For older bugzilla instances, you will need to cache a login token with the "login" subcommand or the "--login" argument. Additionally, the --no-cache-credentials option will tell the bugzilla tool to *not* save or use any authentication cache, including the -bugzillarc file. +``bugzillarc`` file. EXAMPLES From 178fb6fc7fd05877520d8594630317cebc60532b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 4 Sep 2024 14:14:27 -0400 Subject: [PATCH 370/393] man: Regenerate bugzilla.1 Signed-off-by: Cole Robinson --- man/bugzilla.1 | 116 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/man/bugzilla.1 b/man/bugzilla.1 index fd83d516..fba29568 100644 --- a/man/bugzilla.1 +++ b/man/bugzilla.1 @@ -1,8 +1,5 @@ .\" Man page generated from reStructuredText. . -.TH BUGZILLA 1 "" "" "User Commands" -.SH NAME -bugzilla \- command line tool for interacting with Bugzilla . .nr rst2man-indent-level 0 . @@ -30,6 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. +.TH "BUGZILLA" 1 "" "" "User Commands" +.SH NAME +bugzilla \- command line tool for interacting with Bugzilla .SH SYNOPSIS .sp \fBbugzilla\fP [\fIoptions\fP] [\fIcommand\fP] [\fIcommand\-options\fP] @@ -86,7 +86,7 @@ client side certificate file needed by the webserver. .sp \fBSyntax:\fP \fB\-\-login\fP .sp -Run interactive "login" before performing the specified command. +Run interactive \(dqlogin\(dq before performing the specified command. .SS \fB\-\-username\fP .sp \fBSyntax:\fP \fB\-\-username\fP USERNAME @@ -114,13 +114,8 @@ they expire the tool errors, rather than subtly change output. .sp \fBSyntax:\fP \fB\-\-no\-cache\-credentials\fP .sp -Don\(aqt save any bugzilla cookies or tokens to disk, and don\(aqt use any +Don\(aqt save any bugzilla tokens to disk, and don\(aqt use any pre\-existing credentials. -.SS \fB\-\-cookiefile\fP -.sp -\fBSyntax:\fP \fB\-\-cookiefile\fP COOKIEFILE -.sp -cookie file to use for bugzilla authentication .SS \fB\-\-tokenfile\fP .sp \fBSyntax:\fP \fB\-\-tokenfile\fP TOKENFILE @@ -260,7 +255,7 @@ Bug assignee \fBSyntax:\fP \fB\-\-qa_contact\fP QA_CONTACT .sp QA contact -.SS \fB\-\-flag\fP +.SS \fB\-f, \-\-flag\fP .sp \fBSyntax:\fP \fB\-\-flag\fP FLAG .sp @@ -299,16 +294,22 @@ RHBZ QA whiteboard field RHBZ \(aqFixed in version\(aq field .SS \fB\-\-field\fP .sp -\fBSyntax:\fP \fB\-\-field\fP FIELD\(ga\(ga VALUE +\fBSyntax:\fP \fB\-\-field\fP FIELD=VALUE .sp Manually specify a bugzilla API field. FIELD is the raw name used by the bugzilla instance. For example if your bugzilla instance has a custom field cf_my_field, do: \-\-field cf_my_field=VALUE +.SS \fB\-\-field\-json\fP +.sp +\fBSyntax:\fP \fB\-\-field\-json\fP JSONSTRING +.sp +Specify \-\-field data as a JSON string. Example: +\-\-field\-json \(aq{\(dqcf_my_field\(dq: \(dqVALUE\(dq, \(dqcf_array_field\(dq: [1, 2]}\(aq .SH OUTPUT OPTIONS .sp These options are shared by several commands, for tweaking the text output of the command results. -.SS \fB\-f, \-\-full\fP +.SS \fB\-\-full\fP .sp \fBSyntax:\fP \fB\-\-full\fP .sp @@ -379,7 +380,7 @@ the formats are not stable and are subject to change. format. For example, to print a returned bug ID, component, and product, separated with ::, do: .sp -\-\-outputformat "%{id}::%{component}::%{product}" +\-\-outputformat \(dq%{id}::%{component}::%{product}\(dq .sp The fields (like \(aqid\(aq, \(aqcomponent\(aq, etc.) are the names of the values returned by bugzilla\(aqs API. To see a list of all fields, @@ -542,44 +543,78 @@ List the versions for the given product \fBSyntax:\fP \fB\-\-active\-components\fP .sp Only show active components. Combine with \-\-components* -.SH AUTHENTICATION CACHE AND API KEYS +.SH BUGZILLARC CONFIG FILE .sp -Some command usage will require an active login to the bugzilla -instance. For example, if the bugzilla instance has some private bugs, -those bugs will be missing from \(aqquery\(aq output if you do not have an -active login. +Both \fBbugzilla\fP and the python\-bugzilla library will read +a \fBbugzillarc\fP config file if it is present in the following +locations: +.INDENT 0.0 +.IP \(bu 2 +/etc/bugzillarc +.IP \(bu 2 +~/.bugzillarc +.IP \(bu 2 +~/.config/python\-bugzilla/bugzillarc +.UNINDENT .sp -If you are connecting to a bugzilla 5.0 or later instance, the best -option is to use bugzilla API keys. From the bugzilla web UI, log in, -navigate to Preferences\->API Keys, and generate a key (it will be a long -string of characters and numbers). Then create a -~/.config/python\-bugzilla/bugzillarc like this: +The contents of the files are processed and merged together +in the order they are listed above. +.sp +The main usage for \fBbugzillarc\fP is to store API keys for your +bugzilla URLs: .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C -$ cat ~/.config/python\-bugzilla/bugzillarc - [bugzilla.example.com] -api_key=YOUR_API_KEY +api_key=INSERT\-YOUR\-API\-KEY\-HERE + +[bugzilla.redhat.com] +api_key=MY\-REDHAT\-API\-KEY\-BLAH .ft P .fi .UNINDENT .UNINDENT .sp -Replace \(aqbugzilla.example.com\(aq with your bugzilla host name, and -YOUR_API_KEY with the generated API Key from the Web UI. +The sections must be hostnames. Other values that can be +set per hostname section are +.INDENT 0.0 +.IP \(bu 2 +\fBuser\fP: default auth username +.IP \(bu 2 +\fBpassword\fP: default auth password +.IP \(bu 2 +\fBcert\fP: default client side certificate +.UNINDENT +.sp +A \fB[DEFAULTS]\fP section is also accepted, which takes the following +values: +.INDENT 0.0 +.IP \(bu 2 +\fBurl\fP: default bugzilla URL +.UNINDENT +.SH AUTHENTICATION CACHE AND API KEYS +.sp +Some command usage will require an active login to the bugzilla +instance. For example, if the bugzilla instance has some private bugs, +those bugs will be missing from \(aqquery\(aq output if you do not have an +active login. .sp -Alternatively, you can use \(aqbugzilla login \-\-api\-key\(aq, which will ask -for the API key, and save it to bugzillarc for you. +If you are connecting to a bugzilla 5.0 or later instance, the best +option is to use bugzilla API keys. From the bugzilla web UI, log in, +navigate to Preferences\->API Keys, and generate a key (it will be a long +string of characters and numbers). .sp -For older bugzilla instances, you will need to cache a login cookie or -token with the "login" subcommand or the "\-\-login" argument. +Then use \(aqbugzilla \-\-bugzilla URL login \-\-api\-key\(aq, which will ask +for the API key, and save it to \fBbugzillarc\fP for you. +.sp +For older bugzilla instances, you will need to cache a login token +with the \(dqlogin\(dq subcommand or the \(dq\-\-login\(dq argument. .sp Additionally, the \-\-no\-cache\-credentials option will tell the bugzilla tool to \fInot\fP save or use any authentication cache, including the -bugzillarc file. +\fBbugzillarc\fP file. .SH EXAMPLES .nf bugzilla query \-\-bug_id 62037 @@ -590,17 +625,17 @@ bugzilla login bugzilla new \-p Fedora \-v rawhide \-c python\-bugzilla \e .in +2 -\-\-summary "python\-bugzilla causes headaches" \e -\-\-comment "python\-bugzilla made my brain hurt when I used it." +\-\-summary \(dqpython\-bugzilla causes headaches\(dq \e +\-\-comment \(dqpython\-bugzilla made my brain hurt when I used it.\(dq .in -2 -bugzilla attach \-\-file ~/Pictures/cam1.jpg \-\-desc "me, in pain" +bugzilla attach \-\-file ~/Pictures/cam1.jpg \-\-desc \(dqme, in pain\(dq $BUGID bugzilla attach \-\-getall $BUGID -bugzilla modify \-\-close NOTABUG \-\-comment "Actually, you\(aqre -hungover." $BUGID +bugzilla modify \-\-close NOTABUG \-\-comment \(dqActually, you\(aqre +hungover.\(dq $BUGID .fi .sp .SH EXIT STATUS @@ -616,6 +651,7 @@ Please report any bugs as github issues at .SH SEE ALSO .sp \fI\%https://bugzilla.readthedocs.io/en/latest/api/index.html\fP -\fI\%https://bugzilla.redhat.com/docs/en/html/api/Bugzilla/WebService/Bug.html\fP +.sp +\fI\%https://bugzilla.redhat.com/docs/en/html/api/core/v1/bug.html\fP .\" Generated by docutils manpage writer. . From 3dac0fa9d580b9bb24f6310799054e91795fcff4 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Wed, 20 Sep 2023 16:10:09 +0200 Subject: [PATCH 371/393] Run functional RO tests in GitHub actions * Run MariaDB and Bugzilla in service containers * Populate the DB with a defined dump * Include all files to build the Bugzilla image and prepare the environment * Implemented integration tests in new test module * Added a new fixture for request mocking --- .github/workflows/build.yml | 43 + test-requirements.txt | 1 + tests/conftest.py | 54 + tests/integration/__init__.py | 9 + tests/integration/ro_api_test.py | 101 ++ tests/integration/ro_cli_test.py | 47 + tests/services/Dockerfile | 28 + tests/services/README.md | 58 + tests/services/bugs.sql | 2270 ++++++++++++++++++++++++++++++ tests/services/bugzilla.conf | 9 + tests/services/bugzillarc | 2 + tests/services/localconfig | 19 + tests/services/params.json | 104 ++ tests/utils.py | 5 + 14 files changed, 2750 insertions(+) create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/ro_api_test.py create mode 100644 tests/integration/ro_cli_test.py create mode 100644 tests/services/Dockerfile create mode 100644 tests/services/README.md create mode 100644 tests/services/bugs.sql create mode 100644 tests/services/bugzilla.conf create mode 100644 tests/services/bugzillarc create mode 100644 tests/services/localconfig create mode 100644 tests/services/params.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd92d9d8..771d9b72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,6 +66,49 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + # Run functional tests + integrationRO: + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:latest + env: + MARIADB_USER: bugs + MARIADB_DATABASE: bugs + MARIADB_PASSWORD: secret + MARIADB_ROOT_PASSWORD: supersecret + ports: + - 3306:3306 + bugzilla: + image: ghcr.io/crazyscientist/bugzilla:test + ports: + - 80:80 + strategy: + matrix: + python-version: ["3.x"] + steps: + - uses: actions/checkout@v3 + - name: Install MariaDB utils + run: sudo apt install --no-install-recommends -q -y mariadb-client + - name: Restore DB dump + run: mariadb -h 127.0.0.1 -P 3306 --password=secret -u bugs bugs < tests/services/bugs.sql + - name: Store API key + run: | + mkdir -p ~/.config/python-bugzilla/ + cp tests/services/bugzillarc ~/.config/python-bugzilla/ + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov + pip install -r requirements.txt -r test-requirements.txt + - name: Test with pytest + run: pytest --ro-integration + env: + BUGZILLA_URL: http://localhost # Build and install on Windows windows: diff --git a/test-requirements.txt b/test-requirements.txt index 6abde2bb..6e80f2c7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,3 +2,4 @@ pytest pylint<3.1 pycodestyle<2.12 +responses diff --git a/tests/conftest.py b/tests/conftest.py index cfac4671..938932b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,8 +4,10 @@ import locale import logging import os +import re import pytest +import responses import tests import tests.utils @@ -17,6 +19,8 @@ # https://docs.pytest.org/en/latest/writing_plugins.html def pytest_addoption(parser): + parser.addoption("--ro-integration", action="store_true", default=False, + help="Run readonly tests against local Bugzilla instance.") parser.addoption("--ro-functional", action="store_true", default=False, help=("Run readonly functional tests against actual " "bugzilla instances. This will be very slow.")) @@ -40,11 +44,17 @@ def pytest_addoption(parser): def pytest_ignore_collect(path, config): has_ro = config.getoption("--ro-functional") + has_ro_i = config.getoption("--ro-integration") has_rw = config.getoption("--rw-functional") base = os.path.basename(str(path)) is_ro = base == "test_ro_functional.py" + is_ro_i = "tests/integration/ro" in str(path) is_rw = base == "test_rw_functional.py" + + if is_ro_i and not has_ro_i: + return True + if is_ro and not has_ro: return True if is_rw and not has_rw: @@ -107,3 +117,47 @@ def run_cli(capsys, monkeypatch): def _do_run(*args, **kwargs): return tests.utils.do_run_cli(capsys, monkeypatch, *args, **kwargs) return _do_run + + +@pytest.fixture +def mocked_responses(): + """ + Mock responses + + * Quickly return error responses + * Pass through requests to live instances + * Provide an incorrect XMLRPC response + """ + passthrough = () + status_pattern = re.compile(r"https://httpstat.us/(?P\d+).*") + + def status_callback(request): + match = status_pattern.match(request.url) + status_code = 400 + if match: + status_code = int(match.group("status")) + + return status_code, {}, "

Lorem ipsum

" + + test_url = os.getenv("BUGZILLA_URL") + if test_url: + passthrough += (test_url, ) + with responses.RequestsMock(passthru_prefixes=passthrough, + assert_all_requests_are_fired=False) as mock: + mock.add_callback( + method=responses.GET, + url=status_pattern, + callback=status_callback + ) + mock.add_callback( + method=responses.POST, + url=status_pattern, + callback=status_callback + ) + mock.add( + method=responses.POST, + url="https://example.com/#xmlrpc", + status=200, + body="This is no XML" + ) + yield mock diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..908a1f54 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,9 @@ +import os + + +TEST_URL = os.getenv("BUGZILLA_URL", "http://localhost") +TEST_OWNER = "andreas@hasenkopf.xyz" +TEST_PRODUCTS = {"Red Hat Enterprise Linux 9", + "SUSE Linux Enterprise Server 15 SP6", + "TestProduct"} +TEST_SUSE_COMPONENTS = {"Containers", "Kernel"} diff --git a/tests/integration/ro_api_test.py b/tests/integration/ro_api_test.py new file mode 100644 index 00000000..3f096587 --- /dev/null +++ b/tests/integration/ro_api_test.py @@ -0,0 +1,101 @@ +# Ignoring pytest-related warnings: +# pylint: disable=redefined-outer-name,unused-argument +import pytest + +from bugzilla import BugzillaError + +from ..utils import open_bz +from . import TEST_URL, TEST_PRODUCTS, TEST_SUSE_COMPONENTS, TEST_OWNER + + +def test_rest_xmlrpc_detection(mocked_responses): + # The default: use XMLRPC + bz = open_bz(url=TEST_URL) + assert bz.is_xmlrpc() + assert "/xmlrpc.cgi" in bz.url + + # See /rest in the URL, so use REST + bz = open_bz(url=TEST_URL + "/rest") + assert bz.is_rest() + with pytest.raises(BugzillaError) as e: + dummy = bz._proxy # pylint: disable=protected-access + assert "raw XMLRPC access is not provided" in str(e) + + # See /xmlrpc.cgi in the URL, so use XMLRPC + bz = open_bz(url=TEST_URL + "/xmlrpc.cgi") + assert "/xmlrpc.cgi" in bz.url + assert bz.is_xmlrpc() + assert bz._proxy # pylint: disable=protected-access + + +def test_apikey_error_scraping(mocked_responses): + # Ensure the API key does not leak into any requests exceptions + fakekey = "FOOBARMYKEY" + with pytest.raises(Exception) as e: + open_bz("https://httpstat.us/400&foo", + force_xmlrpc=True, api_key=fakekey) + assert "Client Error" in str(e.value) + assert fakekey not in str(e.value) + + with pytest.raises(Exception) as e: + open_bz("https://httpstat.us/400&foo", + force_rest=True, api_key=fakekey) + assert "Client Error" in str(e.value) + assert fakekey not in str(e.value) + + +def test_xmlrpc_bad_url(mocked_responses): + with pytest.raises(BugzillaError) as e: + open_bz(url="https://example.com/#xmlrpc", force_xmlrpc=True) + assert "URL may not be an XMLRPC URL" in str(e) + + +def test_get_products(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + assert len(bz.products) == 3 + assert {p["name"] for p in bz.products} == TEST_PRODUCTS + + rhel = next(p for p in bz.products if p["id"] == 2) + assert {v["name"] for v in rhel["versions"]} == {"9.0", "9.1", "unspecified"} + + +def test_get_components(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + components = bz.getcomponents(product="SUSE Linux Enterprise Server 15 SP6") + assert len(components) == 2 + assert set(components) == TEST_SUSE_COMPONENTS + + +def test_get_component_detail(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + component = bz.getcomponentdetails(product="Red Hat Enterprise Linux 9", + component="python-bugzilla") + assert component["id"] == 2 + assert component["default_assigned_to"] == TEST_OWNER + + +def test_query(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + query = bz.build_query(product="Red Hat Enterprise Linux 9", component="python-bugzilla") + bugs = bz.query(query=query) + + assert len(bugs) == 1 + assert bugs[0].id == 2 + assert bugs[0].summary == "Expect the Spanish inquisition" + + bz = open_bz(url=TEST_URL, **backends) + query = bz.build_query(product="SUSE Linux Enterprise Server 15 SP6") + bugs = bz.query(query=query) + + assert len(bugs) == 1 + assert bugs[0].id == 1 + assert bugs[0].whiteboard == "AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L" + + +def test_get_bug_alias(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = bz.getbug("FOO-1") + + assert bug.id == 1 + assert bug.summary == "ZeroDivisionError in function foo_bar()" diff --git a/tests/integration/ro_cli_test.py b/tests/integration/ro_cli_test.py new file mode 100644 index 00000000..f4d308d9 --- /dev/null +++ b/tests/integration/ro_cli_test.py @@ -0,0 +1,47 @@ +# Ignoring pytest-related warnings: +# pylint: disable=unused-argument +from ..utils import open_bz +from . import TEST_URL, TEST_PRODUCTS, TEST_SUSE_COMPONENTS, TEST_OWNER + + +def test_get_products(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --products", bzinstance=bz) + assert len(out.strip().split("\n")) == 3 + + for product in TEST_PRODUCTS: + assert product in out + + +def test_get_components(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --components 'SUSE Linux Enterprise Server 15 SP6'", bzinstance=bz) + assert len(out.strip().split("\n")) == 2 + for comp in TEST_SUSE_COMPONENTS: + assert comp in out + + +def test_get_component_owners(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --component_owners 'SUSE Linux Enterprise Server 15 SP6'", + bzinstance=bz) + assert TEST_OWNER in out + + +def test_get_versions(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --versions 'Red Hat Enterprise Linux 9'", bzinstance=bz) + versions = set(out.strip().split("\n")) + + assert versions == {"unspecified", "9.0", "9.1"} + + +def test_query(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --product 'Red Hat Enterprise Linux 9' " + "--component 'python-bugzilla'", bzinstance=bz) + lines = out.strip().splitlines() + + assert len(lines) == 1 + assert lines[0].startswith("#2") + assert "Expect the Spanish inquisition" in lines[0] diff --git a/tests/services/Dockerfile b/tests/services/Dockerfile new file mode 100644 index 00000000..83c1e209 --- /dev/null +++ b/tests/services/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:22.04 +LABEL description="Bugzilla image for testing purposes" +ARG DEBIAN_FRONTEND=noninteractive +ENV TZ="Etc/UTC" +RUN apt update && \ + apt install --no-install-recommends -q -y \ + tzdata wget apache2 libcgi-pm-perl libdatetime-perl libdatetime-timezone-perl libdbi-perl \ + libdbix-connector-perl libdigest-sha-perl libemail-address-perl libemail-mime-perl \ + libemail-sender-perl libjson-xs-perl liblist-moreutils-perl libmath-random-isaac-perl \ + libtemplate-perl libtimedate-perl liburi-perl libmariadb-dev-compat libdbd-mysql-perl \ + libxmlrpc-lite-perl libsoap-lite-perl libapache2-mod-perl2 libtest-taint-perl \ + libjson-rpc-perl && \ + apt clean +RUN mkdir -p /var/www/webapps && \ + wget https://ftp.mozilla.org/pub/mozilla.org/webtools/bugzilla-5.0.6.tar.gz \ + -O /tmp/bugzilla-5.0.6.tar.gz&& \ + tar xvzf /tmp/bugzilla-5.0.6.tar.gz && \ + rm /tmp/bugzilla-5.0.6.tar.gz && \ + mv /bugzilla-5.0.6/ /var/www/webapps/bugzilla/ && \ + mkdir /var/www/webapps/bugzilla/data/ +COPY bugzilla.conf /etc/apache2/sites-available/ +COPY localconfig /var/www/webapps/bugzilla/ +COPY params.json /var/www/webapps/bugzilla/data/ +RUN a2dissite 000-default && \ + a2ensite bugzilla && \ + a2enmod cgi headers expires rewrite perl && \ + /var/www/webapps/bugzilla/checksetup.pl +CMD apachectl -D FOREGROUND diff --git a/tests/services/README.md b/tests/services/README.md new file mode 100644 index 00000000..029241b8 --- /dev/null +++ b/tests/services/README.md @@ -0,0 +1,58 @@ +# Working with the containerized Bugzilla instance + +This document describes the steps for building a Bugzilla container image that can be used in the +GitHub Actions as a service and generating a database dump. + +In the following examples, the use of `docker` is assumed. Commands for `podman` should be +identical. + +## Build + +```shell +$ docker network create --driver bridge local-bridge +$ docker run --rm -itd \ + --env MARIADB_USER=bugs \ + --env MARIADB_DATABASE=bugs \ + --env MARIADB_PASSWORD=secret \ + --env MARIADB_ROOT_PASSWORD=supersecret \ + -p 3306:3306 \ + --network local-bridge \ + --name mariadb \ + mariadb:latest +$ mariadb -u bugs -h 127.0.0.1 -P 3306 --password=secret bugs < bugs.sql +$ docker build --network local-bridge . -t ghcr.io/crazyscientist/bugzilla:test +``` + +For those, who can spot the _chicken and egg problem_: The first version of `bugs.sql` was +created after running the Bugzilla installer inside the container. + +## Usage + +Once built, you can follow the above instructions; instead of building +the image, you can run it: + +```shell +docker run --rm -itd \ + -p 8000:80 \ + --network local-bridge \ + ghcr.io/crazyscientist/bugzilla:test +``` + +## Test data + +The test data used by the Bugzilla service in the integration test suite is stored in `bugs.sql`. + +One can edit this file manually or follow the above instructions to start both a MariaDB and +Bugzilla container and edit the data in Bugzilla. Once done, one needs to dump the changed data into +the file again: + +```shell +$ mariadb-dump -u bugs -h 127.0.0.1 -P 3306 --password=secret bugs > bugs.qql +``` + +## Testing +And now, you can run the integration tests against this instance: + +```shell +BUGZILLA_URL=http://localhost:8000 pytest --ro-integration +``` diff --git a/tests/services/bugs.sql b/tests/services/bugs.sql new file mode 100644 index 00000000..c0ddf4ba --- /dev/null +++ b/tests/services/bugs.sql @@ -0,0 +1,2270 @@ +-- MariaDB dump 10.19 Distrib 10.6.12-MariaDB, for debian-linux-gnu (x86_64) +-- +-- Host: 127.0.0.1 Database: bugs +-- ------------------------------------------------------ +-- Server version 11.1.2-MariaDB-1:11.1.2+maria~ubu2204 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `attach_data` +-- + +DROP TABLE IF EXISTS `attach_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `attach_data` ( + `id` mediumint(9) NOT NULL, + `thedata` longblob NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_attach_data_id_attachments_attach_id` FOREIGN KEY (`id`) REFERENCES `attachments` (`attach_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci MAX_ROWS=100000 AVG_ROW_LENGTH=1000000; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `attach_data` +-- + +LOCK TABLES `attach_data` WRITE; +/*!40000 ALTER TABLE `attach_data` DISABLE KEYS */; +/*!40000 ALTER TABLE `attach_data` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `attachments` +-- + +DROP TABLE IF EXISTS `attachments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `attachments` ( + `attach_id` mediumint(9) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `creation_ts` datetime NOT NULL, + `modification_time` datetime NOT NULL, + `description` tinytext NOT NULL, + `mimetype` tinytext NOT NULL, + `ispatch` tinyint(4) NOT NULL DEFAULT 0, + `filename` varchar(255) NOT NULL, + `submitter_id` mediumint(9) NOT NULL, + `isobsolete` tinyint(4) NOT NULL DEFAULT 0, + `isprivate` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`attach_id`), + KEY `attachments_bug_id_idx` (`bug_id`), + KEY `attachments_creation_ts_idx` (`creation_ts`), + KEY `attachments_modification_time_idx` (`modification_time`), + KEY `attachments_submitter_id_idx` (`submitter_id`,`bug_id`), + CONSTRAINT `fk_attachments_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_attachments_submitter_id_profiles_userid` FOREIGN KEY (`submitter_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `attachments` +-- + +LOCK TABLES `attachments` WRITE; +/*!40000 ALTER TABLE `attachments` DISABLE KEYS */; +/*!40000 ALTER TABLE `attachments` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `audit_log` +-- + +DROP TABLE IF EXISTS `audit_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `audit_log` ( + `user_id` mediumint(9) DEFAULT NULL, + `class` varchar(255) NOT NULL, + `object_id` int(11) NOT NULL, + `field` varchar(64) NOT NULL, + `removed` mediumtext DEFAULT NULL, + `added` mediumtext DEFAULT NULL, + `at_time` datetime NOT NULL, + KEY `audit_log_class_idx` (`class`,`at_time`), + KEY `fk_audit_log_user_id_profiles_userid` (`user_id`), + CONSTRAINT `fk_audit_log_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `audit_log` +-- + +LOCK TABLES `audit_log` WRITE; +/*!40000 ALTER TABLE `audit_log` DISABLE KEYS */; +INSERT INTO `audit_log` VALUES (NULL,'Bugzilla::Field',1,'__create__',NULL,'bug_id','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',2,'__create__',NULL,'short_desc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',3,'__create__',NULL,'classification','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',4,'__create__',NULL,'product','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',5,'__create__',NULL,'version','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',6,'__create__',NULL,'rep_platform','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',7,'__create__',NULL,'bug_file_loc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',8,'__create__',NULL,'op_sys','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',9,'__create__',NULL,'bug_status','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',10,'__create__',NULL,'status_whiteboard','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',11,'__create__',NULL,'keywords','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',12,'__create__',NULL,'resolution','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',13,'__create__',NULL,'bug_severity','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',14,'__create__',NULL,'priority','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',15,'__create__',NULL,'component','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',16,'__create__',NULL,'assigned_to','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',17,'__create__',NULL,'reporter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',18,'__create__',NULL,'qa_contact','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',19,'__create__',NULL,'assigned_to_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',20,'__create__',NULL,'reporter_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',21,'__create__',NULL,'qa_contact_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',22,'__create__',NULL,'cc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',23,'__create__',NULL,'dependson','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',24,'__create__',NULL,'blocked','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',25,'__create__',NULL,'attachments.description','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',26,'__create__',NULL,'attachments.filename','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',27,'__create__',NULL,'attachments.mimetype','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',28,'__create__',NULL,'attachments.ispatch','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',29,'__create__',NULL,'attachments.isobsolete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',30,'__create__',NULL,'attachments.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',31,'__create__',NULL,'attachments.submitter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',32,'__create__',NULL,'target_milestone','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',33,'__create__',NULL,'creation_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',34,'__create__',NULL,'delta_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',35,'__create__',NULL,'longdesc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',36,'__create__',NULL,'longdescs.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',37,'__create__',NULL,'longdescs.count','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',38,'__create__',NULL,'alias','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',39,'__create__',NULL,'everconfirmed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',40,'__create__',NULL,'reporter_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',41,'__create__',NULL,'cclist_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',42,'__create__',NULL,'bug_group','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',43,'__create__',NULL,'estimated_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',44,'__create__',NULL,'remaining_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',45,'__create__',NULL,'deadline','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',46,'__create__',NULL,'commenter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',47,'__create__',NULL,'flagtypes.name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',48,'__create__',NULL,'requestees.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',49,'__create__',NULL,'setters.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',50,'__create__',NULL,'work_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',51,'__create__',NULL,'percentage_complete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',52,'__create__',NULL,'content','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',53,'__create__',NULL,'attach_data.thedata','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',54,'__create__',NULL,'owner_idle_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',55,'__create__',NULL,'see_also','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',56,'__create__',NULL,'tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',57,'__create__',NULL,'last_visit_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',58,'__create__',NULL,'comment_tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',59,'__create__',NULL,'days_elapsed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Classification',1,'__create__',NULL,'Unclassified','2023-09-20 13:12:35'),(NULL,'Bugzilla::Group',1,'__create__',NULL,'admin','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',2,'__create__',NULL,'tweakparams','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',3,'__create__',NULL,'editusers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',4,'__create__',NULL,'creategroups','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',5,'__create__',NULL,'editclassifications','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',6,'__create__',NULL,'editcomponents','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',7,'__create__',NULL,'editkeywords','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',8,'__create__',NULL,'editbugs','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',9,'__create__',NULL,'canconfirm','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',10,'__create__',NULL,'bz_canusewhineatothers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',11,'__create__',NULL,'bz_canusewhines','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',12,'__create__',NULL,'bz_sudoers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',13,'__create__',NULL,'bz_sudo_protect','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',14,'__create__',NULL,'bz_quip_moderators','2023-09-20 13:12:40'),(NULL,'Bugzilla::User',1,'__create__',NULL,'andreas@hasenkopf.xyz','2023-09-20 13:12:55'),(NULL,'Bugzilla::Product',1,'__create__',NULL,'TestProduct','2023-09-20 13:12:55'),(NULL,'Bugzilla::Version',1,'__create__',NULL,'unspecified','2023-09-20 13:12:55'),(NULL,'Bugzilla::Milestone',1,'__create__',NULL,'---','2023-09-20 13:12:55'),(NULL,'Bugzilla::Component',1,'__create__',NULL,'TestComponent','2023-09-20 13:12:55'),(1,'Bugzilla::Product',2,'__create__',NULL,'Red Hat Enterprise Linux 9','2023-11-27 12:25:54'),(1,'Bugzilla::Version',2,'__create__',NULL,'unspecified','2023-11-27 12:25:54'),(1,'Bugzilla::Milestone',2,'__create__',NULL,'---','2023-11-27 12:25:54'),(1,'Bugzilla::Component',2,'__create__',NULL,'python-bugzilla','2023-11-27 12:25:54'),(1,'Bugzilla::Version',3,'__create__',NULL,'9.0','2023-11-27 12:26:06'),(1,'Bugzilla::Version',4,'__create__',NULL,'9.1','2023-11-27 12:26:14'),(1,'Bugzilla::Product',3,'__create__',NULL,'SUSE Linux Enterprise Server 15 SP6','2023-11-27 12:29:18'),(1,'Bugzilla::Version',5,'__create__',NULL,'unspecified','2023-11-27 12:29:18'),(1,'Bugzilla::Milestone',3,'__create__',NULL,'---','2023-11-27 12:29:18'),(1,'Bugzilla::Component',3,'__create__',NULL,'Kernel','2023-11-27 12:29:18'),(1,'Bugzilla::Component',4,'__create__',NULL,'Containers','2023-11-27 12:29:46'); +/*!40000 ALTER TABLE `audit_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_group_map` +-- + +DROP TABLE IF EXISTS `bug_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_group_map` ( + `bug_id` mediumint(9) NOT NULL, + `group_id` mediumint(9) NOT NULL, + UNIQUE KEY `bug_group_map_bug_id_idx` (`bug_id`,`group_id`), + KEY `bug_group_map_group_id_idx` (`group_id`), + CONSTRAINT `fk_bug_group_map_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bug_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_group_map` +-- + +LOCK TABLES `bug_group_map` WRITE; +/*!40000 ALTER TABLE `bug_group_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `bug_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_see_also` +-- + +DROP TABLE IF EXISTS `bug_see_also`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_see_also` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `value` varchar(255) NOT NULL, + `class` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `bug_see_also_bug_id_idx` (`bug_id`,`value`), + CONSTRAINT `fk_bug_see_also_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_see_also` +-- + +LOCK TABLES `bug_see_also` WRITE; +/*!40000 ALTER TABLE `bug_see_also` DISABLE KEYS */; +/*!40000 ALTER TABLE `bug_see_also` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_severity` +-- + +DROP TABLE IF EXISTS `bug_severity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_severity` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `bug_severity_value_idx` (`value`), + KEY `bug_severity_sortkey_idx` (`sortkey`,`value`), + KEY `bug_severity_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_severity` +-- + +LOCK TABLES `bug_severity` WRITE; +/*!40000 ALTER TABLE `bug_severity` DISABLE KEYS */; +INSERT INTO `bug_severity` VALUES (1,'blocker',100,1,NULL),(2,'critical',200,1,NULL),(3,'major',300,1,NULL),(4,'normal',400,1,NULL),(5,'minor',500,1,NULL),(6,'trivial',600,1,NULL),(7,'enhancement',700,1,NULL); +/*!40000 ALTER TABLE `bug_severity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_status` +-- + +DROP TABLE IF EXISTS `bug_status`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_status` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + `is_open` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `bug_status_value_idx` (`value`), + KEY `bug_status_sortkey_idx` (`sortkey`,`value`), + KEY `bug_status_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_status` +-- + +LOCK TABLES `bug_status` WRITE; +/*!40000 ALTER TABLE `bug_status` DISABLE KEYS */; +INSERT INTO `bug_status` VALUES (1,'UNCONFIRMED',100,1,NULL,1),(2,'CONFIRMED',200,1,NULL,1),(3,'IN_PROGRESS',300,1,NULL,1),(4,'RESOLVED',400,1,NULL,0),(5,'VERIFIED',500,1,NULL,0); +/*!40000 ALTER TABLE `bug_status` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_tag` +-- + +DROP TABLE IF EXISTS `bug_tag`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_tag` ( + `bug_id` mediumint(9) NOT NULL, + `tag_id` mediumint(9) NOT NULL, + UNIQUE KEY `bug_tag_bug_id_idx` (`bug_id`,`tag_id`), + KEY `fk_bug_tag_tag_id_tag_id` (`tag_id`), + CONSTRAINT `fk_bug_tag_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bug_tag_tag_id_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_tag` +-- + +LOCK TABLES `bug_tag` WRITE; +/*!40000 ALTER TABLE `bug_tag` DISABLE KEYS */; +/*!40000 ALTER TABLE `bug_tag` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bug_user_last_visit` +-- + +DROP TABLE IF EXISTS `bug_user_last_visit`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bug_user_last_visit` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `bug_id` mediumint(9) NOT NULL, + `last_visit_ts` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `bug_user_last_visit_idx` (`user_id`,`bug_id`), + KEY `bug_user_last_visit_last_visit_ts_idx` (`last_visit_ts`), + KEY `fk_bug_user_last_visit_bug_id_bugs_bug_id` (`bug_id`), + CONSTRAINT `fk_bug_user_last_visit_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bug_user_last_visit_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bug_user_last_visit` +-- + +LOCK TABLES `bug_user_last_visit` WRITE; +/*!40000 ALTER TABLE `bug_user_last_visit` DISABLE KEYS */; +INSERT INTO `bug_user_last_visit` VALUES (1,1,1,'2023-11-27 15:53:08'),(2,1,2,'2023-11-27 15:38:47'); +/*!40000 ALTER TABLE `bug_user_last_visit` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs` +-- + +DROP TABLE IF EXISTS `bugs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs` ( + `bug_id` mediumint(9) NOT NULL AUTO_INCREMENT, + `assigned_to` mediumint(9) NOT NULL, + `bug_file_loc` mediumtext NOT NULL DEFAULT '', + `bug_severity` varchar(64) NOT NULL, + `bug_status` varchar(64) NOT NULL, + `creation_ts` datetime DEFAULT NULL, + `delta_ts` datetime NOT NULL, + `short_desc` varchar(255) NOT NULL, + `op_sys` varchar(64) NOT NULL, + `priority` varchar(64) NOT NULL, + `product_id` smallint(6) NOT NULL, + `rep_platform` varchar(64) NOT NULL, + `reporter` mediumint(9) NOT NULL, + `version` varchar(64) NOT NULL, + `component_id` mediumint(9) NOT NULL, + `resolution` varchar(64) NOT NULL DEFAULT '', + `target_milestone` varchar(64) NOT NULL DEFAULT '---', + `qa_contact` mediumint(9) DEFAULT NULL, + `status_whiteboard` mediumtext NOT NULL DEFAULT '', + `lastdiffed` datetime DEFAULT NULL, + `everconfirmed` tinyint(4) NOT NULL, + `reporter_accessible` tinyint(4) NOT NULL DEFAULT 1, + `cclist_accessible` tinyint(4) NOT NULL DEFAULT 1, + `estimated_time` decimal(7,2) NOT NULL DEFAULT 0.00, + `remaining_time` decimal(7,2) NOT NULL DEFAULT 0.00, + `deadline` datetime DEFAULT NULL, + PRIMARY KEY (`bug_id`), + KEY `bugs_assigned_to_idx` (`assigned_to`), + KEY `bugs_creation_ts_idx` (`creation_ts`), + KEY `bugs_delta_ts_idx` (`delta_ts`), + KEY `bugs_bug_severity_idx` (`bug_severity`), + KEY `bugs_bug_status_idx` (`bug_status`), + KEY `bugs_op_sys_idx` (`op_sys`), + KEY `bugs_priority_idx` (`priority`), + KEY `bugs_product_id_idx` (`product_id`), + KEY `bugs_reporter_idx` (`reporter`), + KEY `bugs_version_idx` (`version`), + KEY `bugs_component_id_idx` (`component_id`), + KEY `bugs_resolution_idx` (`resolution`), + KEY `bugs_target_milestone_idx` (`target_milestone`), + KEY `bugs_qa_contact_idx` (`qa_contact`), + CONSTRAINT `fk_bugs_assigned_to_profiles_userid` FOREIGN KEY (`assigned_to`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_qa_contact_profiles_userid` FOREIGN KEY (`qa_contact`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_reporter_profiles_userid` FOREIGN KEY (`reporter`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs` +-- + +LOCK TABLES `bugs` WRITE; +/*!40000 ALTER TABLE `bugs` DISABLE KEYS */; +INSERT INTO `bugs` VALUES (1,1,'','major','IN_PROGRESS','2023-11-27 15:35:33','2023-11-27 15:53:04','ZeroDivisionError in function foo_bar()','Linux','---',3,'PC',1,'unspecified',4,'','---',NULL,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','2023-11-27 15:53:04',1,1,1,0.00,0.00,NULL),(2,1,'','enhancement','CONFIRMED','2023-11-27 15:38:45','2023-11-27 15:38:45','Expect the Spanish inquisition','Linux','---',2,'PC',1,'9.1',2,'','---',NULL,'','2023-11-27 15:38:45',1,1,1,0.00,0.00,NULL); +/*!40000 ALTER TABLE `bugs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs_activity` +-- + +DROP TABLE IF EXISTS `bugs_activity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs_activity` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `attach_id` mediumint(9) DEFAULT NULL, + `who` mediumint(9) NOT NULL, + `bug_when` datetime NOT NULL, + `fieldid` mediumint(9) NOT NULL, + `added` varchar(255) DEFAULT NULL, + `removed` varchar(255) DEFAULT NULL, + `comment_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `bugs_activity_bug_id_idx` (`bug_id`), + KEY `bugs_activity_who_idx` (`who`), + KEY `bugs_activity_bug_when_idx` (`bug_when`), + KEY `bugs_activity_fieldid_idx` (`fieldid`), + KEY `bugs_activity_added_idx` (`added`), + KEY `bugs_activity_removed_idx` (`removed`), + KEY `fk_bugs_activity_attach_id_attachments_attach_id` (`attach_id`), + KEY `fk_bugs_activity_comment_id_longdescs_comment_id` (`comment_id`), + CONSTRAINT `fk_bugs_activity_attach_id_attachments_attach_id` FOREIGN KEY (`attach_id`) REFERENCES `attachments` (`attach_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_comment_id_longdescs_comment_id` FOREIGN KEY (`comment_id`) REFERENCES `longdescs` (`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_fieldid_fielddefs_id` FOREIGN KEY (`fieldid`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_bugs_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs_activity` +-- + +LOCK TABLES `bugs_activity` WRITE; +/*!40000 ALTER TABLE `bugs_activity` DISABLE KEYS */; +INSERT INTO `bugs_activity` VALUES (1,1,NULL,1,'2023-11-27 15:45:09',9,'IN_PROGRESS','CONFIRMED',NULL),(2,1,NULL,1,'2023-11-27 15:47:58',10,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','',NULL),(3,1,NULL,1,'2023-11-27 15:53:04',38,'FOO-1','',NULL); +/*!40000 ALTER TABLE `bugs_activity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs_aliases` +-- + +DROP TABLE IF EXISTS `bugs_aliases`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs_aliases` ( + `alias` varchar(40) NOT NULL, + `bug_id` mediumint(9) DEFAULT NULL, + UNIQUE KEY `bugs_aliases_alias_idx` (`alias`), + KEY `bugs_aliases_bug_id_idx` (`bug_id`), + CONSTRAINT `fk_bugs_aliases_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs_aliases` +-- + +LOCK TABLES `bugs_aliases` WRITE; +/*!40000 ALTER TABLE `bugs_aliases` DISABLE KEYS */; +INSERT INTO `bugs_aliases` VALUES ('FOO-1',1); +/*!40000 ALTER TABLE `bugs_aliases` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bugs_fulltext` +-- + +DROP TABLE IF EXISTS `bugs_fulltext`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bugs_fulltext` ( + `bug_id` mediumint(9) NOT NULL, + `short_desc` varchar(255) NOT NULL, + `comments` mediumtext DEFAULT NULL, + `comments_noprivate` mediumtext DEFAULT NULL, + PRIMARY KEY (`bug_id`), + FULLTEXT KEY `bugs_fulltext_short_desc_idx` (`short_desc`), + FULLTEXT KEY `bugs_fulltext_comments_idx` (`comments`), + FULLTEXT KEY `bugs_fulltext_comments_noprivate_idx` (`comments_noprivate`), + CONSTRAINT `fk_bugs_fulltext_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bugs_fulltext` +-- + +LOCK TABLES `bugs_fulltext` WRITE; +/*!40000 ALTER TABLE `bugs_fulltext` DISABLE KEYS */; +INSERT INTO `bugs_fulltext` VALUES (1,'ZeroDivisionError in function foo_bar()','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.'),(2,'Expect the Spanish inquisition','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.'); +/*!40000 ALTER TABLE `bugs_fulltext` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `bz_schema` +-- + +DROP TABLE IF EXISTS `bz_schema`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `bz_schema` ( + `schema_data` longblob NOT NULL, + `version` decimal(3,2) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `bz_schema` +-- + +LOCK TABLES `bz_schema` WRITE; +/*!40000 ALTER TABLE `bz_schema` DISABLE KEYS */; +INSERT INTO `bz_schema` VALUES ('$VAR1 = {\n \'attach_data\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'attach_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'attachments\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'thedata\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGBLOB\'\n }\n ]\n },\n \'attachments\' => {\n \'FIELDS\' => [\n \'attach_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'creation_ts\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'modification_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'mimetype\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'ispatch\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'filename\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'submitter_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'isobsolete\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'isprivate\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'attachments_bug_id_idx\',\n [\n \'bug_id\'\n ],\n \'attachments_creation_ts_idx\',\n [\n \'creation_ts\'\n ],\n \'attachments_modification_time_idx\',\n [\n \'modification_time\'\n ],\n \'attachments_submitter_id_idx\',\n [\n \'submitter_id\',\n \'bug_id\'\n ]\n ]\n },\n \'audit_log\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'class\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'object_id\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'field\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'removed\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'added\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'at_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'audit_log_class_idx\',\n [\n \'class\',\n \'at_time\'\n ]\n ]\n },\n \'bug_group_map\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'bug_group_map_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'group_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_group_map_group_id_idx\',\n [\n \'group_id\'\n ]\n ]\n },\n \'bug_see_also\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'class\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'bug_see_also_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'bug_severity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'bug_severity_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_severity_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'bug_severity_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'bug_status\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n },\n \'is_open\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'bug_status_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_status_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'bug_status_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'bug_tag\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'tag_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'tag\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'bug_tag_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'tag_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'bug_user_last_visit\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'last_visit_ts\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'bug_user_last_visit_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'bug_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'bug_user_last_visit_last_visit_ts_idx\',\n [\n \'last_visit_ts\'\n ]\n ]\n },\n \'bugs\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'assigned_to\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_file_loc\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'bug_severity\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'bug_status\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'creation_ts\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'delta_ts\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'short_desc\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'op_sys\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'priority\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'rep_platform\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'reporter\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'version\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'component_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'resolution\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'target_milestone\',\n {\n \'DEFAULT\' => \'\\\'---\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'qa_contact\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'status_whiteboard\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'lastdiffed\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'everconfirmed\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'reporter_accessible\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'cclist_accessible\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'estimated_time\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(7,2)\'\n },\n \'remaining_time\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(7,2)\'\n },\n \'deadline\',\n {\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_assigned_to_idx\',\n [\n \'assigned_to\'\n ],\n \'bugs_creation_ts_idx\',\n [\n \'creation_ts\'\n ],\n \'bugs_delta_ts_idx\',\n [\n \'delta_ts\'\n ],\n \'bugs_bug_severity_idx\',\n [\n \'bug_severity\'\n ],\n \'bugs_bug_status_idx\',\n [\n \'bug_status\'\n ],\n \'bugs_op_sys_idx\',\n [\n \'op_sys\'\n ],\n \'bugs_priority_idx\',\n [\n \'priority\'\n ],\n \'bugs_product_id_idx\',\n [\n \'product_id\'\n ],\n \'bugs_reporter_idx\',\n [\n \'reporter\'\n ],\n \'bugs_version_idx\',\n [\n \'version\'\n ],\n \'bugs_component_id_idx\',\n [\n \'component_id\'\n ],\n \'bugs_resolution_idx\',\n [\n \'resolution\'\n ],\n \'bugs_target_milestone_idx\',\n [\n \'target_milestone\'\n ],\n \'bugs_qa_contact_idx\',\n [\n \'qa_contact\'\n ]\n ]\n },\n \'bugs_activity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'attach_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'attach_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'attachments\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'fieldid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'added\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'removed\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'comment_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'comment_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'longdescs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT4\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_activity_bug_id_idx\',\n [\n \'bug_id\'\n ],\n \'bugs_activity_who_idx\',\n [\n \'who\'\n ],\n \'bugs_activity_bug_when_idx\',\n [\n \'bug_when\'\n ],\n \'bugs_activity_fieldid_idx\',\n [\n \'fieldid\'\n ],\n \'bugs_activity_added_idx\',\n [\n \'added\'\n ],\n \'bugs_activity_removed_idx\',\n [\n \'removed\'\n ]\n ]\n },\n \'bugs_aliases\' => {\n \'FIELDS\' => [\n \'alias\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(40)\'\n },\n \'bug_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_aliases_bug_id_idx\',\n [\n \'bug_id\'\n ],\n \'bugs_aliases_alias_idx\',\n {\n \'FIELDS\' => [\n \'alias\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'bugs_fulltext\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'short_desc\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'comments\',\n {\n \'TYPE\' => \'LONGTEXT\'\n },\n \'comments_noprivate\',\n {\n \'TYPE\' => \'LONGTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'bugs_fulltext_short_desc_idx\',\n {\n \'FIELDS\' => [\n \'short_desc\'\n ],\n \'TYPE\' => \'FULLTEXT\'\n },\n \'bugs_fulltext_comments_idx\',\n {\n \'FIELDS\' => [\n \'comments\'\n ],\n \'TYPE\' => \'FULLTEXT\'\n },\n \'bugs_fulltext_comments_noprivate_idx\',\n {\n \'FIELDS\' => [\n \'comments_noprivate\'\n ],\n \'TYPE\' => \'FULLTEXT\'\n }\n ]\n },\n \'bz_schema\' => {\n \'FIELDS\' => [\n \'schema_data\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGBLOB\'\n },\n \'version\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(3,2)\'\n }\n ]\n },\n \'category_group_map\' => {\n \'FIELDS\' => [\n \'category_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series_categories\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'category_group_map_category_id_idx\',\n {\n \'FIELDS\' => [\n \'category_id\',\n \'group_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'cc\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'cc_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'who\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'cc_who_idx\',\n [\n \'who\'\n ]\n ]\n },\n \'classifications\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'description\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'classifications_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'component_cc\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'component_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'component_cc_user_id_idx\',\n {\n \'FIELDS\' => [\n \'component_id\',\n \'user_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'components\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'initialowner\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'initialqacontact\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'components_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'components_name_idx\',\n [\n \'name\'\n ]\n ]\n },\n \'dependencies\' => {\n \'FIELDS\' => [\n \'blocked\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'dependson\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'dependencies_blocked_idx\',\n {\n \'FIELDS\' => [\n \'blocked\',\n \'dependson\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'dependencies_dependson_idx\',\n [\n \'dependson\'\n ]\n ]\n },\n \'duplicates\' => {\n \'FIELDS\' => [\n \'dupe_of\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'dupe\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ]\n },\n \'email_bug_ignore\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'email_bug_ignore_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'bug_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'email_setting\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'relationship\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n },\n \'event\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'email_setting_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'relationship\',\n \'event\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'field_visibility\' => {\n \'FIELDS\' => [\n \'field_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'value_id\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'field_visibility_field_id_idx\',\n {\n \'FIELDS\' => [\n \'field_id\',\n \'value_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'fielddefs\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'type\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'custom\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'long_desc\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'mailhead\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'sortkey\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'obsolete\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'enter_bug\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'buglist\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_field_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'value_field_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'reverse_desc\',\n {\n \'TYPE\' => \'TINYTEXT\'\n },\n \'is_mandatory\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_numeric\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'fielddefs_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'fielddefs_sortkey_idx\',\n [\n \'sortkey\'\n ],\n \'fielddefs_value_field_id_idx\',\n [\n \'value_field_id\'\n ],\n \'fielddefs_is_mandatory_idx\',\n [\n \'is_mandatory\'\n ]\n ]\n },\n \'flagexclusions\' => {\n \'FIELDS\' => [\n \'type_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'flagtypes\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'product_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'component_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'flagexclusions_type_id_idx\',\n {\n \'FIELDS\' => [\n \'type_id\',\n \'product_id\',\n \'component_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'flaginclusions\' => {\n \'FIELDS\' => [\n \'type_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'flagtypes\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'product_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'component_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'components\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'flaginclusions_type_id_idx\',\n {\n \'FIELDS\' => [\n \'type_id\',\n \'product_id\',\n \'component_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'flags\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'type_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'flagtypes\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'status\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'char(1)\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'attach_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'attach_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'attachments\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'creation_date\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'modification_date\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'setter_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'requestee_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'flags_bug_id_idx\',\n [\n \'bug_id\',\n \'attach_id\'\n ],\n \'flags_setter_id_idx\',\n [\n \'setter_id\'\n ],\n \'flags_requestee_id_idx\',\n [\n \'requestee_id\'\n ],\n \'flags_type_id_idx\',\n [\n \'type_id\'\n ]\n ]\n },\n \'flagtypes\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(50)\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'cc_list\',\n {\n \'TYPE\' => \'varchar(200)\'\n },\n \'target_type\',\n {\n \'DEFAULT\' => \'\\\'b\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'char(1)\'\n },\n \'is_active\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_requestable\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_requesteeble\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'is_multiplicable\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'grant_group_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'request_group_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ]\n },\n \'group_control_map\' => {\n \'FIELDS\' => [\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'entry\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'membercontrol\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n },\n \'othercontrol\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n },\n \'canedit\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'editcomponents\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'editbugs\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'canconfirm\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'group_control_map_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'group_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'group_control_map_group_id_idx\',\n [\n \'group_id\'\n ]\n ]\n },\n \'group_group_map\' => {\n \'FIELDS\' => [\n \'member_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'grantor_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'grant_type\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'group_group_map_member_id_idx\',\n {\n \'FIELDS\' => [\n \'member_id\',\n \'grantor_id\',\n \'grant_type\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'groups\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'isbuggroup\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'userregexp\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'TINYTEXT\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'icon_url\',\n {\n \'TYPE\' => \'TINYTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'groups_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'keyworddefs\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'keyworddefs_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'keywords\' => {\n \'FIELDS\' => [\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'keywordid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'keyworddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'keywords_bug_id_idx\',\n {\n \'FIELDS\' => [\n \'bug_id\',\n \'keywordid\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'keywords_keywordid_idx\',\n [\n \'keywordid\'\n ]\n ]\n },\n \'login_failure\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'login_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'ip_addr\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(40)\'\n }\n ],\n \'INDEXES\' => [\n \'login_failure_user_id_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'logincookies\' => {\n \'FIELDS\' => [\n \'cookie\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'varchar(16)\'\n },\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'ipaddr\',\n {\n \'TYPE\' => \'varchar(40)\'\n },\n \'lastused\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'logincookies_lastused_idx\',\n [\n \'lastused\'\n ]\n ]\n },\n \'longdescs\' => {\n \'FIELDS\' => [\n \'comment_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'work_time\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'decimal(7,2)\'\n },\n \'thetext\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGTEXT\'\n },\n \'isprivate\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'already_wrapped\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'type\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'extra_data\',\n {\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_bug_id_idx\',\n [\n \'bug_id\',\n \'work_time\'\n ],\n \'longdescs_who_idx\',\n [\n \'who\',\n \'bug_id\'\n ],\n \'longdescs_bug_when_idx\',\n [\n \'bug_when\'\n ]\n ]\n },\n \'longdescs_tags\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'comment_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'comment_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'longdescs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT4\'\n },\n \'tag\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(24)\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_tags_idx\',\n {\n \'FIELDS\' => [\n \'comment_id\',\n \'tag\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'longdescs_tags_activity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'bug_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'bug_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bugs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'comment_id\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'comment_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'longdescs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT4\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'added\',\n {\n \'TYPE\' => \'varchar(24)\'\n },\n \'removed\',\n {\n \'TYPE\' => \'varchar(24)\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_tags_activity_bug_id_idx\',\n [\n \'bug_id\'\n ]\n ]\n },\n \'longdescs_tags_weights\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'tag\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(24)\'\n },\n \'weight\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'longdescs_tags_weights_tag_idx\',\n {\n \'FIELDS\' => [\n \'tag\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'mail_staging\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'message\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGBLOB\'\n }\n ]\n },\n \'milestones\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'milestones_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'namedqueries\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'query\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'namedqueries_userid_idx\',\n {\n \'FIELDS\' => [\n \'userid\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'namedqueries_link_in_footer\' => {\n \'FIELDS\' => [\n \'namedquery_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'namedqueries\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'namedqueries_link_in_footer_id_idx\',\n {\n \'FIELDS\' => [\n \'namedquery_id\',\n \'user_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'namedqueries_link_in_footer_userid_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'namedquery_group_map\' => {\n \'FIELDS\' => [\n \'namedquery_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'namedqueries\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'namedquery_group_map_namedquery_id_idx\',\n {\n \'FIELDS\' => [\n \'namedquery_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'namedquery_group_map_group_id_idx\',\n [\n \'group_id\'\n ]\n ]\n },\n \'op_sys\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'op_sys_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'op_sys_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'op_sys_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'priority\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'priority_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'priority_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'priority_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'products\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'classification_id\',\n {\n \'DEFAULT\' => \'1\',\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'classifications\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'description\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => 1,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'defaultmilestone\',\n {\n \'DEFAULT\' => \'\\\'---\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'allows_unconfirmed\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'products_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'profile_search\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'bug_list\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'list_order\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'profile_search_user_id_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'profile_setting\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'setting_name\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'name\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'setting\',\n \'created\' => 1\n },\n \'TYPE\' => \'varchar(32)\'\n },\n \'setting_value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(32)\'\n }\n ],\n \'INDEXES\' => [\n \'profile_setting_value_unique_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'setting_name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'profiles\' => {\n \'FIELDS\' => [\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'login_name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'cryptpassword\',\n {\n \'TYPE\' => \'varchar(128)\'\n },\n \'realname\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'disabledtext\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'disable_mail\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'mybugslink\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'extern_id\',\n {\n \'TYPE\' => \'varchar(64)\'\n },\n \'is_enabled\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'last_seen_date\',\n {\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'profiles_login_name_idx\',\n {\n \'FIELDS\' => [\n \'login_name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'profiles_extern_id_idx\',\n {\n \'FIELDS\' => [\n \'extern_id\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'profiles_activity\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'who\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'profiles_when\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'fieldid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'TABLE\' => \'fielddefs\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'oldvalue\',\n {\n \'TYPE\' => \'TINYTEXT\'\n },\n \'newvalue\',\n {\n \'TYPE\' => \'TINYTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'profiles_activity_userid_idx\',\n [\n \'userid\'\n ],\n \'profiles_activity_profiles_when_idx\',\n [\n \'profiles_when\'\n ],\n \'profiles_activity_fieldid_idx\',\n [\n \'fieldid\'\n ]\n ]\n },\n \'quips\' => {\n \'FIELDS\' => [\n \'quipid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'userid\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'SET NULL\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'quip\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(512)\'\n },\n \'approved\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ]\n },\n \'rep_platform\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'rep_platform_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'rep_platform_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'rep_platform_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'reports\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'query\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'LONGTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'reports_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'resolution\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'visibility_value_id\',\n {\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'resolution_value_idx\',\n {\n \'FIELDS\' => [\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'resolution_sortkey_idx\',\n [\n \'sortkey\',\n \'value\'\n ],\n \'resolution_visibility_value_id_idx\',\n [\n \'visibility_value_id\'\n ]\n ]\n },\n \'series\' => {\n \'FIELDS\' => [\n \'series_id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'creator\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'category\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series_categories\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'subcategory\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series_categories\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'frequency\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'query\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'is_public\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'series_creator_idx\',\n [\n \'creator\'\n ],\n \'series_category_idx\',\n {\n \'FIELDS\' => [\n \'category\',\n \'subcategory\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'series_categories\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'SMALLSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n }\n ],\n \'INDEXES\' => [\n \'series_categories_name_idx\',\n {\n \'FIELDS\' => [\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'series_data\' => {\n \'FIELDS\' => [\n \'series_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'series_id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'series\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'series_date\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'series_value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'series_data_series_id_idx\',\n {\n \'FIELDS\' => [\n \'series_id\',\n \'series_date\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'setting\' => {\n \'FIELDS\' => [\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'varchar(32)\'\n },\n \'default_value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(32)\'\n },\n \'is_enabled\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'subclass\',\n {\n \'TYPE\' => \'varchar(32)\'\n }\n ]\n },\n \'setting_value\' => {\n \'FIELDS\' => [\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'name\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'setting\',\n \'created\' => 1\n },\n \'TYPE\' => \'varchar(32)\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(32)\'\n },\n \'sortindex\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'setting_value_nv_unique_idx\',\n {\n \'FIELDS\' => [\n \'name\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'setting_value_ns_unique_idx\',\n {\n \'FIELDS\' => [\n \'name\',\n \'sortindex\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'status_workflow\' => {\n \'FIELDS\' => [\n \'old_status\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bug_status\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'new_status\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'bug_status\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'require_comment\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'status_workflow_idx\',\n {\n \'FIELDS\' => [\n \'old_status\',\n \'new_status\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'tag\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'name\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'tag_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'name\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'tokens\' => {\n \'FIELDS\' => [\n \'userid\',\n {\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'issuedate\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'DATETIME\'\n },\n \'token\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'varchar(16)\'\n },\n \'tokentype\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(16)\'\n },\n \'eventdata\',\n {\n \'TYPE\' => \'TINYTEXT\'\n }\n ],\n \'INDEXES\' => [\n \'tokens_userid_idx\',\n [\n \'userid\'\n ]\n ]\n },\n \'ts_error\' => {\n \'FIELDS\' => [\n \'error_time\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'message\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n },\n \'funcid\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n }\n ],\n \'INDEXES\' => [\n \'ts_error_funcid_idx\',\n [\n \'funcid\',\n \'error_time\'\n ],\n \'ts_error_error_time_idx\',\n [\n \'error_time\'\n ],\n \'ts_error_jobid_idx\',\n [\n \'jobid\'\n ]\n ]\n },\n \'ts_exitstatus\' => {\n \'FIELDS\' => [\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'funcid\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'status\',\n {\n \'TYPE\' => \'INT2\'\n },\n \'completion_time\',\n {\n \'TYPE\' => \'INT4\'\n },\n \'delete_after\',\n {\n \'TYPE\' => \'INT4\'\n }\n ],\n \'INDEXES\' => [\n \'ts_exitstatus_funcid_idx\',\n [\n \'funcid\'\n ],\n \'ts_exitstatus_delete_after_idx\',\n [\n \'delete_after\'\n ]\n ]\n },\n \'ts_funcmap\' => {\n \'FIELDS\' => [\n \'funcid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'funcname\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'ts_funcmap_funcname_idx\',\n {\n \'FIELDS\' => [\n \'funcname\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'ts_job\' => {\n \'FIELDS\' => [\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'funcid\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'arg\',\n {\n \'TYPE\' => \'LONGBLOB\'\n },\n \'uniqkey\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'insert_time\',\n {\n \'TYPE\' => \'INT4\'\n },\n \'run_after\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'grabbed_until\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'priority\',\n {\n \'TYPE\' => \'INT2\'\n },\n \'coalesce\',\n {\n \'TYPE\' => \'varchar(255)\'\n }\n ],\n \'INDEXES\' => [\n \'ts_job_funcid_idx\',\n {\n \'FIELDS\' => [\n \'funcid\',\n \'uniqkey\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'ts_job_run_after_idx\',\n [\n \'run_after\',\n \'funcid\'\n ],\n \'ts_job_coalesce_idx\',\n [\n \'coalesce\',\n \'funcid\'\n ]\n ]\n },\n \'ts_note\' => {\n \'FIELDS\' => [\n \'jobid\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT4\'\n },\n \'notekey\',\n {\n \'TYPE\' => \'varchar(255)\'\n },\n \'value\',\n {\n \'TYPE\' => \'LONGBLOB\'\n }\n ],\n \'INDEXES\' => [\n \'ts_note_jobid_idx\',\n {\n \'FIELDS\' => [\n \'jobid\',\n \'notekey\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'user_api_keys\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'INTSERIAL\'\n },\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'api_key\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'VARCHAR(40)\'\n },\n \'description\',\n {\n \'TYPE\' => \'VARCHAR(255)\'\n },\n \'revoked\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'last_used\',\n {\n \'TYPE\' => \'DATETIME\'\n }\n ],\n \'INDEXES\' => [\n \'user_api_keys_api_key_idx\',\n {\n \'FIELDS\' => [\n \'api_key\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'user_api_keys_user_id_idx\',\n [\n \'user_id\'\n ]\n ]\n },\n \'user_group_map\' => {\n \'FIELDS\' => [\n \'user_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'group_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'groups\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'isbless\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'grant_type\',\n {\n \'DEFAULT\' => 0,\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT1\'\n }\n ],\n \'INDEXES\' => [\n \'user_group_map_user_id_idx\',\n {\n \'FIELDS\' => [\n \'user_id\',\n \'group_id\',\n \'grant_type\',\n \'isbless\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'versions\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'value\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'product_id\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'products\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT2\'\n },\n \'isactive\',\n {\n \'DEFAULT\' => \'TRUE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ],\n \'INDEXES\' => [\n \'versions_product_id_idx\',\n {\n \'FIELDS\' => [\n \'product_id\',\n \'value\'\n ],\n \'TYPE\' => \'UNIQUE\'\n }\n ]\n },\n \'watch\' => {\n \'FIELDS\' => [\n \'watcher\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'watched\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n }\n ],\n \'INDEXES\' => [\n \'watch_watcher_idx\',\n {\n \'FIELDS\' => [\n \'watcher\',\n \'watched\'\n ],\n \'TYPE\' => \'UNIQUE\'\n },\n \'watch_watched_idx\',\n [\n \'watched\'\n ]\n ]\n },\n \'whine_events\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'owner_userid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'userid\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'profiles\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'subject\',\n {\n \'TYPE\' => \'varchar(128)\'\n },\n \'body\',\n {\n \'TYPE\' => \'MEDIUMTEXT\'\n },\n \'mailifnobugs\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n }\n ]\n },\n \'whine_queries\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'eventid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'whine_events\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'query_name\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(64)\'\n },\n \'sortkey\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n },\n \'onemailperbug\',\n {\n \'DEFAULT\' => \'FALSE\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'BOOLEAN\'\n },\n \'title\',\n {\n \'DEFAULT\' => \'\\\'\\\'\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'varchar(128)\'\n }\n ],\n \'INDEXES\' => [\n \'whine_queries_eventid_idx\',\n [\n \'eventid\'\n ]\n ]\n },\n \'whine_schedules\' => {\n \'FIELDS\' => [\n \'id\',\n {\n \'NOTNULL\' => 1,\n \'PRIMARYKEY\' => 1,\n \'TYPE\' => \'MEDIUMSERIAL\'\n },\n \'eventid\',\n {\n \'NOTNULL\' => 1,\n \'REFERENCES\' => {\n \'COLUMN\' => \'id\',\n \'DELETE\' => \'CASCADE\',\n \'TABLE\' => \'whine_events\',\n \'created\' => 1\n },\n \'TYPE\' => \'INT3\'\n },\n \'run_day\',\n {\n \'TYPE\' => \'varchar(32)\'\n },\n \'run_time\',\n {\n \'TYPE\' => \'varchar(32)\'\n },\n \'run_next\',\n {\n \'TYPE\' => \'DATETIME\'\n },\n \'mailto\',\n {\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT3\'\n },\n \'mailto_type\',\n {\n \'DEFAULT\' => \'0\',\n \'NOTNULL\' => 1,\n \'TYPE\' => \'INT2\'\n }\n ],\n \'INDEXES\' => [\n \'whine_schedules_run_next_idx\',\n [\n \'run_next\'\n ],\n \'whine_schedules_eventid_idx\',\n [\n \'eventid\'\n ]\n ]\n }\n };\n',3.00); +/*!40000 ALTER TABLE `bz_schema` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `category_group_map` +-- + +DROP TABLE IF EXISTS `category_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `category_group_map` ( + `category_id` smallint(6) NOT NULL, + `group_id` mediumint(9) NOT NULL, + UNIQUE KEY `category_group_map_category_id_idx` (`category_id`,`group_id`), + KEY `fk_category_group_map_group_id_groups_id` (`group_id`), + CONSTRAINT `fk_category_group_map_category_id_series_categories_id` FOREIGN KEY (`category_id`) REFERENCES `series_categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_category_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `category_group_map` +-- + +LOCK TABLES `category_group_map` WRITE; +/*!40000 ALTER TABLE `category_group_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `category_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cc` +-- + +DROP TABLE IF EXISTS `cc`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cc` ( + `bug_id` mediumint(9) NOT NULL, + `who` mediumint(9) NOT NULL, + UNIQUE KEY `cc_bug_id_idx` (`bug_id`,`who`), + KEY `cc_who_idx` (`who`), + CONSTRAINT `fk_cc_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_cc_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cc` +-- + +LOCK TABLES `cc` WRITE; +/*!40000 ALTER TABLE `cc` DISABLE KEYS */; +/*!40000 ALTER TABLE `cc` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `classifications` +-- + +DROP TABLE IF EXISTS `classifications`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `classifications` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `description` mediumtext DEFAULT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `classifications_name_idx` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `classifications` +-- + +LOCK TABLES `classifications` WRITE; +/*!40000 ALTER TABLE `classifications` DISABLE KEYS */; +INSERT INTO `classifications` VALUES (1,'Unclassified','Not assigned to any classification',0); +/*!40000 ALTER TABLE `classifications` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `component_cc` +-- + +DROP TABLE IF EXISTS `component_cc`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `component_cc` ( + `user_id` mediumint(9) NOT NULL, + `component_id` mediumint(9) NOT NULL, + UNIQUE KEY `component_cc_user_id_idx` (`component_id`,`user_id`), + KEY `fk_component_cc_user_id_profiles_userid` (`user_id`), + CONSTRAINT `fk_component_cc_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_component_cc_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `component_cc` +-- + +LOCK TABLES `component_cc` WRITE; +/*!40000 ALTER TABLE `component_cc` DISABLE KEYS */; +/*!40000 ALTER TABLE `component_cc` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `components` +-- + +DROP TABLE IF EXISTS `components`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `components` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `product_id` smallint(6) NOT NULL, + `initialowner` mediumint(9) NOT NULL, + `initialqacontact` mediumint(9) DEFAULT NULL, + `description` mediumtext NOT NULL, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `components_product_id_idx` (`product_id`,`name`), + KEY `components_name_idx` (`name`), + KEY `fk_components_initialqacontact_profiles_userid` (`initialqacontact`), + KEY `fk_components_initialowner_profiles_userid` (`initialowner`), + CONSTRAINT `fk_components_initialowner_profiles_userid` FOREIGN KEY (`initialowner`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_components_initialqacontact_profiles_userid` FOREIGN KEY (`initialqacontact`) REFERENCES `profiles` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_components_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `components` +-- + +LOCK TABLES `components` WRITE; +/*!40000 ALTER TABLE `components` DISABLE KEYS */; +INSERT INTO `components` VALUES (1,'TestComponent',1,1,NULL,'This is a test component in the test product database. This ought to be blown away and replaced with real stuff in a finished installation of Bugzilla.',1),(2,'python-bugzilla',2,1,NULL,'Lorem ipsum dolor sit amet',1),(3,'Kernel',3,1,NULL,'Lorem ipsum',1),(4,'Containers',3,1,NULL,'Lorem ipsum',1); +/*!40000 ALTER TABLE `components` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `dependencies` +-- + +DROP TABLE IF EXISTS `dependencies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `dependencies` ( + `blocked` mediumint(9) NOT NULL, + `dependson` mediumint(9) NOT NULL, + UNIQUE KEY `dependencies_blocked_idx` (`blocked`,`dependson`), + KEY `dependencies_dependson_idx` (`dependson`), + CONSTRAINT `fk_dependencies_blocked_bugs_bug_id` FOREIGN KEY (`blocked`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_dependencies_dependson_bugs_bug_id` FOREIGN KEY (`dependson`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `dependencies` +-- + +LOCK TABLES `dependencies` WRITE; +/*!40000 ALTER TABLE `dependencies` DISABLE KEYS */; +/*!40000 ALTER TABLE `dependencies` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `duplicates` +-- + +DROP TABLE IF EXISTS `duplicates`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `duplicates` ( + `dupe_of` mediumint(9) NOT NULL, + `dupe` mediumint(9) NOT NULL, + PRIMARY KEY (`dupe`), + KEY `fk_duplicates_dupe_of_bugs_bug_id` (`dupe_of`), + CONSTRAINT `fk_duplicates_dupe_bugs_bug_id` FOREIGN KEY (`dupe`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_duplicates_dupe_of_bugs_bug_id` FOREIGN KEY (`dupe_of`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `duplicates` +-- + +LOCK TABLES `duplicates` WRITE; +/*!40000 ALTER TABLE `duplicates` DISABLE KEYS */; +/*!40000 ALTER TABLE `duplicates` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `email_bug_ignore` +-- + +DROP TABLE IF EXISTS `email_bug_ignore`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `email_bug_ignore` ( + `user_id` mediumint(9) NOT NULL, + `bug_id` mediumint(9) NOT NULL, + UNIQUE KEY `email_bug_ignore_user_id_idx` (`user_id`,`bug_id`), + KEY `fk_email_bug_ignore_bug_id_bugs_bug_id` (`bug_id`), + CONSTRAINT `fk_email_bug_ignore_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_email_bug_ignore_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `email_bug_ignore` +-- + +LOCK TABLES `email_bug_ignore` WRITE; +/*!40000 ALTER TABLE `email_bug_ignore` DISABLE KEYS */; +/*!40000 ALTER TABLE `email_bug_ignore` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `email_setting` +-- + +DROP TABLE IF EXISTS `email_setting`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `email_setting` ( + `user_id` mediumint(9) NOT NULL, + `relationship` tinyint(4) NOT NULL, + `event` tinyint(4) NOT NULL, + UNIQUE KEY `email_setting_user_id_idx` (`user_id`,`relationship`,`event`), + CONSTRAINT `fk_email_setting_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `email_setting` +-- + +LOCK TABLES `email_setting` WRITE; +/*!40000 ALTER TABLE `email_setting` DISABLE KEYS */; +INSERT INTO `email_setting` VALUES (1,0,0),(1,0,1),(1,0,2),(1,0,3),(1,0,4),(1,0,5),(1,0,6),(1,0,7),(1,0,9),(1,0,10),(1,0,11),(1,0,50),(1,1,0),(1,1,1),(1,1,2),(1,1,3),(1,1,4),(1,1,5),(1,1,6),(1,1,7),(1,1,9),(1,1,10),(1,1,11),(1,1,50),(1,2,0),(1,2,1),(1,2,2),(1,2,3),(1,2,4),(1,2,5),(1,2,6),(1,2,7),(1,2,8),(1,2,9),(1,2,10),(1,2,11),(1,2,50),(1,3,0),(1,3,1),(1,3,2),(1,3,3),(1,3,4),(1,3,5),(1,3,6),(1,3,7),(1,3,9),(1,3,10),(1,3,11),(1,3,50),(1,5,0),(1,5,1),(1,5,2),(1,5,3),(1,5,4),(1,5,5),(1,5,6),(1,5,7),(1,5,9),(1,5,10),(1,5,11),(1,5,50),(1,100,100),(1,100,101); +/*!40000 ALTER TABLE `email_setting` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `field_visibility` +-- + +DROP TABLE IF EXISTS `field_visibility`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `field_visibility` ( + `field_id` mediumint(9) DEFAULT NULL, + `value_id` smallint(6) NOT NULL, + UNIQUE KEY `field_visibility_field_id_idx` (`field_id`,`value_id`), + CONSTRAINT `fk_field_visibility_field_id_fielddefs_id` FOREIGN KEY (`field_id`) REFERENCES `fielddefs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `field_visibility` +-- + +LOCK TABLES `field_visibility` WRITE; +/*!40000 ALTER TABLE `field_visibility` DISABLE KEYS */; +/*!40000 ALTER TABLE `field_visibility` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `fielddefs` +-- + +DROP TABLE IF EXISTS `fielddefs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `fielddefs` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `type` smallint(6) NOT NULL DEFAULT 0, + `custom` tinyint(4) NOT NULL DEFAULT 0, + `description` tinytext NOT NULL, + `long_desc` varchar(255) NOT NULL DEFAULT '', + `mailhead` tinyint(4) NOT NULL DEFAULT 0, + `sortkey` smallint(6) NOT NULL, + `obsolete` tinyint(4) NOT NULL DEFAULT 0, + `enter_bug` tinyint(4) NOT NULL DEFAULT 0, + `buglist` tinyint(4) NOT NULL DEFAULT 0, + `visibility_field_id` mediumint(9) DEFAULT NULL, + `value_field_id` mediumint(9) DEFAULT NULL, + `reverse_desc` tinytext DEFAULT NULL, + `is_mandatory` tinyint(4) NOT NULL DEFAULT 0, + `is_numeric` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `fielddefs_name_idx` (`name`), + KEY `fielddefs_sortkey_idx` (`sortkey`), + KEY `fielddefs_value_field_id_idx` (`value_field_id`), + KEY `fielddefs_is_mandatory_idx` (`is_mandatory`), + KEY `fk_fielddefs_visibility_field_id_fielddefs_id` (`visibility_field_id`), + CONSTRAINT `fk_fielddefs_value_field_id_fielddefs_id` FOREIGN KEY (`value_field_id`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_fielddefs_visibility_field_id_fielddefs_id` FOREIGN KEY (`visibility_field_id`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=60 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `fielddefs` +-- + +LOCK TABLES `fielddefs` WRITE; +/*!40000 ALTER TABLE `fielddefs` DISABLE KEYS */; +INSERT INTO `fielddefs` VALUES (1,'bug_id',0,0,'Bug #','',1,100,0,0,1,NULL,NULL,NULL,0,1),(2,'short_desc',0,0,'Summary','',1,200,0,0,1,NULL,NULL,NULL,1,0),(3,'classification',2,0,'Classification','',1,300,0,0,1,NULL,NULL,NULL,0,0),(4,'product',2,0,'Product','',1,400,0,0,1,NULL,NULL,NULL,1,0),(5,'version',0,0,'Version','',1,500,0,0,1,NULL,NULL,NULL,1,0),(6,'rep_platform',2,0,'Platform','',1,600,0,0,1,NULL,NULL,NULL,0,0),(7,'bug_file_loc',0,0,'URL','',1,700,0,0,1,NULL,NULL,NULL,0,0),(8,'op_sys',2,0,'OS/Version','',1,800,0,0,1,NULL,NULL,NULL,0,0),(9,'bug_status',2,0,'Status','',1,900,0,0,1,NULL,NULL,NULL,0,0),(10,'status_whiteboard',0,0,'Status Whiteboard','',1,1000,0,0,1,NULL,NULL,NULL,0,0),(11,'keywords',8,0,'Keywords','',1,1100,0,0,1,NULL,NULL,NULL,0,0),(12,'resolution',2,0,'Resolution','',0,1200,0,0,1,NULL,NULL,NULL,0,0),(13,'bug_severity',2,0,'Severity','',1,1300,0,0,1,NULL,NULL,NULL,0,0),(14,'priority',2,0,'Priority','',1,1400,0,0,1,NULL,NULL,NULL,0,0),(15,'component',2,0,'Component','',1,1500,0,0,1,NULL,NULL,NULL,1,0),(16,'assigned_to',0,0,'AssignedTo','',1,1600,0,0,1,NULL,NULL,NULL,0,0),(17,'reporter',0,0,'ReportedBy','',1,1700,0,0,1,NULL,NULL,NULL,0,0),(18,'qa_contact',0,0,'QAContact','',1,1800,0,0,1,NULL,NULL,NULL,0,0),(19,'assigned_to_realname',0,0,'AssignedToName','',0,1900,0,0,1,NULL,NULL,NULL,0,0),(20,'reporter_realname',0,0,'ReportedByName','',0,2000,0,0,1,NULL,NULL,NULL,0,0),(21,'qa_contact_realname',0,0,'QAContactName','',0,2100,0,0,1,NULL,NULL,NULL,0,0),(22,'cc',0,0,'CC','',1,2200,0,0,0,NULL,NULL,NULL,0,0),(23,'dependson',0,0,'Depends on','',1,2300,0,0,1,NULL,NULL,NULL,0,1),(24,'blocked',0,0,'Blocks','',1,2400,0,0,1,NULL,NULL,NULL,0,1),(25,'attachments.description',0,0,'Attachment description','',0,2500,0,0,0,NULL,NULL,NULL,0,0),(26,'attachments.filename',0,0,'Attachment filename','',0,2600,0,0,0,NULL,NULL,NULL,0,0),(27,'attachments.mimetype',0,0,'Attachment mime type','',0,2700,0,0,0,NULL,NULL,NULL,0,0),(28,'attachments.ispatch',0,0,'Attachment is patch','',0,2800,0,0,0,NULL,NULL,NULL,0,1),(29,'attachments.isobsolete',0,0,'Attachment is obsolete','',0,2900,0,0,0,NULL,NULL,NULL,0,1),(30,'attachments.isprivate',0,0,'Attachment is private','',0,3000,0,0,0,NULL,NULL,NULL,0,1),(31,'attachments.submitter',0,0,'Attachment creator','',0,3100,0,0,0,NULL,NULL,NULL,0,0),(32,'target_milestone',0,0,'Target Milestone','',1,3200,0,0,1,NULL,NULL,NULL,0,0),(33,'creation_ts',0,0,'Creation date','',0,3300,0,0,1,NULL,NULL,NULL,0,0),(34,'delta_ts',0,0,'Last changed date','',0,3400,0,0,1,NULL,NULL,NULL,0,0),(35,'longdesc',0,0,'Comment','',0,3500,0,0,0,NULL,NULL,NULL,0,0),(36,'longdescs.isprivate',0,0,'Comment is private','',0,3600,0,0,0,NULL,NULL,NULL,0,1),(37,'longdescs.count',0,0,'Number of Comments','',0,3700,0,0,1,NULL,NULL,NULL,0,1),(38,'alias',0,0,'Alias','',0,3800,0,0,1,NULL,NULL,NULL,0,0),(39,'everconfirmed',0,0,'Ever Confirmed','',0,3900,0,0,0,NULL,NULL,NULL,0,1),(40,'reporter_accessible',0,0,'Reporter Accessible','',0,4000,0,0,0,NULL,NULL,NULL,0,1),(41,'cclist_accessible',0,0,'CC Accessible','',0,4100,0,0,0,NULL,NULL,NULL,0,1),(42,'bug_group',0,0,'Group','',1,4200,0,0,0,NULL,NULL,NULL,0,0),(43,'estimated_time',0,0,'Estimated Hours','',1,4300,0,0,1,NULL,NULL,NULL,0,1),(44,'remaining_time',0,0,'Remaining Hours','',0,4400,0,0,1,NULL,NULL,NULL,0,1),(45,'deadline',5,0,'Deadline','',1,4500,0,0,1,NULL,NULL,NULL,0,0),(46,'commenter',0,0,'Commenter','',0,4600,0,0,0,NULL,NULL,NULL,0,0),(47,'flagtypes.name',0,0,'Flags','',0,4700,0,0,1,NULL,NULL,NULL,0,0),(48,'requestees.login_name',0,0,'Flag Requestee','',0,4800,0,0,0,NULL,NULL,NULL,0,0),(49,'setters.login_name',0,0,'Flag Setter','',0,4900,0,0,0,NULL,NULL,NULL,0,0),(50,'work_time',0,0,'Hours Worked','',0,5000,0,0,1,NULL,NULL,NULL,0,1),(51,'percentage_complete',0,0,'Percentage Complete','',0,5100,0,0,1,NULL,NULL,NULL,0,1),(52,'content',0,0,'Content','',0,5200,0,0,0,NULL,NULL,NULL,0,0),(53,'attach_data.thedata',0,0,'Attachment data','',0,5300,0,0,0,NULL,NULL,NULL,0,0),(54,'owner_idle_time',0,0,'Time Since Assignee Touched','',0,5400,0,0,0,NULL,NULL,NULL,0,0),(55,'see_also',7,0,'See Also','',0,5500,0,0,0,NULL,NULL,NULL,0,0),(56,'tag',8,0,'Personal Tags','',0,5600,0,0,1,NULL,NULL,NULL,0,0),(57,'last_visit_ts',5,0,'Last Visit','',0,5700,0,0,1,NULL,NULL,NULL,0,0),(58,'comment_tag',0,0,'Comment Tag','',0,5800,0,0,0,NULL,NULL,NULL,0,0),(59,'days_elapsed',0,0,'Days since bug changed','',0,5900,0,0,0,NULL,NULL,NULL,0,0); +/*!40000 ALTER TABLE `fielddefs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flagexclusions` +-- + +DROP TABLE IF EXISTS `flagexclusions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flagexclusions` ( + `type_id` mediumint(9) NOT NULL, + `product_id` smallint(6) DEFAULT NULL, + `component_id` mediumint(9) DEFAULT NULL, + UNIQUE KEY `flagexclusions_type_id_idx` (`type_id`,`product_id`,`component_id`), + KEY `fk_flagexclusions_component_id_components_id` (`component_id`), + KEY `fk_flagexclusions_product_id_products_id` (`product_id`), + CONSTRAINT `fk_flagexclusions_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flagexclusions_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flagexclusions_type_id_flagtypes_id` FOREIGN KEY (`type_id`) REFERENCES `flagtypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flagexclusions` +-- + +LOCK TABLES `flagexclusions` WRITE; +/*!40000 ALTER TABLE `flagexclusions` DISABLE KEYS */; +/*!40000 ALTER TABLE `flagexclusions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flaginclusions` +-- + +DROP TABLE IF EXISTS `flaginclusions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flaginclusions` ( + `type_id` mediumint(9) NOT NULL, + `product_id` smallint(6) DEFAULT NULL, + `component_id` mediumint(9) DEFAULT NULL, + UNIQUE KEY `flaginclusions_type_id_idx` (`type_id`,`product_id`,`component_id`), + KEY `fk_flaginclusions_component_id_components_id` (`component_id`), + KEY `fk_flaginclusions_product_id_products_id` (`product_id`), + CONSTRAINT `fk_flaginclusions_component_id_components_id` FOREIGN KEY (`component_id`) REFERENCES `components` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flaginclusions_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flaginclusions_type_id_flagtypes_id` FOREIGN KEY (`type_id`) REFERENCES `flagtypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flaginclusions` +-- + +LOCK TABLES `flaginclusions` WRITE; +/*!40000 ALTER TABLE `flaginclusions` DISABLE KEYS */; +/*!40000 ALTER TABLE `flaginclusions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flags` +-- + +DROP TABLE IF EXISTS `flags`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flags` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `type_id` mediumint(9) NOT NULL, + `status` char(1) NOT NULL, + `bug_id` mediumint(9) NOT NULL, + `attach_id` mediumint(9) DEFAULT NULL, + `creation_date` datetime NOT NULL, + `modification_date` datetime DEFAULT NULL, + `setter_id` mediumint(9) NOT NULL, + `requestee_id` mediumint(9) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `flags_bug_id_idx` (`bug_id`,`attach_id`), + KEY `flags_setter_id_idx` (`setter_id`), + KEY `flags_requestee_id_idx` (`requestee_id`), + KEY `flags_type_id_idx` (`type_id`), + KEY `fk_flags_attach_id_attachments_attach_id` (`attach_id`), + CONSTRAINT `fk_flags_attach_id_attachments_attach_id` FOREIGN KEY (`attach_id`) REFERENCES `attachments` (`attach_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flags_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_flags_requestee_id_profiles_userid` FOREIGN KEY (`requestee_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_flags_setter_id_profiles_userid` FOREIGN KEY (`setter_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, + CONSTRAINT `fk_flags_type_id_flagtypes_id` FOREIGN KEY (`type_id`) REFERENCES `flagtypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flags` +-- + +LOCK TABLES `flags` WRITE; +/*!40000 ALTER TABLE `flags` DISABLE KEYS */; +/*!40000 ALTER TABLE `flags` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flagtypes` +-- + +DROP TABLE IF EXISTS `flagtypes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `flagtypes` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + `description` mediumtext NOT NULL, + `cc_list` varchar(200) DEFAULT NULL, + `target_type` char(1) NOT NULL DEFAULT 'b', + `is_active` tinyint(4) NOT NULL DEFAULT 1, + `is_requestable` tinyint(4) NOT NULL DEFAULT 0, + `is_requesteeble` tinyint(4) NOT NULL DEFAULT 0, + `is_multiplicable` tinyint(4) NOT NULL DEFAULT 0, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `grant_group_id` mediumint(9) DEFAULT NULL, + `request_group_id` mediumint(9) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_flagtypes_request_group_id_groups_id` (`request_group_id`), + KEY `fk_flagtypes_grant_group_id_groups_id` (`grant_group_id`), + CONSTRAINT `fk_flagtypes_grant_group_id_groups_id` FOREIGN KEY (`grant_group_id`) REFERENCES `groups` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_flagtypes_request_group_id_groups_id` FOREIGN KEY (`request_group_id`) REFERENCES `groups` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flagtypes` +-- + +LOCK TABLES `flagtypes` WRITE; +/*!40000 ALTER TABLE `flagtypes` DISABLE KEYS */; +/*!40000 ALTER TABLE `flagtypes` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `group_control_map` +-- + +DROP TABLE IF EXISTS `group_control_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `group_control_map` ( + `group_id` mediumint(9) NOT NULL, + `product_id` smallint(6) NOT NULL, + `entry` tinyint(4) NOT NULL DEFAULT 0, + `membercontrol` tinyint(4) NOT NULL DEFAULT 0, + `othercontrol` tinyint(4) NOT NULL DEFAULT 0, + `canedit` tinyint(4) NOT NULL DEFAULT 0, + `editcomponents` tinyint(4) NOT NULL DEFAULT 0, + `editbugs` tinyint(4) NOT NULL DEFAULT 0, + `canconfirm` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `group_control_map_product_id_idx` (`product_id`,`group_id`), + KEY `group_control_map_group_id_idx` (`group_id`), + CONSTRAINT `fk_group_control_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_group_control_map_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `group_control_map` +-- + +LOCK TABLES `group_control_map` WRITE; +/*!40000 ALTER TABLE `group_control_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `group_control_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `group_group_map` +-- + +DROP TABLE IF EXISTS `group_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `group_group_map` ( + `member_id` mediumint(9) NOT NULL, + `grantor_id` mediumint(9) NOT NULL, + `grant_type` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `group_group_map_member_id_idx` (`member_id`,`grantor_id`,`grant_type`), + KEY `fk_group_group_map_grantor_id_groups_id` (`grantor_id`), + CONSTRAINT `fk_group_group_map_grantor_id_groups_id` FOREIGN KEY (`grantor_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_group_group_map_member_id_groups_id` FOREIGN KEY (`member_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `group_group_map` +-- + +LOCK TABLES `group_group_map` WRITE; +/*!40000 ALTER TABLE `group_group_map` DISABLE KEYS */; +INSERT INTO `group_group_map` VALUES (1,1,0),(1,1,1),(1,1,2),(1,2,0),(1,2,1),(1,2,2),(1,3,0),(1,3,1),(1,3,2),(1,4,0),(1,4,1),(1,4,2),(1,5,0),(1,5,1),(1,5,2),(1,6,0),(1,6,1),(1,6,2),(1,7,0),(1,7,1),(1,7,2),(1,8,0),(1,8,1),(1,8,2),(1,9,0),(1,9,1),(1,9,2),(1,10,0),(1,10,1),(1,10,2),(1,11,0),(1,11,1),(1,11,2),(8,11,0),(10,11,0),(1,12,0),(1,12,1),(1,12,2),(1,13,0),(1,13,1),(1,13,2),(12,13,0),(1,14,0),(1,14,1),(1,14,2); +/*!40000 ALTER TABLE `group_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `groups` +-- + +DROP TABLE IF EXISTS `groups`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `groups` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `description` mediumtext NOT NULL, + `isbuggroup` tinyint(4) NOT NULL, + `userregexp` tinytext NOT NULL DEFAULT '', + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `icon_url` tinytext DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `groups_name_idx` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `groups` +-- + +LOCK TABLES `groups` WRITE; +/*!40000 ALTER TABLE `groups` DISABLE KEYS */; +INSERT INTO `groups` VALUES (1,'admin','Administrators',0,'',1,NULL),(2,'tweakparams','Can change Parameters',0,'',1,NULL),(3,'editusers','Can edit or disable users',0,'',1,NULL),(4,'creategroups','Can create and destroy groups',0,'',1,NULL),(5,'editclassifications','Can create, destroy, and edit classifications',0,'',1,NULL),(6,'editcomponents','Can create, destroy, and edit components',0,'',1,NULL),(7,'editkeywords','Can create, destroy, and edit keywords',0,'',1,NULL),(8,'editbugs','Can edit all bug fields',0,'.*',1,NULL),(9,'canconfirm','Can confirm a bug or mark it a duplicate',0,'',1,NULL),(10,'bz_canusewhineatothers','Can configure whine reports for other users',0,'',1,NULL),(11,'bz_canusewhines','User can configure whine reports for self',0,'',1,NULL),(12,'bz_sudoers','Can perform actions as other users',0,'',1,NULL),(13,'bz_sudo_protect','Can not be impersonated by other users',0,'',1,NULL),(14,'bz_quip_moderators','Can moderate quips',0,'',1,NULL); +/*!40000 ALTER TABLE `groups` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `keyworddefs` +-- + +DROP TABLE IF EXISTS `keyworddefs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `keyworddefs` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `description` mediumtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `keyworddefs_name_idx` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `keyworddefs` +-- + +LOCK TABLES `keyworddefs` WRITE; +/*!40000 ALTER TABLE `keyworddefs` DISABLE KEYS */; +/*!40000 ALTER TABLE `keyworddefs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `keywords` +-- + +DROP TABLE IF EXISTS `keywords`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `keywords` ( + `bug_id` mediumint(9) NOT NULL, + `keywordid` smallint(6) NOT NULL, + UNIQUE KEY `keywords_bug_id_idx` (`bug_id`,`keywordid`), + KEY `keywords_keywordid_idx` (`keywordid`), + CONSTRAINT `fk_keywords_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_keywords_keywordid_keyworddefs_id` FOREIGN KEY (`keywordid`) REFERENCES `keyworddefs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `keywords` +-- + +LOCK TABLES `keywords` WRITE; +/*!40000 ALTER TABLE `keywords` DISABLE KEYS */; +/*!40000 ALTER TABLE `keywords` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `login_failure` +-- + +DROP TABLE IF EXISTS `login_failure`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `login_failure` ( + `user_id` mediumint(9) NOT NULL, + `login_time` datetime NOT NULL, + `ip_addr` varchar(40) NOT NULL, + KEY `login_failure_user_id_idx` (`user_id`), + CONSTRAINT `fk_login_failure_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `login_failure` +-- + +LOCK TABLES `login_failure` WRITE; +/*!40000 ALTER TABLE `login_failure` DISABLE KEYS */; +/*!40000 ALTER TABLE `login_failure` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `logincookies` +-- + +DROP TABLE IF EXISTS `logincookies`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `logincookies` ( + `cookie` varchar(16) NOT NULL, + `userid` mediumint(9) NOT NULL, + `ipaddr` varchar(40) DEFAULT NULL, + `lastused` datetime NOT NULL, + PRIMARY KEY (`cookie`), + KEY `logincookies_lastused_idx` (`lastused`), + KEY `fk_logincookies_userid_profiles_userid` (`userid`), + CONSTRAINT `fk_logincookies_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `logincookies` +-- + +LOCK TABLES `logincookies` WRITE; +/*!40000 ALTER TABLE `logincookies` DISABLE KEYS */; +INSERT INTO `logincookies` VALUES ('Ypt6rPqHjG',1,NULL,'2023-11-27 15:53:08'); +/*!40000 ALTER TABLE `logincookies` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs` +-- + +DROP TABLE IF EXISTS `longdescs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs` ( + `comment_id` int(11) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `who` mediumint(9) NOT NULL, + `bug_when` datetime NOT NULL, + `work_time` decimal(7,2) NOT NULL DEFAULT 0.00, + `thetext` mediumtext NOT NULL, + `isprivate` tinyint(4) NOT NULL DEFAULT 0, + `already_wrapped` tinyint(4) NOT NULL DEFAULT 0, + `type` smallint(6) NOT NULL DEFAULT 0, + `extra_data` varchar(255) DEFAULT NULL, + PRIMARY KEY (`comment_id`), + KEY `longdescs_bug_id_idx` (`bug_id`,`work_time`), + KEY `longdescs_who_idx` (`who`,`bug_id`), + KEY `longdescs_bug_when_idx` (`bug_when`), + CONSTRAINT `fk_longdescs_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_longdescs_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs` +-- + +LOCK TABLES `longdescs` WRITE; +/*!40000 ALTER TABLE `longdescs` DISABLE KEYS */; +INSERT INTO `longdescs` VALUES (1,1,1,'2023-11-27 15:35:33',0.00,'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.',0,0,0,NULL),(2,1,1,'2023-11-27 15:37:05',0.00,'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.',0,0,0,NULL),(3,2,1,'2023-11-27 15:38:45',0.00,'Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.',0,0,0,NULL); +/*!40000 ALTER TABLE `longdescs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs_tags` +-- + +DROP TABLE IF EXISTS `longdescs_tags`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs_tags` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `comment_id` int(11) DEFAULT NULL, + `tag` varchar(24) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `longdescs_tags_idx` (`comment_id`,`tag`), + CONSTRAINT `fk_longdescs_tags_comment_id_longdescs_comment_id` FOREIGN KEY (`comment_id`) REFERENCES `longdescs` (`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs_tags` +-- + +LOCK TABLES `longdescs_tags` WRITE; +/*!40000 ALTER TABLE `longdescs_tags` DISABLE KEYS */; +/*!40000 ALTER TABLE `longdescs_tags` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs_tags_activity` +-- + +DROP TABLE IF EXISTS `longdescs_tags_activity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs_tags_activity` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `bug_id` mediumint(9) NOT NULL, + `comment_id` int(11) DEFAULT NULL, + `who` mediumint(9) NOT NULL, + `bug_when` datetime NOT NULL, + `added` varchar(24) DEFAULT NULL, + `removed` varchar(24) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `longdescs_tags_activity_bug_id_idx` (`bug_id`), + KEY `fk_longdescs_tags_activity_comment_id_longdescs_comment_id` (`comment_id`), + KEY `fk_longdescs_tags_activity_who_profiles_userid` (`who`), + CONSTRAINT `fk_longdescs_tags_activity_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_longdescs_tags_activity_comment_id_longdescs_comment_id` FOREIGN KEY (`comment_id`) REFERENCES `longdescs` (`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_longdescs_tags_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs_tags_activity` +-- + +LOCK TABLES `longdescs_tags_activity` WRITE; +/*!40000 ALTER TABLE `longdescs_tags_activity` DISABLE KEYS */; +/*!40000 ALTER TABLE `longdescs_tags_activity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `longdescs_tags_weights` +-- + +DROP TABLE IF EXISTS `longdescs_tags_weights`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `longdescs_tags_weights` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `tag` varchar(24) NOT NULL, + `weight` mediumint(9) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `longdescs_tags_weights_tag_idx` (`tag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `longdescs_tags_weights` +-- + +LOCK TABLES `longdescs_tags_weights` WRITE; +/*!40000 ALTER TABLE `longdescs_tags_weights` DISABLE KEYS */; +/*!40000 ALTER TABLE `longdescs_tags_weights` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `mail_staging` +-- + +DROP TABLE IF EXISTS `mail_staging`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `mail_staging` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `message` longblob NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `mail_staging` +-- + +LOCK TABLES `mail_staging` WRITE; +/*!40000 ALTER TABLE `mail_staging` DISABLE KEYS */; +/*!40000 ALTER TABLE `mail_staging` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `milestones` +-- + +DROP TABLE IF EXISTS `milestones`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `milestones` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `product_id` smallint(6) NOT NULL, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `milestones_product_id_idx` (`product_id`,`value`), + CONSTRAINT `fk_milestones_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `milestones` +-- + +LOCK TABLES `milestones` WRITE; +/*!40000 ALTER TABLE `milestones` DISABLE KEYS */; +INSERT INTO `milestones` VALUES (1,1,'---',0,1),(2,2,'---',0,1),(3,3,'---',0,1); +/*!40000 ALTER TABLE `milestones` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `namedqueries` +-- + +DROP TABLE IF EXISTS `namedqueries`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `namedqueries` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `userid` mediumint(9) NOT NULL, + `name` varchar(64) NOT NULL, + `query` mediumtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `namedqueries_userid_idx` (`userid`,`name`), + CONSTRAINT `fk_namedqueries_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `namedqueries` +-- + +LOCK TABLES `namedqueries` WRITE; +/*!40000 ALTER TABLE `namedqueries` DISABLE KEYS */; +/*!40000 ALTER TABLE `namedqueries` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `namedqueries_link_in_footer` +-- + +DROP TABLE IF EXISTS `namedqueries_link_in_footer`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `namedqueries_link_in_footer` ( + `namedquery_id` mediumint(9) NOT NULL, + `user_id` mediumint(9) NOT NULL, + UNIQUE KEY `namedqueries_link_in_footer_id_idx` (`namedquery_id`,`user_id`), + KEY `namedqueries_link_in_footer_userid_idx` (`user_id`), + CONSTRAINT `fk_namedqueries_link_in_footer_namedquery_id_namedqueries_id` FOREIGN KEY (`namedquery_id`) REFERENCES `namedqueries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_namedqueries_link_in_footer_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `namedqueries_link_in_footer` +-- + +LOCK TABLES `namedqueries_link_in_footer` WRITE; +/*!40000 ALTER TABLE `namedqueries_link_in_footer` DISABLE KEYS */; +/*!40000 ALTER TABLE `namedqueries_link_in_footer` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `namedquery_group_map` +-- + +DROP TABLE IF EXISTS `namedquery_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `namedquery_group_map` ( + `namedquery_id` mediumint(9) NOT NULL, + `group_id` mediumint(9) NOT NULL, + UNIQUE KEY `namedquery_group_map_namedquery_id_idx` (`namedquery_id`), + KEY `namedquery_group_map_group_id_idx` (`group_id`), + CONSTRAINT `fk_namedquery_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_namedquery_group_map_namedquery_id_namedqueries_id` FOREIGN KEY (`namedquery_id`) REFERENCES `namedqueries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `namedquery_group_map` +-- + +LOCK TABLES `namedquery_group_map` WRITE; +/*!40000 ALTER TABLE `namedquery_group_map` DISABLE KEYS */; +/*!40000 ALTER TABLE `namedquery_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `op_sys` +-- + +DROP TABLE IF EXISTS `op_sys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `op_sys` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `op_sys_value_idx` (`value`), + KEY `op_sys_sortkey_idx` (`sortkey`,`value`), + KEY `op_sys_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `op_sys` +-- + +LOCK TABLES `op_sys` WRITE; +/*!40000 ALTER TABLE `op_sys` DISABLE KEYS */; +INSERT INTO `op_sys` VALUES (1,'All',100,1,NULL),(2,'Windows',200,1,NULL),(3,'Mac OS',300,1,NULL),(4,'Linux',400,1,NULL),(5,'Other',500,1,NULL); +/*!40000 ALTER TABLE `op_sys` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `priority` +-- + +DROP TABLE IF EXISTS `priority`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `priority` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `priority_value_idx` (`value`), + KEY `priority_sortkey_idx` (`sortkey`,`value`), + KEY `priority_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `priority` +-- + +LOCK TABLES `priority` WRITE; +/*!40000 ALTER TABLE `priority` DISABLE KEYS */; +INSERT INTO `priority` VALUES (1,'Highest',100,1,NULL),(2,'High',200,1,NULL),(3,'Normal',300,1,NULL),(4,'Low',400,1,NULL),(5,'Lowest',500,1,NULL),(6,'---',600,1,NULL); +/*!40000 ALTER TABLE `priority` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `products` +-- + +DROP TABLE IF EXISTS `products`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `products` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `classification_id` smallint(6) NOT NULL DEFAULT 1, + `description` mediumtext NOT NULL, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `defaultmilestone` varchar(64) NOT NULL DEFAULT '---', + `allows_unconfirmed` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `products_name_idx` (`name`), + KEY `fk_products_classification_id_classifications_id` (`classification_id`), + CONSTRAINT `fk_products_classification_id_classifications_id` FOREIGN KEY (`classification_id`) REFERENCES `classifications` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `products` +-- + +LOCK TABLES `products` WRITE; +/*!40000 ALTER TABLE `products` DISABLE KEYS */; +INSERT INTO `products` VALUES (1,'TestProduct',1,'This is a test product. This ought to be blown away and replaced with real stuff in a finished installation of bugzilla.',1,'---',1),(2,'Red Hat Enterprise Linux 9',1,'Lorem ipsum',1,'---',1),(3,'SUSE Linux Enterprise Server 15 SP6',1,'Lorem ipsum dolor sit amet',1,'---',1); +/*!40000 ALTER TABLE `products` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profile_search` +-- + +DROP TABLE IF EXISTS `profile_search`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profile_search` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `bug_list` mediumtext NOT NULL, + `list_order` mediumtext DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `profile_search_user_id_idx` (`user_id`), + CONSTRAINT `fk_profile_search_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profile_search` +-- + +LOCK TABLES `profile_search` WRITE; +/*!40000 ALTER TABLE `profile_search` DISABLE KEYS */; +INSERT INTO `profile_search` VALUES (1,1,'1','bug_status,priority,assigned_to,bug_id'); +/*!40000 ALTER TABLE `profile_search` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profile_setting` +-- + +DROP TABLE IF EXISTS `profile_setting`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profile_setting` ( + `user_id` mediumint(9) NOT NULL, + `setting_name` varchar(32) NOT NULL, + `setting_value` varchar(32) NOT NULL, + UNIQUE KEY `profile_setting_value_unique_idx` (`user_id`,`setting_name`), + KEY `fk_profile_setting_setting_name_setting_name` (`setting_name`), + CONSTRAINT `fk_profile_setting_setting_name_setting_name` FOREIGN KEY (`setting_name`) REFERENCES `setting` (`name`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_profile_setting_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profile_setting` +-- + +LOCK TABLES `profile_setting` WRITE; +/*!40000 ALTER TABLE `profile_setting` DISABLE KEYS */; +/*!40000 ALTER TABLE `profile_setting` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profiles` +-- + +DROP TABLE IF EXISTS `profiles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profiles` ( + `userid` mediumint(9) NOT NULL AUTO_INCREMENT, + `login_name` varchar(255) NOT NULL, + `cryptpassword` varchar(128) DEFAULT NULL, + `realname` varchar(255) NOT NULL DEFAULT '', + `disabledtext` mediumtext NOT NULL DEFAULT '', + `disable_mail` tinyint(4) NOT NULL DEFAULT 0, + `mybugslink` tinyint(4) NOT NULL DEFAULT 1, + `extern_id` varchar(64) DEFAULT NULL, + `is_enabled` tinyint(4) NOT NULL DEFAULT 1, + `last_seen_date` datetime DEFAULT NULL, + PRIMARY KEY (`userid`), + UNIQUE KEY `profiles_login_name_idx` (`login_name`), + UNIQUE KEY `profiles_extern_id_idx` (`extern_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profiles` +-- + +LOCK TABLES `profiles` WRITE; +/*!40000 ALTER TABLE `profiles` DISABLE KEYS */; +INSERT INTO `profiles` VALUES (1,'andreas@hasenkopf.xyz','2207pp7o,ialUTtf7x78ge5SbbN7+W+1lXGJBXmMlYt26C1egd4g{SHA-256}','Andreas','',0,1,NULL,1,'2023-11-27 00:00:00'); +/*!40000 ALTER TABLE `profiles` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `profiles_activity` +-- + +DROP TABLE IF EXISTS `profiles_activity`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profiles_activity` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `userid` mediumint(9) NOT NULL, + `who` mediumint(9) NOT NULL, + `profiles_when` datetime NOT NULL, + `fieldid` mediumint(9) NOT NULL, + `oldvalue` tinytext DEFAULT NULL, + `newvalue` tinytext DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `profiles_activity_userid_idx` (`userid`), + KEY `profiles_activity_profiles_when_idx` (`profiles_when`), + KEY `profiles_activity_fieldid_idx` (`fieldid`), + KEY `fk_profiles_activity_who_profiles_userid` (`who`), + CONSTRAINT `fk_profiles_activity_fieldid_fielddefs_id` FOREIGN KEY (`fieldid`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_profiles_activity_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_profiles_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `profiles_activity` +-- + +LOCK TABLES `profiles_activity` WRITE; +/*!40000 ALTER TABLE `profiles_activity` DISABLE KEYS */; +INSERT INTO `profiles_activity` VALUES (1,1,1,'2023-09-20 13:12:55',33,NULL,'2023-09-20 13:12:55'); +/*!40000 ALTER TABLE `profiles_activity` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `quips` +-- + +DROP TABLE IF EXISTS `quips`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `quips` ( + `quipid` mediumint(9) NOT NULL AUTO_INCREMENT, + `userid` mediumint(9) DEFAULT NULL, + `quip` varchar(512) NOT NULL, + `approved` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`quipid`), + KEY `fk_quips_userid_profiles_userid` (`userid`), + CONSTRAINT `fk_quips_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `quips` +-- + +LOCK TABLES `quips` WRITE; +/*!40000 ALTER TABLE `quips` DISABLE KEYS */; +/*!40000 ALTER TABLE `quips` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `rep_platform` +-- + +DROP TABLE IF EXISTS `rep_platform`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `rep_platform` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `rep_platform_value_idx` (`value`), + KEY `rep_platform_sortkey_idx` (`sortkey`,`value`), + KEY `rep_platform_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `rep_platform` +-- + +LOCK TABLES `rep_platform` WRITE; +/*!40000 ALTER TABLE `rep_platform` DISABLE KEYS */; +INSERT INTO `rep_platform` VALUES (1,'All',100,1,NULL),(2,'PC',200,1,NULL),(3,'Macintosh',300,1,NULL),(4,'Other',400,1,NULL); +/*!40000 ALTER TABLE `rep_platform` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `reports` +-- + +DROP TABLE IF EXISTS `reports`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `reports` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `name` varchar(64) NOT NULL, + `query` mediumtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `reports_user_id_idx` (`user_id`,`name`), + CONSTRAINT `fk_reports_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `reports` +-- + +LOCK TABLES `reports` WRITE; +/*!40000 ALTER TABLE `reports` DISABLE KEYS */; +/*!40000 ALTER TABLE `reports` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `resolution` +-- + +DROP TABLE IF EXISTS `resolution`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `resolution` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + `visibility_value_id` smallint(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `resolution_value_idx` (`value`), + KEY `resolution_sortkey_idx` (`sortkey`,`value`), + KEY `resolution_visibility_value_id_idx` (`visibility_value_id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `resolution` +-- + +LOCK TABLES `resolution` WRITE; +/*!40000 ALTER TABLE `resolution` DISABLE KEYS */; +INSERT INTO `resolution` VALUES (1,'',100,1,NULL),(2,'FIXED',200,1,NULL),(3,'INVALID',300,1,NULL),(4,'WONTFIX',400,1,NULL),(5,'DUPLICATE',500,1,NULL),(6,'WORKSFORME',600,1,NULL); +/*!40000 ALTER TABLE `resolution` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `series` +-- + +DROP TABLE IF EXISTS `series`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `series` ( + `series_id` mediumint(9) NOT NULL AUTO_INCREMENT, + `creator` mediumint(9) DEFAULT NULL, + `category` smallint(6) NOT NULL, + `subcategory` smallint(6) NOT NULL, + `name` varchar(64) NOT NULL, + `frequency` smallint(6) NOT NULL, + `query` mediumtext NOT NULL, + `is_public` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`series_id`), + UNIQUE KEY `series_category_idx` (`category`,`subcategory`,`name`), + KEY `series_creator_idx` (`creator`), + KEY `fk_series_subcategory_series_categories_id` (`subcategory`), + CONSTRAINT `fk_series_category_series_categories_id` FOREIGN KEY (`category`) REFERENCES `series_categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_series_creator_profiles_userid` FOREIGN KEY (`creator`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_series_subcategory_series_categories_id` FOREIGN KEY (`subcategory`) REFERENCES `series_categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `series` +-- + +LOCK TABLES `series` WRITE; +/*!40000 ALTER TABLE `series` DISABLE KEYS */; +INSERT INTO `series` VALUES (1,1,1,2,'UNCONFIRMED',1,'bug_status=UNCONFIRMED&product=Red%20Hat%20Enterprise%20Linux%209',1),(2,1,1,2,'CONFIRMED',1,'bug_status=CONFIRMED&product=Red%20Hat%20Enterprise%20Linux%209',1),(3,1,1,2,'IN_PROGRESS',1,'bug_status=IN_PROGRESS&product=Red%20Hat%20Enterprise%20Linux%209',1),(4,1,1,2,'RESOLVED',1,'bug_status=RESOLVED&product=Red%20Hat%20Enterprise%20Linux%209',1),(5,1,1,2,'VERIFIED',1,'bug_status=VERIFIED&product=Red%20Hat%20Enterprise%20Linux%209',1),(6,1,1,2,'FIXED',1,'resolution=FIXED&product=Red%20Hat%20Enterprise%20Linux%209',1),(7,1,1,2,'INVALID',1,'resolution=INVALID&product=Red%20Hat%20Enterprise%20Linux%209',1),(8,1,1,2,'WONTFIX',1,'resolution=WONTFIX&product=Red%20Hat%20Enterprise%20Linux%209',1),(9,1,1,2,'DUPLICATE',1,'resolution=DUPLICATE&product=Red%20Hat%20Enterprise%20Linux%209',1),(10,1,1,2,'WORKSFORME',1,'resolution=WORKSFORME&product=Red%20Hat%20Enterprise%20Linux%209',1),(11,1,1,2,'All Open',1,'bug_status=UNCONFIRMED&bug_status=CONFIRMED&bug_status=IN_PROGRESS&product=Red%20Hat%20Enterprise%20Linux%209',1),(12,1,1,3,'All Open',1,'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.&product=Red%20Hat%20Enterprise%20Linux%209&component=python-bugzilla',1),(13,1,1,3,'All Closed',1,'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.&product=Red%20Hat%20Enterprise%20Linux%209&component=python-bugzilla',1),(14,1,4,2,'UNCONFIRMED',1,'bug_status=UNCONFIRMED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(15,1,4,2,'CONFIRMED',1,'bug_status=CONFIRMED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(16,1,4,2,'IN_PROGRESS',1,'bug_status=IN_PROGRESS&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(17,1,4,2,'RESOLVED',1,'bug_status=RESOLVED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(18,1,4,2,'VERIFIED',1,'bug_status=VERIFIED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(19,1,4,2,'FIXED',1,'resolution=FIXED&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(20,1,4,2,'INVALID',1,'resolution=INVALID&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(21,1,4,2,'WONTFIX',1,'resolution=WONTFIX&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(22,1,4,2,'DUPLICATE',1,'resolution=DUPLICATE&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(23,1,4,2,'WORKSFORME',1,'resolution=WORKSFORME&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(24,1,4,2,'All Open',1,'bug_status=UNCONFIRMED&bug_status=CONFIRMED&bug_status=IN_PROGRESS&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6',1),(25,1,4,5,'All Open',1,'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Kernel',1),(26,1,4,5,'All Closed',1,'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Kernel',1),(27,1,4,6,'All Open',1,'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Containers',1),(28,1,4,6,'All Closed',1,'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.&product=SUSE%20Linux%20Enterprise%20Server%2015%20SP6&component=Containers',1); +/*!40000 ALTER TABLE `series` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `series_categories` +-- + +DROP TABLE IF EXISTS `series_categories`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `series_categories` ( + `id` smallint(6) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `series_categories_name_idx` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `series_categories` +-- + +LOCK TABLES `series_categories` WRITE; +/*!40000 ALTER TABLE `series_categories` DISABLE KEYS */; +INSERT INTO `series_categories` VALUES (2,'-All-'),(6,'Containers'),(5,'Kernel'),(3,'python-bugzilla'),(1,'Red Hat Enterprise Linux 9'),(4,'SUSE Linux Enterprise Server 15 SP6'); +/*!40000 ALTER TABLE `series_categories` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `series_data` +-- + +DROP TABLE IF EXISTS `series_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `series_data` ( + `series_id` mediumint(9) NOT NULL, + `series_date` datetime NOT NULL, + `series_value` mediumint(9) NOT NULL, + UNIQUE KEY `series_data_series_id_idx` (`series_id`,`series_date`), + CONSTRAINT `fk_series_data_series_id_series_series_id` FOREIGN KEY (`series_id`) REFERENCES `series` (`series_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `series_data` +-- + +LOCK TABLES `series_data` WRITE; +/*!40000 ALTER TABLE `series_data` DISABLE KEYS */; +/*!40000 ALTER TABLE `series_data` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `setting` +-- + +DROP TABLE IF EXISTS `setting`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `setting` ( + `name` varchar(32) NOT NULL, + `default_value` varchar(32) NOT NULL, + `is_enabled` tinyint(4) NOT NULL DEFAULT 1, + `subclass` varchar(32) DEFAULT NULL, + PRIMARY KEY (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `setting` +-- + +LOCK TABLES `setting` WRITE; +/*!40000 ALTER TABLE `setting` DISABLE KEYS */; +INSERT INTO `setting` VALUES ('bugmail_new_prefix','on',1,NULL),('comment_box_position','before_comments',1,NULL),('comment_sort_order','oldest_to_newest',1,NULL),('csv_colsepchar',',',1,NULL),('display_quips','on',1,NULL),('email_format','html',1,NULL),('lang','en',1,'Lang'),('possible_duplicates','on',1,NULL),('post_bug_submit_action','next_bug',1,NULL),('quicksearch_fulltext','on',1,NULL),('quote_replies','quoted_reply',1,NULL),('requestee_cc','on',1,NULL),('skin','Dusk',1,'Skin'),('state_addselfcc','cc_unless_role',1,NULL),('timezone','local',1,'Timezone'),('zoom_textareas','on',1,NULL); +/*!40000 ALTER TABLE `setting` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `setting_value` +-- + +DROP TABLE IF EXISTS `setting_value`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `setting_value` ( + `name` varchar(32) NOT NULL, + `value` varchar(32) NOT NULL, + `sortindex` smallint(6) NOT NULL, + UNIQUE KEY `setting_value_nv_unique_idx` (`name`,`value`), + UNIQUE KEY `setting_value_ns_unique_idx` (`name`,`sortindex`), + CONSTRAINT `fk_setting_value_name_setting_name` FOREIGN KEY (`name`) REFERENCES `setting` (`name`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `setting_value` +-- + +LOCK TABLES `setting_value` WRITE; +/*!40000 ALTER TABLE `setting_value` DISABLE KEYS */; +INSERT INTO `setting_value` VALUES ('bugmail_new_prefix','on',5),('bugmail_new_prefix','off',10),('comment_box_position','before_comments',5),('comment_box_position','after_comments',10),('comment_sort_order','oldest_to_newest',5),('comment_sort_order','newest_to_oldest',10),('comment_sort_order','newest_to_oldest_desc_first',15),('csv_colsepchar',',',5),('csv_colsepchar',';',10),('display_quips','on',5),('display_quips','off',10),('email_format','html',5),('email_format','text_only',10),('possible_duplicates','on',5),('possible_duplicates','off',10),('post_bug_submit_action','next_bug',5),('post_bug_submit_action','same_bug',10),('post_bug_submit_action','nothing',15),('quicksearch_fulltext','on',5),('quicksearch_fulltext','off',10),('quote_replies','quoted_reply',5),('quote_replies','simple_reply',10),('quote_replies','off',15),('requestee_cc','on',5),('requestee_cc','off',10),('state_addselfcc','always',5),('state_addselfcc','never',10),('state_addselfcc','cc_unless_role',15),('zoom_textareas','on',5),('zoom_textareas','off',10); +/*!40000 ALTER TABLE `setting_value` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `status_workflow` +-- + +DROP TABLE IF EXISTS `status_workflow`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `status_workflow` ( + `old_status` smallint(6) DEFAULT NULL, + `new_status` smallint(6) NOT NULL, + `require_comment` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `status_workflow_idx` (`old_status`,`new_status`), + KEY `fk_status_workflow_new_status_bug_status_id` (`new_status`), + CONSTRAINT `fk_status_workflow_new_status_bug_status_id` FOREIGN KEY (`new_status`) REFERENCES `bug_status` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_status_workflow_old_status_bug_status_id` FOREIGN KEY (`old_status`) REFERENCES `bug_status` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `status_workflow` +-- + +LOCK TABLES `status_workflow` WRITE; +/*!40000 ALTER TABLE `status_workflow` DISABLE KEYS */; +INSERT INTO `status_workflow` VALUES (NULL,1,0),(NULL,2,0),(NULL,3,0),(1,2,0),(1,3,0),(1,4,0),(2,3,0),(2,4,0),(3,2,0),(3,4,0),(4,1,0),(4,2,0),(4,5,0),(5,1,0),(5,2,0); +/*!40000 ALTER TABLE `status_workflow` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tag` +-- + +DROP TABLE IF EXISTS `tag`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tag` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL, + `user_id` mediumint(9) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `tag_user_id_idx` (`user_id`,`name`), + CONSTRAINT `fk_tag_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tag` +-- + +LOCK TABLES `tag` WRITE; +/*!40000 ALTER TABLE `tag` DISABLE KEYS */; +/*!40000 ALTER TABLE `tag` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tokens` +-- + +DROP TABLE IF EXISTS `tokens`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tokens` ( + `userid` mediumint(9) DEFAULT NULL, + `issuedate` datetime NOT NULL, + `token` varchar(16) NOT NULL, + `tokentype` varchar(16) NOT NULL, + `eventdata` tinytext DEFAULT NULL, + PRIMARY KEY (`token`), + KEY `tokens_userid_idx` (`userid`), + CONSTRAINT `fk_tokens_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tokens` +-- + +LOCK TABLES `tokens` WRITE; +/*!40000 ALTER TABLE `tokens` DISABLE KEYS */; +INSERT INTO `tokens` VALUES (1,'2023-11-27 15:46:15','5HVJhRRo6t','session','edit_parameters'),(1,'2023-11-27 12:25:54','a9MgwT7N7x','session','edit_product'),(1,'2023-11-27 15:42:50','CRSwDhzaXc','session','edit_parameters'),(1,'2023-11-27 12:29:18','DXFuAIZ5GH','session','edit_product'),(1,'2023-09-20 13:13:14','ery9F3ZaAV','session','edit_user_prefs'),(1,'2023-11-27 15:44:26','gnPazrbni2','session','edit_product'),(1,'2023-11-27 15:43:10','GZT1mYgIAF','session','edit_settings'),(1,'2023-11-27 15:42:57','hYkjAGXNIj','session','add_field'),(1,'2023-11-27 15:46:35','ibDe8MPzGE','session','edit_parameters'),(1,'2023-09-20 13:13:14','oukIJJwYod','api_token',''),(1,'2023-11-27 12:26:29','PIjhZLJ29K','session','edit_product'),(1,'2023-11-27 12:23:39','pIrqNpsRDo','api_token',''),(1,'2023-11-27 15:44:36','rkyOtDBxr4','session','edit_group_controls'),(1,'2023-09-20 13:13:20','VLrgLovfH9','session','edit_user_prefs'),(1,'2023-11-27 15:45:59','xgQpxIS10M','session','edit_user_prefs'); +/*!40000 ALTER TABLE `tokens` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_error` +-- + +DROP TABLE IF EXISTS `ts_error`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_error` ( + `error_time` int(11) NOT NULL, + `jobid` int(11) NOT NULL, + `message` varchar(255) NOT NULL, + `funcid` int(11) NOT NULL DEFAULT 0, + KEY `ts_error_funcid_idx` (`funcid`,`error_time`), + KEY `ts_error_error_time_idx` (`error_time`), + KEY `ts_error_jobid_idx` (`jobid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_error` +-- + +LOCK TABLES `ts_error` WRITE; +/*!40000 ALTER TABLE `ts_error` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_error` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_exitstatus` +-- + +DROP TABLE IF EXISTS `ts_exitstatus`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_exitstatus` ( + `jobid` int(11) NOT NULL AUTO_INCREMENT, + `funcid` int(11) NOT NULL DEFAULT 0, + `status` smallint(6) DEFAULT NULL, + `completion_time` int(11) DEFAULT NULL, + `delete_after` int(11) DEFAULT NULL, + PRIMARY KEY (`jobid`), + KEY `ts_exitstatus_funcid_idx` (`funcid`), + KEY `ts_exitstatus_delete_after_idx` (`delete_after`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_exitstatus` +-- + +LOCK TABLES `ts_exitstatus` WRITE; +/*!40000 ALTER TABLE `ts_exitstatus` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_exitstatus` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_funcmap` +-- + +DROP TABLE IF EXISTS `ts_funcmap`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_funcmap` ( + `funcid` int(11) NOT NULL AUTO_INCREMENT, + `funcname` varchar(255) NOT NULL, + PRIMARY KEY (`funcid`), + UNIQUE KEY `ts_funcmap_funcname_idx` (`funcname`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_funcmap` +-- + +LOCK TABLES `ts_funcmap` WRITE; +/*!40000 ALTER TABLE `ts_funcmap` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_funcmap` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_job` +-- + +DROP TABLE IF EXISTS `ts_job`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_job` ( + `jobid` int(11) NOT NULL AUTO_INCREMENT, + `funcid` int(11) NOT NULL, + `arg` longblob DEFAULT NULL, + `uniqkey` varchar(255) DEFAULT NULL, + `insert_time` int(11) DEFAULT NULL, + `run_after` int(11) NOT NULL, + `grabbed_until` int(11) NOT NULL, + `priority` smallint(6) DEFAULT NULL, + `coalesce` varchar(255) DEFAULT NULL, + PRIMARY KEY (`jobid`), + UNIQUE KEY `ts_job_funcid_idx` (`funcid`,`uniqkey`), + KEY `ts_job_run_after_idx` (`run_after`,`funcid`), + KEY `ts_job_coalesce_idx` (`coalesce`,`funcid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_job` +-- + +LOCK TABLES `ts_job` WRITE; +/*!40000 ALTER TABLE `ts_job` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_job` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `ts_note` +-- + +DROP TABLE IF EXISTS `ts_note`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ts_note` ( + `jobid` int(11) NOT NULL, + `notekey` varchar(255) DEFAULT NULL, + `value` longblob DEFAULT NULL, + UNIQUE KEY `ts_note_jobid_idx` (`jobid`,`notekey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `ts_note` +-- + +LOCK TABLES `ts_note` WRITE; +/*!40000 ALTER TABLE `ts_note` DISABLE KEYS */; +/*!40000 ALTER TABLE `ts_note` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `user_api_keys` +-- + +DROP TABLE IF EXISTS `user_api_keys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `user_api_keys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` mediumint(9) NOT NULL, + `api_key` varchar(40) NOT NULL, + `description` varchar(255) DEFAULT NULL, + `revoked` tinyint(4) NOT NULL DEFAULT 0, + `last_used` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `user_api_keys_api_key_idx` (`api_key`), + KEY `user_api_keys_user_id_idx` (`user_id`), + CONSTRAINT `fk_user_api_keys_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `user_api_keys` +-- + +LOCK TABLES `user_api_keys` WRITE; +/*!40000 ALTER TABLE `user_api_keys` DISABLE KEYS */; +INSERT INTO `user_api_keys` VALUES (1,1,'AxBntHGSL97CmoTahkey8RNyo2K65NEfJBuk5ATe','',0,NULL); +/*!40000 ALTER TABLE `user_api_keys` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `user_group_map` +-- + +DROP TABLE IF EXISTS `user_group_map`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `user_group_map` ( + `user_id` mediumint(9) NOT NULL, + `group_id` mediumint(9) NOT NULL, + `isbless` tinyint(4) NOT NULL DEFAULT 0, + `grant_type` tinyint(4) NOT NULL DEFAULT 0, + UNIQUE KEY `user_group_map_user_id_idx` (`user_id`,`group_id`,`grant_type`,`isbless`), + KEY `fk_user_group_map_group_id_groups_id` (`group_id`), + CONSTRAINT `fk_user_group_map_group_id_groups_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_user_group_map_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `user_group_map` +-- + +LOCK TABLES `user_group_map` WRITE; +/*!40000 ALTER TABLE `user_group_map` DISABLE KEYS */; +INSERT INTO `user_group_map` VALUES (1,1,0,0),(1,1,1,0),(1,3,0,0),(1,8,0,2); +/*!40000 ALTER TABLE `user_group_map` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `versions` +-- + +DROP TABLE IF EXISTS `versions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `versions` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `value` varchar(64) NOT NULL, + `product_id` smallint(6) NOT NULL, + `isactive` tinyint(4) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `versions_product_id_idx` (`product_id`,`value`), + CONSTRAINT `fk_versions_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `versions` +-- + +LOCK TABLES `versions` WRITE; +/*!40000 ALTER TABLE `versions` DISABLE KEYS */; +INSERT INTO `versions` VALUES (1,'unspecified',1,1),(2,'unspecified',2,1),(3,'9.0',2,1),(4,'9.1',2,1),(5,'unspecified',3,1); +/*!40000 ALTER TABLE `versions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `watch` +-- + +DROP TABLE IF EXISTS `watch`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `watch` ( + `watcher` mediumint(9) NOT NULL, + `watched` mediumint(9) NOT NULL, + UNIQUE KEY `watch_watcher_idx` (`watcher`,`watched`), + KEY `watch_watched_idx` (`watched`), + CONSTRAINT `fk_watch_watched_profiles_userid` FOREIGN KEY (`watched`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_watch_watcher_profiles_userid` FOREIGN KEY (`watcher`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `watch` +-- + +LOCK TABLES `watch` WRITE; +/*!40000 ALTER TABLE `watch` DISABLE KEYS */; +/*!40000 ALTER TABLE `watch` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `whine_events` +-- + +DROP TABLE IF EXISTS `whine_events`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `whine_events` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `owner_userid` mediumint(9) NOT NULL, + `subject` varchar(128) DEFAULT NULL, + `body` mediumtext DEFAULT NULL, + `mailifnobugs` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `fk_whine_events_owner_userid_profiles_userid` (`owner_userid`), + CONSTRAINT `fk_whine_events_owner_userid_profiles_userid` FOREIGN KEY (`owner_userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `whine_events` +-- + +LOCK TABLES `whine_events` WRITE; +/*!40000 ALTER TABLE `whine_events` DISABLE KEYS */; +/*!40000 ALTER TABLE `whine_events` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `whine_queries` +-- + +DROP TABLE IF EXISTS `whine_queries`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `whine_queries` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `eventid` mediumint(9) NOT NULL, + `query_name` varchar(64) NOT NULL DEFAULT '', + `sortkey` smallint(6) NOT NULL DEFAULT 0, + `onemailperbug` tinyint(4) NOT NULL DEFAULT 0, + `title` varchar(128) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `whine_queries_eventid_idx` (`eventid`), + CONSTRAINT `fk_whine_queries_eventid_whine_events_id` FOREIGN KEY (`eventid`) REFERENCES `whine_events` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `whine_queries` +-- + +LOCK TABLES `whine_queries` WRITE; +/*!40000 ALTER TABLE `whine_queries` DISABLE KEYS */; +/*!40000 ALTER TABLE `whine_queries` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `whine_schedules` +-- + +DROP TABLE IF EXISTS `whine_schedules`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `whine_schedules` ( + `id` mediumint(9) NOT NULL AUTO_INCREMENT, + `eventid` mediumint(9) NOT NULL, + `run_day` varchar(32) DEFAULT NULL, + `run_time` varchar(32) DEFAULT NULL, + `run_next` datetime DEFAULT NULL, + `mailto` mediumint(9) NOT NULL, + `mailto_type` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `whine_schedules_run_next_idx` (`run_next`), + KEY `whine_schedules_eventid_idx` (`eventid`), + CONSTRAINT `fk_whine_schedules_eventid_whine_events_id` FOREIGN KEY (`eventid`) REFERENCES `whine_events` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `whine_schedules` +-- + +LOCK TABLES `whine_schedules` WRITE; +/*!40000 ALTER TABLE `whine_schedules` DISABLE KEYS */; +/*!40000 ALTER TABLE `whine_schedules` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2023-11-27 16:56:56 diff --git a/tests/services/bugzilla.conf b/tests/services/bugzilla.conf new file mode 100644 index 00000000..c0de1250 --- /dev/null +++ b/tests/services/bugzilla.conf @@ -0,0 +1,9 @@ + + DocumentRoot /var/www/webapps/bugzilla + + AddHandler cgi-script .cgi + Options +ExecCGI + DirectoryIndex index.cgi index.html + AllowOverride All + + diff --git a/tests/services/bugzillarc b/tests/services/bugzillarc new file mode 100644 index 00000000..7f6dafaa --- /dev/null +++ b/tests/services/bugzillarc @@ -0,0 +1,2 @@ +[localhost] +api_key = AxBntHGSL97CmoTahkey8RNyo2K65NEfJBuk5ATe diff --git a/tests/services/localconfig b/tests/services/localconfig new file mode 100644 index 00000000..f3bddb99 --- /dev/null +++ b/tests/services/localconfig @@ -0,0 +1,19 @@ +$create_htaccess = 1; +$webservergroup = 'www-data'; +$use_suexec = 0; +$db_driver = 'mysql'; +$db_host = 'mariadb'; +$db_name = 'bugs'; +$db_user = 'bugs'; +$db_pass = 'secret'; +$db_port = 3306; +$db_sock = ''; +$db_check = 1; +$db_mysql_ssl_ca_file = ''; +$db_mysql_ssl_ca_path = ''; +$db_mysql_ssl_client_cert = ''; +$db_mysql_ssl_client_key = ''; +$index_html = 0; +$interdiffbin = ''; +$diffpath = '/usr/bin'; +$site_wide_secret = 'oCIbi5WC04h86lW7L8fDcPCrVjb3JNeA2St94QlQtfjZrorjKmOdeVV0feHNDeFH'; diff --git a/tests/services/params.json b/tests/services/params.json new file mode 100644 index 00000000..a0ea93ce --- /dev/null +++ b/tests/services/params.json @@ -0,0 +1,104 @@ +{ + "LDAPBaseDN" : "", + "LDAPbinddn" : "", + "LDAPfilter" : "", + "LDAPmailattribute" : "mail", + "LDAPserver" : "", + "LDAPstarttls" : "0", + "LDAPuidattribute" : "uid", + "RADIUS_NAS_IP" : "", + "RADIUS_email_suffix" : "", + "RADIUS_secret" : "", + "RADIUS_server" : "", + "ajax_user_autocompletion" : "1", + "allow_attachment_deletion" : "0", + "allow_attachment_display" : "0", + "allowbugdeletion" : "0", + "allowemailchange" : "1", + "allowuserdeletion" : "0", + "announcehtml" : "", + "attachment_base" : "", + "auth_env_email" : "", + "auth_env_id" : "", + "auth_env_realname" : "", + "chartgroup" : "editbugs", + "collapsed_comment_tags" : "obsolete, spam", + "comment_taggers_group" : "editbugs", + "commentonchange_resolution" : "0", + "commentonduplicate" : "0", + "confirmuniqueusermatch" : "1", + "cookiedomain" : "", + "cookiepath" : "/", + "createemailregexp" : ".*", + "debug_group" : "admin", + "default_search_limit" : "500", + "defaultopsys" : "", + "defaultplatform" : "", + "defaultpriority" : "---", + "defaultquery" : "resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring", + "defaultseverity" : "enhancement", + "duplicate_or_move_bug_status" : "RESOLVED", + "emailregexp" : "^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$", + "emailregexpdesc" : "A legal address must contain exactly one '@', and at least one '.' after the @.", + "emailsuffix" : "", + "font_file" : "", + "globalwatchers" : "", + "inbound_proxies" : "", + "insidergroup" : "", + "last_visit_keep_days" : "10", + "letsubmitterchoosemilestone" : "1", + "letsubmitterchoosepriority" : "1", + "mail_delivery_method" : "None", + "mailfrom" : "bugzilla-daemon", + "maintainer" : "andreas@hasenkopf.xyz", + "makeproductgroups" : "0", + "max_search_results" : "10000", + "maxattachmentsize" : "1000", + "maxlocalattachment" : "0", + "maxusermatches" : "1000", + "memcached_namespace" : "bugzilla:", + "memcached_servers" : "", + "musthavemilestoneonaccept" : "0", + "mybugstemplate" : "buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%", + "noresolveonopenblockers" : "0", + "or_groups" : "1", + "password_check_on_login" : "1", + "password_complexity" : "no_constraints", + "proxy_url" : "", + "querysharegroup" : "editbugs", + "quip_list_entry_control" : "open", + "rememberlogin" : "on", + "requirelogin" : "0", + "search_allow_no_criteria" : "1", + "shadowdb" : "", + "shadowdbhost" : "", + "shadowdbport" : "3306", + "shadowdbsock" : "", + "shutdownhtml" : "", + "smtp_debug" : "0", + "smtp_password" : "", + "smtp_ssl" : "0", + "smtp_username" : "", + "smtpserver" : "localhost", + "ssl_redirect" : "0", + "sslbase" : "", + "strict_isolation" : "0", + "strict_transport_security" : "off", + "timetrackinggroup" : "editbugs", + "upgrade_notification" : "latest_stable_release", + "urlbase" : "", + "use_mailer_queue" : "0", + "use_see_also" : "1", + "useclassification" : "0", + "usemenuforusers" : "0", + "useqacontact" : "0", + "user_info_class" : "CGI", + "user_verify_class" : "DB", + "usestatuswhiteboard" : "0", + "usetargetmilestone" : "0", + "usevisibilitygroups" : "0", + "utf8" : "1", + "webdotbase" : "", + "webservice_email_filter" : "0", + "whinedays" : "7" +} diff --git a/tests/utils.py b/tests/utils.py index cfa1b424..058d81bf 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,6 +10,7 @@ import shlex import sys +from bugzilla import Bugzilla import bugzilla._cli import tests @@ -50,6 +51,10 @@ def open_functional_bz(bzclass, url, kwargs): return bz +def open_bz(url, bzclass=Bugzilla, **kwargs): + return open_functional_bz(bzclass=bzclass, url=url, kwargs=kwargs) + + def diff_compare(inputdata, filename, expect_out=None): """Compare passed string output to contents of filename""" def _process(data): From a4b7f6dbb57674b6c336c0494d184d5314a93bf8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:43:28 +0000 Subject: [PATCH 372/393] ci: bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 771d9b72..5a592570 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,7 +97,7 @@ jobs: mkdir -p ~/.config/python-bugzilla/ cp tests/services/bugzillarc ~/.config/python-bugzilla/ - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From c50c0d6bf7bc0217cc056de00839fc4513137396 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:43:24 +0000 Subject: [PATCH 373/393] ci: bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a592570..f8b8661d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: matrix: python-version: ["3.x"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install MariaDB utils run: sudo apt install --no-install-recommends -q -y mariadb-client - name: Restore DB dump From 430d96516b31455634491a6f1cb046a814ee3bae Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Thu, 13 Jun 2024 10:59:37 +0200 Subject: [PATCH 374/393] Prep for release 3.3.0 --- NEWS.md | 13 +++++++++++++ bugzilla/apiversion.py | 2 +- python-bugzilla.spec | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index e642fbf1..f3c91ab1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,18 @@ # python-bugzilla release news +## Release 3.3.0 (June, 2024) +- Expose error codes from the REST API (Stanislav Levin) +- Fixed broken link in documentation (Danilo C. L. de Paula) +- Set `Bug.weburl` that is compatible with the REST API +- Do not convert 'blocks' or 'depends' to int in `Bugzilla.build_update` (Adam Williamson) +- Use proper REST API route for getting a single bug +- Avoid duplicate entries when one id is 0 (Ricardo Branco) +- Removed unused argument from `Bugzilla.add_dict` +- Fixed API key leak (Ricardo Branco) +- Automatically include alias in include_fields in `Bugzilla._getbugs` +- Added method `Bugzilla.query_return_extra` +- cli: Support --field and --field-json for bugzilla attach + ## Release 3.2.0 (January 12, 2022) - Use soon-to-be-required Authorization header for RH bugzilla - Remove cookie auth support diff --git a/bugzilla/apiversion.py b/bugzilla/apiversion.py index 3a6d3e83..c4c11d14 100644 --- a/bugzilla/apiversion.py +++ b/bugzilla/apiversion.py @@ -4,5 +4,5 @@ # This work is licensed under the GNU GPLv2 or later. # See the COPYING file in the top-level directory. -version = "3.2.0" +version = "3.3.0" __version__ = version diff --git a/python-bugzilla.spec b/python-bugzilla.spec index fe4e459f..716fc933 100644 --- a/python-bugzilla.spec +++ b/python-bugzilla.spec @@ -1,5 +1,5 @@ Name: python-bugzilla -Version: 3.2.0 +Version: 3.3.0 Release: 1%{?dist} Summary: Python library for interacting with Bugzilla From 526e70ab692558c940fc9d1247c5b704acf9f911 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Thu, 19 Sep 2024 17:35:56 +0200 Subject: [PATCH 375/393] Fixed issue in `Bugzilla.fix_url` In a new patch version of Python 3.12 the behavior of `urllib.parse.urlunparse` changed. This change ensures that this method works correctly with the old and new behavior of `urllib.parse.urlunparse`. --- bugzilla/base.py | 3 +++ bugzilla/bug.py | 2 +- tests/test_api_misc.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index eef84aba..eb0f9244 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -155,6 +155,9 @@ def fix_url(url, force_rest=False): if force_rest: path = "rest/" + if not path.startswith("/"): + path = "/" + path + newurl = urllib.parse.urlunparse( (scheme, netloc, path, params, query, fragment)) return newurl diff --git a/bugzilla/bug.py b/bugzilla/bug.py index ec0e9c00..0a7c2d16 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -48,7 +48,7 @@ def _generate_weburl(self): """ parsed = urlparse(self.bugzilla.url) return urlunparse((parsed.scheme, parsed.netloc, - 'show_bug.cgi', '', 'id=%s' % self.bug_id, + '/show_bug.cgi', '', 'id=%s' % self.bug_id, '')) def __str__(self): diff --git a/tests/test_api_misc.py b/tests/test_api_misc.py index ea1f2e47..30889887 100644 --- a/tests/test_api_misc.py +++ b/tests/test_api_misc.py @@ -44,6 +44,7 @@ def test_fixurl(): "https://example.com/xmlrpc.cgi") assert (bugzilla.Bugzilla.fix_url("http://example.com/somepath.cgi") == "http://example.com/somepath.cgi") + assert bugzilla.Bugzilla.fix_url("http:///foo") == "http:///foo" def testPostTranslation(): From 5eedea31bcef0f1ba7a22eb38aba1cdd9b3d7981 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Thu, 19 Sep 2024 16:11:17 +0200 Subject: [PATCH 376/393] Use non-deprecated argument name in test-suite --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 938932b6..8c9c868d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,14 +42,14 @@ def pytest_addoption(parser): parser.addoption("--only-xmlrpc", action="store_true", default=False) -def pytest_ignore_collect(path, config): +def pytest_ignore_collect(collection_path, config): has_ro = config.getoption("--ro-functional") has_ro_i = config.getoption("--ro-integration") has_rw = config.getoption("--rw-functional") - base = os.path.basename(str(path)) + base = os.path.basename(str(collection_path)) is_ro = base == "test_ro_functional.py" - is_ro_i = "tests/integration/ro" in str(path) + is_ro_i = "tests/integration/ro" in str(collection_path) is_rw = base == "test_rw_functional.py" if is_ro_i and not has_ro_i: From 379bf17ec6738ec3a07c3add8659ddc7de220422 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Fri, 20 Sep 2024 12:06:00 +0200 Subject: [PATCH 377/393] Publish package on PyPI from CI --- .github/workflows/publish.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..8ca7db69 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +# This workflow will publish the package on PyPI +# For more information see: https://github.com/pypa/gh-action-pypi-publish + +name: Publish +on: + release: + types: [released] + +jobs: + publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/python-bugzilla + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install pypa/build + run: pip install build + - name: Build a source tarball + run: python -m build --sdist + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: false + verbose: false From 567cfd6ef568457629d2f08a62c5519a7473849a Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Mon, 23 Sep 2024 09:44:46 +0200 Subject: [PATCH 378/393] ci: Test against all supported Python versions --- .github/workflows/build.yml | 4 ++-- setup.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8b8661d..5df40255 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,8 +33,8 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - # python 3.6 is for rhel/centos8 compat - python-version: ["3.6", "3.x"] + # python 3.6 is for rhel/centos8/sles15 compat + python-version: ["3.6", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] steps: - uses: actions/checkout@v4 diff --git a/setup.py b/setup.py index 38ce974d..b9db594d 100755 --- a/setup.py +++ b/setup.py @@ -130,12 +130,12 @@ def _parse_requirements(fname): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], packages=['bugzilla'], data_files=[('share/man/man1', ['man/bugzilla.1'])], From 5f89e286041d367dbff93af31ab628a55c33dfc5 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Fri, 11 Oct 2024 17:10:58 +0200 Subject: [PATCH 379/393] test: Use a class to organize tests in test_backend_rest.py --- tests/test_backend_rest.py | 45 +++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/tests/test_backend_rest.py b/tests/test_backend_rest.py index fdfbd05b..45e3e780 100644 --- a/tests/test_backend_rest.py +++ b/tests/test_backend_rest.py @@ -4,32 +4,41 @@ from bugzilla._session import _BugzillaSession -def test_getbug(): - session = _BugzillaSession(url="http://example.com", +class TestGetBug: + @property + def session(self): + return _BugzillaSession(url="http://example.com", user_agent="py-bugzilla-test", sslverify=False, cert=None, tokencache={}, api_key="", is_redhat_bugzilla=False) - backend = _BackendREST(url="http://example.com", - bugzillasession=session) - def _assertion(self, *args): - self.assertion_called = True - assert args and args[0] == url + @property + def backend(self): + return _BackendREST(url="http://example.com", + bugzillasession=self.session) - setattr(backend, "_get", MethodType(_assertion, backend)) + def test_getbug__not_permissive(self): + backend = self.backend - for _ids, aliases, url in ( - (1, None, "/bug/1"), - ([1], [], "/bug/1"), - (None, "CVE-1999-0001", "/bug/CVE-1999-0001"), - ([], ["CVE-1999-0001"], "/bug/CVE-1999-0001"), - (1, "CVE-1999-0001", "/bug"), - ): - backend.assertion_called = False + def _assertion(self, *args): + self.assertion_called = True + assert args and args[0] == url - backend.bug_get(_ids, aliases, {}) + setattr(backend, "_get", MethodType(_assertion, backend)) - assert backend.assertion_called is True + for _ids, aliases, url in ( + (1, None, "/bug/1"), + ([1], [], "/bug/1"), + (None, "CVE-1999-0001", "/bug/CVE-1999-0001"), + ([], ["CVE-1999-0001"], "/bug/CVE-1999-0001"), + (1, "CVE-1999-0001", "/bug"), + ([1, 2], None, "/bug") + ): + backend.assertion_called = False + + backend.bug_get(_ids, aliases, {}) + + assert backend.assertion_called is True From 7b8d18eaba2c1ae1ba4ab107f83081dac0bdc509 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Fri, 11 Oct 2024 17:11:28 +0200 Subject: [PATCH 380/393] fix: Emulate `permissive` on REST backend (closes #222) `_BackendREST.bug_get` cannot pass a `permissive` parameter to the server as the REST API does not honor such a parameter. With this change, permissiveness is handled inside the method: If `permissive` is false and a single ID or alias is requested, the "get" method is used and an exception gets raised in case of error. Otherwise, the "search" method is used, which may return an empty list, if the client is not authenticated or an ID or alias does not exist. --- bugzilla/_backendrest.py | 3 ++- tests/test_backend_rest.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/bugzilla/_backendrest.py b/bugzilla/_backendrest.py index 00b5563a..45bc4999 100644 --- a/bugzilla/_backendrest.py +++ b/bugzilla/_backendrest.py @@ -109,6 +109,7 @@ def bug_fields(self, paramdict): def bug_get(self, bug_ids, aliases, paramdict): bug_list = listify(bug_ids) alias_list = listify(aliases) + permissive = paramdict.pop("permissive", False) data = paramdict.copy() # FYI: The high-level API expects the backends to raise an exception @@ -116,7 +117,7 @@ def bug_get(self, bug_ids, aliases, paramdict): # API), but the REST API simply returns an empty search result set. # To ensure compliant behavior, the REST backend needs to use the # explicit URL to get a single bug. - if len(bug_list or []) + len(alias_list or []) == 1: + if not permissive and len(bug_list or []) + len(alias_list or []) == 1: for id_list in (bug_list, alias_list): if id_list: return self._get("/bug/%s" % id_list[0], data) diff --git a/tests/test_backend_rest.py b/tests/test_backend_rest.py index 45e3e780..14836975 100644 --- a/tests/test_backend_rest.py +++ b/tests/test_backend_rest.py @@ -42,3 +42,26 @@ def _assertion(self, *args): backend.bug_get(_ids, aliases, {}) assert backend.assertion_called is True + + def test_getbug__permissive(self): + backend = self.backend + + def _assertion(self, *args): + self.assertion_called = True + assert args and args[0] == url and args[1] == params + + setattr(backend, "_get", MethodType(_assertion, backend)) + + for _ids, aliases, url, params in ( + (1, None, "/bug", {"id": [1], "alias": None}), + ([1], [], "/bug", {"id": [1], "alias": []}), + (None, "CVE-1999-0001", "/bug", {"alias": ["CVE-1999-0001"], "id": None}), + ([], ["CVE-1999-0001"], "/bug", {"alias": ["CVE-1999-0001"], "id": []}), + (1, "CVE-1999-0001", "/bug", {"id": [1], "alias": ["CVE-1999-0001"]}), + ([1, 2], None, "/bug", {"id": [1, 2], "alias": None}) + ): + backend.assertion_called = False + + backend.bug_get(_ids, aliases, {"permissive": True}) + + assert backend.assertion_called is True From 8f77896948a3331a478864ca2005028d2d4e5f96 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Tue, 15 Oct 2024 16:11:37 +0200 Subject: [PATCH 381/393] ci: More functional tests Migrated more functional RO tests to the new integration suite. Also, added a comment to the old tests to indicate which integration test corresponds to it. Updated the SQL fixture accordingly. And fixed a typo in the README. --- tests/integration/ro_api_test.py | 54 ++++++++++++++++++++++++++++-- tests/integration/ro_cli_test.py | 57 ++++++++++++++++++++++++++++++++ tests/services/README.md | 2 +- tests/services/bugs.sql | 57 ++++++++++++++++++-------------- tests/test_ro_functional.py | 20 +++++++++++ 5 files changed, 161 insertions(+), 29 deletions(-) diff --git a/tests/integration/ro_api_test.py b/tests/integration/ro_api_test.py index 3f096587..22ee5442 100644 --- a/tests/integration/ro_api_test.py +++ b/tests/integration/ro_api_test.py @@ -1,5 +1,7 @@ # Ignoring pytest-related warnings: # pylint: disable=redefined-outer-name,unused-argument +from xmlrpc.client import Fault + import pytest from bugzilla import BugzillaError @@ -85,7 +87,7 @@ def test_query(mocked_responses, backends): assert bugs[0].summary == "Expect the Spanish inquisition" bz = open_bz(url=TEST_URL, **backends) - query = bz.build_query(product="SUSE Linux Enterprise Server 15 SP6") + query = bz.build_query(product="SUSE Linux Enterprise Server 15 SP6", component="Containers") bugs = bz.query(query=query) assert len(bugs) == 1 @@ -94,8 +96,54 @@ def test_query(mocked_responses, backends): def test_get_bug_alias(mocked_responses, backends): + bug_id, alias = 1, "FOO-1" bz = open_bz(url=TEST_URL, **backends) - bug = bz.getbug("FOO-1") + bug = bz.getbug(alias) - assert bug.id == 1 + assert bug.id == bug_id + assert bug.bug_id == bug_id + assert bug.alias == [alias] assert bug.summary == "ZeroDivisionError in function foo_bar()" + + +def test_get_bug_alias_included_field(mocked_responses, backends): + bug_id, alias = 1, "FOO-1" + bz = open_bz(url=TEST_URL, **backends) + bug = bz.getbug(alias, include_fields=["id"]) + + assert bug.id == bug_id + assert bug.bug_id == bug_id + assert bug.alias == [alias] + assert not hasattr(bug, "summary") + + +def test_get_bug_404(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + try: + bz.getbug(666) + except Fault as error: # XMLRPC API + assert error.faultCode == 101 + except BugzillaError as error: # REST API + assert error.code == 101 + else: + raise AssertionError("No exception raised") + + +def test_get_bug_alias_404(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + try: + bz.getbug("CVE-1234-4321") + except Fault as error: # XMLRPC API + assert error.faultCode == 100 + except BugzillaError as error: # REST API + assert error.code == 100 + else: + raise AssertionError("No exception raised") + + +def test_get_bug_fields(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + fields = bz.getbugfields(names=["product"]) + assert fields == ["product"] + bz.getbugfields(names=["product", "bug_status"], force_refresh=True) + assert set(bz.bugfields) == {"product", "bug_status"} diff --git a/tests/integration/ro_cli_test.py b/tests/integration/ro_cli_test.py index f4d308d9..d5073a78 100644 --- a/tests/integration/ro_cli_test.py +++ b/tests/integration/ro_cli_test.py @@ -1,5 +1,8 @@ # Ignoring pytest-related warnings: # pylint: disable=unused-argument +import re +from urllib.parse import urljoin + from ..utils import open_bz from . import TEST_URL, TEST_PRODUCTS, TEST_SUSE_COMPONENTS, TEST_OWNER @@ -45,3 +48,57 @@ def test_query(mocked_responses, run_cli, backends): assert len(lines) == 1 assert lines[0].startswith("#2") assert "Expect the Spanish inquisition" in lines[0] + + +def test_query_full(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --full --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 5 + + for name in ('Component', 'CC', 'Blocked', 'Depends'): + assert name in out + + assert "Status Whiteboard" not in out + + +def test_query_raw(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --raw --bug_id 2", bzinstance=bz) + + assert "ATTRIBUTE[whiteboard]: lorem ipsum" in out + assert "ATTRIBUTE[id]: 2" in out + + +def test_query_oneline(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --oneline --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 1 + assert "python-bugzilla" in lines[0] + + +def test_query_extra(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --extra --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 5 + assert "Keywords: FooBar" in out + assert "Status Whiteboard: lorem ipsum" in out + + +def test_query_format(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --outputformat=\"id=%{bug_id} " + "sw=%{whiteboard:status} needinfo=%{flag:needinfo} " + "sum=%{summary}\" --bug_id 2", bzinstance=bz) + lines = out.strip().splitlines() + assert len(lines) == 1 + assert out.strip() == "id=2 sw=lorem ipsum needinfo=? sum=Expect the Spanish inquisition" + + +def test_query_url(mocked_responses, run_cli, backends): + url = urljoin(TEST_URL, "/buglist.cgi?version=9.1") + bz = open_bz(url=TEST_URL, **backends) + out = run_cli(f"bugzilla query --from-url \"{url}\"", bzinstance=bz) + assert re.search(r"#2\s+CONFIRMED", out) diff --git a/tests/services/README.md b/tests/services/README.md index 029241b8..c59174f3 100644 --- a/tests/services/README.md +++ b/tests/services/README.md @@ -47,7 +47,7 @@ Bugzilla container and edit the data in Bugzilla. Once done, one needs to dump t the file again: ```shell -$ mariadb-dump -u bugs -h 127.0.0.1 -P 3306 --password=secret bugs > bugs.qql +$ mariadb-dump -u bugs -h 127.0.0.1 -P 3306 --password=secret bugs > bugs.sql ``` ## Testing diff --git a/tests/services/bugs.sql b/tests/services/bugs.sql index c0ddf4ba..15c78d34 100644 --- a/tests/services/bugs.sql +++ b/tests/services/bugs.sql @@ -1,4 +1,5 @@ --- MariaDB dump 10.19 Distrib 10.6.12-MariaDB, for debian-linux-gnu (x86_64) +/*!999999\- enable the sandbox mode */ +-- MariaDB dump 10.19 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64) -- -- Host: 127.0.0.1 Database: bugs -- ------------------------------------------------------ @@ -104,7 +105,7 @@ CREATE TABLE `audit_log` ( LOCK TABLES `audit_log` WRITE; /*!40000 ALTER TABLE `audit_log` DISABLE KEYS */; -INSERT INTO `audit_log` VALUES (NULL,'Bugzilla::Field',1,'__create__',NULL,'bug_id','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',2,'__create__',NULL,'short_desc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',3,'__create__',NULL,'classification','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',4,'__create__',NULL,'product','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',5,'__create__',NULL,'version','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',6,'__create__',NULL,'rep_platform','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',7,'__create__',NULL,'bug_file_loc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',8,'__create__',NULL,'op_sys','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',9,'__create__',NULL,'bug_status','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',10,'__create__',NULL,'status_whiteboard','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',11,'__create__',NULL,'keywords','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',12,'__create__',NULL,'resolution','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',13,'__create__',NULL,'bug_severity','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',14,'__create__',NULL,'priority','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',15,'__create__',NULL,'component','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',16,'__create__',NULL,'assigned_to','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',17,'__create__',NULL,'reporter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',18,'__create__',NULL,'qa_contact','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',19,'__create__',NULL,'assigned_to_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',20,'__create__',NULL,'reporter_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',21,'__create__',NULL,'qa_contact_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',22,'__create__',NULL,'cc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',23,'__create__',NULL,'dependson','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',24,'__create__',NULL,'blocked','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',25,'__create__',NULL,'attachments.description','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',26,'__create__',NULL,'attachments.filename','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',27,'__create__',NULL,'attachments.mimetype','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',28,'__create__',NULL,'attachments.ispatch','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',29,'__create__',NULL,'attachments.isobsolete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',30,'__create__',NULL,'attachments.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',31,'__create__',NULL,'attachments.submitter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',32,'__create__',NULL,'target_milestone','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',33,'__create__',NULL,'creation_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',34,'__create__',NULL,'delta_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',35,'__create__',NULL,'longdesc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',36,'__create__',NULL,'longdescs.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',37,'__create__',NULL,'longdescs.count','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',38,'__create__',NULL,'alias','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',39,'__create__',NULL,'everconfirmed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',40,'__create__',NULL,'reporter_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',41,'__create__',NULL,'cclist_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',42,'__create__',NULL,'bug_group','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',43,'__create__',NULL,'estimated_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',44,'__create__',NULL,'remaining_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',45,'__create__',NULL,'deadline','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',46,'__create__',NULL,'commenter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',47,'__create__',NULL,'flagtypes.name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',48,'__create__',NULL,'requestees.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',49,'__create__',NULL,'setters.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',50,'__create__',NULL,'work_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',51,'__create__',NULL,'percentage_complete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',52,'__create__',NULL,'content','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',53,'__create__',NULL,'attach_data.thedata','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',54,'__create__',NULL,'owner_idle_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',55,'__create__',NULL,'see_also','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',56,'__create__',NULL,'tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',57,'__create__',NULL,'last_visit_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',58,'__create__',NULL,'comment_tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',59,'__create__',NULL,'days_elapsed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Classification',1,'__create__',NULL,'Unclassified','2023-09-20 13:12:35'),(NULL,'Bugzilla::Group',1,'__create__',NULL,'admin','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',2,'__create__',NULL,'tweakparams','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',3,'__create__',NULL,'editusers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',4,'__create__',NULL,'creategroups','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',5,'__create__',NULL,'editclassifications','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',6,'__create__',NULL,'editcomponents','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',7,'__create__',NULL,'editkeywords','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',8,'__create__',NULL,'editbugs','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',9,'__create__',NULL,'canconfirm','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',10,'__create__',NULL,'bz_canusewhineatothers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',11,'__create__',NULL,'bz_canusewhines','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',12,'__create__',NULL,'bz_sudoers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',13,'__create__',NULL,'bz_sudo_protect','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',14,'__create__',NULL,'bz_quip_moderators','2023-09-20 13:12:40'),(NULL,'Bugzilla::User',1,'__create__',NULL,'andreas@hasenkopf.xyz','2023-09-20 13:12:55'),(NULL,'Bugzilla::Product',1,'__create__',NULL,'TestProduct','2023-09-20 13:12:55'),(NULL,'Bugzilla::Version',1,'__create__',NULL,'unspecified','2023-09-20 13:12:55'),(NULL,'Bugzilla::Milestone',1,'__create__',NULL,'---','2023-09-20 13:12:55'),(NULL,'Bugzilla::Component',1,'__create__',NULL,'TestComponent','2023-09-20 13:12:55'),(1,'Bugzilla::Product',2,'__create__',NULL,'Red Hat Enterprise Linux 9','2023-11-27 12:25:54'),(1,'Bugzilla::Version',2,'__create__',NULL,'unspecified','2023-11-27 12:25:54'),(1,'Bugzilla::Milestone',2,'__create__',NULL,'---','2023-11-27 12:25:54'),(1,'Bugzilla::Component',2,'__create__',NULL,'python-bugzilla','2023-11-27 12:25:54'),(1,'Bugzilla::Version',3,'__create__',NULL,'9.0','2023-11-27 12:26:06'),(1,'Bugzilla::Version',4,'__create__',NULL,'9.1','2023-11-27 12:26:14'),(1,'Bugzilla::Product',3,'__create__',NULL,'SUSE Linux Enterprise Server 15 SP6','2023-11-27 12:29:18'),(1,'Bugzilla::Version',5,'__create__',NULL,'unspecified','2023-11-27 12:29:18'),(1,'Bugzilla::Milestone',3,'__create__',NULL,'---','2023-11-27 12:29:18'),(1,'Bugzilla::Component',3,'__create__',NULL,'Kernel','2023-11-27 12:29:18'),(1,'Bugzilla::Component',4,'__create__',NULL,'Containers','2023-11-27 12:29:46'); +INSERT INTO `audit_log` VALUES (NULL,'Bugzilla::Field',1,'__create__',NULL,'bug_id','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',2,'__create__',NULL,'short_desc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',3,'__create__',NULL,'classification','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',4,'__create__',NULL,'product','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',5,'__create__',NULL,'version','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',6,'__create__',NULL,'rep_platform','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',7,'__create__',NULL,'bug_file_loc','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',8,'__create__',NULL,'op_sys','2023-09-20 13:12:34'),(NULL,'Bugzilla::Field',9,'__create__',NULL,'bug_status','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',10,'__create__',NULL,'status_whiteboard','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',11,'__create__',NULL,'keywords','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',12,'__create__',NULL,'resolution','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',13,'__create__',NULL,'bug_severity','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',14,'__create__',NULL,'priority','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',15,'__create__',NULL,'component','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',16,'__create__',NULL,'assigned_to','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',17,'__create__',NULL,'reporter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',18,'__create__',NULL,'qa_contact','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',19,'__create__',NULL,'assigned_to_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',20,'__create__',NULL,'reporter_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',21,'__create__',NULL,'qa_contact_realname','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',22,'__create__',NULL,'cc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',23,'__create__',NULL,'dependson','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',24,'__create__',NULL,'blocked','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',25,'__create__',NULL,'attachments.description','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',26,'__create__',NULL,'attachments.filename','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',27,'__create__',NULL,'attachments.mimetype','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',28,'__create__',NULL,'attachments.ispatch','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',29,'__create__',NULL,'attachments.isobsolete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',30,'__create__',NULL,'attachments.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',31,'__create__',NULL,'attachments.submitter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',32,'__create__',NULL,'target_milestone','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',33,'__create__',NULL,'creation_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',34,'__create__',NULL,'delta_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',35,'__create__',NULL,'longdesc','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',36,'__create__',NULL,'longdescs.isprivate','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',37,'__create__',NULL,'longdescs.count','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',38,'__create__',NULL,'alias','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',39,'__create__',NULL,'everconfirmed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',40,'__create__',NULL,'reporter_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',41,'__create__',NULL,'cclist_accessible','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',42,'__create__',NULL,'bug_group','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',43,'__create__',NULL,'estimated_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',44,'__create__',NULL,'remaining_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',45,'__create__',NULL,'deadline','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',46,'__create__',NULL,'commenter','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',47,'__create__',NULL,'flagtypes.name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',48,'__create__',NULL,'requestees.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',49,'__create__',NULL,'setters.login_name','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',50,'__create__',NULL,'work_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',51,'__create__',NULL,'percentage_complete','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',52,'__create__',NULL,'content','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',53,'__create__',NULL,'attach_data.thedata','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',54,'__create__',NULL,'owner_idle_time','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',55,'__create__',NULL,'see_also','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',56,'__create__',NULL,'tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',57,'__create__',NULL,'last_visit_ts','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',58,'__create__',NULL,'comment_tag','2023-09-20 13:12:35'),(NULL,'Bugzilla::Field',59,'__create__',NULL,'days_elapsed','2023-09-20 13:12:35'),(NULL,'Bugzilla::Classification',1,'__create__',NULL,'Unclassified','2023-09-20 13:12:35'),(NULL,'Bugzilla::Group',1,'__create__',NULL,'admin','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',2,'__create__',NULL,'tweakparams','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',3,'__create__',NULL,'editusers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',4,'__create__',NULL,'creategroups','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',5,'__create__',NULL,'editclassifications','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',6,'__create__',NULL,'editcomponents','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',7,'__create__',NULL,'editkeywords','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',8,'__create__',NULL,'editbugs','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',9,'__create__',NULL,'canconfirm','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',10,'__create__',NULL,'bz_canusewhineatothers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',11,'__create__',NULL,'bz_canusewhines','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',12,'__create__',NULL,'bz_sudoers','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',13,'__create__',NULL,'bz_sudo_protect','2023-09-20 13:12:40'),(NULL,'Bugzilla::Group',14,'__create__',NULL,'bz_quip_moderators','2023-09-20 13:12:40'),(NULL,'Bugzilla::User',1,'__create__',NULL,'andreas@hasenkopf.xyz','2023-09-20 13:12:55'),(NULL,'Bugzilla::Product',1,'__create__',NULL,'TestProduct','2023-09-20 13:12:55'),(NULL,'Bugzilla::Version',1,'__create__',NULL,'unspecified','2023-09-20 13:12:55'),(NULL,'Bugzilla::Milestone',1,'__create__',NULL,'---','2023-09-20 13:12:55'),(NULL,'Bugzilla::Component',1,'__create__',NULL,'TestComponent','2023-09-20 13:12:55'),(1,'Bugzilla::Product',2,'__create__',NULL,'Red Hat Enterprise Linux 9','2023-11-27 12:25:54'),(1,'Bugzilla::Version',2,'__create__',NULL,'unspecified','2023-11-27 12:25:54'),(1,'Bugzilla::Milestone',2,'__create__',NULL,'---','2023-11-27 12:25:54'),(1,'Bugzilla::Component',2,'__create__',NULL,'python-bugzilla','2023-11-27 12:25:54'),(1,'Bugzilla::Version',3,'__create__',NULL,'9.0','2023-11-27 12:26:06'),(1,'Bugzilla::Version',4,'__create__',NULL,'9.1','2023-11-27 12:26:14'),(1,'Bugzilla::Product',3,'__create__',NULL,'SUSE Linux Enterprise Server 15 SP6','2023-11-27 12:29:18'),(1,'Bugzilla::Version',5,'__create__',NULL,'unspecified','2023-11-27 12:29:18'),(1,'Bugzilla::Milestone',3,'__create__',NULL,'---','2023-11-27 12:29:18'),(1,'Bugzilla::Component',3,'__create__',NULL,'Kernel','2023-11-27 12:29:18'),(1,'Bugzilla::Component',4,'__create__',NULL,'Containers','2023-11-27 12:29:46'),(1,'Bugzilla::Keyword',1,'__create__',NULL,'FooBar','2024-10-15 13:05:27'),(1,'Bugzilla::Keyword',2,'__create__',NULL,'LoremIpsum','2024-10-15 13:05:52'),(1,'Bugzilla::FlagType',1,'__create__',NULL,'needinfo','2024-10-15 13:26:28'),(1,'Bugzilla::User',2,'__create__',NULL,'nemo@example.com','2024-10-15 13:28:58'); /*!40000 ALTER TABLE `audit_log` ENABLE KEYS */; UNLOCK TABLES; @@ -266,7 +267,7 @@ CREATE TABLE `bug_user_last_visit` ( KEY `fk_bug_user_last_visit_bug_id_bugs_bug_id` (`bug_id`), CONSTRAINT `fk_bug_user_last_visit_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_bug_user_last_visit_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -275,7 +276,7 @@ CREATE TABLE `bug_user_last_visit` ( LOCK TABLES `bug_user_last_visit` WRITE; /*!40000 ALTER TABLE `bug_user_last_visit` DISABLE KEYS */; -INSERT INTO `bug_user_last_visit` VALUES (1,1,1,'2023-11-27 15:53:08'),(2,1,2,'2023-11-27 15:38:47'); +INSERT INTO `bug_user_last_visit` VALUES (1,1,1,'2024-10-15 14:00:54'),(2,1,2,'2024-10-15 14:00:49'),(3,1,3,'2024-10-15 13:45:42'); /*!40000 ALTER TABLE `bug_user_last_visit` ENABLE KEYS */; UNLOCK TABLES; @@ -333,7 +334,7 @@ CREATE TABLE `bugs` ( CONSTRAINT `fk_bugs_product_id_products_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON UPDATE CASCADE, CONSTRAINT `fk_bugs_qa_contact_profiles_userid` FOREIGN KEY (`qa_contact`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, CONSTRAINT `fk_bugs_reporter_profiles_userid` FOREIGN KEY (`reporter`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -342,7 +343,7 @@ CREATE TABLE `bugs` ( LOCK TABLES `bugs` WRITE; /*!40000 ALTER TABLE `bugs` DISABLE KEYS */; -INSERT INTO `bugs` VALUES (1,1,'','major','IN_PROGRESS','2023-11-27 15:35:33','2023-11-27 15:53:04','ZeroDivisionError in function foo_bar()','Linux','---',3,'PC',1,'unspecified',4,'','---',NULL,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','2023-11-27 15:53:04',1,1,1,0.00,0.00,NULL),(2,1,'','enhancement','CONFIRMED','2023-11-27 15:38:45','2023-11-27 15:38:45','Expect the Spanish inquisition','Linux','---',2,'PC',1,'9.1',2,'','---',NULL,'','2023-11-27 15:38:45',1,1,1,0.00,0.00,NULL); +INSERT INTO `bugs` VALUES (1,1,'','major','IN_PROGRESS','2023-11-27 15:35:33','2023-11-27 15:53:04','ZeroDivisionError in function foo_bar()','Linux','---',3,'PC',1,'unspecified',4,'','---',NULL,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','2023-11-27 15:53:04',1,1,1,0.00,0.00,NULL),(2,1,'','enhancement','CONFIRMED','2023-11-27 15:38:45','2024-10-15 13:29:13','Expect the Spanish inquisition','Linux','---',2,'PC',1,'9.1',2,'','---',NULL,'lorem ipsum','2024-10-15 13:29:13',1,1,1,0.00,0.00,NULL),(3,1,'','enhancement','CONFIRMED','2024-10-15 13:45:40','2024-10-15 13:45:40','Kernel Panic in the Discothek','Linux','---',3,'PC',1,'unspecified',3,'','---',NULL,'','2024-10-15 13:45:40',1,1,1,0.00,0.00,NULL); /*!40000 ALTER TABLE `bugs` ENABLE KEYS */; UNLOCK TABLES; @@ -377,7 +378,7 @@ CREATE TABLE `bugs_activity` ( CONSTRAINT `fk_bugs_activity_comment_id_longdescs_comment_id` FOREIGN KEY (`comment_id`) REFERENCES `longdescs` (`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_bugs_activity_fieldid_fielddefs_id` FOREIGN KEY (`fieldid`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, CONSTRAINT `fk_bugs_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -386,7 +387,7 @@ CREATE TABLE `bugs_activity` ( LOCK TABLES `bugs_activity` WRITE; /*!40000 ALTER TABLE `bugs_activity` DISABLE KEYS */; -INSERT INTO `bugs_activity` VALUES (1,1,NULL,1,'2023-11-27 15:45:09',9,'IN_PROGRESS','CONFIRMED',NULL),(2,1,NULL,1,'2023-11-27 15:47:58',10,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','',NULL),(3,1,NULL,1,'2023-11-27 15:53:04',38,'FOO-1','',NULL); +INSERT INTO `bugs_activity` VALUES (1,1,NULL,1,'2023-11-27 15:45:09',9,'IN_PROGRESS','CONFIRMED',NULL),(2,1,NULL,1,'2023-11-27 15:47:58',10,'AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:L','',NULL),(3,1,NULL,1,'2023-11-27 15:53:04',38,'FOO-1','',NULL),(4,2,NULL,1,'2024-10-15 13:08:14',10,'lorem ipsum','',NULL),(5,2,NULL,1,'2024-10-15 13:08:14',11,'FooBar','',NULL),(6,2,NULL,1,'2024-10-15 13:29:13',47,'needinfo?(nemo@example.com)','',NULL),(7,2,NULL,1,'2024-10-15 13:29:13',22,'nemo@example.com','',NULL); /*!40000 ALTER TABLE `bugs_activity` ENABLE KEYS */; UNLOCK TABLES; @@ -442,7 +443,7 @@ CREATE TABLE `bugs_fulltext` ( LOCK TABLES `bugs_fulltext` WRITE; /*!40000 ALTER TABLE `bugs_fulltext` DISABLE KEYS */; -INSERT INTO `bugs_fulltext` VALUES (1,'ZeroDivisionError in function foo_bar()','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.'),(2,'Expect the Spanish inquisition','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.'); +INSERT INTO `bugs_fulltext` VALUES (1,'ZeroDivisionError in function foo_bar()','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.','Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.'),(2,'Expect the Spanish inquisition','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.','Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.'),(3,'Kernel Panic in the Discothek','lorem ipsum dolor sit amet','lorem ipsum dolor sit amet'); /*!40000 ALTER TABLE `bugs_fulltext` ENABLE KEYS */; UNLOCK TABLES; @@ -518,6 +519,7 @@ CREATE TABLE `cc` ( LOCK TABLES `cc` WRITE; /*!40000 ALTER TABLE `cc` DISABLE KEYS */; +INSERT INTO `cc` VALUES (2,2); /*!40000 ALTER TABLE `cc` ENABLE KEYS */; UNLOCK TABLES; @@ -710,7 +712,7 @@ CREATE TABLE `email_setting` ( LOCK TABLES `email_setting` WRITE; /*!40000 ALTER TABLE `email_setting` DISABLE KEYS */; -INSERT INTO `email_setting` VALUES (1,0,0),(1,0,1),(1,0,2),(1,0,3),(1,0,4),(1,0,5),(1,0,6),(1,0,7),(1,0,9),(1,0,10),(1,0,11),(1,0,50),(1,1,0),(1,1,1),(1,1,2),(1,1,3),(1,1,4),(1,1,5),(1,1,6),(1,1,7),(1,1,9),(1,1,10),(1,1,11),(1,1,50),(1,2,0),(1,2,1),(1,2,2),(1,2,3),(1,2,4),(1,2,5),(1,2,6),(1,2,7),(1,2,8),(1,2,9),(1,2,10),(1,2,11),(1,2,50),(1,3,0),(1,3,1),(1,3,2),(1,3,3),(1,3,4),(1,3,5),(1,3,6),(1,3,7),(1,3,9),(1,3,10),(1,3,11),(1,3,50),(1,5,0),(1,5,1),(1,5,2),(1,5,3),(1,5,4),(1,5,5),(1,5,6),(1,5,7),(1,5,9),(1,5,10),(1,5,11),(1,5,50),(1,100,100),(1,100,101); +INSERT INTO `email_setting` VALUES (1,0,0),(1,0,1),(1,0,2),(1,0,3),(1,0,4),(1,0,5),(1,0,6),(1,0,7),(1,0,9),(1,0,10),(1,0,11),(1,0,50),(1,1,0),(1,1,1),(1,1,2),(1,1,3),(1,1,4),(1,1,5),(1,1,6),(1,1,7),(1,1,9),(1,1,10),(1,1,11),(1,1,50),(1,2,0),(1,2,1),(1,2,2),(1,2,3),(1,2,4),(1,2,5),(1,2,6),(1,2,7),(1,2,8),(1,2,9),(1,2,10),(1,2,11),(1,2,50),(1,3,0),(1,3,1),(1,3,2),(1,3,3),(1,3,4),(1,3,5),(1,3,6),(1,3,7),(1,3,9),(1,3,10),(1,3,11),(1,3,50),(1,5,0),(1,5,1),(1,5,2),(1,5,3),(1,5,4),(1,5,5),(1,5,6),(1,5,7),(1,5,9),(1,5,10),(1,5,11),(1,5,50),(1,100,100),(1,100,101),(2,0,0),(2,0,1),(2,0,2),(2,0,3),(2,0,4),(2,0,5),(2,0,6),(2,0,7),(2,0,9),(2,0,10),(2,0,11),(2,0,50),(2,1,0),(2,1,1),(2,1,2),(2,1,3),(2,1,4),(2,1,5),(2,1,6),(2,1,7),(2,1,9),(2,1,10),(2,1,11),(2,1,50),(2,2,0),(2,2,1),(2,2,2),(2,2,3),(2,2,4),(2,2,5),(2,2,6),(2,2,7),(2,2,8),(2,2,9),(2,2,10),(2,2,11),(2,2,50),(2,3,0),(2,3,1),(2,3,2),(2,3,3),(2,3,4),(2,3,5),(2,3,6),(2,3,7),(2,3,9),(2,3,10),(2,3,11),(2,3,50),(2,5,0),(2,5,1),(2,5,2),(2,5,3),(2,5,4),(2,5,5),(2,5,6),(2,5,7),(2,5,9),(2,5,10),(2,5,11),(2,5,50),(2,100,100),(2,100,101); /*!40000 ALTER TABLE `email_setting` ENABLE KEYS */; UNLOCK TABLES; @@ -838,6 +840,7 @@ CREATE TABLE `flaginclusions` ( LOCK TABLES `flaginclusions` WRITE; /*!40000 ALTER TABLE `flaginclusions` DISABLE KEYS */; +INSERT INTO `flaginclusions` VALUES (1,NULL,NULL); /*!40000 ALTER TABLE `flaginclusions` ENABLE KEYS */; UNLOCK TABLES; @@ -869,7 +872,7 @@ CREATE TABLE `flags` ( CONSTRAINT `fk_flags_requestee_id_profiles_userid` FOREIGN KEY (`requestee_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, CONSTRAINT `fk_flags_setter_id_profiles_userid` FOREIGN KEY (`setter_id`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE, CONSTRAINT `fk_flags_type_id_flagtypes_id` FOREIGN KEY (`type_id`) REFERENCES `flagtypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -878,6 +881,7 @@ CREATE TABLE `flags` ( LOCK TABLES `flags` WRITE; /*!40000 ALTER TABLE `flags` DISABLE KEYS */; +INSERT INTO `flags` VALUES (1,1,'?',2,NULL,'2024-10-15 13:29:13','2024-10-15 13:29:13',1,2); /*!40000 ALTER TABLE `flags` ENABLE KEYS */; UNLOCK TABLES; @@ -906,7 +910,7 @@ CREATE TABLE `flagtypes` ( KEY `fk_flagtypes_grant_group_id_groups_id` (`grant_group_id`), CONSTRAINT `fk_flagtypes_grant_group_id_groups_id` FOREIGN KEY (`grant_group_id`) REFERENCES `groups` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT `fk_flagtypes_request_group_id_groups_id` FOREIGN KEY (`request_group_id`) REFERENCES `groups` (`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -915,6 +919,7 @@ CREATE TABLE `flagtypes` ( LOCK TABLES `flagtypes` WRITE; /*!40000 ALTER TABLE `flagtypes` DISABLE KEYS */; +INSERT INTO `flagtypes` VALUES (1,'needinfo','Need more Info','','b',1,1,1,1,0,NULL,NULL); /*!40000 ALTER TABLE `flagtypes` ENABLE KEYS */; UNLOCK TABLES; @@ -1022,7 +1027,7 @@ CREATE TABLE `keyworddefs` ( `description` mediumtext NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `keyworddefs_name_idx` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1031,6 +1036,7 @@ CREATE TABLE `keyworddefs` ( LOCK TABLES `keyworddefs` WRITE; /*!40000 ALTER TABLE `keyworddefs` DISABLE KEYS */; +INSERT INTO `keyworddefs` VALUES (1,'FooBar','This needs no explanation'),(2,'LoremIpsum','dolor sit amet ...'); /*!40000 ALTER TABLE `keyworddefs` ENABLE KEYS */; UNLOCK TABLES; @@ -1057,6 +1063,7 @@ CREATE TABLE `keywords` ( LOCK TABLES `keywords` WRITE; /*!40000 ALTER TABLE `keywords` DISABLE KEYS */; +INSERT INTO `keywords` VALUES (2,1); /*!40000 ALTER TABLE `keywords` ENABLE KEYS */; UNLOCK TABLES; @@ -1110,7 +1117,7 @@ CREATE TABLE `logincookies` ( LOCK TABLES `logincookies` WRITE; /*!40000 ALTER TABLE `logincookies` DISABLE KEYS */; -INSERT INTO `logincookies` VALUES ('Ypt6rPqHjG',1,NULL,'2023-11-27 15:53:08'); +INSERT INTO `logincookies` VALUES ('StQdHXDOZ2',1,NULL,'2024-10-15 14:02:53'); /*!40000 ALTER TABLE `logincookies` ENABLE KEYS */; UNLOCK TABLES; @@ -1138,7 +1145,7 @@ CREATE TABLE `longdescs` ( KEY `longdescs_bug_when_idx` (`bug_when`), CONSTRAINT `fk_longdescs_bug_id_bugs_bug_id` FOREIGN KEY (`bug_id`) REFERENCES `bugs` (`bug_id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_longdescs_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1147,7 +1154,7 @@ CREATE TABLE `longdescs` ( LOCK TABLES `longdescs` WRITE; /*!40000 ALTER TABLE `longdescs` DISABLE KEYS */; -INSERT INTO `longdescs` VALUES (1,1,1,'2023-11-27 15:35:33',0.00,'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.',0,0,0,NULL),(2,1,1,'2023-11-27 15:37:05',0.00,'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.',0,0,0,NULL),(3,2,1,'2023-11-27 15:38:45',0.00,'Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.',0,0,0,NULL); +INSERT INTO `longdescs` VALUES (1,1,1,'2023-11-27 15:35:33',0.00,'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\nAt vero eos et accusam et justo duo dolores et ea rebum.',0,0,0,NULL),(2,1,1,'2023-11-27 15:37:05',0.00,'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.',0,0,0,NULL),(3,2,1,'2023-11-27 15:38:45',0.00,'Nobody expects the Spanish Inquisition! \n\nOur chief weapon is surprise, surprise and fear, fear and surprise. \n\nOur two weapons are fear and surprise, and ruthless efficiency. \n\nOur three weapons are fear and surprise and ruthless efficiency and an almost fanatical dedication to the pope.',0,0,0,NULL),(4,3,1,'2024-10-15 13:45:40',0.00,'lorem ipsum dolor sit amet',0,0,0,NULL); /*!40000 ALTER TABLE `longdescs` ENABLE KEYS */; UNLOCK TABLES; @@ -1474,7 +1481,7 @@ CREATE TABLE `profile_search` ( PRIMARY KEY (`id`), KEY `profile_search_user_id_idx` (`user_id`), CONSTRAINT `fk_profile_search_user_id_profiles_userid` FOREIGN KEY (`user_id`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1483,7 +1490,7 @@ CREATE TABLE `profile_search` ( LOCK TABLES `profile_search` WRITE; /*!40000 ALTER TABLE `profile_search` DISABLE KEYS */; -INSERT INTO `profile_search` VALUES (1,1,'1','bug_status,priority,assigned_to,bug_id'); +INSERT INTO `profile_search` VALUES (1,1,'1','bug_status,priority,assigned_to,bug_id'),(2,1,'1,2','priority,bug_severity'),(3,1,'2','bug_status,priority,assigned_to,bug_id'); /*!40000 ALTER TABLE `profile_search` ENABLE KEYS */; UNLOCK TABLES; @@ -1535,7 +1542,7 @@ CREATE TABLE `profiles` ( PRIMARY KEY (`userid`), UNIQUE KEY `profiles_login_name_idx` (`login_name`), UNIQUE KEY `profiles_extern_id_idx` (`extern_id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1544,7 +1551,7 @@ CREATE TABLE `profiles` ( LOCK TABLES `profiles` WRITE; /*!40000 ALTER TABLE `profiles` DISABLE KEYS */; -INSERT INTO `profiles` VALUES (1,'andreas@hasenkopf.xyz','2207pp7o,ialUTtf7x78ge5SbbN7+W+1lXGJBXmMlYt26C1egd4g{SHA-256}','Andreas','',0,1,NULL,1,'2023-11-27 00:00:00'); +INSERT INTO `profiles` VALUES (1,'andreas@hasenkopf.xyz','2207pp7o,ialUTtf7x78ge5SbbN7+W+1lXGJBXmMlYt26C1egd4g{SHA-256}','Andreas','',0,1,NULL,1,'2024-10-15 00:00:00'),(2,'nemo@example.com','rimPrF6O,Y0jPDDD1IeOR5myBbCCkt5rW36hOlVe7k/IH8wG513Y{SHA-256}','Nemo','',1,1,NULL,1,NULL); /*!40000 ALTER TABLE `profiles` ENABLE KEYS */; UNLOCK TABLES; @@ -1571,7 +1578,7 @@ CREATE TABLE `profiles_activity` ( CONSTRAINT `fk_profiles_activity_fieldid_fielddefs_id` FOREIGN KEY (`fieldid`) REFERENCES `fielddefs` (`id`) ON UPDATE CASCADE, CONSTRAINT `fk_profiles_activity_userid_profiles_userid` FOREIGN KEY (`userid`) REFERENCES `profiles` (`userid`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_profiles_activity_who_profiles_userid` FOREIGN KEY (`who`) REFERENCES `profiles` (`userid`) ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -1580,7 +1587,7 @@ CREATE TABLE `profiles_activity` ( LOCK TABLES `profiles_activity` WRITE; /*!40000 ALTER TABLE `profiles_activity` DISABLE KEYS */; -INSERT INTO `profiles_activity` VALUES (1,1,1,'2023-09-20 13:12:55',33,NULL,'2023-09-20 13:12:55'); +INSERT INTO `profiles_activity` VALUES (1,1,1,'2023-09-20 13:12:55',33,NULL,'2023-09-20 13:12:55'),(2,2,1,'2024-10-15 13:28:58',33,NULL,'2024-10-15 13:28:58'); /*!40000 ALTER TABLE `profiles_activity` ENABLE KEYS */; UNLOCK TABLES; @@ -1916,7 +1923,7 @@ CREATE TABLE `tokens` ( LOCK TABLES `tokens` WRITE; /*!40000 ALTER TABLE `tokens` DISABLE KEYS */; -INSERT INTO `tokens` VALUES (1,'2023-11-27 15:46:15','5HVJhRRo6t','session','edit_parameters'),(1,'2023-11-27 12:25:54','a9MgwT7N7x','session','edit_product'),(1,'2023-11-27 15:42:50','CRSwDhzaXc','session','edit_parameters'),(1,'2023-11-27 12:29:18','DXFuAIZ5GH','session','edit_product'),(1,'2023-09-20 13:13:14','ery9F3ZaAV','session','edit_user_prefs'),(1,'2023-11-27 15:44:26','gnPazrbni2','session','edit_product'),(1,'2023-11-27 15:43:10','GZT1mYgIAF','session','edit_settings'),(1,'2023-11-27 15:42:57','hYkjAGXNIj','session','add_field'),(1,'2023-11-27 15:46:35','ibDe8MPzGE','session','edit_parameters'),(1,'2023-09-20 13:13:14','oukIJJwYod','api_token',''),(1,'2023-11-27 12:26:29','PIjhZLJ29K','session','edit_product'),(1,'2023-11-27 12:23:39','pIrqNpsRDo','api_token',''),(1,'2023-11-27 15:44:36','rkyOtDBxr4','session','edit_group_controls'),(1,'2023-09-20 13:13:20','VLrgLovfH9','session','edit_user_prefs'),(1,'2023-11-27 15:45:59','xgQpxIS10M','session','edit_user_prefs'); +INSERT INTO `tokens` VALUES (1,'2023-11-27 15:46:15','5HVJhRRo6t','session','edit_parameters'),(1,'2024-10-15 13:06:14','5NG9DysR5W','session','edit_parameters'),(1,'2024-10-15 13:10:16','6m73C0nqfo','session','edit_parameters'),(1,'2024-10-15 13:10:09','7RlXVAQiOb','session','edit_parameters'),(1,'2023-11-27 12:25:54','a9MgwT7N7x','session','edit_product'),(1,'2024-10-15 13:27:02','bSVcXqgap4','session','edit_flagtype'),(1,'2024-10-15 13:06:09','BWsu8P8e2D','session','edit_parameters'),(1,'2023-11-27 15:42:50','CRSwDhzaXc','session','edit_parameters'),(1,'2024-10-15 14:02:08','dAVlRMDOg7','session','edit_component'),(1,'2023-11-27 12:29:18','DXFuAIZ5GH','session','edit_product'),(1,'2023-09-20 13:13:14','ery9F3ZaAV','session','edit_user_prefs'),(1,'2024-10-15 12:46:48','gEsxMu9BHz','api_token',''),(1,'2023-11-27 15:44:26','gnPazrbni2','session','edit_product'),(1,'2023-11-27 15:43:10','GZT1mYgIAF','session','edit_settings'),(1,'2023-11-27 15:42:57','hYkjAGXNIj','session','add_field'),(1,'2024-10-15 13:15:12','I9aiLWHFRJ','session','workflow_edit'),(1,'2023-11-27 15:46:35','ibDe8MPzGE','session','edit_parameters'),(1,'2024-10-15 14:00:21','ITqzn9Ed9n','session','edit_product'),(1,'2024-10-15 13:06:33','jK4PGdugR8','session','edit_parameters'),(1,'2024-10-15 14:02:05','JOhZj5gVqg','session','edit_product'),(1,'2023-09-20 13:13:14','oukIJJwYod','api_token',''),(1,'2023-11-27 12:26:29','PIjhZLJ29K','session','edit_product'),(1,'2023-11-27 12:23:39','pIrqNpsRDo','api_token',''),(1,'2024-10-15 13:28:58','qO1ZPdshDu','session','edit_user'),(1,'2023-11-27 15:44:36','rkyOtDBxr4','session','edit_group_controls'),(1,'2023-09-20 13:13:20','VLrgLovfH9','session','edit_user_prefs'),(1,'2024-10-15 13:10:07','w7KWafB5zu','session','edit_parameters'),(1,'2023-11-27 15:45:59','xgQpxIS10M','session','edit_user_prefs'),(1,'2024-10-15 14:02:53','YnDsGT0jbR','session','add_component'); /*!40000 ALTER TABLE `tokens` ENABLE KEYS */; UNLOCK TABLES; @@ -2112,7 +2119,7 @@ CREATE TABLE `user_group_map` ( LOCK TABLES `user_group_map` WRITE; /*!40000 ALTER TABLE `user_group_map` DISABLE KEYS */; -INSERT INTO `user_group_map` VALUES (1,1,0,0),(1,1,1,0),(1,3,0,0),(1,8,0,2); +INSERT INTO `user_group_map` VALUES (1,1,0,0),(1,1,1,0),(1,3,0,0),(1,8,0,2),(2,8,0,2); /*!40000 ALTER TABLE `user_group_map` ENABLE KEYS */; UNLOCK TABLES; @@ -2267,4 +2274,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2023-11-27 16:56:56 +-- Dump completed on 2024-10-15 16:07:04 diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index df7f9e5d..4a40b114 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -51,6 +51,7 @@ class MyBugzilla(bugzilla.Bugzilla): assert bz._is_redhat_bugzilla is True # pylint: disable=protected-access +# See also: tests/integration/ro_api_test.py::test_rest_xmlrpc_detection def test_rest_xmlrpc_detection(): # The default: use XMLRPC bz = _open_bz("bugzilla.redhat.com") @@ -71,6 +72,7 @@ def test_rest_xmlrpc_detection(): assert bz._proxy # pylint: disable=protected-access +# See also: tests/integration/ro_api_test.py::test_apikey_error_scraping def test_apikey_error_scraping(): # Ensure the API key does not leak into any requests exceptions fakekey = "FOOBARMYKEY" @@ -98,6 +100,7 @@ def test_apikey_error_scraping(): assert fakekey not in str(e.value) +# See also: tests/integration/ro_api_test.py::test_xmlrpc_bad_url def test_xmlrpc_bad_url(): with pytest.raises(bugzilla.BugzillaError) as e: _open_bz("https://example.com/#xmlrpc") @@ -142,6 +145,7 @@ def test_gentoo(backends): ################## +# See also: tests/integration/ro_cli_test.py::test_get_products def testInfoProducts(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -149,6 +153,7 @@ def testInfoProducts(run_cli, backends): _check(out, 123, "Virtualization Tools") +# See also: tests/integration/ro_cli_test.py::test_get_components def testInfoComps(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -156,6 +161,7 @@ def testInfoComps(run_cli, backends): _check(out, 8, "virtinst") +# See also: tests/integration/ro_cli_test.py::test_get_versions def testInfoVers(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -163,6 +169,7 @@ def testInfoVers(run_cli, backends): _check(out, 17, "rawhide") +# See also: tests/integration/ro_cli_test.py::test_get_component_owners def testInfoCompOwners(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -171,6 +178,7 @@ def testInfoCompOwners(run_cli, backends): _check(out, None, "libvirt: Libvirt Maintainers") +# See also: tests/integration/ro_cli_test.py::test_query def testQuery(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -191,6 +199,7 @@ def testQuery(run_cli, backends): l2 == expectbug]) +# See also: tests/integration/ro_cli_test.py::test_query_full def testQueryFull(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -199,6 +208,7 @@ def testQueryFull(run_cli, backends): _check(out, 60, "end-of-life (EOL)") +# See also: tests/integration/ro_cli_test.py::test_query_raw def testQueryRaw(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -207,6 +217,7 @@ def testQueryRaw(run_cli, backends): _check(out, 70, "ATTRIBUTE[whiteboard]: bzcl34nup") +# See also: tests/integration/ro_cli_test.py::test_query_oneline def testQueryOneline(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -223,6 +234,7 @@ def testQueryOneline(run_cli, backends): assert " CVE-2011-2527" in out +# See also: tests/integration/ro_cli_test.py::test_query_extra def testQueryExtra(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -232,6 +244,7 @@ def testQueryExtra(run_cli, backends): assert " +Status Whiteboard: bzcl34nup" in out +# See also: tests/integration/ro_cli_test.py::test_query_format def testQueryFormat(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -253,6 +266,7 @@ def testQueryFormat(run_cli, backends): assert "V34 — system" in out +# See also: tests/integration/ro_cli_test.py::test_query_url def testQueryURL(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -290,6 +304,7 @@ def testQueryExtrafieldPool(run_cli, backends): assert "current_sprint_id" in out2 +# See also: tests/integration/ro_api_test.py::test_get_component_detail def testComponentsDetails(backends): """ Fresh call to getcomponentsdetails should properly refresh @@ -299,6 +314,7 @@ def testComponentsDetails(backends): assert bool(bz.getcomponentsdetails("Red Hat Developer Toolset")) +# See also: tests/integration/ro_api_test.py::test_get_bug_alias def testGetBugAlias(backends): """ getbug() works if passed an alias @@ -309,6 +325,7 @@ def testGetBugAlias(backends): assert bug.bug_id == 720773 +# See also: tests/integration/ro_api_test.py::test_get_bug_404 def testGetBug404(backends): """ getbug() is expected to raise an error, if a bug ID or alias does not exist @@ -325,6 +342,7 @@ def testGetBug404(backends): raise AssertionError("No exception raised") +# See also: tests/integration/ro_api_test.py::test_get_bug_alias_404 def testGetBugAlias404(backends): """ getbug() is expected to raise an error, if a bug ID or alias does not exist @@ -341,6 +359,7 @@ def testGetBugAlias404(backends): raise AssertionError("No exception raised") +# See also: tests/integration/ro_api_test.py::test_get_bug_alias_included_field def testGetBugAliasIncludedField(backends): bz = _open_bz(REDHAT_URL, **backends) @@ -358,6 +377,7 @@ def testQuerySubComponent(run_cli, backends): assert "#1060931 " in out +# See also: tests/integration/ro_api_test.py::test_get_bug_fields def testBugFields(backends): bz = _open_bz(REDHAT_URL, **backends) From 2d3ba4e805acb0e5ddda7e081f5d9b7a72c86b1a Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Thu, 17 Oct 2024 09:57:31 +0200 Subject: [PATCH 382/393] ci: More functional tests Migrated all generic functional RO tests to the new integration suite. Also, added a comment to the old tests to indicate which integration test corresponds to it. --- tests/conftest.py | 2 +- tests/integration/ro_api_test.py | 71 ++++++++++++++++++++++++++++++++ tests/integration/ro_cli_test.py | 32 +++++++++++++- tests/services/params.json | 2 +- tests/test_ro_functional.py | 7 ++++ 5 files changed, 111 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8c9c868d..0740fab6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -141,7 +141,7 @@ def status_callback(request): test_url = os.getenv("BUGZILLA_URL") if test_url: - passthrough += (test_url, ) + passthrough += (test_url, test_url.replace("http://", "https://")) with responses.RequestsMock(passthru_prefixes=passthrough, assert_all_requests_are_fired=False) as mock: mock.add_callback( diff --git a/tests/integration/ro_api_test.py b/tests/integration/ro_api_test.py index 22ee5442..1c47146c 100644 --- a/tests/integration/ro_api_test.py +++ b/tests/integration/ro_api_test.py @@ -1,5 +1,6 @@ # Ignoring pytest-related warnings: # pylint: disable=redefined-outer-name,unused-argument +from urllib.parse import urljoin from xmlrpc.client import Fault import pytest @@ -62,6 +63,18 @@ def test_get_products(mocked_responses, backends): assert {v["name"] for v in rhel["versions"]} == {"9.0", "9.1", "unspecified"} +def test_get_product(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + product_ids = {product["id"] for product in bz.product_get(ptype="enterable", + include_fields=["id"])} + product_names = {product["name"] for product in bz.product_get(ptype="selectable", + include_fields=["name"])} + assert product_ids == {1, 2, 3} + assert product_names == {'Red Hat Enterprise Linux 9', 'SUSE Linux Enterprise Server 15 SP6', + 'TestProduct'} + + def test_get_components(mocked_responses, backends): bz = open_bz(url=TEST_URL, **backends) components = bz.getcomponents(product="SUSE Linux Enterprise Server 15 SP6") @@ -106,6 +119,16 @@ def test_get_bug_alias(mocked_responses, backends): assert bug.summary == "ZeroDivisionError in function foo_bar()" +def test_bug_url(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug_id = 2 + + # Ensure weburl is generated consistently whether + # we are using XMLRPC or REST + bug = bz.getbug(bug_id) + assert bug.weburl == urljoin(TEST_URL, f"/show_bug.cgi?id={bug_id}") + + def test_get_bug_alias_included_field(mocked_responses, backends): bug_id, alias = 1, "FOO-1" bz = open_bz(url=TEST_URL, **backends) @@ -117,6 +140,18 @@ def test_get_bug_alias_included_field(mocked_responses, backends): assert not hasattr(bug, "summary") +def test_get_bug_exclude_fields(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + # Check default extra_fields will pull in comments + bug = bz.getbug(2, exclude_fields=["product"]) + assert not hasattr(bug, "product") + + # Ensure that include_fields overrides default extra_fields + bug = bz.getbug(2) + assert hasattr(bug, "product") + + def test_get_bug_404(mocked_responses, backends): bz = open_bz(url=TEST_URL, **backends) try: @@ -147,3 +182,39 @@ def test_get_bug_fields(mocked_responses, backends): assert fields == ["product"] bz.getbugfields(names=["product", "bug_status"], force_refresh=True) assert set(bz.bugfields) == {"product", "bug_status"} + + +def test_query_autorefresh(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + bz.bug_autorefresh = True + bug = bz.query(bz.build_query(bug_id=1, include_fields=["summary"]))[0] + assert hasattr(bug, "component") + assert bool(bug.component) + + bz.bug_autorefresh = False + bug = bz.query(bz.build_query(bug_id=1, include_fields=["summary"]))[0] + assert not hasattr(bug, "component") + try: + assert bool(bug.component) + except Exception as e: + assert "adjust your include_fields" in str(e) + + +def test_login_stubs(mocked_responses, backends): + # Explicitly set configpaths to avoid interference with an API key set by another test + bz = open_bz(url=TEST_URL, configpaths="/dev/null", **backends) + bz_apikey = open_bz(url=TEST_URL, api_key="random-and-secure-api-key", **backends) + + # Failed login, verifies our backends are calling the correct API + with pytest.raises(BugzillaError) as e: + bz.login("foo", "bar") + assert "Login failed" in str(e) + + # Login is prohibited, when an API key is defined + with pytest.raises(ValueError) as e: + bz_apikey.login("foo", "bar") + assert "cannot login when using an API key" in str(e) + + # Works fine when not logged in + bz.logout() diff --git a/tests/integration/ro_cli_test.py b/tests/integration/ro_cli_test.py index d5073a78..9cd5b3dd 100644 --- a/tests/integration/ro_cli_test.py +++ b/tests/integration/ro_cli_test.py @@ -1,12 +1,34 @@ # Ignoring pytest-related warnings: # pylint: disable=unused-argument import re -from urllib.parse import urljoin +from urllib.parse import urljoin, urlparse, urlunparse from ..utils import open_bz from . import TEST_URL, TEST_PRODUCTS, TEST_SUSE_COMPONENTS, TEST_OWNER +def test_fails(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla query --field=IDONTEXIST=FOO", bzinstance=bz, expectfail=True) + assert "Server error:" in out + + out = run_cli("bugzilla --bugzilla https://example.com/xmlrpc.cgi query --field=IDONTEXIST=FOO", + bzinstance=None, expectfail=True) + assert "Connection lost/failed" in out + + parsed = urlparse(TEST_URL) + netloc = parsed.netloc + if not re.search(r":\d+$", netloc): + netloc += ":80" + + https_test_url = urlunparse(("https", netloc, parsed.path, parsed.params, parsed.query, + parsed.fragment)) + out = run_cli(f"bugzilla --bugzilla {https_test_url} query --bug_id 1234", + bzinstance=None, expectfail=True) + assert "trust the remote server" in out + assert "--nosslverify" in out + + def test_get_products(mocked_responses, run_cli, backends): bz = open_bz(url=TEST_URL, **backends) out = run_cli("bugzilla info --products", bzinstance=bz) @@ -24,6 +46,14 @@ def test_get_components(mocked_responses, run_cli, backends): assert comp in out +def test_get_active_components(mocked_responses, run_cli, backends): + bz = open_bz(url=TEST_URL, **backends) + out = run_cli("bugzilla info --components 'SUSE Linux Enterprise Server 15 SP6' " + "--active-components", bzinstance=bz) + assert "Containers" in out + assert "Kernel" in out + + def test_get_component_owners(mocked_responses, run_cli, backends): bz = open_bz(url=TEST_URL, **backends) out = run_cli("bugzilla info --component_owners 'SUSE Linux Enterprise Server 15 SP6'", diff --git a/tests/services/params.json b/tests/services/params.json index a0ea93ce..1b6f2d14 100644 --- a/tests/services/params.json +++ b/tests/services/params.json @@ -69,7 +69,7 @@ "quip_list_entry_control" : "open", "rememberlogin" : "on", "requirelogin" : "0", - "search_allow_no_criteria" : "1", + "search_allow_no_criteria" : "0", "shadowdb" : "", "shadowdbhost" : "", "shadowdbport" : "3306", diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 4a40b114..8af5c763 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -387,6 +387,7 @@ def testBugFields(backends): assert set(bz.bugfields) == set(["product", "bug_status"]) +# See also: tests/integration/ro_api_test.py::test_get_product def testProductGetMisc(backends): bz = _open_bz(REDHAT_URL, **backends) @@ -394,6 +395,7 @@ def testProductGetMisc(backends): assert bz.product_get(ptype="selectable", include_fields=["name"]) +# See also: tests/integration/ro_api_test.py::test_query_autorefresh def testBugAutoRefresh(backends): bz = _open_bz(REDHAT_URL, **backends) @@ -415,6 +417,7 @@ def testBugAutoRefresh(backends): assert "adjust your include_fields" in str(e) +# See also (in part): tests/integration/ro_api_test.py::test_get_bug_exclude_fields def testExtraFields(backends): bz = _open_bz(REDHAT_URL, **backends) @@ -438,6 +441,7 @@ def testExternalBugsOutput(run_cli, backends): assert "External bug: https://bugs.launchpad.net/bugs/1203576" in out +# See also: tests/integration/ro_cli_test.py::test_get_active_components def testActiveComps(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -449,6 +453,7 @@ def testActiveComps(run_cli, backends): assert "virtinst" not in out +# See also: tests/integration/ro_cli_test.py::test_fails def testFaults(run_cli, backends): bz = _open_bz(REDHAT_URL, **backends) @@ -469,6 +474,7 @@ def testFaults(run_cli, backends): assert "--nosslverify" in out +# See also: tests/integration/ro_api_test.py::test_login_stubs def test_login_stubs(backends): bz = _open_bz(REDHAT_URL, **backends) @@ -489,6 +495,7 @@ def test_redhat_version(backends): _test_version(bz, bzversion) +# See also: tests/integration/ro_api_test.py::test_bug_url def test_bug_misc(backends): bz = _open_bz(REDHAT_URL, **backends) From adf33b6239c363bd3760390934d401455c97b8aa Mon Sep 17 00:00:00 2001 From: Ali Bahrani Date: Wed, 30 Oct 2024 01:59:02 -0700 Subject: [PATCH 383/393] Rename method getcomments to get_comments for better readablity (#226) Using the api, I used to think the only way to retrieve the comments is to go through a Bugzilla instance. Later, I figured out the the Bug objects have the method `get_attachments` and I was amazed that there is no `get_comments`. Digging into the code I realized the method has been named `getcomments`. I have changed all the occurrences of `getcomments` method of the Bug class to `get_comment` --------- Co-authored-by: abahrani Co-authored-by: Andreas Hasenkopf --- bugzilla/bug.py | 4 ++-- examples/getbug.py | 4 ++-- examples/update.py | 4 ++-- tests/test_api_bug.py | 1 + tests/test_rw_functional.py | 5 ++++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 0a7c2d16..6f3ec43b 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -292,14 +292,14 @@ def addcomment(self, comment, private=False): return self.bugzilla.update_bugs(self.bug_id, vals) - def getcomments(self): + def get_comments(self): """ Returns an array of comment dictionaries for this bug """ comment_list = self.bugzilla.get_comments([self.bug_id]) return comment_list['bugs'][str(self.bug_id)]['comments'] - + getcomments = get_comments ##################### # Get/Set bug flags # ##################### diff --git a/examples/getbug.py b/examples/getbug.py index faf4c30f..f164c0a1 100644 --- a/examples/getbug.py +++ b/examples/getbug.py @@ -33,8 +33,8 @@ # comments must be fetched separately on stock bugzilla. this just returns # a raw dict with all the info. -comments = bug.getcomments() +comments = bug.get_comments() print("\nLast comment data:\n%s" % pprint.pformat(comments[-1])) -# getcomments is just a wrapper around bzapi.get_comments(), which can be +# get_comments is just a wrapper around bzapi.get_comments(), which can be # used for bulk comments fetching diff --git a/examples/update.py b/examples/update.py index cd76992e..86b1967c 100644 --- a/examples/update.py +++ b/examples/update.py @@ -38,7 +38,7 @@ # Now let's add a comment -comments = bug.getcomments() +comments = bug.get_comments() print("Bug originally has %d comments" % len(comments)) update = bzapi.build_update(comment="new example comment %s" % time.time()) @@ -46,7 +46,7 @@ # refresh() actually isn't required here because comments are fetched # on demand -comments = bug.getcomments() +comments = bug.get_comments() print("Bug now has %d comments. Last comment=%s" % (len(comments), comments[-1]["text"])) diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index 61572fc5..9d5c2564 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -200,6 +200,7 @@ def _get_fake_bug(apiname): # Stub API testing bug = fakebz.getbug(1165434) bug.get_history_raw() + bug.get_comments() bug.getcomments() # Some hackery to hit a few attachment code paths diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 3d49688e..b59d84cd 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -318,8 +318,11 @@ def test05ModifyStatus(run_cli, backends): assert bug.longdescs[-1]["text"] == comment assert bug.longdescs[-1]["is_private"] == 0 - # Confirm comments is same as getcomments + # Confirm comments is same as get_comments + assert bug.comments == bug.get_comments() + # This method will be removed in a future version assert bug.comments == bug.getcomments() + assert bug.get_comments() == bug.getcomments() # Reset state run_cli(cmd + "--status %s" % origstatus, bz) From ad14b85af03b728becf898ba5c2864506c43189b Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 4 Nov 2024 13:16:10 -0500 Subject: [PATCH 384/393] tests: ro-functional: Handle redhat disabling User.log{in,out} Signed-off-by: Cole Robinson --- tests/test_ro_functional.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/test_ro_functional.py b/tests/test_ro_functional.py index 8af5c763..6bb770da 100644 --- a/tests/test_ro_functional.py +++ b/tests/test_ro_functional.py @@ -478,13 +478,26 @@ def testFaults(run_cli, backends): def test_login_stubs(backends): bz = _open_bz(REDHAT_URL, **backends) - # Failed login, verifies our backends are calling the correct API + # In 2024 bugzilla.redhat.com disabled User.login and User.logout APIs + # for xmlrpc API + with pytest.raises(bugzilla.BugzillaError) as e: bz.login("foo", "bar") assert "Login failed" in str(e) - # Works fine when not logged in - bz.logout() + is_rest = bz.is_rest() + is_xmlrpc = bz.is_xmlrpc() + + msg = None + try: + bz.logout() + except Exception as error: + msg = str(error) + + if is_rest and msg: + raise AssertionError("didn't expect exception: %s" % msg) + if is_xmlrpc: + assert "'User.logout' was not found" in str(msg) def test_redhat_version(backends): From 98de1e242644903a0379f5ed0ae3ec3f138ee585 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Mon, 4 Nov 2024 13:20:29 -0500 Subject: [PATCH 385/393] tests: rw-functional: Handle new permission errors with target-milestone Signed-off-by: Cole Robinson --- tests/test_rw_functional.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index b59d84cd..200f7939 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -534,8 +534,10 @@ def test071ModifyMisc(run_cli, backends): assert targetbug.target_milestone == "rc" assert targetbug.target_release == ["6.10"] except RuntimeError as e: - if have_dev: - raise + # As of Nov 2024 this needs even extra permissions, probably + # due to RHEL products being locked down + # if have_dev: + # raise assert perm_error in str(e) try: From 35c4510314ee62cc4b7dfd50acfbaca0c8baa366 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Mon, 4 Nov 2024 15:48:20 +0100 Subject: [PATCH 386/393] CI: Added RW integration tests * Migrated some RW tests from test_rw_functional.py * Run RW tests in CI * Updated Bugzilla parameter to allow tagging of comments * Use TestProduct for RW tests --- .github/workflows/build.yml | 4 +- tests/conftest.py | 6 ++ tests/integration/ro_api_test.py | 5 ++ tests/integration/rw_api_test.py | 120 +++++++++++++++++++++++++++++++ tests/services/params.json | 2 +- tests/test_rw_functional.py | 8 +++ 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 tests/integration/rw_api_test.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5df40255..72dd3388 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} # Run functional tests - integrationRO: + integration: runs-on: ubuntu-latest services: mariadb: @@ -106,7 +106,7 @@ jobs: pip install pytest pytest-cov pip install -r requirements.txt -r test-requirements.txt - name: Test with pytest - run: pytest --ro-integration + run: pytest --ro-integration --rw-integration env: BUGZILLA_URL: http://localhost diff --git a/tests/conftest.py b/tests/conftest.py index 0740fab6..d398437d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,8 @@ def pytest_addoption(parser): parser.addoption("--ro-integration", action="store_true", default=False, help="Run readonly tests against local Bugzilla instance.") + parser.addoption("--rw-integration", action="store_true", default=False, + help="Run read-write tests against local Bugzilla instance.") parser.addoption("--ro-functional", action="store_true", default=False, help=("Run readonly functional tests against actual " "bugzilla instances. This will be very slow.")) @@ -46,14 +48,18 @@ def pytest_ignore_collect(collection_path, config): has_ro = config.getoption("--ro-functional") has_ro_i = config.getoption("--ro-integration") has_rw = config.getoption("--rw-functional") + has_rw_i = config.getoption("--rw-integration") base = os.path.basename(str(collection_path)) is_ro = base == "test_ro_functional.py" is_ro_i = "tests/integration/ro" in str(collection_path) is_rw = base == "test_rw_functional.py" + is_rw_i = "tests/integration/rw" in str(collection_path) if is_ro_i and not has_ro_i: return True + if is_rw_i and not has_rw_i: + return True if is_ro and not has_ro: return True diff --git a/tests/integration/ro_api_test.py b/tests/integration/ro_api_test.py index 1c47146c..3c45cc09 100644 --- a/tests/integration/ro_api_test.py +++ b/tests/integration/ro_api_test.py @@ -201,6 +201,11 @@ def test_query_autorefresh(mocked_responses, backends): assert "adjust your include_fields" in str(e) +def test_logged_in_no_creds(mocked_responses, backends): + bz = open_bz(url=TEST_URL, use_creds=False, **backends) + assert not bz.logged_in + + def test_login_stubs(mocked_responses, backends): # Explicitly set configpaths to avoid interference with an API key set by another test bz = open_bz(url=TEST_URL, configpaths="/dev/null", **backends) diff --git a/tests/integration/rw_api_test.py b/tests/integration/rw_api_test.py new file mode 100644 index 00000000..8fba7dfb --- /dev/null +++ b/tests/integration/rw_api_test.py @@ -0,0 +1,120 @@ +# pylint: disable=unused-argument +from uuid import uuid4 +from xmlrpc.client import Fault + +from pytest import raises +from pytest import mark + +from bugzilla import Bugzilla, BugzillaError +from bugzilla.bug import Bug + +from ..utils import open_bz +from . import TEST_URL + +# NOTE: The tests in this file assume that an API key is defined in the bugzillarc! + + +DEFAULT_PARAMS = {"product": "TestProduct", + "component": "TestComponent", + "version": "unspecified", + "summary": "A new bug", + "description": "Details on how to reproduce", + "cc": "nemo@example.com", + "op_sys": "Linux", + "platform": "PC"} + + +def _create_bug(bz: Bugzilla, **kwargs) -> Bug: + """ + Create a new bug with overwrite-able defaults + """ + params = DEFAULT_PARAMS.copy() + params.update(kwargs) + + return bz.createbug(**bz.build_createbug(**params)) + + +def test_create_bug(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + + assert isinstance(bug, Bug) + assert bug.id + + bug = bz.getbug(bug.id) + for field in ("product", "component", "version", "summary"): + assert getattr(bug, field) == DEFAULT_PARAMS[field] + + +def test_create_bug_anonymous(mocked_responses, backends): + bz = open_bz(url=TEST_URL, configpaths="/dev/null", **backends) + with raises((Fault, BugzillaError)): + _create_bug(bz) + + +def test_create_bug_alias(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + alias = uuid4().hex + bug = _create_bug(bz, alias=alias) + + bug = bz.getbug(bug.id) + assert alias in bug.alias + + with raises((Fault, BugzillaError)): + _create_bug(bz, alias=alias) + + +def test_update_bug(mocked_responses, backends): + email = "nemo@example.com" + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + params = bz.build_update(resolution="WONTFIX", status="RESOLVED", cc_remove=email) + bz.update_bugs(bug.id, params) + bug.refresh() + + assert bug.resolution == "WONTFIX" + assert bug.status == "RESOLVED" + assert bug.cc == [] + + params = bz.build_update(cc_add=email) + bz.update_bugs(bug.id, params) + bug.refresh() + + assert bug.cc == [email] + + +# Bugzilla instance has no CLOSED status +@mark.xfail +def test_close_bug(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + bug.close(resolution="WORKSFORME", comment="Bla bla", isprivate=True) + bug.refresh() + + assert bug.resolution == "WORKSFORME" + assert bug.status == "CLOSED" + + +def test_add_comment(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = bz.getbug(1) + + comment_count = len(bug.get_comments()) + bug.addcomment("Bla Bla bla", private=True) + bug.refresh() + + assert len(bug.get_comments()) == comment_count + 1 + + +def test_update_flags(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + bug = _create_bug(bz) + flag = {"requestee": "nemo@example.com", "name": "needinfo", "status": "?"} + params = bz.build_update(flags=[flag]) + bz.update_bugs([bug.id], params) + bug.refresh() + + assert len(bug.flags) == 1 + + for key, value in flag.items(): + assert bug.flags[0][key] == value diff --git a/tests/services/params.json b/tests/services/params.json index 1b6f2d14..9a6a9034 100644 --- a/tests/services/params.json +++ b/tests/services/params.json @@ -44,7 +44,7 @@ "font_file" : "", "globalwatchers" : "", "inbound_proxies" : "", - "insidergroup" : "", + "insidergroup" : "editbugs", "last_visit_keep_days" : "10", "letsubmitterchoosemilestone" : "1", "letsubmitterchoosepriority" : "1", diff --git a/tests/test_rw_functional.py b/tests/test_rw_functional.py index 200f7939..600fa7ed 100644 --- a/tests/test_rw_functional.py +++ b/tests/test_rw_functional.py @@ -191,6 +191,7 @@ def _make_subcomponent_bug(run_cli, bz): # test cases # ############## +# See also: tests/integration/rw_api_test.py::test_logged_in_no_creds def test0LoggedInNoCreds(backends): bz = _open_bz(**backends, use_creds=False) assert not bz.logged_in @@ -201,6 +202,9 @@ def test0ClassDetection(): assert bz.__class__ is bugzilla.RHBugzilla +# See also: tests/integration/rw_api_test.py::test_create_bug +# tests/integration/rw_api_test.py::test_create_bug_alias +# tests/integration/rw_api_test.py::test_update_bug def test04NewBugAllFields(run_cli, backends): """ Create a bug using all 'new' fields, check some values, close it @@ -303,6 +307,7 @@ def test05ModifyStatus(run_cli, backends): raise assert perm_error in str(e) + # See also: tests/integration/rw_api_test.py::test_close_bug # bz.close test fixed_in = str(datetime.datetime.today()) bug.close("ERRATA", fixedin=fixed_in) @@ -311,6 +316,7 @@ def test05ModifyStatus(run_cli, backends): assert bug.resolution == "ERRATA" assert bug.fixed_in == fixed_in + # See also: tests/integration/rw_api_test.py::test_add_comment # bz.addcomment test comment = ("yet another test comment %s" % datetime.datetime.today()) bug.addcomment(comment, private=False) @@ -330,6 +336,7 @@ def test05ModifyStatus(run_cli, backends): assert bug.status == origstatus +# See also: tests/integration/rw_api_test.py::test_update_bug def test06ModifyEmails(run_cli, backends): """ Modify cc, assignee, qa_contact for existing bug @@ -391,6 +398,7 @@ def test06ModifyEmails(run_cli, backends): assert perm_error in str(e) +# See also: tests/integration/rw_api_test.py::test_update_flags def test070ModifyMultiFlags(run_cli, backends): """ Modify flags and fixed_in for 2 bugs From a7e6f282b9e0472f846840c590ab6925711c1b16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:00:36 +0000 Subject: [PATCH 387/393] ci: bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72dd3388..c2a27f3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: pytest --cov --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} From 83eee1902ac5d7a838f5181793bf4359b132c1f2 Mon Sep 17 00:00:00 2001 From: George Redivo Date: Thu, 20 Feb 2025 14:41:42 -0300 Subject: [PATCH 388/393] Create Bug.setsummary() method Create a method in Bug class to set summary of the bug. --- bugzilla/bug.py | 16 ++++++++++++++++ tests/test_api_bug.py | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/bugzilla/bug.py b/bugzilla/bug.py index 6f3ec43b..15cea004 100644 --- a/bugzilla/bug.py +++ b/bugzilla/bug.py @@ -360,6 +360,22 @@ def updateflags(self, flags): self.bugzilla.build_update(flags=flaglist)) + ####################### + # Bug fields handling # + ####################### + + def setsummary(self, summary): + """ + Set the summary of bug to the given summary string + """ + # Create update object + vals = self.bugzilla.build_update(summary=summary) + + log.debug("setsummary: update=%s", vals) + + # Send update to bugzilla and return + return self.bugzilla.update_bugs(self.bug_id, vals) + ######################## # Experimental methods # ######################## diff --git a/tests/test_api_bug.py b/tests/test_api_bug.py index 9d5c2564..23448d6b 100644 --- a/tests/test_api_bug.py +++ b/tests/test_api_bug.py @@ -184,6 +184,10 @@ def _get_fake_bug(apiname): assert bug.get_flags("NOPE") is None assert bug.get_flag_status("NOPE") is None + # bug.setsummary test + bug = _get_fake_bug("setsummary") + bug.setsummary("My new summary") + # Minor get_history_raw wrapper fakebz = tests.mockbackend.make_bz(rhbz=True, bug_history_args="data/mockargs/test_bug_api_history.txt", From bad7092a495817eb4001023fd1cb532c5725271f Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Tue, 6 May 2025 08:56:46 +0200 Subject: [PATCH 389/393] Stop using Ubuntu 20.04 image (#232) The Ubuntu 20.04 image got retired and is no longer available. Tests for Python 3.6 are run in a Docker image instead. --- .github/workflows/build.yml | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2a27f3b..f0b450d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,15 +26,30 @@ jobs: run: | pylint --output-format colorized --rcfile .pylintrc \ bugzilla-cli setup.py bugzilla examples tests + test_3_6: + # python 3.6 is for rhel/centos8/sles15 compat + runs-on: ubuntu-latest + container: + image: python:3.6 + steps: + - uses: actions/checkout@v4 - build: - # We stick with 20.04 to get access to python 3.6 - # https://github.com/actions/setup-python/issues/544 - runs-on: ubuntu-20.04 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install -r requirements.txt -r test-requirements.txt + + - name: Test with pytest + run: | + pytest + + + test: + runs-on: ubuntu-latest strategy: matrix: - # python 3.6 is for rhel/centos8/sles15 compat - python-version: ["3.6", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 From 5b7e6e3ed462d82a5bfdec4fa2f57700c6210628 Mon Sep 17 00:00:00 2001 From: Andreas Hasenkopf Date: Mon, 5 May 2025 11:30:42 +0200 Subject: [PATCH 390/393] Support "resolution" in `Bug.build_query` This change allows to search and filter bugs by resolution. Closes #231. --- bugzilla/base.py | 4 +++- tests/integration/ro_api_test.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/bugzilla/base.py b/bugzilla/base.py index eb0f9244..ddda9137 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1205,7 +1205,8 @@ def build_query(self, tags=None, exclude_fields=None, extra_fields=None, - limit=None): + limit=None, + resolution=None): """ Build a query string from passed arguments. Will handle query parameter differences between various bugzilla versions. @@ -1239,6 +1240,7 @@ def build_query(self, "savedsearch": savedsearch, "sharer_id": savedsearch_sharer_id, "limit": limit, + "resolution": resolution, # RH extensions... don't add any more. See comment below "sub_components": listify(sub_component), diff --git a/tests/integration/ro_api_test.py b/tests/integration/ro_api_test.py index 3c45cc09..d3d6b526 100644 --- a/tests/integration/ro_api_test.py +++ b/tests/integration/ro_api_test.py @@ -223,3 +223,16 @@ def test_login_stubs(mocked_responses, backends): # Works fine when not logged in bz.logout() + + +def test_query_resolution(mocked_responses, backends): + bz = open_bz(url=TEST_URL, **backends) + + bugs = bz.query(bz.build_query(short_desc="ZeroDivisionError", resolution=None)) + assert len(bugs) == 1 + + bugs = bz.query(bz.build_query(short_desc="ZeroDivisionError", resolution="---")) + assert len(bugs) == 1 + + bugs = bz.query(bz.build_query(short_desc="ZeroDivisionError", resolution="DUPLICATE")) + assert len(bugs) == 0 From 78f9ada45640778639e353b8331fae550b075779 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:16:58 +0200 Subject: [PATCH 391/393] ci: bump actions/checkout from 4 to 5 (#235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
Release notes

Sourced from actions/checkout's releases.

v5.0.0

What's Changed

⚠️ Minimum Compatible Runner Version

v2.327.1
Release Notes

Make sure your runner is updated to this version or newer to use this release.

Full Changelog: https://github.com/actions/checkout/compare/v4...v5.0.0

v4.3.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4...v4.3.0

v4.2.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v4.2.1...v4.2.2

v4.2.1

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4.2.0...v4.2.1

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

V5.0.0

V4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

v4.1.6

v4.1.5

v4.1.4

v4.1.3

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 10 +++++----- .github/workflows/publish.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0b450d8..abeba41d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: matrix: python-version: ["3.x"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -32,7 +32,7 @@ jobs: container: image: python:3.6 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install dependencies run: | @@ -52,7 +52,7 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -102,7 +102,7 @@ jobs: matrix: python-version: ["3.x"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install MariaDB utils run: sudo apt install --no-install-recommends -q -y mariadb-client - name: Restore DB dump @@ -133,7 +133,7 @@ jobs: python-version: ["3.x"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8ca7db69..bfe244c6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,7 +16,7 @@ jobs: permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: From a6e67ab1fd3b19a1c40914de683b53d5a6914349 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:40:56 +0100 Subject: [PATCH 392/393] ci: bump actions/setup-python from 5 to 6 (#237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
Release notes

Sourced from actions/setup-python's releases.

v6.0.0

What's Changed

Breaking Changes

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Enhancements:

Bug fixes:

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v5...v6.0.0

v5.6.0

What's Changed

Full Changelog: https://github.com/actions/setup-python/compare/v5...v5.6.0

v5.5.0

What's Changed

Enhancements:

Bug fixes:

... (truncated)

Commits
  • e797f83 Upgrade to node 24 (#1164)
  • 3d1e2d2 Revert "Enhance cache-dependency-path handling to support files outside the w...
  • 65b0712 Clarify pythonLocation behavior for PyPy and GraalPy in environment variables...
  • 5b668cf Bump actions/checkout from 4 to 5 (#1181)
  • f62a0e2 Change missing cache directory error to warning (#1182)
  • 9322b3c Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in PackageIn...
  • fbeb884 Bump form-data to fix critical vulnerabilities #182 & #183 (#1163)
  • 03bb615 Bump idna from 2.9 to 3.7 in /tests/data (#843)
  • 36da51d Add version parsing from Pipfile (#1067)
  • 3c6f142 update documentation (#1156)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 8 ++++---- .github/workflows/publish.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index abeba41d..27075192 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -55,7 +55,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -112,7 +112,7 @@ jobs: mkdir -p ~/.config/python-bugzilla/ cp tests/services/bugzillarc ~/.config/python-bugzilla/ - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -136,7 +136,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bfe244c6..1a1f2a5c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" - name: Install pypa/build From ce9093ed8a7aa7a37509e66f56628a904c5ee8a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:41:44 +0100 Subject: [PATCH 393/393] ci: bump actions/checkout from 5 to 6 (#239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
Release notes

Sourced from actions/checkout's releases.

v6.0.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v5.0.0...v6.0.0

v6-beta

What's Changed

Updated persist-credentials to store the credentials under $RUNNER_TEMP instead of directly in the local git config.

This requires a minimum Actions Runner version of v2.329.0 to access the persisted credentials for Docker container action scenarios.

v5.0.1

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v5...v5.0.1

Changelog

Sourced from actions/checkout's changelog.

Changelog

V6.0.0

V5.0.1

V5.0.0

V4.3.1

V4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

v4.1.6

v4.1.5

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 10 +++++----- .github/workflows/publish.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27075192..d01e8bf9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: matrix: python-version: ["3.x"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: @@ -32,7 +32,7 @@ jobs: container: image: python:3.6 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install dependencies run: | @@ -52,7 +52,7 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 @@ -102,7 +102,7 @@ jobs: matrix: python-version: ["3.x"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install MariaDB utils run: sudo apt install --no-install-recommends -q -y mariadb-client - name: Restore DB dump @@ -133,7 +133,7 @@ jobs: python-version: ["3.x"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1a1f2a5c..3b537cb9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,7 +16,7 @@ jobs: permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: