Files
codeocean/spec/controllers/sessions_controller_spec.rb
Sebastian Serth 96f5f1f8d7 Enforce valid lis_outcome_service_url
Recently, a new institution joined CodeOcean and used a relative URL. This won't work, so that we are rejecting non-absolute URLs by now.
2024-04-26 10:21:53 +02:00

253 lines
9.6 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe SessionsController do
render_views
let(:consumer) { create(:consumer) }
describe 'POST #create' do
let(:password) { attributes_for(:teacher)[:password] }
let(:user) { InternalUser.create(user_attributes.merge(password:, consumer:)) }
let(:user_attributes) { build(:teacher).attributes }
context 'with valid credentials' do
before do
user.activate!
post :create, params: {email: user.email, password:, remember_me: 1}
end
expect_flash_message(:notice, :'sessions.create.success')
expect_redirect(:root)
end
context 'with invalid credentials' do
before { post :create, params: {email: user.email, password: '', remember_me: 1} }
expect_flash_message(:danger, :'sessions.create.failure')
expect_template(:new)
end
end
describe 'POST #create_through_lti' do
let(:exercise) { create(:dummy) }
let(:exercise2) { create(:dummy) }
let(:nonce) { SecureRandom.hex }
context 'without OAuth parameters' do
it 'refuses the LTI launch' do
expect(controller).to receive(:refuse_lti_launch).with(message: I18n.t('sessions.oauth.missing_parameters')).and_call_original
post :create_through_lti
end
end
context 'without a valid consumer key' do
it 'refuses the LTI launch' do
expect(controller).to receive(:refuse_lti_launch).with(message: I18n.t('sessions.oauth.invalid_consumer')).and_call_original
post :create_through_lti, params: {oauth_consumer_key: SecureRandom.hex, oauth_signature: SecureRandom.hex}
end
end
context 'with an invalid OAuth signature' do
it 'refuses the LTI launch' do
expect(controller).to receive(:refuse_lti_launch).with(message: I18n.t('sessions.oauth.invalid_signature')).and_call_original
post :create_through_lti, params: {oauth_consumer_key: consumer.oauth_key, oauth_signature: SecureRandom.hex}
end
end
context 'without a unique OAuth nonce' do
it 'refuses the LTI launch' do
allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true)
allow(NonceStore).to receive(:has?).with(nonce).and_return(true)
expect(controller).to receive(:refuse_lti_launch).with(message: I18n.t('sessions.oauth.used_nonce')).and_call_original
post :create_through_lti, params: {oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex}
end
end
context 'without a valid absolute LIS Outcome service URL' do
it 'refuses the LTI launch' do
allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true)
expect(controller).to receive(:refuse_lti_launch).with(message: I18n.t('sessions.oauth.invalid_lis_outcome_service_url')).and_call_original
post :create_through_lti, params: {oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, lis_outcome_service_url: '/relative/url'}
end
end
context 'without a valid exercise token' do
it 'refuses the LTI launch' do
allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true)
expect(controller).to receive(:refuse_lti_launch).with(message: I18n.t('sessions.oauth.invalid_exercise_token')).and_call_original
post :create_through_lti, params: {custom_token: '', oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: '123'}
end
end
context 'with valid launch parameters' do
let(:locale) { :de }
let(:perform_request) { post :create_through_lti, params: {custom_locale: locale, custom_token: exercise.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id, lis_outcome_service_url: 'https://example.org/'} }
let(:user) { create(:external_user, consumer:) }
before { allow_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true) }
it 'assigns the current user' do
perform_request
expect(assigns(:current_user)).to be_an(ExternalUser)
expect(session[:external_user_id]).to eq(user.id)
end
it 'sets the specified locale' do
expect(controller).to receive(:switch_locale).and_call_original
i18n = class_double(I18n, locale:)
allow(I18n).to receive(:locale=).with(I18n.default_locale).and_call_original
allow(I18n).to receive(:locale=).with(locale).and_return(i18n)
perform_request
expect(i18n.locale.to_sym).to eq(locale)
end
it 'assigns the exercise' do
perform_request
expect(assigns(:exercise)).to eq(exercise)
end
it 'stores LTI parameters in the session' do
expect(controller).to receive(:store_lti_session_data)
perform_request
end
it 'stores the OAuth nonce' do
expect(controller).to receive(:store_nonce).with(nonce)
perform_request
end
context 'when LTI outcomes are supported' do
# The expected message should be localized in the requested localization
let(:message) { I18n.t('sessions.create_through_lti.session_with_outcome', consumer:, locale:) }
before do
allow(controller).to receive(:lti_outcome_service?).and_return(true)
perform_request
end
expect_flash_message(:notice, :message)
end
context 'when LTI outcomes are not supported' do
# The expected message should be localized in the requested localization
let(:message) { I18n.t('sessions.create_through_lti.session_without_outcome', consumer:, locale:) }
before do
allow(controller).to receive(:lti_outcome_service?).and_return(false)
perform_request
end
expect_flash_message(:notice, :message)
end
it 'redirects to the requested exercise' do
perform_request
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
end
it 'redirects to recommended exercise if requested token of proxy exercise' do
create(:proxy_exercise, exercises: [exercise])
post :create_through_lti, params: {custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id}
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
end
it 'recommends only exercises who are 1 degree more complicated than what user has seen' do
# dummy user has no exercises finished, therefore his highest difficulty is 0
create(:proxy_exercise, exercises: [exercise, exercise2])
exercise.expected_difficulty = 3
exercise.save
exercise2.expected_difficulty = 1
exercise2.save
post :create_through_lti, params: {custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id}
expect(controller).to redirect_to(implement_exercise_path(exercise2.id))
end
end
end
describe 'DELETE #destroy' do
let(:user) { double }
before do
allow(controller).to receive(:set_sentry_context).and_return(nil)
allow(controller).to receive(:current_user).at_least(:once).and_return(user)
end
context 'with an internal user' do
before do
allow(user).to receive(:external_user?).and_return(false)
allow(user).to receive(:forget_me!)
delete :destroy
end
it 'performs a logout' do
expect(controller).to receive(:logout)
delete :destroy
end
it 'redirects to the root path' do
expect(controller).to redirect_to(:root)
end
end
context 'with an external user' do
before do
allow(user).to receive(:external_user?).and_return(true)
delete :destroy
end
it 'clears the session' do
expect(controller.session).to receive(:delete).with(:external_user_id)
expect(controller.session).to receive(:delete).with(:study_group_id)
expect(controller.session).to receive(:delete).with(:embed_options)
expect(controller.session).to receive(:delete).with(:pg_id)
expect(controller.session).to receive(:delete).with(:pair_programming)
delete :destroy
end
it 'redirects to the root path' do
expect(controller).to redirect_to(:root)
end
end
end
describe 'GET #destroy_through_lti' do
let(:perform_request) { proc { get :destroy_through_lti, params: {submission_id: submission.id} } }
let(:submission) { create(:submission, exercise: create(:dummy)) }
before do
create(:lti_parameter, external_user: submission.contributor)
allow(controller).to receive(:current_user).and_return(submission.contributor)
perform_request.call
end
expect_http_status(:ok)
expect_template(:destroy_through_lti)
end
describe 'GET #new' do
context 'when no user is logged in' do
before do
allow(controller).to receive_messages(set_sentry_context: nil, current_user: nil)
get :new
end
expect_http_status(:ok)
expect_template(:new)
end
context 'when a user is already logged in' do
before do
allow(controller).to receive_messages(set_sentry_context: nil, current_user: build(:teacher))
get :new
end
expect_flash_message(:alert, :'shared.already_signed_in')
expect_redirect(:root)
end
end
end