feat(tests): adicionar suite completa de testes automatizados
- 198 testes cobrindo todos os módulos do backend - Testes unitários para calculador de pontuação (56 testes) - Testes para value objects de período (23 testes) - Testes para cliente Elasticsearch com mocks (27 testes) - Testes para repository de consultores (48 testes) - Testes de integração ES + Repository (6 testes) - Testes para API routes FastAPI (23 testes) - Testes para job de processamento (16 testes) - Cobertura de 54% do código
This commit is contained in:
508
backend/tests/infrastructure/elasticsearch/test_client.py
Normal file
508
backend/tests/infrastructure/elasticsearch/test_client.py
Normal file
@@ -0,0 +1,508 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user