
* Remove deprecated options from environments * Remove deprecation warnings for upcoming Rails 7.2 * Dump schema with new defaults * Remove outdated (and erroneous) data attribute in view * Resolve a `NoMethodError` for seeds_spec.rb
604 lines
19 KiB
Ruby
604 lines
19 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe ExercisesController do
|
|
render_views
|
|
|
|
let(:exercise) { create(:dummy) }
|
|
let(:user) { create(:admin) }
|
|
|
|
before do
|
|
create(:test_file, context: exercise)
|
|
allow(controller).to receive(:current_user).and_return(user)
|
|
end
|
|
|
|
describe 'PUT #batch_update' do
|
|
let(:attributes) { ActionController::Parameters.new(public: 'true').permit! }
|
|
let(:perform_request) { proc { put :batch_update, params: {exercises: {0 => attributes.merge(id: exercise.id)}} } }
|
|
|
|
before { perform_request.call }
|
|
|
|
it 'updates the exercises' do
|
|
expect_any_instance_of(Exercise).to receive(:update).with(attributes)
|
|
perform_request.call
|
|
end
|
|
|
|
expect_json
|
|
expect_http_status(:ok)
|
|
end
|
|
|
|
describe 'POST #clone' do
|
|
let(:perform_request) { proc { post :clone, params: {id: exercise.id} } }
|
|
|
|
context 'when saving succeeds' do
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(exercise: Exercise)
|
|
|
|
it 'clones the exercise' do
|
|
expect_any_instance_of(Exercise).to receive(:duplicate).with(hash_including(public: false, user:)).and_call_original
|
|
expect { perform_request.call }.to change(Exercise, :count).by(1)
|
|
end
|
|
|
|
it 'generates a new token' do
|
|
expect(Exercise.last.token).not_to eq(exercise.token)
|
|
end
|
|
|
|
expect_redirect(Exercise.last)
|
|
end
|
|
|
|
context 'when saving fails' do
|
|
before do
|
|
allow_any_instance_of(Exercise).to receive(:save).and_return(false)
|
|
perform_request.call
|
|
end
|
|
|
|
expect_assigns(exercise: Exercise)
|
|
expect_flash_message(:danger, :'shared.message_failure')
|
|
expect_redirect(:exercise)
|
|
end
|
|
end
|
|
|
|
describe 'POST #create' do
|
|
let(:exercise_attributes) { build(:dummy).attributes }
|
|
|
|
context 'with a valid exercise' do
|
|
let(:perform_request) { proc { post :create, params: {exercise: exercise_attributes} } }
|
|
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(exercise: Exercise)
|
|
|
|
it 'creates the exercise' do
|
|
expect { perform_request.call }.to change(Exercise, :count).by(1)
|
|
end
|
|
|
|
expect_redirect(Exercise.last)
|
|
end
|
|
|
|
context 'when including a file' do
|
|
let(:perform_request) { proc { post :create, params: {exercise: exercise_attributes.merge(files_attributes:)} } }
|
|
|
|
context 'when specifying the file content within the form' do
|
|
let(:files_attributes) { {'0' => build(:file).attributes} }
|
|
|
|
it 'creates the file' do
|
|
expect { perform_request.call }.to change(CodeOcean::File, :count)
|
|
end
|
|
end
|
|
|
|
context 'when uploading a file' do
|
|
let(:files_attributes) { {'0' => build(:file, file_type:).attributes.merge(content: uploaded_file)} }
|
|
|
|
context 'when uploading a binary file' do
|
|
let(:file_path) { Rails.root.join('db/seeds/audio_video/devstories.mp4') }
|
|
let(:file_type) { create(:dot_mp4) }
|
|
let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'video/mp4', true) }
|
|
|
|
it 'creates the file' do
|
|
expect { perform_request.call }.to change(CodeOcean::File, :count)
|
|
end
|
|
|
|
it 'assigns the native file' do
|
|
perform_request.call
|
|
expect(Exercise.last.files.first.native_file).to be_a(FileUploader)
|
|
end
|
|
end
|
|
|
|
context 'when uploading a non-binary file' do
|
|
let(:file_path) { Rails.root.join('db/seeds/fibonacci/exercise.rb') }
|
|
let(:file_type) { create(:dot_rb) }
|
|
let(:uploaded_file) { Rack::Test::UploadedFile.new(file_path, 'text/x-ruby', false) }
|
|
|
|
it 'creates the file' do
|
|
expect { perform_request.call }.to change(CodeOcean::File, :count)
|
|
end
|
|
|
|
it 'assigns the file content' do
|
|
perform_request.call
|
|
expect(Exercise.last.files.first.content).to eq(File.read(file_path))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with an invalid exercise' do
|
|
before { post :create, params: {exercise: {}} }
|
|
|
|
expect_assigns(exercise: Exercise)
|
|
expect_http_status(:ok)
|
|
expect_template(:new)
|
|
end
|
|
end
|
|
|
|
describe 'DELETE #destroy' do
|
|
before { delete :destroy, params: {id: exercise.id} }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
|
|
it 'destroys the exercise' do
|
|
exercise = create(:dummy)
|
|
expect { delete :destroy, params: {id: exercise.id} }.to change(Exercise, :count).by(-1)
|
|
end
|
|
|
|
expect_redirect(:exercises)
|
|
end
|
|
|
|
describe 'GET #edit' do
|
|
before { get :edit, params: {id: exercise.id} }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_http_status(:ok)
|
|
expect_template(:edit)
|
|
end
|
|
|
|
describe 'GET #implement' do
|
|
let(:perform_request) { proc { get :implement, params: {id: exercise.id} } }
|
|
|
|
context 'with an exercise with visible files' do
|
|
let(:exercise) { create(:fibonacci) }
|
|
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
|
|
context 'with an existing submission' do
|
|
let!(:submission) { create(:submission, exercise:, contributor: user) }
|
|
|
|
it "populates the editors with the submission's files' content" do
|
|
perform_request.call
|
|
expect(assigns(:files)).to eq(submission.files)
|
|
end
|
|
end
|
|
|
|
context 'without an existing submission' do
|
|
it "populates the editors with the exercise's files' content" do
|
|
expect(assigns(:files)).to eq(exercise.files.visible)
|
|
end
|
|
end
|
|
|
|
expect_http_status(:ok)
|
|
expect_template(:implement)
|
|
end
|
|
|
|
context 'with an exercise without visible files' do
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_flash_message(:alert, :'exercises.implement.no_files')
|
|
expect_redirect(:exercise)
|
|
end
|
|
|
|
context 'with other users accessing an unpublished exercise' do
|
|
let(:exercise) { create(:fibonacci, unpublished: true) }
|
|
let(:user) { create(:teacher) }
|
|
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_flash_message(:alert, :'exercises.implement.unpublished')
|
|
expect_redirect(:exercise)
|
|
end
|
|
end
|
|
|
|
describe 'GET #index' do
|
|
let(:scope) { Pundit.policy_scope!(user, Exercise) }
|
|
|
|
before do
|
|
create_pair(:dummy)
|
|
get :index
|
|
end
|
|
|
|
expect_assigns(exercises: :scope)
|
|
expect_http_status(:ok)
|
|
expect_template(:index)
|
|
end
|
|
|
|
describe 'GET #new' do
|
|
before { get :new }
|
|
|
|
expect_assigns(execution_environments: ExecutionEnvironment.all, exercise: Exercise)
|
|
expect_assigns(exercise: Exercise)
|
|
expect_http_status(:ok)
|
|
expect_template(:new)
|
|
end
|
|
|
|
describe 'GET #show' do
|
|
context 'when being admin' do
|
|
before { get :show, params: {id: exercise.id} }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_http_status(:ok)
|
|
expect_template(:show)
|
|
end
|
|
end
|
|
|
|
describe 'GET #reload' do
|
|
context 'when being anyone' do
|
|
let(:exercise) { create(:fibonacci) }
|
|
|
|
before { get :reload, format: :json, params: {id: exercise.id} }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_http_status(:ok)
|
|
expect_template(:reload)
|
|
end
|
|
end
|
|
|
|
describe 'GET #statistics' do
|
|
before { get :statistics, params: {id: exercise.id} }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_http_status(:ok)
|
|
expect_template(:statistics)
|
|
end
|
|
|
|
describe 'GET #external_user_statistics' do
|
|
let(:perform_request) { get :external_user_statistics, params: }
|
|
let(:params) { {id: exercise.id, external_user_id: external_user.id} }
|
|
let(:external_user) { create(:external_user) }
|
|
|
|
before do
|
|
create_list(:submission, 2, cause: 'autosave', contributor: external_user, exercise:)
|
|
create_list(:submission, 2, cause: 'run', contributor: external_user, exercise:)
|
|
create(:submission, cause: 'assess', contributor: external_user, exercise:)
|
|
end
|
|
|
|
context 'when viewing the default submission statistics page without a parameter' do
|
|
it 'does not list autosaved submissions' do
|
|
perform_request
|
|
expect(assigns(:all_events).filter {|event| event.is_a? Submission }).to contain_exactly(
|
|
an_object_having_attributes(cause: 'run', contributor: external_user),
|
|
an_object_having_attributes(cause: 'assess', contributor: external_user),
|
|
an_object_having_attributes(cause: 'run', contributor: external_user)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when including autosaved submissions via the query parameter' do
|
|
let(:params) { super().merge(show_autosaves: 'true') }
|
|
|
|
it 'lists all submissions, including autosaved submissions' do
|
|
perform_request
|
|
submissions = assigns(:all_events).filter {|event| event.is_a? Submission }
|
|
expect(submissions).to match_array Submission.all
|
|
expect(submissions).to include an_object_having_attributes(cause: 'autosave', contributor: external_user)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST #submit' do
|
|
let(:output) { {} }
|
|
let(:perform_request) { post :submit, format: :json, params: {id: exercise.id, submission: {cause: 'submit', exercise_id: exercise.id}} }
|
|
let(:contributor) { create(:external_user) }
|
|
let(:scoring_response) do
|
|
[{
|
|
status: :ok,
|
|
stdout: '',
|
|
stderr: '',
|
|
waiting_for_container_time: 0,
|
|
container_execution_time: 0,
|
|
file_role: 'teacher_defined_test',
|
|
count: 1,
|
|
failed: 0,
|
|
error_messages: [],
|
|
passed: 1,
|
|
score: 1.0,
|
|
filename: 'index.html_spec.rb',
|
|
message: 'Well done.',
|
|
weight: 2.0,
|
|
}]
|
|
end
|
|
|
|
before do
|
|
create(:lti_parameter, external_user: contributor, exercise:)
|
|
submission = build(:submission, exercise:, contributor:)
|
|
allow(submission).to receive_messages(normalized_score: 1, calculate_score: scoring_response, redirect_to_feedback?: false)
|
|
allow(Submission).to receive(:create).and_return(submission)
|
|
end
|
|
|
|
context 'when LTI outcomes are supported' do
|
|
before do
|
|
allow(controller).to receive(:lti_outcome_service?).and_return(true)
|
|
end
|
|
|
|
context 'when the score transmission succeeds' do
|
|
before do
|
|
allow(controller).to receive(:send_scores).and_return([{status: 'success'}])
|
|
perform_request
|
|
end
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
|
|
it 'creates a submission' do
|
|
expect(assigns(:submission)).to be_a(Submission)
|
|
end
|
|
|
|
expect_json
|
|
expect_http_status(:ok)
|
|
end
|
|
|
|
context 'when the score transmission fails' do
|
|
before do
|
|
allow(controller).to receive(:send_scores).and_return([{status: 'unsupported'}])
|
|
perform_request
|
|
end
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
|
|
it 'creates a submission' do
|
|
expect(assigns(:submission)).to be_a(Submission)
|
|
end
|
|
|
|
it 'returns an error message' do
|
|
expect(response.parsed_body).to eq('danger' => I18n.t('exercises.submit.failure'))
|
|
end
|
|
|
|
expect_json
|
|
end
|
|
end
|
|
|
|
context 'when LTI outcomes are not supported' do
|
|
before do
|
|
allow(controller).to receive(:lti_outcome_service?).and_return(false)
|
|
perform_request
|
|
end
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
|
|
it 'creates a submission' do
|
|
expect(assigns(:submission)).to be_a(Submission)
|
|
end
|
|
|
|
it 'does not send scores' do
|
|
expect(controller).not_to receive(:send_scores)
|
|
end
|
|
|
|
expect_json
|
|
expect_http_status(:ok)
|
|
end
|
|
end
|
|
|
|
describe 'PUT #update' do
|
|
context 'with a valid exercise' do
|
|
let(:exercise_attributes) { build(:dummy).attributes }
|
|
|
|
before { put :update, params: {exercise: exercise_attributes, id: exercise.id} }
|
|
|
|
expect_assigns(exercise: Exercise)
|
|
expect_redirect(:exercise)
|
|
end
|
|
|
|
context 'with an invalid exercise' do
|
|
before { put :update, params: {exercise: {title: ''}, id: exercise.id} }
|
|
|
|
expect_assigns(exercise: Exercise)
|
|
expect_http_status(:ok)
|
|
expect_template(:edit)
|
|
end
|
|
end
|
|
|
|
RSpec::Matchers.define_negated_matcher :not_include, :include
|
|
# RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 99999
|
|
|
|
describe 'POST #export_external_check' do
|
|
render_views
|
|
|
|
let(:post_request) { post :export_external_check, params: {id: exercise.id} }
|
|
let!(:codeharbor_link) { create(:codeharbor_link, user:) }
|
|
let(:external_check_hash) { {message:, uuid_found: true, update_right:, error:} }
|
|
let(:message) { 'message' }
|
|
let(:update_right) { true }
|
|
let(:error) { nil }
|
|
|
|
before { allow(ExerciseService::CheckExternal).to receive(:call).with(uuid: exercise.uuid, codeharbor_link:).and_return(external_check_hash) }
|
|
|
|
it 'renders the correct contents as json' do
|
|
post_request
|
|
expect(response.parsed_body.symbolize_keys[:message]).to eq('message')
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(
|
|
include('button').and(include('Abort').and(include('Export')))
|
|
)
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(
|
|
not_include('Retry').and(not_include('Hide'))
|
|
)
|
|
end
|
|
|
|
context 'when there is an error' do
|
|
let(:error) { 'error' }
|
|
|
|
it 'renders the correct contents as json' do
|
|
post_request
|
|
expect(response.parsed_body.symbolize_keys[:message]).to eq('message')
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(
|
|
include('button').and(include('Abort')).and(include('Retry'))
|
|
)
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(
|
|
not_include('Export').and(not_include('Hide'))
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when update_right is false' do
|
|
let(:update_right) { false }
|
|
|
|
it 'renders the correct contents as json' do
|
|
post_request
|
|
expect(response.parsed_body.symbolize_keys[:message]).to eq('message')
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(
|
|
include('button').and(include('Abort'))
|
|
)
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(
|
|
not_include('Retry').and(not_include('Export')).and(not_include('Hide'))
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST #export_external_confirm' do
|
|
render_views
|
|
|
|
let!(:codeharbor_link) { create(:codeharbor_link, user:) }
|
|
let(:post_request) { post :export_external_confirm, params: {id: exercise.id, codeharbor_link: codeharbor_link.id} }
|
|
let(:error) { nil }
|
|
let(:zip) { 'zip' }
|
|
|
|
before do
|
|
allow(ProformaService::ExportTask).to receive(:call).with(exercise:).and_return(zip)
|
|
allow(ExerciseService::PushExternal).to receive(:call).with(zip:, codeharbor_link:).and_return(error)
|
|
end
|
|
|
|
it 'renders correct response' do
|
|
post_request
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.parsed_body.symbolize_keys[:message]).to(include('successfully exported'))
|
|
expect(response.parsed_body.symbolize_keys[:status]).to(eql('success'))
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(include('button').and(include('Close')))
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(not_include('Retry').and(not_include('Abort')))
|
|
end
|
|
|
|
context 'when an error occurs' do
|
|
let(:error) { 'exampleerror' }
|
|
|
|
it 'renders correct response' do
|
|
post_request
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.parsed_body.symbolize_keys[:message]).to(include('failed').and(include('exampleerror')))
|
|
expect(response.parsed_body.symbolize_keys[:status]).to(eql('fail'))
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(include('button').and(include('Retry')).and(include('Close')))
|
|
expect(response.parsed_body.symbolize_keys[:actions]).to(not_include('Abort'))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST #import_uuid_check' do
|
|
let(:exercise) { create(:dummy, uuid: SecureRandom.uuid) }
|
|
let!(:codeharbor_link) { create(:codeharbor_link, user:) }
|
|
let(:uuid) { exercise.reload.uuid }
|
|
let(:post_request) { post :import_uuid_check, params: {uuid:} }
|
|
let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} }
|
|
|
|
before { request.headers.merge! headers }
|
|
|
|
it 'renders correct response' do
|
|
post_request
|
|
expect(response).to have_http_status(:success)
|
|
|
|
expect(response.parsed_body.symbolize_keys[:uuid_found]).to be true
|
|
expect(response.parsed_body.symbolize_keys[:update_right]).to be true
|
|
end
|
|
|
|
context 'when api_key is incorrect' do
|
|
let(:headers) { {'Authorization' => 'Bearer XXXXXX'} }
|
|
|
|
it 'renders correct response' do
|
|
post_request
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when the user cannot update the exercise' do
|
|
let(:codeharbor_link) { create(:codeharbor_link, api_key: 'anotherkey') }
|
|
|
|
it 'renders correct response' do
|
|
post_request
|
|
expect(response).to have_http_status(:success)
|
|
|
|
expect(response.parsed_body.symbolize_keys[:uuid_found]).to be true
|
|
expect(response.parsed_body.symbolize_keys[:update_right]).to be false
|
|
end
|
|
end
|
|
|
|
context 'when the searched exercise does not exist' do
|
|
let(:uuid) { 'anotheruuid' }
|
|
|
|
it 'renders correct response' do
|
|
post_request
|
|
expect(response).to have_http_status(:success)
|
|
|
|
expect(response.parsed_body.symbolize_keys[:uuid_found]).to be false
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST #import_task' do
|
|
let(:codeharbor_link) { create(:codeharbor_link, user:) }
|
|
let!(:imported_exercise) { create(:fibonacci) }
|
|
let(:post_request) { post :import_task, body: zip_file_content }
|
|
let(:zip_file_content) { 'zipped task xml' }
|
|
let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} }
|
|
|
|
before do
|
|
request.headers.merge! headers
|
|
allow(ProformaService::Import).to receive(:call).and_return(imported_exercise)
|
|
end
|
|
|
|
it 'responds with correct status code' do
|
|
post_request
|
|
expect(response).to have_http_status(:created)
|
|
end
|
|
|
|
it 'calls service' do
|
|
post_request
|
|
expect(ProformaService::Import).to have_received(:call).with(zip: be_a(Tempfile).and(has_content(zip_file_content)), user:)
|
|
end
|
|
|
|
context 'when import fails with ProformaError' do
|
|
before { allow(ProformaService::Import).to receive(:call).and_raise(ProformaXML::PreImportValidationError) }
|
|
|
|
it 'responds with correct status code' do
|
|
post_request
|
|
expect(response).to have_http_status(:bad_request)
|
|
end
|
|
end
|
|
|
|
context 'when import fails with ExerciseNotOwned' do
|
|
before { allow(ProformaService::Import).to receive(:call).and_raise(ProformaXML::ExerciseNotOwned) }
|
|
|
|
it 'responds with correct status code' do
|
|
post_request
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when import fails due to another error' do
|
|
before { allow(ProformaService::Import).to receive(:call).and_raise(StandardError) }
|
|
|
|
it 'responds with correct status code' do
|
|
post_request
|
|
expect(response).to have_http_status(:internal_server_error)
|
|
end
|
|
end
|
|
|
|
context 'when the imported exercise is invalid' do
|
|
before { allow(ProformaService::Import).to receive(:call) { imported_exercise.tap {|e| e.files = [] }.tap {|e| e.title = nil } } }
|
|
|
|
it 'responds with correct status code' do
|
|
expect { post_request }.not_to(change { imported_exercise.reload.files.count })
|
|
end
|
|
end
|
|
end
|
|
end
|