diff --git a/.gitignore b/.gitignore index 0b4c9a4b..c9b59b22 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ yarn-debug.log* # Ignore Vendor Local /vendor/bundle --local /vendor/bundle + +# Ignore the files specific to VSCode +.vscode/* diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 367e8555..eb37d90d 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -25,6 +25,7 @@ def login; end def validate if check_credentials(params[:username], params[:password]) + reset_session session[:admin] = true redirect_to publications_path else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d4b9b0c8..257c7475 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,6 +2,7 @@ class ApplicationController < ActionController::Base include Pagy::Backend + include ExceptionHandlingManager include UserAuthentication prepend_before_action :check_date diff --git a/app/controllers/concerns/exception_handling_manager.rb b/app/controllers/concerns/exception_handling_manager.rb new file mode 100644 index 00000000..8fd74776 --- /dev/null +++ b/app/controllers/concerns/exception_handling_manager.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# ExceptionHandlingManager is a concern designed to centralize and manage +# the handling of various application-specific exceptions. +# +# Currently, it includes handling for: +# - ActionController::InvalidAuthenticityToken +# +# Future additions may include but are not limited to: +# - RecordNotFound +# - TimeoutErrors +# - CustomApplicationErrors +# +# This concern is included in the ApplicationController, so all controllers inherit +module ExceptionHandlingManager + extend ActiveSupport::Concern + + included do + rescue_from ActionController::InvalidAuthenticityToken, with: :handle_invalid_token + end + + private + + def handle_invalid_token(exception) + Rails.logger.warn("InvalidAuthenticityToken occurred: #{exception}") + user_was_admin = session[:admin] + reset_session + flash.keep[:danger] = 'Your session has expired. Please log in again.' + + if user_was_admin + redirect_to manage_path + else + redirect_to root_path + end + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 619cd704..038c19d0 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -2,7 +2,7 @@ # app/controllers/pages_controller.rb class PagesController < ApplicationController - skip_before_action :check_date + skip_before_action :check_date, :require_authenticated_user ALLOWED_PAGES = %w[closed finished].freeze def show diff --git a/app/controllers/submitters_controller.rb b/app/controllers/submitters_controller.rb index 1ba28981..5495fa52 100644 --- a/app/controllers/submitters_controller.rb +++ b/app/controllers/submitters_controller.rb @@ -26,7 +26,7 @@ def create if @submitter.save reset_session session[:submitter_id] = @submitter.id - # Change to home page + flash.keep[:success] = 'Your account was successfully created.' format.html { redirect_to publications_path } format.json { render :show, status: :created, location: @submitter } diff --git a/spec/controllers/concerns/exception_handling_manager_spec.rb b/spec/controllers/concerns/exception_handling_manager_spec.rb new file mode 100644 index 00000000..59d200fa --- /dev/null +++ b/spec/controllers/concerns/exception_handling_manager_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ExceptionHandlingManager, type: :controller do + controller(ApplicationController) do + include ExceptionHandlingManager + + def index + render plain: 'Hello, world!' + end + end + + let(:specific_exception) { ActionController::InvalidAuthenticityToken.new('Test Exception Message') } + let(:submitter) { FactoryBot.create(:submitter) } + let(:submitter_session) { { submitter_id: submitter.id } } + let(:admin_session) { { admin: true } } + + before do + routes.draw { get 'index' => 'anonymous#index' } + allow(controller).to receive(:index).and_raise(specific_exception) + end + + context 'as a submitter' do + let(:session) { submitter_session } + + it 'logs the exception to the Rails logger' do + allow(Rails.logger).to receive(:warn) + get(:index, session:) + expect(Rails.logger).to have_received(:warn).with('InvalidAuthenticityToken occurred: Test Exception Message') + end + + it 'resets the session' do + get(:index, session:) + expect(controller.session[:admin]).to be_nil + expect(controller.session[:submitter_id]).to be_nil + end + + it 'sets a flash message and redirects to the root path' do + get(:index, session:) + expect(response.status).to eq(302) + expect(response).to redirect_to(root_path) + expect(flash[:danger]).to eq('Your session has expired. Please log in again.') + end + end + + context 'as an admin' do + let(:session) { admin_session } + + it 'logs the exception to the Rails logger' do + allow(Rails.logger).to receive(:warn) + get(:index, session:) + expect(Rails.logger).to have_received(:warn).with('InvalidAuthenticityToken occurred: Test Exception Message') + end + + it 'resets the session' do + get(:index, session:) + expect(controller.session[:admin]).to be_nil + expect(controller.session[:submitter_id]).to be_nil + end + + it 'sets a flash message and redirects to the manage path' do + get(:index, session:) + expect(response.status).to eq(302) + expect(response).to redirect_to(manage_path) + expect(flash[:danger]).to eq('Your session has expired. Please log in again.') + end + end +end diff --git a/spec/controllers/submitters_controller_spec.rb b/spec/controllers/submitters_controller_spec.rb index 67f2a3ca..98f5e5b7 100644 --- a/spec/controllers/submitters_controller_spec.rb +++ b/spec/controllers/submitters_controller_spec.rb @@ -49,8 +49,8 @@ context 'with valid params' do it 'clears the old session' do post :create, params: { submitter: valid_attributes }, session: old_session - expect(session[:submitter_id]).not_to be_nil - expect(session[:submitter_id]).to eq(Submitter.last.id) + expect(session[:submitter_id]).to_not be(old_submitter.id) + expect(session[:submitter_id]).to_not be_nil expect(session[:some_old_key]).to be_nil end @@ -63,6 +63,8 @@ it 'redirects to the publications show page' do post :create, params: { submitter: valid_attributes }, session: {} expect(response).to redirect_to(publications_path) + expect(flash[:success]).to eql 'Your account was successfully created.' + expect(flash[:error]).to be_nil end end diff --git a/spec/features/application_access/access_invalid_authenticity_missing_session_info_spec.rb b/spec/features/application_access/access_invalid_authenticity_missing_session_info_spec.rb new file mode 100644 index 00000000..fa3721d8 --- /dev/null +++ b/spec/features/application_access/access_invalid_authenticity_missing_session_info_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Application Behavior', type: :feature do + let(:submitter) { FactoryBot.build(:submitter) } + + before do + ActionController::Base.allow_forgery_protection = true + end + + after do + ActionController::Base.allow_forgery_protection = false + end + + context 'when an invalid authenticity token is provided along with a missing or invalid session ID' do + it 'redirects to the root page with an error message' do + # The user never tries to log in, otherwise they would have a session. + # The user never gets to fill in information, so there is no authenticity token to make invalid. + visit publications_path + expect_to_be_on_root_page_with_login_message + end + + it 'allows the user to log in after having been redirected to the root page with an error message' do + visit publications_path + visit_publications_page_as_submitter(submitter) + expect(page).to have_current_path(publications_path) + end + end +end diff --git a/spec/features/application_access/access_invalid_authenticity_spec.rb b/spec/features/application_access/access_invalid_authenticity_spec.rb new file mode 100644 index 00000000..952eb703 --- /dev/null +++ b/spec/features/application_access/access_invalid_authenticity_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Handling of invalid authenticity token', type: :feature, js: true do + let(:submitter) { FactoryBot.create(:submitter) } + + before do + ActionController::Base.allow_forgery_protection = true + end + + after do + ActionController::Base.allow_forgery_protection = false + end + + context 'when trying to log in' do + context 'as a submitter' do + it 'triggers an inauthentic token error and redirects to the root path' do + visit root_path + fill_in('submitter[first_name]', with: submitter.first_name) + fill_in('submitter[last_name]', with: submitter.last_name) + find_by_id('submitter_college').find(:xpath, "option[#{submitter.college}]").select_option + fill_in('submitter[department]', with: submitter.department) + fill_in('submitter[mailing_address]', with: submitter.mailing_address) + fill_in('submitter[phone_number]', with: submitter.phone_number) + fill_in('submitter[email_address]', with: submitter.email_address) + make_authenticity_token_invalid + click_on('Next') + expect_to_be_on_root_page_with_expired_error + end + end + + context 'as an admin' do + before do + allow(ENV).to receive(:fetch).and_call_original + end + context 'when the user was previously in an admin session' do + # We are assuming that the previous session was an admin session. + # If it were not, then the user would be redirected to the root path. + it 'triggers an inauthentic token error and redirects to the manage path' do + visit_publications_page_as_admin + visit manage_path + fill_in('username', with: ENV.fetch('ADMIN_USERNAME', nil)) + fill_in('password', with: ENV.fetch('ADMIN_PASSWORD', nil)) + make_authenticity_token_invalid + click_on('Submit') + expect_to_be_on_manage_page_with_expired_error + end + end + + context 'when the user was not previously in an admin session' do + it 'triggers an inauthentic token error and redirects to the root path' do + # We are assuming that the session[:admin] is not already set. + # If it were, then the user would be redirected to the manage path. + visit manage_path + fill_in('username', with: ENV.fetch('ADMIN_USERNAME', nil)) + fill_in('password', with: ENV.fetch('ADMIN_PASSWORD', nil)) + make_authenticity_token_invalid + click_on('Submit') + expect_to_be_on_root_page_with_expired_error + end + end + end + end + + context 'when trying to add a publication' do + context 'as a submitter' do + it 'triggers an inauthentic token error and redirects to the root path' do + visit_publications_page_as_submitter(submitter) + first('a', text: 'New').click # There is no authenticity check for this button + expect(page).to have_current_path(new_artwork_path) + make_authenticity_token_invalid # New artwork will submit an authenticity token. + + fill_in('artwork[author_first_name][]', with: 'Test') + fill_in('artwork[author_last_name][]', with: 'Artist') + fill_in('artwork[work_title]', with: 'Test Artwork') + click_on('Submit') + expect_to_be_on_root_page_with_expired_error + end + end + + context 'as an admin' do + before do + FactoryBot.create(:artwork) # create an artwork to edit + end + it 'triggers an inauthentic token error and redirects to the manage path' do + visit_publications_page_as_admin + expect(page).to have_current_path(publications_path) + find('i.fas.fa-edit', match: :first).click + expect(page).to have_current_path(edit_artwork_path(Artwork.first)) + make_authenticity_token_invalid + click_on('Submit') + expect_to_be_on_manage_page_with_expired_error + end + end + end +end diff --git a/spec/features/application_access/access_missing_session_info_spec.rb b/spec/features/application_access/access_missing_session_info_spec.rb new file mode 100644 index 00000000..a98df6e1 --- /dev/null +++ b/spec/features/application_access/access_missing_session_info_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Application Behavior', type: :feature do + context 'when a user is not logged in' do + # No submitter_id or admin=true + it 'redirects to a login page with a message to submit your information' do + visit publications_path + expect(current_path).to eq(root_path) + expect(page).to have_content('You must submit your information first.') + end + end +end diff --git a/spec/features/application_access/access_outside_open_dates_invalid_authenticity_spec.rb b/spec/features/application_access/access_outside_open_dates_invalid_authenticity_spec.rb new file mode 100644 index 00000000..dcc6bb84 --- /dev/null +++ b/spec/features/application_access/access_outside_open_dates_invalid_authenticity_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Application Behavior', type: :feature, js: true do + before do + ActionController::Base.allow_forgery_protection = true + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with('EXPIRATION_DATE').and_return('Jan 01 2000') + end + + after do + ActionController::Base.allow_forgery_protection = false + end + + context 'when accessing outside open dates and with an invalid authenticity token' do + context 'as an admin' do + before do + FactoryBot.create(:artwork) # create an artwork to edit + end + it 'triggers an inauthentic token error and redirects to the root path' do + visit_publications_page_as_admin + expect(page).to have_current_path(publications_path) + find('i.fas.fa-edit', match: :first).click + expect(page).to have_current_path(edit_artwork_path(Artwork.first)) + make_authenticity_token_invalid + click_on('Submit') + expect_to_be_on_manage_page_with_expired_error + end + end + + context 'as a submitter' do + it 'redirects to the closed page' do + visit root_path + # The user never gets to fill in information, so there is no authenticity token to make invalid. + expect(page).to have_content('The deadline for submissions has passed.') + end + end + end +end diff --git a/spec/features/application_access/access_outside_open_dates_missing_session_info_spec.rb b/spec/features/application_access/access_outside_open_dates_missing_session_info_spec.rb new file mode 100644 index 00000000..552fc30a --- /dev/null +++ b/spec/features/application_access/access_outside_open_dates_missing_session_info_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Application Behavior', type: :feature do + let(:submitter) { FactoryBot.build(:submitter) } + + before do + allow(ENV).to receive(:fetch).with('EXPIRATION_DATE').and_return('Jan 01 2000') + end + + context 'when accessing outside open dates and with a missing or invalid session ID' do + it 'redirects to the closed page' do + visit publications_path + expect(page).to have_content('The deadline for submissions has passed.') + end + end +end diff --git a/spec/features/application_access/access_outside_open_dates_spec.rb b/spec/features/application_access/access_outside_open_dates_spec.rb new file mode 100644 index 00000000..c3709a09 --- /dev/null +++ b/spec/features/application_access/access_outside_open_dates_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Application Behavior', type: :feature do + let(:submitter) { FactoryBot.build(:submitter) } + let(:expected_admin_content) { ['Submitters (', 'Artworks (', 'Books (', 'Book Chapters ('] } + let(:expected_submitter_content) { ['Instructions', 'Contact Information', 'Artworks', 'Books', 'Book Chapters'] } + + context 'when the application is outside of its open dates' do + before do + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with('EXPIRATION_DATE').and_return('Jan 01 2000') + end + + context 'as an admin' do + it 'allows access to the publications page' do + visit_publications_page_as_admin + expected_admin_content.each do |content| + expect(page).to have_content(content) + end + end + end + + context 'as a submitter' do + it 'does not allow access to the publications page' do + visit_publications_page_as_submitter(submitter) + expect(page).to have_content('The deadline for submissions has passed.') + end + end + end +end diff --git a/spec/features/application_access/access_outside_open_invalid_authenticity_missing_session_info_spec.rb b/spec/features/application_access/access_outside_open_invalid_authenticity_missing_session_info_spec.rb new file mode 100644 index 00000000..86f5284a --- /dev/null +++ b/spec/features/application_access/access_outside_open_invalid_authenticity_missing_session_info_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Application Behavior', type: :feature do + let(:submitter) { FactoryBot.build(:submitter) } + + before do + allow(ENV).to receive(:fetch).with('EXPIRATION_DATE').and_return('Jan 01 2000') + ActionController::Base.allow_forgery_protection = true + end + + after do + ActionController::Base.allow_forgery_protection = false + end + + context 'when accessing outside open dates, with an invalid authenticity token, and a missing or invalid session ID' do + it 'redirects to a closed page' do + # The user never tries to log in, otherwise they would have a session. + # The user never gets to fill in information, so there is no authenticity token to make invalid. + visit publications_path + expect(page).to have_content('The deadline for submissions has passed.') + end + end +end diff --git a/spec/features/application_access/access_valid_everything_spec.rb b/spec/features/application_access/access_valid_everything_spec.rb new file mode 100644 index 00000000..f8330a5f --- /dev/null +++ b/spec/features/application_access/access_valid_everything_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# The application is within open dates. +# The user has a valid authentication token. +# (Not changed to invalid as in access_invalid_authenticity.rb) +# The user is logged in as either a submitter, an admin, or both. +# The user is allowed to access the publications page. + +require 'rails_helper' + +RSpec.describe 'Application Behavior', type: :feature, js: true do + before do + ActionController::Base.allow_forgery_protection = true + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with('EXPIRATION_DATE').and_return('Jan 01 2099') + end + + after do + ActionController::Base.allow_forgery_protection = false + end + + let(:submitter) { FactoryBot.create(:submitter) } + let(:expected_admin_content) { ['Submitters (', 'Artworks (', 'Books (', 'Book Chapters ('] } + let(:expected_submitter_content) { ['Instructions', 'Contact Information', 'Artworks', 'Books', 'Book Chapters'] } + + context 'when the application is within its open dates' do + context 'as an admin' do + it 'allows access to the publications page' do + visit_publications_page_as_admin + expected_admin_content.each do |content| + expect(page).to have_content(content) + end + end + end + + context 'as a submitter' do + it 'allows access to the publications page' do + visit_publications_page_as_submitter(submitter) + expected_submitter_content.each do |content| + expect(page).to have_content(content) + end + end + end + end +end diff --git a/spec/features/create_book_chapter_spec.rb b/spec/features/create_book_chapter_spec.rb index 309469d7..a3ad28ae 100644 --- a/spec/features/create_book_chapter_spec.rb +++ b/spec/features/create_book_chapter_spec.rb @@ -9,6 +9,7 @@ it 'from publications index page' do create_submitter(submitter) visit publications_path + find(:xpath, "//a[@href='#{Rails.application.routes.url_helpers.new_book_chapter_path}']").click # New book_chapter Page diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 90884638..8c8757d7 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -77,6 +77,8 @@ def create_submitter(submitter) visit root_path + return if page.has_content?('The deadline for submissions has passed.') + fill_in('submitter[first_name]', with: submitter.first_name) fill_in('submitter[last_name]', with: submitter.last_name) find_by_id('submitter_college').find(:xpath, "option[#{submitter.college}]").select_option diff --git a/spec/support/helpers/access_authorization_for_feature_tests.rb b/spec/support/helpers/access_authorization_for_feature_tests.rb new file mode 100644 index 00000000..487b29cf --- /dev/null +++ b/spec/support/helpers/access_authorization_for_feature_tests.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +def visit_publications_page_as_submitter(submitter) + create_submitter(submitter) +end + +def visit_publications_page_as_admin + visit manage_path + fill_in('username', with: ENV.fetch('ADMIN_USERNAME', nil)) + fill_in('password', with: ENV.fetch('ADMIN_PASSWORD', nil)) + click_on('Submit') +end + +def make_authenticity_token_invalid + expect(page).to have_selector('input[name=authenticity_token]', visible: false) + page.execute_script("document.querySelector('input[name=authenticity_token]').value = 'invalid_token';") +end + +def expect_to_be_on_root_page_with_expired_error + expect(page).to have_current_path(root_path) + expect(page).to have_content('Your session has expired. Please log in again.') +end + +def expect_to_be_on_root_page_with_login_message + expect(page).to have_current_path(root_path) + expect(page).to have_content('You must submit your information first.') +end + +def expect_to_be_on_manage_page_with_expired_error + expect(page).to have_current_path(manage_path) + expect(page).to have_content('Your session has expired. Please log in again.') +end diff --git a/spec/support/helpers/access_authorization.rb b/spec/support/helpers/access_authorization_for_unit_tests.rb similarity index 100% rename from spec/support/helpers/access_authorization.rb rename to spec/support/helpers/access_authorization_for_unit_tests.rb