commit 76bdccc4b1f4604e4a59a1227182c4e35dd5cdd7
Author: silhavyj <jakub.sil@seznam.cz>
Date:   Wed Apr 6 11:21:55 2022 +0200

    re #9421 Added unit tests for resend_cached_payload() and send_data(); moved api_client.py into usb_detector package

diff --git a/client/src/api_client.py b/client/src/api_client.py
deleted file mode 100644
index 7990a85..0000000
--- a/client/src/api_client.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import json
-import requests
-import logging
-from time import sleep
-from diskcache import Deque
-from requests import HTTPError, ConnectionError
-from requests.exceptions import InvalidSchema
-
-_uri = None
-_cache = None
-_config = None
-
-
-def api_client_set_config(config):
-    global _config, _cache, _uri
-    _config = config
-    _cache = _init_cache()
-    _uri = config.server_url + ":" + config.server_port + config.server_endpoint
-
-
-def _init_cache():
-    return Deque(directory=_config.cache_dir)
-
-
-def send_data(payload: dict):
-    if _uri is None:
-        logging.warning(f"sending payload = {payload} failed because uri is set to None")
-        _cache_failed_payload(payload)
-        return
-    try:
-        logging.info(f"sending payload = {payload} to {_uri}")
-        response = requests.post(url=_uri, data=json.dumps(payload))
-        logging.info(f"response text: {response.text}")
-    except (ConnectionError, InvalidSchema):
-        logging.warning(f"sending payload = {payload} to {_uri} failed")
-        _cache_failed_payload(payload)
-    except HTTPError as error:
-        logging.error(f"HTTP Error ({_uri}) payload = {payload}, {error}")
-        _cache_failed_payload(payload)
-
-
-def _cache_failed_payload(payload: dict):
-    if len(_cache) >= _config.cache_max_entries:
-        oldest_payload = _cache.pop()
-        logging.warning(f"cache is full - discarding payload = {oldest_payload}")
-
-    logging.info(f"adding payload = {payload} into cache")
-    _cache.append(payload)
-
-
-def _resend_cached_payloads():
-    retries = min(_config.max_retries, len(_cache))
-    logging.info(f"emptying the cache ({retries} records)")
-    for _ in range(0, retries):
-        payload = _cache.pop()
-        send_data(payload)
-
-
-def api_client_run():
-    while True:
-        _resend_cached_payloads()
-        sleep(_config.cache_retry_period_seconds)
diff --git a/client/src/main.py b/client/src/main.py
index 61d6cc4..bf1b369 100644
--- a/client/src/main.py
+++ b/client/src/main.py
@@ -9,7 +9,7 @@ from sys import exit
 from config_manager import Config
 from usb_detector.detector import register_listener, usb_detector_run, usb_detector_set_config
 from usb_detector.event_listener import usb_connected_callback, usb_disconnected_callback
-from client.src.api_client import api_client_run, api_client_set_config
+from usb_detector.api_client import api_client_run, api_client_set_config
 
 
 def init_logging(app_config: Config):
diff --git a/client/src/usb_detector/api_client.py b/client/src/usb_detector/api_client.py
new file mode 100644
index 0000000..7990a85
--- /dev/null
+++ b/client/src/usb_detector/api_client.py
@@ -0,0 +1,62 @@
+import json
+import requests
+import logging
+from time import sleep
+from diskcache import Deque
+from requests import HTTPError, ConnectionError
+from requests.exceptions import InvalidSchema
+
+_uri = None
+_cache = None
+_config = None
+
+
+def api_client_set_config(config):
+    global _config, _cache, _uri
+    _config = config
+    _cache = _init_cache()
+    _uri = config.server_url + ":" + config.server_port + config.server_endpoint
+
+
+def _init_cache():
+    return Deque(directory=_config.cache_dir)
+
+
+def send_data(payload: dict):
+    if _uri is None:
+        logging.warning(f"sending payload = {payload} failed because uri is set to None")
+        _cache_failed_payload(payload)
+        return
+    try:
+        logging.info(f"sending payload = {payload} to {_uri}")
+        response = requests.post(url=_uri, data=json.dumps(payload))
+        logging.info(f"response text: {response.text}")
+    except (ConnectionError, InvalidSchema):
+        logging.warning(f"sending payload = {payload} to {_uri} failed")
+        _cache_failed_payload(payload)
+    except HTTPError as error:
+        logging.error(f"HTTP Error ({_uri}) payload = {payload}, {error}")
+        _cache_failed_payload(payload)
+
+
+def _cache_failed_payload(payload: dict):
+    if len(_cache) >= _config.cache_max_entries:
+        oldest_payload = _cache.pop()
+        logging.warning(f"cache is full - discarding payload = {oldest_payload}")
+
+    logging.info(f"adding payload = {payload} into cache")
+    _cache.append(payload)
+
+
+def _resend_cached_payloads():
+    retries = min(_config.max_retries, len(_cache))
+    logging.info(f"emptying the cache ({retries} records)")
+    for _ in range(0, retries):
+        payload = _cache.pop()
+        send_data(payload)
+
+
+def api_client_run():
+    while True:
+        _resend_cached_payloads()
+        sleep(_config.cache_retry_period_seconds)
diff --git a/client/src/usb_detector/event_listener.py b/client/src/usb_detector/event_listener.py
index aaa87f9..8c7f927 100644
--- a/client/src/usb_detector/event_listener.py
+++ b/client/src/usb_detector/event_listener.py
@@ -3,7 +3,7 @@ import logging
 import getpass
 from datetime import datetime
 
-from ..api_client import send_data
+from .api_client import send_data
 
 
 def _get_metadata() -> dict:
diff --git a/client/tests/api_client/test_api_client_set_config.py b/client/tests/api_client/test_api_client_set_config.py
index 34c7d9d..4afaaa1 100644
--- a/client/tests/api_client/test_api_client_set_config.py
+++ b/client/tests/api_client/test_api_client_set_config.py
@@ -1,9 +1,9 @@
-import client.src.api_client
+from client.src.usb_detector import api_client
 
 from unittest import mock
 
 
-@mock.patch('client.src.api_client._init_cache')
+@mock.patch('client.src.usb_detector.api_client._init_cache')
 def test_api_client_set_config_1(_init_cache_mock):
     class Config:
         def __init__(self):
@@ -12,7 +12,7 @@ def test_api_client_set_config_1(_init_cache_mock):
             self.server_endpoint = "/api/v1/usb-logs"
 
     config = Config()
-    client.src.api_client.api_client_set_config(config)
+    api_client.api_client_set_config(config)
 
-    assert client.src.api_client._config is config
-    assert client.src.api_client._uri == "127.0.0.1:54444/api/v1/usb-logs"
+    assert api_client._config is config
+    assert api_client._uri == "127.0.0.1:54444/api/v1/usb-logs"
diff --git a/client/tests/api_client/test_cache_failed_payload.py b/client/tests/api_client/test_cache_failed_payload.py
index 1bc8c82..0b5a962 100644
--- a/client/tests/api_client/test_cache_failed_payload.py
+++ b/client/tests/api_client/test_cache_failed_payload.py
@@ -1,4 +1,4 @@
-import client.src.api_client as api_client
+from client.src.usb_detector import api_client
 
 
 class CacheMock:
diff --git a/client/tests/api_client/test_resend_cached_payloads.py b/client/tests/api_client/test_resend_cached_payloads.py
new file mode 100644
index 0000000..3325de7
--- /dev/null
+++ b/client/tests/api_client/test_resend_cached_payloads.py
@@ -0,0 +1,86 @@
+from client.src.usb_detector import api_client
+
+from unittest import mock
+
+
+class CacheMock:
+
+    def __init__(self):
+        self._data = []
+
+    def pop(self):
+        return self._data.pop()
+
+    def append(self, payload):
+        self._data.append(payload)
+
+    def __len__(self):
+        return len(self._data)
+
+
+class ConfigMock:
+
+    def __init__(self):
+        self.cache_max_entries = 5
+        self.max_retries = 3
+
+
+payload_mock = {
+    "device": {
+        "vendor_id": 1,
+        "product_id": 2
+    }
+}
+
+
+@mock.patch('client.src.usb_detector.api_client.send_data')
+def test_resend_cached_payloads_1(send_data_mock):
+    cache = CacheMock()
+    config = ConfigMock()
+
+    api_client._cache = cache
+    api_client._config = config
+
+    for _ in range(0, config.cache_max_entries + 1):
+        api_client._cache_failed_payload(payload_mock)
+
+    api_client._resend_cached_payloads()
+    send_data_mock.assert_called()
+    assert len(api_client._cache) == 2
+
+    api_client._resend_cached_payloads()
+    assert len(api_client._cache) == 0
+
+    api_client._resend_cached_payloads()
+    assert len(api_client._cache) == 0
+
+
+@mock.patch('client.src.usb_detector.api_client.send_data')
+def test_resend_cached_payloads_2(send_data_mock):
+    cache = CacheMock()
+    config = ConfigMock()
+
+    api_client._cache = cache
+    api_client._config = config
+
+    api_client._resend_cached_payloads()
+    send_data_mock.assert_not_called()
+    assert len(api_client._cache) == 0
+
+
+@mock.patch('client.src.usb_detector.api_client.send_data')
+def test_resend_cached_payloads_3(send_data_mock):
+    cache = CacheMock()
+    config = ConfigMock()
+
+    api_client._cache = cache
+    api_client._config = config
+
+    for _ in range(0, 2):
+        api_client._cache_failed_payload(payload_mock)
+
+    assert len(api_client._cache) == 2
+    api_client._resend_cached_payloads()
+    send_data_mock.assert_called()
+    assert len(api_client._cache) == 0
+
diff --git a/client/tests/api_client/test_send_data.py b/client/tests/api_client/test_send_data.py
new file mode 100644
index 0000000..6e37ad7
--- /dev/null
+++ b/client/tests/api_client/test_send_data.py
@@ -0,0 +1,73 @@
+from client.src.usb_detector import api_client
+
+import requests
+from unittest import mock
+
+
+@mock.patch('client.src.usb_detector.api_client._cache_failed_payload')
+def test_send_data_1(_cache_failed_payload_mock):
+    payload_mock = {
+        "device": {
+            "vendor_id": 1,
+            "product_id": 2
+        }
+    }
+
+    api_client._uri = None
+    api_client.send_data(payload_mock)
+
+    args = _cache_failed_payload_mock.call_args.args
+    _cache_failed_payload_mock.assert_called()
+    assert args[0] == payload_mock
+
+
+@mock.patch('client.src.usb_detector.api_client._cache_failed_payload')
+def test_send_data_2(_cache_failed_payload_mock):
+    payload_mock = {
+        "device": {
+            "vendor_id": 1,
+            "product_id": 2
+        }
+    }
+
+    api_client._uri = "127.0.0.1:54444/api/v1/usb-logs"
+    api_client.send_data(payload_mock)
+
+    args = _cache_failed_payload_mock.call_args.args
+    _cache_failed_payload_mock.assert_called()
+    assert args[0] == payload_mock
+
+
+@mock.patch('client.src.usb_detector.api_client.requests.post')
+@mock.patch('client.src.usb_detector.api_client._cache_failed_payload')
+def test_send_data_3(_cache_failed_payload_mock, post_mock):
+    payload_mock = {
+        "device": {
+            "vendor_id": 1,
+            "product_id": 2
+        }
+    }
+
+    api_client._uri = "127.0.0.1:54444/api/v1/usb-logs"
+    api_client.send_data(payload_mock)
+
+    _cache_failed_payload_mock.assert_not_called()
+
+
+@mock.patch('client.src.usb_detector.api_client.requests.post')
+@mock.patch('client.src.usb_detector.api_client._cache_failed_payload')
+def test_send_data_4(_cache_failed_payload_mock, post_mock):
+    payload_mock = {
+        "device": {
+            "vendor_id": 1,
+            "product_id": 2
+        }
+    }
+    post_mock.side_effect = requests.exceptions.HTTPError()
+
+    api_client._uri = "127.0.0.1:54444/api/v1/usb-logs"
+    api_client.send_data(payload_mock)
+
+    args = _cache_failed_payload_mock.call_args.args
+    _cache_failed_payload_mock.assert_called()
+    assert args[0] == payload_mock
