Files
codeocean/spec/controllers/exercises_controller_spec.rb
2021-05-12 16:47:05 +02:00

520 lines
17 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe ExercisesController do
let(:exercise) { FactoryBot.create(:dummy) }
let(:user) { FactoryBot.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
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(:each) { 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_status(200)
end
describe 'POST #clone' do
let(:perform_request) { proc { post :clone, params: { id: exercise.id } } }
context 'when saving succeeds' do
before(:each) { 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: 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(:each) do
expect_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) { FactoryBot.build(:dummy).attributes }
context 'with a valid exercise' do
let(:perform_request) { proc { post :create, params: { exercise: exercise_attributes } } }
before(:each) { 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: files_attributes) } } }
context 'when specifying the file content within the form' do
let(:files_attributes) { {'0' => FactoryBot.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' => FactoryBot.build(:file, file_type: 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) { FactoryBot.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) { FactoryBot.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(:each) { post :create, params: { exercise: { } } }
expect_assigns(exercise: Exercise)
expect_status(200)
expect_template(:new)
end
end
describe 'DELETE #destroy' do
before(:each) { delete :destroy, params: { id: exercise.id } }
expect_assigns(exercise: :exercise)
it 'destroys the exercise' do
exercise = FactoryBot.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(:each) { get :edit, params: { id: exercise.id } }
expect_assigns(exercise: :exercise)
expect_status(200)
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) { FactoryBot.create(:fibonacci) }
before(:each) { perform_request.call }
expect_assigns(exercise: :exercise)
context 'with an existing submission' do
let!(:submission) { FactoryBot.create(:submission, exercise_id: exercise.id, user_id: user.id, user_type: user.class.name) }
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_status(200)
expect_template(:implement)
end
context 'with an exercise without visible files' do
before(:each) { perform_request.call }
expect_assigns(exercise: :exercise)
expect_flash_message(:alert, :'exercises.implement.no_files')
expect_redirect(:exercise)
end
end
describe 'GET #index' do
let(:scope) { Pundit.policy_scope!(user, Exercise) }
before(:all) { FactoryBot.create_pair(:dummy) }
before(:each) { get :index }
expect_assigns(exercises: :scope)
expect_status(200)
expect_template(:index)
end
describe 'GET #new' do
before(:each) { get :new }
expect_assigns(execution_environments: ExecutionEnvironment.all, exercise: Exercise)
expect_assigns(exercise: Exercise)
expect_status(200)
expect_template(:new)
end
describe 'GET #show' do
context 'as admin' do
before(:each) { get :show, params: { id: exercise.id } }
expect_assigns(exercise: :exercise)
expect_status(200)
expect_template(:show)
end
end
describe 'GET #reload' do
context 'as anyone' do
before(:each) { get :reload, format: :json, params: { id: exercise.id } }
expect_assigns(exercise: :exercise)
expect_status(200)
expect_template(:reload)
end
end
describe 'GET #statistics' do
before(:each) { get :statistics, params: { id: exercise.id } }
expect_assigns(exercise: :exercise)
expect_status(200)
expect_template(:statistics)
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(:user) { FactoryBot.create(:external_user) }
let!(:lti_parameter) { FactoryBot.create(:lti_parameter, external_user: user, exercise: exercise) }
before(:each) do
allow_any_instance_of(Submission).to receive(:normalized_score).and_return(1)
expect(controller).to receive(:collect_test_results).and_return([{score: 1, weight: 1}])
expect(controller).to receive(:score_submission).and_call_original
end
context 'when LTI outcomes are supported' do
before(:each) do
expect(controller).to receive(:lti_outcome_service?).and_return(true)
end
context 'when the score transmission succeeds' do
before(:each) do
expect(controller).to receive(:send_score).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_status(200)
end
context 'when the score transmission fails' do
before(:each) do
expect(controller).to receive(:send_score).and_return(status: 'unsupported')
perform_request
end
expect_assigns(exercise: :exercise)
it 'creates a submission' do
expect(assigns(:submission)).to be_a(Submission)
end
expect_json
expect_status(503)
end
end
context 'when LTI outcomes are not supported' do
before(:each) do
expect(controller).to receive(:lti_outcome_service?).and_return(false)
expect(controller).not_to receive(:send_score)
perform_request
end
expect_assigns(exercise: :exercise)
it 'creates a submission' do
expect(assigns(:submission)).to be_a(Submission)
end
expect_json
expect_status(200)
end
end
describe 'PUT #update' do
context 'with a valid exercise' do
let(:exercise_attributes) { FactoryBot.build(:dummy).attributes }
before(:each) { put :update, params: { exercise: exercise_attributes, id: exercise.id } }
expect_assigns(exercise: Exercise)
expect_redirect(:exercise)
end
context 'with an invalid exercise' do
before(:each) { put :update, params: { exercise: {title: ''}, id: exercise.id } }
expect_assigns(exercise: Exercise)
expect_status(200)
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) { FactoryBot.create(:codeharbor_link, user: user) }
let(:external_check_hash) { {message: message, exercise_found: true, update_right: update_right, error: error} }
let(:message) { 'message' }
let(:update_right) { true }
let(:error) {}
before { allow(ExerciseService::CheckExternal).to receive(:call).with(uuid: exercise.uuid, codeharbor_link: codeharbor_link).and_return(external_check_hash) }
it 'renders the correct contents as json' do
post_request
expect(JSON.parse(response.body).symbolize_keys[:message]).to eq('message')
expect(JSON.parse(response.body).symbolize_keys[:actions]).to(
include('button').and(include('Abort').and(include('Export')))
)
expect(JSON.parse(response.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(JSON.parse(response.body).symbolize_keys[:message]).to eq('message')
expect(JSON.parse(response.body).symbolize_keys[:actions]).to(
include('button').and(include('Abort')).and(include('Retry'))
)
expect(JSON.parse(response.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(JSON.parse(response.body).symbolize_keys[:message]).to eq('message')
expect(JSON.parse(response.body).symbolize_keys[:actions]).to(
include('button').and(include('Abort'))
)
expect(JSON.parse(response.body).symbolize_keys[:actions]).to(
not_include('Retry').and(not_include('Export')).and(not_include('Hide'))
)
end
end
end
describe '#export_external_confirm' do
render_views
let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let(:post_request) { post :export_external_confirm, params: {id: exercise.id, codeharbor_link: codeharbor_link.id} }
let(:error) {}
let(:zip) { 'zip' }
before do
allow(ProformaService::ExportTask).to receive(:call).with(exercise: exercise).and_return(zip)
allow(ExerciseService::PushExternal).to receive(:call).with(zip: zip, codeharbor_link: codeharbor_link).and_return(error)
end
it 'renders correct response' do
post_request
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body).symbolize_keys[:message]).to(include('successfully exported'))
expect(JSON.parse(response.body).symbolize_keys[:status]).to(eql('success'))
expect(JSON.parse(response.body).symbolize_keys[:actions]).to(include('button').and(include('Close')))
expect(JSON.parse(response.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(JSON.parse(response.body).symbolize_keys[:message]).to(include('failed').and(include('exampleerror')))
expect(JSON.parse(response.body).symbolize_keys[:status]).to(eql('fail'))
expect(JSON.parse(response.body).symbolize_keys[:actions]).to(include('button').and(include('Retry')).and(include('Close')))
expect(JSON.parse(response.body).symbolize_keys[:actions]).to(not_include('Abort'))
end
end
end
describe '#import_uuid_check' do
let(:exercise) { FactoryBot.create(:dummy, uuid: SecureRandom.uuid) }
let!(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let(:uuid) { exercise.reload.uuid }
let(:post_request) { post :import_uuid_check, params: {uuid: 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(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true
expect(JSON.parse(response.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) { FactoryBot.create(:codeharbor_link, api_key: 'anotherkey') }
it 'renders correct response' do
post_request
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true
expect(JSON.parse(response.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(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be false
end
end
end
describe 'POST #import_exercise' do
let(:codeharbor_link) { FactoryBot.create(:codeharbor_link, user: user) }
let!(:imported_exercise) { FactoryBot.create(:fibonacci) }
let(:post_request) { post :import_exercise, 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: user)
end
context 'when import fails with ProformaError' do
before { allow(ProformaService::Import).to receive(:call).and_raise(Proforma::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(Proforma::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