From 91e6422dd9f17c98571a9d3dbe1440b14e71b47d Mon Sep 17 00:00:00 2001 From: mshriver Date: Fri, 21 Nov 2025 10:26:31 -0500 Subject: [PATCH 1/3] remove tests symlink --- tests | 1 - 1 file changed, 1 deletion(-) delete mode 120000 tests diff --git a/tests b/tests deleted file mode 120000 index 30d74d2..0000000 --- a/tests +++ /dev/null @@ -1 +0,0 @@ -test \ No newline at end of file From 7ddec7a31485cc057f8d9203bbce5914f3aab76e Mon Sep 17 00:00:00 2001 From: mshriver Date: Fri, 21 Nov 2025 11:34:56 -0500 Subject: [PATCH 2/3] Configuration and workflow updates for tests --- .github/workflows/tests.yml | 7 +---- pyproject.toml | 58 +++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 793ef6c..c039960 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,13 +24,8 @@ jobs: run: | python -m pip install --upgrade pip pip install hatch - - name: Install project with test dependencies - run: hatch env create - name: Test with pytest and coverage - run: hatch test --cover --cover-quiet - - name: Generate coverage XML report - if: matrix.python-version == '3.12' - run: hatch run test:coverage xml + run: hatch run test-cov - name: Upload coverage to Codecov if: matrix.python-version == '3.12' uses: codecov/codecov-action@v5 diff --git a/pyproject.toml b/pyproject.toml index f30de30..809dd45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,24 +62,32 @@ include = ["/ibutsu_client", "/test", "/docs", "/requirements.txt", "/test-requi [tool.hatch.build.targets.wheel] packages = ["/ibutsu_client"] -[tool.hatch.envs.hatch-test] -extra-dependencies = ["ibutsu-client[test]"] - -[[tool.hatch.envs.hatch-test.matrix]] -python = ["3.11", "3.12", "3.13"] - -[tool.hatch.envs.test] +[tool.hatch.envs.default] dependencies = [ "pytest", "pytest-cov", - "coverage[toml]", -] -extra-dependencies = [ - "pytest-rerunfailures", "pytest-mock", "pytest-xdist", + "pytest-rerunfailures", + "coverage[toml]", + "pre-commit", ] +[tool.hatch.envs.default.scripts] +test = "pytest {args}" +test-cov = "pytest --cov=ibutsu_client --cov-report=xml --cov-report=term-missing --cov-report=html {args}" +cov-report = "coverage report" +cov-xml = "coverage xml" +cov-html = "coverage html" +lint = "pre-commit run --all-files" + +# CI environment for testing multiple Python versions +[tool.hatch.envs.test-matrix] +template = "default" + +[[tool.hatch.envs.test-matrix.matrix]] +python = ["3.11", "3.12", "3.13"] + [tool.mypy] # Enable strict mode for maximum type safety strict = true @@ -132,6 +140,7 @@ source = ["ibutsu_client"] branch = true omit = [ "*/test/*", + "*/tests/*", "*/test_*", "*/conftest.py", "*/__pycache__/*", @@ -140,22 +149,35 @@ omit = [ ] [tool.coverage.report] +precision = 2 +skip_empty = true exclude_lines = [ + # Pragmas and docstrings "pragma: no cover", "def __repr__", - "if self.debug:", - "if settings.DEBUG", + # Defensive programming patterns "raise AssertionError", "raise NotImplementedError", - "if 0:", + "assert False", + # Type checking blocks + "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", + # Abstract methods + "@abstractmethod", + "@abc.abstractmethod", + # Main guard "if __name__ == .__main__.:", + # Debug-only code + "if self.debug:", + "if settings.DEBUG", + "if.*__debug__:", + "if 0:", + # Protocol definitions "class .*\\bProtocol\\):", - "@(abc\\.)?abstractmethod", - "TYPE_CHECKING", + "@overload", + "@typing.overload", ] show_missing = true -precision = 2 -skip_covered = false [tool.coverage.html] directory = "htmlcov" From e1d346a9754f8f20baced545a9e7942482c98e43 Mon Sep 17 00:00:00 2001 From: mshriver Date: Fri, 21 Nov 2025 11:39:50 -0500 Subject: [PATCH 3/3] real API call test coverage --- test/conftest.py | 70 ++++++++++++++++++++++ test/test_health.py | 125 ++++++++++++++++++++++++++++------------ test/test_health_api.py | 106 +++++++++++++++++++++++++++++----- 3 files changed, 249 insertions(+), 52 deletions(-) create mode 100644 test/conftest.py diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..439732b --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,70 @@ +"""Test configuration and fixtures for ibutsu_client tests.""" + +import json +from typing import Any +from unittest.mock import Mock + +import pytest + +from ibutsu_client.rest import RESTResponse + + +@pytest.fixture +def mock_api_client(mocker): + """Create a mock ApiClient for testing API methods.""" + from ibutsu_client.api_client import ApiClient + + client = ApiClient() + mocker.patch.object(client, "call_api") + return client + + +def create_mock_response( + data: dict[str, Any] | list[Any] | None = None, + status: int = 200, + headers: dict[str, str] | None = None, +) -> RESTResponse: + """Create a mock REST response for testing. + + Args: + data: The response data (will be JSON-encoded) + status: HTTP status code + headers: Response headers + + Returns: + A mock RESTResponse object + """ + if headers is None: + headers = {"Content-Type": "application/json; charset=utf-8"} + + if data is None: + data = {} + + response = Mock(spec=RESTResponse) + response.status = status + response.headers = headers + response.data = json.dumps(data).encode("utf-8") + response.reason = "OK" if status < 400 else "Error" + + def mock_read(): + """Mock the read method to decode the response data.""" + return response.data + + def mock_getheader(name: str, default: str | None = None) -> str | None: + """Mock the getheader method.""" + return headers.get(name, default) + + def mock_getheaders() -> dict[str, str]: + """Mock the getheaders method.""" + return headers + + response.read = mock_read + response.getheader = mock_getheader + response.getheaders = mock_getheaders + return response + + +@pytest.fixture +def mock_rest_response(): + """Fixture that provides the create_mock_response function.""" + return create_mock_response diff --git a/test/test_health.py b/test/test_health.py index 4563a25..1c7a995 100644 --- a/test/test_health.py +++ b/test/test_health.py @@ -9,43 +9,96 @@ Do not edit the class manually. """ -import unittest +import json from ibutsu_client.models.health import Health -class TestHealth(unittest.TestCase): - """Health unit test stubs""" - - def setUp(self): - pass - - def tearDown(self): - pass - - def make_instance(self, include_optional) -> Health: - """Test Health - include_optional is a boolean, when False only required - params are included, when True both required and - optional params are included""" - # uncomment below to create an instance of `Health` - """ - model = Health() - if include_optional: - return Health( - status = 'Error', - message = 'Cannot connect to database' - ) - else: - return Health( - ) - """ - - def testHealth(self): - """Test Health""" - # inst_req_only = self.make_instance(include_optional=False) - # inst_req_and_optional = self.make_instance(include_optional=True) - - -if __name__ == "__main__": - unittest.main() +class TestHealth: + """Health model tests""" + + def test_health_creation_empty(self): + """Test Health creation with no parameters""" + health = Health() + assert health.status is None + assert health.message is None + + def test_health_creation_with_status(self): + """Test Health creation with status only""" + health = Health(status="OK") + assert health.status == "OK" + assert health.message is None + + def test_health_creation_full(self): + """Test Health creation with all parameters""" + health = Health(status="Error", message="Cannot connect to database") + assert health.status == "Error" + assert health.message == "Cannot connect to database" + + def test_health_to_dict(self): + """Test Health to_dict conversion""" + health = Health(status="OK", message="Database is healthy") + health_dict = health.to_dict() + + assert isinstance(health_dict, dict) + assert health_dict["status"] == "OK" + assert health_dict["message"] == "Database is healthy" + + def test_health_to_json(self): + """Test Health to_json conversion""" + health = Health(status="Error", message="Connection failed") + health_json = health.to_json() + + assert isinstance(health_json, str) + parsed = json.loads(health_json) + assert parsed["status"] == "Error" + assert parsed["message"] == "Connection failed" + + def test_health_from_dict(self): + """Test Health from_dict creation""" + health_dict = {"status": "OK", "message": "All systems operational"} + health = Health.from_dict(health_dict) + + assert isinstance(health, Health) + assert health.status == "OK" + assert health.message == "All systems operational" + + def test_health_from_json(self): + """Test Health from_json creation""" + health_json = '{"status": "Pending", "message": "Starting up"}' + health = Health.from_json(health_json) + + assert isinstance(health, Health) + assert health.status == "Pending" + assert health.message == "Starting up" + + def test_health_to_str(self): + """Test Health to_str representation""" + health = Health(status="OK", message="Healthy") + health_str = health.to_str() + + assert isinstance(health_str, str) + assert "OK" in health_str + assert "Healthy" in health_str + + def test_health_none_values_excluded(self): + """Test that None values are excluded from dict""" + health = Health(status="OK") + health_dict = health.to_dict() + + assert "status" in health_dict + assert "message" not in health_dict + + def test_health_from_dict_none(self): + """Test Health.from_dict with None returns None""" + health = Health.from_dict(None) + assert health is None + + def test_health_roundtrip_json(self): + """Test Health can be serialized and deserialized""" + original = Health(status="Error", message="Test error") + json_str = original.to_json() + restored = Health.from_json(json_str) + + assert restored.status == original.status + assert restored.message == original.message diff --git a/test/test_health_api.py b/test/test_health_api.py index bcddf3b..f8a8a31 100644 --- a/test/test_health_api.py +++ b/test/test_health_api.py @@ -9,38 +9,112 @@ Do not edit the class manually. """ -import unittest +import pytest from ibutsu_client.api.health_api import HealthApi +from ibutsu_client.exceptions import ServiceException +from ibutsu_client.models.health import Health +from ibutsu_client.models.health_info import HealthInfo +from test.conftest import create_mock_response -class TestHealthApi(unittest.TestCase): - """HealthApi unit test stubs""" +class TestHealthApi: + """HealthApi unit tests""" - def setUp(self) -> None: - self.api = HealthApi() - - def tearDown(self) -> None: - pass - - def test_get_database_health(self) -> None: + def test_get_database_health(self, mocker): """Test case for get_database_health Get a health report for the database """ + # Mock response data + response_data = {"status": "ok", "message": "Database is healthy"} + + # Create API instance + api = HealthApi() + + # Mock the call_api method + mock_response = create_mock_response(response_data, status=200) + mocker.patch.object(api.api_client, "call_api", return_value=mock_response) + + # Call the method + result = api.get_database_health() + + # Verify the result + assert isinstance(result, Health) + assert result.status == "ok" + assert result.message == "Database is healthy" + + # Verify call_api was called + api.api_client.call_api.assert_called_once() + + def test_get_database_health_error(self, mocker): + """Test case for get_database_health with error response""" + response_data = {"status": "error", "message": "Database connection failed"} - def test_get_health(self) -> None: + api = HealthApi() + mock_response = create_mock_response(response_data, status=500) + mocker.patch.object(api.api_client, "call_api", return_value=mock_response) + + # 500 errors raise ServiceException + with pytest.raises(ServiceException) as exc_info: + api.get_database_health() + + assert exc_info.value.status == 500 + assert "error" in str(exc_info.value.body).lower() + + def test_get_health(self, mocker): """Test case for get_health Get a general health report """ + response_data = {"status": "ok", "message": "Service is healthy"} + + api = HealthApi() + mock_response = create_mock_response(response_data, status=200) + mocker.patch.object(api.api_client, "call_api", return_value=mock_response) - def test_get_health_info(self) -> None: + result = api.get_health() + + assert isinstance(result, Health) + assert result.status == "ok" + assert result.message == "Service is healthy" + + def test_get_health_info(self, mocker): """Test case for get_health_info Get information about the server """ - - -if __name__ == "__main__": - unittest.main() + response_data = { + "frontend": "https://ibutsu.example.com", + "backend": "https://api.ibutsu.example.com", + "api_ui": "https://api.ibutsu.example.com/docs", + } + + api = HealthApi() + mock_response = create_mock_response(response_data, status=200) + mocker.patch.object(api.api_client, "call_api", return_value=mock_response) + + result = api.get_health_info() + + assert isinstance(result, HealthInfo) + assert result.frontend == "https://ibutsu.example.com" + assert result.backend == "https://api.ibutsu.example.com" + assert result.api_ui == "https://api.ibutsu.example.com/docs" + + def test_get_health_info_with_http_info(self, mocker): + """Test case for get_health_info_with_http_info""" + response_data = { + "frontend": "https://ibutsu.example.com", + "backend": "https://api.ibutsu.example.com", + "api_ui": "https://api.ibutsu.example.com/docs", + } + + api = HealthApi() + mock_response = create_mock_response(response_data, status=200) + mocker.patch.object(api.api_client, "call_api", return_value=mock_response) + + result = api.get_health_info_with_http_info() + + assert result.status_code == 200 + assert isinstance(result.data, HealthInfo) + assert result.data.frontend == "https://ibutsu.example.com"