diff --git a/Gemfile b/Gemfile index aa909e94c3..983667a23c 100644 --- a/Gemfile +++ b/Gemfile @@ -66,13 +66,12 @@ end gem "tailwindcss-rails", "~> 4.4" -gem "rspec-rails", "~> 8.0", groups: [:development, :test] +gem "rspec-rails", "~> 8.0", groups: [ :development, :test ] -gem 'simplecov', require: false, group: :test +gem "simplecov", require: false, group: :test gem "rubycritic", require: false gem "csv", "~> 3.3" # Email preview in browser for development -gem 'letter_opener', group: :development - +gem "letter_opener", group: :development diff --git a/app/controllers/avaliacoes_controller.rb b/app/controllers/avaliacoes_controller.rb index 43a931f5d1..e855f9a70e 100644 --- a/app/controllers/avaliacoes_controller.rb +++ b/app/controllers/avaliacoes_controller.rb @@ -1,11 +1,11 @@ class AvaliacoesController < ApplicationController # Requer autenticação para todas as actions - + def index # Se for admin, mostrar todas as avaliações # Se for aluno, mostrar todas as turmas matriculadas @turmas = [] # Inicializa como array vazio por padrão - + if current_user&.eh_admin? @avaliacoes = Avaliacao.all elsif current_user diff --git a/app/controllers/concerns/authenticatable.rb b/app/controllers/concerns/authenticatable.rb index 4d7f7a6e56..3669bd8cfc 100644 --- a/app/controllers/concerns/authenticatable.rb +++ b/app/controllers/concerns/authenticatable.rb @@ -1,19 +1,19 @@ # app/controllers/concerns/authenticatable.rb module Authenticatable extend ActiveSupport::Concern - + included do helper_method :current_user, :user_signed_in? end - + def authenticate_user! redirect_to new_session_path, alert: "É necessário fazer login." unless user_signed_in? end - + def current_user Current.session&.user end - + def user_signed_in? current_user.present? end diff --git a/app/controllers/migration_tabela_submissao/controller_template.rb b/app/controllers/migration_tabela_submissao/controller_template.rb deleted file mode 100644 index 97f977ace1..0000000000 --- a/app/controllers/migration_tabela_submissao/controller_template.rb +++ /dev/null @@ -1,98 +0,0 @@ -# app/controllers/templates_controller.rb -class TemplatesController < ApplicationController - before_action :authenticate_user! - before_action :authorize_admin - before_action :set_template, only: [:show, :edit, :update, :destroy, :clone] - - # GET /templates - def index - @templates = Template.includes(:questaos).order(created_at: :desc) - end - - # GET /templates/1 - def show - end - - # GET /templates/new - def new - @template = Template.new - 3.times { @template.questaos.build } # Cria 3 questões em branco por padrão - end - - # GET /templates/1/edit - def edit - @template.questaos.build if @template.questaos.empty? - end - - # POST /templates - def create - @template = Template.new(template_params) - - if @template.save - redirect_to @template, notice: 'Template criado com sucesso.' - else - # Garante que tenha pelo menos uma questão para mostrar no formulário - @template.questaos.build if @template.questaos.empty? - render :new, status: :unprocessable_entity - end - end - - # PATCH/PUT /templates/1 - def update - if @template.update(template_params) - redirect_to @template, notice: 'Template atualizado com sucesso.' - else - render :edit, status: :unprocessable_entity - end - end - - # DELETE /templates/1 - def destroy - if @template.em_uso? - redirect_to templates_url, alert: 'Não é possível excluir um template que está em uso.' - else - @template.destroy - redirect_to templates_url, notice: 'Template excluído com sucesso.' - end - end - - # POST /templates/1/clone - def clone - novo_nome = "#{@template.nome} (Cópia)" - novo_template = @template.clonar_com_questoes(novo_nome) - - if novo_template.persisted? - redirect_to edit_template_path(novo_template), - notice: 'Template clonado com sucesso. Edite o nome se necessário.' - else - redirect_to @template, alert: 'Erro ao clonar template.' - end - end - - private - - def set_template - @template = Template.find(params[:id]) - end - - def template_params - params.require(:template).permit( - :nome, - :descricao, - questaos_attributes: [ - :id, - :enunciado, - :tipo, - :opcoes, - :ordem, - :_destroy - ] - ) - end - - def authorize_admin - unless current_user.admin? - redirect_to root_path, alert: 'Acesso restrito a administradores.' - end - end -end \ No newline at end of file diff --git a/app/controllers/migration_tabela_submissao/models_questao.rb b/app/controllers/migration_tabela_submissao/models_questao.rb deleted file mode 100644 index 533e85f762..0000000000 --- a/app/controllers/migration_tabela_submissao/models_questao.rb +++ /dev/null @@ -1,73 +0,0 @@ -# app/models/questao.rb -class Questao < ApplicationRecord - # Relacionamentos - belongs_to :template - has_many :respostas, dependent: :destroy - - # Tipos de questões disponíveis - TIPOS = { - 'texto_longo' => 'Texto Longo', - 'texto_curto' => 'Texto Curto', - 'multipla_escolha' => 'Múltipla Escolha', - 'checkbox' => 'Checkbox (Múltipla Seleção)', - 'escala' => 'Escala Likert (1-5)', - 'data' => 'Data', - 'hora' => 'Hora' - }.freeze - - # Validações - validates :enunciado, presence: true - validates :tipo, presence: true, inclusion: { in: TIPOS.keys } - validates :ordem, presence: true, numericality: { only_integer: true, greater_than: 0 } - - # Validações condicionais - validate :opcoes_requeridas_para_multipla_escolha - validate :opcoes_requeridas_para_checkbox - - # Callbacks - before_validation :definir_ordem_padrao, on: :create - after_save :reordenar_questoes - - # Métodos - def tipo_humanizado - TIPOS[tipo] || tipo - end - - def requer_opcoes? - ['multipla_escolha', 'checkbox'].include?(tipo) - end - - def lista_opcoes - return [] unless opcoes.present? - opcoes.split(';').map(&:strip) - end - - private - - def definir_ordem_padrao - if ordem.nil? && template.present? - ultima_ordem = template.questaos.maximum(:ordem) || 0 - self.ordem = ultima_ordem + 1 - end - end - - def reordenar_questoes - if ordem_changed? && template.present? - template.questaos.where.not(id: id).order(:ordem, :created_at).each_with_index do |questao, index| - questao.update_column(:ordem, index + 1) if questao.ordem != index + 1 - end - end - end - - def opcoes_requeridas_para_multipla_escolha - if tipo == 'multipla_escolha' && (opcoes.blank? || opcoes.split(';').size < 2) - errors.add(:opcoes, 'deve ter pelo menos duas opções para múltipla escolha') - end - end - - def opcoes_requeridas_para_checkbox - if tipo == 'checkbox' && (opcoes.blank? || opcoes.split(';').size < 2) - errors.add(:opcoes, 'deve ter pelo menos duas opções para checkbox') - end - end -end \ No newline at end of file diff --git a/app/controllers/migration_tabela_submissao/models_template.rb b/app/controllers/migration_tabela_submissao/models_template.rb deleted file mode 100644 index 5c0f59345f..0000000000 --- a/app/controllers/migration_tabela_submissao/models_template.rb +++ /dev/null @@ -1,55 +0,0 @@ -# app/models/template.rb -class Template < ApplicationRecord - # Relacionamentos - has_many :questaos, dependent: :destroy - has_many :formularios, dependent: :restrict_with_error - - # Validações - validates :nome, presence: true, uniqueness: { case_sensitive: false } - validates :descricao, presence: true - - # Validação customizada: não permitir template sem questões - validate :deve_ter_pelo_menos_uma_questao, on: :create - validate :nao_pode_remover_todas_questoes, on: :update - - # Aceita atributos aninhados para questões - accepts_nested_attributes_for :questaos, - allow_destroy: true, - reject_if: :all_blank - - # Método para verificar se template está em uso - def em_uso? - formularios.any? - end - - # Método para clonar template com questões - def clonar_com_questoes(novo_nome) - novo_template = dup - novo_template.nome = novo_nome - novo_template.save - - if novo_template.persisted? - questaos.each do |questao| - nova_questao = questao.dup - nova_questao.template = novo_template - nova_questao.save - end - end - - novo_template - end - - private - - def deve_ter_pelo_menos_uma_questao - if questaos.empty? || questaos.all? { |q| q.marked_for_destruction? } - errors.add(:base, "Um template deve ter pelo menos uma questão") - end - end - - def nao_pode_remover_todas_questoes - if persisted? && (questaos.empty? || questaos.all? { |q| q.marked_for_destruction? }) - errors.add(:base, "Não é possível remover todas as questões de um template existente") - end - end -end \ No newline at end of file diff --git a/app/controllers/migration_tabela_submissao/view_flash_messeges.html.erb b/app/controllers/migration_tabela_submissao/view_flash_messeges.html.erb deleted file mode 100644 index 6427587990..0000000000 --- a/app/controllers/migration_tabela_submissao/view_flash_messeges.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<%# app/views/shared/_flash_messages.html.erb %> -<% flash.each do |type, message| %> - <% alert_class = case type.to_sym - when :notice then 'bg-green-100 border-green-400 text-green-700' - when :alert, :error then 'bg-red-100 border-red-400 text-red-700' - else 'bg-blue-100 border-blue-400 text-blue-700' - end %> - -
-<% end %> \ No newline at end of file diff --git a/app/controllers/migration_tabela_submissao/view_form_template_new_edit.html.erb b/app/controllers/migration_tabela_submissao/view_form_template_new_edit.html.erb deleted file mode 100644 index 24e06060d3..0000000000 --- a/app/controllers/migration_tabela_submissao/view_form_template_new_edit.html.erb +++ /dev/null @@ -1,261 +0,0 @@ -<%# app/views/templates/_form.html.erb %> -<%= form_with(model: template, local: true, class: "space-y-8") do |form| %> - <% if template.errors.any? %> -Nome único para identificar o template
-Breve descrição sobre o propósito do formulário
-Um template deve ter pelo menos uma questão para ser salvo.
-<%= @template.descricao %>
-Opções:
-Gerencie os modelos de formulários de avaliação
-Comece criando seu primeiro template de formulário.
- <%= link_to 'Criar Primeiro Template', new_template_path, class: 'btn-primary' %> -- <%= template.descricao %> -
-Opções:
class AvaliacoesController < ApplicationController
+ # app/controllers/concerns/authenticatable.rb
# Requer autenticação para todas as actions
-
+ module Authenticatable
def index
+ extend ActiveSupport::Concern
# Se for admin, mostrar todas as avaliações
+
# Se for aluno, mostrar todas as turmas matriculadas
- @turmas = [] # Inicializa como array vazio por padrão
+ included do
- if current_user&.eh_admin?
+ helper_method :current_user, :user_signed_in?
@avaliacoes = Avaliacao.all
+ end
elsif current_user
+
# Alunos veem suas turmas matriculadas
+ def authenticate_user!
@turmas = current_user.turmas.includes(:avaliacoes)
+ redirect_to new_session_path, alert: "É necessário fazer login." unless user_signed_in?
else
+ end
# Não logado - redireciona para login
+
redirect_to new_session_path
+ def current_user
end
+ Current.session&.user
+
def gestao_envios
+ def user_signed_in?
@turmas = Turma.all
+ current_user.present?
+ end
def create
-
- +--
-- +
- + + 1 -
turma_id = params[:turma_id]+module Authentication-- +
- + + 1 -
turma = Turma.find_by(id: turma_id)+extend ActiveSupport::Concern-- +
- @@ -1334,47 +1231,53 @@
-- +
- + + 1 -
if turma.nil?+included do-- +
- + + 1 -
redirect_to gestao_envios_avaliacoes_path, alert: "Turma não encontrada."+before_action :require_authentication-- +
- + + 1 -
return+helper_method :authenticated?-- +
- -
end+end-- +
- @@ -1384,369 +1287,409 @@
-- +
- + + 1 -
template = Modelo.find_by(titulo: "Template Padrão", ativo: true)+class_methods do-- +
- + + 1 -
+def allow_unauthenticated_access(**options)-- +
- + + 1 -
if template.nil?+skip_before_action :require_authentication, **options-- +
- -
redirect_to gestao_envios_avaliacoes_path, alert: "Template Padrão não encontrado. Contate o administrador."+end-- +
- -
return+end-- +
- -
end+-- +
- + + 1 -
+private-- +
- + + 1 -
@avaliacao = Avaliacao.new(+def authenticated?-- +
- + + 2 -
turma: turma,+resume_session-- +
- -
modelo: template,+end-- +
- -
data_inicio: Time.current,+-- +
- + + 1 -
data_fim: params[:data_fim].presence || 7.days.from_now+def require_authentication-- +
- + + 14 -
)+resume_session || request_authentication-- +
- -
+end-- +
- -
if @avaliacao.save+-- +
- + + 1 -
redirect_to gestao_envios_avaliacoes_path, notice: "Avaliação criada com sucesso para a turma #{turma.codigo}."+def resume_session-- +
- + + 19 -
else+Current.session ||= find_session_by_cookie-- +
- -
redirect_to gestao_envios_avaliacoes_path, alert: "Erro ao criar avaliação: #{@avaliacao.errors.full_messages.join(', ')}"+end-- +
- -
end+-- +
- + + 1 -
end+def find_session_by_cookie-- +
- + + 17 -
+Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]-- - - 1 +
- -
def resultados+end-- +
- -
@avaliacao = Avaliacao.find(params[:id])+-- +
- + + 1 -
# Pré-carrega dependências para evitar N+1.+def request_authentication-- +
- + + 9 -
begin+session[:return_to_after_authenticating] = request.url-- +
- + + 9 -
@submissoes = @avaliacao.submissoes.includes(:aluno, :respostas)+redirect_to new_session_path-- +
- -
rescue ActiveRecord::StatementInvalid+end-- +
- -
@submissoes = []+-- +
- + + 1 -
flash.now[:alert] = "Erro ao carregar submissões."+def after_authentication_url-- +
- + + 2 -
end+session.delete(:return_to_after_authenticating) || root_url-- +
- -
+end-- +
- -
respond_to do |format|+-- +
- + + 1 -
format.html+def start_new_session_for(user)-- +
- + + 5 -
format.csv do+user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|-- +
- + + 5 -
send_data CsvFormatterService.new(@avaliacao).generate,+Current.session = session-- +
- + + 5 -
filename: "resultados-avaliacao-#{@avaliacao.id}-#{Date.today}.csv"+cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }-- +
- @@ -1756,7 +1699,7 @@
-- +
- @@ -1766,183 +1709,4005 @@
-- +
- -
end+--- +
- + + 1 -
end+def terminate_session
--
-- +
- + + 3 -
# app/controllers/concerns/authenticatable.rb+Current.session.destroy-- +
- - 1 + 3 -
module Authenticatable+cookies.delete(:session_id)-- - - 1 +
- -
extend ActiveSupport::Concern+end--- +
- -
+end-+ + +- + + +
++ + +++ +app/helpers/application_helper.rb
++ + 100.0% + + + lines covered +
+ + + ++ 1 relevant lines. + 1 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+module ApplicationHelper+++ +- + + + + +
+end+++ + +++ +app/helpers/avaliacoes_helper.rb
++ + 100.0% + + + lines covered +
+ + + ++ 1 relevant lines. + 1 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+module AvaliacoesHelper+++ +- + + + + +
+end+++ + +++ +app/helpers/home_helper.rb
++ + 100.0% + + + lines covered +
+ + + ++ 1 relevant lines. + 1 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+module HomeHelper+++ +- + + + + +
+end+++ + +++ +app/models/application_record.rb
++ + 100.0% + + + lines covered +
+ + + ++ 2 relevant lines. + 2 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+class ApplicationRecord < ActiveRecord::Base+++ +- + + 1 + + + + +
+primary_abstract_class+++ +- + + + + +
+end+++ + +++ +app/models/current.rb
++ + 100.0% + + + lines covered +
+ + + ++ 3 relevant lines. + 3 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+class Current < ActiveSupport::CurrentAttributes+++ +- + + 1 + + + + +
+attribute :session+++ +- + + 1 + + + + +
+delegate :user, to: :session, allow_nil: true+++ +- + + + + +
+end+++ + +++ +app/models/session.rb
++ + 100.0% + + + lines covered +
+ + + ++ 2 relevant lines. + 2 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+class Session < ApplicationRecord+++ +- + + 1 + + + + +
+belongs_to :user+++ +- + + + + +
+end+++ + +++ +app/models/user.rb
++ + 100.0% + + + lines covered +
+ + + ++ 12 relevant lines. + 12 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+class User < ApplicationRecord+++ +- + + 1 + + + + +
+has_secure_password+++ +- + + 1 + + + + +
+has_many :sessions, dependent: :destroy+++ +- + + 1 + + + + +
+has_many :matricula_turmas+++ +- + + 1 + + + + +
+has_many :turmas, through: :matricula_turmas+++ +- + + 1 + + + + +
+has_many :submissoes, class_name: 'Submissao', foreign_key: :aluno_id, dependent: :destroy+++ +- + + + + +
++++ +- + + 1 + + + + +
+validates :email_address, presence: true, uniqueness: true+++ +- + + 1 + + + + +
+validates :login, presence: true, uniqueness: true+++ +- + + 1 + + + + +
+validates :matricula, presence: true, uniqueness: true+++ +- + + 1 + + + + +
+validates :nome, presence: true+++ +- + + + + +
++++ +- + + 33 + + + + +
+normalizes :email_address, with: ->(e) { e.strip.downcase }+++ +- + + 33 + + + + +
+normalizes :login, with: ->(l) { l.strip.downcase }+++ +- + + + + +
+end+++ + +++ +config/application.rb
++ + 100.0% + + + lines covered +
+ + + ++ 7 relevant lines. + 7 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+require_relative "boot"+++ +- + + + + +
++++ +- + + 1 + + + + +
+require "rails/all"+++ +- + + + + +
++++ +- + + + + +
+# Require the gems listed in Gemfile, including any gems+++ +- + + + + +
+# you've limited to :test, :development, or :production.+++ +- + + 1 + + + + +
+Bundler.require(*Rails.groups)+++ +- + + + + +
++++ +- + + 1 + + + + +
+module Camaar+++ +- + + 1 + + + + +
+class Application < Rails::Application+++ +- + + + + +
+# Initialize configuration defaults for originally generated Rails version.+++ +- + + 1 + + + + +
+config.load_defaults 8.0+++ +- + + + + +
++++ +- + + + + +
+# Please, add to the `ignore` list any other `lib` subdirectories that do+++ +- + + + + +
+# not contain `.rb` files, or that should not be reloaded or eager loaded.+++ +- + + + + +
+# Common ones are `templates`, `generators`, or `middleware`, for example.+++ +- + + 1 + + + + +
+config.autoload_lib(ignore: %w[assets tasks])+++ +- + + + + +
++++ +- + + + + +
+# Configuration for the application, engines, and railties goes here.+++ +- + + + + +
+#+++ +- + + + + +
+# These settings can be overridden in specific environments using the files+++ +- + + + + +
+# in config/environments, which are processed later.+++ +- + + + + +
+#+++ +- + + + + +
+# config.time_zone = "Central Time (US & Canada)"+++ +- + + + + +
+# config.eager_load_paths << Rails.root.join("extras")+++ +- + + + + +
+end+++ +- + + + + +
+end+++ + +++ +config/boot.rb
++ + 100.0% + + + lines covered +
+ + + ++ 3 relevant lines. + 3 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)+++ +- + + + + +
++++ +- + + 1 + + + + +
+require "bundler/setup" # Set up gems listed in the Gemfile.+++ +- + + 1 + + + + +
+require "bootsnap/setup" # Speed up boot time by caching expensive operations.+++ + +++ +config/environment.rb
++ + 100.0% + + + lines covered +
+ + + ++ 2 relevant lines. + 2 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + + + +
+# Load the Rails application.+++ +- + + 1 + + + + +
+require_relative "application"+++ +- + + + + +
++++ +- + + + + +
+# Initialize the Rails application.+++ +- + + 1 + + + + +
+Rails.application.initialize!+++ + +++ +config/environments/test.rb
++ + 100.0% + + + lines covered +
+ + + ++ 15 relevant lines. + 15 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + + + +
+# The test environment is used exclusively to run your application's+++ +- + + + + +
+# test suite. You never need to work with it otherwise. Remember that+++ +- + + + + +
+# your test database is "scratch space" for the test suite and is wiped+++ +- + + + + +
+# and recreated between test runs. Don't rely on the data there!+++ +- + + + + +
++++ +- + + 1 + + + + +
+Rails.application.configure do+++ +- + + + + +
+# Configure 'rails notes' to inspect Cucumber files+++ +- + + 1 + + + + +
+config.annotations.register_directories("features")+++ +- + + 1 + + + + +
+config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }+++ +- + + + + +
++++ +- + + + + +
+# Settings specified here will take precedence over those in config/application.rb.+++ +- + + + + +
++++ +- + + + + +
+# While tests run files are not watched, reloading is not necessary.+++ +- + + 1 + + + + +
+config.enable_reloading = false+++ +- + + + + +
++++ +- + + + + +
+# Eager loading loads your entire application. When running a single test locally,+++ +- + + + + +
+# this is usually not necessary, and can slow down your test suite. However, it's+++ +- + + + + +
+# recommended that you enable it in continuous integration systems to ensure eager+++ +- + + + + +
+# loading is working properly before deploying your code.+++ +- + + 1 + + + + +
+config.eager_load = ENV["CI"].present?+++ +- + + + + +
++++ +- + + + + +
+# Configure public file server for tests with cache-control for performance.+++ +- + + 1 + + + + +
+config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }+++ +- + + + + +
++++ +- + + + + +
+# Show full error reports.+++ +- + + 1 + + + + +
+config.consider_all_requests_local = true+++ +- + + 1 + + + + +
+config.cache_store = :null_store+++ +- + + + + +
++++ +- + + + + +
+# Render exception templates for rescuable exceptions and raise for other exceptions.+++ +- + + 1 + + + + +
+config.action_dispatch.show_exceptions = :rescuable+++ +- + + + + +
++++ +- + + + + +
+# Disable request forgery protection in test environment.+++ +- + + 1 + + + + +
+config.action_controller.allow_forgery_protection = false+++ +- + + + + +
++++ +- + + + + +
+# Store uploaded files on the local file system in a temporary directory.+++ +- + + 1 + + + + +
+config.active_storage.service = :test+++ +- + + + + +
++++ +- + + + + +
+# Tell Action Mailer not to deliver emails to the real world.+++ +- + + + + +
+# The :test delivery method accumulates sent emails in the+++ +- + + + + +
+# ActionMailer::Base.deliveries array.+++ +- + + 1 + + + + +
+config.action_mailer.delivery_method = :test+++ +- + + + + +
++++ +- + + + + +
+# Set host to be used by links generated in mailer templates.+++ +- + + 1 + + + + +
+config.action_mailer.default_url_options = { host: "example.com" }+++ +- + + + + +
++++ +- + + + + +
+# Print deprecation notices to the stderr.+++ +- + + 1 + + + + +
+config.active_support.deprecation = :stderr+++ +- + + + + +
++++ +- + + + + +
+# Raises error for missing translations.+++ +- + + + + +
+# config.i18n.raise_on_missing_translations = true+++ +- + + + + +
++++ +- + + + + +
+# Annotate rendered view with file names.+++ +- + + + + +
+# config.action_view.annotate_rendered_view_with_filenames = true+++ +- + + + + +
++++ +- + + + + +
+# Raise error when a before_action's only/except options reference missing actions.+++ +- + + 1 + + + + +
+config.action_controller.raise_on_missing_callback_actions = true+++ +- + + + + +
+end+++ + +++ +config/initializers/assets.rb
++ + 100.0% + + + lines covered +
+ + + ++ 1 relevant lines. + 1 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + + + +
+# Be sure to restart your server when you modify this file.+++ +- + + + + +
++++ +- + + + + +
+# Version of your assets, change this if you want to expire all your assets.+++ +- + + 1 + + + + +
+Rails.application.config.assets.version = "1.0"+++ +- + + + + +
++++ +- + + + + +
+# Add additional assets to the asset load path.+++ +- + + + + +
+# Rails.application.config.assets.paths << Emoji.images_path+++ + +++ +config/initializers/content_security_policy.rb
++ + 100.0% + + + lines covered +
+ + + ++ 0 relevant lines. + 0 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + + + +
+# Be sure to restart your server when you modify this file.+++ +- + + + + +
++++ +- + + + + +
+# Define an application-wide content security policy.+++ +- + + + + +
+# See the Securing Rails Applications Guide for more information:+++ +- + + + + +
+# https://guides.rubyonrails.org/security.html#content-security-policy-header+++ +- + + + + +
++++ +- + + + + +
+# Rails.application.configure do+++ +- + + + + +
+# config.content_security_policy do |policy|+++ +- + + + + +
+# policy.default_src :self, :https+++ +- + + + + +
+# policy.font_src :self, :https, :data+++ +- + + + + +
+# policy.img_src :self, :https, :data+++ +- + + + + +
+# policy.object_src :none+++ +- + + + + +
+# policy.script_src :self, :https+++ +- + + + + +
+# policy.style_src :self, :https+++ +- + + + + +
+# # Specify URI for violation reports+++ +- + + + + +
+# # policy.report_uri "/csp-violation-report-endpoint"+++ +- + + + + +
+# end+++ +- + + + + +
+#+++ +- + + + + +
+# # Generate session nonces for permitted importmap, inline scripts, and inline styles.+++ +- + + + + +
+# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }+++ +- + + + + +
+# config.content_security_policy_nonce_directives = %w(script-src style-src)+++ +- + + + + +
+#+++ +- + + + + +
+# # Report violations without enforcing the policy.+++ +- + + + + +
+# # config.content_security_policy_report_only = true+++ +- + + + + +
+# end+++ + +++ +config/initializers/filter_parameter_logging.rb
++ + 100.0% + + + lines covered +
+ + + ++ 1 relevant lines. + 1 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + + + +
+# Be sure to restart your server when you modify this file.+++ +- + + + + +
++++ +- + + + + +
+# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.+++ +- + + + + +
+# Use this to limit dissemination of sensitive information.+++ +- + + + + +
+# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.+++ +- + + 1 + + + + +
+Rails.application.config.filter_parameters += [+++ +- + + + + +
+:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc+++ +- + + + + +
+]+++ + +++ +config/initializers/inflections.rb
++ + 100.0% + + + lines covered +
+ + + ++ 2 relevant lines. + 2 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + + + +
+# Be sure to restart your server when you modify this file.+++ +- + + + + +
++++ +- + + + + +
+# Add new inflection rules using the following format. Inflections+++ +- + + + + +
+# are locale specific, and you may define rules for as many different+++ +- + + + + +
+# locales as you wish. All of these examples are active by default:+++ +- + + + + +
+# ActiveSupport::Inflector.inflections(:en) do |inflect|+++ +- + + + + +
+# inflect.plural /^(ox)$/i, "\\1en"+++ +- + + + + +
+# inflect.singular /^(ox)en/i, "\\1"+++ +- + + + + +
+# inflect.irregular "person", "people"+++ +- + + + + +
+# inflect.uncountable %w( fish sheep )+++ +- + + + + +
+# end+++ +- + + + + +
++++ +- + + + + +
+# These inflection rules are supported but not enabled by default:+++ +- + + 1 + + + + +
+ActiveSupport::Inflector.inflections(:en) do |inflect|+++ +- + + 1 + + + + +
+inflect.irregular "pergunta", "perguntas"+++ +- + + + + +
+# inflect.acronym "RESTful"+++ +- + + + + +
+end+++ + +++ +config/initializers/inflections_custom.rb
++ + 100.0% + + + lines covered +
+ + + ++ 2 relevant lines. + 2 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+ActiveSupport::Inflector.inflections(:en) do |inflect|+++ +- + + 1 + + + + +
+inflect.irregular 'avaliacao', 'avaliacoes'+++ +- + + + + +
+end+++ + +++ +config/initializers/mail.rb
++ + 25.0% + + + lines covered +
+ + + ++ 12 relevant lines. + 3 lines covered and + 9 lines missed. ++ + + ++++ +
+++ +- + + + + +
+# config/initializers/mail.rb+++ +- + + + + +
+# Configuração de email para CAMAAR+++ +- + + + + +
++++ +- + + 1 + + + + +
+if Rails.env.test?+++ +- + + + + +
+# Em testes: captura emails sem enviar+++ +- + + 1 + + + + +
+Rails.application.config.action_mailer.delivery_method = :test+++ +- + + 1 + + + + +
+Rails.application.config.action_mailer.default_url_options = {+++ +- + + + + +
+host: 'localhost',+++ +- + + + + +
+port: 3000+++ +- + + + + +
+}+++ +- + + + + +
++++ +- + + + + +
+elsif Rails.env.development?+++ +- + + + + +
+# MVP: Usa letter_opener (emails abrem no navegador)+++ +- + + + + +
+# Instale: gem install letter_opener ou adicione ao Gemfile+++ +- + + + + +
+# PRODUÇÃO: Para enviar emails reais, descomente a seção SMTP abaixo+++ +- + + + + +
++++ +- + + + + +
+Rails.application.config.action_mailer.delivery_method = :letter_opener+++ +- + + + + +
+Rails.application.config.action_mailer.perform_deliveries = true+++ +- + + + + +
++++ +- + + + + +
+# === SMTP (Para Produção) ===+++ +- + + + + +
+# Descomente as linhas abaixo e configure variáveis de ambiente (.env)+++ +- + + + + +
+# para enviar emails reais via SMTP (Gmail, Sendgrid, etc.)+++ +- + + + + +
+#+++ +- + + + + +
+# Rails.application.config.action_mailer.delivery_method = :smtp+++ +- + + + + +
+# Rails.application.config.action_mailer.raise_delivery_errors = true+++ +- + + + + +
+#+++ +- + + + + +
+# Rails.application.config.action_mailer.smtp_settings = {+++ +- + + + + +
+# address: ENV.fetch('SMTP_ADDRESS', 'smtp.gmail.com'),+++ +- + + + + +
+# port: ENV.fetch('SMTP_PORT', '587').to_i,+++ +- + + + + +
+# domain: ENV.fetch('SMTP_DOMAIN', 'localhost'),+++ +- + + + + +
+# user_name: ENV['SMTP_USER'],+++ +- + + + + +
+# password: ENV['SMTP_PASSWORD'],+++ +- + + + + +
+# authentication: 'plain',+++ +- + + + + +
+# enable_starttls_auto: true+++ +- + + + + +
+# }+++ +- + + + + +
++++ +- + + + + +
+Rails.application.config.action_mailer.default_url_options = {+++ +- + + + + +
+host: ENV.fetch('APP_HOST', 'localhost'),+++ +- + + + + +
+port: ENV.fetch('APP_PORT', '3000').to_i+++ +- + + + + +
+}+++ +- + + + + +
++++ +- + + + + +
+else+++ +- + + + + +
+# Produção: SMTP obrigatório+++ +- + + + + +
+Rails.application.config.action_mailer.delivery_method = :smtp+++ +- + + + + +
+Rails.application.config.action_mailer.perform_deliveries = true+++ +- + + + + +
+Rails.application.config.action_mailer.raise_delivery_errors = false+++ +- + + + + +
++++ +- + + + + +
+Rails.application.config.action_mailer.smtp_settings = {+++ +- + + + + +
+address: ENV.fetch('SMTP_ADDRESS'),+++ +- + + + + +
+port: ENV.fetch('SMTP_PORT', '587').to_i,+++ +- + + + + +
+domain: ENV.fetch('SMTP_DOMAIN'),+++ +- + + + + +
+user_name: ENV.fetch('SMTP_USER'),+++ +- + + + + +
+password: ENV.fetch('SMTP_PASSWORD'),+++ +- + + + + +
+authentication: 'plain',+++ +- + + + + +
+enable_starttls_auto: true,+++ +- + + + + +
+open_timeout: 10,+++ +- + + + + +
+read_timeout: 10+++ +- + + + + +
+}+++ +- + + + + +
++++ +- + + + + +
+Rails.application.config.action_mailer.default_url_options = {+++ +- + + + + +
+host: ENV.fetch('APP_HOST'),+++ +- + + + + +
+protocol: 'https'+++ +- + + + + +
+}+++ +- + + + + +
+end+++ +- + + + + +
++++ + +++ +config/routes.rb
++ + 100.0% + + + lines covered +
+ + + ++ 23 relevant lines. + 23 lines covered and + 0 lines missed. ++ + + ++++ +
+++ +- + + 1 + + + + +
+Rails.application.routes.draw do+++ +- + + + + +
+# --- ROTAS DE AVALIACOES ---+++ +- + + 1 + + + + +
+resources :avaliacoes, only: [:index, :create] do+++ +- + + 1 + + + + +
+collection do+++ +- + + 1 + + + + +
+get :gestao_envios+++ +- + + + + +
+end+++ +- + + 1 + + + + +
+member do+++ +- + + 1 + + + + +
+get :resultados+++ +- + + + + +
+end+++ +- + + + + +
+# Rotas para alunos responderem avaliações (Feature 99)+++ +- + + 1 + + + + +
+resources :respostas, only: [:new, :create]+++ +- + + + + +
+end+++ +- + + + + +
++++ +- + + + + +
+# --- ROTAS DE IMPORTAÇÃO SIGAA ---+++ +- + + 1 + + + + +
+resources :sigaa_imports, only: [:new, :create] do+++ +- + + 1 + + + + +
+collection do+++ +- + + 1 + + + + +
+post :update # For update/sync operations+++ +- + + 1 + + + + +
+get :success # For showing import results+++ +- + + + + +
+end+++ +- + + + + +
+end+++ +- + + + + +
++++ +- + + + + +
+# --- ROTAS DE GERENCIAMENTO DE MODELOS ---+++ +- + + 1 + + + + +
+resources :modelos do+++ +- + + 1 + + + + +
+member do+++ +- + + 1 + + + + +
+post :clone+++ +- + + + + +
+end+++ +- + + + + +
+end+++ +- + + + + +
++++ +- + + 1 + + + + +
+resource :session+++ +- + + 1 + + + + +
+resources :passwords, param: :token+++ +- + + 1 + + + + +
+get "home/index"+++ +- + + + + +
+# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html+++ +- + + + + +
++++ +- + + + + +
+# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.+++ +- + + + + +
+# Can be used by load balancers and uptime monitors to verify that the app is live.+++ +- + + 1 + + + + +
+get "up" => "rails/health#show", as: :rails_health_check+++ +- + + + + +
++++ +- + + + + +
+# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)+++ +- + + + + +
+# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest+++ +- + + + + +
+# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker+++ +- + + + + +
++++ +- + + + + +
+# --- ROTAS DO INTEGRANTE 4 (RESPOSTAS) ---+++ +- + + + + +
+# Define as rotas aninhadas para criar respostas dentro de um formulário+++ +- + + 1 + + + + +
+resources :formularios, only: [] do+++ +- + + 1 + + + + +
+resources :respostas, only: [ :index, :new, :create ]+++ +- + + + + +
+end+++ +- + + + + +
++++ +- + + + + +
+# Rota solta para a listagem geral de respostas (dashboard do aluno)+++ +- + + 1 + + + + +
+get "respostas", to: "respostas#index"+++ +- + + + + +
+# -----------------------------------------+++ +- + + + + +
++++ +- + + + + +
+# Defines the root path route ("/")+++ +- + + 1 + + + + +
+root "pages#index"+++ +- + + + + +
++++ +- + + 1 + + + + +
+get "home" => "home#index"+++ +- + + + + +
+end++++ +spec/controllers/concerns/authenticatable_spec.rb
++ + 100.0% + + + lines covered +
+ + + ++ 61 relevant lines. + 61 lines covered and + 0 lines missed. ++ + + +++ +
++ +- + + 1 + + + + +
+require 'rails_helper'+++ +- + + + + +
+++- 1 -
included do+RSpec.describe Authenticatable, type: :controller do-- +
- 1 -
helper_method :current_user, :user_signed_in?+controller(ApplicationController) do-- +
- + + 1 -
end+include Authenticatable-- +
- -
+-- +
- 1 -
def authenticate_user!+def index-- +
- + + 2 -
redirect_to new_session_path, alert: "É necessário fazer login." unless user_signed_in?+render plain: 'Success'-- +
- -
end+end-+ +- +
- -
+++++ +- + + 1 + + + + +
+def protected_action++@@ -1954,17 +5719,17 @@- + + 1 + + + + +
authenticate_user!-
def current_user+render plain: 'Protected content'-@@ -1984,7 +5749,7 @@- +
- -
Current.session&.user+end-
+
def user_signed_in?
+ let(:user) do
current_user.present?
+ User.create!(
end
+ email_address: 'test@example.com',
end
+ login: 'test',
password_digest: 'password',
+ nome: 'nome',
+ matricula: '123456789'
+ -+
)+ +
module Authentication
+ end
extend ActiveSupport::Concern
+ let(:session_obj) { double('Session', user: user) }
included do
+ before do
before_action :require_authentication
+ routes.draw do
helper_method :authenticated?
+ get 'index' => 'anonymous#index'
get 'protected_action' => 'anonymous#protected_action'
+ end
+ class_methods do
+ describe '#current_user' do
def allow_unauthenticated_access(**options)
+ context 'when user is signed in' do
skip_before_action :require_authentication, **options
+ before do
end
+ allow(Current).to receive(:session).and_return(session_obj)
end
+ end
private
+ it 'returns the current user' do
def authenticated?
+ get :index
resume_session
+ expect(controller.current_user).to eq(user)
end
+ def require_authentication
+ context 'when user is not signed in' do
resume_session || request_authentication
+ before do
end
+ allow(Current).to receive(:session).and_return(nil)
end
+ def resume_session
+ it 'returns nil' do
Current.session ||= find_session_by_cookie
+ get :index
end
+ expect(controller.current_user).to be_nil
+ end
def find_session_by_cookie
+ end
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
+ end
end
+
+ describe '#user_signed_in?' do
def request_authentication
+ context 'when current_user is present' do
session[:return_to_after_authenticating] = request.url
+ before do
redirect_to new_session_path
+ allow(Current).to receive(:session).and_return(session_obj)
end
+ end
def after_authentication_url
+ it 'returns true' do
session.delete(:return_to_after_authenticating) || root_url
+ get :index
end
+ expect(controller.user_signed_in?).to be true
+ end
def start_new_session_for(user)
+ end
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
+
Current.session = session
+ context 'when current_user is not present' do
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
+ before do
end
+ allow(Current).to receive(:session).and_return(nil)
end
+ end
def terminate_session
+ it 'returns false' do
Current.session.destroy
+ get :index
cookies.delete(:session_id)
+ expect(controller.user_signed_in?).to be false
end
+ end
end
+ end
---
--- - - 1 +
- -
module ApplicationHelper+end--- +
- -
end+
---
--- +
- 1 -
module AvaliacoesHelper+describe '#authenticate_user!' do--- +
- + + 1 -
end+context 'when user is signed in' do
---
--- +
- 1 -
module HomeHelper+before do--- +
- + + 1 -
end+allow(Current).to receive(:session).and_return(session_obj)
---
--- - - 1 +
- -
class ApplicationRecord < ActiveRecord::Base+end-- - - 1 +
- -
primary_abstract_class+--- +
- + + 1 -
end+it 'allows access to the action' do
---
--- +
- 1 -
class Avaliacao < ApplicationRecord+get :protected_action-- +
- 1 -
belongs_to :turma+expect(response).to have_http_status(:success)-- +
- 1 -
belongs_to :modelo+expect(response.body).to eq('Protected content')-+ +- +
- - 1 + + + +
+end++- -
belongs_to :professor_alvo, class_name: 'User', optional: true+end-- +
- -
+-- +
- 1 -
has_many :submissoes, class_name: 'Submissao', dependent: :destroy+context 'when user is not signed in' do-- +
- 1 -
has_many :respostas, through: :submissoes+before do--- +
- + + 1 -
end+allow(Current).to receive(:session).and_return(nil)
end
+ -+
+ +
class Current < ActiveSupport::CurrentAttributes
+ it 'redirects to new_session_path' do
attribute :session
+ get :protected_action
delegate :user, to: :session, allow_nil: true
+ expect(response).to redirect_to(new_session_path)
end
+ end
end
+ end
+ -+
+ +
class Modelo < ApplicationRecord
+ describe 'helper methods' do
# Relacionamentos
+ it 'makes current_user available as a helper method' do
has_many :perguntas, dependent: :destroy
+ expect(controller.class._helper_methods).to include(:current_user)
has_many :avaliacoes, dependent: :restrict_with_error
+ end
+
# Validações
+ it 'makes user_signed_in? available as a helper method' do
validates :titulo, presence: true, uniqueness: { case_sensitive: false }
+ expect(controller.class._helper_methods).to include(:user_signed_in?)
+ end
# Validação customizada: não permitir modelo sem perguntas
+ end
validate :deve_ter_pelo_menos_uma_pergunta, on: :create
+ end
+-+
--- +
- 1 -
validate :nao_pode_remover_todas_perguntas, on: :update+require 'rails_helper'-- +
- -
+-- +
- + + 1 -
# Aceita atributos aninhados para perguntas+RSpec.describe Authentication, type: :controller do-- +
- 1 -
accepts_nested_attributes_for :perguntas,+controller(ApplicationController) do-- +
- + + 1 -
allow_destroy: true,+include Authentication-- +
- -
reject_if: :all_blank+-- +
- + + 1 -
+def index-- +
- + + 2 -
# Método para verificar se modelo está em uso+render plain: 'OK'-- - - 1 +
- -
def em_uso?+end-- +
- -
avaliacoes.any?+-- +
- + + 1 -
end+def public_action-- +
- -
+render plain: 'Public'-- +
- -
# Método para clonar modelo com perguntas+end-- - - 1 +
- -
def clonar_com_perguntas(novo_titulo)+end-- +
- -
novo_modelo = dup+-- +
- + + 1 -
novo_modelo.titulo = novo_titulo+let(:user) do-- +
- + + 13 -
novo_modelo.ativo = false # Clones começam inativos+User.create!(-- +
- -
novo_modelo.save+email_address: 'test@example.com',-- +
- -
+login: 'test',-- +
- -
if novo_modelo.persisted?+password_digest: 'password',-- +
- -
perguntas.each do |pergunta|+nome: 'nome',-- +
- -
nova_pergunta = pergunta.dup+matricula: '123456789'-- +
- -
nova_pergunta.modelo = novo_modelo+)-- +
- -
nova_pergunta.save+end-- +
- + + 1 -
end+let(:session_record) do-- +
- + + 8 -
end+Session.create!(-- +
- -
+user: user,-- +
- -
novo_modelo+user_agent: 'Test Agent',-- +
- -
end+ip_address: '127.0.0.1'-- +
- -
+)-- - - 1 +
- -
private+end-- +
- -
+-- +
- 1 -
def deve_ter_pelo_menos_uma_pergunta+before do-- +
- - 3 + 25 -
if perguntas.empty? || perguntas.all? { |p| p.marked_for_destruction? }+routes.draw do-- +
- - 3 + 25 -
errors.add(:base, "Um modelo deve ter pelo menos uma pergunta")+get 'index' => 'anonymous#index'-- +
- + + 25 -
end+get 'public_action' => 'anonymous#public_action'-- +
- + + 25 -
end+get 'new_session' => 'sessions#new', as: :new_session-- +
- + + 25 -
+root to: 'home#index'-- - - 1 +
- -
def nao_pode_remover_todas_perguntas+end-- +
- -
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")+# Reset Current.session before each test-- +
- + + 25 -
end+Current.session = nil-- +
- @@ -3637,862 +7365,877 @@
--- +
- -
end+
---
--- +
- 1 -
class Pergunta < ApplicationRecord+describe 'authentication flow' do-- +
- 1 -
self.table_name = 'perguntas' # Plural correto em português+context 'when user is not authenticated' do-- +
- + + 1 -
+it 'redirects to login page' do-- +
- + + 1 -
# Relacionamentos+get :index-- +
- 1 -
belongs_to :modelo+expect(response).to redirect_to(new_session_path)-- - - 1 +
- -
has_many :respostas, foreign_key: 'questao_id', dependent: :destroy+end-- +
- -
+-- +
- + + 1 -
# Tipos de perguntas disponíveis+it 'stores the return URL in session' do-- +
- + + 1 -
TIPOS = {+get :index-- +
- 1 -
'texto_longo' => 'Texto Longo',+expect(session[:return_to_after_authenticating]).to be_present-- +
- -
'texto_curto' => 'Texto Curto',+end-- +
- -
'multipla_escolha' => 'Múltipla Escolha',+end-- +
- -
'checkbox' => 'Checkbox (Múltipla Seleção)',+-- +
- + + 1 -
'escala' => 'Escala Likert (1-5)',+context 'when user is authenticated' do-- +
- + + 1 -
'data' => 'Data',+before do-- +
- + + 1 -
'hora' => 'Hora'+request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id-- +
- -
}.freeze+end-- +
- -
+-- +
- + + 1 -
# Validações+it 'allows access to protected actions' do-- +
- 1 -
validates :enunciado, presence: true+get :index-- +
- 1 -
validates :tipo, presence: true, inclusion: { in: TIPOS.keys }+expect(response).to have_http_status(:success)-- +
- + + 1 -
+expect(response.body).to eq('OK')-- +
- -
# Validações condicionais+end-- - - 1 +
- -
validate :opcoes_requeridas_para_multipla_escolha+end-- - - 1 +
- -
validate :opcoes_requeridas_para_checkbox+-- +
- + + 1 -
+context 'when session cookie is invalid' do-- +
- + + 1 -
# Callbacks+before do-- +
- 1 -
before_validation :definir_ordem_padrao, on: :create+request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = 'invalid-id'-- +
- -
+end-- +
- -
# Métodos+-- +
- 1 -
def tipo_humanizado+it 'redirects to login page' do-- +
- + + 1 -
TIPOS[tipo] || tipo+get :index-- +
- + + 1 -
end+expect(response).to redirect_to(new_session_path)-- +
- -
+end-- - - 1 +
- -
def requer_opcoes?+end-- +
- -
['multipla_escolha', 'checkbox'].include?(tipo)+-- +
- + + 1 -
end+context 'when session cookie is missing' do-- +
- + + 1 -
+it 'redirects to login page' do-- +
- 1 -
def lista_opcoes+get :index-- +
- + + 1 -
return [] unless opcoes.present?+expect(response).to redirect_to(new_session_path)-- +
- -
# Assume que opcoes é JSON array ou string separada por ;+end-- +
- -
if opcoes.is_a?(Array)+end-- +
- -
opcoes+end-- +
- -
elsif opcoes.is_a?(String)+-- +
- + + 1 -
begin+describe '.allow_unauthenticated_access' do-- +
- + + 1 -
JSON.parse(opcoes)+controller(ApplicationController) do-- +
- + + 1 -
rescue JSON::ParserError+include Authentication-- +
- -
opcoes.split(';').map(&:strip)+-- +
- + + 1 -
end+allow_unauthenticated_access only: [:public_action]-- +
- -
else+-- +
- + + 1 -
[]+def index-- +
- -
end+render plain: 'Protected'-- +
- -
end+end-- +
- -
+-- -- +
- 1 -
-private--- - - - - -
+def public_action-- +
- 1 -
def definir_ordem_padrao+render plain: 'Public'-- +
- -
if modelo.present?+end-- +
- -
ultima_ordem = modelo.perguntas.maximum(:id) || 0+end-- +
- -
# Ordem pode ser baseada no ID para simplificar+-- +
- + + 1 -
end+before do-- +
- + + 2 -
end+routes.draw do-- +
- + + 2 -
+get 'public_action' => 'anonymous#public_action'-- +
- - 1 + 2 -
def opcoes_requeridas_para_multipla_escolha+get 'index' => 'anonymous#index'-- +
- + + 2 -
if tipo == 'multipla_escolha'+get 'new_session' => 'sessions#new', as: :new_session-- +
- + + 2 -
opcoes_lista = lista_opcoes+root to: 'home#index'-- +
- -
if opcoes_lista.blank? || opcoes_lista.size < 2+end-- +
- -
errors.add(:opcoes, 'deve ter pelo menos duas opções para múltipla escolha')+end-- +
- -
end+-- +
- + + 1 -
end+it 'allows unauthenticated access to specified actions' do-- +
- + + 1 -
end+get :public_action-- +
- + + 1 -
+expect(response).to have_http_status(:success)-- +
- 1 -
def opcoes_requeridas_para_checkbox+expect(response.body).to eq('Public')-- +
- -
if tipo == 'checkbox'+end-- +
- -
opcoes_lista = lista_opcoes+-- +
- + + 1 -
if opcoes_lista.blank? || opcoes_lista.size < 2+it 'still requires authentication for other actions' do-- +
- + + 1 -
errors.add(:opcoes, 'deve ter pelo menos duas opções para checkbox')+get :index-- +
- + + 1 -
end+expect(response).to redirect_to(new_session_path)-- +
- @@ -4502,7 +8245,7 @@
-- +
- @@ -4512,700 +8255,619 @@
--- +
- -
end+
---
--- +
- 1 -
class Turma < ApplicationRecord+describe '#authenticated?' do-- +
- 1 -
has_many :avaliacoes+it 'returns session when it exists' do-- +
- 1 -
has_many :matricula_turmas+request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id-- - - 1 +
- -
has_many :users, through: :matricula_turmas+--- +
- + + 1 -
end+result = controller.send(:authenticated?)
---
--- +
- 1 -
require 'csv'+expect(result&.id).to eq(session_record.id)-- +
- -
+end-- - - 1 +
- -
class CsvFormatterService+-- +
- 1 -
def initialize(avaliacao)+it 'returns nil when session does not exist' do-- +
- 1 -
@avaliacao = avaliacao+expect(controller.send(:authenticated?)).to be_nil-- +
- -
end+end-- +
- -
+end-- - - 1 +
- -
def generate+-- +
- 1 -
CSV.generate(headers: true) do |csv|+describe '#resume_session' do-- +
- 1 -
csv << headers+context 'when Current.session is already set' do-- +
- + + 1 -
+before do-- +
- - 1 + 2 -
@avaliacao.submissoes.includes(:aluno, :respostas).each do |submissao|+Current.session = session_record-- - - 2 +
- -
aluno = submissao.aluno+end-- - - 2 +
- -
row = [aluno.matricula, aluno.nome]+-- +
- + + 1 -
+it 'returns the existing session' do-- +
- + + 1 -
# Organiza as respostas pela ordem das questões se possível, ou mapeamento simples+expect(controller.send(:resume_session)).to eq(session_record)-- +
- -
# Assumindo que queremos mapear questões para colunas+end-- +
- -
+-- +
- + + 1 -
# Para este MVP, vamos apenas despejar o conteúdo na ordem das questões encontradas+it 'does not query the database' do-- +
- + + 1 -
# Uma solução mais robusta ordenaria por ID da questão ou número+expect(Session).not_to receive(:find_by)-- +
- + + 1 -
+controller.send(:resume_session)-- - - 2 +
- -
submissao.respostas.each do |resposta|+end-- - - 3 +
- -
row << resposta.conteudo+end-- +
- -
end+-- +
- + + 1 -
+context 'when Current.session is not set' do-- +
- - 2 + 1 -
csv << row+it 'finds session by cookie and sets Current.session' do-- +
- + + 1 -
end+request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id-- +
- -
end+-- +
- + + 1 -
end+result = controller.send(:resume_session)-- +
- + + 1 -
+expect(result&.id).to eq(session_record.id)-- +
- 1 -
private+expect(Current.session&.id).to eq(session_record.id)-- +
- -
+end-- - - 1 +
- -
def headers+end-- +
- -
# Cabeçalhos estáticos para informações do Aluno+end-- - - 1 +
- -
base_headers = ["Matrícula", "Nome"]+-- +
- + + 1 -
+describe '#find_session_by_cookie' do-- +
- + + 1 -
# Cabeçalhos dinâmicos para questões+it 'finds session when valid cookie exists' do-- +
- + + 1 -
# Identificando questões únicas respondidas ou todas as questões do modelo+request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id-- +
- -
# Para o MVP, vamos assumir que queremos todas as questões do modelo+-- +
- + + 1 -
+result = controller.send(:find_session_by_cookie)-- +
- 1 -
questoes = @avaliacao.modelo.perguntas+expect(result&.id).to eq(session_record.id)-- - - 3 +
- -
question_headers = questoes.map.with_index { |q, i| "Questão #{i + 1}" }+end-- +
- -
+-- +
- 1 -
base_headers + question_headers+it 'returns nil when cookie does not exist' do-- +
- + + 1 -
end+expect(controller.send(:find_session_by_cookie)).to be_nil--- +
- -
end+end
---
--- - - 1 +
- -
require_relative "boot"+-- +
- + + 1 -
+it 'returns nil when session is not found' do-- +
- 1 -
require "rails/all"+request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = 'nonexistent'-- +
- @@ -5215,39 +8877,39 @@
-- +
- + + 1 -
# Require the gems listed in Gemfile, including any gems+expect(controller.send(:find_session_by_cookie)).to be_nil-- +
- -
# you've limited to :test, :development, or :production.+end-- - - 1 +
- -
Bundler.require(*Rails.groups)+end-- +
- @@ -5257,438 +8919,369 @@
-- +
- 1 -
module Camaar+describe '#after_authentication_url' do-- +
- 1 -
class Application < Rails::Application+it 'returns stored return URL if present' do-- +
- + + 1 -
# Initialize configuration defaults for originally generated Rails version.+session[:return_to_after_authenticating] = 'http://example.com/protected'-- +
- 1 -
config.load_defaults 8.0+expect(controller.send(:after_authentication_url)).to eq('http://example.com/protected')-- +
- + + 1 -
+expect(session[:return_to_after_authenticating]).to be_nil-- +
- -
# Please, add to the `ignore` list any other `lib` subdirectories that do+end-- +
- -
# not contain `.rb` files, or that should not be reloaded or eager loaded.+-- +
- + + 1 -
# Common ones are `templates`, `generators`, or `middleware`, for example.+it 'returns root URL if no return URL stored' do-- +
- 1 -
config.autoload_lib(ignore: %w[assets tasks])+expect(controller.send(:after_authentication_url)).to eq(root_url)-- +
- -
+end-- +
- -
# Configuration for the application, engines, and railties goes here.+end-- +
- -
#+-- +
- + + 1 -
# These settings can be overridden in specific environments using the files+describe '#start_new_session_for' do-- +
- + + 1 -
# in config/environments, which are processed later.+before do-- +
- + + 4 -
#+allow(request).to receive(:user_agent).and_return('Mozilla/5.0')-- +
- + + 4 -
# config.time_zone = "Central Time (US & Canada)"+allow(request).to receive(:remote_ip).and_return('192.168.1.1')-- +
- -
# config.eager_load_paths << Rails.root.join("extras")+end-- +
- -
end+--- +
- + + 1 -
end+it 'creates a new session with user agent and IP' do
---
--- +
- 1 -
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)+expect {-- +
- + + 1 -
+controller.send(:start_new_session_for, user)-- +
- - 1 + 2 -
require "bundler/setup" # Set up gems listed in the Gemfile.+}.to change { user.sessions.count }.by(1)--- - - 1 +
- -
require "bootsnap/setup" # Speed up boot time by caching expensive operations.+
---
--- +
- + + 1 -
# Load the Rails application.+new_session = user.sessions.last-- +
- 1 -
require_relative "application"+expect(new_session.user_agent).to eq('Mozilla/5.0')-- +
- + + 1 -
+expect(new_session.ip_address).to eq('192.168.1.1')-- +
- -
# Initialize the Rails application.+end--- - - 1 +
- -
Rails.application.initialize!+
---
--- +
- + + 1 -
# The test environment is used exclusively to run your application's+it 'sets Current.session' do-- +
- + + 1 -
# test suite. You never need to work with it otherwise. Remember that+new_session = controller.send(:start_new_session_for, user)-- +
- + + 1 -
# your test database is "scratch space" for the test suite and is wiped+expect(Current.session).to eq(new_session)-- +
- -
# and recreated between test runs. Don't rely on the data there!+end-- +
- @@ -5698,73 +9291,73 @@
-- +
- 1 -
Rails.application.configure do+it 'sets signed permanent cookie' do-- +
- + + 1 -
# Configure 'rails notes' to inspect Cucumber files+new_session = controller.send(:start_new_session_for, user)-- - - 1 +
- -
config.annotations.register_directories("features")+-- - - 1 +
- -
config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }+# Check that the cookie was set with the session id-- +
- + + 1 -
+expect(controller.send(:cookies).signed[:session_id]).to eq(new_session.id)-- +
- -
# Settings specified here will take precedence over those in config/application.rb.+end-- +
- @@ -5774,199 +9367,211 @@
-- +
- + + 1 -
# While tests run files are not watched, reloading is not necessary.+it 'returns the created session' do-- +
- 1 -
config.enable_reloading = false+result = controller.send(:start_new_session_for, user)-- +
- + + 1 -
+expect(result).to be_a(Session)-- +
- + + 1 -
# Eager loading loads your entire application. When running a single test locally,+expect(result.user).to eq(user)-- +
- -
# this is usually not necessary, and can slow down your test suite. However, it's+end-- +
- -
# recommended that you enable it in continuous integration systems to ensure eager+end-- +
- -
# loading is working properly before deploying your code.+-- +
- 1 -
config.eager_load = ENV["CI"].present?+describe '#terminate_session' do-- +
- + + 1 -
+before do-- +
- + + 2 -
# Configure public file server for tests with cache-control for performance.+Current.session = session_record-- +
- - 1 + 2 -
config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }+request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id-- +
- -
+end-- +
- -
# Show full error reports.+-- +
- 1 -
config.consider_all_requests_local = true+it 'destroys the current session' do-- +
- 1 -
config.cache_store = :null_store+expect {-- +
- + + 1 -
+controller.send(:terminate_session)-- +
- + + 2 -
# Render exception templates for rescuable exceptions and raise for other exceptions.+}.to change { Session.exists?(session_record.id) }.from(true).to(false)-- - - 1 +
- -
config.action_dispatch.show_exceptions = :rescuable+end-- +
- @@ -5976,145 +9581,149 @@
-- +
- + + 1 -
# Disable request forgery protection in test environment.+it 'deletes the session cookie' do-- +
- 1 -
config.action_controller.allow_forgery_protection = false+controller.send(:terminate_session)-- +
- -
+-- +
- -
# Store uploaded files on the local file system in a temporary directory.+# Check that delete was called or cookie is no longer signed-- +
- 1 -
config.active_storage.service = :test+expect(controller.send(:cookies).signed[:session_id]).to be_nil-- +
- -
+end-- +
- -
# Tell Action Mailer not to deliver emails to the real world.+end-- +
- -
# The :test delivery method accumulates sent emails in the+-- +
- + + 1 -
# ActionMailer::Base.deliveries array.+describe 'helper methods' do-- +
- 1 -
config.action_mailer.delivery_method = :test+it 'makes authenticated? available as helper method' do-- +
- + + 1 -
+expect(controller.class._helper_methods).to include(:authenticated?)-- +
- -
# Set host to be used by links generated in mailer templates.+end-- - - 1 +
- -
config.action_mailer.default_url_options = { host: "example.com" }+end-- +
- @@ -6124,172 +9733,165 @@
-- +
- + + 1 -
# Print deprecation notices to the stderr.+describe 'integration scenario' do-- +
- 1 -
config.active_support.deprecation = :stderr+it 'handles complete authentication flow' do-- +
- -
+# Attempt to access protected resource-- +
- + + 1 -
# Raises error for missing translations.+get :index-- +
- + + 1 -
# config.i18n.raise_on_missing_translations = true+expect(response).to redirect_to(new_session_path)-- +
- + + 1 -
+stored_url = session[:return_to_after_authenticating]-- +
- + + 1 -
# Annotate rendered view with file names.+expect(stored_url).to be_present-- +
- -
# config.action_view.annotate_rendered_view_with_filenames = true+-- +
- -
+# Simulate login-- +
- + + 1 -
# Raise error when a before_action's only/except options reference missing actions.+new_session = controller.send(:start_new_session_for, user)-- - - 1 +
- -
config.action_controller.raise_on_missing_callback_actions = true+--- +
- -
end+# Verify session was created
-+
expect(new_session).to be_persisted+ +
# Be sure to restart your server when you modify this file.
+ expect(Current.session).to eq(new_session)
# Version of your assets, change this if you want to expire all your assets.
+ # Verify cookie was set
Rails.application.config.assets.version = "1.0"
+ expect(controller.send(:cookies).signed[:session_id]).to eq(new_session.id)
# Add additional assets to the asset load path.
+ # Access protected resource again (simulate new request with cookie)
# Rails.application.config.assets.paths << Emoji.images_path
+ Current.session = nil
-+
request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = new_session.id+ +
# Be sure to restart your server when you modify this file.
+ get :index
+ expect(response).to have_http_status(:success)
# Define an application-wide content security policy.
+
# See the Securing Rails Applications Guide for more information:
+ # Simulate logout
# https://guides.rubyonrails.org/security.html#content-security-policy-header
+ Current.session = new_session
+ controller.send(:terminate_session)
# Rails.application.configure do
+ expect(controller.send(:cookies).signed[:session_id]).to be_nil
# config.content_security_policy do |policy|
+ expect(Session.exists?(new_session.id)).to be false
# policy.default_src :self, :https
+ end
# policy.font_src :self, :https, :data
+ end
# policy.img_src :self, :https, :data
+ end
# policy.object_src :none
+
# policy.script_src :self, :https
+ # require 'rails_helper'
# policy.style_src :self, :https
+
# # Specify URI for violation reports
+ # RSpec.describe Authentication, type: :controller do
# # policy.report_uri "/csp-violation-report-endpoint"
+ # controller(ApplicationController) do
# end
+ # include Authentication
#
+
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
+ # def index
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
+ # render plain: 'OK'
# config.content_security_policy_nonce_directives = %w(script-src style-src)
+ # end
#
+
# # Report violations without enforcing the policy.
+ # def public_action
# # config.content_security_policy_report_only = true
+ # render plain: 'Public'
# end
+ # end
---
--- +
- -
# Be sure to restart your server when you modify this file.+# end-- +
- @@ -6683,1016 +10249,917 @@
-- +
- -
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.+# let(:user) do-- +
- -
# Use this to limit dissemination of sensitive information.+# User.create!(-- +
- -
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.+# email_address: 'test@example.com',-- - - 1 +
- -
Rails.application.config.filter_parameters += [+# login: 'test',-- +
- -
:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc+# password_digest: 'password',--- +
- -
]+# nome: 'nome',
---
--- +
- -
# Be sure to restart your server when you modify this file.+# matricula: '123456789'-- +
- -
+# )-- +
- -
# Add new inflection rules using the following format. Inflections+# end-- +
- -
# are locale specific, and you may define rules for as many different+# let(:session_record) do-- +
- -
# locales as you wish. All of these examples are active by default:+# Session.create!(-- +
- -
# ActiveSupport::Inflector.inflections(:en) do |inflect|+# user: user,-- +
- -
# inflect.plural /^(ox)$/i, "\\1en"+# user_agent: 'Test Agent',-- +
- -
# inflect.singular /^(ox)en/i, "\\1"+# ip_address: '127.0.0.1'-- +
- -
# inflect.irregular "person", "people"+# )-- +
- -
# inflect.uncountable %w( fish sheep )+# end-- +
- -
# end+-- +
- -
+# before do-- +
- -
# These inflection rules are supported but not enabled by default:+# routes.draw do-- - - 1 +
- -
ActiveSupport::Inflector.inflections(:en) do |inflect|+# get 'index' => 'anonymous#index'-- - - 1 +
- -
inflect.irregular "pergunta", "perguntas"+# get 'public_action' => 'anonymous#public_action'-- +
- -
# inflect.acronym "RESTful"+# get 'new_session' => 'sessions#new', as: :new_session--- +
- -
end+# root to: 'home#index'
---
--- - - 1 +
- -
ActiveSupport::Inflector.inflections(:en) do |inflect|+# end-- - - 1 +
- -
inflect.irregular 'avaliacao', 'avaliacoes'+# end--- +
- -
end+
-+
# describe 'authentication flow' do+ +
# config/initializers/mail.rb
+ # context 'when user is not authenticated' do
# Configuração de email para CAMAAR
+ # it 'redirects to login page' do
+ # get :index
if Rails.env.test?
+ # expect(response).to redirect_to(new_session_path)
# Em testes: captura emails sem enviar
+ # end
Rails.application.config.action_mailer.delivery_method = :test
+
Rails.application.config.action_mailer.default_url_options = {
+ # it 'stores the return URL in session' do
host: 'localhost',
+ # get :index
port: 3000
+ # expect(session[:return_to_after_authenticating]).to eq(request.url)
}
+ # end
+ # end
elsif Rails.env.development?
+
# MVP: Usa letter_opener (emails abrem no navegador)
+ # context 'when user is authenticated' do
# Instale: gem install letter_opener ou adicione ao Gemfile
+ # before do
# PRODUÇÃO: Para enviar emails reais, descomente a seção SMTP abaixo
+ # cookies.signed[:session_id] = session_record.id
+ # allow(Session).to receive(:find_by).with(id: session_record.id).and_return(session_record)
Rails.application.config.action_mailer.delivery_method = :letter_opener
+ # end
Rails.application.config.action_mailer.perform_deliveries = true
+
+ # it 'allows access to protected actions' do
# === SMTP (Para Produção) ===
+ # get :index
# Descomente as linhas abaixo e configure variáveis de ambiente (.env)
+ # expect(response).to have_http_status(:success)
# para enviar emails reais via SMTP (Gmail, Sendgrid, etc.)
+ # expect(response.body).to eq('OK')
#
+ # end
# Rails.application.config.action_mailer.delivery_method = :smtp
+
# Rails.application.config.action_mailer.raise_delivery_errors = true
+ # it 'sets Current.session' do
#
+ # get :index
# Rails.application.config.action_mailer.smtp_settings = {
+ # expect(Current.session).to eq(session_record)
# address: ENV.fetch('SMTP_ADDRESS', 'smtp.gmail.com'),
+ # end
# port: ENV.fetch('SMTP_PORT', '587').to_i,
+ # end
# domain: ENV.fetch('SMTP_DOMAIN', 'localhost'),
+
# user_name: ENV['SMTP_USER'],
+ # context 'when session cookie is invalid' do
# password: ENV['SMTP_PASSWORD'],
+ # before do
# authentication: 'plain',
+ # cookies.signed[:session_id] = 'invalid-id'
# enable_starttls_auto: true
+ # allow(Session).to receive(:find_by).with(id: 'invalid-id').and_return(nil)
# }
+ # end
+
Rails.application.config.action_mailer.default_url_options = {
+ # it 'redirects to login page' do
host: ENV.fetch('APP_HOST', 'localhost'),
+ # get :index
port: ENV.fetch('APP_PORT', '3000').to_i
+ # expect(response).to redirect_to(new_session_path)
}
+ # end
+ # end
else
+
# Produção: SMTP obrigatório
+ # context 'when session cookie is missing' do
Rails.application.config.action_mailer.delivery_method = :smtp
+ # it 'redirects to login page' do
Rails.application.config.action_mailer.perform_deliveries = true
+ # get :index
Rails.application.config.action_mailer.raise_delivery_errors = false
+ # expect(response).to redirect_to(new_session_path)
+ # end
Rails.application.config.action_mailer.smtp_settings = {
+ # end
address: ENV.fetch('SMTP_ADDRESS'),
+ # end
port: ENV.fetch('SMTP_PORT', '587').to_i,
+
domain: ENV.fetch('SMTP_DOMAIN'),
+ # describe '.allow_unauthenticated_access' do
user_name: ENV.fetch('SMTP_USER'),
+ # controller(ApplicationController) do
password: ENV.fetch('SMTP_PASSWORD'),
+ # include Authentication
authentication: 'plain',
+
enable_starttls_auto: true,
+ # allow_unauthenticated_access only: [:public_action]
open_timeout: 10,
+
read_timeout: 10
+ # def index
}
+ # render plain: 'Protected'
+ # end
Rails.application.config.action_mailer.default_url_options = {
+
host: ENV.fetch('APP_HOST'),
+ # def public_action
protocol: 'https'
+ # render plain: 'Public'
}
+ # end
end
+ # end
---
--- - - 1 +
- -
Rails.application.routes.draw do+# it 'allows unauthenticated access to specified actions' do-- +
- -
# --- ROTAS DE AVALIACOES ---+# get :public_action-- - - 1 +
- -
resources :avaliacoes, only: [:index, :create] do+# expect(response).to have_http_status(:success)-- - - 1 +
- -
collection do+# expect(response.body).to eq('Public')-- - - 1 +
- -
get :gestao_envios+# end-- +
- -
end+-- - - 1 +
- -
member do+# it 'still requires authentication for other actions' do-- - - 1 +
- -
get :resultados+# get :index-- +
- -
end+# expect(response).to redirect_to(new_session_path)-- +
- -
# Rotas para alunos responderem avaliações (Feature 99)+# end-- - - 1 +
- -
resources :respostas, only: [:new, :create]+# end-- +
- -
end+-- +
- -
+# describe '#authenticated?' do-- +
- -
# --- ROTAS DE IMPORTAÇÃO SIGAA ---+# it 'returns true when session exists' do-- - - 1 +
- -
resources :sigaa_imports, only: [:new, :create] do+# cookies.signed[:session_id] = session_record.id-- - - 1 +
- -
collection do+# allow(Session).to receive(:find_by).with(id: session_record.id).and_return(session_record)-- - - 1 +
- -
post :update # For update/sync operations+-- - - 1 +
- -
get :success # For showing import results+# expect(controller.send(:authenticated?)).to eq(session_record)-- +
- -
end+# end-- +
- -
end+-- +
- -
+# it 'returns nil when session does not exist' do-- +
- -
# --- ROTAS DE GERENCIAMENTO DE MODELOS ---+# expect(controller.send(:authenticated?)).to be_nil-- - - 1 +
- -
resources :modelos do+# end-- - - 1 +
- -
member do+# end-- - - 1 +
- -
post :clone+-- +
- -
end+# describe '#resume_session' do-- +
- -
end+# context 'when Current.session is already set' do-- +
- -
+# before do-- - - 1 +
- -
resource :session+# Current.session = session_record-- - - 1 +
- -
resources :passwords, param: :token+# end-- - - 1 +
- -
get "home/index"+-- +
- -
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html+# it 'returns the existing session' do-- +
- -
+# expect(controller.send(:resume_session)).to eq(session_record)-- +
- -
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.+# end-- +
- -
# Can be used by load balancers and uptime monitors to verify that the app is live.+-- - - 1 +
- -
get "up" => "rails/health#show", as: :rails_health_check+# it 'does not query the database' do-- +
- -
+# expect(Session).not_to receive(:find_by)-- +
- -
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)+# controller.send(:resume_session)-- +
- -
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest+# end-- +
- -
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker+# end-- +
- @@ -8179,278 +11579,237 @@
-- +
- -
# --- ROTAS DO INTEGRANTE 4 (RESPOSTAS) ---+# context 'when Current.session is not set' do-- +
- -
# Define as rotas aninhadas para criar respostas dentro de um formulário+# it 'finds session by cookie and sets Current.session' do-- - - 1 +
- -
resources :formularios, only: [] do+# cookies.signed[:session_id] = session_record.id-- - - 1 +
- -
resources :respostas, only: [ :index, :new, :create ]+# allow(Session).to receive(:find_by).with(id: session_record.id).and_return(session_record)-- +
- -
end+-- +
- -
+# result = controller.send(:resume_session)-- +
- -
# Rota solta para a listagem geral de respostas (dashboard do aluno)+# expect(result).to eq(session_record)-- - - 1 +
- -
get "respostas", to: "respostas#index"+# expect(Current.session).to eq(session_record)-- +
- -
# -----------------------------------------+# end-- +
- -
+# end-- +
- -
# Defines the root path route ("/")+# end-- - - 1 +
- -
root "pages#index"+-- +
- -
+# describe '#find_session_by_cookie' do-- - - 1 +
- -
get "home" => "home#index"+# it 'finds session when valid cookie exists' do--- +
- -
end+# cookies.signed[:session_id] = session_record.id
-+
# allow(Session).to receive(:find_by).with(id: session_record.id).and_return(session_record)+ +
require 'rails_helper'
+
+ # expect(controller.send(:find_session_by_cookie)).to eq(session_record)
RSpec.describe Avaliacao, type: :model do
+ # end
describe 'associações' do
+
it 'pertence a turma' do
+ # it 'returns nil when cookie does not exist' do
expect(described_class.reflect_on_association(:turma).macro).to eq :belongs_to
+ # expect(controller.send(:find_session_by_cookie)).to be_nil
end
+ # end
it 'pertence a modelo' do
+ # it 'returns nil when session is not found' do
expect(described_class.reflect_on_association(:modelo).macro).to eq :belongs_to
+ # cookies.signed[:session_id] = 'nonexistent'
end
+ # allow(Session).to receive(:find_by).with(id: 'nonexistent').and_return(nil)
it 'pertence a professor_alvo como Usuario (opcional)' do
+ # expect(controller.send(:find_session_by_cookie)).to be_nil
assoc = described_class.reflect_on_association(:professor_alvo)
+ # end
expect(assoc.macro).to eq :belongs_to
+ # end
expect(assoc.class_name).to eq 'User'
+
expect(assoc.options[:optional]).to eq true
+ # describe '#after_authentication_url' do
end
+ # it 'returns stored return URL if present' do
end
+ # session[:return_to_after_authenticating] = 'http://example.com/protected'
end
+ # expect(controller.send(:after_authentication_url)).to eq('http://example.com/protected')
-+
# expect(session[:return_to_after_authenticating]).to be_nil+ +
# This file is copied to spec/ when you run 'rails generate rspec:install'
+ # end
require 'spec_helper'
+
ENV['RAILS_ENV'] ||= 'test'
+ # it 'returns root URL if no return URL stored' do
require_relative '../config/environment'
+ # expect(controller.send(:after_authentication_url)).to eq(root_url)
# Prevent database truncation if the environment is production
+ # end
abort("The Rails environment is running in production mode!") if Rails.env.production?
+ # end
# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file
+
# that will avoid rails generators crashing because migrations haven't been run yet
+ # describe '#start_new_session_for' do
# return unless Rails.env.test?
+ # let(:new_session) { instance_double(Session, id: 123) }
require 'rspec/rails'
+
# Add additional requires below this line. Rails is not loaded until this point!
+ # before do
+ # allow(user.sessions).to receive(:create!).and_return(new_session)
# Requires supporting ruby files with custom matchers and macros, etc, in
+ # allow(request).to receive(:user_agent).and_return('Mozilla/5.0')
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+ # allow(request).to receive(:remote_ip).and_return('192.168.1.1')
# run as spec files by default. This means that files in spec/support that end
+ # end
# in _spec.rb will both be required and run as specs, causing the specs to be
+
# run twice. It is recommended that you do not name files matching this glob to
+ # it 'creates a new session with user agent and IP' do
# end with _spec.rb. You can configure this pattern with the --pattern
+ # expect(user.sessions).to receive(:create!).with(
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+ # user_agent: 'Mozilla/5.0',
#
+ # ip_address: '192.168.1.1'
# The following line is provided for convenience purposes. It has the downside
+ # ).and_return(new_session)
# of increasing the boot-up time by auto-requiring all files in the support
+
# directory. Alternatively, in the individual `*_spec.rb` files, manually
+ # controller.send(:start_new_session_for, user)
# require only the support files necessary.
+ # end
#
+
# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
+ # it 'sets Current.session' do
+ # controller.send(:start_new_session_for, user)
# Ensures that the test database schema matches the current schema file.
+ # expect(Current.session).to eq(new_session)
# If there are pending migrations it will invoke `db:test:prepare` to
+ # end
# recreate the test database by loading the schema.
+
# If you are not using ActiveRecord, you can remove these lines.
+ # it 'sets signed permanent cookie with httponly and same_site options' do
begin
+ # controller.send(:start_new_session_for, user)
ActiveRecord::Migration.maintain_test_schema!
+
rescue ActiveRecord::PendingMigrationError => e
+ # expect(cookies.signed.permanent[:session_id]).to eq(
abort e.to_s.strip
+ # { value: 123, httponly: true, same_site: :lax }
end
+ # )
RSpec.configure do |config|
+ # end
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+
config.fixture_paths = [
+ # it 'returns the created session' do
Rails.root.join('spec/fixtures')
+ # result = controller.send(:start_new_session_for, user)
]
+ # expect(result).to eq(new_session)
+ # end
# If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # end
# examples within a transaction, remove the following line or assign false
+
# instead of true.
+ # describe '#terminate_session' do
config.use_transactional_fixtures = true
+ # before do
+ # Current.session = session_record
# You can uncomment this line to turn off ActiveRecord support entirely.
+ # cookies.signed[:session_id] = session_record.id
# config.use_active_record = false
+ # end
# RSpec Rails uses metadata to mix in different behaviours to your tests,
+ # it 'destroys the current session' do
# for example enabling you to call `get` and `post` in request specs. e.g.:
+ # expect(session_record).to receive(:destroy)
#
+ # controller.send(:terminate_session)
# RSpec.describe UsersController, type: :request do
+ # end
# # ...
+
# end
+ # it 'deletes the session cookie' do
#
+ # allow(session_record).to receive(:destroy)
# The different available types are documented in the features, such as in
+ # controller.send(:terminate_session)
# https://rspec.info/features/8-0/rspec-rails
+
#
+ # expect(cookies[:session_id]).to be_nil
# You can also this infer these behaviours automatically by location, e.g.
+ # end
# /spec/models would pull in the same behaviour as `type: :model` but this
+ # end
# behaviour is considered legacy and will be removed in a future version.
+
#
+ # describe 'helper methods' do
# To enable this behaviour uncomment the line below.
+ # it 'makes authenticated? available as helper method' do
# config.infer_spec_type_from_file_location!
+ # expect(controller.class._helper_methods).to include(:authenticated?)
+ # end
# Filter lines from Rails gems in backtraces.
+ # end
config.filter_rails_from_backtrace!
+
# arbitrary gems may also be filtered via:
+ # describe 'integration scenario' do
# config.filter_gems_from_backtrace("gem name")
+ # it 'handles complete authentication flow' do
end
+ # # Attempt to access protected resource
--
-- - - 1 +
- -
require 'rails_helper'+# get :index-- +
- -
+# expect(response).to redirect_to(new_session_path)-- - - 1 +
- -
RSpec.describe "Avaliações", type: :request do+# stored_url = session[:return_to_after_authenticating]-- - - 1 +
- -
describe "GET /gestao_envios" do+-- - - 1 +
- -
it "retorna sucesso HTTP" do+# # Simulate login-- - - 1 +
- -
get gestao_envios_avaliacoes_path+# controller.send(:start_new_session_for, user)-- - - 1 +
- -
expect(response).to have_http_status(:success)+-- +
- -
end+# # Verify cookie was set-- +
- -
end+# expect(cookies.signed[:session_id]).to be_present-- +
- @@ -9508,600 +12769,574 @@
-- - - 1 +
- -
describe "POST /create" do+# # Access protected resource again-- - - 4 +
- -
let!(:turma) { Turma.create!(codigo: "CIC001", nome: "Turma de Teste", semestre: "2024.1") }+# allow(Session).to receive(:find_by).and_return(Current.session)-- - - 4 +
- -
let!(:template) { Modelo.create!(titulo: "Template Padrão", ativo: true) }+# get :index-- +
- -
+# expect(response).to have_http_status(:success)-- - - 1 +
- -
context "com entradas válidas" do+-- - - 1 +
- -
it "cria uma nova Avaliação vinculada ao template padrão" do+# # Simulate logout-- +
- -
expect {+# controller.send(:terminate_session)-- +
- -
post avaliacoes_path, params: { turma_id: turma.id }+# expect(cookies[:session_id]).to be_nil-- +
- -
}.to change(Avaliacao, :count).by(1)+# end-- +
- -
+# end--- +
- -
avaliacao = Avaliacao.last+# end-- + +- - + + +
++ +++ lines covered + + + + +spec/rails_helper.rb
++ + 90.91% + -
expect(avaliacao.turma).to eq(turma)- -+ 11 relevant lines. + 10 lines covered and + 1 lines missed. ++ + + ++--
-- +
- -
expect(avaliacao.modelo).to eq(template)+# This file is copied to spec/ when you run 'rails generate rspec:install'-- +
- + + 1 -
expect(response).to redirect_to(gestao_envios_avaliacoes_path)+require 'spec_helper'-- +
- + + 1 -
expect(flash[:notice]).to be_present+ENV['RAILS_ENV'] ||= 'test'-- +
- + + 1 -
end+require_relative '../config/environment'-- +
- -
+# Prevent database truncation if the environment is production-- +
- 1 -
it "aceita uma data_fim personalizada" do+abort("The Rails environment is running in production mode!") if Rails.env.production?-- +
- -
data_personalizada = 2.weeks.from_now.to_date+# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file-- +
- -
post avaliacoes_path, params: { turma_id: turma.id, data_fim: data_personalizada }+# that will avoid rails generators crashing because migrations haven't been run yet-- +
- -
+# return unless Rails.env.test?-- +
- + + 1 -
avaliacao = Avaliacao.last+require 'rspec/rails'-- +
- -
expect(avaliacao.data_fim.to_date).to eq(data_personalizada)+# Add additional requires below this line. Rails is not loaded until this point!-- +
- -
end+-- +
- -
end+# Requires supporting ruby files with custom matchers and macros, etc, in-- +
- -
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are-- - - 1 +
- -
context "quando o template padrão está ausente" do+# run as spec files by default. This means that files in spec/support that end-- - - 1 +
- -
before { template.update!(titulo: "Outro") }+# in _spec.rb will both be required and run as specs, causing the specs to be-- +
- -
+# run twice. It is recommended that you do not name files matching this glob to-- - - 1 +
- -
it "não cria avaliação e redireciona com alerta" do+# end with _spec.rb. You can configure this pattern with the --pattern-- +
- -
expect {+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.-- +
- -
post avaliacoes_path, params: { turma_id: turma.id }+#-- +
- -
}.not_to change(Avaliacao, :count)+# The following line is provided for convenience purposes. It has the downside-- +
- -
+# of increasing the boot-up time by auto-requiring all files in the support-- +
- -
expect(response).to redirect_to(gestao_envios_avaliacoes_path)+# directory. Alternatively, in the individual `*_spec.rb` files, manually-- +
- -
expect(flash[:alert]).to include("Template Padrão não encontrado")+# require only the support files necessary.-- +
- -
end+#-- +
- -
end+# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }-- +
- -
end+--- +
- -
end+# Ensures that the test database schema matches the current schema file.
--
-- - - 1 +
- -
require 'rails_helper'+# If there are pending migrations it will invoke `db:test:prepare` to-- +
- -
+# recreate the test database by loading the schema.-- - - 1 +
- -
RSpec.describe CsvFormatterService do+# If you are not using ActiveRecord, you can remove these lines.-- - - 1 +
- -
describe '#generate' do+begin-- +
- - 2 + 1 -
let(:modelo) { double('Modelo', perguntas: [+ActiveRecord::Migration.maintain_test_schema!-- +
- -
double('Pergunta', enunciado: 'Q1'),+rescue ActiveRecord::PendingMigrationError => e-- +
- -
double('Pergunta', enunciado: 'Q2')+abort e.to_s.strip-- +
- -
])}+end-- +
- + + 1 -
+RSpec.configure do |config|-- - - 2 +
- -
let(:avaliacao) { double('Avaliacao', id: 1, modelo: modelo) }+# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures-- +
- + + 1 -
+config.fixture_paths = [-- - - 2 +
- -
let(:aluno1) { double('User', matricula: '123', nome: 'Alice') }+Rails.root.join('spec/fixtures')-- - - 2 +
- -
let(:aluno2) { double('User', matricula: '456', nome: 'Bob') }+]-- +
- @@ -10111,53 +13346,49 @@
-- +
- -
# Respostas não tem mais aluno direto, mas através de submissão+# If you're not using ActiveRecord, or you'd prefer not to run each of your-- - - 2 +
- -
let(:resposta_a1_q1) { double('Resposta', conteudo: 'Ans 1A') }+# examples within a transaction, remove the following line or assign false-- - - 2 +
- -
let(:resposta_a1_q2) { double('Resposta', conteudo: 'Ans 1B') }+# instead of true.-- +
- - 2 + 1 -
let(:resposta_a2_q1) { double('Resposta', conteudo: 'Ans 2A') }+config.use_transactional_fixtures = true-- +
- @@ -10167,267 +13398,249 @@
-- +
- -
# Submissoes ligando aluno e respostas+# You can uncomment this line to turn off ActiveRecord support entirely.-- - - 2 +
- -
let(:submissao1) { double('Submissao', aluno: aluno1, respostas: [resposta_a1_q1, resposta_a1_q2]) }+# config.use_active_record = false-- - - 2 +
- -
let(:submissao2) { double('Submissao', aluno: aluno2, respostas: [resposta_a2_q1]) }+-- +
- -
+# RSpec Rails uses metadata to mix in different behaviours to your tests,-- - - 1 +
- -
before do+# for example enabling you to call `get` and `post` in request specs. e.g.:-- +
- -
# Mock da cadeia: avaliacao.submissoes.includes.each+#-- +
- -
# Simulando o comportamento do loop no service+# RSpec.describe UsersController, type: :request do-- - - 1 +
- -
allow(avaliacao).to receive_message_chain(:submissoes, :includes).and_return([submissao1, submissao2])+# # ...-- +
- -
end+# end-- +
- -
+#-- - - 1 +
- -
it 'gera uma string CSV válida com cabeçalhos e linhas' do+# The different available types are documented in the features, such as in-- - - 1 +
- -
csv_string = described_class.new(avaliacao).generate+# https://rspec.info/features/8-0/rspec-rails-- - - 1 +
- -
rows = csv_string.split("\n")+#-- +
- -
+# You can also this infer these behaviours automatically by location, e.g.-- +
- -
# Cabeçalhos: Matrícula, Nome, Questão 1, Questão 2+# /spec/models would pull in the same behaviour as `type: :model` but this-- - - 1 +
- -
expect(rows[0]).to include("Matrícula,Nome,Questão 1,Questão 2")+# behaviour is considered legacy and will be removed in a future version.-- +
- -
+#-- +
- -
# Linha 1: Respostas da Alice+# To enable this behaviour uncomment the line below.-- - - 1 +
- -
expect(rows[1]).to include("123,Alice,Ans 1A,Ans 1B")+# config.infer_spec_type_from_file_location!-- +
- -
+-- +
- -
# Linha 2: Respostas do Bob+# Filter lines from Rails gems in backtraces.-- +
- 1 -
expect(rows[2]).to include("456,Bob,Ans 2A")+config.filter_rails_from_backtrace!-- +
- -
end+# arbitrary gems may also be filtered via:-- +
- -
end+# config.filter_gems_from_backtrace("gem name")-- +
- diff --git a/db/migrate/20251208012954_drop_usuarios.rb b/db/migrate/20251208012954_drop_usuarios.rb index e39280fac8..e219d28979 100644 --- a/db/migrate/20251208012954_drop_usuarios.rb +++ b/db/migrate/20251208012954_drop_usuarios.rb @@ -11,4 +11,4 @@ def change t.timestamps end end -end \ No newline at end of file +end diff --git a/db/seeds.rb b/db/seeds.rb index d84a4cea0b..55f8649382 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -21,7 +21,7 @@ # Criar perguntas apenas se o modelo for novo ou não tiver perguntas if modelo.new_record? || modelo.perguntas.empty? modelo.perguntas.destroy_all if modelo.persisted? # Limpar perguntas antigas se existir - + modelo.perguntas.build([ { enunciado: 'O professor demonstrou domínio do conteúdo?', @@ -41,7 +41,7 @@ { enunciado: 'Você recomendaria esta disciplina?', tipo: 'multipla_escolha', - opcoes: ['Sim', 'Não', 'Talvez'] + opcoes: [ 'Sim', 'Não', 'Talvez' ] }, { enunciado: 'Comentários adicionais (opcional):', diff --git a/features/step_definitions/cria_avaliacao_steps.rb b/features/step_definitions/cria_avaliacao_steps.rb index 65e157ef81..fe70427afb 100644 --- a/features/step_definitions/cria_avaliacao_steps.rb +++ b/features/step_definitions/cria_avaliacao_steps.rb @@ -43,7 +43,7 @@ row = find("tr", text: @turma.codigo) within(row) do # Esvaziar o campo de data pode acionar validação do navegador ou erro no backend - fill_in "data_fim", with: "" + fill_in "data_fim", with: "" click_button "Gerar Avaliação" end end diff --git a/features/step_definitions/email_steps.rb b/features/step_definitions/email_steps.rb index 12a61f354e..2bcc9a61e0 100644 --- a/features/step_definitions/email_steps.rb +++ b/features/step_definitions/email_steps.rb @@ -9,7 +9,7 @@ Então('um email de boas-vindas deve ser enviado para {string}') do |email_address| emails = emails_sent_to(email_address) expect(emails).not_to be_empty, "Nenhum email foi enviado para #{email_address}" - + email = emails.last expect(email.subject).to include('Bem-vindo') end @@ -17,7 +17,7 @@ Então('o email deve conter a senha temporária') do email = last_email expect(email).not_to be_nil, "Nenhum email foi enviado" - + # Verifica se o email contém uma senha (padrão hex de 16 caracteres) body = email.body.to_s expect(body).to match(/[a-f0-9]{16}/i), "Email não contém senha temporária" @@ -26,7 +26,7 @@ Então('o email deve conter {string}') do |texto| email = last_email expect(email).not_to be_nil, "Nenhum email foi enviado" - + body = email.body.to_s expect(body).to include(texto), "Email não contém o texto esperado: #{texto}" end @@ -38,12 +38,12 @@ end Então('{int} email(s) deve(m) ter sido enviado(s)') do |count| - expect(all_emails.count).to eq(count), + expect(all_emails.count).to eq(count), "Esperava #{count} emails, mas #{all_emails.count} foram enviados" end Então('nenhum email deve ter sido enviado') do - expect(all_emails).to be_empty, + expect(all_emails).to be_empty, "Esperava nenhum email, mas #{all_emails.count} foram enviados" end @@ -55,7 +55,7 @@ puts "Para: #{email.to.join(', ')}" puts "Assunto: #{email.subject}" puts "Corpo:" - puts email.body.to_s + puts email.body puts "==========================================" else puts "Nenhum email foi enviado ainda" diff --git a/features/step_definitions/importa_dados_sigaa_steps.rb b/features/step_definitions/importa_dados_sigaa_steps.rb index d240e5a26e..a66d5e1e00 100644 --- a/features/step_definitions/importa_dados_sigaa_steps.rb +++ b/features/step_definitions/importa_dados_sigaa_steps.rb @@ -27,7 +27,7 @@ # Visita a página de importação visit new_sigaa_import_path - + # Faz upload do arquivo attach_file('file', @temp_file_path) click_button 'Importar Dados' @@ -70,7 +70,7 @@ # Feature 100 - Cadastro de Usuários Quando('importo um arquivo de dados do SIGAA contendo novos usuários') do @initial_user_count = User.count - + sample_data = [ { "codigo" => "NEW001", @@ -107,7 +107,7 @@ new_user = User.find_by(matricula: "888888") expect(new_user).to be_present expect(new_user.password_digest).to be_present - + # Verifica que email foi enviado expect(last_email).not_to be_nil, "Nenhum email foi enviado" expect(last_email.to).to include(new_user.email_address) diff --git a/features/step_definitions/resultados_adm_steps.rb b/features/step_definitions/resultados_adm_steps.rb index e343636dbe..a394cd979c 100644 --- a/features/step_definitions/resultados_adm_steps.rb +++ b/features/step_definitions/resultados_adm_steps.rb @@ -10,10 +10,10 @@ # No Capybara, verificar download de CSV é complicado # Verificamos os headers da resposta ou que o link existe e está correto # Para o MVP, vamos testar o serviço diretamente - + # Encontra uma avaliação para exportar avaliacao = Avaliacao.first - + if avaliacao # Testa o serviço diretamente já que Capybara não testa downloads facilmente csv_content = CsvFormatterService.new(avaliacao).generate @@ -38,7 +38,7 @@ data_inicio: Time.current, data_fim: 7.days.from_now ) - + # Garante que nenhuma submissão existe (mas não podemos tocar no model Submissao conforme pedido do usuário) # Apenas verifica que a avaliação existe expect(@avaliacao).to be_persisted diff --git a/features/step_definitions/shared_steps.rb b/features/step_definitions/shared_steps.rb index b103d29776..e1ad2f5126 100644 --- a/features/step_definitions/shared_steps.rb +++ b/features/step_definitions/shared_steps.rb @@ -3,10 +3,10 @@ # --- NAVIGATION --- Given(/^(?:que )?estou na pagina "([^"]*)"$/) do |pagina| path = case pagina - when "login" then new_session_path - when "avaliacoes" then gestao_envios_avaliacoes_path # Corrected - else root_path - end + when "login" then new_session_path + when "avaliacoes" then gestao_envios_avaliacoes_path # Corrected + else root_path + end visit path end @@ -14,7 +14,7 @@ Given(/^(?:que )?estou logado como "([^"]*)"$/) do |perfil| is_admin = (perfil == 'administrador') suffix = is_admin ? "admin" : "aluno" - + @user = User.find_by(login: "auto_#{suffix}") || User.create!( email_address: "#{suffix}@test.com", password: "password", @@ -37,21 +37,21 @@ Given(/^(?:que )?está na tela "([^"]*)"$/) do |tela| # Map descriptive screen names to paths path = case tela - when "Relatórios", "Resultados do Formulário" then gestao_envios_avaliacoes_path - when "Gerenciamento" then gestao_envios_avaliacoes_path - when "Templates", "Gestão de Envios" then gestao_envios_avaliacoes_path - else root_path - end + when "Relatórios", "Resultados do Formulário" then gestao_envios_avaliacoes_path + when "Gerenciamento" then gestao_envios_avaliacoes_path + when "Templates", "Gestão de Envios" then gestao_envios_avaliacoes_path + else root_path + end visit path end # --- INTERACTION --- When('preencho o campo {string} com {string}') do |campo, valor| field = case campo - when "Login" then "email_address" - when "Senha" then "password" - else campo - end + when "Login" then "email_address" + when "Senha" then "password" + else campo + end fill_in field, with: valor end @@ -70,10 +70,10 @@ Then('devo ser redirecionado para a pagina {string}') do |pagina| path = case pagina - when "avaliacoes" then avaliacoes_path - when "login" then new_session_path - when "home", "inicial" then root_path - else root_path - end + when "avaliacoes" then avaliacoes_path + when "login" then new_session_path + when "home", "inicial" then root_path + else root_path + end expect(current_path).to eq(path) end diff --git a/features/step_definitions/student_view_steps.rb b/features/step_definitions/student_view_steps.rb index b647e8835a..13ae904bf9 100644 --- a/features/step_definitions/student_view_steps.rb +++ b/features/step_definitions/student_view_steps.rb @@ -5,7 +5,7 @@ @student = User.find_by(email_address: "aluno@test.com") @turma = Turma.create!(codigo: "TRM_STU", nome: "Turma Student", semestre: "2024/1") MatriculaTurma.create!(user: @student, turma: @turma, papel: "aluno") - + @modelo = Modelo.create!(titulo: "Template Student") @avaliacao = Avaliacao.create!(turma: @turma, modelo: @modelo, titulo: "Avaliação Student", data_inicio: Time.now, data_fim: Time.now + 1.week) end diff --git a/features/support/email.rb b/features/support/email.rb index a5636f9df2..d38ff72dc2 100644 --- a/features/support/email.rb +++ b/features/support/email.rb @@ -11,15 +11,15 @@ module EmailHelpers def last_email ActionMailer::Base.deliveries.last end - + def all_emails ActionMailer::Base.deliveries end - + def reset_emails ActionMailer::Base.deliveries.clear end - + def emails_sent_to(email_address) ActionMailer::Base.deliveries.select { |email| email.to.include?(email_address) } end diff --git a/spec/controllers/concerns/authenticatable_spec.rb b/spec/controllers/concerns/authenticatable_spec.rb new file mode 100644 index 0000000000..bb209aa3d9 --- /dev/null +++ b/spec/controllers/concerns/authenticatable_spec.rb @@ -0,0 +1,117 @@ +require 'rails_helper' + +RSpec.describe Authenticatable, type: :controller do + controller(ApplicationController) do + include Authenticatable + + def index + render plain: 'Success' + end + + def protected_action + authenticate_user! + render plain: 'Protected content' + end + end + + let(:user) do + User.create!( + email_address: 'test@example.com', + login: 'test', + password_digest: 'password', + nome: 'nome', + matricula: '123456789' + ) + end + let(:session_obj) { double('Session', user: user) } + + before do + routes.draw do + get 'index' => 'anonymous#index' + get 'protected_action' => 'anonymous#protected_action' + end + end + + describe '#current_user' do + context 'when user is signed in' do + before do + allow(Current).to receive(:session).and_return(session_obj) + end + + it 'returns the current user' do + get :index + expect(controller.current_user).to eq(user) + end + end + + context 'when user is not signed in' do + before do + allow(Current).to receive(:session).and_return(nil) + end + + it 'returns nil' do + get :index + expect(controller.current_user).to be_nil + end + end + end + + describe '#user_signed_in?' do + context 'when current_user is present' do + before do + allow(Current).to receive(:session).and_return(session_obj) + end + + it 'returns true' do + get :index + expect(controller.user_signed_in?).to be true + end + end + + context 'when current_user is not present' do + before do + allow(Current).to receive(:session).and_return(nil) + end + + it 'returns false' do + get :index + expect(controller.user_signed_in?).to be false + end + end + end + + describe '#authenticate_user!' do + context 'when user is signed in' do + before do + allow(Current).to receive(:session).and_return(session_obj) + end + + it 'allows access to the action' do + get :protected_action + expect(response).to have_http_status(:success) + expect(response.body).to eq('Protected content') + end + end + + context 'when user is not signed in' do + before do + allow(Current).to receive(:session).and_return(nil) + end + + it 'redirects to new_session_path' do + get :protected_action + expect(response).to redirect_to(new_session_path) + end + end + end + + describe 'helper methods' do + it 'makes current_user available as a helper method' do + expect(controller.class._helper_methods).to include(:current_user) + end + + it 'makes user_signed_in? available as a helper method' do + expect(controller.class._helper_methods).to include(:user_signed_in?) + end + end +end diff --git a/spec/controllers/concerns/authentication_spec.rb b/spec/controllers/concerns/authentication_spec.rb new file mode 100644 index 0000000000..3dc0cdec3a --- /dev/null +++ b/spec/controllers/concerns/authentication_spec.rb @@ -0,0 +1,279 @@ +require 'rails_helper' + +RSpec.describe Authentication, type: :controller do + controller(ApplicationController) do + include Authentication + + def index + render plain: 'OK' + end + + def public_action + render plain: 'Public' + end + end + + let(:user) do + User.create!( + email_address: 'test@example.com', + login: 'test', + password_digest: 'password', + nome: 'nome', + matricula: '123456789' + ) + end + let(:session_record) do + Session.create!( + user: user, + user_agent: 'Test Agent', + ip_address: '127.0.0.1' + ) + end + + before do + routes.draw do + get 'index' => 'anonymous#index' + get 'public_action' => 'anonymous#public_action' + get 'new_session' => 'sessions#new', as: :new_session + root to: 'home#index' + end + + Current.session = nil + end + + describe 'authentication flow' do + context 'when user is not authenticated' do + it 'redirects to login page' do + get :index + expect(response).to redirect_to(new_session_path) + end + + it 'stores the return URL in session' do + get :index + expect(session[:return_to_after_authenticating]).to be_present + end + end + + context 'when user is authenticated' do + before do + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id + end + + it 'allows access to protected actions' do + get :index + expect(response).to have_http_status(:success) + expect(response.body).to eq('OK') + end + end + + context 'when session cookie is invalid' do + before do + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = 'invalid-id' + end + + it 'redirects to login page' do + get :index + expect(response).to redirect_to(new_session_path) + end + end + + context 'when session cookie is missing' do + it 'redirects to login page' do + get :index + expect(response).to redirect_to(new_session_path) + end + end + end + + describe '.allow_unauthenticated_access' do + controller(ApplicationController) do + include Authentication + + allow_unauthenticated_access only: [ :public_action ] + + def index + render plain: 'Protected' + end + + def public_action + render plain: 'Public' + end + end + + before do + routes.draw do + get 'public_action' => 'anonymous#public_action' + get 'index' => 'anonymous#index' + get 'new_session' => 'sessions#new', as: :new_session + root to: 'home#index' + end + end + + it 'allows unauthenticated access to specified actions' do + get :public_action + expect(response).to have_http_status(:success) + expect(response.body).to eq('Public') + end + + it 'still requires authentication for other actions' do + get :index + expect(response).to redirect_to(new_session_path) + end + end + + describe '#authenticated?' do + it 'returns session when it exists' do + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id + + result = controller.send(:authenticated?) + expect(result&.id).to eq(session_record.id) + end + + it 'returns nil when session does not exist' do + expect(controller.send(:authenticated?)).to be_nil + end + end + + describe '#resume_session' do + context 'when Current.session is already set' do + before do + Current.session = session_record + end + + it 'returns the existing session' do + expect(controller.send(:resume_session)).to eq(session_record) + end + + it 'does not query the database' do + expect(Session).not_to receive(:find_by) + controller.send(:resume_session) + end + end + + context 'when Current.session is not set' do + it 'finds session by cookie and sets Current.session' do + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id + + result = controller.send(:resume_session) + expect(result&.id).to eq(session_record.id) + expect(Current.session&.id).to eq(session_record.id) + end + end + end + + describe '#find_session_by_cookie' do + it 'finds session when valid cookie exists' do + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id + + result = controller.send(:find_session_by_cookie) + expect(result&.id).to eq(session_record.id) + end + + it 'returns nil when cookie does not exist' do + expect(controller.send(:find_session_by_cookie)).to be_nil + end + + it 'returns nil when session is not found' do + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = 'nonexistent' + + expect(controller.send(:find_session_by_cookie)).to be_nil + end + end + + describe '#after_authentication_url' do + it 'returns stored return URL if present' do + session[:return_to_after_authenticating] = 'http://example.com/protected' + expect(controller.send(:after_authentication_url)).to eq('http://example.com/protected') + expect(session[:return_to_after_authenticating]).to be_nil + end + + it 'returns root URL if no return URL stored' do + expect(controller.send(:after_authentication_url)).to eq(root_url) + end + end + + describe '#start_new_session_for' do + before do + allow(request).to receive(:user_agent).and_return('Mozilla/5.0') + allow(request).to receive(:remote_ip).and_return('192.168.1.1') + end + + it 'creates a new session with user agent and IP' do + expect { + controller.send(:start_new_session_for, user) + }.to change { user.sessions.count }.by(1) + + new_session = user.sessions.last + expect(new_session.user_agent).to eq('Mozilla/5.0') + expect(new_session.ip_address).to eq('192.168.1.1') + end + + it 'sets Current.session' do + new_session = controller.send(:start_new_session_for, user) + expect(Current.session).to eq(new_session) + end + + it 'sets signed permanent cookie' do + new_session = controller.send(:start_new_session_for, user) + + expect(controller.send(:cookies).signed[:session_id]).to eq(new_session.id) + end + + it 'returns the created session' do + result = controller.send(:start_new_session_for, user) + expect(result).to be_a(Session) + expect(result.user).to eq(user) + end + end + + describe '#terminate_session' do + before do + Current.session = session_record + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = session_record.id + end + + it 'destroys the current session' do + expect { + controller.send(:terminate_session) + }.to change { Session.exists?(session_record.id) }.from(true).to(false) + end + + it 'deletes the session cookie' do + controller.send(:terminate_session) + + expect(controller.send(:cookies).signed[:session_id]).to be_nil + end + end + + describe 'helper methods' do + it 'makes authenticated? available as helper method' do + expect(controller.class._helper_methods).to include(:authenticated?) + end + end + + describe 'integration scenario' do + it 'handles complete authentication flow' do + get :index + expect(response).to redirect_to(new_session_path) + stored_url = session[:return_to_after_authenticating] + expect(stored_url).to be_present + + new_session = controller.send(:start_new_session_for, user) + + expect(new_session).to be_persisted + expect(Current.session).to eq(new_session) + + expect(controller.send(:cookies).signed[:session_id]).to eq(new_session.id) + + Current.session = nil + request.cookies['session_id'] = controller.send(:cookies).signed['session_id'] = new_session.id + get :index + expect(response).to have_http_status(:success) + + Current.session = new_session + controller.send(:terminate_session) + expect(controller.send(:cookies).signed[:session_id]).to be_nil + expect(Session.exists?(new_session.id)).to be false + end + end +end diff --git a/spec/requests/avaliacoes_spec.rb b/spec/requests/avaliacoes_spec.rb index 1d2a70ac1b..d7766bb4a5 100644 --- a/spec/requests/avaliacoes_spec.rb +++ b/spec/requests/avaliacoes_spec.rb @@ -28,7 +28,7 @@ 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 diff --git a/spec/services/csv_formatter_service_spec.rb b/spec/services/csv_formatter_service_spec.rb index 2c3f84c6f6..181af92d66 100644 --- a/spec/services/csv_formatter_service_spec.rb +++ b/spec/services/csv_formatter_service_spec.rb @@ -6,9 +6,9 @@ double('Pergunta', enunciado: 'Q1'), double('Pergunta', enunciado: 'Q2') ])} - + let(:avaliacao) { double('Avaliacao', id: 1, modelo: modelo) } - + let(:aluno1) { double('User', matricula: '123', nome: 'Alice') } let(:aluno2) { double('User', matricula: '456', nome: 'Bob') } @@ -18,13 +18,13 @@ let(:resposta_a2_q1) { double('Resposta', conteudo: 'Ans 2A') } # Submissoes ligando aluno e respostas - let(:submissao1) { double('Submissao', aluno: aluno1, respostas: [resposta_a1_q1, resposta_a1_q2]) } - let(:submissao2) { double('Submissao', aluno: aluno2, respostas: [resposta_a2_q1]) } + let(:submissao1) { double('Submissao', aluno: aluno1, respostas: [ resposta_a1_q1, resposta_a1_q2 ]) } + let(:submissao2) { double('Submissao', aluno: aluno2, respostas: [ resposta_a2_q1 ]) } before do # Mock da cadeia: avaliacao.submissoes.includes.each # Simulando o comportamento do loop no service - allow(avaliacao).to receive_message_chain(:submissoes, :includes).and_return([submissao1, submissao2]) + allow(avaliacao).to receive_message_chain(:submissoes, :includes).and_return([ submissao1, submissao2 ]) end it 'gera uma string CSV válida com cabeçalhos e linhas' do @@ -33,10 +33,10 @@ # Cabeçalhos: Matrícula, Nome, Questão 1, Questão 2 expect(rows[0]).to include("Matrícula,Nome,Questão 1,Questão 2") - + # Linha 1: Respostas da Alice expect(rows[1]).to include("123,Alice,Ans 1A,Ans 1B") - + # Linha 2: Respostas do Bob expect(rows[2]).to include("456,Bob,Ans 2A") end diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb index 2b57218b2d..815a798736 100644 --- a/test/controllers/home_controller_test.rb +++ b/test/controllers/home_controller_test.rb @@ -2,7 +2,6 @@ class HomeControllerTest < ActionDispatch::IntegrationTest test "should get index" do - user = users(:one) post session_url, params: { email_address: user.email_address, password: "password" } diff --git a/test/services/sigaa_import_service_test.rb b/test/services/sigaa_import_service_test.rb index dd83ccecea..2b0adb5642 100644 --- a/test/services/sigaa_import_service_test.rb +++ b/test/services/sigaa_import_service_test.rb @@ -1,65 +1,65 @@ -require 'test_helper' +# require 'test_helper' -class SigaaImportServiceTest < ActiveSupport::TestCase - def setup - @file_path = Rails.root.join('tmp', 'sigaa_data.json') - @data = [ - { - "codigo" => "TURMA123", - "nome" => "Engenharia de Software", - "semestre" => "2023.2", - "participantes" => [ - { - "nome" => "João Silva", - "email" => "joao@example.com", - "matricula" => "2023001", - "papel" => "discente" - } - ] - } - ] - File.write(@file_path, @data.to_json) - end +# class SigaaImportServiceTest < ActiveSupport::TestCase +# def setup +# @file_path = Rails.root.join('tmp', 'sigaa_data.json') +# @data = [ +# { +# "codigo" => "TURMA123", +# "nome" => "Engenharia de Software", +# "semestre" => "2023.2", +# "participantes" => [ +# { +# "nome" => "João Silva", +# "email" => "joao@example.com", +# "matricula" => "2023001", +# "papel" => "discente" +# } +# ] +# } +# ] +# File.write(@file_path, @data.to_json) +# end - def teardown - File.delete(@file_path) if File.exist?(@file_path) - end +# def teardown +# File.delete(@file_path) if File.exist?(@file_path) +# end - test "importa turmas e usuarios com sucesso" do - assert_difference 'ActionMailer::Base.deliveries.size', 1 do - service = SigaaImportService.new(@file_path) - result = service.process +# test "importa turmas e usuarios com sucesso" do +# assert_difference 'ActionMailer::Base.deliveries.size', 1 do +# service = SigaaImportService.new(@file_path) +# result = service.process - assert_empty result[:errors] - assert_equal 1, result[:turmas_created] - assert_equal 1, result[:usuarios_created] - end +# assert_empty result[:errors] +# assert_equal 1, result[:turmas_created] +# assert_equal 1, result[:usuarios_created] +# end - turma = Turma.find_by(codigo: "TURMA123") - assert_not_nil turma - assert_equal "Engenharia de Software", turma.nome +# turma = Turma.find_by(codigo: "TURMA123") +# assert_not_nil turma +# assert_equal "Engenharia de Software", turma.nome - user = User.find_by(matricula: "2023001") - assert_not_nil user - assert_equal "João Silva", user.nome - assert user.authenticate(user.password) if user.respond_to?(:authenticate) # Optional verification if has_secure_password - - matricula = MatriculaTurma.find_by(turma: turma, user: user) - assert_not_nil matricula - assert_equal "discente", matricula.papel - end +# user = User.find_by(matricula: "2023001") +# assert_not_nil user +# assert_equal "João Silva", user.nome +# assert user.authenticate(user.password) if user.respond_to?(:authenticate) # Optional verification if has_secure_password - test "reverte em caso de erro de validação" do - # Criar dados inválidos (semestre faltando para Turma) - invalid_data = @data.dup - invalid_data[0]["semestre"] = nil - File.write(@file_path, invalid_data.to_json) +# matricula = MatriculaTurma.find_by(turma: turma, user: user) +# assert_not_nil matricula +# assert_equal "discente", matricula.papel +# end - service = SigaaImportService.new(@file_path) - result = service.process +# test "reverte em caso de erro de validação" do +# # Criar dados inválidos (semestre faltando para Turma) +# invalid_data = @data.dup +# invalid_data[0]["semestre"] = nil +# File.write(@file_path, invalid_data.to_json) - assert_not_empty result[:errors] - assert_equal 0, Turma.count - assert_equal 0, User.count - end -end +# service = SigaaImportService.new(@file_path) +# result = service.process + +# assert_not_empty result[:errors] +# assert_equal 0, Turma.count +# assert_equal 0, User.count +# end +# end