diff --git a/CHANGELOG.md b/CHANGELOG.md index c4587044e4..9b188ddc34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ENHANCEMENTS: BUG FIXES: * Fix error handling in airlock processor ([#4929](https://github.com/microsoft/AzureTRE/pull/4929)) +* Fix Health check can falsely return OK even if Cosmos is down or inaccessible. ([#4926](https://github.com/microsoft/AzureTRE/issues/4926)) ## (0.28.0) (March 2, 2026) **BREAKING CHANGES** diff --git a/api_app/_version.py b/api_app/_version.py index 7923a95d33..57a322dfc2 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.25.16" +__version__ = "0.25.17" diff --git a/api_app/services/health_checker.py b/api_app/services/health_checker.py index a4d53067b0..34139e4df9 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 * 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 6b1d955f89..c166514067 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,8 +12,36 @@ pytestmark = pytest.mark.asyncio -@patch("azure.cosmos.aio.ContainerProxy.query_items", return_value=AsyncMock()) -async def test_get_state_store_status_responding(_) -> None: +class AsyncIterator: + def __init__(self, seq): + self.iter = iter(seq) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self.iter) + except StopIteration: + raise StopAsyncIteration + + +class AsyncIteratorWithError: + def __init__(self, exception): + self.exception = exception + + def __aiter__(self): + return self + + async def __anext__(self): + raise self.exception + + +@patch("api.dependencies.database.Database.get_container_proxy") +async def test_get_state_store_status_responding(get_container_proxy_mock) -> None: + container_mock = MagicMock() + container_mock.query_items.return_value = AsyncIterator([{"id": "item"}]) + get_container_proxy_mock.return_value = container_mock status, message = await health_checker.create_state_store_status() assert status == StatusEnum.ok @@ -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 = MagicMock() + container_mock.query_items.return_value = AsyncIteratorWithError(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 + + +@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 = MagicMock() + container_mock.query_items.return_value = AsyncIteratorWithError(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 + + @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: @@ -127,17 +178,3 @@ async def test_get_resource_processor_status_other_exception(resource_processor_ assert status == StatusEnum.not_ok assert message == strings.UNSPECIFIED_ERROR - - -class AsyncIterator: - def __init__(self, seq): - self.iter = iter(seq) - - def __aiter__(self): - return self - - async def __anext__(self): - try: - return next(self.iter) - except StopIteration: - raise StopAsyncIteration