From e64023dde8a37cb0eaa80086111dc258f9d6ef37 Mon Sep 17 00:00:00 2001
From: Leonardo Rossetti <lrossett@redhat.com>
Date: Jun 24 2022 16:42:33 +0000
Subject: [PATCH 1/2] adding custom error class


---

diff --git a/src/kojid_cloud_scheduler/errors.py b/src/kojid_cloud_scheduler/errors.py
new file mode 100644
index 0000000..fbcfacc
--- /dev/null
+++ b/src/kojid_cloud_scheduler/errors.py
@@ -0,0 +1,23 @@
+class KCSError(Exception):
+    """
+    Project base exception class.
+
+    This error should used to raise errors
+    or inherited by other exception classes.
+    """
+
+    def __init__(self, message, code=1):
+        """
+        Creates a new KCSError object instance.
+
+        Parameters
+        ----------
+        message: str
+            Error message
+        code: int
+            Error code, defaults to 1.
+        """
+        super(KCSError, self).__init__(message)
+
+        self.message = message
+        self.code = code

From 0b5cc6949805908b0f0ea6cc831a189064ae9d68 Mon Sep 17 00:00:00 2001
From: Leonardo Rossetti <lrossett@redhat.com>
Date: Jun 28 2022 10:37:44 +0000
Subject: [PATCH 2/2] adding kojiclient code


---

diff --git a/poetry.lock b/poetry.lock
index 6cc4b5f..4170e6b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -21,6 +21,25 @@ tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)"
 tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
 
 [[package]]
+name = "certifi"
+version = "2022.6.15"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.0.12"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
 name = "colorama"
 version = "0.4.4"
 description = "Cross-platform colored terminal text."
@@ -29,6 +48,33 @@ optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "gssapi"
+version = "1.7.3"
+description = "Python GSSAPI Wrapper"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+decorator = "*"
+
+[[package]]
+name = "idna"
+version = "3.3"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
 name = "iniconfig"
 version = "1.1.1"
 description = "iniconfig: brain-dead simple config-ini parsing"
@@ -37,6 +83,20 @@ optional = false
 python-versions = "*"
 
 [[package]]
+name = "koji"
+version = "1.29.0"
+description = "Koji is a system for building and tracking RPMS. The base package contains shared libraries and the command-line interface."
+category = "main"
+optional = false
+python-versions = ">=2.7"
+
+[package.dependencies]
+python-dateutil = "*"
+requests = "*"
+requests-gssapi = "*"
+six = "*"
+
+[[package]]
 name = "packaging"
 version = "21.3"
 description = "Core utilities for Python packages"
@@ -100,6 +160,20 @@ tomli = ">=1.0.0"
 testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
 
 [[package]]
+name = "pytest-mock"
+version = "3.7.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pytest = ">=5.0"
+
+[package.extras]
+dev = ["pre-commit", "tox", "pytest-asyncio"]
+
+[[package]]
 name = "pytest-sugar"
 version = "0.9.4"
 description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)."
@@ -113,6 +187,55 @@ pytest = ">=2.9"
 termcolor = ">=1.1.0"
 
 [[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "requests"
+version = "2.28.0"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2.0.0,<2.1.0"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+
+[[package]]
+name = "requests-gssapi"
+version = "1.2.3"
+description = "A GSSAPI authentication handler for python-requests"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+gssapi = "*"
+requests = ">=1.1.0"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
 name = "termcolor"
 version = "1.1.0"
 description = "ANSII Color formatting for output in terminal."
@@ -128,10 +251,23 @@ category = "dev"
 optional = false
 python-versions = ">=3.7"
 
+[[package]]
+name = "urllib3"
+version = "1.26.9"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.10"
-content-hash = "f3749f8acd88278dca034f733bbfef3ea3c3a4efa56daffe2e5c914f6b71bc47"
+content-hash = "fad70e809d8180abbaec8db51b1abf838778fde8b8c6f70c5da6301daa9dffcd"
 
 [metadata.files]
 atomicwrites = [
@@ -142,14 +278,56 @@ attrs = [
     {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
     {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
 ]
+certifi = [
+    {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
+    {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
+]
+charset-normalizer = [
+    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
+    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
+]
 colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
     {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
+decorator = [
+    {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+    {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+gssapi = [
+    {file = "gssapi-1.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:69104cb87205ab6b6ac1e265b2df1c94b661140fdd5db223fefc564f958bcd44"},
+    {file = "gssapi-1.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fd3546303db6ae238764d61b4b639ffb74d615d0eb4735389887af804ff336"},
+    {file = "gssapi-1.7.3-cp310-cp310-win32.whl", hash = "sha256:ee76851059aecfcf2927ae991d74bd8c635de3e958d8e1cd3cd213d566ec7911"},
+    {file = "gssapi-1.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:2788648b614ac10fdf3df71a15ac04ec256e1721dd0c84ac8e104b71cd5fbac5"},
+    {file = "gssapi-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a5ba691f66e554d9adaf7d6aa8c82f384494d954059bdfcc6be560e7f9122ed3"},
+    {file = "gssapi-1.7.3-cp36-cp36m-win32.whl", hash = "sha256:35e864942c50d0e19608d12052a721745c8f28221c229cba05e534669d7b6225"},
+    {file = "gssapi-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2b9a47aa3d9c267c9c3677131f305fbf3c3c544450d2999f113af6e3ba738d2c"},
+    {file = "gssapi-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0eb28405cf3d5171b37796fa7cf5c06bb48eefd6d854b587aafe31b4b4d068db"},
+    {file = "gssapi-1.7.3-cp37-cp37m-win32.whl", hash = "sha256:6e5a0aae3be78fa5747120f51baa9d070dbc188c105064669ca6b53b75a08c80"},
+    {file = "gssapi-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:35dcddb6915dc7c5ae73afacaa6bb1e2da79c24668ad5a3cf2d7890d9c149d3e"},
+    {file = "gssapi-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e62981c96b4c138684e8b9dd932a4d52dcc7cc185cf8ac10aae9dd3db23d7e2"},
+    {file = "gssapi-1.7.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:56ae0564140ef3cd53034bdb0e4236e00f69dd2b0e3680c41263025d40d02c9d"},
+    {file = "gssapi-1.7.3-cp38-cp38-win32.whl", hash = "sha256:a7a9c9af5c96bfb95daa9b7166ae071549c8e6a01fbbdf9a589856d60677a823"},
+    {file = "gssapi-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:a2d272d6df0cdbc934d06a0e299fcfb8d7dff211b63d43ce291dcc8055a598ac"},
+    {file = "gssapi-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec25bfa506dbf82e0471f9ffce198b10da8f4b787f1779d52dbe514eb2cda999"},
+    {file = "gssapi-1.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13971279ccad7df0847d6be33ed2ea1c643bd58f065735c4954cd37c6f85d333"},
+    {file = "gssapi-1.7.3-cp39-cp39-win32.whl", hash = "sha256:c65221f5bc91f1fa317b48866d1a6da6248195d5c58e743f6c512a80c7c98119"},
+    {file = "gssapi-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:8bb1603b24a1f21a0da19df9f2544c122af896b1f28a17b0d45cf87773de58c4"},
+    {file = "gssapi-1.7.3.tar.gz", hash = "sha256:c69b9f633a0c03c1b84ba14c73b0ec132f6323056e675702c1a5f75f316e06fb"},
+]
+idna = [
+    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
 ]
+koji = [
+    {file = "koji-1.29.0-py2-none-any.whl", hash = "sha256:df7395dfc4157af4ee8ddb6c6498c762a98adeb77fb43d72cb7ce204857c367d"},
+    {file = "koji-1.29.0-py3-none-any.whl", hash = "sha256:7a24bc9e9c7508ded2e174f243780c57562422b8c7320714cff839b841b79b7a"},
+    {file = "koji-1.29.0.tar.gz", hash = "sha256:7db1ea4f68dd94d556c612e40bbfc2268164bfebc4a4cd08b63971bddaa686bc"},
+]
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
     {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@@ -170,9 +348,28 @@ pytest = [
     {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
     {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
 ]
+pytest-mock = [
+    {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"},
+    {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"},
+]
 pytest-sugar = [
     {file = "pytest-sugar-0.9.4.tar.gz", hash = "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3"},
 ]
+python-dateutil = [
+    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+requests = [
+    {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"},
+    {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"},
+]
+requests-gssapi = [
+    {file = "requests-gssapi-1.2.3.tar.gz", hash = "sha256:20784508981401f7153c933eed095338933a40818da65a259dbf2d80dccb150e"},
+]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
 termcolor = [
     {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
 ]
@@ -180,3 +377,7 @@ tomli = [
     {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
+urllib3 = [
+    {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
+    {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
+]
diff --git a/pyproject.toml b/pyproject.toml
index edcbbc2..f4d6e41 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,10 +10,12 @@ packages = [
 
 [tool.poetry.dependencies]
 python = "^3.10"
+koji = "^1.29.0"
 
 [tool.poetry.dev-dependencies]
 pytest = "^7.1.2"
 pytest-sugar = "^0.9.4"
+pytest-mock = "^3.7.0"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
diff --git a/src/kojid_cloud_scheduler/kojiclient.py b/src/kojid_cloud_scheduler/kojiclient.py
new file mode 100644
index 0000000..0c16f68
--- /dev/null
+++ b/src/kojid_cloud_scheduler/kojiclient.py
@@ -0,0 +1,89 @@
+import koji
+import requests
+
+from .errors import KCSError
+
+
+class KojiClient:
+    """
+    Koji client class which handles Koji Hub API integration.
+
+    This client doesn't require authentication as it only uses
+    read API calls.
+    """
+
+    def __init__(self, hub_url):
+        """
+        Creates a new KojiClient instance.
+
+        Parameters
+        ----------
+        hub_url: str
+            The base hub api endpoint to use, example: https://koji.fedoraproject.org/kojihub
+        """
+        self.session = None
+        self.hub_url = hub_url
+
+    def connect(self):
+        """
+        Creates a new client session to the hub api server.
+
+        Raises
+        ------
+        errors.KCSError
+        """
+        try:
+            self.session = koji.ClientSession(self.hub_url)
+        except AssertionError:
+            raise KCSError('"hub_url" must be set')
+
+    def ping(self):
+        """
+        Test the client connection.
+
+        Raises
+        ------
+        errors.KSCError
+
+        Return
+        ------
+        str
+            A 'Hello World' string message from the server
+        """
+        try:
+            return self.session.hello()
+        except requests.exceptions.RequestException as e:
+            raise KCSError(str(e))
+    
+    def list_tasks(self, **kwargs):
+        """
+        List tasks based on the provided kwargs.
+
+        The provided kwargs will be forwarded to
+        `self.session.listTasks`.
+
+        Check the upstream Koji documentation for
+        a detailed explanation about what can be
+        used in kwargs.
+
+        Parameters
+        ----------
+        kwargs: dict[str, any]
+            Extra key word arguments to be used as a dictionary
+            by `self.session.listTasks`.
+
+        Return
+        ------
+        list
+            A list of tasks or an empty list if none are found.
+        """
+        opts = {
+            'arch': ['x86_64'],
+            'state': [koji.TASK_STATES['OPEN']],
+        }
+        opts.update(**kwargs)
+
+        try:
+            return self.session.listTasks(opts)
+        except requests.exceptions.RequestException as e:
+            raise KCSError(str(e))
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 0000000..e684a42
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,32 @@
+import pytest
+
+
+@pytest.fixture
+def list_tasks_result():
+    return [
+        {
+            'arch': 'x86_64',
+            'awaited': True,
+            'channel_id': 1,
+            'completion_time': None,
+            'completion_ts': None,
+            'create_time': '2022-06-23 21:29:13.877050+00:00',
+            'create_ts': 1656019753.87705,
+            'host_id': 377,
+            'id': 88661672,
+            'label': 'x86_64',
+            'method': 'buildArch',
+            'owner': 4760,
+            'owner_name': 'koschei/koschei-backend01.iad2.fedoraproject.org',
+            'owner_type': 0,
+            'parent': 88661591,
+            'priority': 49,
+            'request': "",
+            'result': None,
+            'start_time': '2022-06-23 21:29:46.766581+00:00',
+            'start_ts': 1656019786.766581,
+            'state': 1,
+            'waiting': None,
+            'weight': 1.5653944408333333
+        }
+    ]
diff --git a/test/test_kojiclient.py b/test/test_kojiclient.py
new file mode 100644
index 0000000..ac734e2
--- /dev/null
+++ b/test/test_kojiclient.py
@@ -0,0 +1,79 @@
+import koji
+import pytest
+import requests
+
+from kojid_cloud_scheduler import kojiclient, errors
+
+
+def test_connect_ok():
+    url = 'https://koji-hub:443'
+    client = kojiclient.KojiClient(url)
+    client.connect()
+
+    assert client.session is not None
+
+
+def test_connect_error():
+    url = None
+    client = kojiclient.KojiClient(url)
+
+    with pytest.raises(errors.KCSError):
+        client.connect()
+
+
+def test_ping_ok(mocker): 
+    url = 'https://koji.fedoraproject.org/kojihub'
+    client = kojiclient.KojiClient(url)
+    client.connect()
+    
+    fn = mocker.Mock(return_value='Hello World')
+    mocker.patch.object(client.session, 'hello', fn)
+
+    assert 'Hello World' == client.ping()
+
+
+def test_ping_error(mocker):
+    url = 'https://koji-hub:8443'
+    client = kojiclient.KojiClient(url)
+    client.connect()
+
+    fn = mocker.Mock(side_effect=requests.exceptions.RequestException('fail'))
+    mocker.patch.object(client.session, 'hello', fn)
+    
+    with pytest.raises(errors.KCSError):
+        client.ping()
+
+
+def test_list_tasks_ok(mocker, list_tasks_result):
+    url = 'https://koji.fedoraproject.org/kojihub'
+    client = kojiclient.KojiClient(url)
+    client.connect()
+    
+    fn = mocker.Mock(return_value=list_tasks_result)
+    mocker.patch.object(client.session, 'listTasks', fn)
+
+    assert client.list_tasks() == list_tasks_result
+
+
+def test_list_tasks_error_notfound(mocker):
+    url = 'https://koji.fedoraproject.org/kojihub'
+    client = kojiclient.KojiClient(url)
+    client.connect()
+
+    fn = mocker.Mock(return_value=[])
+    mocker.patch.object(client.session, 'listTasks', fn)
+
+    assert [] == client.list_tasks(channel_id=[-1])
+
+
+def test_list_tasks_error_connection(mocker):
+    url = 'https://koji-hub:8443'
+    client = kojiclient.KojiClient(url)
+    client.connect()
+
+    fn = mocker.Mock(side_effect=requests.exceptions.RequestException('fail'))
+    mocker.patch.object(client.session, 'listTasks', fn)
+    
+    with pytest.raises(errors.KCSError):
+        client.list_tasks()
+

