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:
Frederico Castro
2025-12-29 08:06:08 -03:00
parent e0692ee49c
commit 143ec401f5
19 changed files with 2899 additions and 0 deletions

View File

View File

View File

@@ -0,0 +1,278 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime
from src.application.jobs.processar_ranking import ProcessarRankingJob
from src.infrastructure.ranking_store import RankingEntry
@pytest.fixture
def mock_es_client():
client = AsyncMock()
client.contar_com_atuacoes.return_value = 100
client.buscar_todos_consultores.return_value = {"processados": 100, "batches": 10}
return client
@pytest.fixture
def mock_ranking_store():
store = AsyncMock()
store.set_entries = AsyncMock()
return store
@pytest.fixture
def mock_oracle_repo():
repo = MagicMock()
repo.limpar_tabela = MagicMock()
repo.inserir_batch = MagicMock()
repo.atualizar_posicoes = MagicMock()
return repo
@pytest.fixture
def job(mock_es_client, mock_ranking_store, mock_oracle_repo):
return ProcessarRankingJob(
es_client=mock_es_client,
ranking_store=mock_ranking_store,
ranking_oracle_repo=mock_oracle_repo,
)
class TestGerarEntriesOrdenadas:
def test_lista_vazia(self):
entries = ProcessarRankingJob._gerar_entries_ordenadas([])
assert len(entries) == 0
def test_ordenacao_por_pontuacao(self):
consultores = [
{"id_pessoa": 1, "nome": "A", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5},
{"id_pessoa": 2, "nome": "B", "pontuacao_total": 300, "bloco_a": 150, "bloco_b": 80, "bloco_c": 50, "bloco_d": 10, "bloco_e": 10, "ativo": True, "anos_atuacao": 10},
{"id_pessoa": 3, "nome": "C", "pontuacao_total": 200, "bloco_a": 100, "bloco_b": 50, "bloco_c": 30, "bloco_d": 10, "bloco_e": 10, "ativo": False, "anos_atuacao": 3},
]
entries = ProcessarRankingJob._gerar_entries_ordenadas(consultores)
assert len(entries) == 3
assert entries[0].id_pessoa == 2
assert entries[0].posicao == 1
assert entries[0].pontuacao_total == 300
assert entries[1].id_pessoa == 3
assert entries[1].posicao == 2
assert entries[2].id_pessoa == 1
assert entries[2].posicao == 3
def test_posicoes_atribuidas_sequencialmente(self):
consultores = [
{"id_pessoa": i, "nome": f"C{i}", "pontuacao_total": 100 - i, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5}
for i in range(1, 11)
]
entries = ProcessarRankingJob._gerar_entries_ordenadas(consultores)
for i, entry in enumerate(entries, start=1):
assert entry.posicao == i
def test_empate_desempata_por_id(self):
consultores = [
{"id_pessoa": 10, "nome": "A", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5},
{"id_pessoa": 5, "nome": "B", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5},
]
entries = ProcessarRankingJob._gerar_entries_ordenadas(consultores)
assert entries[0].id_pessoa == 5
assert entries[1].id_pessoa == 10
class TestObterEstatisticas:
def test_lista_vazia(self):
stats = ProcessarRankingJob._obter_estatisticas([])
assert stats["total_consultores"] == 0
assert stats["pontuacao_media"] == 0
assert stats["pontuacao_maxima"] == 0
def test_estatisticas_calculadas(self):
entries = [
RankingEntry(id_pessoa=1, nome="A", posicao=1, pontuacao_total=300, bloco_a=100, bloco_b=80, bloco_c=60, bloco_d=40, bloco_e=20, ativo=True, anos_atuacao=5, detalhes={}),
RankingEntry(id_pessoa=2, nome="B", posicao=2, pontuacao_total=200, bloco_a=80, bloco_b=50, bloco_c=40, bloco_d=20, bloco_e=10, ativo=True, anos_atuacao=3, detalhes={}),
RankingEntry(id_pessoa=3, nome="C", posicao=3, pontuacao_total=100, bloco_a=40, bloco_b=30, bloco_c=20, bloco_d=5, bloco_e=5, ativo=False, anos_atuacao=2, detalhes={}),
]
stats = ProcessarRankingJob._obter_estatisticas(entries)
assert stats["total_consultores"] == 3
assert stats["total_ativos"] == 2
assert stats["total_inativos"] == 1
assert stats["pontuacao_maxima"] == 300
assert stats["pontuacao_minima"] == 100
assert stats["pontuacao_media"] == 200.0
def test_media_componentes(self):
entries = [
RankingEntry(id_pessoa=1, nome="A", posicao=1, pontuacao_total=300, bloco_a=100, bloco_b=50, bloco_c=80, bloco_d=40, bloco_e=30, ativo=True, anos_atuacao=5, detalhes={}),
RankingEntry(id_pessoa=2, nome="B", posicao=2, pontuacao_total=200, bloco_a=80, bloco_b=30, bloco_c=60, bloco_d=20, bloco_e=10, ativo=True, anos_atuacao=3, detalhes={}),
]
stats = ProcessarRankingJob._obter_estatisticas(entries)
assert stats["media_componentes"]["a"] == 90.0
assert stats["media_componentes"]["b"] == 40.0
class TestGerarJsonDetalhes:
def test_gerar_json_consultor_completo(self, job):
from src.domain.entities.consultor import Consultor, CoordenacaoCapes, Consultoria
from src.domain.value_objects.periodo import Periodo
from src.domain.value_objects.pontuacao import PontuacaoBloco, PontuacaoCompleta
periodo = Periodo(inicio=datetime(2020, 1, 1), fim=None)
consultor = Consultor(
id_pessoa=123,
nome="João Silva",
coordenacoes_capes=[
CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="CIÊNCIAS",
periodo=periodo,
)
],
consultoria=Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=periodo,
anos_consecutivos=5,
retornos=0,
),
)
consultor.pontuacao = PontuacaoCompleta(
bloco_a=PontuacaoBloco(bloco="A", atuacoes=[]),
bloco_b=PontuacaoBloco(bloco="B", atuacoes=[]),
bloco_c=PontuacaoBloco(bloco="C", atuacoes=[]),
bloco_d=PontuacaoBloco(bloco="D", atuacoes=[]),
bloco_e=PontuacaoBloco(bloco="E", atuacoes=[]),
)
result = job._gerar_json_detalhes(consultor)
assert result["id_pessoa"] == 123
assert result["nome"] == "João Silva"
assert len(result["coordenacoes_capes"]) == 1
assert result["consultoria"] is not None
assert result["consultoria"]["codigo"] == "CONS_ATIVO"
def test_gerar_json_consultor_sem_consultoria(self, job):
from src.domain.entities.consultor import Consultor
from src.domain.value_objects.pontuacao import PontuacaoBloco, PontuacaoCompleta
consultor = Consultor(
id_pessoa=456,
nome="Maria Santos",
)
consultor.pontuacao = PontuacaoCompleta(
bloco_a=PontuacaoBloco(bloco="A", atuacoes=[]),
bloco_b=PontuacaoBloco(bloco="B", atuacoes=[]),
bloco_c=PontuacaoBloco(bloco="C", atuacoes=[]),
bloco_d=PontuacaoBloco(bloco="D", atuacoes=[]),
bloco_e=PontuacaoBloco(bloco="E", atuacoes=[]),
)
result = job._gerar_json_detalhes(consultor)
assert result["id_pessoa"] == 456
assert result["consultoria"] is None
class TestProcessarBatch:
@pytest.mark.asyncio
async def test_processar_batch_atualiza_progresso(self, job):
with patch.object(job.consultor_repo, "_construir_consultor") as mock_construir:
from src.domain.entities.consultor import Consultor
from src.domain.value_objects.pontuacao import PontuacaoBloco, PontuacaoCompleta
consultor = Consultor(id_pessoa=1, nome="Test")
consultor.pontuacao = PontuacaoCompleta(
bloco_a=PontuacaoBloco(bloco="A", atuacoes=[]),
bloco_b=PontuacaoBloco(bloco="B", atuacoes=[]),
bloco_c=PontuacaoBloco(bloco="C", atuacoes=[]),
bloco_d=PontuacaoBloco(bloco="D", atuacoes=[]),
bloco_e=PontuacaoBloco(bloco="E", atuacoes=[]),
)
mock_construir.return_value = consultor
docs = [{"id": 1, "dadosPessoais": {"nome": "Test"}, "atuacoes": []}]
progress = {"processados": 1, "batch_atual": 1, "percentual": 10}
with patch("src.application.jobs.processar_ranking.job_status"):
await job._processar_batch(docs, progress)
assert len(job._consultores) == 1
class TestExecutar:
@pytest.mark.asyncio
async def test_executar_job_ja_rodando(self, job):
with patch("src.application.jobs.processar_ranking.job_status") as mock_status:
mock_status.is_running = True
with pytest.raises(RuntimeError, match="Job já está em execução"):
await job.executar()
@pytest.mark.asyncio
async def test_executar_sucesso(self, job, mock_es_client, mock_ranking_store):
with patch("src.application.jobs.processar_ranking.job_status") as mock_status:
mock_status.is_running = False
mock_status.iniciar = MagicMock()
mock_status.atualizar_progresso = MagicMock()
mock_status.finalizar = MagicMock()
mock_status.mensagem = ""
mock_status.tempo_decorrido = 10.5
job._consultores = [
{"id_pessoa": 1, "nome": "Test", "pontuacao_total": 100, "bloco_a": 50, "bloco_b": 20, "bloco_c": 20, "bloco_d": 5, "bloco_e": 5, "ativo": True, "anos_atuacao": 5}
]
resultado = await job.executar()
assert resultado["sucesso"] is True
mock_status.iniciar.assert_called_once()
mock_status.finalizar.assert_called_once_with(sucesso=True)
@pytest.mark.asyncio
async def test_executar_erro(self, job, mock_es_client):
with patch("src.application.jobs.processar_ranking.job_status") as mock_status:
mock_status.is_running = False
mock_status.iniciar = MagicMock()
mock_status.finalizar = MagicMock()
mock_es_client.contar_com_atuacoes.side_effect = Exception("ES Error")
with pytest.raises(RuntimeError):
await job.executar()
mock_status.finalizar.assert_called_once()
args = mock_status.finalizar.call_args
assert args[1]["sucesso"] is False
class TestPersistirOracle:
@pytest.mark.asyncio
async def test_persistir_oracle_limpa_antes(self, job, mock_oracle_repo):
consultores = [
{"id_pessoa": 1, "nome": "Test", "pontuacao_total": 100, "posicao": 1}
]
await job._persistir_oracle(consultores, limpar_antes=True)
mock_oracle_repo.limpar_tabela.assert_called_once()
mock_oracle_repo.inserir_batch.assert_called()
mock_oracle_repo.atualizar_posicoes.assert_called_once()
@pytest.mark.asyncio
async def test_persistir_oracle_sem_limpar(self, job, mock_oracle_repo):
consultores = [
{"id_pessoa": 1, "nome": "Test", "pontuacao_total": 100, "posicao": 1}
]
await job._persistir_oracle(consultores, limpar_antes=False)
mock_oracle_repo.limpar_tabela.assert_not_called()
@pytest.mark.asyncio
async def test_persistir_oracle_em_batches(self, job, mock_oracle_repo):
consultores = [{"id_pessoa": i, "nome": f"Test{i}", "pontuacao_total": 100} for i in range(5000)]
await job._persistir_oracle(consultores, limpar_antes=True)
assert mock_oracle_repo.inserir_batch.call_count == 3

294
backend/tests/conftest.py Normal file
View File

@@ -0,0 +1,294 @@
import pytest
from datetime import datetime
from dateutil.relativedelta import relativedelta
from src.domain.entities.consultor import (
Consultor,
CoordenacaoCapes,
Consultoria,
Inscricao,
AvaliacaoComissao,
Premiacao,
BolsaCNPQ,
Participacao,
Orientacao,
MembroBanca,
)
from src.domain.value_objects.periodo import Periodo
@pytest.fixture
def data_referencia():
return datetime(2025, 1, 1)
@pytest.fixture
def hoje():
return datetime.now()
def criar_periodo(anos_atras: int, ativo: bool = False, duracao_anos: int = 0) -> Periodo:
inicio = datetime.now() - relativedelta(years=anos_atras)
if ativo:
fim = None
else:
fim = inicio + relativedelta(years=duracao_anos) if duracao_anos > 0 else datetime.now()
return Periodo(inicio=inicio, fim=fim)
@pytest.fixture
def periodo_ativo_5_anos():
return criar_periodo(anos_atras=5, ativo=True)
@pytest.fixture
def periodo_historico_3_anos():
return criar_periodo(anos_atras=5, duracao_anos=3)
@pytest.fixture
def periodo_historico_10_anos():
return criar_periodo(anos_atras=12, duracao_anos=10)
@pytest.fixture
def coordenacao_ca_ativa(periodo_ativo_5_anos):
return CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="CIÊNCIAS AMBIENTAIS",
periodo=periodo_ativo_5_anos,
)
@pytest.fixture
def coordenacao_ca_historica(periodo_historico_3_anos):
return CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="MEDICINA I",
periodo=periodo_historico_3_anos,
)
@pytest.fixture
def coordenacao_caj_ativa():
return CoordenacaoCapes(
codigo="CAJ",
tipo="Coordenador Adjunto",
area_avaliacao="ENGENHARIA I",
periodo=criar_periodo(anos_atras=3, ativo=True),
)
@pytest.fixture
def coordenacao_caj_mp_historica():
return CoordenacaoCapes(
codigo="CAJ_MP",
tipo="Coordenador Adjunto de Mestrado Profissionalizante",
area_avaliacao="ADMINISTRAÇÃO",
periodo=criar_periodo(anos_atras=6, duracao_anos=4),
)
@pytest.fixture
def coordenacao_cam_ativa():
return CoordenacaoCapes(
codigo="CAM",
tipo="Câmara Temática",
area_avaliacao="INTERDISCIPLINAR",
periodo=criar_periodo(anos_atras=2, ativo=True),
)
@pytest.fixture
def consultoria_ativa_8_anos():
return Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=8, ativo=True),
periodos=[criar_periodo(anos_atras=8, ativo=True)],
anos_consecutivos=8,
retornos=0,
)
@pytest.fixture
def consultoria_historica_5_anos():
return Consultoria(
codigo="CONS_HIST",
situacao="Inativo",
periodo=criar_periodo(anos_atras=7, duracao_anos=5),
periodos=[criar_periodo(anos_atras=7, duracao_anos=5)],
anos_consecutivos=5,
retornos=0,
)
@pytest.fixture
def consultoria_com_retorno():
periodo1 = criar_periodo(anos_atras=10, duracao_anos=3)
periodo2 = criar_periodo(anos_atras=4, ativo=True)
return Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=periodo2,
periodos=[periodo1, periodo2],
anos_consecutivos=4,
retornos=1,
)
@pytest.fixture
def inscricao_autor():
return Inscricao(
codigo="INSC_AUTOR",
tipo="Autor",
premio="PCT",
ano=2024,
situacao="Aprovada",
)
@pytest.fixture
def inscricao_institucional():
return Inscricao(
codigo="INSC_INST_AUTOR",
tipo="Institucional",
premio="PCT",
ano=2024,
situacao="Aprovada",
)
@pytest.fixture
def avaliacao_comissao_premio():
return AvaliacaoComissao(
codigo="AVAL_COMIS_PREMIO",
tipo="Membro de Comissão",
premio="PCT",
ano=2024,
)
@pytest.fixture
def avaliacao_comissao_gp():
return AvaliacaoComissao(
codigo="AVAL_COMIS_GP",
tipo="Membro de Comissão",
premio="Grande Prêmio",
ano=2024,
)
@pytest.fixture
def coord_comissao_premio():
return AvaliacaoComissao(
codigo="COORD_COMIS_PREMIO",
tipo="Coordenador/Presidente",
premio="PCT",
ano=2024,
comissao_tipo="Coordenador",
)
@pytest.fixture
def premiacao_gp_autor():
return Premiacao(
codigo="PREMIACAO_GP_AUTOR",
tipo="Grande Prêmio",
nome_premio="Grande Prêmio CAPES de Tese",
ano=2024,
papel="Autor",
)
@pytest.fixture
def premiacao_autor():
return Premiacao(
codigo="PREMIACAO_AUTOR",
tipo="Prêmio",
nome_premio="Prêmio CAPES de Tese",
ano=2024,
papel="Autor",
)
@pytest.fixture
def mencao_autor():
return Premiacao(
codigo="MENCAO_AUTOR",
tipo="Menção Honrosa",
nome_premio="Prêmio CAPES de Tese",
ano=2024,
papel="Autor",
)
@pytest.fixture
def bolsa_cnpq():
return BolsaCNPQ(
codigo="BOL_BPQ_NIVEL",
nivel="1A",
area="Ciências Exatas",
)
@pytest.fixture
def participacao_evento():
return Participacao(
codigo="EVENTO",
tipo="Evento",
descricao="Seminário CAPES 2024",
ano=2024,
)
@pytest.fixture
def participacao_projeto():
return Participacao(
codigo="PROJ",
tipo="Projeto",
descricao="Projeto de Pesquisa",
ano=2024,
)
@pytest.fixture
def consultor_vazio():
return Consultor(
id_pessoa=1,
nome="Consultor Vazio",
)
@pytest.fixture
def consultor_coordenador_area(coordenacao_ca_ativa):
return Consultor(
id_pessoa=2,
nome="Coordenador de Área Ativo",
coordenacoes_capes=[coordenacao_ca_ativa],
)
@pytest.fixture
def consultor_completo(
coordenacao_ca_ativa,
consultoria_ativa_8_anos,
inscricao_autor,
avaliacao_comissao_premio,
premiacao_autor,
bolsa_cnpq,
participacao_evento,
):
return Consultor(
id_pessoa=3,
nome="Consultor Completo",
coordenacoes_capes=[coordenacao_ca_ativa],
consultoria=consultoria_ativa_8_anos,
inscricoes=[inscricao_autor],
avaliacoes_comissao=[avaliacao_comissao_premio],
premiacoes=[premiacao_autor],
bolsas_cnpq=[bolsa_cnpq],
participacoes=[participacao_evento],
)

View File

View File

@@ -0,0 +1,599 @@
import pytest
from datetime import datetime
from dateutil.relativedelta import relativedelta
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
from src.domain.entities.consultor import (
Consultor,
CoordenacaoCapes,
Consultoria,
Inscricao,
AvaliacaoComissao,
Premiacao,
BolsaCNPQ,
Participacao,
)
from src.domain.value_objects.periodo import Periodo
from src.domain.value_objects.criterios_pontuacao import CRITERIOS
def criar_periodo(anos_atras: int, ativo: bool = False, duracao_anos: int = 0) -> Periodo:
inicio = datetime.now() - relativedelta(years=anos_atras)
if ativo:
fim = None
else:
fim = inicio + relativedelta(years=duracao_anos) if duracao_anos > 0 else datetime.now()
return Periodo(inicio=inicio, fim=fim)
class TestBlocoACoordenacaoCapes:
def test_coordenacao_vazia_retorna_bloco_vazio(self):
resultado = CalculadorPontuacao.calcular_bloco_a([])
assert resultado.bloco == "A"
assert resultado.total == 0
assert len(resultado.atuacoes) == 0
def test_coordenador_area_base_200_pontos(self):
coord = CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="CIÊNCIAS AMBIENTAIS",
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
assert resultado.total >= 200
def test_coordenador_area_ativo_recebe_bonus_atualidade(self):
coord = CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="CIÊNCIAS AMBIENTAIS",
periodo=criar_periodo(anos_atras=1, ativo=True),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
atuacao = resultado.atuacoes[0]
assert atuacao.bonus >= 30
def test_coordenador_area_historico_sem_bonus_atualidade(self):
coord = CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="MEDICINA I",
periodo=criar_periodo(anos_atras=5, duracao_anos=3),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
atuacao = resultado.atuacoes[0]
assert atuacao.codigo == "CA"
assert 30 not in [atuacao.bonus] or atuacao.bonus < 30
def test_coordenador_area_5_anos_pontos_tempo(self):
coord = CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="CIÊNCIAS AMBIENTAIS",
periodo=criar_periodo(anos_atras=5, duracao_anos=5),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
atuacao = resultado.atuacoes[0]
assert atuacao.tempo == 50
def test_coordenador_area_teto_tempo_100_pontos(self):
coord = CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="CIÊNCIAS AMBIENTAIS",
periodo=criar_periodo(anos_atras=15, duracao_anos=15),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
atuacao = resultado.atuacoes[0]
assert atuacao.tempo == 100
def test_coordenador_area_teto_maximo_450(self):
coord = CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Área",
area_avaliacao="CIÊNCIAS AMBIENTAIS",
periodo=criar_periodo(anos_atras=20, ativo=True),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
atuacao = resultado.atuacoes[0]
assert atuacao.total <= 450
def test_coordenador_adjunto_base_150_pontos(self):
coord = CoordenacaoCapes(
codigo="CAJ",
tipo="Coordenador Adjunto",
area_avaliacao="ENGENHARIA I",
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
assert resultado.total >= 150
def test_coordenador_adjunto_mp_base_120_pontos(self):
coord = CoordenacaoCapes(
codigo="CAJ_MP",
tipo="Coordenador Adjunto de Mestrado Profissionalizante",
area_avaliacao="ADMINISTRAÇÃO",
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
assert resultado.total >= 120
def test_camara_tematica_base_100_pontos(self):
coord = CoordenacaoCapes(
codigo="CAM",
tipo="Câmara Temática",
area_avaliacao="INTERDISCIPLINAR",
periodo=criar_periodo(anos_atras=0, duracao_anos=0),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
assert resultado.total >= 100
def test_multiplas_coordenacoes_mesmo_tipo_soma_tempo(self):
periodo1 = criar_periodo(anos_atras=10, duracao_anos=3)
periodo2 = criar_periodo(anos_atras=5, duracao_anos=3)
coords = [
CoordenacaoCapes(
codigo="CA", tipo="Coordenador de Área",
area_avaliacao="ÁREA 1", periodo=periodo1
),
CoordenacaoCapes(
codigo="CA", tipo="Coordenador de Área",
area_avaliacao="ÁREA 2", periodo=periodo2
),
]
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
assert len(resultado.atuacoes) == 1
atuacao = resultado.atuacoes[0]
assert atuacao.quantidade == 2
assert atuacao.tempo == 60
def test_retorno_coordenacao_gera_bonus(self):
periodo1 = criar_periodo(anos_atras=10, duracao_anos=3)
periodo2 = criar_periodo(anos_atras=3, ativo=True)
coords = [
CoordenacaoCapes(
codigo="CA", tipo="Coordenador de Área",
area_avaliacao="ÁREA 1", periodo=periodo1
),
CoordenacaoCapes(
codigo="CA", tipo="Coordenador de Área",
area_avaliacao="ÁREA 1", periodo=periodo2
),
]
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
atuacao = resultado.atuacoes[0]
assert atuacao.bonus >= 20
def test_tipos_diferentes_geram_atuacoes_separadas(self):
coords = [
CoordenacaoCapes(
codigo="CA", tipo="Coordenador de Área",
area_avaliacao="ÁREA 1", periodo=criar_periodo(2, duracao_anos=2)
),
CoordenacaoCapes(
codigo="CAJ", tipo="Coordenador Adjunto",
area_avaliacao="ÁREA 1", periodo=criar_periodo(2, duracao_anos=2)
),
]
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
assert len(resultado.atuacoes) == 2
def test_codigo_com_hifen_normalizado(self):
coord = CoordenacaoCapes(
codigo="CAJ-MP",
tipo="Coordenador Adjunto de Mestrado Profissionalizante",
area_avaliacao="ADMINISTRAÇÃO",
periodo=criar_periodo(anos_atras=2, duracao_anos=2),
)
resultado = CalculadorPontuacao.calcular_bloco_a([coord])
assert len(resultado.atuacoes) == 1
assert resultado.atuacoes[0].codigo == "CAJ_MP"
class TestBlocoBConsultoria:
def test_consultoria_vazia_retorna_bloco_vazio(self):
resultado = CalculadorPontuacao.calcular_bloco_b(None)
assert resultado.bloco == "B"
assert resultado.total == 0
def test_consultor_ativo_base_150_pontos(self):
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=1, ativo=True),
anos_consecutivos=1,
retornos=0,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
assert resultado.total >= 150
def test_consultor_historico_base_100_pontos(self):
consultoria = Consultoria(
codigo="CONS_HIST",
situacao="Inativo",
periodo=criar_periodo(anos_atras=5, duracao_anos=3),
anos_consecutivos=3,
retornos=0,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
assert resultado.total >= 100
def test_consultor_falecido_base_100_pontos(self):
consultoria = Consultoria(
codigo="CONS_FALECIDO",
situacao="Falecido",
periodo=criar_periodo(anos_atras=10, duracao_anos=8),
anos_consecutivos=8,
retornos=0,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
assert resultado.total >= 100
def test_consultoria_5_anos_pontos_tempo(self):
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=5, ativo=True),
periodos=[criar_periodo(anos_atras=5, ativo=True)],
anos_consecutivos=5,
retornos=0,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.tempo == 25
def test_consultoria_teto_tempo_50_pontos(self):
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=15, ativo=True),
periodos=[criar_periodo(anos_atras=15, ativo=True)],
anos_consecutivos=15,
retornos=0,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.tempo == 50
def test_consultor_ativo_bonus_atualidade_20(self):
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=1, ativo=True),
anos_consecutivos=1,
retornos=0,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.bonus >= 20
def test_consultor_8_anos_bonus_continuidade(self):
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=8, ativo=True),
anos_consecutivos=8,
retornos=0,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.bonus >= 40
def test_consultor_com_retorno_bonus_15(self):
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=2, ativo=True),
periodos=[
criar_periodo(anos_atras=8, duracao_anos=3),
criar_periodo(anos_atras=2, ativo=True),
],
anos_consecutivos=2,
retornos=1,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.bonus >= 35
def test_consultoria_teto_maximo_230(self):
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Atividade Contínua",
periodo=criar_periodo(anos_atras=20, ativo=True),
periodos=[criar_periodo(anos_atras=20, ativo=True)],
anos_consecutivos=20,
retornos=1,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.total <= 230
class TestBlocoCPremiacoesAvaliacoes:
def test_bloco_c_vazio(self):
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [], [], [])
assert resultado.bloco == "C"
assert resultado.total == 0
def test_inscricao_autor_base_10_pontos(self):
inscricao = Inscricao(
codigo="INSC_AUTOR", tipo="Autor",
premio="PCT", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([inscricao], [], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR"), None)
assert atuacao is not None
assert atuacao.base >= 10
def test_inscricao_institucional_base_20_pontos(self):
inscricao = Inscricao(
codigo="INSC_INST_AUTOR", tipo="Institucional",
premio="PCT", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([inscricao], [], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_INST_AUTOR"), None)
assert atuacao is not None
assert atuacao.base >= 20
def test_avaliacao_comissao_premio_base_30(self):
avaliacao = AvaliacaoComissao(
codigo="AVAL_COMIS_PREMIO", tipo="Membro de Comissão",
premio="PCT", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_PREMIO"), None)
assert atuacao is not None
assert atuacao.base >= 30
def test_avaliacao_comissao_gp_base_40(self):
avaliacao = AvaliacaoComissao(
codigo="AVAL_COMIS_GP", tipo="Membro de Comissão",
premio="Grande Prêmio", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_GP"), None)
assert atuacao is not None
assert atuacao.base >= 40
def test_coord_comissao_premio_base_40(self):
avaliacao = AvaliacaoComissao(
codigo="COORD_COMIS_PREMIO", tipo="Coordenador",
premio="PCT", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "COORD_COMIS_PREMIO"), None)
assert atuacao is not None
assert atuacao.base >= 40
def test_coord_comissao_gp_base_50(self):
avaliacao = AvaliacaoComissao(
codigo="COORD_COMIS_GP", tipo="Coordenador",
premio="Grande Prêmio", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([], [avaliacao], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "COORD_COMIS_GP"), None)
assert atuacao is not None
assert atuacao.base >= 50
def test_premiacao_gp_autor_base_100(self):
premiacao = Premiacao(
codigo="PREMIACAO_GP_AUTOR", tipo="Grande Prêmio",
nome_premio="Grande Prêmio CAPES", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [premiacao], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PREMIACAO_GP_AUTOR"), None)
assert atuacao is not None
assert atuacao.base >= 100
def test_premiacao_autor_base_50(self):
premiacao = Premiacao(
codigo="PREMIACAO_AUTOR", tipo="Prêmio",
nome_premio="Prêmio CAPES de Tese", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [premiacao], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PREMIACAO_AUTOR"), None)
assert atuacao is not None
assert atuacao.base >= 50
def test_mencao_autor_base_30(self):
premiacao = Premiacao(
codigo="MENCAO_AUTOR", tipo="Menção Honrosa",
nome_premio="Prêmio CAPES de Tese", ano=2024
)
resultado = CalculadorPontuacao.calcular_bloco_c([], [], [premiacao], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "MENCAO_AUTOR"), None)
assert atuacao is not None
assert atuacao.base >= 30
def test_avaliacao_recorrente_bonus_anual(self):
avaliacoes = [
AvaliacaoComissao(
codigo="AVAL_COMIS_PREMIO", tipo="Membro",
premio="PCT", ano=2022
),
AvaliacaoComissao(
codigo="AVAL_COMIS_PREMIO", tipo="Membro",
premio="PCT", ano=2023
),
AvaliacaoComissao(
codigo="AVAL_COMIS_PREMIO", tipo="Membro",
premio="PCT", ano=2024
),
]
resultado = CalculadorPontuacao.calcular_bloco_c([], avaliacoes, [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_PREMIO"), None)
assert atuacao is not None
assert atuacao.bonus > 0
assert atuacao.quantidade == 3
def test_inscricoes_multiplas_acumulam(self):
inscricoes = [
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2022),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2023),
]
resultado = CalculadorPontuacao.calcular_bloco_c(inscricoes, [], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR"), None)
assert atuacao is not None
assert atuacao.quantidade == 2
assert atuacao.base == 20
def test_teto_inscricao_autor_20_pontos(self):
inscricoes = [
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=i)
for i in range(2015, 2025)
]
resultado = CalculadorPontuacao.calcular_bloco_c(inscricoes, [], [], [], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR"), None)
assert atuacao is not None
assert atuacao.total <= 20
class TestBlocoDParticipacoes:
def test_bloco_d_vazio(self):
resultado = CalculadorPontuacao.calcular_bloco_d([], [])
assert resultado.bloco == "D"
assert resultado.total == 0
def test_bolsa_cnpq_base_30(self):
bolsa = BolsaCNPQ(codigo="BOL_BPQ_NIVEL", nivel="1A")
resultado = CalculadorPontuacao.calcular_bloco_d([bolsa], [])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "BOL_BPQ_NIVEL"), None)
assert atuacao is not None
assert atuacao.base >= 30
def test_evento_base_1_ponto(self):
evento = Participacao(codigo="EVENTO", tipo="Evento", ano=2024)
resultado = CalculadorPontuacao.calcular_bloco_d([], [evento])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "EVENTO"), None)
assert atuacao is not None
assert atuacao.base >= 1
def test_projeto_base_10_pontos(self):
projeto = Participacao(codigo="PROJ", tipo="Projeto", ano=2024)
resultado = CalculadorPontuacao.calcular_bloco_d([], [projeto])
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PROJ"), None)
assert atuacao is not None
assert atuacao.base >= 10
def test_eventos_multiplos_bonus_recorrencia(self):
eventos = [
Participacao(codigo="EVENTO", tipo="Evento", ano=i)
for i in range(2020, 2025)
]
resultado = CalculadorPontuacao.calcular_bloco_d([], eventos)
atuacao = next((a for a in resultado.atuacoes if a.codigo == "EVENTO"), None)
assert atuacao is not None
assert atuacao.quantidade == 5
assert atuacao.bonus > 0
def test_evento_teto_5_pontos(self):
eventos = [
Participacao(codigo="EVENTO", tipo="Evento", ano=i)
for i in range(2000, 2025)
]
resultado = CalculadorPontuacao.calcular_bloco_d([], eventos)
atuacao = next((a for a in resultado.atuacoes if a.codigo == "EVENTO"), None)
assert atuacao is not None
assert atuacao.total <= 5
def test_projeto_teto_30_pontos(self):
projetos = [
Participacao(codigo="PROJ", tipo="Projeto", ano=i)
for i in range(2000, 2025)
]
resultado = CalculadorPontuacao.calcular_bloco_d([], projetos)
atuacao = next((a for a in resultado.atuacoes if a.codigo == "PROJ"), None)
assert atuacao is not None
assert atuacao.total <= 30
class TestBlocoECoordPPG:
def test_bloco_e_sem_coordenador(self):
resultado = CalculadorPontuacao.calcular_bloco_e(False)
assert resultado.bloco == "E"
assert resultado.total == 0
def test_bloco_e_com_coordenador(self):
resultado = CalculadorPontuacao.calcular_bloco_e(True)
assert resultado.bloco == "E"
assert len(resultado.atuacoes) == 1
assert resultado.atuacoes[0].codigo == "PPG_COORD"
class TestPontuacaoCompleta:
def test_consultor_vazio_pontuacao_zero(self, consultor_vazio):
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_vazio)
assert resultado.total == 0
def test_consultor_com_coordenacao(self, coordenacao_ca_ativa):
consultor = Consultor(
id_pessoa=1,
nome="Coordenador",
coordenacoes_capes=[coordenacao_ca_ativa],
)
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
assert resultado.bloco_a.total > 0
assert resultado.bloco_b.total == 0
def test_consultor_completo_todos_blocos(self, consultor_completo):
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_completo)
assert resultado.bloco_a.total > 0
assert resultado.bloco_b.total > 0
assert resultado.bloco_c.total > 0
assert resultado.bloco_d.total > 0
def test_pontuacao_total_soma_blocos(self, consultor_completo):
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_completo)
soma_esperada = (
resultado.bloco_a.total
+ resultado.bloco_b.total
+ resultado.bloco_c.total
+ resultado.bloco_d.total
+ resultado.bloco_e.total
)
assert resultado.total == soma_esperada
def test_detalhamento_retorna_dict(self, consultor_completo):
resultado = CalculadorPontuacao.calcular_pontuacao_completa(consultor_completo)
detalhes = resultado.detalhamento
assert isinstance(detalhes, dict)
assert "bloco_a" in detalhes
assert "bloco_b" in detalhes
assert "bloco_c" in detalhes
assert "bloco_d" in detalhes
assert "bloco_e" in detalhes
assert "pontuacao_total" in detalhes
class TestCriteriosConfigurados:
def test_criterio_ca_existe(self):
assert "CA" in CRITERIOS
criterio = CRITERIOS["CA"]
assert criterio.base == 200
assert criterio.teto == 450
def test_criterio_caj_existe(self):
assert "CAJ" in CRITERIOS
criterio = CRITERIOS["CAJ"]
assert criterio.base == 150
assert criterio.teto == 370
def test_criterio_cons_ativo_existe(self):
assert "CONS_ATIVO" in CRITERIOS
criterio = CRITERIOS["CONS_ATIVO"]
assert criterio.base == 150
assert criterio.teto == 230
def test_todos_criterios_tem_codigo_valido(self):
for codigo, criterio in CRITERIOS.items():
assert criterio.codigo == codigo
assert criterio.base >= 0
assert criterio.teto >= 0

View File

@@ -0,0 +1,169 @@
import pytest
from datetime import datetime
from dateutil.relativedelta import relativedelta
from src.domain.value_objects.periodo import (
Periodo,
mesclar_periodos,
anos_completos_periodos,
)
class TestPeriodo:
def test_periodo_ativo_sem_fim(self):
inicio = datetime(2020, 1, 1)
periodo = Periodo(inicio=inicio, fim=None)
assert periodo.ativo is True
def test_periodo_inativo_com_fim(self):
inicio = datetime(2020, 1, 1)
fim = datetime(2023, 1, 1)
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.ativo is False
def test_anos_decorridos_3_anos(self):
inicio = datetime.now() - relativedelta(years=3)
fim = datetime.now()
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.anos_decorridos >= 2.9
assert periodo.anos_decorridos <= 3.1
def test_anos_decorridos_periodo_ativo(self):
inicio = datetime.now() - relativedelta(years=5)
periodo = Periodo(inicio=inicio, fim=None)
assert periodo.anos_decorridos >= 4.9
assert periodo.anos_decorridos <= 5.1
def test_anos_completos_retorna_inteiro(self):
inicio = datetime(2020, 1, 1)
fim = datetime(2023, 6, 15)
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.anos_completos() == 3
def test_anos_completos_menos_de_um_ano(self):
inicio = datetime.now() - relativedelta(months=6)
fim = datetime.now()
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.anos_completos() == 0
def test_anos_completos_com_data_referencia(self):
inicio = datetime(2020, 1, 1)
periodo = Periodo(inicio=inicio, fim=None)
data_ref = datetime(2025, 1, 1)
assert periodo.anos_completos(data_ref) == 5
def test_fim_anterior_inicio_corrige_para_none(self):
inicio = datetime(2023, 1, 1)
fim = datetime(2020, 1, 1)
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.fim is None
assert periodo.ativo is True
class TestMesclarPeriodos:
def test_lista_vazia_retorna_vazia(self):
resultado = mesclar_periodos([])
assert resultado == []
def test_um_periodo_retorna_mesmo(self):
periodo = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
resultado = mesclar_periodos([periodo])
assert len(resultado) == 1
assert resultado[0].inicio == periodo.inicio
def test_periodos_consecutivos_mesclados(self):
p1 = Periodo(datetime(2020, 1, 1), datetime(2022, 1, 1))
p2 = Periodo(datetime(2021, 6, 1), datetime(2024, 1, 1))
resultado = mesclar_periodos([p1, p2])
assert len(resultado) == 1
assert resultado[0].inicio == datetime(2020, 1, 1)
assert resultado[0].fim == datetime(2024, 1, 1)
def test_periodos_separados_nao_mesclados(self):
p1 = Periodo(datetime(2015, 1, 1), datetime(2017, 1, 1))
p2 = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
resultado = mesclar_periodos([p1, p2])
assert len(resultado) == 2
def test_periodo_ativo_preservado(self):
p1 = Periodo(datetime(2020, 1, 1), datetime(2022, 1, 1))
p2 = Periodo(datetime(2021, 1, 1), None)
resultado = mesclar_periodos([p1, p2])
assert len(resultado) == 1
assert resultado[0].ativo is True
def test_tres_periodos_mesclados(self):
p1 = Periodo(datetime(2018, 1, 1), datetime(2020, 1, 1))
p2 = Periodo(datetime(2019, 1, 1), datetime(2021, 1, 1))
p3 = Periodo(datetime(2020, 6, 1), datetime(2023, 1, 1))
resultado = mesclar_periodos([p1, p2, p3])
assert len(resultado) == 1
assert resultado[0].inicio == datetime(2018, 1, 1)
assert resultado[0].fim == datetime(2023, 1, 1)
def test_ordenacao_automatica(self):
p1 = Periodo(datetime(2022, 1, 1), datetime(2024, 1, 1))
p2 = Periodo(datetime(2018, 1, 1), datetime(2020, 1, 1))
resultado = mesclar_periodos([p1, p2])
assert resultado[0].inicio == datetime(2018, 1, 1)
class TestAnosCompletosPeriodos:
def test_lista_vazia_retorna_zero(self):
resultado = anos_completos_periodos([])
assert resultado == 0
def test_um_periodo_3_anos(self):
periodo = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
resultado = anos_completos_periodos([periodo])
assert resultado == 3
def test_dois_periodos_soma(self):
p1 = Periodo(datetime(2015, 1, 1), datetime(2017, 1, 1))
p2 = Periodo(datetime(2020, 1, 1), datetime(2023, 1, 1))
resultado = anos_completos_periodos([p1, p2])
assert resultado == 5
def test_com_data_referencia(self):
p1 = Periodo(datetime(2020, 1, 1), None)
p2 = Periodo(datetime(2015, 1, 1), datetime(2017, 1, 1))
data_ref = datetime(2025, 1, 1)
resultado = anos_completos_periodos([p1, p2], data_ref)
assert resultado == 7
class TestCasosEspeciais:
def test_periodo_muito_curto(self):
inicio = datetime.now() - relativedelta(days=30)
fim = datetime.now()
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.anos_completos() == 0
def test_periodo_exatamente_um_ano(self):
inicio = datetime(2023, 1, 1)
fim = datetime(2024, 1, 1)
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.anos_completos() == 1
def test_periodo_quase_um_ano(self):
inicio = datetime(2023, 1, 1)
fim = datetime(2023, 12, 31)
periodo = Periodo(inicio=inicio, fim=fim)
assert periodo.anos_completos() == 0
def test_mesclagem_periodos_sobrepostos_complexos(self):
periodos = [
Periodo(datetime(2010, 1, 1), datetime(2012, 1, 1)),
Periodo(datetime(2011, 6, 1), datetime(2014, 1, 1)),
Periodo(datetime(2013, 1, 1), datetime(2015, 1, 1)),
Periodo(datetime(2018, 1, 1), datetime(2020, 1, 1)),
Periodo(datetime(2019, 6, 1), None),
]
resultado = mesclar_periodos(periodos)
assert len(resultado) == 2
assert resultado[0].fim == datetime(2015, 1, 1)
assert resultado[1].ativo is True

View File

View 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"

View File

@@ -0,0 +1,483 @@
import pytest
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock
from src.infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
@pytest.fixture
def mock_es_client():
return AsyncMock()
@pytest.fixture
def repository(mock_es_client):
return ConsultorRepositoryImpl(es_client=mock_es_client)
class TestParseDate:
def test_parse_date_formato_brasileiro(self, repository):
result = repository._parse_date("15/03/2020")
assert result is not None
assert result.day == 15
assert result.month == 3
assert result.year == 2020
def test_parse_date_formato_iso(self, repository):
result = repository._parse_date("2020-03-15")
assert result is not None
assert result.year == 2020
assert result.month == 3
assert result.day == 15
def test_parse_date_none(self, repository):
result = repository._parse_date(None)
assert result is None
def test_parse_date_string_vazia(self, repository):
result = repository._parse_date("")
assert result is None
def test_parse_date_invalida(self, repository):
result = repository._parse_date("data_invalida")
assert result is None
class TestInferirTipoCoordenacao:
def test_coordenador_area(self, repository):
coord = {"dadosCoordenacaoArea": {"tipo": "Coordenador de Área"}}
result = repository._inferir_tipo_coordenacao(coord)
assert result == "CA"
def test_coordenador_adjunto(self, repository):
coord = {"dadosCoordenacaoArea": {"tipo": "Coordenador Adjunto"}}
result = repository._inferir_tipo_coordenacao(coord)
assert result == "CAJ"
def test_coordenador_adjunto_mp(self, repository):
coord = {"dadosCoordenacaoArea": {"tipo": "Coordenador Adjunto de Mestrado Profissionalizante"}}
result = repository._inferir_tipo_coordenacao(coord)
assert result == "CAJ_MP"
def test_camara_tematica(self, repository):
coord = {"dadosCoordenacaoArea": {"tipo": "Câmara Temática"}}
result = repository._inferir_tipo_coordenacao(coord)
assert result == "CAM"
def test_inferencia_por_descricao(self, repository):
coord = {"dadosCoordenacaoArea": {}, "descricao": "Coordenador Adjunto - MEDICINA"}
result = repository._inferir_tipo_coordenacao(coord)
assert result == "CAJ"
def test_fallback_para_ca(self, repository):
coord = {"dadosCoordenacaoArea": {"tipo": "Tipo Desconhecido"}}
result = repository._inferir_tipo_coordenacao(coord)
assert result == "CA"
class TestInferirPremiacaoTipo:
def test_grande_premio(self):
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Grande Prêmio CAPES")
assert result == "GP"
def test_mencao_honrosa(self):
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Menção Honrosa")
assert result == "MENCAO"
def test_premio_regular(self):
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Prêmio CAPES de Tese")
assert result == "PREMIO"
def test_texto_sem_premiacao(self):
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("Texto qualquer")
assert result is None
def test_texto_vazio(self):
result = ConsultorRepositoryImpl._inferir_premiacao_tipo("")
assert result is None
class TestExtrairCoordenacoesCapes:
def test_lista_vazia(self, repository):
result = repository._extrair_coordenacoes_capes([])
assert result == []
def test_atuacao_sem_tipo_coordenacao(self, repository):
atuacoes = [{"tipo": "Consultor"}]
result = repository._extrair_coordenacoes_capes(atuacoes)
assert result == []
def test_coordenacao_ativa(self, repository):
atuacoes = [{
"tipo": "Coordenação de Área de Avaliação",
"inicio": "01/01/2020",
"fim": None,
"dadosCoordenacaoArea": {
"tipo": "Coordenador de Área",
"inicioVinculacao": "01/01/2020",
"fimVinculacao": None,
"areaAvaliacao": {"nome": "CIÊNCIAS AMBIENTAIS"},
}
}]
result = repository._extrair_coordenacoes_capes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "CA"
assert result[0].area_avaliacao == "CIÊNCIAS AMBIENTAIS"
assert result[0].periodo.ativo is True
def test_coordenacao_historica(self, repository):
atuacoes = [{
"tipo": "Histórico de Coordenação de Área de Avaliação",
"dadosCoordenacaoArea": {
"tipo": "Coordenador Adjunto",
"inicioVinculacao": "01/01/2018",
"fimVinculacao": "31/12/2020",
"areaAvaliacao": {"nome": "MEDICINA I"},
}
}]
result = repository._extrair_coordenacoes_capes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "CAJ"
assert result[0].periodo.ativo is False
def test_coordenacao_sem_inicio_ignorada(self, repository):
atuacoes = [{
"tipo": "Coordenação de Área de Avaliação",
"dadosCoordenacaoArea": {
"tipo": "Coordenador de Área",
"inicioVinculacao": None,
"areaAvaliacao": {"nome": "CIÊNCIAS"},
}
}]
result = repository._extrair_coordenacoes_capes(atuacoes)
assert len(result) == 0
class TestExtrairConsultoria:
def test_sem_consultoria(self, repository):
result = repository._extrair_consultoria([])
assert result is None
def test_consultor_ativo(self, repository):
atuacoes = [{
"tipo": "Consultor",
"inicio": "01/01/2020",
"fim": None,
"dadosConsultoria": {
"situacaoConsultoria": "Atividade Contínua",
"inicioVinculacao": "01/01/2020",
"ies": {"id": "1", "nome": "USP", "sigla": "USP"},
}
}]
result = repository._extrair_consultoria(atuacoes)
assert result is not None
assert result.codigo == "CONS_ATIVO"
assert result.periodo.ativo is True
def test_consultor_historico(self, repository):
atuacoes = [{
"tipo": "Histórico de Consultoria",
"inicio": "01/01/2015",
"fim": "31/12/2018",
"dadosConsultoria": {
"situacaoConsultoria": "Desligado",
"inicioVinculacao": "01/01/2015",
"inativacaoSituacao": "31/12/2018",
}
}]
result = repository._extrair_consultoria(atuacoes)
assert result is not None
assert result.codigo == "CONS_HIST"
def test_consultor_falecido(self, repository):
atuacoes = [{
"tipo": "Consultor",
"inicio": "01/01/2010",
"fim": "31/12/2020",
"dadosConsultoria": {
"situacaoConsultoria": "Falecido",
}
}]
result = repository._extrair_consultoria(atuacoes)
assert result is not None
assert result.codigo == "CONS_FALECIDO"
class TestExtrairInscricoes:
def test_sem_inscricoes(self, repository):
result = repository._extrair_inscricoes([])
assert result == []
def test_inscricao_autor(self, repository):
atuacoes = [{
"tipo": "Inscrição Prêmio",
"inicio": "01/01/2024",
"dadosParticipacaoInscricaoPremio": {
"tipo": "Autor",
"nomePremio": "PCT",
"ano": 2024,
}
}]
result = repository._extrair_inscricoes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "INSC_AUTOR"
assert result[0].ano == 2024
def test_inscricao_institucional(self, repository):
atuacoes = [{
"tipo": "Inscrição Prêmio",
"dadosParticipacaoInscricaoPremio": {
"tipo": "Coordenador PPG",
"nomePremio": "PCT",
"ano": 2024,
}
}]
result = repository._extrair_inscricoes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "INSC_INST_AUTOR"
class TestExtrairAvaliacoesComissao:
def test_sem_avaliacoes(self, repository):
result = repository._extrair_avaliacoes_comissao([])
assert result == []
def test_avaliador_premio(self, repository):
atuacoes = [{
"tipo": "Avaliação Prêmio",
"dadosParticipacaoPremio": {
"tipo": "Membro de Comissão",
"nomePremio": "Prêmio CAPES de Tese",
"ano": 2024,
}
}]
result = repository._extrair_avaliacoes_comissao(atuacoes)
assert len(result) == 1
assert result[0].codigo == "AVAL_COMIS_PREMIO"
def test_avaliador_grande_premio(self, repository):
atuacoes = [{
"tipo": "Avaliação Prêmio",
"dadosParticipacaoPremio": {
"tipo": "Membro de Comissão",
"nomePremio": "Grande Prêmio CAPES",
"ano": 2024,
}
}]
result = repository._extrair_avaliacoes_comissao(atuacoes)
assert len(result) == 1
assert result[0].codigo == "AVAL_COMIS_GP"
def test_coordenador_comissao(self, repository):
atuacoes = [{
"tipo": "Avaliação Prêmio",
"dadosParticipacaoPremio": {
"tipo": "Coordenador/Presidente",
"nomePremio": "Prêmio CAPES",
"ano": 2024,
}
}]
result = repository._extrair_avaliacoes_comissao(atuacoes)
assert len(result) == 1
assert result[0].codigo == "COORD_COMIS_PREMIO"
class TestExtrairPremiacoes:
def test_sem_premiacoes(self, repository):
result = repository._extrair_premiacoes([])
assert result == []
def test_grande_premio(self, repository):
atuacoes = [{
"tipo": "Premiação Prêmio",
"dadosPremiacaoPremio": {
"tipoPremiacao": "Grande Prêmio",
"nomePremio": "Grande Prêmio CAPES de Tese",
"ano": 2024,
}
}]
result = repository._extrair_premiacoes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "PREMIACAO_GP_AUTOR"
def test_premio_regular(self, repository):
atuacoes = [{
"tipo": "Premiação Prêmio",
"dadosPremiacaoPremio": {
"tipoPremiacao": "Prêmio",
"nomePremio": "Prêmio CAPES de Tese",
"ano": 2024,
}
}]
result = repository._extrair_premiacoes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "PREMIACAO_AUTOR"
def test_mencao_honrosa(self, repository):
atuacoes = [{
"tipo": "Premiação Prêmio",
"dadosPremiacaoPremio": {
"tipoPremiacao": "Menção Honrosa",
"nomePremio": "PCT",
"ano": 2024,
}
}]
result = repository._extrair_premiacoes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "MENCAO_AUTOR"
class TestExtrairBolsasCNPQ:
def test_sem_bolsas(self, repository):
result = repository._extrair_bolsas_cnpq([])
assert result == []
def test_bolsa_cnpq(self, repository):
atuacoes = [{
"tipo": "Bolsa CNPQ",
"dadosBolsa": {
"nivel": "1A",
"areaConhecimento": "Ciências Exatas",
}
}]
result = repository._extrair_bolsas_cnpq(atuacoes)
assert len(result) == 1
assert result[0].codigo == "BOL_BPQ_NIVEL"
assert result[0].nivel == "1A"
class TestExtrairParticipacoes:
def test_sem_participacoes(self, repository):
result = repository._extrair_participacoes([])
assert result == []
def test_evento(self, repository):
atuacoes = [{
"tipo": "Evento",
"descricao": "Seminário CAPES 2024",
"inicio": "01/06/2024",
}]
result = repository._extrair_participacoes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "EVENTO"
assert result[0].ano == 2024
def test_projeto(self, repository):
atuacoes = [{
"tipo": "Projeto",
"descricao": "Projeto de Pesquisa",
"inicio": "01/01/2024",
}]
result = repository._extrair_participacoes(atuacoes)
assert len(result) == 1
assert result[0].codigo == "PROJ"
class TestExtrairOrientacoes:
def test_sem_orientacoes(self, repository):
result = repository._extrair_orientacoes([])
assert result == []
def test_orientacao_discentes_contagem(self, repository):
atuacoes = [{
"tipo": "Orientação de Discentes",
"dadosOrientacaoDiscente": {
"totalOrientacaoFinalizadaMestrado": 5,
"totalOrientacaoFinalizadaDoutorado": 3,
"totalAcompanhamentoPosDoutorado": 2,
}
}]
result = repository._extrair_orientacoes(atuacoes)
assert len(result) == 10
pos_doc = [o for o in result if o.codigo == "ORIENT_POS_DOC"]
tese = [o for o in result if o.codigo == "ORIENT_TESE"]
diss = [o for o in result if o.codigo == "ORIENT_DISS"]
assert len(pos_doc) == 2
assert len(tese) == 3
assert len(diss) == 5
class TestConstruirConsultor:
@pytest.mark.asyncio
async def test_construir_consultor_completo(self, repository):
doc = {
"id": 12345,
"dadosPessoais": {"nome": "João da Silva", "cpf": "12345678900"},
"atuacoes": [
{
"tipo": "Coordenação de Área de Avaliação",
"inicio": "01/01/2020",
"dadosCoordenacaoArea": {
"tipo": "Coordenador de Área",
"inicioVinculacao": "01/01/2020",
"areaAvaliacao": {"nome": "CIÊNCIAS AMBIENTAIS"},
}
},
{
"tipo": "Consultor",
"inicio": "01/01/2018",
"dadosConsultoria": {
"situacaoConsultoria": "Atividade Contínua",
}
},
]
}
consultor = await repository._construir_consultor(doc)
assert consultor.id_pessoa == 12345
assert consultor.nome == "João da Silva"
assert len(consultor.coordenacoes_capes) == 1
assert consultor.consultoria is not None
assert consultor.pontuacao is not None
assert consultor.pontuacao_total > 0
@pytest.mark.asyncio
async def test_construir_consultor_vazio(self, repository):
doc = {
"id": 99999,
"dadosPessoais": {"nome": "Consultor Vazio"},
"atuacoes": []
}
consultor = await repository._construir_consultor(doc)
assert consultor.id_pessoa == 99999
assert consultor.nome == "Consultor Vazio"
assert len(consultor.coordenacoes_capes) == 0
assert consultor.consultoria is None
class TestBuscarPorId:
@pytest.mark.asyncio
async def test_buscar_por_id_encontrado(self, repository, mock_es_client):
mock_es_client.buscar_por_id.return_value = {
"id": 12345,
"dadosPessoais": {"nome": "João"},
"atuacoes": []
}
result = await repository.buscar_por_id(12345)
assert result is not None
assert result.id_pessoa == 12345
mock_es_client.buscar_por_id.assert_called_once_with(12345)
@pytest.mark.asyncio
async def test_buscar_por_id_nao_encontrado(self, repository, mock_es_client):
mock_es_client.buscar_por_id.return_value = None
result = await repository.buscar_por_id(99999)
assert result is None
@pytest.mark.asyncio
async def test_buscar_por_id_erro_es(self, repository, mock_es_client):
mock_es_client.buscar_por_id.side_effect = Exception("ES error")
result = await repository.buscar_por_id(12345)
assert result is None

View File

@@ -0,0 +1,125 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from httpx import Response
from src.infrastructure.elasticsearch.client import ElasticsearchClient
from src.infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
def create_mock_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
@pytest.fixture
def es_client():
return ElasticsearchClient(
url="http://localhost:9200",
index="atuacapes_test",
user="test",
password="test"
)
@pytest.fixture
def repository(es_client):
return ConsultorRepositoryImpl(es_client)
class TestIntegracaoCoordenadorDeArea:
@pytest.mark.asyncio
async def test_coordenador_area_ativo_pontuacao_completa(self, es_client, repository):
doc_es = {
"id": 1001,
"dadosPessoais": {"nome": "COORDENADOR ATIVO SILVA"},
"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", "id": 1}
}
}]
}
with patch.object(es_client, '_client') as mock_client:
mock_client.is_closed = False
mock_client.post = AsyncMock(return_value=create_mock_response({
"hits": {"total": {"value": 1}, "hits": [{"_source": doc_es}]}
}))
consultor = await repository.buscar_por_id(1001)
assert consultor is not None
assert consultor.nome == "COORDENADOR ATIVO SILVA"
assert len(consultor.coordenacoes_capes) == 1
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
assert pontuacao.bloco_a.total > 0
coord = consultor.coordenacoes_capes[0]
assert coord.codigo == "CA"
assert pontuacao.bloco_a.total >= 200
class TestCenariosBorda:
@pytest.mark.asyncio
async def test_consultor_sem_atuacoes(self, es_client, repository):
doc_es = {
"id": 8001,
"dadosPessoais": {"nome": "SEM ATUACOES"},
"atuacoes": []
}
with patch.object(es_client, '_client') as mock_client:
mock_client.is_closed = False
mock_client.post = AsyncMock(return_value=create_mock_response({
"hits": {"total": {"value": 1}, "hits": [{"_source": doc_es}]}
}))
consultor = await repository.buscar_por_id(8001)
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
assert consultor.nome == "SEM ATUACOES"
assert pontuacao.total == 0
@pytest.mark.asyncio
async def test_atuacao_tipo_desconhecido(self, es_client, repository):
doc_es = {
"id": 8002,
"dadosPessoais": {"nome": "TIPO ESTRANHO"},
"atuacoes": [{
"tipo": "Tipo Inexistente No Sistema",
"dados": {"campo": "valor"}
}]
}
with patch.object(es_client, '_client') as mock_client:
mock_client.is_closed = False
mock_client.post = AsyncMock(return_value=create_mock_response({
"hits": {"total": {"value": 1}, "hits": [{"_source": doc_es}]}
}))
consultor = await repository.buscar_por_id(8002)
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
assert pontuacao.total == 0
@pytest.mark.asyncio
async def test_consultor_nao_encontrado(self, es_client, repository):
with patch.object(es_client, '_client') as mock_client:
mock_client.is_closed = False
mock_client.post = AsyncMock(return_value=create_mock_response({
"hits": {"total": {"value": 0}, "hits": []}
}))
consultor = await repository.buscar_por_id(99999)
assert consultor is None

View File

@@ -0,0 +1,142 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from httpx import Response
from src.infrastructure.elasticsearch.client import ElasticsearchClient
from src.infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
def create_mock_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
def criar_doc_consultor(id_pessoa, nome, atuacoes):
return {
"id": id_pessoa,
"dadosPessoais": {"nome": nome},
"atuacoes": atuacoes
}
@pytest.fixture
def es_client():
return ElasticsearchClient(
url="http://localhost:9200",
index="atuacapes_test"
)
@pytest.fixture
def repository(es_client):
return ConsultorRepositoryImpl(es_client)
class TestScrollFlowCompleto:
@pytest.mark.asyncio
async def test_processamento_scroll_com_ranking(self, es_client, repository):
docs_batch_1 = [
criar_doc_consultor(1, "COORD AREA", [{
"tipo": "Coordenação de Área de Avaliação",
"inicio": "01/01/2018",
"dadosCoordenacaoArea": {"tipo": "Coordenador de Área"}
}]),
criar_doc_consultor(2, "COORD ADJUNTO", [{
"tipo": "Coordenação de Área de Avaliação",
"inicio": "01/01/2020",
"dadosCoordenacaoArea": {"tipo": "Coordenador Adjunto"}
}]),
]
docs_batch_2 = [
criar_doc_consultor(3, "CONSULTOR ATIVO", [{
"tipo": "Consultor",
"inicio": "01/01/2015",
"dadosConsultoria": {"situacaoConsultoria": "Atividade Contínua"}
}]),
criar_doc_consultor(4, "PESSOA SEM ATUACAO", []),
]
scroll_1 = {
"_scroll_id": "scroll_1",
"hits": {
"total": {"value": 4},
"hits": [{"_source": d} for d in docs_batch_1]
}
}
scroll_2 = {
"_scroll_id": "scroll_2",
"hits": {"hits": [{"_source": d} for d in docs_batch_2]}
}
scroll_empty = {"_scroll_id": "scroll_3", "hits": {"hits": []}}
ranking_results = []
async def processar_batch(docs, progress):
for doc in docs:
consultor = await repository._construir_consultor(doc)
pontuacao = CalculadorPontuacao.calcular_pontuacao_completa(consultor)
ranking_results.append({
"id": consultor.id_pessoa,
"nome": consultor.nome,
"total": pontuacao.total
})
with patch.object(es_client, '_client') as mock_client:
mock_client.is_closed = False
mock_client.post = AsyncMock(side_effect=[
create_mock_response(scroll_1),
create_mock_response(scroll_2),
create_mock_response(scroll_empty)
])
mock_client.delete = AsyncMock(return_value=create_mock_response({}))
result = await es_client.buscar_todos_consultores(processar_batch, batch_size=2)
assert result["total"] == 4
assert result["processados"] == 4
assert len(ranking_results) == 4
class TestProgressoScroll:
@pytest.mark.asyncio
async def test_progress_callback(self, es_client):
progress_history = []
async def callback(docs, progress):
progress_history.append(progress.copy())
scroll_1 = {
"_scroll_id": "s1",
"hits": {"total": {"value": 100}, "hits": [{"_source": {"id": i}} for i in range(50)]}
}
scroll_2 = {
"_scroll_id": "s2",
"hits": {"hits": [{"_source": {"id": i}} for i in range(50, 100)]}
}
scroll_empty = {"_scroll_id": "s3", "hits": {"hits": []}}
with patch.object(es_client, '_client') as mock_client:
mock_client.is_closed = False
mock_client.post = AsyncMock(side_effect=[
create_mock_response(scroll_1),
create_mock_response(scroll_2),
create_mock_response(scroll_empty)
])
mock_client.delete = AsyncMock(return_value=create_mock_response({}))
await es_client.buscar_todos_consultores(callback, batch_size=50)
assert len(progress_history) == 2
assert progress_history[0]["percentual"] == 50
assert progress_history[0]["processados"] == 50
assert progress_history[1]["percentual"] == 100
assert progress_history[1]["processados"] == 100

View File

View File

View File

@@ -0,0 +1,301 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
from fastapi import FastAPI
from src.interface.api.routes import router
from src.interface.api.dependencies import (
get_repository,
get_ranking_store,
get_processar_job,
get_es_client,
get_ranking_oracle_repo,
)
@pytest.fixture
def mock_repository():
repo = AsyncMock()
repo.contar_total.return_value = 100
return repo
@pytest.fixture
def mock_ranking_store():
store = MagicMock()
store.is_ready.return_value = False
return store
@pytest.fixture
def mock_oracle_repo():
repo = MagicMock()
repo.contar_total.return_value = 1000
return repo
@pytest.fixture
def mock_es_client():
return AsyncMock()
@pytest.fixture
def mock_job():
return AsyncMock()
@pytest.fixture
def app(mock_repository, mock_ranking_store, mock_oracle_repo, mock_es_client, mock_job):
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_repository] = lambda: mock_repository
app.dependency_overrides[get_ranking_store] = lambda: mock_ranking_store
app.dependency_overrides[get_ranking_oracle_repo] = lambda: mock_oracle_repo
app.dependency_overrides[get_es_client] = lambda: mock_es_client
app.dependency_overrides[get_processar_job] = lambda: mock_job
return app
@pytest.fixture
def client(app):
return TestClient(app)
class TestHealthCheck:
def test_health_check_ok(self, client):
response = client.get("/api/v1/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
class TestRankingStatus:
def test_status_inicial(self, client):
with patch("src.interface.api.routes.job_status") as mock_status:
mock_status.to_dict.return_value = {
"running": False,
"progress": 0,
"processados": 0,
"total": 0,
"mensagem": "",
"batch_atual": 0,
"total_batches": 0,
"tempo_decorrido": None,
"tempo_estimado": None,
"inicio": None,
"fim": None,
"erro": None,
}
response = client.get("/api/v1/ranking/status")
assert response.status_code == 200
data = response.json()
assert data["running"] is False
class TestRankingPaginado:
def test_ranking_paginado_sem_oracle(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 0
response = client.get("/api/v1/ranking/paginado")
assert response.status_code == 503
def test_ranking_paginado_com_dados(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 100
mock_oracle_repo.buscar_paginado.return_value = [
MagicMock(
id_pessoa=1,
nome="Consultor 1",
posicao=1,
pontuacao_total=500,
componente_a=200,
componente_b=100,
componente_c=100,
componente_d=50,
componente_e=50,
ativo=True,
anos_atuacao=5.0,
json_detalhes="{}"
)
]
response = client.get("/api/v1/ranking/paginado?page=1&size=10")
assert response.status_code == 200
data = response.json()
assert data["total"] == 100
assert len(data["consultores"]) == 1
def test_ranking_paginado_filtro_ativo(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 50
mock_oracle_repo.buscar_paginado.return_value = []
response = client.get("/api/v1/ranking/paginado?ativo=true")
assert response.status_code == 200
def test_ranking_paginado_filtro_selos(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 0
response = client.get("/api/v1/ranking/paginado?selos=COORD_PPG,BPQ")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
class TestBuscaPorNome:
def test_busca_por_nome_encontrado(self, client, mock_oracle_repo):
mock_oracle_repo.buscar_por_nome.return_value = [
{"ID_PESSOA": 1, "NOME": "João Silva", "POSICAO": 10, "PONTUACAO_TOTAL": 500}
]
response = client.get("/api/v1/ranking/busca?nome=João")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["nome"] == "João Silva"
def test_busca_por_nome_nao_encontrado(self, client, mock_oracle_repo):
mock_oracle_repo.buscar_por_nome.return_value = []
response = client.get("/api/v1/ranking/busca?nome=XYZ")
assert response.status_code == 200
data = response.json()
assert len(data) == 0
def test_busca_por_nome_minimo_3_chars(self, client):
response = client.get("/api/v1/ranking/busca?nome=AB")
assert response.status_code == 422
class TestRankingEstatisticas:
def test_estatisticas_sem_dados(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 0
response = client.get("/api/v1/ranking/estatisticas")
assert response.status_code == 503
def test_estatisticas_com_dados(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 1000
mock_oracle_repo.obter_estatisticas.return_value = {
"total_consultores": 1000,
"total_ativos": 800,
"total_inativos": 200,
"ultima_atualizacao": "2024-01-01",
"pontuacao_media": 150.5,
"pontuacao_maxima": 850,
"pontuacao_minima": 10,
"media_componentes": {"a": 50, "b": 30, "c": 40, "d": 20, "e": 10}
}
mock_oracle_repo.obter_distribuicao.return_value = [
{"faixa": "0-100", "quantidade": 500},
{"faixa": "100-200", "quantidade": 300},
]
response = client.get("/api/v1/ranking/estatisticas")
assert response.status_code == 200
data = response.json()
assert data["total_consultores"] == 1000
assert data["total_ativos"] == 800
class TestPosicaoRanking:
def test_posicao_encontrada(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 1000
mock_oracle_repo.buscar_por_id.return_value = MagicMock(
id_pessoa=123,
nome="João Silva",
posicao=42,
pontuacao_total=500,
componente_a=200,
componente_b=100,
componente_c=100,
componente_d=50,
componente_e=50,
ativo=True,
)
response = client.get("/api/v1/ranking/posicao/123")
assert response.status_code == 200
data = response.json()
assert data["id_pessoa"] == 123
assert data["posicao"] == 42
assert data["encontrado"] is True
def test_posicao_nao_encontrada(self, client, mock_oracle_repo):
mock_oracle_repo.contar_total.return_value = 1000
mock_oracle_repo.buscar_por_id.return_value = None
response = client.get("/api/v1/ranking/posicao/99999")
assert response.status_code == 200
data = response.json()
assert data["encontrado"] is False
class TestProcessarRanking:
def test_processar_ranking_inicia(self, client, mock_job):
with patch("src.interface.api.routes.job_status") as mock_status:
mock_status.is_running = False
response = client.post("/api/v1/ranking/processar")
assert response.status_code == 200
data = response.json()
assert data["sucesso"] is True
def test_processar_ranking_ja_executando(self, client):
with patch("src.interface.api.routes.job_status") as mock_status:
mock_status.is_running = True
response = client.post("/api/v1/ranking/processar")
assert response.status_code == 409
class TestListarSelos:
def test_listar_selos(self, client):
response = client.get("/api/v1/ranking/selos")
assert response.status_code == 200
data = response.json()
assert "selos" in data
assert isinstance(data["selos"], list)
class TestExportarInfo:
def test_info_exportacao(self, client, mock_oracle_repo):
mock_oracle_repo.contar_para_exportacao.return_value = 500
response = client.get("/api/v1/ranking/exportar/info")
assert response.status_code == 200
data = response.json()
assert data["total_consultores"] == 500
assert "estimativa_tamanho_mb" in data
class TestCorrigirEncoding:
def test_corrigir_encoding_sem_problemas(self):
from src.interface.api.routes import corrigir_encoding
result = corrigir_encoding("Texto normal")
assert result == "Texto normal"
def test_corrigir_encoding_none(self):
from src.interface.api.routes import corrigir_encoding
result = corrigir_encoding(None)
assert result is None
def test_corrigir_encoding_vazio(self):
from src.interface.api.routes import corrigir_encoding
result = corrigir_encoding("")
assert result == ""
class TestNormalizarTexto:
def test_normalizar_texto_acentos(self):
from src.interface.api.routes import normalizar_texto
result = normalizar_texto("Ciências Ambientais")
assert result == "ciencias ambientais"
def test_normalizar_texto_html_entities(self):
from src.interface.api.routes import normalizar_texto
result = normalizar_texto("Ci&ecirc;ncias")
assert "ciencias" in result.lower()
def test_normalizar_texto_vazio(self):
from src.interface.api.routes import normalizar_texto
result = normalizar_texto("")
assert result == ""