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