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
 


Reply via email to