diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad99b47a..323e36250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ENHANCEMENTS: * Update the version of `super-linter` used in the `build_validation_develop` workflow to 8.7.0 ([#4957](https://github.com/microsoft/AzureTRE/issues/4957)) BUG FIXES: +* Fix Health check can falsely return OK even if Cosmos is down or inaccessible. ([#4926](https://github.com/microsoft/AzureTRE/issues/4926)) * Fix workspace deletion when backup is enabled for the base, unrestricted and airlock-import-review workspaces by adding a `delete_backups_on_uninstall` flag and a pre-teardown backup cleanup (`remove_backup.sh`) that stops protection and either deletes or retains the Recovery Services Vault, so deletion works with Azure secure-by-default soft delete ([#4962](https://github.com/microsoft/AzureTRE/issues/4962)) * Fix Nexus shared service security: fetch admin password from Key Vault at runtime via managed identity (IMDS) instead of embedding it in the VM Run Command script content. Fix `deploy_nexus_container.sh` short-circuit path to fail loudly if the container does not start. (`sonatype-nexus` 3.10.0) ([#4983](https://github.com/microsoft/AzureTRE/pull/4983)) * Fix UI TypeScript deprecation warning by updating `moduleResolution` to `bundler` in `tsconfig.json`. ([#4968](https://github.com/microsoft/AzureTRE/issues/4968)) diff --git a/api_app/_version.py b/api_app/_version.py index 7c4a9591e..025f4c5d0 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.26.0" +__version__ = "0.26.1" diff --git a/api_app/services/health_checker.py b/api_app/services/health_checker.py index a4d53067b..4a4340c9f 100644 --- a/api_app/services/health_checker.py +++ b/api_app/services/health_checker.py @@ -19,7 +19,8 @@ async def create_state_store_status() -> Tuple[StatusEnum, str]: message = "" try: container: ContainerProxy = await Database().get_container_proxy(STATE_STORE_RESOURCES_CONTAINER) - container.query_items("SELECT TOP 1 * FROM c") + async for _ in container.query_items("SELECT TOP 1 VALUE 1 FROM c", max_item_count=1): + break except exceptions.ServiceRequestError: status = StatusEnum.not_ok message = strings.STATE_STORE_ENDPOINT_NOT_RESPONDING diff --git a/api_app/tests_ma/test_services/test_health_checker.py b/api_app/tests_ma/test_services/test_health_checker.py index 6b1d955f8..8d9d23a70 100644 --- a/api_app/tests_ma/test_services/test_health_checker.py +++ b/api_app/tests_ma/test_services/test_health_checker.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest from azure.core.exceptions import ServiceRequestError +from azure.cosmos.exceptions import CosmosHttpResponseError from azure.servicebus.exceptions import ServiceBusConnectionError from mock import patch from models.schemas.status import StatusEnum @@ -11,12 +12,40 @@ pytestmark = pytest.mark.asyncio -@patch("azure.cosmos.aio.ContainerProxy.query_items", return_value=AsyncMock()) -async def test_get_state_store_status_responding(_) -> None: +def create_mock_container(query_results=None, query_error=None): + container_mock = MagicMock() + + # Mock query_items() + query_items_mock = MagicMock() + if query_error: + query_items_mock.return_value.__aiter__.side_effect = query_error + else: + query_items_mock.return_value.__aiter__.return_value = query_results or [] + + container_mock.query_items = query_items_mock + return container_mock + + +@patch("api.dependencies.database.Database.get_container_proxy") +async def test_get_state_store_status_responding(get_container_proxy_mock) -> None: + container_mock = create_mock_container(query_results=[{"id": "item"}]) + get_container_proxy_mock.return_value = container_mock status, message = await health_checker.create_state_store_status() assert status == StatusEnum.ok assert message == "" + container_mock.query_items.assert_called_once_with("SELECT TOP 1 VALUE 1 FROM c", max_item_count=1) + + +@patch("api.dependencies.database.Database.get_container_proxy") +async def test_get_state_store_status_empty_results(get_container_proxy_mock) -> None: + container_mock = create_mock_container(query_results=[]) + get_container_proxy_mock.return_value = container_mock + status, message = await health_checker.create_state_store_status() + + assert status == StatusEnum.ok + assert message == "" + container_mock.query_items.assert_called_once_with("SELECT TOP 1 VALUE 1 FROM c", max_item_count=1) @patch("api.dependencies.database.Database.get_container_proxy") @@ -39,6 +68,28 @@ async def test_get_state_store_status_other_exception(container_proxy_mock) -> N assert message == strings.UNSPECIFIED_ERROR +@patch("api.dependencies.database.Database.get_container_proxy") +async def test_get_state_store_status_cosmos_http_error(get_container_proxy_mock) -> None: + container_mock = create_mock_container(query_error=CosmosHttpResponseError(message="some message")) + get_container_proxy_mock.return_value = container_mock + status, message = await health_checker.create_state_store_status() + + assert status == StatusEnum.not_ok + assert message == strings.STATE_STORE_ENDPOINT_NOT_ACCESSIBLE + container_mock.query_items.assert_called_once_with("SELECT TOP 1 VALUE 1 FROM c", max_item_count=1) + + +@patch("api.dependencies.database.Database.get_container_proxy") +async def test_get_state_store_status_service_request_error(get_container_proxy_mock) -> None: + container_mock = create_mock_container(query_error=ServiceRequestError(message="some message")) + get_container_proxy_mock.return_value = container_mock + status, message = await health_checker.create_state_store_status() + + assert status == StatusEnum.not_ok + assert message == strings.STATE_STORE_ENDPOINT_NOT_RESPONDING + container_mock.query_items.assert_called_once_with("SELECT TOP 1 VALUE 1 FROM c", max_item_count=1) + + @patch("core.credentials.get_credential_async_context") @patch("services.health_checker.ServiceBusClient") async def test_get_service_bus_status_responding(service_bus_client_mock, get_credential_async_context) -> None: