From eed473c5f3f2354672f8b12fccf6defd62f48808 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Sat, 15 Aug 2026 11:50:37 -0300 Subject: [PATCH] fix: telefone de contato sem DDI e updates/excludes que fingiam sucesso MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/db/clientes.py | 7 ++++- app/db/contatos.py | 12 ++++++-- app/db/funcionarios.py | 7 ++++- app/routers/clientes.py | 16 +++++++++- app/routers/contatos.py | 32 ++++++++++++++++---- app/routers/funcionarios.py | 35 +++++++++++++++------- app/templates/clientes_lista.html | 6 ++-- tests/test_db_clientes.py | 6 +++- tests/test_db_contatos.py | 12 ++++++-- tests/test_router_clientes.py | 50 +++++++++++++++++++++++++++++++ tests/test_router_contatos.py | 40 +++++++++++++++++++++++-- tests/test_router_funcionarios.py | 44 +++++++++++++++++++++++++++ 12 files changed, 238 insertions(+), 29 deletions(-) diff --git a/app/db/clientes.py b/app/db/clientes.py index f8c7fcd..2fe7af8 100644 --- a/app/db/clientes.py +++ b/app/db/clientes.py @@ -155,7 +155,11 @@ def atualizar_cliente(cliente_id: int, dados: dict) -> None: conn.close() -def excluir_cliente(cliente_id: int) -> None: +def excluir_cliente(cliente_id: int) -> bool: + """True se a linha realmente existia (cur.rowcount > 0) — um ID + inexistente não gera erro nenhum do MySQL, então sem checar rowcount + o chamador não tem como distinguir "excluiu" de "não tinha nada pra + excluir".""" conn = get_connection() try: with conn.cursor() as cur: @@ -165,5 +169,6 @@ def excluir_cliente(cliente_id: int) -> None: raise ClienteComDependenciasError( "Este cliente tem contatos cadastrados (CLIENTE_CONTATO) — remova-os antes de excluir." ) from exc + return cur.rowcount > 0 finally: conn.close() diff --git a/app/db/contatos.py b/app/db/contatos.py index 811ead6..58902b7 100644 --- a/app/db/contatos.py +++ b/app/db/contatos.py @@ -50,7 +50,12 @@ def inserir_contato(cliente_id: int, nome: str | None, telefone: str, email: str def atualizar_contato( contato_id: int, cliente_id: int, nome: str | None, telefone: str, email: str | None = None -) -> None: +) -> bool: + """True se realmente existia um CLIENTE_CONTATO com esse ID nesse + cliente (cur.rowcount > 0) — um ID inexistente, ou de outro cliente, + não gera erro nenhum do MySQL (WHERE simplesmente não bate com + nada), então sem checar rowcount o chamador não tem como saber que + não alterou nada.""" conn = get_connection() try: with conn.cursor() as cur: @@ -59,11 +64,13 @@ def atualizar_contato( "WHERE ID = %s AND CLIENTE_ID = %s", (nome, telefone, email, contato_id, cliente_id), ) + return cur.rowcount > 0 finally: conn.close() -def excluir_contato(contato_id: int, cliente_id: int) -> None: +def excluir_contato(contato_id: int, cliente_id: int) -> bool: + """True se uma linha foi de fato excluída — ver atualizar_contato.""" conn = get_connection() try: with conn.cursor() as cur: @@ -71,5 +78,6 @@ def excluir_contato(contato_id: int, cliente_id: int) -> None: "DELETE FROM CLIENTE_CONTATO WHERE ID = %s AND CLIENTE_ID = %s", (contato_id, cliente_id), ) + return cur.rowcount > 0 finally: conn.close() diff --git a/app/db/funcionarios.py b/app/db/funcionarios.py index 420f582..4b7d49b 100644 --- a/app/db/funcionarios.py +++ b/app/db/funcionarios.py @@ -117,7 +117,11 @@ def atualizar_funcionario(funcionario_id: int, nome: str, login: str, perfil: st conn.close() -def redefinir_senha(funcionario_id: int, nova_senha: str) -> None: +def redefinir_senha(funcionario_id: int, nova_senha: str) -> bool: + """True se o ID realmente existia (cur.rowcount > 0) — sem essa + checagem um ID inexistente fazia um UPDATE que não afeta nenhuma + linha, sem erro nenhum do MySQL, e o chamador registrava "sucesso" + no log mesmo sem ter mudado a senha de ninguém.""" conn = get_connection() try: with conn.cursor() as cur: @@ -125,6 +129,7 @@ def redefinir_senha(funcionario_id: int, nova_senha: str) -> None: "UPDATE FUNCIONARIO_PAINEL SET SENHA_HASH = %s WHERE ID = %s", (gerar_hash(nova_senha), funcionario_id), ) + return cur.rowcount > 0 finally: conn.close() diff --git a/app/routers/clientes.py b/app/routers/clientes.py index f29a1e9..aae8757 100644 --- a/app/routers/clientes.py +++ b/app/routers/clientes.py @@ -259,6 +259,15 @@ def form_editar(request: Request, cliente_id: int, usuario: str = Depends(exigir @router.post("/{cliente_id}/editar", dependencies=[Depends(exigir_csrf)]) async def editar(request: Request, cliente_id: int, usuario: str = Depends(exigir_login)): + if db_clientes.obter_cliente(cliente_id) is None: + # Sem essa checagem, um cliente_id inexistente (ex.: URL editada à + # mão, ou um duplo submit depois de já ter sido excluído em outra + # aba) caía direto no UPDATE — que não afeta nenhuma linha, não + # levanta erro nenhum do MySQL, e ainda assim registrava "editar + # ... sucesso" no log antes de redirecionar pra uma página de + # detalhe que também vai dizer "cliente não encontrado". + return _redirect_com_mensagem("/clientes", "Cliente não encontrado.", "danger") + form = dict((await request.form())) dados = _ler_form_cliente(form) _normalizar_documento_e_telefones(dados) @@ -300,9 +309,14 @@ async def editar(request: Request, cliente_id: int, usuario: str = Depends(exigi @router.post("/{cliente_id}/excluir", dependencies=[Depends(exigir_csrf)]) def excluir(cliente_id: int, usuario: str = Depends(exigir_login)): try: - db_clientes.excluir_cliente(cliente_id) + excluido = db_clientes.excluir_cliente(cliente_id) except db_clientes.ClienteComDependenciasError as exc: db_log.registrar(usuario, "excluir", "cliente", str(cliente_id), False, str(exc)) return _redirect_com_mensagem(f"/clientes/{cliente_id}", str(exc), "danger") + + if not excluido: + db_log.registrar(usuario, "excluir", "cliente", str(cliente_id), False, "cliente não encontrado") + return _redirect_com_mensagem("/clientes", "Cliente não encontrado.", "danger") + db_log.registrar(usuario, "excluir", "cliente", str(cliente_id), True) return _redirect_com_mensagem("/clientes", "Cliente excluído.") diff --git a/app/routers/contatos.py b/app/routers/contatos.py index 3e324de..2529b1f 100644 --- a/app/routers/contatos.py +++ b/app/routers/contatos.py @@ -2,6 +2,15 @@ Rotas de gestão de CLIENTE_CONTATO — sempre no contexto de um cliente (cliente_id na URL). Redireciona de volta para /clientes/{cliente_id} (a tela de detalhe do cliente, montada na Task 8). + +TELEFONE é gravado sempre com o DDI 55 embutido (normalizar_telefone_com_ddi, +mesma normalização usada em CLIENTE.TELEFONE/CELULAR — ver app/normalize.py) +porque é esse valor que o workflow de envio automático de chave por +WhatsApp lê direto de CLIENTE_CONTATO.TELEFONE, sem nenhuma normalização +própria (node "10 - Validar Telefones" do workflow n8n só faz +`String(telefone)`, sem adicionar DDI). Um contato cadastrado aqui sem o +55 embutido nunca recebe a chave automaticamente — silencioso, sem erro +visível no painel. """ from urllib.parse import urlencode @@ -13,7 +22,7 @@ from ..csrf import exigir_csrf from ..db import clientes as db_clientes from ..db import contatos as db_contatos from ..db import log as db_log -from ..normalize import normalizar_telefone +from ..normalize import normalizar_telefone_com_ddi router = APIRouter(prefix="/clientes") @@ -34,7 +43,7 @@ def adicionar( if db_clientes.obter_cliente(cliente_id) is None: return _redirect_com_mensagem(cliente_id, "Cliente não encontrado.", "danger") - telefone_normalizado = normalizar_telefone(telefone) + telefone_normalizado = normalizar_telefone_com_ddi(telefone) if not telefone_normalizado: return _redirect_com_mensagem(cliente_id, "Telefone obrigatório.", "danger") @@ -56,7 +65,7 @@ def editar( email: str = Form(""), usuario: str = Depends(exigir_login), ): - telefone_normalizado = normalizar_telefone(telefone) + telefone_normalizado = normalizar_telefone_com_ddi(telefone) if not telefone_normalizado: return _redirect_com_mensagem(cliente_id, "Telefone obrigatório.", "danger") @@ -64,13 +73,26 @@ def editar( db_log.registrar(usuario, "editar", "contato", str(contato_id), False, "telefone duplicado") return _redirect_com_mensagem(cliente_id, f"O telefone {telefone_normalizado} já está cadastrado para este cliente.", "warning") - db_contatos.atualizar_contato(contato_id, cliente_id, nome.strip() or None, telefone_normalizado, email.strip() or None) + atualizado = db_contatos.atualizar_contato( + contato_id, cliente_id, nome.strip() or None, telefone_normalizado, email.strip() or None + ) + if not atualizado: + # ID de contato que não existe (ou pertence a outro cliente — a + # query já é escopada por CLIENTE_ID) — sem essa checagem o log + # registrava "sucesso" mesmo sem nenhuma linha alterada. + db_log.registrar(usuario, "editar", "contato", str(contato_id), False, "contato não encontrado") + return _redirect_com_mensagem(cliente_id, "Contato não encontrado.", "danger") + db_log.registrar(usuario, "editar", "contato", str(contato_id), True) return _redirect_com_mensagem(cliente_id, "Contato atualizado.") @router.post("/{cliente_id}/contatos/{contato_id}/excluir", dependencies=[Depends(exigir_csrf)]) def excluir(cliente_id: int, contato_id: int, usuario: str = Depends(exigir_login)): - db_contatos.excluir_contato(contato_id, cliente_id) + excluido = db_contatos.excluir_contato(contato_id, cliente_id) + if not excluido: + db_log.registrar(usuario, "excluir", "contato", str(contato_id), False, "contato não encontrado") + return _redirect_com_mensagem(cliente_id, "Contato não encontrado.", "danger") + db_log.registrar(usuario, "excluir", "contato", str(contato_id), True) return _redirect_com_mensagem(cliente_id, "Contato excluído.") diff --git a/app/routers/funcionarios.py b/app/routers/funcionarios.py index bbc35b5..7937314 100644 --- a/app/routers/funcionarios.py +++ b/app/routers/funcionarios.py @@ -121,15 +121,17 @@ def editar( erro = f"O login \"{login}\" já está em uso." else: atual = db_funcionarios.obter_funcionario(funcionario_id) - rebaixando_ultimo_admin = ( - atual is not None - and atual["PERFIL"] == "ADMIN" - and atual["ATIVO"] == 1 - and perfil == "PADRAO" - and db_funcionarios.contar_admins_ativos() <= 1 - ) - if rebaixando_ultimo_admin: - erro = "Não é possível — este é o único administrador ativo." + if atual is None: + erro = "Funcionário não encontrado." + else: + rebaixando_ultimo_admin = ( + atual["PERFIL"] == "ADMIN" + and atual["ATIVO"] == 1 + and perfil == "PADRAO" + and db_funcionarios.contar_admins_ativos() <= 1 + ) + if rebaixando_ultimo_admin: + erro = "Não é possível — este é o único administrador ativo." if erro: return templates.TemplateResponse( @@ -158,7 +160,11 @@ def redefinir_senha( db_log.registrar(str(usuario), "redefinir_senha", "funcionario", str(funcionario_id), False, "senhas não coincidem") return _redirect_com_mensagem("As senhas digitadas não coincidem.", "danger") - db_funcionarios.redefinir_senha(funcionario_id, nova_senha) + atualizado = db_funcionarios.redefinir_senha(funcionario_id, nova_senha) + if not atualizado: + db_log.registrar(str(usuario), "redefinir_senha", "funcionario", str(funcionario_id), False, "funcionário não encontrado") + return _redirect_com_mensagem("Funcionário não encontrado.", "danger") + db_log.registrar(str(usuario), "redefinir_senha", "funcionario", str(funcionario_id), True) return _redirect_com_mensagem("Senha redefinida.") @@ -166,7 +172,14 @@ def redefinir_senha( @router.post("/{funcionario_id}/alternar-ativo", dependencies=[Depends(exigir_csrf)]) def alternar_ativo(funcionario_id: int, usuario: UsuarioLogado = Depends(exigir_admin)): atual = db_funcionarios.obter_funcionario(funcionario_id) - if atual is not None and atual["ATIVO"] == 1: + if atual is None: + # Sem essa checagem, um ID inexistente caía direto no UPDATE (que + # não afeta nenhuma linha, sem erro do MySQL) e ainda registrava + # "sucesso" no log. + db_log.registrar(str(usuario), "alternar_ativo", "funcionario", str(funcionario_id), False, "funcionário não encontrado") + return _redirect_com_mensagem("Funcionário não encontrado.", "danger") + + if atual["ATIVO"] == 1: # Só desativações (ATIVO 1 -> 0) de um admin passam pela checagem — # reativar (0 -> 1) nunca reduz a contagem de admins ativos. vai_desativar_ultimo_admin = atual["PERFIL"] == "ADMIN" and db_funcionarios.contar_admins_ativos() <= 1 diff --git a/app/templates/clientes_lista.html b/app/templates/clientes_lista.html index 99e1586..2f9eddd 100644 --- a/app/templates/clientes_lista.html +++ b/app/templates/clientes_lista.html @@ -24,7 +24,7 @@ {% for campo, rotulo in colunas_tabela %} {% set nova_direcao = "desc" if (ordenar_por == campo and direcao == "asc") else "asc" %} - + {{ rotulo }}{% if ordenar_por == campo %}{{ " ▲" if direcao == "asc" else " ▼" }}{% endif %} @@ -45,11 +45,11 @@ {% if total_paginas and total_paginas > 1 %}
{% if pagina > 1 %} - ← Anterior + ← Anterior {% endif %} Página {{ pagina }} de {{ total_paginas }} ({{ total }} clientes) {% if pagina < total_paginas %} - Próxima → + Próxima → {% endif %}
{% endif %} diff --git a/tests/test_db_clientes.py b/tests/test_db_clientes.py index 4d9efc8..26ce736 100644 --- a/tests/test_db_clientes.py +++ b/tests/test_db_clientes.py @@ -83,10 +83,14 @@ def test_atualizar_cliente(cliente_id): def test_excluir_cliente_sem_dependencias(cliente_id): - db_clientes.excluir_cliente(cliente_id) + assert db_clientes.excluir_cliente(cliente_id) is True assert db_clientes.obter_cliente(cliente_id) is None +def test_excluir_cliente_inexistente_retorna_false(): + assert db_clientes.excluir_cliente(999999999) is False + + def test_listar_clientes_busca_e_restrita_a_coluna_escolhida(cliente_id): # ordenar_por="codigo" restringe a busca à coluna CODIGO — o termo # "TESTE PAINEL RAZAO" existe em RAZAO/FANTASIA, mas não deve aparecer diff --git a/tests/test_db_contatos.py b/tests/test_db_contatos.py index 50f3495..48cc826 100644 --- a/tests/test_db_contatos.py +++ b/tests/test_db_contatos.py @@ -47,13 +47,21 @@ def test_contato_duplicado_ignora_proprio_id(cliente_id): def test_atualizar_contato(cliente_id): contato_id = db_contatos.inserir_contato(cliente_id, "Fulano", "75999090001") - db_contatos.atualizar_contato(contato_id, cliente_id, "Fulano Editado", "75999090003") + assert db_contatos.atualizar_contato(contato_id, cliente_id, "Fulano Editado", "75999090003") is True contatos = db_contatos.listar_contatos_por_cliente(cliente_id) assert contatos[0]["NOME"] == "Fulano Editado" assert contatos[0]["TELEFONE"] == "75999090003" +def test_atualizar_contato_inexistente_retorna_false(cliente_id): + assert db_contatos.atualizar_contato(999999, cliente_id, "Fulano", "75999090001") is False + + def test_excluir_contato(cliente_id): contato_id = db_contatos.inserir_contato(cliente_id, "Fulano", "75999090001") - db_contatos.excluir_contato(contato_id, cliente_id) + assert db_contatos.excluir_contato(contato_id, cliente_id) is True assert db_contatos.listar_contatos_por_cliente(cliente_id) == [] + + +def test_excluir_contato_inexistente_retorna_false(cliente_id): + assert db_contatos.excluir_contato(999999, cliente_id) is False diff --git a/tests/test_router_clientes.py b/tests/test_router_clientes.py index c84065a..68069ae 100644 --- a/tests/test_router_clientes.py +++ b/tests/test_router_clientes.py @@ -123,6 +123,27 @@ def test_cabecalhos_da_lista_alternam_direcao(monkeypatch): assert 'ordenar_por=razao&direcao=asc' in resposta.text +def test_busca_com_e_comercial_nao_quebra_links_de_paginacao_e_ordenacao(monkeypatch): + # "&" cru dentro de busca, sem urlencode, corta o resto da query string + # do link (ex.: "busca=A&B&pagina=2" vira busca="A", parâmetro solto + # "B", perdendo "pagina=2") — reprodução do bug corrigido em + # clientes_lista.html. + monkeypatch.setattr( + router_clientes.db_clientes, + "listar_clientes", + lambda busca=None, limite=100, offset=0, ordenar_por="razao", direcao="asc": [ + {"ID": i, "CODIGO": f"{i:06d}", "RAZAO": f"Cliente {i}", "FANTASIA": f"C{i}", "CNPJ": "1"} + for i in range(1, 6) + ], + ) + monkeypatch.setattr(router_clientes.db_clientes, "contar_clientes", lambda busca=None, ordenar_por="razao": 250) + + resposta = client.get("/clientes?pagina=2&busca=A%26B") + assert resposta.status_code == 200 + assert "busca=A%26B" in resposta.text + assert "busca=A&B" not in resposta.text + + def test_listar_clientes_direcao_invalida_cai_no_padrao(monkeypatch): capturado = {} @@ -300,6 +321,7 @@ def test_criar_cliente_normaliza_cnpj_e_telefones(monkeypatch): def test_editar_cliente_normaliza_cnpj_e_telefones(monkeypatch): + monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cliente_id: {"ID": cliente_id}) monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False) capturado = {} monkeypatch.setattr( @@ -325,6 +347,7 @@ def test_editar_cliente_normaliza_cnpj_e_telefones(monkeypatch): def test_editar_cliente_codigo_duplicado_bloqueia_sem_bypass(monkeypatch): + monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cliente_id: {"ID": cliente_id}) monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: True) atualizado = {} monkeypatch.setattr( @@ -340,6 +363,32 @@ def test_editar_cliente_codigo_duplicado_bloqueia_sem_bypass(monkeypatch): assert "ok" not in atualizado +def test_editar_cliente_inexistente_nao_finge_sucesso(monkeypatch): + # Sem a checagem de existência, isso caía direto no UPDATE (0 linhas + # afetadas, sem erro do MySQL) e ainda registrava "sucesso" no log. + monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cliente_id: None) + atualizado = {} + monkeypatch.setattr( + router_clientes.db_clientes, + "atualizar_cliente", + lambda cliente_id, dados: atualizado.setdefault("ok", True), + ) + + resposta = client.post("/clientes/999/editar", data=_form_valido()) + assert resposta.status_code == 303 + assert resposta.headers["location"].startswith("/clientes?") + assert "ok" not in atualizado + + +def test_excluir_cliente_inexistente_nao_finge_sucesso(monkeypatch): + monkeypatch.setattr(router_clientes.db_clientes, "excluir_cliente", lambda cliente_id: False) + + resposta = client.post("/clientes/999/excluir", data={"csrf_token": CSRF_TESTE}) + assert resposta.status_code == 303 + assert resposta.headers["location"].startswith("/clientes?") + assert "danger" in resposta.headers["location"] + + def test_excluir_cliente_com_dependencia_mostra_erro(monkeypatch): monkeypatch.setattr( router_clientes.db_clientes, @@ -436,6 +485,7 @@ def test_criar_cliente_com_cnpj_dv_invalido_bloqueia_sempre(monkeypatch): def test_editar_cliente_com_cnpj_dv_invalido_bloqueia(monkeypatch): + monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cliente_id: {"ID": cliente_id}) monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False) atualizado = {} monkeypatch.setattr( diff --git a/tests/test_router_contatos.py b/tests/test_router_contatos.py index cc39327..97607e4 100644 --- a/tests/test_router_contatos.py +++ b/tests/test_router_contatos.py @@ -39,7 +39,10 @@ def test_adicionar_contato_com_sucesso(monkeypatch): "/clientes/1/contatos", data={"nome": "Fulano", "telefone": "(75) 99909-0001", "csrf_token": CSRF_TESTE} ) assert resposta.status_code == 303 - assert chamadas["dados"] == (1, "Fulano", "75999090001", None) + # 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): @@ -77,7 +80,27 @@ def test_editar_contato_com_sucesso(monkeypatch): }, ) assert resposta.status_code == 303 - assert chamadas["dados"] == (5, 1, "Fulano Editado", "75999090002", "fulano@example.com") + 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): @@ -104,3 +127,16 @@ def test_excluir_contato(monkeypatch): 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 diff --git a/tests/test_router_funcionarios.py b/tests/test_router_funcionarios.py index c59d12d..7df34a1 100644 --- a/tests/test_router_funcionarios.py +++ b/tests/test_router_funcionarios.py @@ -195,6 +195,25 @@ def test_editar_admin_para_padrao_com_outros_admins_permite(monkeypatch): assert capturado["dados"] == (1, "Admin", "admin.db", "PADRAO") +def test_editar_funcionario_inexistente_nao_finge_sucesso(monkeypatch): + monkeypatch.setattr(router_funcionarios.db_funcionarios, "login_em_uso", lambda login, ignorar_id=None: False) + monkeypatch.setattr(router_funcionarios.db_funcionarios, "obter_funcionario", lambda fid: None) + chamou = {} + monkeypatch.setattr( + router_funcionarios.db_funcionarios, + "atualizar_funcionario", + lambda fid, nome, login, perfil: chamou.setdefault("ok", True), + ) + + resposta = client.post( + "/funcionarios/999/editar", + data={"nome": "Fulano", "login": "fulano", "perfil": "PADRAO", "csrf_token": CSRF_TESTE}, + ) + assert resposta.status_code == 200 + assert "não encontrado" in resposta.text + assert "ok" not in chamou + + def test_redefinir_senha_com_sucesso(monkeypatch): capturado = {} monkeypatch.setattr( @@ -226,6 +245,31 @@ def test_redefinir_senha_nao_coincide_bloqueia(monkeypatch): assert "ok" not in chamou +def test_redefinir_senha_funcionario_inexistente_nao_finge_sucesso(monkeypatch): + monkeypatch.setattr(router_funcionarios.db_funcionarios, "redefinir_senha", lambda fid, senha: False) + + resposta = client.post( + "/funcionarios/999/redefinir-senha", + data={"nova_senha": "novaSenha123", "confirmar_nova_senha": "novaSenha123", "csrf_token": CSRF_TESTE}, + follow_redirects=True, + ) + assert "não encontrado" in resposta.text + + +def test_alternar_ativo_funcionario_inexistente_nao_finge_sucesso(monkeypatch): + monkeypatch.setattr(router_funcionarios.db_funcionarios, "obter_funcionario", lambda fid: None) + chamou = {} + monkeypatch.setattr( + router_funcionarios.db_funcionarios, "alternar_ativo", lambda fid: chamou.setdefault("ok", True) + ) + + resposta = client.post( + "/funcionarios/999/alternar-ativo", data={"csrf_token": CSRF_TESTE}, follow_redirects=True + ) + assert "não encontrado" in resposta.text + assert "ok" not in chamou + + def test_alternar_ativo(monkeypatch): monkeypatch.setattr( router_funcionarios.db_funcionarios,