Adiciona testes do conector com fixtures offline
Cobre o caminho completo sem depender da VPS estar no ar: cifra com uma chave de teste descartavel, troca a chave do app por monkeypatch e exercita a rota. 8 casos: recusa sem token e com token errado; caminho feliz; envelope adulterado (o GCM tem que estourar, nao passar); cifrado com outra chave; payload fora do contrato (422, nao 200 nem 500); e dois travando o contrato com a API da VPS — se o PayloadCompras dos dois lados divergir, quebra aqui em vez de em producao. Fixtures em tests/fixtures/: par de chaves so pra teste, o payload real do Holmes e o payload ja tratado. O .gitignore abre excecao estreita pra tests/fixtures/*.pem — chave real continua barrada. Tambem: - 422 passa a usar HTTP_422_UNPROCESSABLE_CONTENT (o _ENTITY deprecou) - app/services/holmes.py removido: o conector nao fala com o Holmes - app/utils/cesar.py entra como veio Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f902ccbecb
commit
7a9b0f8aa5
9 changed files with 283 additions and 705 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -12,3 +12,7 @@ configs.json.*
|
||||||
*.bak-*
|
*.bak-*
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
|
|
||||||
|
# Excecao: chaves descartaveis de teste. Geradas so pro pytest, nunca
|
||||||
|
# usadas em runtime. As reais continuam barradas pela regra acima.
|
||||||
|
!tests/fixtures/*.pem
|
||||||
|
|
|
||||||
|
|
@ -1,704 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import tempfile
|
|
||||||
import time
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from app.config import settings
|
|
||||||
from app.services.controle_api import registrar_contador
|
|
||||||
from oracledb import Connection
|
|
||||||
|
|
||||||
_TOKEN_FILE = os.path.join(tempfile.gettempdir(), "holmes_token_cache.json")
|
|
||||||
|
|
||||||
|
|
||||||
def _ler_token_arquivo() -> dict | None:
|
|
||||||
try:
|
|
||||||
with open(_TOKEN_FILE) as f:
|
|
||||||
return json.load(f)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _salvar_token_arquivo(token: dict):
|
|
||||||
try:
|
|
||||||
with open(_TOKEN_FILE, "w") as f:
|
|
||||||
json.dump(token, f)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _invalidar_token_usuario():
|
|
||||||
try:
|
|
||||||
os.remove(_TOKEN_FILE)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def token_usuario():
|
|
||||||
cached = _ler_token_arquivo()
|
|
||||||
if cached:
|
|
||||||
return cached
|
|
||||||
await asyncio.sleep(random.uniform(0, 0.5))
|
|
||||||
cached = _ler_token_arquivo()
|
|
||||||
if cached:
|
|
||||||
return cached
|
|
||||||
url = "https://app-api.holmesdoc.io/v1/session"
|
|
||||||
body = {"email": settings.holmes.usuario , "password": settings.holmes.senha}
|
|
||||||
headers = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"}
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(url, json=body, headers=headers)
|
|
||||||
response.raise_for_status()
|
|
||||||
token = {"authorization": response.json()["token"]}
|
|
||||||
_salvar_token_arquivo(token)
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
async def get_header() -> tuple[dict, str]:
|
|
||||||
if random.randint(1, 1) >= 2:
|
|
||||||
return {"api_token": settings.holmes.token_api}, "holmes"
|
|
||||||
header = await token_usuario()
|
|
||||||
return header, "holmes_user"
|
|
||||||
|
|
||||||
|
|
||||||
async def _holmes_request(
|
|
||||||
method: str, url: str, **kwargs
|
|
||||||
) -> tuple[httpx.Response, str]:
|
|
||||||
api_nome: str = "holmes"
|
|
||||||
for tentativa in range(2):
|
|
||||||
header, api_nome = await get_header()
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await getattr(client, method)(url, headers=header, **kwargs)
|
|
||||||
if (
|
|
||||||
response.status_code == 401
|
|
||||||
and api_nome == "holmes_user"
|
|
||||||
and tentativa == 0
|
|
||||||
):
|
|
||||||
_invalidar_token_usuario()
|
|
||||||
continue
|
|
||||||
return response, api_nome
|
|
||||||
raise RuntimeError("Holmes: falha de autenticação após retry")
|
|
||||||
|
|
||||||
|
|
||||||
def extrair_origem(origem: str):
|
|
||||||
"""Extrair origem da string do Holmes
|
|
||||||
|
|
||||||
Args:
|
|
||||||
origem (str): 1548 Entrada de Freio
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 1548
|
|
||||||
"""
|
|
||||||
return str(origem[:4])
|
|
||||||
|
|
||||||
|
|
||||||
def extrair_empresa(unidade: str):
|
|
||||||
"""Extrai a empresa da string inteira do holmes
|
|
||||||
|
|
||||||
Args:
|
|
||||||
unidade (str): Ex 10.1 Hyundai Teix. Freitas
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
empresa: 10 | None
|
|
||||||
revenda: 1 | None
|
|
||||||
"""
|
|
||||||
match = re.search(r"^(\d+)\.(\d+)", unidade)
|
|
||||||
if match:
|
|
||||||
return match.group(1), match.group(2)
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def extrair_transacao(transacao: str) -> str:
|
|
||||||
"""Extrai apenas a parte transação do texto do Holmes
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transacao (str): D15 Entrada de Nota
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: D15
|
|
||||||
"""
|
|
||||||
return transacao[:3]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_holmes_process(id_processo: str, conn: Connection):
|
|
||||||
"""
|
|
||||||
Busca um processo no Holmes. Centralizado para Peças e Despesas.
|
|
||||||
"""
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/processes/{id_processo}"
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("get", url)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return response.json()
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes (ID {id_processo}): {e}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao consultar Holmes: {e}")
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}",
|
|
||||||
metodo="GET",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_holmes_process_details(id_processo: str, conn: Connection):
|
|
||||||
"""
|
|
||||||
Busca os detalhes (properties) de um processo no Holmes.
|
|
||||||
"""
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/processes/{id_processo}/details"
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("get", url)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return response.json()
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes details (ID {id_processo}): {e}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao consultar Holmes details: {e}")
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}/details",
|
|
||||||
metodo="GET",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_holmes_history(id_processo: str, conn: Connection):
|
|
||||||
"""
|
|
||||||
Busca o historico no holmes (importante para pegar o id da ultima task, para avançar futuramente)
|
|
||||||
Args:
|
|
||||||
id_processo (str): id do processo no holmes
|
|
||||||
"""
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/processes/{id_processo}/history"
|
|
||||||
payload = {
|
|
||||||
"filters": [],
|
|
||||||
"page": 1,
|
|
||||||
"per_page": 100,
|
|
||||||
"sortBy": ["created_at", "desc"],
|
|
||||||
}
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("post", url, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return response.json()
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes (ID {id_processo}): {e}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao consultar Holmes: {e}")
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}/history",
|
|
||||||
metodo="POST",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_holmes_rateio(id_processo: str, conn: Connection):
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/processes/{id_processo}/tables/e124b2d0-ee14-11ef-95b4-25dee32fe73f/table_items?page=1&per_page=800"
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("get", url)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return response.json()
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes (ID {id_processo}): {e}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao consultar Holmes: {e}")
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}/table/(rateio)",
|
|
||||||
metodo="GET",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def task_id_recente(id_processo: str, conn: Connection):
|
|
||||||
dados_tasks = await get_holmes_history(id_processo, conn)
|
|
||||||
|
|
||||||
if (
|
|
||||||
not dados_tasks
|
|
||||||
or "histories" not in dados_tasks
|
|
||||||
or not dados_tasks["histories"]
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
|
|
||||||
mais_recente = max(dados_tasks["histories"], key=lambda x: x["created_at"])
|
|
||||||
|
|
||||||
return mais_recente["properties"]["task_id"]
|
|
||||||
|
|
||||||
|
|
||||||
async def task_mais_recente(id_processo: str, conn: Connection):
|
|
||||||
dados_tasks = await get_holmes_history(id_processo, conn)
|
|
||||||
|
|
||||||
if not dados_tasks or "histories" not in dados_tasks:
|
|
||||||
return None # Tratamento se a API falhar
|
|
||||||
|
|
||||||
# print(dados_tasks)
|
|
||||||
|
|
||||||
mais_recente = max(dados_tasks["histories"], key=lambda x: x["created_at"])
|
|
||||||
|
|
||||||
return mais_recente
|
|
||||||
|
|
||||||
|
|
||||||
async def historicos_task(id_processo: str, conn: Connection) -> dict | None:
|
|
||||||
"""_summary_
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id_processo (str): Id do processo no holmes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict | None : dicionario do historico | None
|
|
||||||
"""
|
|
||||||
dados_tasks = await get_holmes_history(id_processo, conn)
|
|
||||||
if not dados_tasks or "histories" not in dados_tasks:
|
|
||||||
return None # Tratamento se a API falhar
|
|
||||||
|
|
||||||
return dados_tasks
|
|
||||||
|
|
||||||
|
|
||||||
async def buscar_processo(
|
|
||||||
conn: Connection,
|
|
||||||
chave: str | None = None,
|
|
||||||
fluxos: list[str] | bool = False,
|
|
||||||
ativos: bool = True,
|
|
||||||
payload: dict | bool = False,
|
|
||||||
) -> dict:
|
|
||||||
"""Obtem os processos que existem com a sua chave
|
|
||||||
|
|
||||||
Args:
|
|
||||||
chave (str): Chave principal a ser procurada, preferencialmente unica pfvr, ajuda ae po.
|
|
||||||
ativos (bool) Defaults to True
|
|
||||||
fluxos (list[str] | bool, optional): _description_. Defaults to False. se quer pegar de um fluxo específico ou geral. Padrão: Geral
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: _description_
|
|
||||||
"""
|
|
||||||
if not payload and chave is None:
|
|
||||||
raise ValueError(
|
|
||||||
"Chave é obrigatória caso o payload não seja enviado chefia, fica alerta ae rapa"
|
|
||||||
)
|
|
||||||
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
url = "https://app-api.holmesdoc.io/v2/search"
|
|
||||||
|
|
||||||
if not payload:
|
|
||||||
payload = {
|
|
||||||
"query": {
|
|
||||||
"from": 0,
|
|
||||||
"size": 200,
|
|
||||||
"context": "process",
|
|
||||||
"sort": "updated_at",
|
|
||||||
"order": "desc",
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"match_all": True,
|
|
||||||
"terms": [
|
|
||||||
{
|
|
||||||
"value": f"{chave}",
|
|
||||||
"type": "match_phrase",
|
|
||||||
"field": "_content",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"trash": False,
|
|
||||||
"deleted_by_me": False,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("post", url, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
dados = response.json()
|
|
||||||
docs = dados.get("docs", [])
|
|
||||||
if ativos:
|
|
||||||
docs = [d for d in docs if d.get("status") != "canceled"]
|
|
||||||
if fluxos:
|
|
||||||
docs = [d for d in docs if d.get("name") in fluxos]
|
|
||||||
return {"status": True, "dados": {**dados, "docs": docs, "total": len(docs)}}
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes (ID {chave}): {e}")
|
|
||||||
return {"status": False, "error": e}
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao consultar Holmes: {e}")
|
|
||||||
return {"status": False, "error": e}
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}/search/por-chave",
|
|
||||||
metodo="POST",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def buscar_processo_por_chaves(
|
|
||||||
conn: Connection,
|
|
||||||
combinacoes: list[list[str]],
|
|
||||||
fluxos: list[str] | bool = False,
|
|
||||||
ativos: bool = True,
|
|
||||||
) -> dict:
|
|
||||||
"""Busca processos no Holmes usando combinações de termos.
|
|
||||||
|
|
||||||
Cada item de combinacoes é uma lista de valores que juntos identificam
|
|
||||||
um processo único (ex: [cnpj, numero_nf]). Cada combinação vira um group
|
|
||||||
separado na query.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
combinacoes: Ex: [["03657256000164", "19"], ["698cd1c570fd0f8f5f8436a4"]]
|
|
||||||
ativos: Ignora processos cancelados. Padrão: True.
|
|
||||||
fluxos: Filtra por nome de fluxo. Padrão: False (todos).
|
|
||||||
"""
|
|
||||||
url = "https://app-api.holmesdoc.io/v2/search"
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"query": {
|
|
||||||
"from": 0,
|
|
||||||
"size": 200,
|
|
||||||
"context": "process",
|
|
||||||
"sort": "updated_at",
|
|
||||||
"order": "desc",
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"match_all": True,
|
|
||||||
"terms": [
|
|
||||||
{"value": termo, "type": "match_phrase", "field": "_content"}
|
|
||||||
for termo in combinacao
|
|
||||||
],
|
|
||||||
}
|
|
||||||
for combinacao in combinacoes
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"trash": False,
|
|
||||||
"deleted_by_me": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("post", url, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
dados = response.json()
|
|
||||||
docs = dados.get("docs", [])
|
|
||||||
if ativos:
|
|
||||||
docs = [d for d in docs if d.get("status") != "canceled"]
|
|
||||||
if fluxos:
|
|
||||||
docs = [d for d in docs if d.get("name") in fluxos]
|
|
||||||
return {"status": True, "dados": {**dados, "docs": docs, "total": len(docs)}}
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes (combinacoes {combinacoes}): {e}")
|
|
||||||
return {"status": False, "error": e}
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao consultar Holmes: {e}")
|
|
||||||
return {"status": False, "error": e}
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}/search/por-chaves",
|
|
||||||
metodo="POST",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def action(payload: dict, id_task: str, id_processo: str, conn: Connection):
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/tasks/{id_task}/action"
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(
|
|
||||||
url, headers={"api_token": settings.holmes.token_api}, json=payload
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return True, response.json()
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes (ID {id_processo}): {e}")
|
|
||||||
return False, e
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao consultar Holmes: {e}")
|
|
||||||
return False, e
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}/action",
|
|
||||||
metodo="POST",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def cria_processo(
|
|
||||||
id_start: str,
|
|
||||||
payload: dict,
|
|
||||||
conn: Connection
|
|
||||||
) -> tuple[bool, dict | str]:
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/workflows/{id_start}/start"
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("post", url, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return True, response.json()
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro na API Holmes - Criar Processo ({payload}): {e}")
|
|
||||||
return False, str(e)
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao criar processo no Holmes: {e}")
|
|
||||||
return False, str(e)
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/workflows/{id}/start",
|
|
||||||
metodo="POST",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def enviar_documento(
|
|
||||||
id_processo: str,
|
|
||||||
arquivo: bytes,
|
|
||||||
nome_arquivo: str,
|
|
||||||
id_documento: str,
|
|
||||||
conn: Connection,
|
|
||||||
) -> tuple[bool, str | dict]:
|
|
||||||
task_id = await task_id_recente(id_processo, conn)
|
|
||||||
|
|
||||||
if not task_id:
|
|
||||||
return False, "Não foi possível obter a task mais recente do Holmes"
|
|
||||||
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/tasks/{task_id}/documents/{id_documento}"
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
files = {"file": (nome_arquivo, arquivo, "application/pdf")}
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(
|
|
||||||
url, headers={"api_token": settings.holmes.token_api}, files=files
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return True, {"task_id": task_id}
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro ao enviar documento Holmes (ID {id_processo}): {e}")
|
|
||||||
return False, str(e)
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao enviar documento Holmes: {e}")
|
|
||||||
return False, str(e)
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/tasks/{id}/documents/{id_documento}",
|
|
||||||
metodo="POST",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Para testar as funcoes
|
|
||||||
|
|
||||||
# async def main():
|
|
||||||
# from app.infra.database import db_instance
|
|
||||||
# db_instance.create_pool()
|
|
||||||
# conn = db_instance.pool.acquire()
|
|
||||||
# try:
|
|
||||||
# print(await get_holmes_history('69e65ea83fad950fad5715ed', conn))
|
|
||||||
# finally:
|
|
||||||
# db_instance.pool.release(conn)
|
|
||||||
|
|
||||||
# if __name__ == '__main__':
|
|
||||||
# import asyncio
|
|
||||||
# asyncio.run(main())
|
|
||||||
|
|
||||||
|
|
||||||
async def cancela_processo(
|
|
||||||
id_processo: str, conn: Connection
|
|
||||||
) -> tuple[bool, str | dict]:
|
|
||||||
url = f"https://app-api.holmesdoc.io/v1/processes/{id_processo}/cancel"
|
|
||||||
payload = {"reason": "Erro na emissão, data de vencimento. Problema na Disal."}
|
|
||||||
inicio = time.perf_counter()
|
|
||||||
status = None
|
|
||||||
sucesso = False
|
|
||||||
erro = None
|
|
||||||
api_nome = "holmes"
|
|
||||||
|
|
||||||
try:
|
|
||||||
response, api_nome = await _holmes_request("put", url, json=payload)
|
|
||||||
response.raise_for_status()
|
|
||||||
status = response.status_code
|
|
||||||
sucesso = True
|
|
||||||
return True, response.json() if response.content else {
|
|
||||||
"mensagem": "processo cancelado"
|
|
||||||
}
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
status = e.response.status_code
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro ao cancelar processo Holmes (ID {id_processo}): {e}")
|
|
||||||
return False, str(e)
|
|
||||||
except Exception as e:
|
|
||||||
erro = str(e)
|
|
||||||
logging.error(f"Erro inesperado ao cancelar processo Holmes: {e}")
|
|
||||||
return False, str(e)
|
|
||||||
finally:
|
|
||||||
duracao = (time.perf_counter() - inicio) * 1000
|
|
||||||
registrar_contador(
|
|
||||||
conn=conn,
|
|
||||||
api=api_nome,
|
|
||||||
endpoint="/v1/processes/{id}/cancel",
|
|
||||||
metodo="PUT",
|
|
||||||
status_code=status,
|
|
||||||
sucesso=sucesso,
|
|
||||||
duracao_ms=duracao,
|
|
||||||
erro=erro,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
# print(aaaa())
|
|
||||||
print(await token_usuario())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
41
app/utils/cesar.py
Normal file
41
app/utils/cesar.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""Cifra de Cesar para criptografar/descriptografar senhas."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def caesar_encrypt(text: str, shift: int) -> str:
|
||||||
|
result = []
|
||||||
|
for char in text:
|
||||||
|
if char.isalpha():
|
||||||
|
base = ord("A") if char.isupper() else ord("a")
|
||||||
|
result.append(chr((ord(char) - base + shift) % 26 + base))
|
||||||
|
elif char.isdigit():
|
||||||
|
result.append(str((int(char) + shift) % 10))
|
||||||
|
else:
|
||||||
|
result.append(char)
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def caesar_decrypt(text: str, shift: int) -> str:
|
||||||
|
return caesar_encrypt(text, -shift)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 4:
|
||||||
|
print("Uso: python cezar.py <encript|decript> <deslocamento> <texto> [texto2 ...]")
|
||||||
|
print("Exemplo: python cezar.py encript 12 minha_senha outra_senha")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
modo = sys.argv[1].lower()
|
||||||
|
shift = int(sys.argv[2])
|
||||||
|
textos = sys.argv[3:]
|
||||||
|
|
||||||
|
if modo == "encript":
|
||||||
|
for i, texto in enumerate(textos, start=1):
|
||||||
|
print(f"texto_criptografado{i}: {caesar_encrypt(texto, shift)}")
|
||||||
|
elif modo == "decript":
|
||||||
|
for i, texto in enumerate(textos, start=1):
|
||||||
|
print(f"texto_descriptografado{i}: {caesar_decrypt(texto, shift)}")
|
||||||
|
else:
|
||||||
|
print("Modo inválido. Use 'encript' ou 'decript'.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
@ -37,7 +37,7 @@ async def receber_compras(envelope: Envelope) -> dict:
|
||||||
# Decifrou mas o formato mudou: os dois lados sairam de sincronia.
|
# Decifrou mas o formato mudou: os dois lados sairam de sincronia.
|
||||||
logging.error(f"Payload fora do contrato: {e}")
|
logging.error(f"Payload fora do contrato: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
detail="payload nao bate com o contrato esperado",
|
detail="payload nao bate com o contrato esperado",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
28
tests/fixtures/chave_teste_privada.pem
vendored
Normal file
28
tests/fixtures/chave_teste_privada.pem
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/B6IxhXHEgkmR
|
||||||
|
INcZoR2py+NlOU0AihMvoPGcZcgD5YsDOLI2WuNlPn4d4bPLN3hyfSsduROUEpwn
|
||||||
|
78q/ZuEpHEW4YBE8wWrhPOTcMSQ4gwnJ/vDlXhXu1I5iwauOGn4qd8tUtqPavqZ0
|
||||||
|
+H7UCwo/JXmiPSB4z95rY8ZU+AhRY+YAYa2KP/Omjypb2bKurlglY3NId0862lgK
|
||||||
|
gfAMe6fCu16Cbw74A4lU1xtv3v98zb8QtRR19qzz4UuSTGPGHWBsHbNnmBmq/sHv
|
||||||
|
IL0BDSFuWxUdtiT61f+mzmCI0bqdvi9EzgyMjcLChzKUJzQnYiJXUd9anS3poHX9
|
||||||
|
L0uVQxSRAgMBAAECgf84DKS6hzLyZvqd9jUTVNunsq1rJ6OvBz585ob40HoKx6EL
|
||||||
|
tlz0Vwiup0HvgpIq/5knRu5MVl0fx87UcDAp0gD2E85NyExoQ4Nqr+cuD+r/1UKR
|
||||||
|
RZzEoUWKjvncTVd8otCSNjz52BWXUv1AgENacXiLW93IW+NzonHopjccCUngQCdF
|
||||||
|
CSgxCNUZYJN8+DqcaPos4DyBhyQa7jIlGINaAQjvXfy0OzPeTRkEYKUd/UvjArpe
|
||||||
|
CYk0JEQSh5GIdFy5ljMqRmMzKGfbvlSXASfpH8Rv1UP/Z9meLd241rhvvSicOHKe
|
||||||
|
W7t6jgjberbgYon+VY6nioTWrFKP7htdvRNwQ/ECgYEA7lSEaeS+yx9RWwRLqHnX
|
||||||
|
z8JWe8RUD2k7fgtIr9dADqxtBjpSZD5Ito6yZkrENlKKdtmrChOkO+6lQdwMShcd
|
||||||
|
TuWg/83UxzAdSzmobPGw4nehZ3FvO0HhVV5GCXjFqLdlIFLC/q4CTA+Kr9Zq8eJw
|
||||||
|
jxVVjT8e+utGPNwSL3Hx0n0CgYEAzTFcW07PqG0wxP9VIQyn1fAO+NnjgP0rO61l
|
||||||
|
0pX8e7bbX/LezT1RaeZ6GXKNcpTthoZbM79QMq+yrwVU+IiG2gocGJ0ta5F1PLoN
|
||||||
|
Ndr2MpH4XIR/w3Hc5QJqqo7FXQpaVwBmtxkMcncJzXkayU6iuX5rTMMgNuK9i5aM
|
||||||
|
M7aSMqUCgYEAz6bp/A2mwvnVe5ThirgmdclgatPjXc4VXLveJ/9Gu8I198Am+1pd
|
||||||
|
JlpsS74G+UGvjOAYK15zsRg7+ocMWh2LlqtyPI8NPkPIsjtZaRqoQJl1Etj9zkaX
|
||||||
|
WzcZlLUamua3gJM7fIUGUVkVQCF06q2d3rz63mdJydvmRa6FVbtUtE0CgYEAloH5
|
||||||
|
U4Q4bztARY9gGvDUfukpokD4ThnlR03F3TRk0T3sJbHY2UR73ijPMLFErWIt47nT
|
||||||
|
Vd6jbbpQX26SyAYkm/REbY2Ezl0QWG3D2Nf2NFu7h+ksaeiv9U7TfK1ieP9qClzh
|
||||||
|
+rWl2qQUDaIiErzaQSNIgzKxA3FHRQc1aY0mKX0CgYEAxFOdYHbi08BdGvigs4jG
|
||||||
|
BaXDYhKo4+B+N8Rim0DcfEmMTl03mIhcHPiprOspKntcryUirkizXr/KYJsAoegS
|
||||||
|
e/BdvEAfl3Nv/gabCMLHch3As5plCa0MJ1lFaAJVzbo7h7YnpdCsrxBxJ4xUTZKG
|
||||||
|
JO8fje7X8wfQ4DUxXZHkbkY=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
9
tests/fixtures/chave_teste_publica.pem
vendored
Normal file
9
tests/fixtures/chave_teste_publica.pem
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvweiMYVxxIJJkSDXGaEd
|
||||||
|
qcvjZTlNAIoTL6DxnGXIA+WLAziyNlrjZT5+HeGzyzd4cn0rHbkTlBKcJ+/Kv2bh
|
||||||
|
KRxFuGARPMFq4Tzk3DEkOIMJyf7w5V4V7tSOYsGrjhp+KnfLVLaj2r6mdPh+1AsK
|
||||||
|
PyV5oj0geM/ea2PGVPgIUWPmAGGtij/zpo8qW9myrq5YJWNzSHdPOtpYCoHwDHun
|
||||||
|
wrtegm8O+AOJVNcbb97/fM2/ELUUdfas8+FLkkxjxh1gbB2zZ5gZqv7B7yC9AQ0h
|
||||||
|
blsVHbYk+tX/ps5giNG6nb4vRM4MjI3CwocylCc0J2IiV1HfWp0t6aB1/S9LlUMU
|
||||||
|
kQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
1
tests/fixtures/holmes_compras.json
vendored
Normal file
1
tests/fixtures/holmes_compras.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"6a6a59266f8555a6725125a7","author":{"id":"69b00f9897ee2e2f33fc7dc8","name":"Acrisio Dos Santos","email":"acrisio@comercialscardua.com.br"},"created_at":null,"properties":{"protocolo":"protocolo","Tipo de Compra":"COMPRA INTERNA - DESPESAS","Fornecedores":"ACIMAQ EQUIPAMENTOS INDUSTRIAIS E COMERCIAIS LTDA","Prioridade Alta - Motivo":"","Observações":"","Possui Adiantamento do Cliente?":"Não","Fornecedor - Exclusividade?":false,"Cond. Pagamento - 1 (Em dias ex. 30/60/90)":"3060","Forma de Pagamento - 1 ":"Boleto","Descrição da Compra":"ertyhrewerth","ORIGEM CENTRO CUSTO":"32456","PEDIDO_LINX":345,"Pagamento Antecipado Fornecedor":"Sim","Valor Adiantamento fornecedor":null,"parcela1":100.0,"parcela2":100.0,"parcela3":100.0,"parcela4":100.0,"parcela5":100.0,"parcela6":100.0,"parcela7":100.0,"parcela8":100.0,"parcela9":100.0,"vencimento parcela3":"2026-07-10T00:00:00.000Z","vencimento parcela4":"2026-07-10T00:00:00.000Z","vencimento parcela5":"2026-07-22T00:00:00.000Z","vencimento parcela6":"2026-07-17T00:00:00.000Z","vencimento parcela7":"2026-07-11T00:00:00.000Z","vencimento parcela8":"2026-07-23T00:00:00.000Z","vencimento parcela9":"2026-07-17T00:00:00.000Z","vencimento parcela11":"2026-07-23T00:00:00.000Z","vencimento parcela12":"2026-07-03T00:00:00.000Z","parcela11":100720.26,"parcela12":100720.26,"num. parcelas":11,"CNPJ":"28482230000153"},"documents":[]}
|
||||||
21
tests/fixtures/payload_tratado.json
vendored
Normal file
21
tests/fixtures/payload_tratado.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"id_processo": "6a6a59266f8555a6725125a7",
|
||||||
|
"protocolo": "protocolo",
|
||||||
|
"cnpj": "28482230000153",
|
||||||
|
"pedido_linx": "345",
|
||||||
|
"fornecedor": "ACIMAQ EQUIPAMENTOS INDUSTRIAIS E COMERCIAIS LTDA",
|
||||||
|
"tipo": "COMPRA INTERNA - DESPESAS",
|
||||||
|
"nf_entrada": null,
|
||||||
|
"aprovador": "Acrisio Dos Santos",
|
||||||
|
"valor_total": 202340.52,
|
||||||
|
"parcelas": {
|
||||||
|
"1": {
|
||||||
|
"valor": 100.0,
|
||||||
|
"vencimento": null
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"valor": 100.0,
|
||||||
|
"vencimento": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
178
tests/test_conector.py
Normal file
178
tests/test_conector.py
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
"""
|
||||||
|
Testes do caminho completo do conector, sem depender da VPS estar no ar.
|
||||||
|
|
||||||
|
A chave em tests/fixtures/ e descartavel, gerada so pra isto — nao tem
|
||||||
|
relacao com a do configs.json. O teste cifra com a publica de teste e
|
||||||
|
troca a chave do app pela privada de teste, entao roda offline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from app.schemas import PayloadCompras
|
||||||
|
from app.services import cripto
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
FIXTURES = Path(__file__).parent / "fixtures"
|
||||||
|
ROTA = "/v1/compras/dados"
|
||||||
|
|
||||||
|
_OAEP = padding.OAEP(
|
||||||
|
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||||
|
algorithm=hashes.SHA256(),
|
||||||
|
label=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cifrar(dados: dict) -> dict:
|
||||||
|
"""Faz o papel da API da VPS: cifra com a publica de teste."""
|
||||||
|
pem = (FIXTURES / "chave_teste_publica.pem").read_bytes()
|
||||||
|
publica = serialization.load_pem_public_key(pem)
|
||||||
|
|
||||||
|
chave_aes = AESGCM.generate_key(bit_length=256)
|
||||||
|
nonce = b"\x00" * 12 # fixo no teste: nao precisa de aleatoriedade aqui
|
||||||
|
corpo = json.dumps(dados, ensure_ascii=False, separators=(",", ":")).encode()
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
return {
|
||||||
|
"alg": cripto.ALGORITMO,
|
||||||
|
"chave": base64.b64encode(publica.encrypt(chave_aes, _OAEP)).decode(),
|
||||||
|
"nonce": base64.b64encode(nonce).decode(),
|
||||||
|
"dados": base64.b64encode(AESGCM(chave_aes).encrypt(nonce, corpo, None)).decode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def payload() -> dict:
|
||||||
|
return json.loads((FIXTURES / "payload_tratado.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cliente(monkeypatch) -> TestClient:
|
||||||
|
"""App com a chave de teste no lugar da real."""
|
||||||
|
from app.v1 import compras
|
||||||
|
|
||||||
|
privada = cripto.carregar_chave_privada(
|
||||||
|
(FIXTURES / "chave_teste_privada.pem").read_text()
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(compras, "_CHAVE", privada)
|
||||||
|
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
return TestClient(app, raise_server_exceptions=False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def token() -> str:
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
return settings.vps.token
|
||||||
|
|
||||||
|
|
||||||
|
# --- autenticacao ---------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_sem_token_recusa(cliente, payload):
|
||||||
|
r = cliente.post(ROTA, json=_cifrar(payload))
|
||||||
|
assert r.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_errado_recusa(cliente, payload):
|
||||||
|
r = cliente.post(ROTA, json=_cifrar(payload), headers={"X-Token": "chute"})
|
||||||
|
assert r.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
# --- caminho feliz --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_recebe_e_valida(cliente, payload, token):
|
||||||
|
r = cliente.post(ROTA, json=_cifrar(payload), headers={"X-Token": token})
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
corpo = r.json()
|
||||||
|
assert corpo["id_processo"] == payload["id_processo"]
|
||||||
|
assert corpo["parcelas"] == len(payload["parcelas"])
|
||||||
|
# Enquanto nao houver Oracle isto tem que continuar False — se virar
|
||||||
|
# True sem camada de banco, alguem mentiu na resposta.
|
||||||
|
assert corpo["persistido"] is False
|
||||||
|
|
||||||
|
|
||||||
|
# --- rejeicoes ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_envelope_adulterado_recusa(cliente, payload, token):
|
||||||
|
"""O GCM autentica: mexer no corpo tem que estourar, nao passar."""
|
||||||
|
envelope = _cifrar(payload)
|
||||||
|
envelope["dados"] = "A" + envelope["dados"][1:]
|
||||||
|
|
||||||
|
r = cliente.post(ROTA, json=envelope, headers={"X-Token": token})
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_chave_errada_recusa(cliente, payload, token):
|
||||||
|
"""Cifrado com outra publica: a privada do conector nao abre."""
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
|
outra = rsa.generate_private_key(public_exponent=65537, key_size=2048).public_key()
|
||||||
|
envelope = _cifrar(payload)
|
||||||
|
import base64
|
||||||
|
|
||||||
|
envelope["chave"] = base64.b64encode(
|
||||||
|
outra.encrypt(AESGCM.generate_key(bit_length=256), _OAEP)
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
r = cliente.post(ROTA, json=envelope, headers={"X-Token": token})
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_fora_do_contrato_recusa(cliente, payload, token):
|
||||||
|
"""
|
||||||
|
Decifra mas o formato mudou: os dois lados sairam de sincronia.
|
||||||
|
|
||||||
|
Tem que dar 422 (nao 200 nem 500) — falhar explicito e melhor do que
|
||||||
|
gravar dado torto no Oracle depois.
|
||||||
|
"""
|
||||||
|
torto = dict(payload)
|
||||||
|
del torto["cnpj"]
|
||||||
|
|
||||||
|
r = cliente.post(ROTA, json=_cifrar(torto), headers={"X-Token": token})
|
||||||
|
assert r.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
# --- contrato -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_contrato_aceita_o_payload_da_api(payload):
|
||||||
|
"""
|
||||||
|
Trava o contrato com a API da VPS.
|
||||||
|
|
||||||
|
Se este teste quebrar, o PayloadCompras daqui e o de la divergiram —
|
||||||
|
conferir os dois antes de qualquer deploy.
|
||||||
|
"""
|
||||||
|
dados = PayloadCompras.model_validate(payload)
|
||||||
|
|
||||||
|
assert dados.id_processo
|
||||||
|
assert dados.valor_total > 0
|
||||||
|
assert dados.parcelas
|
||||||
|
for numero, parcela in dados.parcelas.items():
|
||||||
|
assert isinstance(numero, int)
|
||||||
|
assert parcela.valor > 0
|
||||||
|
assert parcela.vencimento is None or isinstance(parcela.vencimento, datetime)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vencimento_com_z_vira_datetime_utc():
|
||||||
|
"""O Holmes manda '2026-07-10T00:00:00.000Z'; tem que virar datetime aware."""
|
||||||
|
dados = PayloadCompras.model_validate(
|
||||||
|
{
|
||||||
|
"id_processo": "x", "protocolo": "p", "cnpj": "1", "pedido_linx": "2",
|
||||||
|
"fornecedor": "F", "tipo": "T", "aprovador": "A", "valor_total": 10.0,
|
||||||
|
"parcelas": {"3": {"valor": 10.0, "vencimento": "2026-07-10T00:00:00.000Z"}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
vencimento = dados.parcelas[3].vencimento
|
||||||
|
assert vencimento == datetime(2026, 7, 10, tzinfo=timezone.utc)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue