sgmp/solicitacoes/tests/test_desligamento_services.py

311 lines
13 KiB
Python
Raw Normal View History

2026-03-09 18:46:01 +00:00
# /SGMP_PROD/solicitacoes/tests/test_desligamento_services.py
import json
from datetime import date, timedelta
from unittest import TestCase as AssertionsMixin
from pathlib import Path
from django.test import TestCase
from django.db import IntegrityError
from ..models import (
PessoaRM,
UsuarioSistema,
Solicitacao,
Desligamento,
Aprovacao,
StatusSolicitacao,
DecisaoAprovacao,
EtapaAprovacao
)
from .. import services
# --- Estrutura para Geração do Relatório JSON ---
# Esta estrutura será populada durante a execução dos testes
TEST_RESULTS = {
"summary": {
"total": 0,
"passed": 0,
"failed": 0,
"errors": 0,
},
"details": [],
}
class TestResultLogger(AssertionsMixin):
"""
Um mixin para registrar os resultados de cada teste em nossa estrutura JSON.
"""
def run(self, result=None):
self.test_result_data = {
"name": self.id(),
"status": "PASS",
"description": self._testMethodDoc or "Sem descrição.",
"steps": [],
"error_message": None,
}
# Armazena a referência original do 'addFailure'
original_addFailure = result.addFailure
# Monkey-patch para capturar a falha
def custom_addFailure(test, err):
self.test_result_data["status"] = "FAIL"
self.test_result_data["error_message"] = str(err[1])
original_addFailure(test, err)
result.addFailure = custom_addFailure
super().run(result)
# Restaura o método original para não afetar outros testes
result.addFailure = original_addFailure
TEST_RESULTS["details"].append(self.test_result_data)
TEST_RESULTS["summary"]["total"] += 1
if self.test_result_data["status"] == "PASS":
TEST_RESULTS["summary"]["passed"] += 1
else:
TEST_RESULTS["summary"]["failed"] += 1
def _add_step(self, description, success=True):
self.test_result_data["steps"].append({
"description": description,
"success": success
})
class DesligamentoServiceTests(TestResultLogger, TestCase):
"""
Testa o ciclo de vida completo de uma Solicitação de Desligamento,
incluindo criação, validações, permissões e o fluxo de aprovação.
"""
@classmethod
def tearDownClass(cls):
"""
Executado uma vez no final de todos os testes desta classe.
Gera o arquivo JSON com os resultados.
"""
output_path = Path("./test_results.json")
with open(output_path, "w", encoding="utf-8") as f:
json.dump(TEST_RESULTS, f, indent=4, ensure_ascii=False)
print(f"\n[INFO] Relatório de testes salvo em: {output_path.resolve()}")
super().tearDownClass()
def setUp(self):
"""
Prepara o ambiente para cada teste, criando os usuários e
o funcionário que serão usados nos cenários.
"""
# 1. Criação dos Atores do Processo
self.solicitante = UsuarioSistema.objects.create(
matricula="100", nome="Gestor Solicitante", perfil=UsuarioSistema.Perfil.GESTOR
)
self.aprovador_head = UsuarioSistema.objects.create(
matricula="150", nome="Aprovador Head", perfil=UsuarioSistema.Perfil.HEAD
)
self.aprovador_gg = UsuarioSistema.objects.create(
matricula="200", nome="Aprovador de GG", perfil=UsuarioSistema.Perfil.GG
)
self.aprovador_controladoria = UsuarioSistema.objects.create(
matricula="300", nome="Aprovador da Controladoria", perfil=UsuarioSistema.Perfil.CONTROLADORIA
)
self.aprovador_diretoria = UsuarioSistema.objects.create(
matricula="400", nome="Aprovador da Diretoria", perfil=UsuarioSistema.Perfil.DIRETORIA
)
# 2. Criação do Objeto do Processo
self.funcionario = PessoaRM.objects.create(
id_rm="1-12345", matricula="12345", nome="Colaborador Teste", situacao='A'
)
# --- Testes de Caminho Feliz (Happy Path) ---
def test_criar_desligamento_sucesso(self):
"""
Garante que uma solicitação de desligamento é criada corretamente
com o status inicial 'RASCUNHO'.
"""
solicitacao = services.criar_solicitacao_desligamento(
solicitante=self.solicitante,
funcionario=self.funcionario,
motivo="Teste de criação",
data_prevista_desligamento=date.today(),
)
self._add_step("Service 'criar_solicitacao_desligamento' executado.")
self.assertIsNotNone(solicitacao)
self.assertEqual(Solicitacao.objects.count(), 1)
self.assertEqual(Desligamento.objects.count(), 1)
self._add_step("Objetos Solicitacao e Desligamento criados no DB.")
self.assertEqual(solicitacao.status, StatusSolicitacao.RASCUNHO)
self.assertEqual(solicitacao.tipo, "DESLIGAMENTO")
self.assertEqual(solicitacao.desligamento.motivo, "Teste de criação")
self._add_step("Validação dos atributos e status inicial da solicitação.")
def test_fluxo_aprovacao_completo_sucesso(self):
"""
Simula o fluxo completo de aprovação de um desligamento,
passando por todas as etapas até a finalização.
"""
# Etapa 1: Criação e Envio (gestor envia → AGUARDANDO_HEAD)
solicitacao = services.criar_solicitacao_desligamento(
solicitante=self.solicitante, funcionario=self.funcionario,
motivo="Fluxo completo", data_prevista_desligamento=date.today()
)
services.enviar_solicitacao(solicitacao, usuario=self.solicitante)
solicitacao.refresh_from_db()
self.assertEqual(solicitacao.status, StatusSolicitacao.AGUARDANDO_HEAD)
self.assertIsNone(solicitacao.enviada_em)
self._add_step("Solicitação criada e enviada (status: AGUARDANDO_HEAD).")
# Etapa 1b: Aprovação Head (libera para ENVIADA)
services.aprovar_reprovar_por_head(
solicitacao, self.aprovador_head, DecisaoAprovacao.APROVADO, "OK Head"
)
solicitacao.refresh_from_db()
self.assertEqual(solicitacao.status, StatusSolicitacao.ENVIADA)
self.assertIsNotNone(solicitacao.enviada_em)
self._add_step("Aprovação pelo Head (status: ENVIADA).")
# Etapa 2: Aprovação GG
services.aprovar_reprovar_solicitacao(
solicitacao, self.aprovador_gg, DecisaoAprovacao.APROVADO, "OK GG"
)
solicitacao.refresh_from_db()
self.assertEqual(solicitacao.status, StatusSolicitacao.APROVADA_GG)
self.assertTrue(Aprovacao.objects.filter(solicitacao=solicitacao, etapa=EtapaAprovacao.GG).exists())
self._add_step("Aprovação por Gente & Gestão (status: APROVADA_GG).")
# Etapa 3: Aprovação Controladoria
services.aprovar_reprovar_solicitacao(
solicitacao, self.aprovador_controladoria, DecisaoAprovacao.APROVADO, "OK Controladoria"
)
solicitacao.refresh_from_db()
self.assertEqual(solicitacao.status, StatusSolicitacao.APROVADA_CONTROLADORIA)
self._add_step("Aprovação pela Controladoria (status: APROVADA_CONTROLADORIA).")
# Etapa 4: Aprovação Diretoria (Finalização)
services.aprovar_reprovar_solicitacao(
solicitacao, self.aprovador_diretoria, DecisaoAprovacao.APROVADO, "OK Diretoria"
)
solicitacao.refresh_from_db()
self.assertEqual(solicitacao.status, StatusSolicitacao.FINALIZADA)
self.assertIsNotNone(solicitacao.finalizada_em)
self._add_step("Aprovação pela Diretoria, finalizando a solicitação (status: FINALIZADA).")
def test_fluxo_reprovacao_sucesso(self):
"""
Garante que a reprovação em qualquer etapa move a solicitação
para o status 'REPROVADA' e a finaliza.
"""
solicitacao = services.criar_solicitacao_desligamento(
solicitante=self.solicitante, funcionario=self.funcionario,
motivo="Teste de reprovação", data_prevista_desligamento=date.today()
)
services.enviar_solicitacao(solicitacao, usuario=self.solicitante)
self._add_step("Solicitação criada e enviada (AGUARDANDO_HEAD).")
# Reprovação na primeira etapa (Head)
services.aprovar_reprovar_por_head(
solicitacao, self.aprovador_head, DecisaoAprovacao.REPROVADO, "Reprovado pelo Head"
)
solicitacao.refresh_from_db()
self._add_step("Service de reprovação executado pelo Head.")
self.assertEqual(solicitacao.status, StatusSolicitacao.REPROVADA)
self.assertIsNotNone(solicitacao.finalizada_em)
self.assertTrue(Aprovacao.objects.filter(solicitacao=solicitacao, decisao=DecisaoAprovacao.REPROVADO).exists())
self._add_step("Status da solicitação e data de finalização validados como REPROVADA.")
# --- Testes de Validação e Permissão (Caminho Infeliz) ---
def test_criar_solicitacao_duplicada_falha(self):
"""
Verifica se o service levanta ValidacaoError ao tentar criar uma
solicitação para um funcionário que possui uma em andamento.
"""
# Cria a primeira solicitação (válida)
services.criar_solicitacao_desligamento(
solicitante=self.solicitante, funcionario=self.funcionario,
motivo="Primeira solicitação", data_prevista_desligamento=date.today()
)
self._add_step("Primeira solicitação criada com sucesso.")
# Tenta criar a segunda (inválida)
with self.assertRaises(services.ValidacaoError) as ctx:
services.criar_solicitacao_desligamento(
solicitante=self.solicitante, funcionario=self.funcionario,
motivo="Segunda solicitação (inválida)", data_prevista_desligamento=date.today()
)
self.assertIn("Já existe uma solicitação em andamento", str(ctx.exception))
self.assertEqual(Solicitacao.objects.count(), 1)
self._add_step("Tentativa de criar solicitação duplicada levantou ValidacaoError como esperado.")
def test_enviar_por_nao_solicitante_falha(self):
"""
Garante que PermissaoError é levantada se um usuário que não é
o solicitante original tenta enviar a solicitação.
"""
solicitacao = services.criar_solicitacao_desligamento(
solicitante=self.solicitante, funcionario=self.funcionario,
motivo="Teste de permissão", data_prevista_desligamento=date.today()
)
self._add_step("Solicitação criada pelo solicitante original.")
with self.assertRaises(services.PermissaoError):
# GG tenta enviar uma solicitação que não é dele
services.enviar_solicitacao(solicitacao, usuario=self.aprovador_gg)
self._add_step("Tentativa de envio por outro usuário levantou PermissaoError.")
def test_aprovar_com_perfil_errado_falha(self):
"""
Verifica se PermissaoError é levantada quando um usuário com perfil
inadequado tenta aprovar a etapa do Head.
"""
solicitacao = services.criar_solicitacao_desligamento(
solicitante=self.solicitante, funcionario=self.funcionario,
motivo="Teste de perfil", data_prevista_desligamento=date.today()
)
services.enviar_solicitacao(solicitacao, usuario=self.solicitante)
self._add_step("Solicitação criada e enviada, aguardando aprovação do Head.")
# O usuário da Controladoria tenta aprovar a etapa do Head
with self.assertRaises(services.PermissaoError) as ctx:
services.aprovar_reprovar_por_head(
solicitacao, self.aprovador_controladoria, DecisaoAprovacao.APROVADO, "Aprovação indevida"
)
self.assertIn("Head", str(ctx.exception))
solicitacao.refresh_from_db()
self.assertEqual(solicitacao.status, StatusSolicitacao.AGUARDANDO_HEAD)
self._add_step("Tentativa de aprovação com perfil incorreto levantou PermissaoError.")
# --- Teste de Atomicidade ---
def test_criacao_atomica_falha_nao_persiste_dados(self):
"""
Garante que, se a criação do objeto 'Desligamento' falhar,
a 'Solicitacao' correspondente também não será criada (rollback).
"""
self._add_step("Iniciando teste de transação atômica.")
# Forçamos um erro passando um valor inválido para um campo DateField,
# o que causará um erro ANTES do .save() ser chamado no service.
# Uma abordagem mais robusta seria mockar o .create() para levantar uma exceção.
with self.assertRaises(Exception): # Captura genérica (pode ser ValueError, TypeError, etc)
services.criar_solicitacao_desligamento(
solicitante=self.solicitante,
funcionario=self.funcionario,
motivo="Teste de falha atômica",
data_prevista_desligamento="DATA_INVALIDA", # Isso causará um erro
)
self._add_step("Execução do service com dados inválidos levantou uma exceção.")
# A asserção mais importante: nada deve ter sido criado no banco de dados.
self.assertEqual(Solicitacao.objects.count(), 0)
self.assertEqual(Desligamento.objects.count(), 0)
self._add_step("Confirmado que nenhum registro foi persistido no banco de dados devido ao rollback.")