Hello community, here is the log from the commit of package python-python-jenkins for openSUSE:Factory checked in at 2018-12-12 17:29:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-python-jenkins (Old) and /work/SRC/openSUSE:Factory/.python-python-jenkins.new.28833 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-python-jenkins" Wed Dec 12 17:29:26 2018 rev:7 rq:657235 version:1.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-python-jenkins/python-python-jenkins.changes 2018-09-07 15:39:48.290510336 +0200 +++ /work/SRC/openSUSE:Factory/.python-python-jenkins.new.28833/python-python-jenkins.changes 2018-12-12 17:29:27.714802628 +0100 @@ -1,0 +2,12 @@ +Tue Dec 11 15:07:10 UTC 2018 - Thomas Bechtold <tbecht...@suse.com> + +- update to 1.4.0: + * Update min tox version to 2.0 + * Request multiple folder levels at once in get\_all\_jobs + * Replace build\_jobs\_list\_responses with actual Jenkins responses + * Clean up job/folder path handling + * Test requested URLs in test\_getall + * Make jjb-tox-cross-jenkins-job-builder voting + * Allow adding extra HTTP headers to Jenkins requests + +------------------------------------------------------------------- Old: ---- python-jenkins-1.2.1.tar.gz New: ---- python-jenkins-1.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-python-jenkins.spec ++++++ --- /var/tmp/diff_new_pack.7C42JO/_old 2018-12-12 17:29:28.406801751 +0100 +++ /var/tmp/diff_new_pack.7C42JO/_new 2018-12-12 17:29:28.406801751 +0100 @@ -13,13 +13,13 @@ # license that conforms to the Open Source Definition (Version 1.9) # published by the Open Source Initiative. -# Please submit bugfixes or comments via http://bugs.opensuse.org/ +# Please submit bugfixes or comments via https://bugs.opensuse.org/ # %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-python-jenkins -Version: 1.2.1 +Version: 1.4.0 Release: 0 Summary: Python bindings for the remote Jenkins API License: BSD-3-Clause ++++++ python-jenkins-1.2.1.tar.gz -> python-jenkins-1.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/.zuul.yaml new/python-jenkins-1.4.0/.zuul.yaml --- old/python-jenkins-1.2.1/.zuul.yaml 2018-08-24 19:43:33.000000000 +0200 +++ new/python-jenkins-1.4.0/.zuul.yaml 2018-11-19 02:55:24.000000000 +0100 @@ -7,7 +7,6 @@ - openstack/python-jenkins - openstack-infra/jenkins-job-builder voting: true - failure-message: WARNING - project: templates: @@ -29,3 +28,4 @@ - openstack-tox-py27 - openstack-tox-py35 - openstack-tox-py36 + - jjb-tox-cross-jenkins-job-builder diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/AUTHORS new/python-jenkins-1.4.0/AUTHORS --- old/python-jenkins-1.2.1/AUTHORS 2018-08-24 19:45:47.000000000 +0200 +++ new/python-jenkins-1.4.0/AUTHORS 2018-11-19 02:56:08.000000000 +0100 @@ -1,5 +1,6 @@ Abhijeet Kasurde <akasu...@redhat.com> Adam Gandelman <ad...@canonical.com> +Aigars Mahinovs <aigar...@debian.org> Akshat Tandon <akshat.tan...@concur.com> Alexandre Conrad <alexan...@surveymonkey.com> Aliaksandr Buhayeu <abuha...@mirantis.com> @@ -19,6 +20,7 @@ Darragh Bailey <dbai...@hp.com> Darragh Bailey <dbai...@hpe.com> David Strauss <da...@davidstrauss.net> +Dennis Dmitriev <ddmitr...@mirantis.com> Dong Ma <winterma.d...@gmail.com> Eduardo Gonzalez <dabar...@gmail.com> Emilien Macchi <emil...@redhat.com> @@ -57,9 +59,9 @@ Thanh Ha <zxi...@linux.com> Tomas Janousek <tomas.janou...@gooddata.com> ZhangHongtao <zhanghongtao0...@126.com> -Zuul <z...@review.openstack.org> grahamlyons <gra...@grahamlyons.com> huang.zhiping <huang.zhip...@99cloud.net> +joelee <lj_2...@163.com> lvxianguo <lvxian...@inspur.com> melissaml <ma....@99cloud.net> mhuin <mh...@redhat.com> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/ChangeLog new/python-jenkins-1.4.0/ChangeLog --- old/python-jenkins-1.2.1/ChangeLog 2018-08-24 19:45:47.000000000 +0200 +++ new/python-jenkins-1.4.0/ChangeLog 2018-11-19 02:56:08.000000000 +0100 @@ -1,6 +1,21 @@ CHANGES ======= +1.4.0 +----- + +* Update min tox version to 2.0 + +1.3.0 +----- + +* Request multiple folder levels at once in get\_all\_jobs +* Replace build\_jobs\_list\_responses with actual Jenkins responses +* Clean up job/folder path handling +* Test requested URLs in test\_getall +* Make jjb-tox-cross-jenkins-job-builder voting +* Allow adding extra HTTP headers to Jenkins requests + 1.2.1 ----- @@ -12,12 +27,14 @@ * Avoid empty body failure on HEAD requests * Fix item being ignored in get\_info +* Add folder credential support * detect and respect http redirects 1.1.0 ----- * Fix run\_script method +* Check for 'Location' header in the response * Adopt use of pre-commit hooks * Adds support for executing Groovy scripts on jenkins nodes * Allow use of unicode job names diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/PKG-INFO new/python-jenkins-1.4.0/PKG-INFO --- old/python-jenkins-1.2.1/PKG-INFO 2018-08-24 19:45:47.000000000 +0200 +++ new/python-jenkins-1.4.0/PKG-INFO 2018-11-19 02:56:08.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: python-jenkins -Version: 1.2.1 +Version: 1.4.0 Summary: Python bindings for the remote Jenkins API Home-page: http://git.openstack.org/cgit/openstack/python-jenkins Author: Ken Conley diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/jenkins/__init__.py new/python-jenkins-1.4.0/jenkins/__init__.py --- old/python-jenkins-1.2.1/jenkins/__init__.py 2018-08-24 19:43:52.000000000 +0200 +++ new/python-jenkins-1.4.0/jenkins/__init__.py 2018-11-19 02:55:24.000000000 +0100 @@ -62,6 +62,7 @@ from six.moves.http_client import BadStatusLine from six.moves.urllib.error import URLError from six.moves.urllib.parse import quote, urlencode, urljoin, urlparse +import xml.etree.ElementTree as ET from jenkins import plugins @@ -96,7 +97,8 @@ PLUGIN_INFO = 'pluginManager/api/json?depth=%(depth)s' CRUMB_URL = 'crumbIssuer/api/json' WHOAMI_URL = 'me/api/json?depth=%(depth)s' -JOBS_QUERY = '?tree=jobs[url,color,name,jobs]' +JOBS_QUERY = '?tree=%s' +JOBS_QUERY_TREE = 'jobs[url,color,name,%s]' JOB_INFO = '%(folder_url)sjob/%(short_name)s/api/json?depth=%(depth)s' JOB_NAME = '%(folder_url)sjob/%(short_name)s/api/json?tree=name' ALL_BUILDS = '%(folder_url)sjob/%(short_name)s/api/json?tree=allBuilds[number,url]' @@ -141,6 +143,14 @@ DELETE_PROMOTION = '%(folder_url)sjob/%(short_name)s/promotion/process/%(name)s/doDelete' CREATE_PROMOTION = '%(folder_url)sjob/%(short_name)s/promotion/createProcess?name=%(name)s' CONFIG_PROMOTION = '%(folder_url)sjob/%(short_name)s/promotion/process/%(name)s/config.xml' +LIST_CREDENTIALS = '%(folder_url)sjob/%(short_name)s/credentials/store/folder/' \ + 'domain/%(domain_name)s/api/json?tree=credentials[id]' +CREATE_CREDENTIAL = '%(folder_url)sjob/%(short_name)s/credentials/store/folder/' \ + 'domain/%(domain_name)s/createCredentials' +CONFIG_CREDENTIAL = '%(folder_url)sjob/%(short_name)s/credentials/store/folder/' \ + 'domain/%(domain_name)s/credential/%(name)s/config.xml' +CREDENTIAL_INFO = '%(folder_url)sjob/%(short_name)s/credentials/store/folder/' \ + 'domain/%(domain_name)s/credential/%(name)s/api/json?depth=0' QUIET_DOWN = 'quietDown' # for testing only @@ -321,6 +331,14 @@ self.timeout = timeout self._session = WrappedSession() + extra_headers = os.environ.get("JENKINS_API_EXTRA_HEADERS", "") + if extra_headers: + logging.warning("JENKINS_API_EXTRA_HEADERS adds these HTTP headers: %s", extra_headers.split("\n")) + for token in extra_headers.split("\n"): + if ":" in token: + header, value = token.split(":", 1) + self._session.headers[header] = value.strip() + if os.getenv('PYTHONHTTPSVERIFY', '1') == '0': logging.debug('PYTHONHTTPSVERIFY=0 detected so we will ' 'disable requests library SSL verification to keep ' @@ -457,17 +475,21 @@ raise JenkinsException( "Could not parse JSON info for job[%s]" % name) - def get_job_info_regex(self, pattern, depth=0, folder_depth=0): + def get_job_info_regex(self, pattern, depth=0, folder_depth=0, + folder_depth_per_request=10): '''Get a list of jobs information that contain names which match the regex pattern. :param pattern: regex pattern, ``str`` :param depth: JSON depth, ``int`` :param folder_depth: folder level depth to search ``int`` + :param folder_depth_per_request: Number of levels to fetch at once, + ``int``. See :func:`get_all_jobs`. :returns: List of jobs info, ``list`` ''' result = [] - jobs = self.get_all_jobs(folder_depth) + jobs = self.get_all_jobs(folder_depth=folder_depth, + folder_depth_per_request=folder_depth_per_request) for job in jobs: if re.search(pattern, job['name']): result.append(self.get_job_info(job['name'], depth=depth)) @@ -513,6 +535,7 @@ headers = response.headers if (headers.get('content-length') is None and headers.get('transfer-encoding') is None and + headers.get('location') is None and (response.content is None or len(response.content) <= 0)): # response body should only exist if one of these is provided raise EmptyResponseException( @@ -924,7 +947,7 @@ return plugins_data - def get_jobs(self, folder_depth=0, view_name=None): + def get_jobs(self, folder_depth=0, folder_depth_per_request=10, view_name=None): """Get list of jobs. Each job is a dictionary with 'name', 'url', 'color' and 'fullname' @@ -937,6 +960,8 @@ :param folder_depth: Number of levels to search, ``int``. By default 0, which will limit search to toplevel. None disables the limit. + :param folder_depth_per_request: Number of levels to fetch at once, + ``int``. See :func:`get_all_jobs`. :param view_name: Name of a Jenkins view for which to retrieve jobs, ``str``. By default, the job list is not limited to a specific view. @@ -958,9 +983,10 @@ if view_name: return self._get_view_jobs(name=view_name) else: - return self.get_all_jobs(folder_depth=folder_depth) + return self.get_all_jobs(folder_depth=folder_depth, + folder_depth_per_request=folder_depth_per_request) - def get_all_jobs(self, folder_depth=None): + def get_all_jobs(self, folder_depth=None, folder_depth_per_request=10): """Get list of all jobs recursively to the given folder depth. Each job is a dictionary with 'name', 'url', 'color' and 'fullname' @@ -968,65 +994,57 @@ :param folder_depth: Number of levels to search, ``int``. By default None, which will search all levels. 0 limits to toplevel. + :param folder_depth_per_request: Number of levels to fetch at once, + ``int``. By default 10, which is usually enough to fetch all jobs + using a single request and still easily fits into an HTTP request. :returns: list of jobs, ``[ { str: str} ]`` .. note:: - On instances with many folders it may be more efficient to use the - run_script method to retrieve all jobs instead. + On instances with many folders it would not be efficient to fetch + each folder separately, hence `folder_depth_per_request` levels + are fetched at once using the ``tree`` query parameter:: + + ?tree=jobs[url,color,name,jobs[...,jobs[...,jobs[...,jobs]]]] - Example:: + If there are more folder levels than the query asks for, Jenkins + returns empty [#]_ objects at the deepest level:: - server.run_script(\"\"\" - import groovy.json.JsonBuilder; + {"name": "folder", "url": "...", "jobs": [{}, {}, ...]} - // get all projects excluding matrix configuration - // as they are simply part of a matrix project. - // there may be better ways to get just jobs - items = Jenkins.instance.getAllItems(AbstractProject); - items.removeAll { - it instanceof hudson.matrix.MatrixConfiguration - }; - - def json = new JsonBuilder() - def root = json { - jobs items.collect { - [ - name: it.name, - url: Jenkins.instance.getRootUrl() + it.getUrl(), - color: it.getIconColor().toString(), - fullname: it.getFullName() - ] - } - } - - // use json.toPrettyString() if viewing - println json.toString() - \"\"\") + This makes it possible to detect when additional requests are + needed. + .. [#] Actually recent Jenkins includes a ``_class`` field + everywhere, but it's missing the requested fields. """ - jobs_list = [] + jobs_query = 'jobs' + for _ in range(folder_depth_per_request): + jobs_query = JOBS_QUERY_TREE % jobs_query + jobs_query = JOBS_QUERY % jobs_query - jobs = [(0, "", self.get_info(query=JOBS_QUERY)['jobs'])] + jobs_list = [] + jobs = [(0, [], self.get_info(query=jobs_query)['jobs'])] for lvl, root, lvl_jobs in jobs: if not isinstance(lvl_jobs, list): lvl_jobs = [lvl_jobs] for job in lvl_jobs: + path = root + [job[u'name']] # insert fullname info if it doesn't exist to # allow callers to easily reference unambiguously if u'fullname' not in job: - job[u'fullname'] = '/'.join( - [p for p in root.split('/') - if p and p != 'job'] + - [job[u'name']]) + job[u'fullname'] = '/'.join(path) jobs_list.append(job) - if 'jobs' in job: # folder + if 'jobs' in job and isinstance(job['jobs'], list): # folder if folder_depth is None or lvl < folder_depth: - path = '/job/'.join((root, job[u'name'])) - jobs.append( - (lvl + 1, path, - self.get_info(path, - query=JOBS_QUERY)['jobs'])) + children = job['jobs'] + # once folder_depth_per_request is reached, Jenkins + # returns empty objects + if any('url' not in child for child in job['jobs']): + url_path = ''.join(['/job/' + p for p in path]) + children = self.get_info(url_path, + query=jobs_query)['jobs'] + jobs.append((lvl + 1, path, children)) return jobs_list def copy_job(self, from_name, to_name): @@ -1145,22 +1163,6 @@ '''Get the number of jobs on the Jenkins server :returns: Total number of jobs, ``int`` - - .. note:: - - On instances with many folders it may be more efficient to use the - run_script method to retrieve the total number of jobs instead. - - Example:: - - # get all projects excluding matrix configuration - # as they are simply part of a matrix project. - server.run_script( - "print(Hudson.instance.getAllItems(" - " hudson.model.AbstractProject).count{" - " !(it instanceof hudson.matrix.MatrixConfiguration)" - " })") - ''' return len(self.get_all_jobs()) @@ -1274,6 +1276,12 @@ ''' response = self.jenkins_request(requests.Request( 'POST', self.build_job_url(name, parameters, token))) + + if 'Location' not in response.headers: + raise EmptyResponseException( + "Header 'Location' not found in " + "response from server[%s]" % self.server) + location = response.headers['Location'] # location is a queue item, eg. "http://jenkins/queue/item/25/" if location.endswith('/'): @@ -1924,6 +1932,189 @@ 'GET', self._build_url(CONFIG_PROMOTION, locals())) return self.jenkins_open(request) + def _get_tag_text(self, name, xml): + '''Get text of tag from xml + + :param name: XML tag name, ``str`` + :param xml: XML configuration, ``str`` + :returns: Text of tag, ``str`` + :throws: :class:`JenkinsException` whenever tag does not exist + or has invalidated text + ''' + tag = ET.fromstring(xml).find(name) + try: + text = tag.text.strip() + if text: + return text + raise JenkinsException("tag[%s] is invalidated" % name) + except AttributeError: + raise JenkinsException("tag[%s] is invalidated" % name) + + def assert_folder(self, name, exception_message='job[%s] is not a folder'): + '''Raise an exception if job is not Cloudbees Folder + + :param name: Name of job, ``str`` + :param exception_message: Message to use for the exception. + :throws: :class:`JenkinsException` whenever the job is + not Cloudbees Folder + ''' + if not self.is_folder(name): + raise JenkinsException(exception_message % name) + + def is_folder(self, name): + '''Check whether a job is Cloudbees Folder + + :param name: Job name, ``str`` + :returns: ``True`` if job is folder, ``False`` otherwise + ''' + return 'com.cloudbees.hudson.plugins.folder.Folder' \ + == self.get_job_info(name)['_class'] + + def assert_credential_exists(self, name, folder_name, domain_name='_', + exception_message='credential[%s] does not ' + 'exist in the domain[%s] of [%s]'): + '''Raise an exception if credential does not exist in domain of folder + + :param name: Name of credential, ``str`` + :param folder_name: Folder name, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + :param exception_message: Message to use for the exception. + Formatted with ``name``, ``domain_name``, + and ``folder_name`` + :throws: :class:`JenkinsException` whenever the credentail + does not exist in domain of folder + ''' + if not self.credential_exists(name, folder_name, domain_name): + raise JenkinsException(exception_message + % (name, domain_name, folder_name)) + + def credential_exists(self, name, folder_name, domain_name='_'): + '''Check whether a credentail exists in domain of folder + + :param name: Name of credentail, ``str`` + :param folder_name: Folder name, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + :returns: ``True`` if credentail exists, ``False`` otherwise + ''' + try: + return self.get_credential_info(name, folder_name, + domain_name)['id'] == name + except JenkinsException: + return False + + def get_credential_info(self, name, folder_name, domain_name='_'): + '''Get credential information dictionary in domain of folder + + :param name: Name of credentail, ``str`` + :param folder_name: folder_name, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + :returns: Dictionary of credential info, ``dict`` + ''' + self.assert_folder(folder_name) + folder_url, short_name = self._get_job_folder(folder_name) + try: + response = self.jenkins_open(requests.Request( + 'GET', self._build_url(CREDENTIAL_INFO, locals()) + )) + if response: + return json.loads(response) + else: + raise JenkinsException('credential[%s] does not exist ' + 'in the domain[%s] of [%s]' + % (name, domain_name, folder_name)) + except (req_exc.HTTPError, NotFoundException): + raise JenkinsException('credential[%s] does not exist ' + 'in the domain[%s] of [%s]' + % (name, domain_name, folder_name)) + except ValueError: + raise JenkinsException( + 'Could not parse JSON info for credential[%s] ' + 'in the domain[%s] of [%s]' + % (name, domain_name, folder_name) + ) + + def get_credential_config(self, name, folder_name, domain_name='_'): + '''Get configuration of credential in domain of folder. + + :param name: Name of credentail, ``str`` + :param folder_name: Folder name, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + :returns: Credential configuration (XML format) + ''' + self.assert_folder(folder_name) + folder_url, short_name = self._get_job_folder(folder_name) + return self.jenkins_open(requests.Request( + 'GET', self._build_url(CONFIG_CREDENTIAL, locals()) + )) + + def create_credential(self, folder_name, config_xml, + domain_name='_'): + '''Create credentail in domain of folder + + :param folder_name: Folder name, ``str`` + :param config_xml: New XML configuration, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + ''' + folder_url, short_name = self._get_job_folder(folder_name) + name = self._get_tag_text('id', config_xml) + if self.credential_exists(name, folder_name, domain_name): + raise JenkinsException('credential[%s] already exists ' + 'in the domain[%s] of [%s]' + % (name, domain_name, folder_name)) + + self.jenkins_open(requests.Request( + 'POST', self._build_url(CREATE_CREDENTIAL, locals()), + data=config_xml.encode('utf-8'), + headers=DEFAULT_HEADERS + )) + self.assert_credential_exists(name, folder_name, domain_name, + 'create[%s] failed in the ' + 'domain[%s] of [%s]') + + def delete_credential(self, name, folder_name, domain_name='_'): + '''Delete credential from domain of folder + + :param name: Name of credentail, ``str`` + :param folder_name: Folder name, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + ''' + folder_url, short_name = self._get_job_folder(folder_name) + self.jenkins_open(requests.Request( + 'DELETE', self._build_url(CONFIG_CREDENTIAL, locals()) + )) + if self.credential_exists(name, folder_name, domain_name): + raise JenkinsException('delete credential[%s] from ' + 'domain[%s] of [%s] failed' + % (name, domain_name, folder_name)) + + def reconfig_credential(self, folder_name, config_xml, domain_name='_'): + '''Reconfig credential with new config in domain of folder + + :param folder_name: Folder name, ``str`` + :param config_xml: New XML configuration, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + ''' + folder_url, short_name = self._get_job_folder(folder_name) + name = self._get_tag_text('id', config_xml) + self.assert_credential_exists(name, folder_name, domain_name) + self.jenkins_open(requests.Request( + 'POST', self._build_url(CONFIG_CREDENTIAL, locals()) + )) + + def list_credentials(self, folder_name, domain_name='_'): + '''List credentials in domain of folder + + :param folder_name: Folder name, ``str`` + :param domain_name: Domain name, default is '_', ``str`` + :returns: Credentials list, ``list`` + ''' + self.assert_folder(folder_name) + folder_url, short_name = self._get_job_folder(folder_name) + response = self.jenkins_open(requests.Request( + 'GET', self._build_url(LIST_CREDENTIALS, locals()) + )) + return json.loads(response)['credentials'] + def quiet_down(self): '''Prepare Jenkins for shutdown. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/python_jenkins.egg-info/PKG-INFO new/python-jenkins-1.4.0/python_jenkins.egg-info/PKG-INFO --- old/python-jenkins-1.2.1/python_jenkins.egg-info/PKG-INFO 2018-08-24 19:45:47.000000000 +0200 +++ new/python-jenkins-1.4.0/python_jenkins.egg-info/PKG-INFO 2018-11-19 02:56:08.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: python-jenkins -Version: 1.2.1 +Version: 1.4.0 Summary: Python bindings for the remote Jenkins API Home-page: http://git.openstack.org/cgit/openstack/python-jenkins Author: Ken Conley diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/python_jenkins.egg-info/SOURCES.txt new/python-jenkins-1.4.0/python_jenkins.egg-info/SOURCES.txt --- old/python-jenkins-1.2.1/python_jenkins.egg-info/SOURCES.txt 2018-08-24 19:45:47.000000000 +0200 +++ new/python-jenkins-1.4.0/python_jenkins.egg-info/SOURCES.txt 2018-11-19 02:56:08.000000000 +0100 @@ -33,6 +33,7 @@ tests/base.py tests/helper.py tests/test_build.py +tests/test_credential.py tests/test_info.py tests/test_jenkins.py tests/test_jenkins_sockets.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/python_jenkins.egg-info/pbr.json new/python-jenkins-1.4.0/python_jenkins.egg-info/pbr.json --- old/python-jenkins-1.2.1/python_jenkins.egg-info/pbr.json 2018-08-24 19:45:47.000000000 +0200 +++ new/python-jenkins-1.4.0/python_jenkins.egg-info/pbr.json 2018-11-19 02:56:08.000000000 +0100 @@ -1 +1 @@ -{"git_version": "93515ae", "is_release": true} \ No newline at end of file +{"git_version": "7166f87", "is_release": true} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/base.py new/python-jenkins-1.4.0/tests/base.py --- old/python-jenkins-1.2.1/tests/base.py 2018-08-24 19:43:52.000000000 +0200 +++ new/python-jenkins-1.4.0/tests/base.py 2018-11-19 02:55:24.000000000 +0100 @@ -43,3 +43,9 @@ for req in requests: req[0][0].prepare() + + def got_request_urls(self, mock): + return [ + call[0][0].url.split('?')[0] + for call in mock.call_args_list + ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/helper.py new/python-jenkins-1.4.0/tests/helper.py --- old/python-jenkins-1.2.1/tests/helper.py 2018-08-24 19:43:33.000000000 +0200 +++ new/python-jenkins-1.4.0/tests/helper.py 2018-11-19 02:55:24.000000000 +0100 @@ -78,14 +78,15 @@ *args, **kwargs) -def build_response_mock(status_code, json_body=None, headers=None, **kwargs): +def build_response_mock(status_code, json_body=None, headers=None, + add_content_length=True, **kwargs): real_response = requests.Response() real_response.status_code = status_code text = None if json_body is not None: text = json.dumps(json_body) - if headers is not {}: + if add_content_length and headers is not {}: real_response.headers['content-length'] = len(text) if headers is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/jobs/base.py new/python-jenkins-1.4.0/tests/jobs/base.py --- old/python-jenkins-1.2.1/tests/jobs/base.py 2018-08-24 19:43:33.000000000 +0200 +++ new/python-jenkins-1.4.0/tests/jobs/base.py 2018-11-19 02:55:24.000000000 +0100 @@ -1,6 +1,3 @@ -import copy -import json - from tests.base import JenkinsTestBase @@ -16,54 +13,72 @@ class JenkinsGetJobsTestBase(JenkinsJobsTestBase): jobs_in_folder = [ - [ - {'name': 'my_job1'}, - {'name': 'my_folder1', 'jobs': None}, - {'name': 'my_job2'} - ], + {'jobs': [ + {'name': 'my_job1', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_folder1', 'url': 'http://...', 'jobs': [{}, {}]}, + {'name': 'my_job2', 'color': 'blue', 'url': 'http://...'}, + {'name': 'job', 'url': 'http://...', 'jobs': [{}]} + ]}, # my_folder1 jobs - [ - {'name': 'my_job3'}, - {'name': 'my_job4'} - ] + {'jobs': [ + {'name': 'my_job3', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_job4', 'color': 'blue', 'url': 'http://...'} + ]}, + # "job" folder jobs + {'jobs': [ + {'name': 'my_job', 'color': 'blue', 'url': 'http://...'} + ]} ] - jobs_in_multiple_folders = copy.deepcopy(jobs_in_folder) - jobs_in_multiple_folders[1].insert( - 0, {'name': 'my_folder2', 'jobs': None}) - jobs_in_multiple_folders.append( + jobs_in_multiple_folders = [ + {'jobs': [ + {'name': 'my_job1', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_folder1', 'url': 'http://...', 'jobs': [{}, {}, {}]}, + {'name': 'my_job2', 'color': 'blue', 'url': 'http://...'} + ]}, + # my_folder1 jobs + {'jobs': [ + {'name': 'my_folder2', 'url': 'http://...', 'jobs': [{}, {}]}, + {'name': 'my_job3', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_job4', 'color': 'blue', 'url': 'http://...'} + ]}, # my_folder1/my_folder2 jobs - [ - {'name': 'my_job1'}, - {'name': 'my_job2'} - ] - ) - - jobs_in_unsafe_name_folders = copy.deepcopy(jobs_in_folder) - jobs_in_unsafe_name_folders[1].insert( - 0, {'name': 'my spaced folder', 'jobs': None}) - jobs_in_unsafe_name_folders.append( - # my_folder1/my\ spaced\ folder jobs - [ - {'name': 'my job 5'} - ] - ) - - -def build_jobs_list_responses(jobs_list, server_url): - responses = [] - for jobs in jobs_list: - get_jobs_response = [] - for job in jobs: - job_json = { - u'url': u'%s/job/%s' % (server_url.rstrip('/'), job['name']), - u'name': job['name'], - u'color': u'blue' - } - if 'jobs' in job: - job_json[u'jobs'] = "null" - get_jobs_response.append(job_json) + {'jobs': [ + {'name': 'my_job1', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_job2', 'color': 'blue', 'url': 'http://...'} + ]} + ] - responses.append(json.dumps({u'jobs': get_jobs_response})) + jobs_in_unsafe_name_folders = [ + {'jobs': [ + {'name': 'my_job1', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_folder1', 'url': 'http://...', 'jobs': [{}, {}]}, + {'name': 'my_job2', 'color': 'blue', 'url': 'http://...'} + ]}, + # my_folder1 jobs + {'jobs': [ + {'name': 'my spaced folder', 'url': 'http://...', 'jobs': [{}]}, + {'name': 'my_job3', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_job4', 'color': 'blue', 'url': 'http://...'} + ]}, + # my_folder1/my\ spaced\ folder jobs + {'jobs': [ + {'name': 'my job 5', 'color': 'blue', 'url': 'http://...'} + ]} + ] - return responses + jobs_in_folder_deep_query = [ + {'jobs': [ + {'name': 'top_folder', 'url': 'http://...', 'jobs': [ + {'name': 'middle_folder', 'url': 'http://...', 'jobs': [ + {'name': 'bottom_folder', 'url': 'http://...', + 'jobs': [{}, {}]} + ]} + ]} + ]}, + # top_folder/middle_folder/bottom_folder jobs + {'jobs': [ + {'name': 'my_job1', 'color': 'blue', 'url': 'http://...'}, + {'name': 'my_job2', 'color': 'blue', 'url': 'http://...'} + ]} + ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/jobs/test_build.py new/python-jenkins-1.4.0/tests/jobs/test_build.py --- old/python-jenkins-1.2.1/tests/jobs/test_build.py 2018-08-24 19:43:33.000000000 +0200 +++ new/python-jenkins-1.4.0/tests/jobs/test_build.py 2018-11-19 02:55:24.000000000 +0100 @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from mock import patch +import jenkins from six.moves.urllib.parse import quote from tests.helper import build_response_mock from tests.jobs.base import JenkinsJobsTestBase @@ -20,6 +21,49 @@ self.assertEqual(queue_id, 25) @patch('jenkins.requests.Session.send', autospec=True) + def test_assert_no_location(self, session_send_mock): + session_send_mock.return_value = build_response_mock(302, {}) + + with self.assertRaises(jenkins.EmptyResponseException) as context_mgr: + self.j.build_job(u'Test Job') + + self.assertEqual( + str(context_mgr.exception), + "Header 'Location' not found in response from server[{0}]".format( + self.make_url(''))) + + @patch.object(jenkins.Jenkins, 'maybe_add_crumb') + @patch('jenkins.requests.Session.send', autospec=True) + def test_simple_no_content_lenght(self, session_send_mock, + maybe_add_crumb_mock): + maybe_add_crumb_mock.return_value = None + session_send_mock.return_value = build_response_mock( + 201, None, add_content_length=False, + headers={'Location': self.make_url('/queue/item/25/')}) + + queue_id = self.j.build_job(u'Test Job') + + self.assertEqual(session_send_mock.call_args[0][1].url, + self.make_url('job/Test%20Job/build')) + self.assertEqual(queue_id, 25) + + @patch.object(jenkins.Jenkins, 'maybe_add_crumb') + @patch('jenkins.requests.Session.send', autospec=True) + def test_assert_no_content_lenght_no_location(self, session_send_mock, + maybe_add_crumb_mock): + maybe_add_crumb_mock.return_value = None + session_send_mock.return_value = build_response_mock( + 201, None, add_content_length=False) + + with self.assertRaises(jenkins.EmptyResponseException) as context_mgr: + self.j.build_job(u'Test Job') + + self.assertEqual( + str(context_mgr.exception), + 'Error communicating with server[{0}]: empty response'.format( + self.make_url(''))) + + @patch('jenkins.requests.Session.send', autospec=True) def test_in_folder(self, session_send_mock): session_send_mock.return_value = build_response_mock( 302, {}, headers={'Location': self.make_url('/queue/item/25/')}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/jobs/test_get.py new/python-jenkins-1.4.0/tests/jobs/test_get.py --- old/python-jenkins-1.2.1/tests/jobs/test_get.py 2018-08-24 19:43:33.000000000 +0200 +++ new/python-jenkins-1.4.0/tests/jobs/test_get.py 2018-11-19 02:55:24.000000000 +0100 @@ -3,7 +3,6 @@ import jenkins from tests.helper import build_response_mock -from tests.jobs.base import build_jobs_list_responses from tests.jobs.base import JenkinsGetJobsTestBase @@ -19,7 +18,7 @@ job_info_to_return = {u'jobs': jobs} jenkins_mock.return_value = json.dumps(job_info_to_return) - job_info = self.j.get_jobs() + job_info = self.j.get_jobs(folder_depth_per_request=1) jobs[u'fullname'] = jobs[u'name'] self.assertEqual(job_info, [jobs]) @@ -30,14 +29,12 @@ @patch.object(jenkins.Jenkins, 'jenkins_open') def test_folders_simple(self, jenkins_mock): - response = build_jobs_list_responses( - self.jobs_in_folder, self.make_url('')) - jenkins_mock.side_effect = iter(response) + jenkins_mock.side_effect = map(json.dumps, self.jobs_in_folder) jobs_info = self.j.get_jobs() expected_fullnames = [ - u"my_job1", u"my_folder1", u"my_job2" + u"my_job1", u"my_folder1", u"my_job2", u"job" ] self.assertEqual(len(expected_fullnames), len(jobs_info)) got_fullnames = [job[u"fullname"] for job in jobs_info] @@ -45,15 +42,13 @@ @patch.object(jenkins.Jenkins, 'jenkins_open') def test_folders_additional_level(self, jenkins_mock): - response = build_jobs_list_responses( - self.jobs_in_folder, self.make_url('')) - jenkins_mock.side_effect = iter(response) + jenkins_mock.side_effect = map(json.dumps, self.jobs_in_folder) jobs_info = self.j.get_jobs(folder_depth=1) expected_fullnames = [ - u"my_job1", u"my_folder1", u"my_job2", - u"my_folder1/my_job3", u"my_folder1/my_job4" + u"my_job1", u"my_folder1", u"my_job2", u"job", + u"my_folder1/my_job3", u"my_folder1/my_job4", u"job/my_job" ] self.assertEqual(len(expected_fullnames), len(jobs_info)) got_fullnames = [job[u"fullname"] for job in jobs_info] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/jobs/test_getall.py new/python-jenkins-1.4.0/tests/jobs/test_getall.py --- old/python-jenkins-1.2.1/tests/jobs/test_getall.py 2018-08-24 19:43:33.000000000 +0200 +++ new/python-jenkins-1.4.0/tests/jobs/test_getall.py 2018-11-19 02:55:24.000000000 +0100 @@ -1,7 +1,7 @@ +import json from mock import patch import jenkins -from tests.jobs.base import build_jobs_list_responses from tests.jobs.base import JenkinsGetJobsTestBase @@ -9,25 +9,30 @@ @patch.object(jenkins.Jenkins, 'jenkins_open') def test_simple(self, jenkins_mock): - response = build_jobs_list_responses( - self.jobs_in_folder, 'http://example.com/') - jenkins_mock.side_effect = iter(response) + jenkins_mock.side_effect = map(json.dumps, self.jobs_in_folder) jobs_info = self.j.get_all_jobs() expected_fullnames = [ - u"my_job1", u"my_folder1", u"my_job2", - u"my_folder1/my_job3", u"my_folder1/my_job4" + u"my_job1", u"my_folder1", u"my_job2", u"job", + u"my_folder1/my_job3", u"my_folder1/my_job4", u"job/my_job" ] self.assertEqual(len(expected_fullnames), len(jobs_info)) got_fullnames = [job[u"fullname"] for job in jobs_info] self.assertEqual(expected_fullnames, got_fullnames) + expected_request_urls = [ + self.make_url('api/json'), + self.make_url('job/my_folder1/api/json'), + self.make_url('job/job/api/json') + ] + self.assertEqual(expected_request_urls, + self.got_request_urls(jenkins_mock)) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_multi_level(self, jenkins_mock): - response = build_jobs_list_responses( - self.jobs_in_multiple_folders, 'http://example.com/') - jenkins_mock.side_effect = iter(response) + jenkins_mock.side_effect = map( + json.dumps, self.jobs_in_multiple_folders) jobs_info = self.j.get_all_jobs() @@ -44,11 +49,18 @@ for job in jobs_info if job['name'] == u"my_job1"])) + expected_request_urls = [ + self.make_url('api/json'), + self.make_url('job/my_folder1/api/json'), + self.make_url('job/my_folder1/job/my_folder2/api/json') + ] + self.assertEqual(expected_request_urls, + self.got_request_urls(jenkins_mock)) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_folders_depth(self, jenkins_mock): - response = build_jobs_list_responses( - self.jobs_in_multiple_folders, 'http://example.com/') - jenkins_mock.side_effect = iter(response) + jenkins_mock.side_effect = map( + json.dumps, self.jobs_in_multiple_folders) jobs_info = self.j.get_all_jobs(folder_depth=1) @@ -60,11 +72,17 @@ got_fullnames = [job[u"fullname"] for job in jobs_info] self.assertEqual(expected_fullnames, got_fullnames) + expected_request_urls = [ + self.make_url('api/json'), + self.make_url('job/my_folder1/api/json') + ] + self.assertEqual(expected_request_urls, + self.got_request_urls(jenkins_mock)) + @patch.object(jenkins.Jenkins, 'jenkins_open') def test_unsafe_chars(self, jenkins_mock): - response = build_jobs_list_responses( - self.jobs_in_unsafe_name_folders, 'http://example.com/') - jenkins_mock.side_effect = iter(response) + jenkins_mock.side_effect = map( + json.dumps, self.jobs_in_unsafe_name_folders) jobs_info = self.j.get_all_jobs() @@ -76,3 +94,36 @@ self.assertEqual(len(expected_fullnames), len(jobs_info)) got_fullnames = [job[u"fullname"] for job in jobs_info] self.assertEqual(expected_fullnames, got_fullnames) + + expected_request_urls = [ + self.make_url('api/json'), + self.make_url('job/my_folder1/api/json'), + self.make_url('job/my_folder1/job/my%20spaced%20folder/api/json') + ] + self.assertEqual(expected_request_urls, + self.got_request_urls(jenkins_mock)) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_deep_query(self, jenkins_mock): + jenkins_mock.side_effect = map( + json.dumps, self.jobs_in_folder_deep_query) + + jobs_info = self.j.get_all_jobs() + + expected_fullnames = [ + u"top_folder", + u"top_folder/middle_folder", + u"top_folder/middle_folder/bottom_folder", + u"top_folder/middle_folder/bottom_folder/my_job1", + u"top_folder/middle_folder/bottom_folder/my_job2" + ] + self.assertEqual(len(expected_fullnames), len(jobs_info)) + got_fullnames = [job[u"fullname"] for job in jobs_info] + self.assertEqual(expected_fullnames, got_fullnames) + + expected_request_urls = [ + self.make_url('api/json'), + self.make_url('job/top_folder/job/middle_folder/job/bottom_folder/api/json') + ] + self.assertEqual(expected_request_urls, + self.got_request_urls(jenkins_mock)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/test_credential.py new/python-jenkins-1.4.0/tests/test_credential.py --- old/python-jenkins-1.2.1/tests/test_credential.py 1970-01-01 01:00:00.000000000 +0100 +++ new/python-jenkins-1.4.0/tests/test_credential.py 2018-11-19 02:55:24.000000000 +0100 @@ -0,0 +1,353 @@ +import json +from mock import patch + +import jenkins +from tests.base import JenkinsTestBase + + +class JenkinsCredentialTestBase(JenkinsTestBase): + config_xml = """<com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl> + <scope>GLOBAL</scope> + <id>Test Credential</id> + <username>Test-User</username> + <password>secret123</password> + </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>""" + + +class JenkinsGetTagTextTest(JenkinsCredentialTestBase): + + def test_simple(self): + name_to_return = self.j._get_tag_text('id', self.config_xml) + self.assertEqual('Test Credential', name_to_return) + + def test_failed(self): + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j._get_tag_text('id', '<xml></xml>') + self.assertEqual(str(context_manager.exception), + 'tag[id] is invalidated') + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j._get_tag_text('id', '<xml><id></id></xml>') + self.assertEqual(str(context_manager.exception), + 'tag[id] is invalidated') + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j._get_tag_text('id', '<xml><id> </id></xml>') + self.assertEqual(str(context_manager.exception), + 'tag[id] is invalidated') + + +class JenkinsIsFolderTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_is_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + ] + self.assertTrue(self.j.is_folder('Test Folder')) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_is_not_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'org.jenkinsci.plugins.workflow.job.WorkflowJob'}), + ] + self.assertFalse(self.j.is_folder('Test Job')) + + +class JenkinsAssertFolderTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_is_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + ] + self.j.assert_folder('Test Folder') + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_is_not_folder(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'org.jenkinsci.plugins.workflow.job.WorkflowJob'}), + ] + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.assert_folder('Test Job') + self.assertEqual(str(context_manager.exception), + 'job[Test Job] is not a folder') + + +class JenkinsAssertCredentialTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_credential_missing(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + jenkins.NotFoundException() + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.assert_credential_exists('NonExistent', 'TestFoler') + self.assertEqual( + str(context_manager.exception), + 'credential[NonExistent] does not exist' + ' in the domain[_] of [TestFoler]') + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_credential_exists(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps({'id': 'ExistingCredential'}) + ] + self.j.assert_credential_exists('ExistingCredential', 'TestFoler') + self._check_requests(jenkins_mock.call_args_list) + + +class JenkinsCredentialExistsTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_credential_missing(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + jenkins.NotFoundException() + ] + + self.assertEqual(self.j.credential_exists('NonExistent', 'TestFolder'), + False) + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_credential_exists(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps({'id': 'ExistingCredential'}) + ] + + self.assertEqual(self.j.credential_exists('ExistingCredential', + 'TestFolder'), + True) + self._check_requests(jenkins_mock.call_args_list) + + +class JenkinsGetCredentialInfoTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_simple(self, jenkins_mock): + credential_info_to_return = {'id': 'ExistingCredential'} + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps(credential_info_to_return) + ] + + credential_info = self.j.get_credential_info('ExistingCredential', 'TestFolder') + + self.assertEqual(credential_info, credential_info_to_return) + self.assertEqual( + jenkins_mock.call_args[0][0].url, + self.make_url('job/TestFolder/credentials/store/folder/' + 'domain/_/credential/ExistingCredential/api/json?depth=0')) + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_nonexistent(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + None, + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.get_credential_info('NonExistent', 'TestFolder') + + self.assertEqual( + str(context_manager.exception), + 'credential[NonExistent] does not exist ' + 'in the domain[_] of [TestFolder]') + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_invalid_json(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + '{invalid_json}' + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.get_credential_info('NonExistent', 'TestFolder') + + self.assertEqual( + str(context_manager.exception), + 'Could not parse JSON info for credential[NonExistent]' + ' in the domain[_] of [TestFolder]') + + +class JenkinsGetCredentialConfigTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_encodes_credential_name(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + None, + ] + self.j.get_credential_config(u'Test Credential', u'Test Folder') + + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/domain/' + '_/credential/Test%20Credential/config.xml')) + self._check_requests(jenkins_mock.call_args_list) + + +class JenkinsCreateCredentialTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_simple(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + jenkins.NotFoundException(), + None, + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps({'id': 'Test Credential'}), + ] + + self.j.create_credential('Test Folder', self.config_xml) + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/' + 'domain/_/credential/Test%20Credential/api/json?depth=0')) + + self.assertEqual( + jenkins_mock.call_args_list[2][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/' + 'domain/_/createCredentials')) + + self.assertEqual( + jenkins_mock.call_args_list[4][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/' + 'domain/_/credential/Test%20Credential/api/json?depth=0')) + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_already_exists(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps({'id': 'Test Credential'}), + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.create_credential('Test Folder', self.config_xml) + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/' + 'domain/_/credential/Test%20Credential/api/json?depth=0')) + + self.assertEqual( + str(context_manager.exception), + 'credential[Test Credential] already exists' + ' in the domain[_] of [Test Folder]') + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_failed(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + jenkins.NotFoundException(), + None, + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + None, + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.create_credential('Test Folder', self.config_xml) + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/' + 'domain/_/credential/Test%20Credential/api/json?depth=0')) + self.assertEqual( + jenkins_mock.call_args_list[2][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/' + 'folder/domain/_/createCredentials')) + self.assertEqual( + jenkins_mock.call_args_list[4][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/' + 'domain/_/credential/Test%20Credential/api/json?depth=0')) + self.assertEqual( + str(context_manager.exception), + 'create[Test Credential] failed in the domain[_] of [Test Folder]') + self._check_requests(jenkins_mock.call_args_list) + + +class JenkinsDeleteCredentialTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_simple(self, jenkins_mock): + jenkins_mock.side_effect = [ + True, + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + jenkins.NotFoundException(), + ] + + self.j.delete_credential(u'Test Credential', 'TestFolder') + + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].url, + self.make_url('job/TestFolder/credentials/store/folder/domain/' + '_/credential/Test%20Credential/config.xml')) + self._check_requests(jenkins_mock.call_args_list) + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_failed(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'id': 'ExistingCredential'}), + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps({'id': 'ExistingCredential'}) + ] + + with self.assertRaises(jenkins.JenkinsException) as context_manager: + self.j.delete_credential(u'ExistingCredential', 'TestFolder') + self.assertEqual( + jenkins_mock.call_args_list[0][0][0].url, + self.make_url('job/TestFolder/credentials/store/folder/' + 'domain/_/credential/ExistingCredential/config.xml')) + self.assertEqual( + str(context_manager.exception), + 'delete credential[ExistingCredential] from ' + 'domain[_] of [TestFolder] failed') + self._check_requests(jenkins_mock.call_args_list) + + +class JenkinsReconfigCredentialTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_simple(self, jenkins_mock): + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps({'id': 'Test Credential'}), + None + ] + + self.j.reconfig_credential(u'Test Folder', self.config_xml) + + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/domain/' + '_/credential/Test%20Credential/api/json?depth=0')) + self.assertEqual( + jenkins_mock.call_args_list[2][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/domain/' + '_/credential/Test%20Credential/config.xml')) + self._check_requests(jenkins_mock.call_args_list) + + +class JenkinsListCredentialConfigTest(JenkinsCredentialTestBase): + + @patch.object(jenkins.Jenkins, 'jenkins_open') + def test_simple(self, jenkins_mock): + credentials_to_return = [{'id': 'Test Credential'}] + jenkins_mock.side_effect = [ + json.dumps({'_class': 'com.cloudbees.hudson.plugins.folder.Folder'}), + json.dumps({'credentials': [{'id': 'Test Credential'}]}), + ] + credentials = self.j.list_credentials(u'Test Folder') + self.assertEqual(credentials, credentials_to_return) + self.assertEqual( + jenkins_mock.call_args_list[1][0][0].url, + self.make_url('job/Test%20Folder/credentials/store/folder/domain/' + '_/api/json?tree=credentials[id]')) + self._check_requests(jenkins_mock.call_args_list) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tests/test_jenkins.py new/python-jenkins-1.4.0/tests/test_jenkins.py --- old/python-jenkins-1.2.1/tests/test_jenkins.py 2018-08-24 19:43:52.000000000 +0200 +++ new/python-jenkins-1.4.0/tests/test_jenkins.py 2018-11-19 02:55:24.000000000 +0100 @@ -120,6 +120,34 @@ self.assertFalse('.crumb' in request.headers) +class JenkinsMaybeAddHeaders(JenkinsTestBase): + @patch('jenkins.requests.Session.send', autospec=True) + def test_simple(self, session_send_mock): + session_send_mock.return_value = build_response_mock( + 404, reason="Not Found") + request = jenkins.requests.Request('GET', 'http://example.com/job/TestJob') + + with patch.dict('os.environ', {}): + j = jenkins.Jenkins(self.base_url, 'test', 'test') + request = j._session.prepare_request(request) + + self.assertEqual(request.headers, self.j._session.headers) + self.assertNotIn("X-Auth", request.headers) + + @patch('jenkins.requests.Session.send', autospec=True) + def test_add_header(self, session_send_mock): + session_send_mock.return_value = build_response_mock( + 404, reason="Not Found") + request = jenkins.requests.Request('GET', 'http://example.com/job/TestJob') + + with patch.dict('os.environ', {"JENKINS_API_EXTRA_HEADERS": "X-Auth: 123\nX-Key: 234"}): + j = jenkins.Jenkins(self.base_url, 'test', 'test') + request = j._session.prepare_request(request) + + self.assertEqual(request.headers["X-Auth"], "123") + self.assertEqual(request.headers["X-Key"], "234") + + class JenkinsOpenTest(JenkinsTestBase): @patch('jenkins.requests.Session.send', autospec=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-jenkins-1.2.1/tox.ini new/python-jenkins-1.4.0/tox.ini --- old/python-jenkins-1.2.1/tox.ini 2018-08-24 19:43:33.000000000 +0200 +++ new/python-jenkins-1.4.0/tox.ini 2018-11-19 02:55:24.000000000 +0100 @@ -1,5 +1,5 @@ [tox] -minversion = 1.6 +minversion = 2.0 skipsdist = True envlist = py{34,27,35}, linters, pypy