311 lines
13 KiB
Python
311 lines
13 KiB
Python
# /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 já 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.") |