- app/routers/contatos.py: CLIENTE_CONTATO.TELEFONE passa a ser gravado sempre com DDI 55 (normalizar_telefone_com_ddi, mesma normalização já usada em CLIENTE.TELEFONE/CELULAR) — confirmado no node '10 - Validar Telefones' do workflow n8n de produção (ER6Piy9yu93uOVuY) que o telefone é usado exatamente como está gravado, sem nenhuma normalização própria. Um contato cadastrado pelo painel sem o funcionário digitar '55' na frente nunca recebia a chave automática por WhatsApp — falha silenciosa, sem nenhum erro visível. - app/db/clientes.py, app/db/contatos.py, app/db/funcionarios.py: excluir_cliente/atualizar_contato/excluir_contato/redefinir_senha passam a retornar bool (cur.rowcount > 0) em vez de None — um ID inexistente não gera erro nenhum do MySQL (WHERE só não bate com nada), então sem checar rowcount o router não tinha como saber que não alterou/excluiu nada. - app/routers/clientes.py, app/routers/contatos.py, app/routers/funcionarios.py: editar/excluir/redefinir-senha/ alternar-ativo passam a checar essa existência e mostrar 'não encontrado' em vez de registrar 'sucesso' no log e redirecionar pra uma tela que também vai dizer que não encontrou nada. - app/templates/clientes_lista.html: busca não estava com urlencode nos links de paginação/ordenação — um termo de busca com '&' (comum em razão social, ex. 'Fulano & Cia') cortava o resto da query string do link, perdendo página/ordenação. Suíte completa (192 testes) passando, com 13 testes novos cobrindo os casos de 'não encontrado' e o DDI do telefone de contato. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
143 lines
6.5 KiB
Python
143 lines
6.5 KiB
Python
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.auth import exigir_login
|
|
from app.routers import contatos as router_contatos
|
|
from app.routers import clientes as router_clientes
|
|
|
|
app_teste = FastAPI()
|
|
app_teste.include_router(router_clientes.router)
|
|
app_teste.include_router(router_contatos.router)
|
|
app_teste.dependency_overrides[exigir_login] = lambda: "teste"
|
|
client = TestClient(app_teste, follow_redirects=False)
|
|
# Este app de teste não tem o middleware de CSRF (só o main.py real o monta).
|
|
# Simula um navegador que já tem o cookie definido: os testes de POST
|
|
# precisam mandar o mesmo valor no campo oculto "csrf_token".
|
|
CSRF_TESTE = "teste-csrf-token"
|
|
client.cookies.set("csrf_token", CSRF_TESTE)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _sem_log_real(monkeypatch):
|
|
"""Os routers registram cada ação em LOG_PAINEL — sem no-op, bateria no banco real."""
|
|
monkeypatch.setattr(router_contatos.db_log, "registrar", lambda *args, **kwargs: None)
|
|
monkeypatch.setattr(router_clientes.db_log, "registrar", lambda *args, **kwargs: None)
|
|
|
|
|
|
def test_adicionar_contato_com_sucesso(monkeypatch):
|
|
monkeypatch.setattr(router_contatos.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001"})
|
|
monkeypatch.setattr(router_contatos.db_contatos, "contato_duplicado", lambda cid, tel, ignorar_id=None: False)
|
|
chamadas = {}
|
|
monkeypatch.setattr(
|
|
router_contatos.db_contatos,
|
|
"inserir_contato",
|
|
lambda cid, nome, tel, email=None: chamadas.setdefault("dados", (cid, nome, tel, email)),
|
|
)
|
|
|
|
resposta = client.post(
|
|
"/clientes/1/contatos", data={"nome": "Fulano", "telefone": "(75) 99909-0001", "csrf_token": CSRF_TESTE}
|
|
)
|
|
assert resposta.status_code == 303
|
|
# DDI 55 sempre embutido — é esse valor que o workflow n8n de envio de
|
|
# chave por WhatsApp lê direto de CLIENTE_CONTATO.TELEFONE, sem
|
|
# normalizar nada (ver comentário no topo de app/routers/contatos.py).
|
|
assert chamadas["dados"] == (1, "Fulano", "5575999090001", None)
|
|
|
|
|
|
def test_adicionar_contato_duplicado_avisa(monkeypatch):
|
|
monkeypatch.setattr(router_contatos.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001"})
|
|
monkeypatch.setattr(router_contatos.db_contatos, "contato_duplicado", lambda cid, tel, ignorar_id=None: True)
|
|
# Also mock the clientes router's db functions for the redirect
|
|
monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001", "RAZAO": "Cliente", "FANTASIA": "Cliente"})
|
|
monkeypatch.setattr(router_clientes.db_contatos, "listar_contatos_por_cliente", lambda cid: [])
|
|
monkeypatch.setattr(router_clientes.db_chaves, "status_chaves", lambda codigo: None)
|
|
|
|
resposta = client.post(
|
|
"/clientes/1/contatos",
|
|
data={"nome": "Fulano", "telefone": "75999090001", "csrf_token": CSRF_TESTE},
|
|
follow_redirects=True,
|
|
)
|
|
assert "já está cadastrado" in resposta.text
|
|
|
|
|
|
def test_editar_contato_com_sucesso(monkeypatch):
|
|
monkeypatch.setattr(router_contatos.db_contatos, "contato_duplicado", lambda cid, tel, ignorar_id=None: False)
|
|
chamadas = {}
|
|
monkeypatch.setattr(
|
|
router_contatos.db_contatos,
|
|
"atualizar_contato",
|
|
lambda contato_id, cid, nome, tel, email=None: chamadas.setdefault(
|
|
"dados", (contato_id, cid, nome, tel, email)
|
|
),
|
|
)
|
|
|
|
resposta = client.post(
|
|
"/clientes/1/contatos/5/editar",
|
|
data={
|
|
"nome": "Fulano Editado", "telefone": "(75) 99909-0002", "email": "fulano@example.com",
|
|
"csrf_token": CSRF_TESTE,
|
|
},
|
|
)
|
|
assert resposta.status_code == 303
|
|
assert chamadas["dados"] == (5, 1, "Fulano Editado", "5575999090002", "fulano@example.com")
|
|
|
|
|
|
def test_editar_contato_inexistente_avisa_e_nao_finge_sucesso(monkeypatch):
|
|
# atualizar_contato retorna False quando o WHERE (ID + CLIENTE_ID) não
|
|
# bate com nenhuma linha — ID errado, ou de outro cliente.
|
|
monkeypatch.setattr(router_contatos.db_contatos, "contato_duplicado", lambda cid, tel, ignorar_id=None: False)
|
|
monkeypatch.setattr(router_contatos.db_contatos, "atualizar_contato", lambda *a, **k: False)
|
|
registros = []
|
|
monkeypatch.setattr(
|
|
router_contatos.db_log, "registrar", lambda *args, **kwargs: registros.append(args)
|
|
)
|
|
|
|
resposta = client.post(
|
|
"/clientes/1/contatos/999/editar",
|
|
data={"nome": "Fulano", "telefone": "75999090002", "csrf_token": CSRF_TESTE},
|
|
follow_redirects=False,
|
|
)
|
|
assert resposta.status_code == 303
|
|
assert "mensagem_tipo=danger" in resposta.headers["location"]
|
|
assert registros[-1][4] is False # sucesso=False no log (args: usuario, acao, entidade, entidade_id, sucesso, detalhe)
|
|
|
|
|
|
def test_editar_contato_duplicado_avisa(monkeypatch):
|
|
monkeypatch.setattr(router_contatos.db_contatos, "contato_duplicado", lambda cid, tel, ignorar_id=None: True)
|
|
monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001", "RAZAO": "Cliente", "FANTASIA": "Cliente"})
|
|
monkeypatch.setattr(router_clientes.db_contatos, "listar_contatos_por_cliente", lambda cid: [])
|
|
monkeypatch.setattr(router_clientes.db_chaves, "status_chaves", lambda codigo: None)
|
|
|
|
resposta = client.post(
|
|
"/clientes/1/contatos/5/editar",
|
|
data={"nome": "Fulano", "telefone": "75999090001", "csrf_token": CSRF_TESTE},
|
|
follow_redirects=True,
|
|
)
|
|
assert "já está cadastrado" in resposta.text
|
|
|
|
|
|
def test_excluir_contato(monkeypatch):
|
|
monkeypatch.setattr(router_contatos.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001"})
|
|
chamadas = {}
|
|
monkeypatch.setattr(
|
|
router_contatos.db_contatos, "excluir_contato", lambda contato_id, cid: chamadas.setdefault("dados", (contato_id, cid))
|
|
)
|
|
|
|
resposta = client.post("/clientes/1/contatos/5/excluir", data={"csrf_token": CSRF_TESTE})
|
|
assert resposta.status_code == 303
|
|
assert chamadas["dados"] == (5, 1)
|
|
|
|
|
|
def test_excluir_contato_inexistente_avisa_e_nao_finge_sucesso(monkeypatch):
|
|
monkeypatch.setattr(router_contatos.db_contatos, "excluir_contato", lambda *a, **k: False)
|
|
registros = []
|
|
monkeypatch.setattr(
|
|
router_contatos.db_log, "registrar", lambda *args, **kwargs: registros.append(args)
|
|
)
|
|
|
|
resposta = client.post("/clientes/1/contatos/999/excluir", data={"csrf_token": CSRF_TESTE}, follow_redirects=False)
|
|
assert resposta.status_code == 303
|
|
assert "mensagem_tipo=danger" in resposta.headers["location"]
|
|
assert registros[-1][4] is False
|