From 718e845651d536819fa2d3aa638c2ab7c6e77a2a Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 13 Aug 2026 20:29:45 -0300 Subject: [PATCH] feat: coluna PERFIL em FUNCIONARIO_PAINEL + contar_admins_ativos Co-Authored-By: Claude Sonnet 5 --- app/db/funcionarios.py | 47 ++++++++++++++++++++++++++++------- tests/test_db_funcionarios.py | 30 ++++++++++++++++++---- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/app/db/funcionarios.py b/app/db/funcionarios.py index 55b26a5..420f582 100644 --- a/app/db/funcionarios.py +++ b/app/db/funcionarios.py @@ -10,12 +10,26 @@ primeiro com: NOME VARCHAR(80) NOT NULL, LOGIN VARCHAR(40) NOT NULL, SENHA_HASH VARCHAR(255) NOT NULL, + PERFIL ENUM('ADMIN','PADRAO') NOT NULL DEFAULT 'ADMIN', ATIVO TINYINT(1) NOT NULL DEFAULT 1, CRIADO_EM DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (ID), UNIQUE KEY UQ_FUNCIONARIO_PAINEL_LOGIN (LOGIN) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +Se a tabela já existir sem a coluna PERFIL (ambientes criados antes dessa +funcionalidade), rode: + + ALTER TABLE FUNCIONARIO_PAINEL + ADD COLUMN PERFIL ENUM('ADMIN','PADRAO') NOT NULL DEFAULT 'ADMIN' + AFTER SENHA_HASH; + +O DEFAULT 'ADMIN' do ALTER TABLE é intencional: todo funcionário cadastrado +antes dessa migração vira admin automaticamente (decisão do projeto — ver +docs/superpowers/specs/2026-08-13-permissoes-login-sessao-design.md). +Cadastros novos sempre informam PERFIL explicitamente (vindo do +formulário), nunca dependem desse default. + Nunca excluído de verdade — "excluir" aqui é alternar ATIVO para 0 (mesma lógica de nunca apagar histórico usada no resto do projeto). """ @@ -27,7 +41,9 @@ def listar_funcionarios() -> list[dict]: conn = get_connection() try: with conn.cursor() as cur: - cur.execute("SELECT ID, NOME, LOGIN, ATIVO, CRIADO_EM FROM FUNCIONARIO_PAINEL ORDER BY NOME") + cur.execute( + "SELECT ID, NOME, LOGIN, PERFIL, ATIVO, CRIADO_EM FROM FUNCIONARIO_PAINEL ORDER BY NOME" + ) return cur.fetchall() finally: conn.close() @@ -38,7 +54,8 @@ def obter_funcionario(funcionario_id: int) -> dict | None: try: with conn.cursor() as cur: cur.execute( - "SELECT ID, NOME, LOGIN, ATIVO, CRIADO_EM FROM FUNCIONARIO_PAINEL WHERE ID = %s", (funcionario_id,) + "SELECT ID, NOME, LOGIN, PERFIL, ATIVO, CRIADO_EM FROM FUNCIONARIO_PAINEL WHERE ID = %s", + (funcionario_id,), ) return cur.fetchone() finally: @@ -51,7 +68,8 @@ def obter_por_login(login: str) -> dict | None: try: with conn.cursor() as cur: cur.execute( - "SELECT ID, NOME, LOGIN, SENHA_HASH, ATIVO FROM FUNCIONARIO_PAINEL WHERE LOGIN = %s", (login,) + "SELECT ID, NOME, LOGIN, SENHA_HASH, PERFIL, ATIVO FROM FUNCIONARIO_PAINEL WHERE LOGIN = %s", + (login,), ) return cur.fetchone() finally: @@ -73,26 +91,27 @@ def login_em_uso(login: str, ignorar_id: int | None = None) -> bool: conn.close() -def criar_funcionario(nome: str, login: str, senha: str) -> int: +def criar_funcionario(nome: str, login: str, senha: str, perfil: str) -> int: conn = get_connection() try: with conn.cursor() as cur: cur.execute( - "INSERT INTO FUNCIONARIO_PAINEL (NOME, LOGIN, SENHA_HASH, ATIVO) VALUES (%s, %s, %s, 1)", - (nome, login, gerar_hash(senha)), + "INSERT INTO FUNCIONARIO_PAINEL (NOME, LOGIN, SENHA_HASH, PERFIL, ATIVO) VALUES (%s, %s, %s, %s, 1)", + (nome, login, gerar_hash(senha), perfil), ) return cur.lastrowid finally: conn.close() -def atualizar_funcionario(funcionario_id: int, nome: str, login: str) -> None: - """Só nome/login — troca de senha é redefinir_senha().""" +def atualizar_funcionario(funcionario_id: int, nome: str, login: str, perfil: str) -> None: + """Nome/login/perfil — troca de senha é redefinir_senha().""" conn = get_connection() try: with conn.cursor() as cur: cur.execute( - "UPDATE FUNCIONARIO_PAINEL SET NOME = %s, LOGIN = %s WHERE ID = %s", (nome, login, funcionario_id) + "UPDATE FUNCIONARIO_PAINEL SET NOME = %s, LOGIN = %s, PERFIL = %s WHERE ID = %s", + (nome, login, perfil, funcionario_id), ) finally: conn.close() @@ -117,3 +136,13 @@ def alternar_ativo(funcionario_id: int) -> None: cur.execute("UPDATE FUNCIONARIO_PAINEL SET ATIVO = 1 - ATIVO WHERE ID = %s", (funcionario_id,)) finally: conn.close() + + +def contar_admins_ativos() -> int: + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute("SELECT COUNT(*) AS total FROM FUNCIONARIO_PAINEL WHERE PERFIL = 'ADMIN' AND ATIVO = 1") + return cur.fetchone()["total"] + finally: + conn.close() diff --git a/tests/test_db_funcionarios.py b/tests/test_db_funcionarios.py index 80b3446..11badd7 100644 --- a/tests/test_db_funcionarios.py +++ b/tests/test_db_funcionarios.py @@ -19,7 +19,7 @@ def _limpar(): @pytest.fixture def funcionario_id(): _limpar() - novo_id = db_funcionarios.criar_funcionario("Fulano Teste", LOGIN_TESTE, "senhaForte123") + novo_id = db_funcionarios.criar_funcionario("Fulano Teste", LOGIN_TESTE, "senhaForte123", "PADRAO") yield novo_id _limpar() @@ -30,14 +30,16 @@ def test_criar_e_obter_funcionario(funcionario_id): assert f["NOME"] == "Fulano Teste" assert f["LOGIN"] == LOGIN_TESTE assert f["ATIVO"] == 1 + assert f["PERFIL"] == "PADRAO" assert "SENHA_HASH" not in f # obter_funcionario não expõe o hash -def test_obter_por_login_inclui_senha_hash(funcionario_id): +def test_obter_por_login_inclui_senha_hash_e_perfil(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 + assert f["PERFIL"] == "PADRAO" def test_obter_por_login_inexistente(funcionario_id): @@ -51,16 +53,18 @@ def test_login_em_uso(funcionario_id): def test_listar_funcionarios_inclui_o_criado(funcionario_id): - logins = [f["LOGIN"] for f in db_funcionarios.listar_funcionarios()] - assert LOGIN_TESTE in logins + encontrados = {f["LOGIN"]: f for f in db_funcionarios.listar_funcionarios()} + assert LOGIN_TESTE in encontrados + assert encontrados[LOGIN_TESTE]["PERFIL"] == "PADRAO" def test_atualizar_funcionario(funcionario_id): novo_login = "teste.db.funcionarios.editado" - db_funcionarios.atualizar_funcionario(funcionario_id, "Fulano Editado", novo_login) + db_funcionarios.atualizar_funcionario(funcionario_id, "Fulano Editado", novo_login, "ADMIN") f = db_funcionarios.obter_funcionario(funcionario_id) assert f["NOME"] == "Fulano Editado" assert f["LOGIN"] == novo_login + assert f["PERFIL"] == "ADMIN" def test_redefinir_senha(funcionario_id): @@ -76,3 +80,19 @@ def test_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 + + +def test_contar_admins_ativos(funcionario_id): + # funcionario_id é PADRAO (fixture acima) — não conta. + antes = db_funcionarios.contar_admins_ativos() + + admin_id = db_funcionarios.criar_funcionario( + "Admin Teste", "teste.db.funcionarios.admin", "senhaForte123", "ADMIN" + ) + try: + assert db_funcionarios.contar_admins_ativos() == antes + 1 + + db_funcionarios.alternar_ativo(admin_id) # desativa + assert db_funcionarios.contar_admins_ativos() == antes + finally: + _limpar()