# /SGMP_PROD/solicitacoes/tests/test_desligamento_services.py import json from datetime import date, timedelta from pathlib import Path from unittest import TestCase as AssertionsMixin from unittest.mock import patch from django.test import TestCase from django.db import IntegrityError from ..models import ( PessoaRM, UsuarioSistema, Solicitacao, Desligamento, Aprovacao, Parecer, StatusSolicitacao, DecisaoAprovacao, EtapaAprovacao, TipoDesligamento, TipoAvisoPrevio, ) 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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.") @patch("solicitacoes.services.verificar_estabilidades_colaborador", return_value=[]) def test_diretoria_cria_direto_enviada_e_enviar_idempotente(self, _mock_estabilidades): """ Diretoria cria em ENVIADA com enviada_em; enviar_solicitacao é no-op. """ diretoria = UsuarioSistema.objects.create( matricula="405", nome="Diretoria Criadora", perfil=UsuarioSistema.Perfil.DIRETORIA ) solicitacao = services.criar_solicitacao_desligamento( solicitante=diretoria, funcionario=self.funcionario, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, motivo="Diretoria cria direto", data_prevista_desligamento=date.today(), ) self.assertEqual(solicitacao.status, StatusSolicitacao.ENVIADA) self.assertIsNotNone(solicitacao.enviada_em) services.enviar_solicitacao(solicitacao, usuario=diretoria) solicitacao.refresh_from_db() self.assertEqual(solicitacao.status, StatusSolicitacao.ENVIADA) 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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: Parecer GG (status permanece ENVIADA até ambos os pareceres) services.registrar_parecer(solicitacao, self.aprovador_gg, "OK GG") solicitacao.refresh_from_db() self.assertEqual(solicitacao.status, StatusSolicitacao.ENVIADA) self.assertTrue( Parecer.objects.filter(solicitacao=solicitacao, etapa=EtapaAprovacao.GG).exists() ) self._add_step("Parecer GG registrado (status: ENVIADA).") # Etapa 3: Parecer Controladoria → AGUARDANDO_DIRETORIA services.registrar_parecer(solicitacao, self.aprovador_controladoria, "OK Controladoria") solicitacao.refresh_from_db() self.assertEqual(solicitacao.status, StatusSolicitacao.AGUARDANDO_DIRETORIA) self.assertTrue( Parecer.objects.filter(solicitacao=solicitacao, etapa=EtapaAprovacao.CONTROLADORIA).exists() ) self._add_step("Parecer Controladoria (status: AGUARDANDO_DIRETORIA).") # 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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, tipo_desligamento=TipoDesligamento.OUTROS, aviso_previo=TipoAvisoPrevio.TRABALHADO, 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.")