diff --git a/src/services/opengin_service.py b/src/services/opengin_service.py index cf18fc3..399b09a 100644 --- a/src/services/opengin_service.py +++ b/src/services/opengin_service.py @@ -95,6 +95,8 @@ async def fetch_relation(self, entityId: str, relation: Relation): raise BadRequestError(f"Read API Error: Bad request for id {entityId}") response.raise_for_status() data = await response.json() + if data is None: + return [] result = [Relation.model_validate(item) for item in data] return result diff --git a/src/services/organisation_service.py b/src/services/organisation_service.py index 35db10a..9abf448 100644 --- a/src/services/organisation_service.py +++ b/src/services/organisation_service.py @@ -199,6 +199,13 @@ async def active_portfolio_list(self, president_id: str, selected_date: str): raise BadRequestError("Selected date is required") try: + try: + await self.opengin_service.get_entities( + entity=Entity(id=president_id) + ) + except NotFoundError as e: + raise NotFoundError("President not found for the given ID") from e + # First retrieve the relation list of the active portfolios under given president and given date relation = Relation(name=RelationNameEnum.AS_MINISTER.value,activeAt=Util.normalize_timestamp(selected_date),direction=RelationDirectionEnum.OUTGOING.value) activePortfolioList = await self.opengin_service.fetch_relation( @@ -206,6 +213,20 @@ async def active_portfolio_list(self, president_id: str, selected_date: str): relation=relation ) + # A valid president may have no active portfolio relations for the + # selected date. Normalize a nullable upstream response and return + # the standard empty response without attempting portfolio processing. + activePortfolioList = activePortfolioList or [] + if not activePortfolioList: + return { + "NoOfCabinetMinistries": 0, + "NoOfStateMinistries": 0, + "newMinistries": 0, + "newMinisters": 0, + "ministriesUnderPresident": 0, + "portfolioList": [], + } + # Process each portfolio item in parallel results =await asyncio.gather(*[ self.process_portfolio_item(portfolio, president_id, selected_date) @@ -226,7 +247,7 @@ async def active_portfolio_list(self, president_id: str, selected_date: str): else: successful_portfolios.append(results[i]) - if len(exceptions) == len(results): + if results and len(exceptions) == len(results): raise InternalServerError("Failed to process all portfolios") # Calculate final counts diff --git a/test/test_opengin_service.py b/test/test_opengin_service.py index b844230..1ee3850 100644 --- a/test/test_opengin_service.py +++ b/test/test_opengin_service.py @@ -119,6 +119,24 @@ async def test_fetch_relation_success(mock_service, mock_session): assert result == [Relation(id="relation_123",relationName=RelationNameEnum.AS_MINISTER.value,direction=RelationDirectionEnum.OUTGOING.value)] mock_session.post.assert_called_once() +@pytest.mark.asyncio +async def test_fetch_relation_none_response_returns_empty_list( + mock_service, mock_session +): + entity_id = "entity_123" + mock_session.post.return_value = MockResponse(None) + + result = await mock_service.fetch_relation( + entity_id, + relation=Relation( + name=RelationNameEnum.AS_MINISTER.value, + direction=RelationDirectionEnum.OUTGOING.value, + ), + ) + + assert result == [] + mock_session.post.assert_called_once() + @pytest.mark.asyncio async def test_fetch_relation_empty_entity_id(mock_service, mock_session): entity_id = "" @@ -372,4 +390,3 @@ async def test_get_attributes_bad_request(mock_service, mock_session): with pytest.raises(BadRequestError, match="Bad request"): await mock_service.get_attributes("category_123", "dataset_abc") - diff --git a/test/test_organisation_service.py b/test/test_organisation_service.py index ae33c7e..1df896d 100644 --- a/test/test_organisation_service.py +++ b/test/test_organisation_service.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import AsyncMock, patch, MagicMock from src.enums import EntityIdEnum, RelationDirectionEnum, RelationNameEnum -from src.exception import BadRequestError, InternalServerError +from src.exception import BadRequestError, InternalServerError, NotFoundError from src.models import Entity, Relation from src.utils import Util @@ -196,6 +196,129 @@ async def test_enrich_department_item_not_new( ) +@pytest.mark.asyncio +async def test_active_portfolio_list_invalid_president_id( + organisation_service, mock_opengin_service +): + president_id = "invalid_president_123" + selected_date = "2021-10-27" + + mock_opengin_service.get_entities.side_effect = NotFoundError("President not found") + + with pytest.raises(NotFoundError): + await organisation_service.active_portfolio_list( + president_id=president_id, selected_date=selected_date + ) + + mock_opengin_service.get_entities.assert_called_once_with( + entity=Entity(id=president_id) + ) + + +@pytest.mark.asyncio +async def test_active_portfolio_list_valid_president_id( + organisation_service, mock_opengin_service +): + president_id = "president_123" + selected_date = "2021-10-27" + + mock_opengin_service.get_entities.return_value = [ + Entity(id=president_id, name="mocked_protobuf_name") + ] + mock_opengin_service.fetch_relation.return_value = [ + Relation( + id="portfolio_relation_123", + relatedEntityId="portfolio_123", + name=RelationNameEnum.AS_MINISTER.value, + startTime="2020-08-09T00:00:00Z", + endTime="2022-03-08T00:00:00Z", + direction=RelationDirectionEnum.OUTGOING.value, + ) + ] + + with patch( + "src.services.organisation_service.OrganisationService.process_portfolio_item", + new_callable=AsyncMock, + ) as mock_process_portfolio_item: + mock_process_portfolio_item.return_value = { + "id": "portfolio_123", + "name": "Portfolio X", + "type": "cabinetMinister", + "isNew": False, + "ministers": [], + } + + result = await organisation_service.active_portfolio_list( + president_id=president_id, selected_date=selected_date + ) + + assert result == { + "NoOfCabinetMinistries": 1, + "NoOfStateMinistries": 0, + "newMinistries": 0, + "newMinisters": 0, + "ministriesUnderPresident": 0, + "portfolioList": [ + { + "id": "portfolio_123", + "name": "Portfolio X", + "type": "cabinetMinister", + "isNew": False, + "ministers": [], + } + ], + } + + mock_opengin_service.get_entities.assert_called_once_with( + entity=Entity(id=president_id) + ) + mock_opengin_service.fetch_relation.assert_called_once_with( + entityId=president_id, + relation=Relation( + name=RelationNameEnum.AS_MINISTER.value, + activeAt=f"{selected_date}T00:00:00Z", + direction=RelationDirectionEnum.OUTGOING.value, + ), + ) + mock_process_portfolio_item.assert_called_once_with( + mock_opengin_service.fetch_relation.return_value[0], + president_id, + selected_date, + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("relations", [None, []]) +async def test_active_portfolio_list_valid_president_without_active_relations( + organisation_service, mock_opengin_service, relations +): + president_id = "president_123" + selected_date = "2021-10-27" + + mock_opengin_service.get_entities.return_value = [ + Entity(id=president_id, name="mocked_protobuf_name") + ] + mock_opengin_service.fetch_relation.return_value = relations + + with patch( + "src.services.organisation_service.OrganisationService.process_portfolio_item", + new_callable=AsyncMock, + ) as mock_process_portfolio_item: + result = await organisation_service.active_portfolio_list( + president_id=president_id, selected_date=selected_date + ) + + assert result == { + "NoOfCabinetMinistries": 0, + "NoOfStateMinistries": 0, + "newMinistries": 0, + "newMinisters": 0, + "ministriesUnderPresident": 0, + "portfolioList": [], + } + mock_process_portfolio_item.assert_not_awaited() + + @pytest.mark.asyncio async def test_departments_by_portfolio_id_success( organisation_service, mock_opengin_service