- 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
509 lines
18 KiB
Python
509 lines
18 KiB
Python
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"
|