Files
codeocean/spec/controllers/exercises_controller_spec.rb
Sebastian Serth 99bd46af1a Align project files with CodeHarbor
Since both projects are developed together and by the same team, we also want to have the same code structure and utility methods available in both projects. Therefore, this commit changes many files, but without a functional change.
2023-10-11 00:18:33 +02:00

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) { {public: 'true'} }
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