feat: login individual por funcionario (cadastro, senha com hash, ativar/desativar)
- Tabela FUNCIONARIO_PAINEL (nova, ja criada no banco de dev) + app/db/funcionarios.py: nome/login/senha (hash scrypt, stdlib, sem dependencia nova) por funcionario, nunca excluido de verdade (so ativo/inativo). - app/senha.py: gerar_hash/verificar_senha com hashlib.scrypt + salt aleatorio por senha. - app/auth.py: login passa a checar duas fontes, nessa ordem - ADMIN_USER/ADMIN_PASSWORD (bootstrap, continua exigindo as duas preenchidas) e depois FUNCIONARIO_PAINEL (so contas ATIVO=1). O nome retornado (usado no log) passa a ser o nome de verdade do funcionario, nao mais sempre 'admin'. - Tela /funcionarios: listar, criar (com confirmacao de senha e checagem de login duplicado), editar nome/login, redefinir senha (form separado) e ativar/desativar. Tudo logado em LOG_PAINEL. - Testado ponta a ponta contra o app real: funcionario criado via tela consegue logar de verdade, e a acao aparece no /log com o nome de quem fez. 93/93 testes passando (23 novos).
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.auth import exigir_login
|
||||
from app.db import funcionarios as db_funcionarios
|
||||
from app.db.connection import get_connection
|
||||
|
||||
app_teste = FastAPI()
|
||||
|
||||
@@ -45,3 +48,44 @@ def test_admin_user_e_senha_vazios_nao_autentica_com_credenciais_vazias(monkeypa
|
||||
monkeypatch.setenv("ADMIN_PASSWORD", "")
|
||||
resposta = client.get("/protegido", headers=_basic("", ""))
|
||||
assert resposta.status_code == 401
|
||||
|
||||
|
||||
def _limpar_login(login: str) -> None:
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM FUNCIONARIO_PAINEL WHERE LOGIN = %s", (login,))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def funcionario_teste():
|
||||
login = "teste.login.auth"
|
||||
_limpar_login(login)
|
||||
funcionario_id = db_funcionarios.criar_funcionario("Fulano de Teste", login, "senhaForte123")
|
||||
yield login
|
||||
_limpar_login(login)
|
||||
|
||||
|
||||
def test_funcionario_ativo_com_senha_correta_autentica(funcionario_teste):
|
||||
resposta = client.get("/protegido", headers=_basic(funcionario_teste, "senhaForte123"))
|
||||
assert resposta.status_code == 200
|
||||
assert resposta.json() == {"usuario": "Fulano de Teste"}
|
||||
|
||||
|
||||
def test_funcionario_com_senha_errada_nao_autentica(funcionario_teste):
|
||||
resposta = client.get("/protegido", headers=_basic(funcionario_teste, "senhaErrada"))
|
||||
assert resposta.status_code == 401
|
||||
|
||||
|
||||
def test_funcionario_inativo_nao_autentica(funcionario_teste):
|
||||
funcionario = db_funcionarios.obter_por_login(funcionario_teste)
|
||||
db_funcionarios.alternar_ativo(funcionario["ID"]) # desativa
|
||||
resposta = client.get("/protegido", headers=_basic(funcionario_teste, "senhaForte123"))
|
||||
assert resposta.status_code == 401
|
||||
|
||||
|
||||
def test_login_inexistente_nao_autentica():
|
||||
resposta = client.get("/protegido", headers=_basic("nao-existe-esse-login", "qualquer"))
|
||||
assert resposta.status_code == 401
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Testes de integração contra o banco de dev (ver test_db_connection.py)."""
|
||||
import pytest
|
||||
|
||||
from app.db import funcionarios as db_funcionarios
|
||||
from app.db.connection import get_connection
|
||||
|
||||
LOGIN_TESTE = "teste.db.funcionarios"
|
||||
|
||||
|
||||
def _limpar():
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM FUNCIONARIO_PAINEL WHERE LOGIN LIKE 'teste.db.funcionarios%'")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def funcionario_id():
|
||||
_limpar()
|
||||
novo_id = db_funcionarios.criar_funcionario("Fulano Teste", LOGIN_TESTE, "senhaForte123")
|
||||
yield novo_id
|
||||
_limpar()
|
||||
|
||||
|
||||
def test_criar_e_obter_funcionario(funcionario_id):
|
||||
f = db_funcionarios.obter_funcionario(funcionario_id)
|
||||
assert f is not None
|
||||
assert f["NOME"] == "Fulano Teste"
|
||||
assert f["LOGIN"] == LOGIN_TESTE
|
||||
assert f["ATIVO"] == 1
|
||||
assert "SENHA_HASH" not in f # obter_funcionario não expõe o hash
|
||||
|
||||
|
||||
def test_obter_por_login_inclui_senha_hash(funcionario_id):
|
||||
f = db_funcionarios.obter_por_login(LOGIN_TESTE)
|
||||
assert f is not None
|
||||
assert "SENHA_HASH" in f
|
||||
assert f["SENHA_HASH"] != "senhaForte123" # nunca em texto puro
|
||||
|
||||
|
||||
def test_obter_por_login_inexistente(funcionario_id):
|
||||
assert db_funcionarios.obter_por_login("nao-existe-esse-login-xyz") is None
|
||||
|
||||
|
||||
def test_login_em_uso(funcionario_id):
|
||||
assert db_funcionarios.login_em_uso(LOGIN_TESTE) is True
|
||||
assert db_funcionarios.login_em_uso(LOGIN_TESTE, ignorar_id=funcionario_id) is False
|
||||
assert db_funcionarios.login_em_uso("login-que-nao-existe") is False
|
||||
|
||||
|
||||
def test_listar_funcionarios_inclui_o_criado(funcionario_id):
|
||||
logins = [f["LOGIN"] for f in db_funcionarios.listar_funcionarios()]
|
||||
assert LOGIN_TESTE in logins
|
||||
|
||||
|
||||
def test_atualizar_funcionario(funcionario_id):
|
||||
novo_login = "teste.db.funcionarios.editado"
|
||||
db_funcionarios.atualizar_funcionario(funcionario_id, "Fulano Editado", novo_login)
|
||||
f = db_funcionarios.obter_funcionario(funcionario_id)
|
||||
assert f["NOME"] == "Fulano Editado"
|
||||
assert f["LOGIN"] == novo_login
|
||||
|
||||
|
||||
def test_redefinir_senha(funcionario_id):
|
||||
hash_antes = db_funcionarios.obter_por_login(LOGIN_TESTE)["SENHA_HASH"]
|
||||
db_funcionarios.redefinir_senha(funcionario_id, "outraSenha456")
|
||||
hash_depois = db_funcionarios.obter_por_login(LOGIN_TESTE)["SENHA_HASH"]
|
||||
assert hash_antes != hash_depois
|
||||
|
||||
|
||||
def test_alternar_ativo(funcionario_id):
|
||||
assert db_funcionarios.obter_funcionario(funcionario_id)["ATIVO"] == 1
|
||||
db_funcionarios.alternar_ativo(funcionario_id)
|
||||
assert db_funcionarios.obter_funcionario(funcionario_id)["ATIVO"] == 0
|
||||
db_funcionarios.alternar_ativo(funcionario_id)
|
||||
assert db_funcionarios.obter_funcionario(funcionario_id)["ATIVO"] == 1
|
||||
@@ -0,0 +1,150 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.auth import exigir_login
|
||||
from app.routers import funcionarios as router_funcionarios
|
||||
|
||||
app_teste = FastAPI()
|
||||
app_teste.include_router(router_funcionarios.router)
|
||||
app_teste.dependency_overrides[exigir_login] = lambda: "teste"
|
||||
client = TestClient(app_teste, follow_redirects=False)
|
||||
CSRF_TESTE = "teste-csrf-token"
|
||||
client.cookies.set("csrf_token", CSRF_TESTE)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _sem_log_real(monkeypatch):
|
||||
monkeypatch.setattr(router_funcionarios.db_log, "registrar", lambda *args, **kwargs: None)
|
||||
|
||||
|
||||
def test_listar_funcionarios(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios,
|
||||
"listar_funcionarios",
|
||||
lambda: [{"ID": 1, "NOME": "Fulano", "LOGIN": "fulano", "ATIVO": 1, "CRIADO_EM": "2026-01-01"}],
|
||||
)
|
||||
resposta = client.get("/funcionarios")
|
||||
assert resposta.status_code == 200
|
||||
assert "Fulano" in resposta.text
|
||||
|
||||
|
||||
def test_criar_funcionario_com_sucesso(monkeypatch):
|
||||
monkeypatch.setattr(router_funcionarios.db_funcionarios, "login_em_uso", lambda login, ignorar_id=None: False)
|
||||
criado = {}
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios,
|
||||
"criar_funcionario",
|
||||
lambda nome, login, senha: criado.setdefault("dados", (nome, login, senha)) and 1,
|
||||
)
|
||||
|
||||
resposta = client.post(
|
||||
"/funcionarios/novo",
|
||||
data={
|
||||
"nome": "Fulano", "login": "fulano", "senha": "senhaForte123", "confirmar_senha": "senhaForte123",
|
||||
"csrf_token": CSRF_TESTE,
|
||||
},
|
||||
)
|
||||
assert resposta.status_code == 303
|
||||
assert criado["dados"] == ("Fulano", "fulano", "senhaForte123")
|
||||
|
||||
|
||||
def test_criar_funcionario_senhas_diferentes_bloqueia(monkeypatch):
|
||||
monkeypatch.setattr(router_funcionarios.db_funcionarios, "login_em_uso", lambda login, ignorar_id=None: False)
|
||||
chamou = {}
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios,
|
||||
"criar_funcionario",
|
||||
lambda nome, login, senha: chamou.setdefault("ok", True),
|
||||
)
|
||||
|
||||
resposta = client.post(
|
||||
"/funcionarios/novo",
|
||||
data={
|
||||
"nome": "Fulano", "login": "fulano", "senha": "senhaForte123", "confirmar_senha": "outraCoisa",
|
||||
"csrf_token": CSRF_TESTE,
|
||||
},
|
||||
)
|
||||
assert resposta.status_code == 200
|
||||
assert "não coincidem" in resposta.text
|
||||
assert "ok" not in chamou
|
||||
|
||||
|
||||
def test_criar_funcionario_login_duplicado_bloqueia(monkeypatch):
|
||||
monkeypatch.setattr(router_funcionarios.db_funcionarios, "login_em_uso", lambda login, ignorar_id=None: True)
|
||||
chamou = {}
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios,
|
||||
"criar_funcionario",
|
||||
lambda nome, login, senha: chamou.setdefault("ok", True),
|
||||
)
|
||||
|
||||
resposta = client.post(
|
||||
"/funcionarios/novo",
|
||||
data={
|
||||
"nome": "Fulano", "login": "fulano", "senha": "senhaForte123", "confirmar_senha": "senhaForte123",
|
||||
"csrf_token": CSRF_TESTE,
|
||||
},
|
||||
)
|
||||
assert resposta.status_code == 200
|
||||
assert "já está em uso" in resposta.text
|
||||
assert "ok" not in chamou
|
||||
|
||||
|
||||
def test_editar_funcionario_com_sucesso(monkeypatch):
|
||||
monkeypatch.setattr(router_funcionarios.db_funcionarios, "login_em_uso", lambda login, ignorar_id=None: False)
|
||||
capturado = {}
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios,
|
||||
"atualizar_funcionario",
|
||||
lambda fid, nome, login: capturado.setdefault("dados", (fid, nome, login)),
|
||||
)
|
||||
|
||||
resposta = client.post(
|
||||
"/funcionarios/1/editar",
|
||||
data={"nome": "Fulano Editado", "login": "fulano.editado", "csrf_token": CSRF_TESTE},
|
||||
)
|
||||
assert resposta.status_code == 303
|
||||
assert capturado["dados"] == (1, "Fulano Editado", "fulano.editado")
|
||||
|
||||
|
||||
def test_redefinir_senha_com_sucesso(monkeypatch):
|
||||
capturado = {}
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios,
|
||||
"redefinir_senha",
|
||||
lambda fid, senha: capturado.setdefault("dados", (fid, senha)),
|
||||
)
|
||||
|
||||
resposta = client.post(
|
||||
"/funcionarios/1/redefinir-senha",
|
||||
data={"nova_senha": "novaSenha123", "confirmar_nova_senha": "novaSenha123", "csrf_token": CSRF_TESTE},
|
||||
)
|
||||
assert resposta.status_code == 303
|
||||
assert capturado["dados"] == (1, "novaSenha123")
|
||||
|
||||
|
||||
def test_redefinir_senha_nao_coincide_bloqueia(monkeypatch):
|
||||
chamou = {}
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios, "redefinir_senha", lambda fid, senha: chamou.setdefault("ok", True)
|
||||
)
|
||||
|
||||
resposta = client.post(
|
||||
"/funcionarios/1/redefinir-senha",
|
||||
data={"nova_senha": "a", "confirmar_nova_senha": "b", "csrf_token": CSRF_TESTE},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert "não coincidem" in resposta.text
|
||||
assert "ok" not in chamou
|
||||
|
||||
|
||||
def test_alternar_ativo(monkeypatch):
|
||||
capturado = {}
|
||||
monkeypatch.setattr(
|
||||
router_funcionarios.db_funcionarios, "alternar_ativo", lambda fid: capturado.setdefault("id", fid)
|
||||
)
|
||||
|
||||
resposta = client.post("/funcionarios/1/alternar-ativo", data={"csrf_token": CSRF_TESTE})
|
||||
assert resposta.status_code == 303
|
||||
assert capturado["id"] == 1
|
||||
@@ -0,0 +1,26 @@
|
||||
from app.senha import gerar_hash, verificar_senha
|
||||
|
||||
|
||||
def test_hash_confere_com_senha_correta():
|
||||
h = gerar_hash("minhaSenha123")
|
||||
assert verificar_senha("minhaSenha123", h) is True
|
||||
|
||||
|
||||
def test_hash_rejeita_senha_errada():
|
||||
h = gerar_hash("minhaSenha123")
|
||||
assert verificar_senha("senhaErrada", h) is False
|
||||
|
||||
|
||||
def test_hashes_de_senhas_iguais_sao_diferentes():
|
||||
# Salt aleatório por chamada — dois hashes da mesma senha não são iguais.
|
||||
h1 = gerar_hash("mesmaSenha")
|
||||
h2 = gerar_hash("mesmaSenha")
|
||||
assert h1 != h2
|
||||
assert verificar_senha("mesmaSenha", h1) is True
|
||||
assert verificar_senha("mesmaSenha", h2) is True
|
||||
|
||||
|
||||
def test_hash_malformado_nao_lanca():
|
||||
assert verificar_senha("qualquer", "isso-nao-e-um-hash-valido") is False
|
||||
assert verificar_senha("qualquer", "") is False
|
||||
assert verificar_senha("qualquer", "md5$abc$def") is False
|
||||
Reference in New Issue
Block a user