import pytest from unittest.mock import AsyncMock, MagicMock, patch from httpx import Response from src.infrastructure.elasticsearch.client import ElasticsearchClient @pytest.fixture def es_client(): return ElasticsearchClient( url="http://localhost:9200", index="atuacapes_test", user="test_user", password="test_pass" ) @pytest.fixture def mock_httpx_response(): def _create_response(json_data, status_code=200): response = MagicMock(spec=Response) response.json.return_value = json_data response.status_code = status_code response.raise_for_status = MagicMock() return response return _create_response class TestElasticsearchClientConnect: @pytest.mark.asyncio async def test_connect_cria_cliente(self, es_client): await es_client.connect() assert es_client._client is not None await es_client.close() @pytest.mark.asyncio async def test_connect_com_auth(self, es_client): await es_client.connect() assert es_client._client is not None await es_client.close() @pytest.mark.asyncio async def test_close_fecha_cliente(self, es_client): await es_client.connect() await es_client.close() @pytest.mark.asyncio async def test_client_property_sem_conexao(self, es_client): with pytest.raises(RuntimeError, match="não conectado"): _ = es_client.client class TestBuscarPorId: @pytest.mark.asyncio async def test_buscar_por_id_encontrado(self, es_client, mock_httpx_response): es_response = { "hits": { "total": {"value": 1}, "hits": [{ "_source": { "id": 12345, "dadosPessoais": {"nome": "MARIA SILVA"}, "atuacoes": [] } }] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.buscar_por_id(12345) assert result is not None assert result["id"] == 12345 assert result["dadosPessoais"]["nome"] == "MARIA SILVA" @pytest.mark.asyncio async def test_buscar_por_id_nao_encontrado(self, es_client, mock_httpx_response): es_response = { "hits": { "total": {"value": 0}, "hits": [] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.buscar_por_id(99999) assert result is None @pytest.mark.asyncio async def test_buscar_por_id_erro(self, es_client): with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(side_effect=Exception("Connection failed")) with pytest.raises(RuntimeError, match="Erro ao buscar consultor"): await es_client.buscar_por_id(12345) class TestBuscarPorIds: @pytest.mark.asyncio async def test_buscar_por_ids_multiplos(self, es_client, mock_httpx_response): es_response = { "hits": { "hits": [ {"_source": {"id": 1, "dadosPessoais": {"nome": "PESSOA 1"}}}, {"_source": {"id": 2, "dadosPessoais": {"nome": "PESSOA 2"}}}, {"_source": {"id": 3, "dadosPessoais": {"nome": "PESSOA 3"}}} ] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.buscar_por_ids([1, 2, 3]) assert len(result) == 3 assert result[0]["id"] == 1 assert result[2]["dadosPessoais"]["nome"] == "PESSOA 3" @pytest.mark.asyncio async def test_buscar_por_ids_vazio(self, es_client): result = await es_client.buscar_por_ids([]) assert result == [] @pytest.mark.asyncio async def test_buscar_por_ids_com_source_fields(self, es_client, mock_httpx_response): es_response = {"hits": {"hits": [{"_source": {"id": 1}}]}} with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) await es_client.buscar_por_ids([1], source_fields=["id", "dadosPessoais.nome"]) call_args = mock_client.post.call_args query = call_args[1]["json"] assert "_source" in query assert "id" in query["_source"] class TestBuscarDocumentoCompleto: @pytest.mark.asyncio async def test_buscar_documento_completo(self, es_client, mock_httpx_response): es_response = { "hits": { "hits": [{ "_index": "atuacapes_test", "_id": "abc123", "_score": 1.0, "_source": {"id": 12345, "dadosPessoais": {"nome": "TESTE"}} }] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.buscar_documento_completo(12345) assert result is not None assert result["_index"] == "atuacapes_test" assert result["_id"] == "abc123" assert result["_source"]["id"] == 12345 class TestBuscarComAtuacoes: @pytest.mark.asyncio async def test_buscar_com_atuacoes(self, es_client, mock_httpx_response): es_response = { "hits": { "hits": [ {"_source": {"id": 1, "atuacoes": [{"tipo": "Consultor"}]}}, {"_source": {"id": 2, "atuacoes": [{"tipo": "Coordenação de Área de Avaliação"}]}} ] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.buscar_com_atuacoes(size=100) assert len(result) == 2 assert result[0]["atuacoes"][0]["tipo"] == "Consultor" @pytest.mark.asyncio async def test_buscar_com_atuacoes_paginacao(self, es_client, mock_httpx_response): es_response = {"hits": {"hits": []}} with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) await es_client.buscar_com_atuacoes(size=50, from_=100) call_args = mock_client.post.call_args query = call_args[1]["json"] assert query["size"] == 50 assert query["from"] == 100 class TestContarComAtuacoes: @pytest.mark.asyncio async def test_contar_com_atuacoes(self, es_client, mock_httpx_response): es_response = {"count": 85432} with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.contar_com_atuacoes() assert result == 85432 @pytest.mark.asyncio async def test_contar_com_atuacoes_erro(self, es_client): with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(side_effect=Exception("Timeout")) with pytest.raises(RuntimeError, match="Erro ao contar consultores"): await es_client.contar_com_atuacoes() class TestBuscarCandidatosRanking: @pytest.mark.asyncio async def test_buscar_candidatos_ranking(self, es_client, mock_httpx_response): es_response = { "hits": { "hits": [ {"_score": 15.5, "_source": {"id": 1, "dadosPessoais": {"nome": "COORD A"}}}, {"_score": 10.2, "_source": {"id": 2, "dadosPessoais": {"nome": "CONSULTOR B"}}} ] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.buscar_candidatos_ranking(size=100) assert len(result) == 2 assert result[0]["_score_es"] == 15.5 assert result[1]["_score_es"] == 10.2 class TestScrollAPI: @pytest.mark.asyncio async def test_iniciar_scroll(self, es_client, mock_httpx_response): es_response = { "_scroll_id": "scroll_123abc", "hits": { "total": {"value": 5000}, "hits": [ {"_source": {"id": 1}}, {"_source": {"id": 2}}, {"_source": {"id": 3}} ] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.iniciar_scroll(size=1000) assert result["scroll_id"] == "scroll_123abc" assert result["total"] == 5000 assert len(result["hits"]) == 3 @pytest.mark.asyncio async def test_continuar_scroll(self, es_client, mock_httpx_response): es_response = { "_scroll_id": "scroll_456def", "hits": { "hits": [ {"_source": {"id": 4}}, {"_source": {"id": 5}} ] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.continuar_scroll("scroll_123abc") assert result["scroll_id"] == "scroll_456def" assert len(result["hits"]) == 2 @pytest.mark.asyncio async def test_limpar_scroll(self, es_client, mock_httpx_response): with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.delete = AsyncMock(return_value=mock_httpx_response({"succeeded": True})) await es_client.limpar_scroll("scroll_123abc") mock_client.delete.assert_called_once() @pytest.mark.asyncio async def test_limpar_scroll_ignora_erros(self, es_client): with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.delete = AsyncMock(side_effect=Exception("Scroll already expired")) await es_client.limpar_scroll("scroll_expired") class TestBuscarTodosConsultores: @pytest.mark.asyncio async def test_buscar_todos_consultores_callback(self, es_client, mock_httpx_response): batches_recebidos = [] progress_recebido = [] async def callback(docs, progress): batches_recebidos.append(docs) progress_recebido.append(progress.copy()) scroll_response_1 = { "_scroll_id": "scroll_1", "hits": { "total": {"value": 6}, "hits": [ {"_source": {"id": 1}}, {"_source": {"id": 2}}, {"_source": {"id": 3}} ] } } scroll_response_2 = { "_scroll_id": "scroll_2", "hits": { "hits": [ {"_source": {"id": 4}}, {"_source": {"id": 5}}, {"_source": {"id": 6}} ] } } scroll_response_3 = { "_scroll_id": "scroll_3", "hits": {"hits": []} } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(side_effect=[ mock_httpx_response(scroll_response_1), mock_httpx_response(scroll_response_2), mock_httpx_response(scroll_response_3) ]) mock_client.delete = AsyncMock(return_value=mock_httpx_response({})) result = await es_client.buscar_todos_consultores(callback, batch_size=3) assert result["total"] == 6 assert result["processados"] == 6 assert len(batches_recebidos) == 2 assert progress_recebido[0]["percentual"] == 50 assert progress_recebido[1]["percentual"] == 100 @pytest.mark.asyncio async def test_buscar_todos_limpa_scroll_ao_final(self, es_client, mock_httpx_response): async def callback(docs, progress): pass scroll_response = { "_scroll_id": "scroll_cleanup", "hits": {"total": {"value": 0}, "hits": []} } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(scroll_response)) mock_client.delete = AsyncMock(return_value=mock_httpx_response({})) await es_client.buscar_todos_consultores(callback) mock_client.delete.assert_called_once() class TestSugerirConsultores: @pytest.mark.asyncio async def test_sugerir_consultores_por_tema(self, es_client, mock_httpx_response): es_response = { "hits": { "hits": [ {"_score": 25.5, "_source": {"id": 1, "dadosPessoais": {"nome": "ESPECIALISTA IA"}}}, {"_score": 20.1, "_source": {"id": 2, "dadosPessoais": {"nome": "PESQUISADOR ML"}}} ] } } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) result = await es_client.sugerir_consultores(tema="inteligência artificial") assert len(result) == 2 assert result[0]["_score_match"] == 25.5 @pytest.mark.asyncio async def test_sugerir_consultores_filtro_area(self, es_client, mock_httpx_response): es_response = {"hits": {"hits": []}} with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) await es_client.sugerir_consultores( tema="machine learning", area_avaliacao="CIÊNCIA DA COMPUTAÇÃO" ) call_args = mock_client.post.call_args query = call_args[1]["json"] assert len(query["query"]["bool"]["must"]) >= 2 @pytest.mark.asyncio async def test_sugerir_consultores_apenas_ativos(self, es_client, mock_httpx_response): es_response = {"hits": {"hits": []}} with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response(es_response)) await es_client.sugerir_consultores(tema="biologia", apenas_ativos=True) call_args = mock_client.post.call_args query = call_args[1]["json"] assert len(query["query"]["bool"]["must"]) >= 2 class TestListarAreasAvaliacao: @pytest.mark.asyncio async def test_listar_areas_avaliacao(self, es_client): result = await es_client.listar_areas_avaliacao() assert len(result) > 40 assert any(a["nome"] == "CIÊNCIA DA COMPUTAÇÃO" for a in result) assert any(a["nome"] == "MEDICINA I" for a in result) assert all("count" in a for a in result) class TestIntegracaoCompleta: @pytest.mark.asyncio async def test_fluxo_completo_consultor(self, es_client, mock_httpx_response): doc_consultor = { "id": 12345, "dadosPessoais": { "nome": "MARIA COORDENADORA SILVA", "nascimento": "1970-05-15" }, "atuacoes": [ { "tipo": "Coordenação de Área de Avaliação", "inicio": "01/01/2020", "fim": None, "dadosCoordenacaoArea": { "tipo": "Coordenador de Área", "areaAvaliacao": {"nome": "CIÊNCIA DA COMPUTAÇÃO"} } }, { "tipo": "Consultor", "inicio": "01/01/2015", "dadosConsultoria": { "situacaoConsultoria": "Atividade Contínua" } } ] } with patch.object(es_client, '_client') as mock_client: mock_client.is_closed = False mock_client.post = AsyncMock(return_value=mock_httpx_response({ "hits": {"total": {"value": 1}, "hits": [{"_source": doc_consultor}]} })) result = await es_client.buscar_por_id(12345) assert result["id"] == 12345 assert len(result["atuacoes"]) == 2 coord = next(a for a in result["atuacoes"] if "Coordenação" in a["tipo"]) assert coord["dadosCoordenacaoArea"]["tipo"] == "Coordenador de Área" cons = next(a for a in result["atuacoes"] if a["tipo"] == "Consultor") assert cons["dadosConsultoria"]["situacaoConsultoria"] == "Atividade Contínua"