import pytest from unittest.mock import patch, AsyncMock from src.agent.nodes.imdb_enrichment_node import imdb_enrich_ticket, should_continue from src.components.clients.exceptions.imdb_exceptions import ( ImdbClientError, ImdbHttpError, ImdbTimeoutError, ImdbConnectionError, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_state(request_context=None, session_id="sess-123"): """Build a minimal AgentState for tests.""" return { "messages": [], "current_step": "start", "iteration_count": 0, "tool_results": None, "final_response": None, "session_id": session_id, "metadata": {"request_context": request_context} if request_context else {}, "error": None, } def _valid_context(cpf_cnpj="12345678901", msisdn="5511999999999"): return { "customer": { "cpfCnpj": cpf_cnpj, "msisdn": msisdn, } } MOCK_IMDB_RESPONSE = { "plan": "TIM Black", "statusType": "Active", "statusDescription": "Acesso Full", "socialSecNo": "06252533106", } IMDB_CLIENT_PATH = "src.agent.nodes.imdb_enrichment_node.ImdbClient" # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- @pytest.mark.unit @pytest.mark.asyncio class TestImdbEnrichmentNode: # --- Happy path --------------------------------------------------------- @patch(IMDB_CLIENT_PATH) async def test_imdb_enrichment_node_success(self, mock_client_cls): """Node should store imdb_access_data in context and set step to 'imdb_enrichment_completed'.""" # Arrange mock_client = AsyncMock() mock_client.get_imdb_access_data_with_retry.return_value = AsyncMock(model_dump=lambda: MOCK_IMDB_RESPONSE) mock_client_cls.return_value = mock_client state = make_state(request_context=_valid_context()) # Act final_state = await imdb_enrich_ticket(state) # Assert assert final_state["current_step"] == "imdb_enrichment_completed" assert final_state["error"] is None imdb_data = final_state["metadata"]["request_context"]["imdb_access_data"] assert imdb_data["plan"] == "TIM Black" assert imdb_data["statusType"] == "Active" assert imdb_data["cpf_cnpj"] == "12345678901" assert imdb_data["socialSecNo"] == "06252533106" mock_client.get_imdb_access_data_with_retry.assert_called_once() # --- Validation errors -------------------------------------------------- async def test_imdb_enrichment_node_missing_context(self): """No request_context → ValidationError.""" state = make_state(request_context=None) final_state = await imdb_enrich_ticket(state) assert final_state["error"]["type"] == "ValidationError" assert final_state["error"]["step"] == "imdb_enrichment_failed" async def test_imdb_enrichment_node_missing_msisdn(self): """msisdn=None → Redirect (smart human handling).""" state = make_state(request_context=_valid_context(msisdn=None)) final_state = await imdb_enrich_ticket(state) assert final_state["current_step"] == "imdb_enrichment_completed" assert final_state["metadata"]["request_context"]["smart_human_handling"] is True # --- Client errors ------------------------------------------------------ @patch(IMDB_CLIENT_PATH) async def test_imdb_enrichment_node_http_error(self, mock_client_cls): """ImdbHttpError → error.type == 'ImdbHttpError', step == 'imdb_enrichment_failed'.""" mock_client = AsyncMock() mock_client.get_imdb_access_data_with_retry.side_effect = ImdbHttpError(404, "Not Found") mock_client_cls.return_value = mock_client final_state = await imdb_enrich_ticket(make_state(request_context=_valid_context())) assert final_state["error"]["type"] == "ImdbHttpError" assert final_state["current_step"] == "imdb_enrichment_failed" @patch(IMDB_CLIENT_PATH) async def test_imdb_enrichment_node_timeout_error(self, mock_client_cls): """ImdbTimeoutError → error.type == 'ImdbTimeoutError'.""" mock_client = AsyncMock() mock_client.get_imdb_access_data_with_retry.side_effect = ImdbTimeoutError("url", 5.0) mock_client_cls.return_value = mock_client final_state = await imdb_enrich_ticket(make_state(request_context=_valid_context())) assert final_state["error"]["type"] == "ImdbTimeoutError" assert final_state["current_step"] == "imdb_enrichment_failed" @patch(IMDB_CLIENT_PATH) async def test_imdb_enrichment_node_connection_error(self, mock_client_cls): """ImdbConnectionError → error.type == 'ImdbConnectionError'.""" mock_client = AsyncMock() mock_client.get_imdb_access_data_with_retry.side_effect = ImdbConnectionError("url", "Connection refused") mock_client_cls.return_value = mock_client final_state = await imdb_enrich_ticket(make_state(request_context=_valid_context())) assert final_state["error"]["type"] == "ImdbConnectionError" assert final_state["current_step"] == "imdb_enrichment_failed" @patch(IMDB_CLIENT_PATH) async def test_imdb_enrichment_node_no_content_response(self, mock_client_cls): """204 No Content (None response) → step == 'imdb_enrichment_completed'.""" mock_client = AsyncMock() mock_client.get_imdb_access_data_with_retry.return_value = None mock_client_cls.return_value = mock_client final_state = await imdb_enrich_ticket(make_state(request_context=_valid_context())) assert final_state["current_step"] == "imdb_enrichment_completed" assert final_state["error"] is None context = final_state["metadata"]["request_context"] assert "imdb_access_data" in context assert context["imdb_access_data"]["plan"] is None assert context["imdb_access_data"]["status_code"] == 204 @patch(IMDB_CLIENT_PATH) async def test_imdb_enrichment_node_generic_client_error(self, mock_client_cls): """Generic ImdbClientError → error.type == 'ImdbClientError'.""" mock_client = AsyncMock() mock_client.get_imdb_access_data_with_retry.side_effect = ImdbClientError("Unexpected error") mock_client_cls.return_value = mock_client final_state = await imdb_enrich_ticket(make_state(request_context=_valid_context())) assert final_state["error"]["type"] == "ImdbClientError" assert final_state["current_step"] == "imdb_enrichment_failed" # --------------------------------------------------------------------------- # Router tests # --------------------------------------------------------------------------- @pytest.mark.unit class TestShouldContinue: def test_should_continue_when_imdb_enrichment_completed(self): state = make_state() state["current_step"] = "imdb_enrichment_completed" assert should_continue(state) == "continue" def test_should_end_when_imdb_enrichment_failed(self): state = make_state() state["current_step"] = "imdb_enrichment_failed" assert should_continue(state) == "failed" def test_should_end_on_unknown_step(self): state = make_state() state["current_step"] = "some_other_step" assert should_continue(state) == "failed"