# /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.")