Compare commits

...

10 Commits

Author SHA1 Message Date
Frederico Castro
c273349663 fix: corrigir filtro Ativo/Inativo e cálculo de anos para consultores
Filtro E_CONSULTOR:
- Adicionar coluna E_CONSULTOR na tabela TB_RANKING_CONSULTOR
- Filtrar apenas consultores ao selecionar Ativo/Inativo
- Não-consultores não aparecem mais no filtro de inativos

Correção de anos para inativos:
- Calcular anos_consecutivos baseado no período histórico (não mais 0)
- Calcular anos_atuacao usando data fim do período (não datetime.now())
2025-12-31 03:12:43 -03:00
Frederico Castro
0a0a47ecc4 feat(tests): adicionar testes para filtro E_CONSULTOR e RankingStore
- Adicionar campo e_consultor nos testes de processar_ranking
- Adicionar testes de inserção com E_CONSULTOR no ranking_repository
- Criar test_ranking_store.py com testes de filtro ativo/inativo
- Garantir que filtros de ativo/inativo só afetam consultores
2025-12-30 23:32:02 -03:00
Frederico Castro
3558a4b6ca feat(tests): adicionar testes para ExcelService e PDFService
ExcelService (41 testes):
- Geração de Excel com consultores
- Parse de JSON, formatação de filtros
- Extração de coordenações, consultoria, prêmios, titulação

PDFService (44 testes):
- Formatação de datas (completa e curta)
- Ordenação por data com múltiplos formatos
- Wrappers (ConsultorWrapper, DictWrapper)
- Geração de ficha e PDF de equipe (mocked)

Correções:
- Ajuste nos testes Oracle para acessar params corretamente

Cobertura: 54% → 66%
2025-12-29 09:29:33 -03:00
Frederico Castro
7d02289605 feat(tests): adicionar testes de regras PDF e componentes frontend
Backend:
- test_pdf_rules.py: 108 testes para regras de pontuação do PDF
- test_pdf_selos.py: validação de selos disponíveis

Frontend:
- Configuração Vitest para testes de componentes React
- FiltroSelos.test.jsx: testes do componente de filtro
- Header.test.jsx: testes do componente de cabeçalho
2025-12-29 09:16:19 -03:00
Frederico Castro
d48fff2236 fix(selos): adicionar selos faltantes em SELOS_DISPONIVEIS
Adiciona 22 selos que estavam faltando na lista:
- Orientações: ORIENT_POS_DOC, ORIENT_TESE_PREM, ORIENT_DISS_PREM
- Co-orientações: CO_ORIENT_POS_DOC, CO_ORIENT_TESE, CO_ORIENT_DISS (+ _PREM)
- Bancas: MB_BANCA_POS_DOC, MB_BANCA_TESE, MB_BANCA_DISS (+ _PREM)
- Idiomas: IDIOMA_BILINGUE
- Titulações: TITULACAO_MESTRE, TITULACAO_DOUTOR, TITULACAO_POS_DOUTOR
- Outros: BOL_BPQ_NIVEL, PPG_COORD
2025-12-29 09:15:00 -03:00
Frederico Castro
143ec401f5 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
2025-12-29 08:06:08 -03:00
Frederico Castro
e0692ee49c fix(frontend): evitar requisições duplicadas causadas pelo React StrictMode
- Adicionar ref para controlar requisições já feitas no App.jsx
- Adicionar ref para controlar fetch no RawDataModal.jsx
- Adicionar componente FiltroAtivo para filtrar consultores
2025-12-29 03:29:42 -03:00
Frederico Castro
78670c40de fix(selos): mostrar nome do selo no tooltip ao passar o mouse 2025-12-28 01:49:19 -03:00
Frederico Castro
840934a187 feat(export): adicionar exportação Excel do ranking com barra de progresso
- Novo endpoint GET /api/v1/ranking/exportar/excel
- Exporta apenas consultores com pontuação > 0
- Usa xlsxwriter para geração rápida (~40s para 300k registros)
- Layout profissional com formatação, filtros e cores condicionais
- Barra de progresso real no frontend com dois estados:
  - Animação indeterminada durante geração no servidor
  - Progresso real durante download do arquivo
- Botão de exportação integrado ao layout do sistema
- Suporte a cancelamento da exportação
2025-12-28 01:45:52 -03:00
Frederico Castro
015c8f5741 fix(selos): normalizar filtro de selos para case-insensitive
Converte selos para uppercase em todas as camadas (frontend, API e
repository) para garantir que o filtro funcione independente de como
o usuário digita ou seleciona os selos.
2025-12-28 00:20:35 -03:00
49 changed files with 5506 additions and 34 deletions

View File

@@ -9,3 +9,4 @@ rich==13.7.0
oracledb==2.5.1
weasyprint>=62.3
jinja2==3.1.2
xlsxwriter==3.2.0

View File

@@ -14,17 +14,20 @@ CREATE TABLE TB_RANKING_CONSULTOR (
COMPONENTE_D NUMBER(10,2) DEFAULT 0,
COMPONENTE_E NUMBER(10,2) DEFAULT 0,
ATIVO CHAR(1) DEFAULT 'N',
E_CONSULTOR CHAR(1) DEFAULT 'N',
ANOS_ATUACAO NUMBER(5,1) DEFAULT 0,
DT_CALCULO TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
JSON_DETALHES CLOB,
CONSTRAINT PK_RANKING_CONSULTOR PRIMARY KEY (ID_PESSOA),
CONSTRAINT CHK_ATIVO CHECK (ATIVO IN ('S', 'N'))
CONSTRAINT CHK_ATIVO CHECK (ATIVO IN ('S', 'N')),
CONSTRAINT CHK_E_CONSULTOR CHECK (E_CONSULTOR IN ('S', 'N'))
);
-- Índices para performance
CREATE INDEX IDX_RANKING_POSICAO ON TB_RANKING_CONSULTOR(POSICAO);
CREATE INDEX IDX_RANKING_PONTUACAO ON TB_RANKING_CONSULTOR(PONTUACAO_TOTAL DESC);
CREATE INDEX IDX_RANKING_ATIVO ON TB_RANKING_CONSULTOR(ATIVO);
CREATE INDEX IDX_RANKING_E_CONSULTOR ON TB_RANKING_CONSULTOR(E_CONSULTOR);
CREATE INDEX IDX_RANKING_DT_CALCULO ON TB_RANKING_CONSULTOR(DT_CALCULO DESC);
-- Procedure para atualizar posições após processamento

View File

@@ -260,6 +260,7 @@ class ProcessarRankingJob:
bloco_d=int(c.get("bloco_d", 0)),
bloco_e=int(c.get("bloco_e", 0)),
ativo=bool(c.get("ativo", False)),
e_consultor=c.get("consultoria") is not None,
anos_atuacao=float(c.get("anos_atuacao", 0) or 0),
detalhes=c,
)

View File

@@ -0,0 +1,216 @@
from io import BytesIO
from datetime import datetime
from typing import List, Dict, Any, Optional
import json
import xlsxwriter
class ExcelService:
def gerar_ranking_excel(
self,
consultores: List[Dict[str, Any]],
filtros_aplicados: Optional[Dict[str, Any]] = None
) -> bytes:
output = BytesIO()
wb = xlsxwriter.Workbook(output, {'in_memory': True, 'constant_memory': True})
ws = wb.add_worksheet('Ranking Consultores')
title_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 16, 'bold': True,
'font_color': '#1F4E79', 'align': 'center', 'valign': 'vcenter'
})
subtitle_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 11,
'font_color': '#666666', 'align': 'center', 'valign': 'vcenter'
})
header_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 11, 'bold': True,
'font_color': '#FFFFFF', 'bg_color': '#1F4E79',
'align': 'center', 'valign': 'vcenter', 'text_wrap': True,
'border': 1, 'border_color': '#D9D9D9'
})
data_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 10,
'border': 1, 'border_color': '#D9D9D9'
})
data_center_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 10,
'align': 'center', 'valign': 'vcenter',
'border': 1, 'border_color': '#D9D9D9'
})
data_number_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 10,
'align': 'center', 'valign': 'vcenter',
'border': 1, 'border_color': '#D9D9D9',
'num_format': '#,##0'
})
data_decimal_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 10,
'align': 'center', 'valign': 'vcenter',
'border': 1, 'border_color': '#D9D9D9',
'num_format': '#,##0.0'
})
ativo_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 10, 'bold': True,
'align': 'center', 'valign': 'vcenter',
'bg_color': '#C6EFCE', 'font_color': '#006100',
'border': 1, 'border_color': '#D9D9D9'
})
inativo_fmt = wb.add_format({
'font_name': 'Calibri', 'font_size': 10, 'bold': True,
'align': 'center', 'valign': 'vcenter',
'bg_color': '#FFC7CE', 'font_color': '#9C0006',
'border': 1, 'border_color': '#D9D9D9'
})
ws.merge_range('A1:O1', 'RANKING DE CONSULTORES CAPES', title_fmt)
ws.set_row(0, 30)
data_geracao = datetime.now().strftime('%d/%m/%Y às %H:%M')
total = len(consultores)
filtros_texto = self._formatar_filtros(filtros_aplicados) if filtros_aplicados else ""
subtitulo = f"Gerado em {data_geracao} | Total: {total:,} consultores com pontuação"
if filtros_texto:
subtitulo += f" | Filtros: {filtros_texto}"
ws.merge_range('A2:O2', subtitulo, subtitle_fmt)
ws.set_row(1, 20)
headers = [
("Posição", 10),
("ID", 12),
("Nome", 40),
("Pontuação Total", 15),
("Bloco A\n(Coord. CAPES)", 14),
("Bloco C\n(Consultoria)", 14),
("Bloco D\n(Prêmios/Aval.)", 14),
("Bloco E\n(Coord. PPG)", 14),
("Status", 10),
("Anos Atuação", 12),
("Selos", 30),
("Coord. CAPES", 25),
("Situação Consultoria", 18),
("Prêmios", 25),
("Titulação", 35),
]
header_row = 3
for col_idx, (header_text, width) in enumerate(headers):
ws.write(header_row, col_idx, header_text, header_fmt)
ws.set_column(col_idx, col_idx, width)
ws.set_row(header_row, 35)
for row_idx, consultor in enumerate(consultores):
excel_row = header_row + 1 + row_idx
detalhes = self._parse_json_detalhes(consultor.get("JSON_DETALHES"))
selos = consultor.get("SELOS") or ""
is_ativo = consultor.get("ATIVO") == "S"
coord_capes = self._extrair_coordenacoes_resumo(detalhes)
situacao_cons = self._extrair_situacao_consultoria(detalhes)
premios = self._extrair_premios_resumo(detalhes)
titulacao = self._extrair_titulacao(detalhes)
ws.write_number(excel_row, 0, consultor.get("POSICAO") or 0, data_center_fmt)
ws.write_number(excel_row, 1, consultor.get("ID_PESSOA") or 0, data_center_fmt)
ws.write_string(excel_row, 2, consultor.get("NOME") or "", data_fmt)
ws.write_number(excel_row, 3, float(consultor.get("PONTUACAO_TOTAL") or 0), data_number_fmt)
ws.write_number(excel_row, 4, float(consultor.get("COMPONENTE_A") or 0), data_number_fmt)
ws.write_number(excel_row, 5, float(consultor.get("COMPONENTE_C") or 0), data_number_fmt)
ws.write_number(excel_row, 6, float(consultor.get("COMPONENTE_D") or 0), data_number_fmt)
ws.write_number(excel_row, 7, float(consultor.get("COMPONENTE_E") or 0), data_number_fmt)
ws.write_string(excel_row, 8, "Ativo" if is_ativo else "Inativo", ativo_fmt if is_ativo else inativo_fmt)
ws.write_number(excel_row, 9, float(consultor.get("ANOS_ATUACAO") or 0), data_decimal_fmt)
ws.write_string(excel_row, 10, selos.replace(",", ", "), data_fmt)
ws.write_string(excel_row, 11, coord_capes, data_fmt)
ws.write_string(excel_row, 12, situacao_cons, data_fmt)
ws.write_string(excel_row, 13, premios, data_fmt)
ws.write_string(excel_row, 14, titulacao, data_fmt)
last_row = header_row + len(consultores)
ws.autofilter(header_row, 0, last_row, 14)
ws.freeze_panes(header_row + 1, 0)
wb.close()
output.seek(0)
return output.getvalue()
def _parse_json_detalhes(self, json_str) -> Dict[str, Any]:
if not json_str:
return {}
if hasattr(json_str, "read"):
json_str = json_str.read()
try:
return json.loads(json_str) if isinstance(json_str, str) else json_str
except (json.JSONDecodeError, TypeError):
return {}
def _formatar_filtros(self, filtros: Dict[str, Any]) -> str:
partes = []
if filtros.get("ativo") is not None:
partes.append("Ativos" if filtros["ativo"] else "Inativos")
if filtros.get("selos"):
partes.append(f"Selos: {', '.join(filtros['selos'])}")
return " | ".join(partes)
def _extrair_coordenacoes_resumo(self, detalhes: Dict[str, Any]) -> str:
coords = detalhes.get("coordenacoes_capes", [])
if not coords:
return ""
resumos = []
for c in coords[:3]:
tipo = c.get("tipo", "")
area = c.get("area", "")
if tipo and area:
resumos.append(f"{tipo}: {area}")
elif tipo:
resumos.append(tipo)
return "; ".join(resumos)
def _extrair_situacao_consultoria(self, detalhes: Dict[str, Any]) -> str:
cons = detalhes.get("consultoria")
if not cons:
return ""
return cons.get("situacao", "")
def _extrair_premios_resumo(self, detalhes: Dict[str, Any]) -> str:
premios = detalhes.get("premiacoes", [])
if not premios:
return ""
resumos = []
for p in premios[:3]:
premio = p.get("premio", "")
tipo = p.get("tipo", "")
ano = p.get("ano", "")
if premio:
texto = f"{premio}"
if tipo:
texto += f" ({tipo})"
if ano:
texto += f" - {ano}"
resumos.append(texto)
if len(premios) > 3:
resumos.append(f"+{len(premios) - 3} outros")
return "; ".join(resumos)
def _extrair_titulacao(self, detalhes: Dict[str, Any]) -> str:
titulacao = detalhes.get("titulacao")
if titulacao and isinstance(titulacao, str):
return titulacao
lattes = detalhes.get("lattes", {})
titulacoes = lattes.get("titulacoes", []) if lattes else []
if titulacoes:
primeira = titulacoes[0]
grau = primeira.get("grau", "")
ies = primeira.get("ies_sigla", "")
ano = primeira.get("ano", "")
if grau:
texto = grau
if ies:
texto += f" - {ies}"
if ano:
texto += f" ({ano})"
return texto
return ""

View File

@@ -153,7 +153,8 @@ class Consultor:
def anos_atuacao(self) -> float:
if not self.consultoria or not self.consultoria.periodo.inicio:
return 0.0
dias = (datetime.now() - self.consultoria.periodo.inicio).days
fim = self.consultoria.periodo.fim or datetime.now()
dias = (fim - self.consultoria.periodo.inicio).days
return round(dias / 365.25, 1)
@property

View File

@@ -25,11 +25,11 @@ class RankingOracleRepository:
INSERT INTO TB_RANKING_CONSULTOR (
ID_PESSOA, NOME, PONTUACAO_TOTAL,
COMPONENTE_A, COMPONENTE_B, COMPONENTE_C, COMPONENTE_D, COMPONENTE_E,
ATIVO, ANOS_ATUACAO, JSON_DETALHES, SELOS, DT_CALCULO
ATIVO, E_CONSULTOR, ANOS_ATUACAO, JSON_DETALHES, SELOS, DT_CALCULO
) VALUES (
:id_pessoa, :nome, :pontuacao_total,
:componente_a, :componente_b, :componente_c, :componente_d, :componente_e,
:ativo, :anos_atuacao, :json_detalhes, :selos, CURRENT_TIMESTAMP
:ativo, :e_consultor, :anos_atuacao, :json_detalhes, :selos, CURRENT_TIMESTAMP
)
"""
@@ -38,6 +38,7 @@ class RankingOracleRepository:
json_str = json.dumps(consultor, ensure_ascii=False)
selos_set = extrair_selos_entry(consultor)
selos_str = ",".join(sorted(selos_set)) if selos_set else None
e_consultor = consultor.get("consultoria") is not None
batch_data.append({
"id_pessoa": int(consultor["id_pessoa"]),
"nome": str(consultor.get("nome", ""))[:500],
@@ -48,6 +49,7 @@ class RankingOracleRepository:
"componente_d": int(consultor.get("bloco_d") or consultor.get("componente_d") or 0),
"componente_e": int(consultor.get("bloco_e") or consultor.get("componente_e") or 0),
"ativo": "S" if consultor.get("ativo") else "N",
"e_consultor": "S" if e_consultor else "N",
"anos_atuacao": float(consultor.get("anos_atuacao") or 0),
"json_detalhes": json_str,
"selos": selos_str
@@ -91,14 +93,15 @@ class RankingOracleRepository:
}
if filtro_ativo is not None:
where_clauses.append("E_CONSULTOR = 'S'")
where_clauses.append("ATIVO = :ativo")
params["ativo"] = "S" if filtro_ativo else "N"
if filtro_selos:
for i, selo in enumerate(filtro_selos):
param_name = f"selo_{i}"
where_clauses.append(f"((',' || SELOS || ',') LIKE '%,' || :{param_name} || ',%')")
params[param_name] = selo
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
params[param_name] = str(selo).upper()
where_clause = ""
if where_clauses:
@@ -169,14 +172,15 @@ class RankingOracleRepository:
params = {}
if filtro_ativo is not None:
where_clauses.append("E_CONSULTOR = 'S'")
where_clauses.append("ATIVO = :ativo")
params["ativo"] = "S" if filtro_ativo else "N"
if filtro_selos:
for i, selo in enumerate(filtro_selos):
param_name = f"selo_{i}"
where_clauses.append(f"((',' || SELOS || ',') LIKE '%,' || :{param_name} || ',%')")
params[param_name] = selo
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
params[param_name] = str(selo).upper()
where_clause = ""
if where_clauses:
@@ -394,3 +398,101 @@ class RankingOracleRepository:
raise RuntimeError(f"Erro ao limpar tabela: {e2}")
finally:
cursor.close()
def buscar_para_exportacao(
self,
filtro_ativo: Optional[bool] = None,
filtro_selos: Optional[List[str]] = None,
batch_size: int = 5000
) -> List[Dict[str, Any]]:
"""
Busca todos os consultores com pontuação > 0 para exportação.
Otimizado para alto volume usando fetch em batches.
Retorna dicts crus do Oracle para processamento direto.
"""
where_clauses = ["PONTUACAO_TOTAL > 0"]
params = {}
if filtro_ativo is not None:
where_clauses.append("E_CONSULTOR = 'S'")
where_clauses.append("ATIVO = :ativo")
params["ativo"] = "S" if filtro_ativo else "N"
if filtro_selos:
for i, selo in enumerate(filtro_selos):
param_name = f"selo_{i}"
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
params[param_name] = str(selo).upper()
where_clause = " AND ".join(where_clauses)
query = f"""
SELECT
ID_PESSOA,
NOME,
POSICAO,
PONTUACAO_TOTAL,
COMPONENTE_A,
COMPONENTE_B,
COMPONENTE_C,
COMPONENTE_D,
COMPONENTE_E,
ATIVO,
ANOS_ATUACAO,
SELOS,
JSON_DETALHES
FROM TB_RANKING_CONSULTOR
WHERE {where_clause}
ORDER BY POSICAO NULLS LAST, PONTUACAO_TOTAL DESC
"""
resultados = []
with self.client.get_connection() as conn:
cursor = conn.cursor()
try:
cursor.arraysize = batch_size
cursor.execute(query, params)
colunas = [col[0] for col in cursor.description]
while True:
rows = cursor.fetchmany(batch_size)
if not rows:
break
for row in rows:
registro = dict(zip(colunas, row))
json_det = registro.get("JSON_DETALHES")
if hasattr(json_det, "read"):
registro["JSON_DETALHES"] = json_det.read()
resultados.append(registro)
finally:
cursor.close()
return resultados
def contar_para_exportacao(
self,
filtro_ativo: Optional[bool] = None,
filtro_selos: Optional[List[str]] = None
) -> int:
"""
Conta consultores com pontuação > 0 que seriam exportados.
"""
where_clauses = ["PONTUACAO_TOTAL > 0"]
params = {}
if filtro_ativo is not None:
where_clauses.append("E_CONSULTOR = 'S'")
where_clauses.append("ATIVO = :ativo")
params["ativo"] = "S" if filtro_ativo else "N"
if filtro_selos:
for i, selo in enumerate(filtro_selos):
param_name = f"selo_{i}"
where_clauses.append(f"((',' || UPPER(SELOS) || ',') LIKE '%,' || :{param_name} || ',%')")
params[param_name] = str(selo).upper()
where_clause = " AND ".join(where_clauses)
query = f"SELECT COUNT(*) AS TOTAL FROM TB_RANKING_CONSULTOR WHERE {where_clause}"
results = self.client.executar_query(query, params)
return results[0]["TOTAL"] if results else 0

View File

@@ -24,11 +24,33 @@ SELOS_DISPONIVEIS = [
"COORIENT_GP",
"COORIENT_PREMIO",
"COORIENT_MENCAO",
"ORIENT_POS_DOC",
"ORIENT_POS_DOC_PREM",
"ORIENT_TESE",
"ORIENT_TESE_PREM",
"ORIENT_DISS",
"ORIENT_DISS_PREM",
"CO_ORIENT_POS_DOC",
"CO_ORIENT_POS_DOC_PREM",
"CO_ORIENT_TESE",
"CO_ORIENT_TESE_PREM",
"CO_ORIENT_DISS",
"CO_ORIENT_DISS_PREM",
"MB_BANCA_POS_DOC",
"MB_BANCA_POS_DOC_PREM",
"MB_BANCA_TESE",
"MB_BANCA_TESE_PREM",
"MB_BANCA_DISS",
"MB_BANCA_DISS_PREM",
"IDIOMA_BILINGUE",
"IDIOMA_MULTILINGUE",
"TITULACAO_MESTRE",
"TITULACAO_DOUTOR",
"TITULACAO_POS_DOUTOR",
"BOL_BPQ_NIVEL",
"PPG_COORD",
"EVENTO",
"PROJ",
"IDIOMA_MULTILINGUE",
]
@@ -139,6 +161,7 @@ class RankingEntry:
bloco_d: int
bloco_e: int
ativo: bool
e_consultor: bool
anos_atuacao: float
detalhes: Dict[str, Any]
@@ -170,7 +193,7 @@ class RankingStore:
def total(self, filtro_ativo: Optional[bool] = None) -> int:
if filtro_ativo is None:
return len(self._entries)
return sum(1 for e in self._entries if e.ativo == filtro_ativo)
return sum(1 for e in self._entries if e.e_consultor and e.ativo == filtro_ativo)
def get_page(
self,
@@ -187,7 +210,7 @@ class RankingStore:
entries = self._entries
if filtro_ativo is not None:
entries = [e for e in entries if e.ativo == filtro_ativo]
entries = [e for e in entries if e.e_consultor and e.ativo == filtro_ativo]
if filtro_selos:
selos_set = set(filtro_selos)
@@ -212,7 +235,7 @@ class RankingStore:
if filtro_ativo is None:
entries = self._entries
else:
entries = [e for e in self._entries if e.ativo == filtro_ativo]
entries = [e for e in self._entries if e.e_consultor and e.ativo == filtro_ativo]
total = len(entries)
return total, entries[offset : offset + limit]

View File

@@ -222,13 +222,24 @@ class ConsultorRepositoryImpl(ConsultorRepository):
mesclados = mesclar_periodos(periodos)
periodo_ativo = next((p for p in mesclados if p.ativo), None)
anos_consecutivos = periodo_ativo.anos_completos(datetime.now()) if periodo_ativo else 0
if periodo_ativo:
anos_consecutivos = periodo_ativo.anos_completos(datetime.now())
elif mesclados:
anos_list = [p.anos_completos(p.fim or datetime.now()) for p in mesclados]
anos_consecutivos = max(anos_list) if anos_list else 0
else:
anos_consecutivos = 0
retornos = max(0, len(mesclados) - 1)
ativo = any(p.ativo for p in periodos)
situacao_final = situacoes[-1] if situacoes else "N/A"
is_ativo = ativo or "atividade" in situacao_final.lower() or "ativo" in situacao_final.lower()
is_falecido = "falecido" in situacao_final.lower()
situacao_lower = situacao_final.lower()
is_ativo = ativo or (
("atividade" in situacao_lower or "ativo" in situacao_lower)
and "inatividade" not in situacao_lower
and "inativo" not in situacao_lower
)
is_falecido = "falecido" in situacao_lower
if is_falecido:
codigo = "CONS_FALECIDO"

View File

@@ -214,11 +214,15 @@ async def ranking_paginado(
if not oracle_repo:
raise HTTPException(status_code=503, detail="Oracle não configurado")
selos_lista = [s.strip() for s in selos.split(",") if s.strip()] if selos else None
selos_lista = (
[s.strip().upper() for s in selos.split(",") if s.strip()]
if selos
else None
)
total = oracle_repo.contar_total(filtro_ativo=ativo, filtro_selos=selos_lista)
if total == 0:
if selos_lista:
if selos_lista or ativo is not None:
return RankingPaginadoResponseSchema(
total=0, page=page, size=size, total_pages=0, consultores=[]
)
@@ -988,3 +992,78 @@ async def listar_areas_avaliacao(
return [AreaAvaliacaoSchema(**a) for a in areas]
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao listar areas de avaliacao: {str(e)}")
@router.get("/ranking/exportar/excel")
async def exportar_ranking_excel(
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
selos: Optional[str] = Query(default=None, description="Filtrar por selos (separados por vírgula)"),
oracle_repo = Depends(get_ranking_oracle_repo),
):
from ...application.services.excel_service import ExcelService
if not oracle_repo:
raise HTTPException(status_code=503, detail="Oracle não configurado")
selos_lista = (
[s.strip().upper() for s in selos.split(",") if s.strip()]
if selos
else None
)
total = oracle_repo.contar_para_exportacao(filtro_ativo=ativo, filtro_selos=selos_lista)
if total == 0:
raise HTTPException(
status_code=404,
detail="Nenhum consultor com pontuação encontrado para os filtros aplicados."
)
consultores = oracle_repo.buscar_para_exportacao(
filtro_ativo=ativo,
filtro_selos=selos_lista
)
filtros = {"ativo": ativo, "selos": selos_lista}
excel_service = ExcelService()
excel_bytes = excel_service.gerar_ranking_excel(consultores, filtros)
data_atual = datetime.now().strftime('%Y%m%d_%H%M')
nome_arquivo = f"ranking_consultores_capes_{data_atual}.xlsx"
from fastapi.responses import Response
return Response(
content=excel_bytes,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f'attachment; filename="{nome_arquivo}"',
"Content-Length": str(len(excel_bytes)),
"Cache-Control": "no-cache"
}
)
@router.get("/ranking/exportar/info")
async def info_exportacao(
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
selos: Optional[str] = Query(default=None, description="Filtrar por selos (separados por vírgula)"),
oracle_repo = Depends(get_ranking_oracle_repo),
):
if not oracle_repo:
raise HTTPException(status_code=503, detail="Oracle não configurado")
selos_lista = (
[s.strip().upper() for s in selos.split(",") if s.strip()]
if selos
else None
)
total = oracle_repo.contar_para_exportacao(filtro_ativo=ativo, filtro_selos=selos_lista)
return {
"total_consultores": total,
"filtros": {
"ativo": ativo,
"selos": selos_lista
},
"estimativa_tamanho_mb": round(total * 0.003, 2)
}

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, e_consultor=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, e_consultor=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, e_consultor=True, 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, e_consultor=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, e_consultor=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

View File

@@ -0,0 +1,384 @@
import pytest
from io import BytesIO
import json
from openpyxl import load_workbook
from src.application.services.excel_service import ExcelService
@pytest.fixture
def excel_service():
return ExcelService()
@pytest.fixture
def consultor_basico():
return {
"POSICAO": 1,
"ID_PESSOA": 12345,
"NOME": "MARIA SILVA SANTOS",
"PONTUACAO_TOTAL": 450.0,
"COMPONENTE_A": 200.0,
"COMPONENTE_C": 150.0,
"COMPONENTE_D": 50.0,
"COMPONENTE_E": 50.0,
"ATIVO": "S",
"ANOS_ATUACAO": 10.5,
"SELOS": "CA,CONS_ATIVO,AUTOR_GP",
"JSON_DETALHES": json.dumps({
"coordenacoes_capes": [
{"tipo": "CA", "area": "CIÊNCIAS DA COMPUTAÇÃO"}
],
"consultoria": {"situacao": "Atividade Contínua"},
"premiacoes": [
{"premio": "PCT", "tipo": "Grande Prêmio", "ano": 2023}
],
"titulacao": "Doutorado - USP (2010)"
})
}
@pytest.fixture
def consultor_inativo():
return {
"POSICAO": 100,
"ID_PESSOA": 99999,
"NOME": "JOÃO PEREIRA",
"PONTUACAO_TOTAL": 100.0,
"COMPONENTE_A": 0.0,
"COMPONENTE_C": 100.0,
"COMPONENTE_D": 0.0,
"COMPONENTE_E": 0.0,
"ATIVO": "N",
"ANOS_ATUACAO": 3.0,
"SELOS": "",
"JSON_DETALHES": None
}
class TestGerarRankingExcel:
def test_gerar_excel_lista_vazia(self, excel_service):
resultado = excel_service.gerar_ranking_excel([])
assert isinstance(resultado, bytes)
assert len(resultado) > 0
wb = load_workbook(BytesIO(resultado))
ws = wb.active
assert ws.title == "Ranking Consultores"
def test_gerar_excel_um_consultor(self, excel_service, consultor_basico):
resultado = excel_service.gerar_ranking_excel([consultor_basico])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
assert ws["A1"].value == "RANKING DE CONSULTORES CAPES"
assert ws["A5"].value == 1
assert ws["B5"].value == 12345
assert ws["C5"].value == "MARIA SILVA SANTOS"
assert ws["D5"].value == 450.0
def test_gerar_excel_multiplos_consultores(self, excel_service, consultor_basico, consultor_inativo):
consultores = [consultor_basico, consultor_inativo]
resultado = excel_service.gerar_ranking_excel(consultores)
wb = load_workbook(BytesIO(resultado))
ws = wb.active
assert ws["C5"].value == "MARIA SILVA SANTOS"
assert ws["C6"].value == "JOÃO PEREIRA"
def test_gerar_excel_com_filtros(self, excel_service, consultor_basico):
filtros = {"ativo": True, "selos": ["CA", "CONS_ATIVO"]}
resultado = excel_service.gerar_ranking_excel([consultor_basico], filtros_aplicados=filtros)
wb = load_workbook(BytesIO(resultado))
ws = wb.active
subtitulo = ws["A2"].value
assert "Ativos" in subtitulo
assert "CA" in subtitulo
def test_gerar_excel_consultor_ativo(self, excel_service, consultor_basico):
resultado = excel_service.gerar_ranking_excel([consultor_basico])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
assert ws["I5"].value == "Ativo"
def test_gerar_excel_consultor_inativo(self, excel_service, consultor_inativo):
resultado = excel_service.gerar_ranking_excel([consultor_inativo])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
assert ws["I5"].value == "Inativo"
def test_gerar_excel_extrai_coordenacoes(self, excel_service, consultor_basico):
resultado = excel_service.gerar_ranking_excel([consultor_basico])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
coord_cell = ws["L5"].value
assert "CA" in coord_cell
assert "CIÊNCIAS DA COMPUTAÇÃO" in coord_cell
def test_gerar_excel_extrai_consultoria(self, excel_service, consultor_basico):
resultado = excel_service.gerar_ranking_excel([consultor_basico])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
assert ws["M5"].value == "Atividade Contínua"
def test_gerar_excel_extrai_premios(self, excel_service, consultor_basico):
resultado = excel_service.gerar_ranking_excel([consultor_basico])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
premios_cell = ws["N5"].value
assert "PCT" in premios_cell
assert "2023" in premios_cell
def test_gerar_excel_extrai_titulacao(self, excel_service, consultor_basico):
resultado = excel_service.gerar_ranking_excel([consultor_basico])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
assert "Doutorado" in ws["O5"].value
def test_gerar_excel_selos_formatados(self, excel_service, consultor_basico):
resultado = excel_service.gerar_ranking_excel([consultor_basico])
wb = load_workbook(BytesIO(resultado))
ws = wb.active
selos_cell = ws["K5"].value
assert "CA" in selos_cell
assert ", " in selos_cell
class TestParseJsonDetalhes:
def test_parse_json_string_valido(self, excel_service):
json_str = '{"coordenacoes_capes": [{"tipo": "CA"}]}'
resultado = excel_service._parse_json_detalhes(json_str)
assert resultado == {"coordenacoes_capes": [{"tipo": "CA"}]}
def test_parse_json_none(self, excel_service):
resultado = excel_service._parse_json_detalhes(None)
assert resultado == {}
def test_parse_json_string_vazia(self, excel_service):
resultado = excel_service._parse_json_detalhes("")
assert resultado == {}
def test_parse_json_invalido(self, excel_service):
resultado = excel_service._parse_json_detalhes("{invalid json")
assert resultado == {}
def test_parse_json_dict_direto(self, excel_service):
dados = {"coordenacoes_capes": []}
resultado = excel_service._parse_json_detalhes(dados)
assert resultado == dados
def test_parse_json_file_like_object(self, excel_service):
class MockLob:
def read(self):
return '{"test": "value"}'
resultado = excel_service._parse_json_detalhes(MockLob())
assert resultado == {"test": "value"}
class TestFormatarFiltros:
def test_formatar_filtros_ativo_true(self, excel_service):
filtros = {"ativo": True}
resultado = excel_service._formatar_filtros(filtros)
assert resultado == "Ativos"
def test_formatar_filtros_ativo_false(self, excel_service):
filtros = {"ativo": False}
resultado = excel_service._formatar_filtros(filtros)
assert resultado == "Inativos"
def test_formatar_filtros_com_selos(self, excel_service):
filtros = {"selos": ["CA", "CAJ"]}
resultado = excel_service._formatar_filtros(filtros)
assert "Selos: CA, CAJ" in resultado
def test_formatar_filtros_combinados(self, excel_service):
filtros = {"ativo": True, "selos": ["CA"]}
resultado = excel_service._formatar_filtros(filtros)
assert "Ativos" in resultado
assert "Selos: CA" in resultado
def test_formatar_filtros_vazio(self, excel_service):
filtros = {}
resultado = excel_service._formatar_filtros(filtros)
assert resultado == ""
class TestExtrairCoordenacoesResumo:
def test_extrair_coordenacoes_vazio(self, excel_service):
resultado = excel_service._extrair_coordenacoes_resumo({})
assert resultado == ""
def test_extrair_coordenacoes_lista_vazia(self, excel_service):
resultado = excel_service._extrair_coordenacoes_resumo({"coordenacoes_capes": []})
assert resultado == ""
def test_extrair_coordenacoes_com_tipo_e_area(self, excel_service):
detalhes = {
"coordenacoes_capes": [
{"tipo": "CA", "area": "MATEMÁTICA"}
]
}
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
assert resultado == "CA: MATEMÁTICA"
def test_extrair_coordenacoes_apenas_tipo(self, excel_service):
detalhes = {
"coordenacoes_capes": [
{"tipo": "CAJ"}
]
}
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
assert resultado == "CAJ"
def test_extrair_coordenacoes_multiplas(self, excel_service):
detalhes = {
"coordenacoes_capes": [
{"tipo": "CA", "area": "FÍSICA"},
{"tipo": "CAJ", "area": "QUÍMICA"}
]
}
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
assert "CA: FÍSICA" in resultado
assert "CAJ: QUÍMICA" in resultado
assert ";" in resultado
def test_extrair_coordenacoes_maximo_tres(self, excel_service):
detalhes = {
"coordenacoes_capes": [
{"tipo": "CA", "area": "A"},
{"tipo": "CAJ", "area": "B"},
{"tipo": "CAM", "area": "C"},
{"tipo": "CAJ_MP", "area": "D"},
]
}
resultado = excel_service._extrair_coordenacoes_resumo(detalhes)
assert "CA: A" in resultado
assert "CAJ: B" in resultado
assert "CAM: C" in resultado
assert "CAJ_MP" not in resultado
class TestExtrairSituacaoConsultoria:
def test_extrair_situacao_vazio(self, excel_service):
resultado = excel_service._extrair_situacao_consultoria({})
assert resultado == ""
def test_extrair_situacao_sem_consultoria(self, excel_service):
resultado = excel_service._extrair_situacao_consultoria({"consultoria": None})
assert resultado == ""
def test_extrair_situacao_ativa(self, excel_service):
detalhes = {"consultoria": {"situacao": "Atividade Contínua"}}
resultado = excel_service._extrair_situacao_consultoria(detalhes)
assert resultado == "Atividade Contínua"
class TestExtrairPremiosResumo:
def test_extrair_premios_vazio(self, excel_service):
resultado = excel_service._extrair_premios_resumo({})
assert resultado == ""
def test_extrair_premios_lista_vazia(self, excel_service):
resultado = excel_service._extrair_premios_resumo({"premiacoes": []})
assert resultado == ""
def test_extrair_premio_completo(self, excel_service):
detalhes = {
"premiacoes": [
{"premio": "PCT", "tipo": "Grande Prêmio", "ano": 2023}
]
}
resultado = excel_service._extrair_premios_resumo(detalhes)
assert "PCT" in resultado
assert "Grande Prêmio" in resultado
assert "2023" in resultado
def test_extrair_premio_apenas_nome(self, excel_service):
detalhes = {
"premiacoes": [{"premio": "PCT"}]
}
resultado = excel_service._extrair_premios_resumo(detalhes)
assert resultado == "PCT"
def test_extrair_premios_mais_de_tres(self, excel_service):
detalhes = {
"premiacoes": [
{"premio": "A"},
{"premio": "B"},
{"premio": "C"},
{"premio": "D"},
{"premio": "E"},
]
}
resultado = excel_service._extrair_premios_resumo(detalhes)
assert "A" in resultado
assert "B" in resultado
assert "C" in resultado
assert "+2 outros" in resultado
class TestExtrairTitulacao:
def test_extrair_titulacao_vazio(self, excel_service):
resultado = excel_service._extrair_titulacao({})
assert resultado == ""
def test_extrair_titulacao_string_direta(self, excel_service):
detalhes = {"titulacao": "Doutorado - UNICAMP (2015)"}
resultado = excel_service._extrair_titulacao(detalhes)
assert resultado == "Doutorado - UNICAMP (2015)"
def test_extrair_titulacao_do_lattes(self, excel_service):
detalhes = {
"lattes": {
"titulacoes": [
{"grau": "Doutorado", "ies_sigla": "USP", "ano": 2010}
]
}
}
resultado = excel_service._extrair_titulacao(detalhes)
assert "Doutorado" in resultado
assert "USP" in resultado
assert "2010" in resultado
def test_extrair_titulacao_lattes_apenas_grau(self, excel_service):
detalhes = {
"lattes": {
"titulacoes": [{"grau": "Mestrado"}]
}
}
resultado = excel_service._extrair_titulacao(detalhes)
assert resultado == "Mestrado"
def test_extrair_titulacao_lattes_vazio(self, excel_service):
detalhes = {"lattes": {"titulacoes": []}}
resultado = excel_service._extrair_titulacao(detalhes)
assert resultado == ""

View File

@@ -0,0 +1,383 @@
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
from dataclasses import dataclass
from src.application.services.pdf_service import PDFService, ConsultorWrapper, DictWrapper
class TestFormatDate:
def test_format_date_vazio(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date("")
assert resultado == "-"
def test_format_date_none(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date(None)
assert resultado == "-"
def test_format_date_formato_brasileiro(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date("25/12/2023")
assert resultado == "25/12/2023"
def test_format_date_iso(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date("2023-12-25T10:30:00")
assert resultado == "25/12/2023"
def test_format_date_iso_com_z(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date("2023-12-25T10:30:00Z")
assert resultado == "25/12/2023"
def test_format_date_invalido(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date("data-invalida")
assert resultado == "data-invalida"
class TestFormatDateShort:
def test_format_date_short_vazio(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date_short("")
assert resultado == "-"
def test_format_date_short_none(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date_short(None)
assert resultado == "-"
def test_format_date_short_brasileiro_3_partes(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date_short("25/12/2023")
assert resultado == "Dez/2023"
def test_format_date_short_brasileiro_2_partes(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date_short("12/2023")
assert resultado == "Dez/2023"
def test_format_date_short_iso(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date_short("2023-06-15T10:30:00")
assert resultado == "Jun/2023"
def test_format_date_short_janeiro(self):
service = PDFService.__new__(PDFService)
resultado = service._format_date_short("15/01/2024")
assert resultado == "Jan/2024"
class TestSortByDate:
def test_sort_by_date_lista_vazia(self):
service = PDFService.__new__(PDFService)
resultado = service._sort_by_date([], "data")
assert resultado == []
def test_sort_by_date_sem_campo(self):
service = PDFService.__new__(PDFService)
items = [{"nome": "A"}, {"nome": "B"}]
resultado = service._sort_by_date(items)
assert resultado == items
def test_sort_by_date_ordena_desc(self):
service = PDFService.__new__(PDFService)
items = [
{"nome": "A", "data": "2020-01-01"},
{"nome": "B", "data": "2023-01-01"},
{"nome": "C", "data": "2021-01-01"},
]
resultado = service._sort_by_date(items, "data")
assert resultado[0]["nome"] == "B"
assert resultado[1]["nome"] == "C"
assert resultado[2]["nome"] == "A"
def test_sort_by_date_formato_brasileiro(self):
service = PDFService.__new__(PDFService)
items = [
{"nome": "A", "data": "01/01/2020"},
{"nome": "B", "data": "01/01/2023"},
]
resultado = service._sort_by_date(items, "data")
assert resultado[0]["nome"] == "B"
def test_sort_by_date_com_ano_numerico(self):
service = PDFService.__new__(PDFService)
items = [
{"nome": "A", "ano": 2020},
{"nome": "B", "ano": 2023},
]
resultado = service._sort_by_date(items, "ano")
assert resultado[0]["nome"] == "B"
def test_sort_by_date_com_ano_string(self):
service = PDFService.__new__(PDFService)
items = [
{"nome": "A", "ano": "2020"},
{"nome": "B", "ano": "2023"},
]
resultado = service._sort_by_date(items, "ano")
assert resultado[0]["nome"] == "B"
def test_sort_by_date_valores_nulos_no_final(self):
service = PDFService.__new__(PDFService)
items = [
{"nome": "A", "data": None},
{"nome": "B", "data": "2023-01-01"},
{"nome": "C", "data": ""},
]
resultado = service._sort_by_date(items, "data")
assert resultado[0]["nome"] == "B"
def test_sort_by_date_fallback_campos(self):
service = PDFService.__new__(PDFService)
items = [
{"nome": "A", "inicio": "2020-01-01"},
{"nome": "B", "data": "2023-01-01"},
]
resultado = service._sort_by_date(items, "data", "inicio")
assert resultado[0]["nome"] == "B"
assert resultado[1]["nome"] == "A"
class TestConsultorToDict:
def test_consultor_to_dict_dict(self):
service = PDFService.__new__(PDFService)
dados = {"nome": "Teste", "id": 123}
resultado = service._consultor_to_dict(dados)
assert resultado == dados
def test_consultor_to_dict_dataclass(self):
@dataclass
class ConsultorTest:
nome: str
id: int
service = PDFService.__new__(PDFService)
consultor = ConsultorTest(nome="Teste", id=123)
resultado = service._consultor_to_dict(consultor)
assert resultado == {"nome": "Teste", "id": 123}
def test_consultor_to_dict_pydantic_model_dump(self):
service = PDFService.__new__(PDFService)
mock_obj = MagicMock()
mock_obj.model_dump.return_value = {"nome": "Teste"}
del mock_obj.dict
resultado = service._consultor_to_dict(mock_obj)
assert resultado == {"nome": "Teste"}
def test_consultor_to_dict_pydantic_dict(self):
service = PDFService.__new__(PDFService)
mock_obj = MagicMock(spec=['dict'])
mock_obj.dict.return_value = {"nome": "Teste"}
resultado = service._consultor_to_dict(mock_obj)
assert resultado == {"nome": "Teste"}
class TestExtrairPontuacaoCoord:
def test_extrair_pontuacao_coord_vazio(self):
service = PDFService.__new__(PDFService)
resultado = service._extrair_pontuacao_coord({})
assert resultado == {}
def test_extrair_pontuacao_coord_sem_pontuacao(self):
service = PDFService.__new__(PDFService)
resultado = service._extrair_pontuacao_coord({"nome": "Teste"})
assert resultado == {}
def test_extrair_pontuacao_coord_com_atuacoes(self):
service = PDFService.__new__(PDFService)
dados = {
"pontuacao": {
"bloco_a": {
"atuacoes": [
{"codigo": "CA", "base": 200, "tempo": 50, "bonus": 30, "total": 280},
{"codigo": "CAJ", "base": 150, "tempo": 40, "bonus": 20, "total": 210},
]
}
}
}
resultado = service._extrair_pontuacao_coord(dados)
assert "CA" in resultado
assert resultado["CA"]["base"] == 200
assert resultado["CA"]["tempo"] == 50
assert resultado["CA"]["total"] == 280
assert "CAJ" in resultado
class TestConsultorWrapper:
def test_wrapper_acesso_atributo_simples(self):
wrapper = ConsultorWrapper({"nome": "Teste", "id": 123})
assert wrapper.nome == "Teste"
assert wrapper.id == 123
def test_wrapper_atributo_inexistente(self):
wrapper = ConsultorWrapper({"nome": "Teste"})
assert wrapper.id is None
def test_wrapper_atributo_dict_retorna_dictwrapper(self):
wrapper = ConsultorWrapper({
"dados": {"campo": "valor"}
})
assert isinstance(wrapper.dados, DictWrapper)
assert wrapper.dados.campo == "valor"
def test_wrapper_atributo_lista_de_dicts(self):
wrapper = ConsultorWrapper({
"itens": [{"nome": "A"}, {"nome": "B"}]
})
assert len(wrapper.itens) == 2
assert isinstance(wrapper.itens[0], DictWrapper)
assert wrapper.itens[0].nome == "A"
def test_wrapper_bool_true(self):
wrapper = ConsultorWrapper({"nome": "Teste"})
assert bool(wrapper) is True
def test_wrapper_bool_false(self):
wrapper = ConsultorWrapper({})
assert bool(wrapper) is False
class TestDictWrapper:
def test_dictwrapper_acesso_atributo(self):
wrapper = DictWrapper({"campo": "valor"})
assert wrapper.campo == "valor"
def test_dictwrapper_atributo_inexistente(self):
wrapper = DictWrapper({"campo": "valor"})
assert wrapper.outro is None
def test_dictwrapper_aninhado(self):
wrapper = DictWrapper({
"nivel1": {"nivel2": {"nivel3": "valor"}}
})
assert wrapper.nivel1.nivel2.nivel3 == "valor"
def test_dictwrapper_lista(self):
wrapper = DictWrapper({
"itens": [{"a": 1}, {"b": 2}]
})
assert len(wrapper.itens) == 2
assert wrapper.itens[0].a == 1
def test_dictwrapper_get(self):
wrapper = DictWrapper({"campo": "valor"})
assert wrapper.get("campo") == "valor"
assert wrapper.get("inexistente", "default") == "default"
def test_dictwrapper_str(self):
wrapper = DictWrapper({"campo": "valor"})
assert "campo" in str(wrapper)
def test_dictwrapper_bool_true(self):
wrapper = DictWrapper({"campo": "valor"})
assert bool(wrapper) is True
def test_dictwrapper_bool_false(self):
wrapper = DictWrapper({})
assert bool(wrapper) is False
class TestGerarFichaConsultor:
def test_gerar_ficha_consultor_mock(self):
import sys
mock_weasyprint = MagicMock()
mock_weasyprint.HTML.return_value.write_pdf.return_value = b"PDF_CONTENT"
mock_weasyprint.CSS.return_value = MagicMock()
sys.modules['weasyprint'] = mock_weasyprint
try:
with patch.object(PDFService, '__init__', lambda self: None):
service = PDFService()
service.template = MagicMock()
service.template.render.return_value = "<html></html>"
from pathlib import Path
service.template_dir = Path("/tmp")
consultor = {"nome": "Teste", "id": 123}
resultado = service.gerar_ficha_consultor(consultor)
assert resultado == b"PDF_CONTENT"
service.template.render.assert_called_once()
finally:
del sys.modules['weasyprint']
class TestGerarPdfEquipe:
def test_gerar_pdf_equipe_mock(self):
import sys
mock_weasyprint = MagicMock()
mock_weasyprint.HTML.return_value.write_pdf.return_value = b"PDF_EQUIPE"
mock_weasyprint.CSS.return_value = MagicMock()
sys.modules['weasyprint'] = mock_weasyprint
try:
with patch.object(PDFService, '__init__', lambda self: None):
service = PDFService()
service.env = MagicMock()
mock_template = MagicMock()
mock_template.render.return_value = "<html></html>"
service.env.get_template.return_value = mock_template
from pathlib import Path
service.template_dir = Path("/tmp")
consultores = [
{"nome": "A", "ies": "USP", "areas_avaliacao": ["FÍSICA"], "situacao": "Atividade Contínua", "foi_coordenador": True, "foi_premiado": False},
{"nome": "B", "ies": "UNICAMP", "areas_avaliacao": ["QUÍMICA"], "situacao": "Inativo", "foi_coordenador": False, "foi_premiado": True},
]
resultado = service.gerar_pdf_equipe("Tema X", "FÍSICA", consultores)
assert resultado == b"PDF_EQUIPE"
mock_template.render.assert_called_once()
call_kwargs = mock_template.render.call_args[1]
assert call_kwargs["tema"] == "Tema X"
assert call_kwargs["area_avaliacao"] == "FÍSICA"
assert call_kwargs["estatisticas"]["total"] == 2
assert call_kwargs["estatisticas"]["coordenadores"] == 1
assert call_kwargs["estatisticas"]["premiados"] == 1
assert call_kwargs["estatisticas"]["ies_distintas"] == 2
finally:
del sys.modules['weasyprint']
def test_estatisticas_equipe_ativos(self):
import sys
mock_weasyprint = MagicMock()
mock_weasyprint.HTML.return_value.write_pdf.return_value = b"PDF"
mock_weasyprint.CSS.return_value = MagicMock()
sys.modules['weasyprint'] = mock_weasyprint
try:
with patch.object(PDFService, '__init__', lambda self: None):
service = PDFService()
service.env = MagicMock()
mock_template = MagicMock()
mock_template.render.return_value = "<html></html>"
service.env.get_template.return_value = mock_template
from pathlib import Path
service.template_dir = Path("/tmp")
consultores = [
{"nome": "A", "situacao": "Atividade Contínua"},
{"nome": "B", "situacao": "Atividade Contínua"},
{"nome": "C", "situacao": "Inativo"},
]
service.gerar_pdf_equipe("Tema", "ÁREA", consultores)
call_kwargs = mock_template.render.call_args[1]
assert call_kwargs["estatisticas"]["ativos"] == 2
finally:
del sys.modules['weasyprint']

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,232 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
import pytest
from src.domain.services.calculador_pontuacao import CalculadorPontuacao
from src.domain.value_objects.criterios_pontuacao import CRITERIOS
from src.domain.value_objects.periodo import Periodo
from src.domain.entities.consultor import (
Consultoria,
CoordenacaoCapes,
Inscricao,
AvaliacaoComissao,
Participacao,
)
PDF_BASE_TETO = {
"CA": (200, 450),
"CAJ": (150, 370),
"CAJ_MP": (120, 315),
"CAM": (100, 280),
"PPG_COORD": (0, 0),
"CONS_ATIVO": (150, 230),
"CONS_HIST": (100, 230),
"CONS_FALECIDO": (100, 230),
"INSC_AUTOR": (10, 20),
"INSC_INST_AUTOR": (20, 50),
"AVAL_COMIS_PREMIO": (30, 60),
"AVAL_COMIS_GP": (40, 80),
"COORD_COMIS_PREMIO": (40, 100),
"COORD_COMIS_GP": (50, 120),
"BOL_BPQ_NIVEL": (30, 60),
"PREMIACAO_GP_AUTOR": (100, 300),
"PREMIACAO_AUTOR": (50, 150),
"MENCAO_AUTOR": (30, 90),
"EVENTO": (1, 5),
"PROJ": (10, 30),
"IDIOMA_BILINGUE": (0, 0),
"IDIOMA_MULTILINGUE": (0, 0),
"TITULACAO_MESTRE": (0, 0),
"TITULACAO_DOUTOR": (0, 0),
"TITULACAO_POS_DOUTOR": (0, 0),
"ORIENT_POS_DOC": (0, 0),
"ORIENT_POS_DOC_PREM": (0, 0),
"ORIENT_TESE": (0, 0),
"ORIENT_TESE_PREM": (0, 0),
"ORIENT_DISS": (0, 0),
"ORIENT_DISS_PREM": (0, 0),
"CO_ORIENT_POS_DOC": (0, 0),
"CO_ORIENT_POS_DOC_PREM": (0, 0),
"CO_ORIENT_TESE": (0, 0),
"CO_ORIENT_TESE_PREM": (0, 0),
"CO_ORIENT_DISS": (0, 0),
"CO_ORIENT_DISS_PREM": (0, 0),
"MB_BANCA_POS_DOC": (0, 0),
"MB_BANCA_POS_DOC_PREM": (0, 0),
"MB_BANCA_TESE": (0, 0),
"MB_BANCA_TESE_PREM": (0, 0),
"MB_BANCA_DISS": (0, 0),
"MB_BANCA_DISS_PREM": (0, 0),
}
PDF_TEMPO = {
"CA": (10, 100),
"CAJ": (8, 80),
"CAJ_MP": (6, 60),
"CAM": (5, 50),
"PPG_COORD": (0, 0),
"CONS_ATIVO": (5, 50),
"CONS_HIST": (5, 50),
"CONS_FALECIDO": (5, 50),
}
PDF_BONUS = {
"CA": {"atualidade": 30, "retorno": 20},
"CAJ": {"atualidade": 20, "retorno": 15},
"CAJ_MP": {"atualidade": 15, "retorno": 10},
"CAM": {"atualidade": 20, "retorno": 10},
"PPG_COORD": {"atualidade": 15, "retorno": 10, "continuidade": 15},
"CONS_ATIVO": {"atualidade": 20, "retorno": 15, "continuidade": 20},
"CONS_HIST": {"retorno": 20, "continuidade": 20},
"CONS_FALECIDO": {"continuidade": 20},
}
PDF_RECORRENCIA = {
"INSC_AUTOR": {"por_participacao": 2, "teto_participacao": 10},
"INSC_INST_AUTOR": {"por_participacao": 5, "teto_participacao": 10},
"AVAL_COMIS_PREMIO": {"por_ano": 2, "teto_ano": 15},
"AVAL_COMIS_GP": {"por_ano": 3, "teto_ano": 20},
"COORD_COMIS_PREMIO": {"por_ano": 4, "teto_ano": 20},
"COORD_COMIS_GP": {"por_ano": 6, "teto_ano": 20},
"EVENTO": {"por_participacao": 1, "teto_participacao": 10},
"PROJ": {"por_participacao": 2, "teto_participacao": 10},
}
def periodo_anos(anos: int, ativo: bool = False) -> Periodo:
inicio = datetime.now() - relativedelta(years=anos)
fim = None if ativo else datetime.now()
return Periodo(inicio=inicio, fim=fim)
@pytest.mark.parametrize("codigo,base_teto", PDF_BASE_TETO.items())
def test_pdf_base_teto(codigo, base_teto):
criterio = CRITERIOS.get(codigo)
assert criterio is not None
base, teto = base_teto
assert criterio.base == base
assert criterio.teto == teto
def test_pdf_criterios_cobrem_43_codigos():
assert set(CRITERIOS.keys()) == set(PDF_BASE_TETO.keys())
assert len(CRITERIOS) == 43
@pytest.mark.parametrize("codigo,tempo", PDF_TEMPO.items())
def test_pdf_regras_tempo(codigo, tempo):
criterio = CRITERIOS[codigo]
multiplicador, teto_tempo = tempo
assert criterio.pontua_tempo is True
assert criterio.multiplicador_tempo == multiplicador
assert criterio.teto_tempo == teto_tempo
@pytest.mark.parametrize("codigo", sorted(set(PDF_BASE_TETO) - set(PDF_TEMPO)))
def test_pdf_codigos_sem_tempo(codigo):
criterio = CRITERIOS[codigo]
assert criterio.pontua_tempo is False
assert criterio.multiplicador_tempo == 0
assert criterio.teto_tempo == 0
@pytest.mark.parametrize("codigo,bonus", PDF_BONUS.items())
def test_pdf_bonus_configurados(codigo, bonus):
criterio = CRITERIOS[codigo]
assert criterio.bonus_atualidade == bonus.get("atualidade", 0)
assert criterio.bonus_retorno == bonus.get("retorno", 0)
assert criterio.bonus_continuidade_8anos == bonus.get("continuidade", 0)
@pytest.mark.parametrize("codigo,rec", PDF_RECORRENCIA.items())
def test_pdf_regras_recorrencia(codigo, rec):
criterio = CRITERIOS[codigo]
assert criterio.bonus_recorrencia_anual == rec.get("por_ano", 0)
assert criterio.teto_recorrencia == rec.get("teto_ano", 0)
assert criterio.bonus_recorrencia_participacao == rec.get("por_participacao", 0)
assert criterio.teto_recorrencia_participacao == rec.get("teto_participacao", 0)
def test_calculo_tempo_e_bonus_ca():
periodo_historico = Periodo(
inicio=datetime.now() - relativedelta(years=12),
fim=datetime.now() - relativedelta(years=2),
)
periodo_ativo = periodo_anos(2, ativo=True)
coords = [
CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Area",
area_avaliacao="AREA",
periodo=periodo_historico,
),
CoordenacaoCapes(
codigo="CA",
tipo="Coordenador de Area",
area_avaliacao="AREA",
periodo=periodo_ativo,
),
]
resultado = CalculadorPontuacao.calcular_bloco_a(coords)
atuacao = resultado.atuacoes[0]
assert atuacao.tempo == 100
assert atuacao.bonus == 50
def test_calculo_bonus_consultoria_completo():
consultoria = Consultoria(
codigo="CONS_ATIVO",
situacao="Ativo",
periodo=periodo_anos(9, ativo=True),
periodos=[periodo_anos(9, ativo=True)],
anos_consecutivos=9,
retornos=1,
)
resultado = CalculadorPontuacao.calcular_bloco_b(consultoria)
atuacao = resultado.atuacoes[0]
assert atuacao.bonus == 55
def test_calculo_recorrencia_inscricao_autor_bonus():
inscricoes = [
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2020),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2021),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2022),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2023),
Inscricao(codigo="INSC_AUTOR", tipo="Autor", premio="PCT", ano=2024),
]
resultado = CalculadorPontuacao.calcular_bloco_c(inscricoes, [], [], [], [])
atuacao = next(a for a in resultado.atuacoes if a.codigo == "INSC_AUTOR")
assert atuacao.bonus == 10
def test_calculo_recorrencia_avaliacao_gp_bonus():
avaliacoes = [
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2021),
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2022),
AvaliacaoComissao(codigo="AVAL_COMIS_GP", tipo="Membro", premio="GP", ano=2023),
]
resultado = CalculadorPontuacao.calcular_bloco_c([], avaliacoes, [], [], [])
atuacao = next(a for a in resultado.atuacoes if a.codigo == "AVAL_COMIS_GP")
assert atuacao.bonus == 9
def test_calculo_recorrencia_evento_bonus():
eventos = [
Participacao(codigo="EVENTO", tipo="Evento", ano=2020),
Participacao(codigo="EVENTO", tipo="Evento", ano=2021),
Participacao(codigo="EVENTO", tipo="Evento", ano=2022),
Participacao(codigo="EVENTO", tipo="Evento", ano=2023),
Participacao(codigo="EVENTO", tipo="Evento", ano=2024),
Participacao(codigo="EVENTO", tipo="Evento", ano=2025),
Participacao(codigo="EVENTO", tipo="Evento", ano=2026),
Participacao(codigo="EVENTO", tipo="Evento", ano=2027),
Participacao(codigo="EVENTO", tipo="Evento", ano=2028),
Participacao(codigo="EVENTO", tipo="Evento", ano=2029),
Participacao(codigo="EVENTO", tipo="Evento", ano=2030),
]
resultado = CalculadorPontuacao.calcular_bloco_d([], eventos)
atuacao = next(a for a in resultado.atuacoes if a.codigo == "EVENTO")
assert atuacao.bonus == 10

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,208 @@
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from src.infrastructure.oracle.ranking_repository import RankingOracleRepository
class DummyCLOB:
def __init__(self, value: str) -> None:
self._value = value
def read(self) -> str:
return self._value
@pytest.fixture
def oracle_client():
client = MagicMock()
client.executar_query.return_value = []
return client
@pytest.fixture
def repo(oracle_client):
return RankingOracleRepository(oracle_client)
def test_buscar_paginado_monta_params_e_parseia_json(repo, oracle_client):
oracle_client.executar_query.return_value = [
{
"ID_PESSOA": 1,
"NOME": "Teste",
"POSICAO": 10,
"PONTUACAO_TOTAL": 200,
"COMPONENTE_A": 10,
"COMPONENTE_B": 20,
"COMPONENTE_C": 30,
"COMPONENTE_D": 40,
"COMPONENTE_E": 0,
"ATIVO": "S",
"ANOS_ATUACAO": 5,
"DT_CALCULO": datetime(2025, 1, 1),
"JSON_DETALHES": DummyCLOB("{\"k\": 1}"),
}
]
resultado = repo.buscar_paginado(page=2, size=20, filtro_ativo=True, filtro_selos=["CA", "EVENTO"])
assert len(resultado) == 1
consultor = resultado[0]
assert consultor.id_pessoa == 1
assert consultor.ativo is True
assert consultor.json_detalhes == "{\"k\": 1}"
call_args = oracle_client.executar_query.call_args
params = call_args[0][1]
assert params["offset"] == 20
assert params["limit_end"] == 40
assert params["ativo"] == "S"
assert params["selo_0"] == "CA"
assert params["selo_1"] == "EVENTO"
def test_contar_total_com_filtros(repo, oracle_client):
oracle_client.executar_query.return_value = [{"TOTAL": 123}]
total = repo.contar_total(filtro_ativo=False, filtro_selos=["CONS_ATIVO"])
assert total == 123
call_args = oracle_client.executar_query.call_args
params = call_args[0][1]
assert params["ativo"] == "N"
assert params["selo_0"] == "CONS_ATIVO"
def test_buscar_por_nome_monta_query(repo, oracle_client):
oracle_client.executar_query.return_value = [{"ID_PESSOA": 1, "NOME": "JOAO", "POSICAO": 1, "PONTUACAO_TOTAL": 10}]
resultado = repo.buscar_por_nome("Joao Silva", limit=3)
assert resultado[0]["ID_PESSOA"] == 1
call_args = oracle_client.executar_query.call_args
params = call_args[0][1]
assert params["limit"] == 3
assert params["p0"] == "%JOAO%"
assert params["p1"] == "%SILVA%"
def test_buscar_por_id_parseia_json(repo, oracle_client):
oracle_client.executar_query.return_value = [
{
"ID_PESSOA": 5,
"NOME": "Teste",
"POSICAO": 20,
"PONTUACAO_TOTAL": 300,
"COMPONENTE_A": 10,
"COMPONENTE_B": 20,
"COMPONENTE_C": 30,
"COMPONENTE_D": 40,
"COMPONENTE_E": 0,
"ATIVO": "N",
"ANOS_ATUACAO": 2,
"DT_CALCULO": datetime(2024, 12, 31),
"JSON_DETALHES": DummyCLOB("{\"x\": true}"),
}
]
consultor = repo.buscar_por_id(5)
assert consultor is not None
assert consultor.id_pessoa == 5
assert consultor.ativo is False
assert consultor.json_detalhes == "{\"x\": true}"
def test_atualizar_posicoes_chama_procedure(repo, oracle_client):
cursor = MagicMock()
conn = MagicMock()
conn.cursor.return_value = cursor
oracle_client.get_connection.return_value.__enter__.return_value = conn
repo.atualizar_posicoes()
cursor.callproc.assert_called_once_with("SP_ATUALIZAR_POSICOES")
conn.commit.assert_called_once()
def test_obter_estatisticas(repo, oracle_client):
oracle_client.executar_query.return_value = [
{
"TOTAL_CONSULTORES": 10,
"TOTAL_ATIVOS": 7,
"TOTAL_INATIVOS": 3,
"ULTIMA_ATUALIZACAO": datetime(2025, 1, 2, 10, 0, 0),
"PONTUACAO_MEDIA": 100,
"PONTUACAO_MAXIMA": 200,
"PONTUACAO_MINIMA": 10,
"MEDIA_COMP_A": 10,
"MEDIA_COMP_B": 20,
"MEDIA_COMP_C": 30,
"MEDIA_COMP_D": 40,
"MEDIA_COMP_E": 0,
}
]
stats = repo.obter_estatisticas()
assert stats["total_consultores"] == 10
assert stats["total_ativos"] == 7
assert stats["ultima_atualizacao"].startswith("2025-01-02")
assert stats["media_componentes"]["a"] == 10
def test_obter_distribuicao(repo, oracle_client):
oracle_client.executar_query.return_value = [
{"FAIXA": "0-199", "QUANTIDADE": 5, "PERCENTUAL": 50},
{"FAIXA": "200-399", "QUANTIDADE": 5, "PERCENTUAL": 50},
]
dist = repo.obter_distribuicao()
assert dist[0]["faixa"] == "0-199"
assert dist[1]["percentual"] == 50.0
def test_buscar_para_exportacao_usa_cursor(repo, oracle_client):
cursor = MagicMock()
cursor.description = [("ID_PESSOA",), ("NOME",), ("JSON_DETALHES",)]
cursor.fetchmany.side_effect = [
[(1, "Teste", DummyCLOB("{\"k\":1}"))],
[],
]
conn = MagicMock()
conn.cursor.return_value = cursor
oracle_client.get_connection.return_value.__enter__.return_value = conn
registros = repo.buscar_para_exportacao(filtro_ativo=True, filtro_selos=["CA"], batch_size=1)
assert registros[0]["ID_PESSOA"] == 1
assert registros[0]["JSON_DETALHES"] == "{\"k\":1}"
def test_contar_para_exportacao_com_filtros(repo, oracle_client):
oracle_client.executar_query.return_value = [{"TOTAL": 42}]
total = repo.contar_para_exportacao(filtro_ativo=True, filtro_selos=["EVENTO", "PROJ"])
assert total == 42
call_args = oracle_client.executar_query.call_args
params = call_args[0][1]
assert params["ativo"] == "S"
assert params["selo_0"] == "EVENTO"
assert params["selo_1"] == "PROJ"
def test_filtro_ativo_filtra_apenas_consultores(repo, oracle_client):
oracle_client.executar_query.return_value = [{"TOTAL": 100}]
repo.contar_total(filtro_ativo=True)
call_args = oracle_client.executar_query.call_args
query = call_args[0][0]
assert "E_CONSULTOR = 'S'" in query
assert "ATIVO = :ativo" in query
def test_filtro_ativo_false_filtra_apenas_consultores(repo, oracle_client):
oracle_client.executar_query.return_value = [{"TOTAL": 100}]
repo.contar_total(filtro_ativo=False)
call_args = oracle_client.executar_query.call_args
query = call_args[0][0]
assert "E_CONSULTOR = 'S'" in query
assert "ATIVO = :ativo" in query
params = call_args[0][1]
assert params["ativo"] == "N"

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,53 @@
from src.infrastructure.ranking_store import SELOS_DISPONIVEIS
PDF_SELOS = {
"CA",
"CAJ",
"CAJ_MP",
"CAM",
"PRESID_CAMARA",
"CONS_ATIVO",
"AVAL_COMIS",
"COORD_COMIS",
"AUTOR_GP",
"AUTOR_PREMIO",
"AUTOR_MENCAO",
"ORIENT_GP",
"ORIENT_PREMIO",
"ORIENT_MENCAO",
"COORIENT_GP",
"COORIENT_PREMIO",
"COORIENT_MENCAO",
"ORIENT_POS_DOC",
"ORIENT_POS_DOC_PREM",
"ORIENT_TESE",
"ORIENT_TESE_PREM",
"ORIENT_DISS",
"ORIENT_DISS_PREM",
"CO_ORIENT_POS_DOC",
"CO_ORIENT_POS_DOC_PREM",
"CO_ORIENT_TESE",
"CO_ORIENT_TESE_PREM",
"CO_ORIENT_DISS",
"CO_ORIENT_DISS_PREM",
"MB_BANCA_POS_DOC",
"MB_BANCA_POS_DOC_PREM",
"MB_BANCA_TESE",
"MB_BANCA_TESE_PREM",
"MB_BANCA_DISS",
"MB_BANCA_DISS_PREM",
"IDIOMA_BILINGUE",
"IDIOMA_MULTILINGUE",
"TITULACAO_MESTRE",
"TITULACAO_DOUTOR",
"TITULACAO_POS_DOUTOR",
"BOL_BPQ_NIVEL",
"PPG_COORD",
"EVENTO",
"PROJ",
}
def test_pdf_selos_disponiveis():
assert set(SELOS_DISPONIVEIS) == PDF_SELOS

View File

@@ -0,0 +1,120 @@
import pytest
from src.infrastructure.ranking_store import RankingStore, RankingEntry
@pytest.fixture
def store():
return RankingStore()
@pytest.fixture
def entries_com_consultores_e_nao_consultores():
return [
RankingEntry(
id_pessoa=1,
nome="Consultor Ativo",
posicao=1,
pontuacao_total=500,
bloco_a=200,
bloco_b=0,
bloco_c=150,
bloco_d=100,
bloco_e=50,
ativo=True,
e_consultor=True,
anos_atuacao=5.0,
detalhes={},
),
RankingEntry(
id_pessoa=2,
nome="Consultor Inativo",
posicao=2,
pontuacao_total=400,
bloco_a=200,
bloco_b=0,
bloco_c=100,
bloco_d=50,
bloco_e=50,
ativo=False,
e_consultor=True,
anos_atuacao=3.0,
detalhes={},
),
RankingEntry(
id_pessoa=3,
nome="Nao Consultor (Coordenador)",
posicao=3,
pontuacao_total=300,
bloco_a=250,
bloco_b=0,
bloco_c=0,
bloco_d=50,
bloco_e=0,
ativo=False,
e_consultor=False,
anos_atuacao=2.0,
detalhes={},
),
]
@pytest.mark.asyncio
async def test_total_sem_filtro_traz_todos(store, entries_com_consultores_e_nao_consultores):
await store.set_entries(entries_com_consultores_e_nao_consultores)
assert store.total() == 3
@pytest.mark.asyncio
async def test_total_filtro_ativo_somente_consultores_ativos(store, entries_com_consultores_e_nao_consultores):
await store.set_entries(entries_com_consultores_e_nao_consultores)
assert store.total(filtro_ativo=True) == 1
@pytest.mark.asyncio
async def test_total_filtro_inativo_somente_consultores_inativos(store, entries_com_consultores_e_nao_consultores):
await store.set_entries(entries_com_consultores_e_nao_consultores)
assert store.total(filtro_ativo=False) == 1
@pytest.mark.asyncio
async def test_get_page_filtro_ativo_somente_consultores_ativos(store, entries_com_consultores_e_nao_consultores):
await store.set_entries(entries_com_consultores_e_nao_consultores)
total, entries = store.get_page(page=1, size=10, filtro_ativo=True)
assert total == 1
ids = [e.id_pessoa for e in entries]
assert 1 in ids
assert 2 not in ids
assert 3 not in ids
@pytest.mark.asyncio
async def test_get_page_filtro_inativo_somente_consultores_inativos(store, entries_com_consultores_e_nao_consultores):
await store.set_entries(entries_com_consultores_e_nao_consultores)
total, entries = store.get_page(page=1, size=10, filtro_ativo=False)
assert total == 1
ids = [e.id_pessoa for e in entries]
assert 2 in ids
assert 1 not in ids
assert 3 not in ids
@pytest.mark.asyncio
async def test_get_slice_filtro_ativo_somente_consultores_ativos(store, entries_com_consultores_e_nao_consultores):
await store.set_entries(entries_com_consultores_e_nao_consultores)
total, entries = store.get_slice(offset=0, limit=10, filtro_ativo=True)
assert total == 1
ids = [e.id_pessoa for e in entries]
assert 1 in ids
assert 2 not in ids
assert 3 not in ids
@pytest.mark.asyncio
async def test_get_page_sem_filtro_traz_todos(store, entries_com_consultores_e_nao_consultores):
await store.set_entries(entries_com_consultores_e_nao_consultores)
total, entries = store.get_page(page=1, size=10)
assert total == 3
ids = [e.id_pessoa for e in entries]
assert 1 in ids
assert 2 in ids
assert 3 in ids

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

View File

@@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"react": "^19.2.0",
@@ -17,6 +18,10 @@
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0"
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
"jsdom": "^24.0.0",
"vite": "^5.0.0",
"vitest": "^1.6.0"
}
}

View File

@@ -145,10 +145,39 @@
transform: translateY(-1px);
}
.btn-exportar {
padding: 0.5rem 1rem;
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.15));
border: 1px solid rgba(34, 197, 94, 0.4);
color: #86efac;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 150ms ease;
white-space: nowrap;
display: flex;
align-items: center;
gap: 0.4rem;
}
.btn-exportar:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.3), rgba(16, 185, 129, 0.2));
border-color: rgba(34, 197, 94, 0.6);
transform: translateY(-1px);
}
.btn-exportar:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.pagination {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
}
.pagination button {

View File

@@ -3,7 +3,9 @@ import Header from './components/Header';
import ConsultorCard from './components/ConsultorCard';
import CompararModal from './components/CompararModal';
import FiltroSelos from './components/FiltroSelos';
import FiltroAtivo from './components/FiltroAtivo';
import SugerirConsultores from './components/SugerirConsultores';
import ExportProgress from './components/ExportProgress';
import { rankingService } from './services/api';
import './App.css';
@@ -24,7 +26,13 @@ function App() {
const [selecionados, setSelecionados] = useState([]);
const [modalAberto, setModalAberto] = useState(false);
const [filtroSelos, setFiltroSelos] = useState([]);
const [filtroAtivo, setFiltroAtivo] = useState(null);
const [sugerirAberto, setSugerirAberto] = useState(false);
const [exportando, setExportando] = useState(false);
const [exportProgress, setExportProgress] = useState({ loaded: 0, total: 0, percent: 0 });
const [exportStatus, setExportStatus] = useState('preparing');
const abortControllerRef = useRef(null);
const loadingRef = useRef(false);
const toggleSelecionado = (consultor) => {
setSelecionados((prev) => {
@@ -60,9 +68,54 @@ function App() {
}
};
const handleExportarExcel = async () => {
if (exportando) return;
try {
setExportando(true);
setExportStatus('preparing');
setExportProgress({ loaded: 0, total: 0, percent: 0 });
abortControllerRef.current = new AbortController();
await rankingService.downloadRankingExcel(
filtroSelos,
filtroAtivo,
(progress) => {
setExportStatus('downloading');
setExportProgress(progress);
},
abortControllerRef.current
);
setExportStatus('complete');
setTimeout(() => setExportando(false), 1000);
} catch (err) {
if (err.name === 'CanceledError' || err.code === 'ERR_CANCELED') {
console.log('Exportação cancelada pelo usuário');
} else {
console.error('Erro ao exportar Excel:', err);
setExportStatus('error');
setTimeout(() => setExportando(false), 2000);
}
}
};
const handleCancelExport = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
setExportando(false);
};
useEffect(() => {
const requestKey = `${page}-${pageSize}-${filtroSelos.join(',')}-${filtroAtivo}`;
if (loadingRef.current === requestKey) {
return;
}
loadingRef.current = requestKey;
loadRanking();
}, [page, pageSize, filtroSelos]);
}, [page, pageSize, filtroSelos, filtroAtivo]);
const loadRanking = async (retryCount = 0) => {
const MAX_RETRIES = 10;
@@ -72,7 +125,7 @@ function App() {
setLoading(true);
setError(null);
setProcessMessage('');
const response = await rankingService.getRanking(page, pageSize, filtroSelos);
const response = await rankingService.getRanking(page, pageSize, filtroSelos, filtroAtivo);
setConsultores(response.consultores);
setTotal(response.total);
setTotalPages(response.total_pages || 0);
@@ -125,7 +178,7 @@ function App() {
throw new Error('Timeout: processamento demorou mais que 45 minutos');
}
const response = await rankingService.getRanking(page, pageSize, filtroSelos);
const response = await rankingService.getRanking(page, pageSize, filtroSelos, filtroAtivo);
setConsultores(response.consultores);
setTotal(response.total);
setTotalPages(response.total_pages || 0);
@@ -216,10 +269,32 @@ function App() {
onChange={(selos) => { setFiltroSelos(selos); setPage(1); }}
/>
<FiltroAtivo
valor={filtroAtivo}
onChange={(valor) => { setFiltroAtivo(valor); setPage(1); }}
/>
<button className="btn-sugerir" onClick={() => setSugerirAberto(true)}>
Sugerir por Tema
</button>
<button
className="btn-exportar"
onClick={handleExportarExcel}
disabled={exportando || loading}
title="Exportar ranking para Excel (apenas consultores com pontuação)"
>
{exportando ? 'Exportando...' : '📊 Exportar Excel'}
</button>
<div className="pagination">
<button onClick={() => setPage(1)} disabled={page <= 1}>«</button>
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}></button>
<span className="page-info">{page} / {totalPages || '?'}</span>
<button onClick={() => setPage((p) => (totalPages ? Math.min(totalPages, p + 1) : p + 1))} disabled={totalPages && page >= totalPages}></button>
<button onClick={() => totalPages && setPage(totalPages)} disabled={totalPages && page >= totalPages}>»</button>
</div>
<form className="search-box" onSubmit={handleSubmitBuscar}>
<input
type="text"
@@ -231,14 +306,6 @@ function App() {
{buscando ? '...' : 'Buscar'}
</button>
</form>
<div className="pagination">
<button onClick={() => setPage(1)} disabled={page <= 1}>«</button>
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}></button>
<span className="page-info">{page} / {totalPages || '?'}</span>
<button onClick={() => setPage((p) => (totalPages ? Math.min(totalPages, p + 1) : p + 1))} disabled={totalPages && page >= totalPages}></button>
<button onClick={() => totalPages && setPage(totalPages)} disabled={totalPages && page >= totalPages}>»</button>
</div>
</div>
<div className="ranking-list">
@@ -290,6 +357,14 @@ function App() {
/>
)}
{exportando && (
<ExportProgress
progress={exportProgress}
status={exportStatus}
onCancel={handleCancelExport}
/>
)}
<footer>
<p>Dados: ATUACAPES (Elasticsearch) + Oracle</p>
<p>Clique em qualquer consultor para ver detalhes</p>

View File

@@ -186,7 +186,7 @@ const SelosBadges = ({ selos, compacto = false, onSeloClick }) => {
<span
key={idx}
className={`selo ${selo.cor} ${onSeloClick && temDados ? 'selo-clicavel' : ''}`}
title={temDados ? `Clique para ver detalhes` : (selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`)}
title={selo.hint || `${selo.label}${selo.qtd > 1 ? ` (${selo.qtd}x)` : ''}`}
onMouseDown={(e) => onSeloClick && temDados && e.stopPropagation()}
onClick={(e) => handleClick(e, selo)}
>

View File

@@ -0,0 +1,196 @@
.export-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1100;
animation: fadeIn 200ms ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.export-modal {
background: linear-gradient(155deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.95));
border: 1px solid var(--stroke);
border-radius: 20px;
padding: 2rem 2.5rem;
min-width: 320px;
max-width: 400px;
text-align: center;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6), 0 0 40px rgba(34, 197, 94, 0.1);
animation: slideUp 300ms ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.export-icon {
width: 60px;
height: 60px;
margin: 0 auto 1rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.15));
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 16px;
}
.export-icon.generating {
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.export-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(120deg, #86efac, #22c55e);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.export-status {
font-size: 0.9rem;
color: var(--muted);
margin-bottom: 1.5rem;
}
.progress-container {
margin-bottom: 1.5rem;
}
.progress-bar {
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.75rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #22c55e, #16a34a);
border-radius: 4px;
transition: width 150ms ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.progress-fill.complete {
background: linear-gradient(90deg, #22c55e, #16a34a);
}
.progress-fill.complete::after {
animation: none;
}
.progress-fill.error {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.progress-fill.indeterminate {
width: 30%;
animation: indeterminate 1.5s ease-in-out infinite;
}
@keyframes indeterminate {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(230%);
}
100% {
transform: translateX(-100%);
}
}
.progress-generating {
font-size: 0.85rem;
color: var(--muted);
animation: blink 1.5s ease-in-out infinite;
}
@keyframes blink {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-percent {
font-size: 1.1rem;
font-weight: 700;
color: #86efac;
}
.progress-bytes {
font-size: 0.8rem;
color: var(--muted);
}
.export-cancel {
padding: 0.6rem 1.5rem;
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--stroke);
color: var(--text);
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 0.9rem;
transition: all 150ms ease;
}
.export-cancel:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #fca5a5;
}

View File

@@ -0,0 +1,82 @@
import './ExportProgress.css';
function ExportProgress({ progress, status, onCancel }) {
const formatBytes = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};
const isGenerating = status === 'preparing' || (status === 'downloading' && progress.total === 0);
const isDownloading = status === 'downloading' && progress.total > 0;
const getStatusText = () => {
if (isGenerating) {
return 'Gerando arquivo no servidor...';
}
switch (status) {
case 'downloading':
return 'Baixando arquivo...';
case 'complete':
return 'Concluído!';
case 'error':
return 'Erro na exportação';
default:
return 'Exportando...';
}
};
const percent = Math.min(Math.round(progress.percent || 0), 100);
const downloaded = progress.loaded || 0;
const total = progress.total || 0;
return (
<div className="export-overlay">
<div className="export-modal">
<div className={`export-icon ${isGenerating ? 'generating' : ''}`}>
{status === 'complete' ? '✓' : status === 'error' ? '✕' : '📊'}
</div>
<h3 className="export-title">Exportação Excel</h3>
<p className="export-status">{getStatusText()}</p>
<div className="progress-container">
<div className="progress-bar">
{isGenerating ? (
<div className="progress-fill indeterminate" />
) : (
<div
className={`progress-fill ${status === 'complete' ? 'complete' : ''} ${status === 'error' ? 'error' : ''}`}
style={{ width: `${percent}%` }}
/>
)}
</div>
<div className="progress-info">
{isGenerating ? (
<span className="progress-generating">Processando ~300k registros...</span>
) : (
<>
<span className="progress-percent">{percent}%</span>
{total > 0 && (
<span className="progress-bytes">
{formatBytes(downloaded)} / {formatBytes(total)}
</span>
)}
</>
)}
</div>
</div>
{status !== 'complete' && status !== 'error' && (
<button className="export-cancel" onClick={onCancel}>
Cancelar
</button>
)}
</div>
</div>
);
}
export default ExportProgress;

View File

@@ -0,0 +1,153 @@
.filtro-ativo {
position: relative;
}
.filtro-ativo-trigger {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.9rem;
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--stroke);
border-radius: 8px;
color: var(--text);
font-size: 0.9rem;
cursor: pointer;
transition: all 200ms ease;
white-space: nowrap;
}
.filtro-ativo-trigger:hover {
border-color: var(--accent-2);
background: rgba(255, 255, 255, 0.08);
}
.filtro-ativo-trigger.ativo {
border-color: var(--accent);
background: rgba(79, 70, 229, 0.15);
}
.filtro-ativo .filtro-icone {
font-size: 1rem;
}
.filtro-ativo .filtro-label {
font-weight: 500;
}
.filtro-ativo .filtro-seta {
font-size: 0.7rem;
color: var(--muted);
transition: transform 200ms ease;
}
.filtro-ativo .filtro-seta.aberto {
transform: rotate(180deg);
}
.filtro-ativo .filtro-limpar {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
font-size: 0.7rem;
background: rgba(255, 59, 48, 0.2);
color: #ff6b6b;
border-radius: 50%;
margin-left: 0.25rem;
transition: all 150ms ease;
}
.filtro-ativo .filtro-limpar:hover {
background: rgba(255, 59, 48, 0.4);
}
.filtro-ativo-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
min-width: 180px;
background: linear-gradient(165deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.98));
border: 1px solid var(--stroke);
border-radius: 12px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4), 0 0 20px rgba(79, 70, 229, 0.1);
z-index: 100;
backdrop-filter: blur(12px);
animation: dropdownSlide 200ms ease;
overflow: hidden;
}
@keyframes dropdownSlide {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.filtro-ativo-header {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--stroke);
font-size: 0.85rem;
color: var(--muted);
}
.filtro-ativo-opcoes {
padding: 0.5rem;
}
.filtro-opcao-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
font-size: 0.9rem;
width: 100%;
}
.filtro-opcao-item:hover {
background: rgba(255, 255, 255, 0.08);
border-color: var(--stroke);
}
.filtro-opcao-item.selecionado {
background: rgba(79, 70, 229, 0.2);
border-color: var(--accent);
}
.filtro-opcao-item .opcao-icone {
font-size: 1rem;
}
.filtro-opcao-item .opcao-label {
color: var(--text);
flex: 1;
}
.filtro-opcao-item .opcao-check {
color: var(--accent);
font-weight: bold;
}
@media (max-width: 480px) {
.filtro-ativo-dropdown {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
min-width: 100%;
border-radius: 16px 16px 0 0;
}
}

View File

@@ -0,0 +1,78 @@
import { useState, useRef, useEffect } from 'react';
import './FiltroAtivo.css';
const OPCOES = [
{ value: null, label: 'Todos', icone: '👥' },
{ value: true, label: 'Ativos', icone: '✅' },
{ value: false, label: 'Inativos', icone: '📋' },
];
function FiltroAtivo({ valor, onChange }) {
const [aberto, setAberto] = useState(false);
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
setAberto(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const selecionarOpcao = (novoValor) => {
onChange(novoValor);
setAberto(false);
};
const limparFiltro = (e) => {
e.stopPropagation();
onChange(null);
};
const opcaoAtual = OPCOES.find((o) => o.value === valor) || OPCOES[0];
const filtroAtivo = valor !== null;
return (
<div className="filtro-ativo" ref={ref}>
<button
className={`filtro-ativo-trigger ${filtroAtivo ? 'ativo' : ''}`}
onClick={() => setAberto(!aberto)}
>
<span className="filtro-icone">{opcaoAtual.icone}</span>
<span className="filtro-label">{opcaoAtual.label}</span>
<span className={`filtro-seta ${aberto ? 'aberto' : ''}`}></span>
{filtroAtivo && (
<span className="filtro-limpar" onClick={limparFiltro} title="Mostrar todos">
</span>
)}
</button>
{aberto && (
<div className="filtro-ativo-dropdown">
<div className="filtro-ativo-header">
<span>Filtrar por status</span>
</div>
<div className="filtro-ativo-opcoes">
{OPCOES.map((opcao) => (
<label
key={String(opcao.value)}
className={`filtro-opcao-item ${valor === opcao.value ? 'selecionado' : ''}`}
onClick={() => selecionarOpcao(opcao.value)}
>
<span className="opcao-icone">{opcao.icone}</span>
<span className="opcao-label">{opcao.label}</span>
{valor === opcao.value && <span className="opcao-check"></span>}
</label>
))}
</div>
</div>
)}
</div>
);
}
export default FiltroAtivo;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import { rankingService } from '../services/api';
import './RawDataModal.css';
@@ -473,8 +473,13 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => {
const [filterType, setFilterType] = useState('all');
const [copyFeedback, setCopyFeedback] = useState(false);
const [downloadingPDF, setDownloadingPDF] = useState(false);
const fetchedRef = useRef(null);
useEffect(() => {
if (fetchedRef.current === idPessoa) {
return;
}
fetchedRef.current = idPessoa;
const fetchData = async () => {
try {
setLoading(true);

View File

@@ -0,0 +1,27 @@
import { render, screen, fireEvent } from '@testing-library/react';
import FiltroSelos from '../FiltroSelos';
describe('FiltroSelos', () => {
it('applies selected seals', () => {
const handleChange = vi.fn();
render(<FiltroSelos selecionados={[]} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /Filtrar por selos/i }));
const checkbox = screen.getByLabelText(/Coord\./i);
fireEvent.click(checkbox);
fireEvent.click(screen.getByRole('button', { name: /Aplicar/i }));
expect(handleChange).toHaveBeenCalledWith(['CA']);
});
it('clears filters from the trigger when active', () => {
const handleChange = vi.fn();
render(<FiltroSelos selecionados={['CA']} onChange={handleChange} />);
fireEvent.click(screen.getByTitle('Limpar filtros'));
expect(handleChange).toHaveBeenCalledWith([]);
});
});

View File

@@ -0,0 +1,19 @@
import { render, screen, fireEvent } from '@testing-library/react';
import Header from '../Header';
describe('Header', () => {
it('renders the title and total count', () => {
render(<Header total={1000} />);
expect(screen.getByText('Ranking de Consultores CAPES')).toBeInTheDocument();
expect(screen.getByText(/Total:\s+1\.000 consultores/)).toBeInTheDocument();
});
it('opens the criteria modal when a block is clicked', () => {
render(<Header total={0} />);
fireEvent.click(screen.getByText('A - Coordenacao CAPES'));
expect(screen.getByText(/Coordena/)).toBeInTheDocument();
});
});

View File

@@ -9,10 +9,18 @@ const api = axios.create({
});
export const rankingService = {
async getRanking(page = 1, size = 100, selos = []) {
async getRanking(page = 1, size = 100, selos = [], ativo = null) {
const params = { page, size };
if (selos && selos.length > 0) {
params.selos = selos.join(',');
const normalizados = selos
.map((s) => String(s || '').trim().toUpperCase())
.filter(Boolean);
if (normalizados.length > 0) {
params.selos = normalizados.join(',');
}
}
if (ativo !== null) {
params.ativo = ativo;
}
const response = await api.get('/ranking/paginado', { params });
const data = response.data;
@@ -181,6 +189,79 @@ export const rankingService = {
const response = await api.get(`/consultor/${idPessoa}/lattes`);
return response.data;
},
async getExportInfo(selos = [], ativo = null) {
const params = {};
if (selos && selos.length > 0) {
const normalizados = selos
.map((s) => String(s || '').trim().toUpperCase())
.filter(Boolean);
if (normalizados.length > 0) {
params.selos = normalizados.join(',');
}
}
if (ativo !== null) {
params.ativo = ativo;
}
const response = await api.get('/ranking/exportar/info', { params });
return response.data;
},
async downloadRankingExcel(selos = [], ativo = null, onProgress = null, abortController = null) {
const params = {};
if (selos && selos.length > 0) {
const normalizados = selos
.map((s) => String(s || '').trim().toUpperCase())
.filter(Boolean);
if (normalizados.length > 0) {
params.selos = normalizados.join(',');
}
}
if (ativo !== null) {
params.ativo = ativo;
}
const config = {
params,
responseType: 'blob',
timeout: 300000,
};
if (onProgress) {
config.onDownloadProgress = (progressEvent) => {
const { loaded, total } = progressEvent;
const percent = total ? Math.round((loaded * 100) / total) : 0;
onProgress({ loaded, total, percent });
};
}
if (abortController) {
config.signal = abortController.signal;
}
const response = await api.get('/ranking/exportar/excel', config);
const contentDisposition = response.headers['content-disposition'];
let nomeArquivo = 'ranking_consultores_capes.xlsx';
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+)"/);
if (match) {
nomeArquivo = match[1];
}
}
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = nomeArquivo;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
},
};
export default api;

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@@ -3,6 +3,11 @@ import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/setupTests.js',
globals: true,
},
server: {
host: '0.0.0.0',
port: 5173,