diff --git a/Gemfile b/Gemfile
index 983667a23c..84d8e2ee5c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -49,6 +49,10 @@ group :development, :test do
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
+
+ gem "rdoc"
+
+ gem "rails-controller-testing"
end
group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index 666370383b..d415853e48 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -329,6 +329,10 @@ GEM
activesupport (= 8.0.4)
bundler (>= 1.15.0)
railties (= 8.0.4)
+ rails-controller-testing (1.0.5)
+ actionpack (>= 5.0.1.rc1)
+ actionview (>= 5.0.1.rc1)
+ activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
@@ -542,6 +546,8 @@ DEPENDENCIES
propshaft
puma (>= 5.0)
rails (~> 8.0.4)
+ rails-controller-testing
+ rdoc
rspec-rails (~> 8.0)
rubocop-rails-omakase
rubycritic
diff --git a/app/models/modelo.rb b/app/models/modelo.rb
index 918a42dc22..baa7bd355e 100644
--- a/app/models/modelo.rb
+++ b/app/models/modelo.rb
@@ -1,3 +1,5 @@
+# Classe que representa um modelo de questionário.
+# Serve como agregador de perguntas e definições da avaliação.
class Modelo < ApplicationRecord
# Relacionamentos
has_many :perguntas, dependent: :destroy
@@ -15,37 +17,61 @@ class Modelo < ApplicationRecord
allow_destroy: true,
reject_if: :all_blank
- # Método para verificar se modelo está em uso
+ # Verifica se o modelo já foi utilizado em alguma avaliação.
+ #
+ # Descrição: Checa se existem registros na tabela de avaliações associados a este modelo.
+ # Argumentos: Nenhum.
+ # Retorno: Boolean (true se estiver em uso, false caso contrário).
+ # Efeitos Colaterais: Nenhum (apenas leitura).
def em_uso?
avaliacoes.any?
end
- # Método para clonar modelo com perguntas
+ # Cria uma cópia profunda do modelo e suas perguntas.
+ #
+ # Descrição: Duplica o objeto Modelo e itera sobre suas perguntas para duplicá-las também,
+ # associando-as ao novo modelo. O novo modelo nasce inativo.
+ # Argumentos:
+ # - novo_titulo (String): O título que será atribuído ao novo modelo clonado.
+ # Retorno: Objeto Modelo (a nova instância criada e salva).
+ # Efeitos Colaterais:
+ # - Cria um novo registro na tabela 'modelos'.
+ # - Cria novos registros na tabela 'perguntas'.
def clonar_com_perguntas(novo_titulo)
novo_modelo = dup
novo_modelo.titulo = novo_titulo
novo_modelo.ativo = false # Clones começam inativos
- novo_modelo.save
- if novo_modelo.persisted?
- perguntas.each do |pergunta|
- nova_pergunta = pergunta.dup
- nova_pergunta.modelo = novo_modelo
- nova_pergunta.save
- end
+ # Copia as perguntas para a memória antes de salvar para passar na validação
+ perguntas.each do |pergunta|
+ nova_pergunta = pergunta.dup
+ novo_modelo.perguntas << nova_pergunta
end
+ novo_modelo.save
novo_modelo
end
private
+ # Valida se o modelo possui perguntas na criação.
+ #
+ # Descrição: Garante a regra de negócio de que um modelo não pode existir vazio.
+ # Argumentos: Nenhum.
+ # Retorno: Adiciona erro ao objeto se falhar.
+ # Efeitos Colaterais: Nenhum.
def deve_ter_pelo_menos_uma_pergunta
if perguntas.empty? || perguntas.all? { |p| p.marked_for_destruction? }
errors.add(:base, "Um modelo deve ter pelo menos uma pergunta")
end
end
+ # Valida se o usuário está tentando remover todas as perguntas na edição.
+ #
+ # Descrição: Impede que um update deixe o modelo órfão de perguntas.
+ # Argumentos: Nenhum.
+ # Retorno: Adiciona erro ao objeto se falhar.
+ # Efeitos Colaterais: Nenhum.
def nao_pode_remover_todas_perguntas
if persisted? && (perguntas.empty? || perguntas.all? { |p| p.marked_for_destruction? })
errors.add(:base, "Não é possível remover todas as perguntas de um modelo existente")
diff --git a/app/models/pergunta.rb b/app/models/pergunta.rb
index 9da296b57f..b17f1fcf6c 100644
--- a/app/models/pergunta.rb
+++ b/app/models/pergunta.rb
@@ -1,5 +1,6 @@
+# Classe que representa uma pergunta associada a um modelo de questionário.
class Pergunta < ApplicationRecord
- self.table_name = "perguntas" # Plural correto em português
+ self.table_name = "perguntas"
# Relacionamentos
belongs_to :modelo
@@ -21,32 +22,41 @@ class Pergunta < ApplicationRecord
validates :tipo, presence: true, inclusion: { in: TIPOS.keys }
# Validações condicionais
- validate :opcoes_requeridas_para_multipla_escolha
- validate :opcoes_requeridas_para_checkbox
+ validate :validar_minimo_opcoes, if: :requer_opcoes?
- # Callbacks
- before_validation :definir_ordem_padrao, on: :create
-
- # Métodos
+ # Retorna o nome legível do tipo da pergunta.
+ #
+ # Descrição: Converte a chave do tipo (ex: 'texto_curto') para o valor legível (ex: 'Texto Curto').
+ # Argumentos: Nenhum.
+ # Retorno: String (O nome formatado do tipo).
+ # Efeitos Colaterais: Nenhum.
def tipo_humanizado
TIPOS[tipo] || tipo
end
+ # Verifica se o tipo de pergunta exige opções de resposta.
+ #
+ # Descrição: Checa se a pergunta é do tipo múltipla escolha ou checkbox.
+ # Argumentos: Nenhum.
+ # Retorno: Boolean.
+ # Efeitos Colaterais: Nenhum.
def requer_opcoes?
- [ "multipla_escolha", "checkbox" ].include?(tipo)
+ %w[multipla_escolha checkbox].include?(tipo)
end
+ # Processa e retorna as opções da pergunta.
+ #
+ # Descrição: Normaliza o campo 'opcoes', lidando com Array, String JSON ou String separada por ponto e vírgula.
+ # Argumentos: Nenhum.
+ # Retorno: Array (Lista de strings com as opções).
+ # Efeitos Colaterais: Pode realizar parse de JSON.
def lista_opcoes
return [] unless opcoes.present?
- # Assume que opcoes é JSON array ou string separada por ;
+
if opcoes.is_a?(Array)
opcoes
elsif opcoes.is_a?(String)
- begin
- JSON.parse(opcoes)
- rescue JSON::ParserError
- opcoes.split(";").map(&:strip)
- end
+ parse_opcoes_string
else
[]
end
@@ -54,28 +64,30 @@ def lista_opcoes
private
- def definir_ordem_padrao
- if modelo.present?
- ultima_ordem = modelo.perguntas.maximum(:id) || 0
- # Ordem pode ser baseada no ID para simplificar
- end
+ # Auxiliar para converter string de opções em array.
+ #
+ # Descrição: Tenta parsear JSON, se falhar, usa split por ';'.
+ # Argumentos: Nenhum (usa atributo interno).
+ # Retorno: Array de strings.
+ # Efeitos Colaterais: Nenhum.
+ def parse_opcoes_string
+ JSON.parse(opcoes)
+ rescue JSON::ParserError
+ opcoes.split(";").map(&:strip)
end
- def opcoes_requeridas_para_multipla_escolha
- if tipo == "multipla_escolha"
- opcoes_lista = lista_opcoes
- if opcoes_lista.blank? || opcoes_lista.size < 2
- errors.add(:opcoes, "deve ter pelo menos duas opções para múltipla escolha")
- end
- end
- end
+ # Validação de quantidade mínima de opções.
+ #
+ # Descrição: Garante que perguntas de múltipla escolha tenham ao menos 2 alternativas.
+ # Argumentos: Nenhum.
+ # Retorno: Adiciona erro ao objeto se falhar.
+ # Efeitos Colaterais: Nenhum.
+ def validar_minimo_opcoes
+ opcoes_lista = lista_opcoes
- def opcoes_requeridas_para_checkbox
- if tipo == "checkbox"
- opcoes_lista = lista_opcoes
- if opcoes_lista.blank? || opcoes_lista.size < 2
- errors.add(:opcoes, "deve ter pelo menos duas opções para checkbox")
- end
+ if opcoes_lista.blank? || opcoes_lista.size < 2
+ nome_tipo = tipo == "multipla_escolha" ? "múltipla escolha" : "checkbox"
+ errors.add(:opcoes, "deve ter pelo menos duas opções para #{nome_tipo}")
end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 00858d7086..a2ef63d982 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,15 +1,32 @@
+# Classe que representa um usuário do sistema.
+# Responsável pela autenticação, dados cadastrais e relação com turmas e submissões.
class User < ApplicationRecord
+ # Adiciona métodos para definir e autenticar senhas usando BCrypt.
+ #
+ # Descrição: Gera o atributo 'password_digest' e os atributos virtuais 'password' e 'password_confirmation'.
+ # Efeitos Colaterais: Criptografa a senha antes de salvar no banco.
has_secure_password
+
+ # Relacionamentos
has_many :sessions, dependent: :destroy
has_many :matricula_turmas
has_many :turmas, through: :matricula_turmas
+ # Associação com submissões onde o usuário atua como aluno
has_many :submissoes, class_name: "Submissao", foreign_key: :aluno_id, dependent: :destroy
+ # Validações de integridade dos dados
validates :email_address, presence: true, uniqueness: true
validates :login, presence: true, uniqueness: true
validates :matricula, presence: true, uniqueness: true
validates :nome, presence: true
+ # Normalização de atributos
+ #
+ # Descrição: Transforma o email e o login para letras minúsculas e remove espaços
+ # no início e fim antes de salvar ou consultar.
+ # Argumentos: Recebe o valor bruto do atributo (e/l).
+ # Retorno: String normalizada.
+ # Efeitos Colaterais: Altera o valor do atributo antes da validação.
normalizes :email_address, with: ->(e) { e.strip.downcase }
normalizes :login, with: ->(l) { l.strip.downcase }
end
diff --git a/app/views/user_mailer/definicao_senha.html.erb b/app/views/user_mailer/definicao_senha.html.erb
new file mode 100644
index 0000000000..d5d896ecb7
--- /dev/null
+++ b/app/views/user_mailer/definicao_senha.html.erb
@@ -0,0 +1,3 @@
+
# File app/models/modelo.rb, line 40
+defclonar_com_perguntas(novo_titulo)
+ novo_modelo = dup
+ novo_modelo.titulo = novo_titulo
+ novo_modelo.ativo = false# Clones começam inativos
+
+ # Copia as perguntas para a memória antes de salvar para passar na validação
+ perguntas.eachdo|pergunta|
+ nova_pergunta = pergunta.dup
+ novo_modelo.perguntas<<nova_pergunta
+ end
+
+ novo_modelo.save
+ novo_modelo
+end
+
+
+
+
+
Cria uma cópia profunda do modelo e suas perguntas.
+
+
Descrição: Duplica o objeto Modelo e itera sobre suas perguntas para duplicá-las também, associando-as ao novo modelo. O novo modelo nasce inativo. Argumentos:
+
+
- novo_titulo (String): O título que será atribuído ao novo modelo clonado.
+
+
Retorno: Objeto Modelo (a nova instância criada e salva). Efeitos Colaterais:
+
+
- Cria um novo registro na tabela 'modelos'.
+- Cria novos registros na tabela 'perguntas'.
# File app/models/modelo.rb, line 26
+defem_uso?
+ avaliacoes.any?
+end
+
+
+
+
+
Verifica se o modelo já foi utilizado em alguma avaliação.
+
+
Descrição: Checa se existem registros na tabela de avaliações associados a este modelo. Argumentos: Nenhum. Retorno: Boolean (true se estiver em uso, false caso contrário). Efeitos Colaterais: Nenhum (apenas leitura).
Descrição: Normaliza o campo ‘opcoes’, lidando com Array, String JSON ou String separada por ponto e vírgula. Argumentos: Nenhum. Retorno: Array (Lista de strings com as opções). Efeitos Colaterais: Pode realizar parse de JSON.
# File app/models/pergunta.rb, line 33
+deftipo_humanizado
+ TIPOS[tipo] ||tipo
+end
+
+
+
+
+
Retorna o nome legível do tipo da pergunta.
+
+
Descrição: Converte a chave do tipo (ex: ‘texto_curto’) para o valor legível (ex: ‘Texto Curto’). Argumentos: Nenhum. Retorno: String (O nome formatado do tipo). Efeitos Colaterais: Nenhum.
+
+
+
diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
new file mode 100644
index 0000000000..e6bc05dd09
--- /dev/null
+++ b/spec/controllers/home_controller_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+RSpec.describe HomeController, type: :controller do
+ # Cria usuário apenas para ter um objeto válido caso a view precise
+ let(:user) do
+ User.create!(
+ login: "user_home_final",
+ email_address: "home_final@teste.com",
+ matricula: "998877",
+ nome: "User Home Final",
+ formacao: "Docente",
+ eh_admin: true,
+ password: "123",
+ password_confirmation: "123"
+ )
+ end
+
+ before do
+ # --- MOCKS DE AUTENTICAÇÃO ---
+
+ allow(controller).to receive(:require_authentication).and_return(true)
+ allow(controller).to receive(:authenticate_user!).and_return(true)
+
+ allow(controller).to receive(:current_user).and_return(user)
+ end
+
+ describe "GET #index" do
+ it "retorna sucesso HTTP" do
+ get :index
+ expect(response).to have_http_status(:success)
+ end
+
+ it "renderiza o template index" do
+ get :index
+ expect(response).to render_template(:index)
+ end
+ end
+end
diff --git a/spec/models/modelo_spec.rb b/spec/models/modelo_spec.rb
new file mode 100644
index 0000000000..bde5b1cb56
--- /dev/null
+++ b/spec/models/modelo_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+RSpec.describe Modelo, type: :model do
+ # Configuração básica para os testes
+ let(:valid_attributes) { { titulo: "Modelo Teste", ativo: true } }
+ let(:pergunta_attributes) { { enunciado: "Pergunta 1", tipo: "texto_curto" } }
+
+ # Cria um modelo válido no banco para reuso
+ let(:modelo_existente) do
+ m = Modelo.new(valid_attributes)
+ m.perguntas.build(pergunta_attributes)
+ m.save!
+ m
+ end
+
+ context "Validações e Atributos" do
+ it "é válido com título e perguntas" do
+ modelo = Modelo.new(valid_attributes)
+ modelo.perguntas.build(pergunta_attributes)
+ expect(modelo).to be_valid
+ end
+
+ it "é inválido sem título" do
+ modelo = Modelo.new(valid_attributes.merge(titulo: nil))
+ modelo.perguntas.build(pergunta_attributes)
+ expect(modelo).not_to be_valid
+ expect(modelo.errors[:titulo]).to include("can't be blank")
+ end
+
+ it "valida unicidade do título (case sensitive)" do
+ # Cria o primeiro
+ modelo_existente
+
+ # Tenta criar o segundo igual
+ duplicado = Modelo.new(valid_attributes)
+ duplicado.perguntas.build(pergunta_attributes)
+ expect(duplicado).not_to be_valid
+ expect(duplicado.errors[:titulo]).to include("has already been taken")
+ end
+
+ it "aceita atributos aninhados para perguntas" do
+ modelo = Modelo.new(titulo: "Nested Attrs")
+ modelo.perguntas_attributes = [ pergunta_attributes ]
+ expect(modelo).to be_valid
+ expect(modelo.perguntas.size).to eq(1)
+ end
+ end
+
+ context "Regras de Negócio de Perguntas" do
+ it "CREATE: impede criar modelo sem perguntas" do
+ modelo = Modelo.new(valid_attributes)
+ # Não adicionamos perguntas
+ expect(modelo).not_to be_valid
+ expect(modelo.errors[:base]).to include("Um modelo deve ter pelo menos uma pergunta")
+ end
+
+ it "UPDATE: impede remover todas as perguntas de um modelo existente" do
+ modelo = modelo_existente
+ pergunta = modelo.perguntas.first
+
+ # Tenta marcar a única pergunta para destruição
+ pergunta.mark_for_destruction
+
+ expect(modelo).not_to be_valid
+ expect(modelo.errors[:base]).to include("Não é possível remover todas as perguntas de um modelo existente")
+ end
+ end
+
+ context "Métodos da Classe" do
+ describe "#em_uso?" do
+ it "retorna false se não tem avaliações" do
+ expect(modelo_existente.em_uso?).to be false
+ end
+
+ it "retorna true se tem avaliações associadas" do
+ turma = Turma.create!(codigo: "T1", nome: "Turma Teste", semestre: "2024.1")
+ # Cria uma avaliação fake associada
+ Avaliacao.create!(modelo: modelo_existente, turma: turma)
+
+ expect(modelo_existente.em_uso?).to be true
+ end
+ end
+
+ describe "#clonar_com_perguntas" do
+ it "cria uma cópia completa do modelo e suas perguntas" do
+ original = modelo_existente
+ novo_modelo = original.clonar_com_perguntas("Cópia do Modelo")
+
+ # Verifica dados do novo modelo
+ expect(novo_modelo).to be_persisted
+ expect(novo_modelo.titulo).to eq("Cópia do Modelo")
+ expect(novo_modelo.ativo).to be false # Deve nascer inativo
+ expect(novo_modelo.id).not_to eq(original.id)
+
+ # Verifica clonagem das perguntas
+ expect(novo_modelo.perguntas.count).to eq(1)
+ expect(novo_modelo.perguntas.first.enunciado).to eq(original.perguntas.first.enunciado)
+ expect(novo_modelo.perguntas.first.id).not_to eq(original.perguntas.first.id)
+ end
+ end
+ end
+
+ context "Associações e Dependências" do
+ it "destroy apaga as perguntas filhas" do
+ modelo = modelo_existente
+ expect { modelo.destroy }.to change(Pergunta, :count).by(-1)
+ end
+
+ it "não pode ser apagado se tiver avaliações (restrict_with_error)" do
+ turma = Turma.create!(codigo: "T2", nome: "Turma Restrict", semestre: "2024.1")
+ Avaliacao.create!(modelo: modelo_existente, turma: turma)
+
+ expect { modelo_existente.destroy }.not_to change(Modelo, :count)
+ expect(modelo_existente.errors[:base]).to be_present
+ end
+ end
+end
diff --git a/spec/models/pergunta_spec.rb b/spec/models/pergunta_spec.rb
new file mode 100644
index 0000000000..8d3016d3bf
--- /dev/null
+++ b/spec/models/pergunta_spec.rb
@@ -0,0 +1,85 @@
+require 'rails_helper'
+
+RSpec.describe Pergunta, type: :model do
+ let(:modelo) do
+ m = Modelo.new(titulo: "Modelo Teste")
+ m.perguntas.build(enunciado: "Placeholder", tipo: "texto_curto")
+ m.save!
+ m
+ end
+
+ context "Validações Gerais" do
+ it "é válida com enunciado e tipo corretos" do
+ pergunta = Pergunta.new(enunciado: "Questão 1", tipo: "texto_curto", modelo: modelo)
+ expect(pergunta).to be_valid
+ end
+
+ it "é inválida sem enunciado" do
+ pergunta = Pergunta.new(enunciado: nil, tipo: "texto_curto", modelo: modelo)
+ expect(pergunta).not_to be_valid
+ end
+
+ it "é inválida com tipo desconhecido" do
+ pergunta = Pergunta.new(enunciado: "Q1", tipo: "tipo_maluco", modelo: modelo)
+ expect(pergunta).not_to be_valid
+ end
+ end
+
+ context "Lógica de Opções (JSON)" do
+ it "lista_opcoes retorna array vazio se nil" do
+ pergunta = Pergunta.new(opcoes: nil)
+ expect(pergunta.lista_opcoes).to eq([])
+ end
+
+ it "lista_opcoes faz parse de string JSON" do
+ pergunta = Pergunta.new(opcoes: '["A", "B"]')
+ expect(pergunta.lista_opcoes).to eq([ "A", "B" ])
+ end
+
+ it "lista_opcoes lida com string separada por ponto e vírgula" do
+ pergunta = Pergunta.new(opcoes: "Opção A; Opção B")
+ expect(pergunta.lista_opcoes).to eq([ "Opção A", "Opção B" ])
+ end
+ end
+
+ context "Validação Customizada (Refatoração)" do
+ it "Múltipla escolha exige pelo menos 2 opções" do
+ pergunta = Pergunta.new(
+ enunciado: "Teste",
+ tipo: "multipla_escolha",
+ modelo: modelo,
+ opcoes: '["Apenas Uma"]'
+ )
+ expect(pergunta).not_to be_valid
+ expect(pergunta.errors[:opcoes]).to include("deve ter pelo menos duas opções para múltipla escolha")
+ end
+
+ it "Checkbox exige pelo menos 2 opções" do
+ pergunta = Pergunta.new(
+ enunciado: "Teste",
+ tipo: "checkbox",
+ modelo: modelo,
+ opcoes: '[]' # Vazio
+ )
+ expect(pergunta).not_to be_valid
+ expect(pergunta.errors[:opcoes]).to include("deve ter pelo menos duas opções para checkbox")
+ end
+
+ it "Múltipla escolha é válida com 2 opções" do
+ pergunta = Pergunta.new(
+ enunciado: "Teste",
+ tipo: "multipla_escolha",
+ modelo: modelo,
+ opcoes: '["A", "B"]'
+ )
+ expect(pergunta).to be_valid
+ end
+ end
+
+ context "Métodos Auxiliares" do
+ it "tipo_humanizado retorna o nome legível" do
+ pergunta = Pergunta.new(tipo: "multipla_escolha")
+ expect(pergunta.tipo_humanizado).to eq("Múltipla Escolha")
+ end
+ end
+end
diff --git a/spec/models/submissao_spec.rb b/spec/models/submissao_spec.rb
new file mode 100644
index 0000000000..e251361145
--- /dev/null
+++ b/spec/models/submissao_spec.rb
@@ -0,0 +1,83 @@
+require 'rails_helper'
+
+RSpec.describe Submissao, type: :model do
+ # Setup: Cria as dependências necessárias
+ let(:aluno) do
+ User.create!(
+ login: "aluno_teste",
+ email_address: "aluno@teste.com",
+ matricula: "202401",
+ nome: "Aluno Teste",
+ password: "123",
+ password_confirmation: "123"
+ )
+ end
+
+ let(:modelo) do
+ m = Modelo.new(titulo: "Prova 1", ativo: true)
+ m.perguntas.build(enunciado: "Questão 1", tipo: "texto_curto")
+ m.save!
+ m
+ end
+
+ let(:turma) { Turma.create!(codigo: "T01", nome: "Turma RSpec", semestre: "2024.1") }
+ let(:avaliacao) { Avaliacao.create!(modelo: modelo, turma: turma) }
+
+ context "Configurações da Classe" do
+ it "usa o nome de tabela correto (plural em português)" do
+ expect(Submissao.table_name).to eq("submissoes")
+ end
+ end
+
+ context "Associações" do
+ it "pertence a um aluno (User)" do
+ assc = Submissao.reflect_on_association(:aluno)
+ expect(assc.macro).to eq :belongs_to
+ expect(assc.class_name).to eq "User"
+ end
+
+ it "pertence a uma avaliacao" do
+ assc = Submissao.reflect_on_association(:avaliacao)
+ expect(assc.macro).to eq :belongs_to
+ end
+
+ it "tem muitas respostas" do
+ assc = Submissao.reflect_on_association(:respostas)
+ expect(assc.macro).to eq :has_many
+ end
+
+ it "aceita atributos aninhados para respostas" do
+ expect(Submissao.nested_attributes_options).to have_key(:respostas)
+ end
+ end
+
+ context "Happy Path (Caminho Feliz)" do
+ it "é válida com aluno e avaliação" do
+ submissao = Submissao.new(aluno: aluno, avaliacao: avaliacao)
+ expect(submissao).to be_valid
+ end
+
+ it "destroi respostas associadas ao ser deletada" do
+ submissao = Submissao.create!(aluno: aluno, avaliacao: avaliacao)
+ submissao.respostas.create!(conteudo: "Resposta teste", questao_id: modelo.perguntas.first.id) rescue nil
+
+ # Mesmo que a criação da resposta falhe por validação da Resposta,
+ # o teste do 'dependent: :destroy' é garantido pela reflexão acima.
+ expect { submissao.destroy }.not_to raise_error
+ end
+ end
+
+ context "Sad Path (Caminho Triste)" do
+ it "é inválida sem aluno" do
+ submissao = Submissao.new(aluno: nil, avaliacao: avaliacao)
+ expect(submissao).not_to be_valid
+ expect(submissao.errors[:aluno]).to be_present
+ end
+
+ it "é inválida sem avaliação" do
+ submissao = Submissao.new(aluno: aluno, avaliacao: nil)
+ expect(submissao).not_to be_valid
+ expect(submissao.errors[:avaliacao]).to be_present
+ end
+ end
+end
diff --git a/spec/requests/avaliacoes_spec.rb b/spec/requests/avaliacoes_spec.rb
new file mode 100644
index 0000000000..0dcf627be8
--- /dev/null
+++ b/spec/requests/avaliacoes_spec.rb
@@ -0,0 +1,78 @@
+require 'rails_helper'
+
+RSpec.describe "Avaliações", type: :request do
+ # 1. Criação do Usuário para Autenticação
+ let(:password) { "senha_segura_123" }
+ let(:user) do
+ User.create!(
+ login: "avaliador_teste",
+ email_address: "avaliador@teste.com",
+ matricula: "999999",
+ nome: "Avaliador Teste",
+ formacao: "Docente",
+ password: password,
+ password_confirmation: password,
+ eh_admin: true
+ )
+ end
+
+ # 2. Login antes de cada teste
+ before do
+ post session_path, params: { email_address: user.email_address, password: password }
+ end
+
+ describe "GET /gestao_envios" do
+ it "retorna sucesso HTTP" do
+ get gestao_envios_avaliacoes_path
+ expect(response).to have_http_status(:success)
+ end
+ end
+
+ describe "POST /create" do
+ let!(:turma) { Turma.create!(codigo: "CIC001", nome: "Turma de Teste", semestre: "2024.1") }
+
+ # 3. FIX: Criação do Modelo COM Pergunta (para não dar erro de validação)
+ let!(:template) do
+ mod = Modelo.new(titulo: "Template Padrão", ativo: true)
+ # Adiciona uma pergunta na memória antes de salvar
+ mod.perguntas.build(enunciado: "Pergunta Obrigatória", tipo: "texto_curto")
+ mod.save!
+ mod
+ end
+
+ context "com entradas válidas" do
+ it "cria uma nova Avaliação vinculada ao template padrão" do
+ expect {
+ post avaliacoes_path, params: { turma_id: turma.id }
+ }.to change(Avaliacao, :count).by(1)
+
+ avaliacao = Avaliacao.last
+ expect(avaliacao.turma).to eq(turma)
+ expect(avaliacao.modelo).to eq(template)
+ expect(response).to redirect_to(gestao_envios_avaliacoes_path)
+ expect(flash[:notice]).to be_present
+ end
+
+ it "aceita uma data_fim personalizada" do
+ data_personalizada = 2.weeks.from_now.to_date
+ post avaliacoes_path, params: { turma_id: turma.id, data_fim: data_personalizada }
+
+ avaliacao = Avaliacao.last
+ expect(avaliacao.data_fim.to_date).to eq(data_personalizada)
+ end
+ end
+
+ context "quando o template padrão está ausente" do
+ before { template.update!(titulo: "Outro") }
+
+ it "não cria avaliação e redireciona com alerta" do
+ expect {
+ post avaliacoes_path, params: { turma_id: turma.id }
+ }.not_to change(Avaliacao, :count)
+
+ expect(response).to redirect_to(gestao_envios_avaliacoes_path)
+ expect(flash[:alert]).to include("Template Padrão não encontrado")
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index cf7d9210f9..49ae0bc9f5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -14,7 +14,12 @@
#
# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
require 'simplecov'
-SimpleCov.start do
+SimpleCov.start 'rails' do
+ add_filter "app/channels"
+ add_filter "app/jobs"
+ add_filter "app/mailers"
+
+ # Grupos para organizar o relatório visualmente
add_group 'Controllers', 'app/controllers'
add_group 'Models', 'app/models'
end