From 1dfbe0d2e7c5860f04b6a1148570b7b4d9286251 Mon Sep 17 00:00:00 2001 From: Caue Felipe Trovatto Tragante Date: Sun, 2 Nov 2025 15:20:23 -0300 Subject: [PATCH] update: Vercel --- administrador/tests.py | 37 ++++ core/forms.py | 11 + core/templatetags/core_tags.py | 4 +- core/tests.py | 190 ++++++++++++++++++ core/tests_integracao_dinamica.py | 89 +++++++- .../templates/profissional_saude/tv2.html | 4 +- profissional_saude/views.py | 4 +- .../recepcionista/cadastrar_paciente.html | 1 + start.sh | 3 - 9 files changed, 333 insertions(+), 10 deletions(-) delete mode 100644 start.sh diff --git a/administrador/tests.py b/administrador/tests.py index 259ab8d..6899b5c 100644 --- a/administrador/tests.py +++ b/administrador/tests.py @@ -306,3 +306,40 @@ def test_data_integrity_multiple_registrations(self): # Verificar que todos foram criados self.assertEqual(CustomUser.objects.filter(last_name="Teste").count(), 3) + + def test_editar_funcionario(self): + self.client.login(cpf="11122233344", password="adminpass") + url = reverse("administrador:editar_funcionario", args=[self.func.pk]) + data = { + "cpf": "12345678909", + "username": "12345678909", + "first_name": "Edited", + "last_name": "Funcionario", + "email": "edited@func.com", + "funcao": "guiche", + "password1": "newpass123", + "password2": "newpass123", + } + resp = self.client.post(url, data, follow=True) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Funcionário atualizado com sucesso") + self.func.refresh_from_db() + self.assertEqual(self.func.first_name, "Edited") + self.assertEqual(self.func.funcao, "guiche") + + def test_editar_funcionario_invalido(self): + self.client.login(cpf="11122233344", password="adminpass") + url = reverse("administrador:editar_funcionario", args=[self.func.pk]) + data = { + "cpf": "12345678909", + "username": "12345678909", + "first_name": "Edited", + "last_name": "Funcionario", + "email": "edited@func.com", + "funcao": "guiche", + "password1": "newpass123", + "password2": "differentpass", # Senhas diferentes + } + resp = self.client.post(url, data) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Erro ao atualizar o funcionário") diff --git a/core/forms.py b/core/forms.py index cfee332..cdfbe9a 100644 --- a/core/forms.py +++ b/core/forms.py @@ -60,6 +60,16 @@ def clean_nome_completo(self): raise forms.ValidationError("Entrada inválida: scripts não são permitidos.") return nome_completo + def clean_cartao_sus(self): + cartao_sus = self.cleaned_data.get("cartao_sus") + if cartao_sus: + # Verifica se já existe um paciente com este cartão SUS + if Paciente.objects.filter(cartao_sus=cartao_sus).exists(): + raise forms.ValidationError( + "Já existe um paciente cadastrado com este cartão SUS." + ) + return cartao_sus + class Meta: model = Paciente fields = [ @@ -95,6 +105,7 @@ class Meta: class CadastrarFuncionarioForm(UserCreationForm): import re + @staticmethod def validate_cpf(value): # Remove caracteres não numéricos digits = re.sub(r"\D", "", value) diff --git a/core/templatetags/core_tags.py b/core/templatetags/core_tags.py index 24c1ad4..48fb23b 100644 --- a/core/templatetags/core_tags.py +++ b/core/templatetags/core_tags.py @@ -13,14 +13,14 @@ def get_proporcao_field(form, field_name): if value is None or value == "": # Define o atributo 'value' para "1" return field.as_widget( - attrs={"class": field.widget.attrs.get("class", ""), "value": "1"} + attrs={"class": field.field.widget.attrs.get("class", ""), "value": "1"} ) else: return field except: # Em caso de erro, também retorna o campo com value="1" return field.as_widget( - attrs={"class": field.widget.attrs.get("class", ""), "value": "1"} + attrs={"class": field.field.widget.attrs.get("class", ""), "value": "1"} ) diff --git a/core/tests.py b/core/tests.py index 4a6125c..fda3c4c 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,5 +1,6 @@ from django.test import Client, TestCase from django.urls import reverse +from django.http import HttpResponse from django.utils import timezone from unittest.mock import patch @@ -1165,6 +1166,49 @@ def test_username_field_hidden(self): user = form.save() self.assertEqual(user.username, user.cpf) + def test_cpf_validation_digit2_ten_becomes_zero(self): + """Testa CPF onde segundo dígito verificador seria 10, vira 0.""" + # CPF 10000002810 faz digit2 = 10 -> 0, mas vamos alterar último dígito para falhar + cpf_with_digit2_ten = "10000002811" # Último dígito alterado para falhar + data = self.valid_data.copy() + data["cpf"] = cpf_with_digit2_ten + data["username"] = cpf_with_digit2_ten + form = CadastrarFuncionarioForm(data=data) + self.assertFalse(form.is_valid()) + self.assertIn("cpf", form.errors) + + def test_cpf_validation_second_digit_check_fails(self): + """Testa CPF que passa primeira verificação mas falha na segunda.""" + # CPF válido 52998224725, alterando último dígito + cpf_second_digit_wrong = "52998224726" + data = self.valid_data.copy() + data["cpf"] = cpf_second_digit_wrong + data["username"] = cpf_second_digit_wrong + form = CadastrarFuncionarioForm(data=data) + self.assertFalse(form.is_valid()) + self.assertIn("cpf", form.errors) + + def test_cpf_validation_digit1_ten_becomes_zero(self): + """Testa CPF onde primeiro dígito verificador seria 10, vira 0.""" + # CPF 10000000108 faz digit1 = 10 -> 0, mas vamos alterar penúltimo dígito para falhar + cpf_with_digit1_ten = "10000000118" # Penúltimo dígito alterado + data = self.valid_data.copy() + data["cpf"] = cpf_with_digit1_ten + data["username"] = cpf_with_digit1_ten + form = CadastrarFuncionarioForm(data=data) + self.assertFalse(form.is_valid()) + self.assertIn("cpf", form.errors) + + def test_cpf_validation_digit1_ten_valid_cpf(self): + """Testa CPF válido onde primeiro dígito verificador é 10 (vira 0).""" + # CPF 10000000108: primeiro dígito calculado é 10 -> 0, segundo é 8 + cpf_valid_digit1_ten = "10000000108" + data = self.valid_data.copy() + data["cpf"] = cpf_valid_digit1_ten + data["username"] = cpf_valid_digit1_ten + form = CadastrarFuncionarioForm(data=data) + self.assertTrue(form.is_valid()) + class LoginFormTest(TestCase): """Testes abrangentes para LoginForm com foco em segurança.""" @@ -1430,11 +1474,65 @@ def test_login_redirect_based_on_role(self): response, reverse("profissional_saude:painel_profissional") ) + def test_login_view_post_form_invalid(self): + """Testa login com formulário inválido (CPF vazio).""" + response = self.client.post( + reverse("login"), + {"cpf": "", "password": "testpass"}, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Este campo é obrigatório") # Ou similar + self.assertFalse(response.context["user"].is_authenticated) + + def test_login_redirect_unknown_role(self): + """Testa redirecionamento para função desconhecida.""" + unknown_user = CustomUser.objects.create_user( + cpf="55566677788", + username="55566677788", + password="unknownpass", + funcao="desconhecida", # Função não reconhecida + ) + response = self.client.post( + reverse("login"), + {"cpf": "55566677788", "password": "unknownpass"}, + follow=True, + ) + self.assertRedirects(response, reverse("pagina_inicial")) + + def test_admin_access_registro_acesso(self): + """Testa acesso à página admin de RegistroDeAcesso para cobrir configuração.""" + admin_user = CustomUser.objects.create_user( + cpf="11122233344", + username="11122233344", + password="adminpass", + funcao="administrador", + is_staff=True, + is_superuser=True, + ) + self.client.login(cpf="11122233344", password="adminpass") + response = self.client.get("/admin/core/registrodeacesso/") + self.assertEqual(response.status_code, 200) + def test_logout_view(self): self.client.login(cpf="00011122233", password="testpass") response = self.client.get(reverse("logout"), follow=True) self.assertEqual(response.status_code, 200) + def test_login_creates_registro_acesso(self): + """Testa se login cria RegistroDeAcesso via sinal.""" + from core.models import RegistroDeAcesso + + initial_count = RegistroDeAcesso.objects.count() + response = self.client.post( + reverse("login"), + {"cpf": "00011122233", "password": "testpass"}, + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(RegistroDeAcesso.objects.count(), initial_count + 1) + registro = RegistroDeAcesso.objects.last() + self.assertEqual(registro.tipo_de_acesso, "login") + def test_pagina_inicial_requires_login(self): response = self.client.get(reverse("pagina_inicial")) self.assertEqual(response.status_code, 302) # Redirect to login @@ -1541,3 +1639,95 @@ def test_enviar_whatsapp_erro_api(self, mock_client): self.assertFalse(resultado) mock_client.assert_called_once_with("test_sid", "test_token") + + +class DecoratorTest(TestCase): + """Testes para os decorators de permissões.""" + + def setUp(self): + self.client = Client() + # Cria usuário recepcionista (não administrador) + self.user = CustomUser.objects.create_user( + cpf="11122233344", + username="11122233344", + password="testpass123", + first_name="Maria", + last_name="Santos", + email="maria.santos@test.com", + funcao="recepcionista", + ) + + def test_admin_required_redirects_non_admin(self): + """Testa que admin_required redireciona usuário não administrador.""" + from core.decorators import admin_required + from django.http import HttpRequest + + # Cria uma view mock + def mock_admin_view(request): + return HttpResponse("Acesso permitido") + + # Decora a view + decorated_view = admin_required(mock_admin_view) + + # Cria request mock com usuário não admin + request = HttpRequest() + request.user = self.user + + # Chama a view decorada + response = decorated_view(request) + + # Deve redirecionar para pagina_inicial + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("pagina_inicial")) + + +class TemplateTagsTest(TestCase): + """Testes para template tags em core.templatetags.core_tags.""" + + def setUp(self): + from guiche.forms import GuicheForm + + # Cria um formulário GuicheForm que tem os campos proporcao_* + self.form = GuicheForm() + + def test_get_proporcao_field_with_empty_value(self): + """Testa get_proporcao_field quando o valor está vazio.""" + from core.templatetags.core_tags import get_proporcao_field + from guiche.forms import GuicheForm + + # Modifica o form para simular um campo vazio + # Como o form é dinâmico, vamos criar um form com dados que façam value() retornar vazio + form_data = {"proporcao_g": ""} # Campo vazio + form = GuicheForm(data=form_data) + + result = get_proporcao_field(form, "tipo_senha_g") + + # Deve retornar o widget com value="1" porque o valor está vazio + self.assertIn('value="1"', str(result)) + + def test_get_proporcao_field_with_value(self): + """Testa get_proporcao_field quando o valor não está vazio.""" + from core.templatetags.core_tags import get_proporcao_field + from guiche.forms import GuicheForm + + # Campo com valor + form_data = {"proporcao_g": "5"} + form = GuicheForm(data=form_data) + + result = get_proporcao_field(form, "tipo_senha_g") + + # Deve retornar o campo original (não modificado) + self.assertEqual(result, form["proporcao_g"]) + + def test_add_class_filter(self): + """Testa o filtro add_class.""" + from core.templatetags.core_tags import add_class + from guiche.forms import GuicheForm + + form = GuicheForm() + field = form["proporcao_g"] + + result = add_class(field, "my-custom-class") + + # Deve conter a classe CSS adicionada + self.assertIn('class="my-custom-class"', str(result)) diff --git a/core/tests_integracao_dinamica.py b/core/tests_integracao_dinamica.py index 6dae798..14d30dd 100644 --- a/core/tests_integracao_dinamica.py +++ b/core/tests_integracao_dinamica.py @@ -161,7 +161,7 @@ def test_fluxo_profissional_saude_acessa_painel(self): self.assertTemplateUsed(response, "profissional_saude/painel_profissional.html") def test_fluxo_completo_com_whatsapp(self): - """Testa fluxo completo incluindo notificação WhatsApp.""" + """Testa fluxo completo incluindo notificação WhatsApp com dados válidos.""" # 1. Admin cria recepcionista e profissional de saúde recepcionista = self.criar_usuario_direto("recepcionista") @@ -195,6 +195,56 @@ def test_fluxo_completo_com_whatsapp(self): f"Paciente não foi criado. Status: {response.status_code}. Content: {response.content.decode()}" ) + def test_fluxo_completo_com_whatsapp_falha(self): + """Testa fluxo com WhatsApp quando cadastro falha para cobrir bloco except.""" + # 1. Admin cria recepcionista e profissional de saúde + recepcionista = self.criar_usuario_direto("recepcionista", "88888888888") + profissional = self.criar_usuario_direto("profissional_saude", "99999999999") + + # 2. Recepcionista tenta cadastrar paciente com dados que causam erro + client_recep = Client() + client_recep.login(cpf=recepcionista.cpf, password="recep123") + + # Primeiro cadastra um paciente válido + paciente_data_valido = { + "nome_completo": "Paciente Original", + "cartao_sus": "888888888888888", + "telefone_celular": "(11) 97777-7777", + "horario_agendamento": timezone.now().strftime("%Y-%m-%dT%H:%M"), + "profissional_saude": profissional.id, + "tipo_senha": "G", + } + + client_recep.post( + reverse("recepcionista:cadastrar_paciente"), data=paciente_data_valido + ) + + # Agora tenta cadastrar com mesmo cartão SUS (deve falhar) + paciente_data_invalido = { + "nome_completo": "Paciente Duplicado", + "cartao_sus": "888888888888888", # Mesmo cartão SUS + "telefone_celular": "(11) 96666-6666", + "horario_agendamento": timezone.now().strftime("%Y-%m-%dT%H:%M"), + "profissional_saude": profissional.id, + "tipo_senha": "P", + } + + response = client_recep.post( + reverse("recepcionista:cadastrar_paciente"), data=paciente_data_invalido + ) + + # Verifica se o POST falhou + try: + paciente = Paciente.objects.get( + cartao_sus="888888888888888", nome_completo="Paciente Duplicado" + ) + self.fail("Paciente duplicado não deveria ter sido criado") + except Paciente.DoesNotExist: + # Se paciente não foi criado, verifica se há mensagens de erro na resposta + self.assertContains( + response, "Já existe" + ) # Deve conter mensagem de erro de duplicata + def test_fluxo_completo_dinamico_cadastro_chamada_consulta(self): """Testa o fluxo completo dinâmico: cadastro -> guichê -> profissional -> consulta.""" # 1. CRIAR USUÁRIOS @@ -553,3 +603,40 @@ def test_autorizacao_acesso_por_funcao(self): [302, 403], f"{user_type} não deveria acessar {url}", ) + + def test_cadastro_paciente_com_dados_invalidos(self): + """Testa tentativa de cadastro de paciente com dados inválidos para cobrir bloco de erro.""" + # Criar recepcionista + recepcionista = self.criar_usuario_direto("recepcionista") + profissional = self.criar_usuario_direto("profissional_saude") + + # Recepcionista faz login + client = Client() + client.login(cpf=recepcionista.cpf, password="recep123") + + # Tentar cadastrar paciente com dados inválidos (telefone inválido) + paciente_data_invalido = { + "nome_completo": "Paciente Inválido", + "cartao_sus": "123456789012345", + "telefone_celular": "1199999999", # Inválido - 10 dígitos + "horario_agendamento": timezone.now().strftime("%Y-%m-%dT%H:%M"), + "profissional_saude": profissional.id, + "tipo_senha": "G", + } + + response = client.post( + reverse("recepcionista:cadastrar_paciente"), data=paciente_data_invalido + ) + + # Verifica se o POST retornou erro (status 200 com form inválido) + self.assertEqual(response.status_code, 200) + + # Verifica se paciente NÃO foi criado devido aos dados inválidos + try: + paciente = Paciente.objects.get(cartao_sus="123456789012345") + self.fail("Paciente não deveria ter sido criado com dados inválidos") + except Paciente.DoesNotExist: + # Se paciente não foi criado, verifica se há mensagens de erro na resposta + self.assertContains( + response, "celular válido" + ) # Deve conter mensagem de erro diff --git a/profissional_saude/templates/profissional_saude/tv2.html b/profissional_saude/templates/profissional_saude/tv2.html index 4c14cae..3a38876 100644 --- a/profissional_saude/templates/profissional_saude/tv2.html +++ b/profissional_saude/templates/profissional_saude/tv2.html @@ -157,7 +157,7 @@ {% if senha_chamada %}

- {{ nome_completo }} - Sala: {{ sala_consulta }} + {{ nome_completo }} - Profissional: {{ profissional_nome }}

Profissional: {{ profissional_nome }}

@@ -214,7 +214,7 @@

Histórico de Chamadas

if (data.nome_completo && data.sala_consulta && data.profissional_nome && data.id !== ultimaChamadaId) { // Atualiza a tela - var nomeSala = data.nome_completo + " - Sala: " + data.sala_consulta; + var nomeSala = data.nome_completo + " - Profissional: " + data.profissional_nome; $('#senha-chamada-texto').text(nomeSala); $('#senha-chamada-texto').data('nome', data.nome_completo); // Atualiza o data-nome $('#senha-chamada-texto').data('sala', data.sala_consulta); // Atualiza o data-sala diff --git a/profissional_saude/views.py b/profissional_saude/views.py index e308cb1..5036e63 100644 --- a/profissional_saude/views.py +++ b/profissional_saude/views.py @@ -138,7 +138,7 @@ def tv2_view(request): sala_consulta = ( ultima_chamada.profissional_saude.sala ) # Acessa a sala através do profissional - profissional_nome = ultima_chamada.profissional_saude.first_name + profissional_nome = ultima_chamada.profissional_saude.get_full_name() # Pega as 5 chamadas mais recentes, excluindo a última chamada historico_chamadas = ( @@ -181,7 +181,7 @@ def tv2_api_view(request): sala_consulta = ( ultima_chamada.profissional_saude.sala ) # Acessa a sala através do profissional - profissional_nome = ultima_chamada.profissional_saude.first_name + profissional_nome = ultima_chamada.profissional_saude.get_full_name() chamada_id = ultima_chamada.id data = { diff --git a/recepcionista/templates/recepcionista/cadastrar_paciente.html b/recepcionista/templates/recepcionista/cadastrar_paciente.html index cbb4552..be08355 100644 --- a/recepcionista/templates/recepcionista/cadastrar_paciente.html +++ b/recepcionista/templates/recepcionista/cadastrar_paciente.html @@ -14,6 +14,7 @@

Cadastrar Paciente

{{ form.cartao_sus.label_tag }} {{ form.cartao_sus }} + {{ form.cartao_sus.errors }}
{{ form.horario_agendamento.label_tag }} {{ form.horario_agendamento }} diff --git a/start.sh b/start.sh deleted file mode 100644 index 6291665..0000000 --- a/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -python manage.py migrate -gunicorn sga.wsgi:application --bind 0.0.0.0:8080 \ No newline at end of file