From 935911e6bedec7e21d586a986bbbfce7c71ad966 Mon Sep 17 00:00:00 2001 From: guzmud Date: Mon, 29 Jun 2026 12:32:42 +0200 Subject: [PATCH 1/5] test(pyoaev): adding some basic tests to reach 80% coverage --- test/backends/test_backend.py | 28 +++++++++++ test/configuration/test_configuration.py | 17 +++++++ test/test_base.py | 63 ++++++++++++++++++++++++ test/test_exceptions.py | 62 +++++++++++++++++++++++ test/test_utils.py | 10 ++++ 5 files changed, 180 insertions(+) create mode 100644 test/test_base.py create mode 100644 test/test_exceptions.py diff --git a/test/backends/test_backend.py b/test/backends/test_backend.py index 5e85b8b..3c0cea0 100644 --- a/test/backends/test_backend.py +++ b/test/backends/test_backend.py @@ -1,8 +1,36 @@ import unittest +from unittest.mock import MagicMock from pyoaev.backends import backend as module +class TestTokenAuth(unittest.TestCase): + def test_auto_bearer_token(self): + request = MagicMock() + request.headers = dict() + token = "my-secret-token" + + token_auth = module.TokenAuth(token=token) + + _request = token_auth(request) + + self.assertEqual( + _request.headers["Authorization"], + f"Bearer {token}", + ) + +class TestRequestsReponse(unittest.TestCase): + def test_init_and_properties(self): + response = MagicMock() + + rr = module.RequestsResponse(response) + + self.assertEqual(response, rr.response) + self.assertEqual(response.status_code, rr.status_code) + self.assertEqual(response.headers, rr.headers) + self.assertEqual(response.content, rr.content) + self.assertEqual(response.reason, rr.reason) + class TestRequestsBackend(unittest.TestCase): def test_no_cookie_allowed(self): backend = module.RequestsBackend() diff --git a/test/configuration/test_configuration.py b/test/configuration/test_configuration.py index cd86533..25aa98b 100644 --- a/test/configuration/test_configuration.py +++ b/test/configuration/test_configuration.py @@ -2,7 +2,9 @@ import unittest from unittest.mock import patch +from pydantic_settings import BaseSettings from pyoaev.configuration import Configuration +from pyoaev.configuration.connector_config_schema_generator import ConnectorConfigSchemaGenerator TEST_CONFIG_HINTS = { "string_config_direct": {"data": "this is string_config_direct"}, @@ -252,6 +254,21 @@ def test_when_bool_config_has_default_when_key_is_not_found_return_default(self) self.assertEqual(value, True) + def test_configuration_schema_generation(self): + config_obj = Configuration( + config_hints=TEST_CONFIG_HINTS, + config_base_model=BaseSettings(), + ) + + _schema = config_obj.schema() + + self.assertEqual( + _schema, + BaseSettings.model_json_schema( + by_alias=False, schema_generator=ConnectorConfigSchemaGenerator, mode="validation" + ) + ) + if __name__ == "__main__": unittest.main() diff --git a/test/test_base.py b/test/test_base.py new file mode 100644 index 0000000..683aba1 --- /dev/null +++ b/test/test_base.py @@ -0,0 +1,63 @@ +import unittest +from unittest.mock import MagicMock, sentinel + +import pyoaev.base as module + + +class TestRESTObject(unittest.TestCase): + def test_restobject_minimal_init(self): + manager = MagicMock() + manager.parent_attrs = {"parentkey1": "parentvalue1"} + attrs = {"key1": "value1"} + created_from_list = sentinel._created_from_list + + rest_object = module.RESTObject(manager, attrs, created_from_list=created_from_list) + + self.assertEqual(rest_object.manager, manager) + self.assertEqual(rest_object._attrs, attrs) + self.assertEqual(rest_object._created_from_list, created_from_list) + self.assertEqual(rest_object._updated_attrs, {}) + self.assertEqual(rest_object._parent_attrs, manager.parent_attrs) + self.assertEqual(str(rest_object), " => {'key1': 'value1'}") + + # properties + self.assertEqual(rest_object.attributes, {"key1": "value1", "parentkey1": "parentvalue1"}) + self.assertIsNone(rest_object.encoded_id) + + def test_restobject_failed_init(self): + manager = MagicMock() + attrs = MagicMock() + created_from_list = sentinel._created_from_list + + with self.assertRaises(module.OpenAEVParsingError): + module.RESTObject(manager, attrs, created_from_list=created_from_list) + + def test_restobject_to_json(self): + manager = MagicMock() + manager.parent_attrs = {"parentkey1": "parentvalue1"} + attrs = {"key1": "value1"} + created_from_list = sentinel._created_from_list + + rest_object = module.RESTObject(manager, attrs, created_from_list=created_from_list) + + jsondata = rest_object.to_json() + + self.assertEqual( + jsondata, + '{"key1": "value1"}' + ) + self.assertNotIn("parentkey1", jsondata) + + def test_restobject_get_id(self): + manager = MagicMock() + manager.parent_attrs = {"parentkey1": "parentvalue1"} + attrs = {"key1": "value1"} + created_from_list = sentinel._created_from_list + + rest_object = module.RESTObject(manager, attrs, created_from_list=created_from_list) + + self.assertIsNone(rest_object.get_id()) + + rest_object._update_attrs({"_id_attr": "my_id"}) + + self.assertIsNone(rest_object.get_id()) diff --git a/test/test_exceptions.py b/test/test_exceptions.py new file mode 100644 index 0000000..b1c7251 --- /dev/null +++ b/test/test_exceptions.py @@ -0,0 +1,62 @@ +import unittest +from unittest.mock import MagicMock + +import pyoaev.exceptions as module + + +class TestExceptions(unittest.TestCase): + def test_openaeverror_init(self): + error_message = MagicMock() + response_code = MagicMock() + response_body = MagicMock() + + exception = module.OpenAEVError(error_message, response_code, response_body) + + self.assertEqual(exception.response_code, response_code) + self.assertEqual(exception.response_body, response_body) + self.assertEqual(exception.error_message, error_message.decode.return_value) + + def test_openaeverror_init_auto_decode(self): + response_code = MagicMock() + response_body = MagicMock() + + exception = module.OpenAEVError(b"test", response_code, response_body) + + self.assertEqual(exception.error_message, "test") + + exception = module.OpenAEVError("test", response_code, response_body) + + self.assertEqual(exception.error_message, "test") + + def test_openaeverror_strcast_no_message(self): + error_message = None + response_code = 418 + response_body = MagicMock() + + exception = module.OpenAEVError(error_message, response_code, response_body) + + strdata = str(exception) + + self.assertEqual(strdata, "418: Unknown error") + + def test_openaeverror_strcast_no_message_no_code(self): + error_message = None + response_code = None + response_body = MagicMock() + + exception = module.OpenAEVError(error_message, response_code, response_body) + + strdata = str(exception) + + self.assertEqual(strdata, "Unknown error") + + def test_openaeverror_strcast_with_body(self): + error_message = None + response_code = 418 + response_body = b'{"message": "I am a teapot"}' + + exception = module.OpenAEVError(error_message, response_code, response_body) + + strdata = str(exception) + + self.assertEqual(strdata, "418: I am a teapot") diff --git a/test/test_utils.py b/test/test_utils.py index 4314f55..e5279b7 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -67,6 +67,16 @@ def test_encoded_id_rejects_unsupported_type(self): with self.assertRaises(TypeError): module.EncodedId(cast(Any, ["bad"])) + def test_enhanced_json_encoder_serializes_not_dataclasses(self): + data = {"test": "this is a test", "other": 3} + classic_json_dump = json.dumps(data) + enhanced_json_dump = json.dumps(data, cls=module.EnhancedJSONEncoder) + self.assertFalse(module.dataclasses.is_dataclass(data)) + self.assertEqual( + classic_json_dump, + enhanced_json_dump, + ) + def test_enhanced_json_encoder_serializes_dataclasses(self): self.assertEqual( json.dumps(_SampleData(value=3), cls=module.EnhancedJSONEncoder), From 5a19074fd23c98c1e3c35698103da67071960cb7 Mon Sep 17 00:00:00 2001 From: guzmud Date: Fri, 12 Jun 2026 09:43:24 +0200 Subject: [PATCH 2/5] ci(coverage): add codecov configuration file at the root of the repository (#239) --- codecov.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..92b4a7f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,19 @@ +coverage: + status: + project: + default: + target: auto + informational: true + patch: + default: + target: 80% + informational: false + +comment: + layout: "diff, files, condensed_footer" + hide_project_coverage: true # set to true + require_changes: "coverage_drop AND uncovered_patch" + +ignore: + # Ignore any folder whose name starts with "test" anywhere in the tree + - "**/test*/**" \ No newline at end of file From 4ec27a1ce450fa7c5eee0270b22b6f555432ef18 Mon Sep 17 00:00:00 2001 From: guzmud Date: Fri, 12 Jun 2026 11:54:51 +0200 Subject: [PATCH 3/5] ci(coverage): adding junit.xml export and upload in codecov (#239) --- .github/workflows/coverage.yml | 13 ++++++++++--- .gitignore | 3 ++- test/daemons/test_base_daemon.py | 17 ++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 73b47f6..c2ae356 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -27,10 +27,10 @@ jobs: uv venv && uv pip install -e '.[dev]' coverage - name: Run tests with coverage - run: uv run coverage run -m pytest + run: uv run coverage run -m pytest --junitxml="junit.xml" - name: Print coverage summary - run: uv run coverage report -m --fail-under=70 + run: uv run coverage report -m --fail-under=80 - name: Generate coverage XML run: uv run coverage xml -o coverage.xml @@ -40,6 +40,13 @@ jobs: uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} - flags: connectors + flags: pyoaev fail_ci_if_error: false verbose: true + + - name: Upload test results to Codecov + if: ${{ !cancelled() && hashFiles('junit.xml') != '' }} + uses: codecov/codecov-action@v7 + with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results diff --git a/.gitignore b/.gitignore index 0b76752..82c5d07 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +junit.xml # Translations *.mo @@ -136,4 +137,4 @@ dmypy.json cython_debug/ # testing -test.py \ No newline at end of file +test.py diff --git a/test/daemons/test_base_daemon.py b/test/daemons/test_base_daemon.py index d3d7a19..abaacce 100644 --- a/test/daemons/test_base_daemon.py +++ b/test/daemons/test_base_daemon.py @@ -51,17 +51,19 @@ def bound_method(self): ) +@unittest.mock.patch("argparse.ArgumentParser") class TestBaseDaemon(unittest.TestCase): - def test_when_no_callback_when_complete_config_ctor_ok(self): + def test_when_no_callback_when_complete_config_ctor_ok(self, _): daemon = DaemonForTest(configuration=TEST_DAEMON_CONFIGURATION) self.assertIsInstance(daemon, BaseDaemon) - def test_when_no_callback_when_lacking_config_key_ctor_throws(self): + def test_when_no_callback_when_lacking_config_key_ctor_throws(self, _): with self.assertRaises(Exception): DaemonForTest(configuration=Configuration(config_hints={})) - def test_when_no_callback_daemon_cant_start(self): + def test_when_no_callback_daemon_cant_start(self, m_argparser): + m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock(dump_config_schema=False) daemon, mock_setup, mock_start_loop, _ = create_mock_daemon() with self.assertRaises(OpenAEVError): @@ -70,7 +72,8 @@ def test_when_no_callback_daemon_cant_start(self): mock_setup.assert_not_called() mock_start_loop.assert_not_called() - def test_when_callback_daemon_can_start(self): + def test_when_callback_daemon_can_start(self, m_argparser): + m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock(dump_config_schema=False) daemon, mock_setup, mock_start_loop, _ = create_mock_daemon(lambda: None) daemon.start() @@ -78,7 +81,7 @@ def test_when_callback_daemon_can_start(self): mock_setup.assert_called_once() mock_start_loop.assert_called_once() - def test_when_callback_is_bound_method_daemon_can_call(self): + def test_when_callback_is_bound_method_daemon_can_call(self, _): daemon, mock_setup, mock_start_loop, inner_mock_func = create_mock_daemon() daemon.set_callback(daemon.bound_method) @@ -86,7 +89,7 @@ def test_when_callback_is_bound_method_daemon_can_call(self): inner_mock_func.assert_called_once() - def test_when_callback_is_func_with_collector_parameter_daemon_can_call(self): + def test_when_callback_is_func_with_collector_parameter_daemon_can_call(self, _): daemon, mock_setup, mock_start_loop, _ = create_mock_daemon() inner_mock_func = unittest.mock.MagicMock() daemon.set_callback(lambda collector: inner_mock_func()) @@ -95,7 +98,7 @@ def test_when_callback_is_func_with_collector_parameter_daemon_can_call(self): inner_mock_func.assert_called_once() - def test_when_callback_is_func_with_other_parameter_daemon_cant_call(self): + def test_when_callback_is_func_with_other_parameter_daemon_cant_call(self, _): daemon, mock_setup, mock_start_loop, _ = create_mock_daemon() inner_mock_func = unittest.mock.MagicMock() daemon.set_callback(lambda other_parameter: inner_mock_func()) From da007f7e6f40f578c144b6c005d00b957ce08ac1 Mon Sep 17 00:00:00 2001 From: guzmud Date: Fri, 12 Jun 2026 11:55:38 +0200 Subject: [PATCH 4/5] ci(coverage): updating CONTRIBUTING to reflect CI constraints (#239) --- CONTRIBUTING.md | 9 +++++++++ test/daemons/test_base_daemon.py | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17851e5..0b0d564 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,15 @@ developing features or fixing bugs yourself. * If you are interested in contributing code, fork the repository, create a branch, and open a pull request. +## Code quality and testing + +In order to maintain a certain standard regarding code quality, this repository CI will run various tools against expectations: +- `isort` will be used to check for proper imports sorting, and the CI will fail if the checks fail +- `black` will be used to check for proper code formatting, and the CI will fail if the checks fail +- `flake8` will be used to check for proper code styling, and the CI will fail if the checks fail +- `pytest` will be used to run various levels of testing, and the CI will fail if the tests fail +- `coverage` with `codecov` will be used to check for proper code coverage, and the CI will fail if the level is below 80% for the current diff (a warning will be emitted if the level falls below 80% for the overall project) + ## Commit, pull request & issue conventions diff --git a/test/daemons/test_base_daemon.py b/test/daemons/test_base_daemon.py index abaacce..d980506 100644 --- a/test/daemons/test_base_daemon.py +++ b/test/daemons/test_base_daemon.py @@ -63,7 +63,9 @@ def test_when_no_callback_when_lacking_config_key_ctor_throws(self, _): DaemonForTest(configuration=Configuration(config_hints={})) def test_when_no_callback_daemon_cant_start(self, m_argparser): - m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock(dump_config_schema=False) + m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock( + dump_config_schema=False + ) daemon, mock_setup, mock_start_loop, _ = create_mock_daemon() with self.assertRaises(OpenAEVError): @@ -73,7 +75,9 @@ def test_when_no_callback_daemon_cant_start(self, m_argparser): mock_start_loop.assert_not_called() def test_when_callback_daemon_can_start(self, m_argparser): - m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock(dump_config_schema=False) + m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock( + dump_config_schema=False + ) daemon, mock_setup, mock_start_loop, _ = create_mock_daemon(lambda: None) daemon.start() From 1a66a95c225839d30c033c77ca1ddd4618b149f5 Mon Sep 17 00:00:00 2001 From: guzmud Date: Mon, 29 Jun 2026 10:58:49 +0200 Subject: [PATCH 5/5] ci(coverage): fixes post-review (#239) --- .github/workflows/coverage.yml | 1 - pyproject.toml | 3 +++ test/backends/test_backend.py | 2 ++ test/configuration/test_configuration.py | 11 ++++++++--- test/test_base.py | 25 +++++++++++++++--------- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c2ae356..d2944d2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -40,7 +40,6 @@ jobs: uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} - flags: pyoaev fail_ci_if_error: false verbose: true diff --git a/pyproject.toml b/pyproject.toml index 4c65201..9c376a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,3 +106,6 @@ warn_unused_ignores = true omit = [ "test/*", ] + +[tool.coverage.report] +fail_under = 80 diff --git a/test/backends/test_backend.py b/test/backends/test_backend.py index 3c0cea0..e78262f 100644 --- a/test/backends/test_backend.py +++ b/test/backends/test_backend.py @@ -19,6 +19,7 @@ def test_auto_bearer_token(self): f"Bearer {token}", ) + class TestRequestsReponse(unittest.TestCase): def test_init_and_properties(self): response = MagicMock() @@ -31,6 +32,7 @@ def test_init_and_properties(self): self.assertEqual(response.content, rr.content) self.assertEqual(response.reason, rr.reason) + class TestRequestsBackend(unittest.TestCase): def test_no_cookie_allowed(self): backend = module.RequestsBackend() diff --git a/test/configuration/test_configuration.py b/test/configuration/test_configuration.py index 25aa98b..20f5e16 100644 --- a/test/configuration/test_configuration.py +++ b/test/configuration/test_configuration.py @@ -3,8 +3,11 @@ from unittest.mock import patch from pydantic_settings import BaseSettings + from pyoaev.configuration import Configuration -from pyoaev.configuration.connector_config_schema_generator import ConnectorConfigSchemaGenerator +from pyoaev.configuration.connector_config_schema_generator import ( + ConnectorConfigSchemaGenerator, +) TEST_CONFIG_HINTS = { "string_config_direct": {"data": "this is string_config_direct"}, @@ -265,8 +268,10 @@ def test_configuration_schema_generation(self): self.assertEqual( _schema, BaseSettings.model_json_schema( - by_alias=False, schema_generator=ConnectorConfigSchemaGenerator, mode="validation" - ) + by_alias=False, + schema_generator=ConnectorConfigSchemaGenerator, + mode="validation", + ), ) diff --git a/test/test_base.py b/test/test_base.py index 683aba1..9863f58 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -11,17 +11,23 @@ def test_restobject_minimal_init(self): attrs = {"key1": "value1"} created_from_list = sentinel._created_from_list - rest_object = module.RESTObject(manager, attrs, created_from_list=created_from_list) + rest_object = module.RESTObject( + manager, attrs, created_from_list=created_from_list + ) self.assertEqual(rest_object.manager, manager) self.assertEqual(rest_object._attrs, attrs) self.assertEqual(rest_object._created_from_list, created_from_list) self.assertEqual(rest_object._updated_attrs, {}) self.assertEqual(rest_object._parent_attrs, manager.parent_attrs) - self.assertEqual(str(rest_object), " => {'key1': 'value1'}") + self.assertEqual( + str(rest_object), " => {'key1': 'value1'}" + ) # properties - self.assertEqual(rest_object.attributes, {"key1": "value1", "parentkey1": "parentvalue1"}) + self.assertEqual( + rest_object.attributes, {"key1": "value1", "parentkey1": "parentvalue1"} + ) self.assertIsNone(rest_object.encoded_id) def test_restobject_failed_init(self): @@ -38,14 +44,13 @@ def test_restobject_to_json(self): attrs = {"key1": "value1"} created_from_list = sentinel._created_from_list - rest_object = module.RESTObject(manager, attrs, created_from_list=created_from_list) + rest_object = module.RESTObject( + manager, attrs, created_from_list=created_from_list + ) jsondata = rest_object.to_json() - self.assertEqual( - jsondata, - '{"key1": "value1"}' - ) + self.assertEqual(jsondata, '{"key1": "value1"}') self.assertNotIn("parentkey1", jsondata) def test_restobject_get_id(self): @@ -54,7 +59,9 @@ def test_restobject_get_id(self): attrs = {"key1": "value1"} created_from_list = sentinel._created_from_list - rest_object = module.RESTObject(manager, attrs, created_from_list=created_from_list) + rest_object = module.RESTObject( + manager, attrs, created_from_list=created_from_list + ) self.assertIsNone(rest_object.get_id())