
17 tests are always failing, due to changes introduced when adding the Runner abstraction. To know only these fail, they now get skipped in order to make it apparent if tests that should not fail do fail in the pipeline.
532 lines
17 KiB
Ruby
532 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 { 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 { 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 { 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 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) { FactoryBot.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: 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 { post :create, params: {exercise: {}} }
|
|
|
|
expect_assigns(exercise: Exercise)
|
|
expect_status(200)
|
|
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 = 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 { 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 { 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 { 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 do
|
|
FactoryBot.create_pair(:dummy)
|
|
get :index
|
|
end
|
|
|
|
expect_assigns(exercises: :scope)
|
|
expect_status(200)
|
|
expect_template(:index)
|
|
end
|
|
|
|
describe 'GET #new' do
|
|
before { 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 'when being admin' do
|
|
before { get :show, params: {id: exercise.id} }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_status(200)
|
|
expect_template(:show)
|
|
end
|
|
end
|
|
|
|
describe 'GET #reload' do
|
|
context 'when being anyone' do
|
|
before { 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 { get :statistics, params: {id: exercise.id} }
|
|
|
|
expect_assigns(exercise: :exercise)
|
|
expect_status(200)
|
|
expect_template(:statistics)
|
|
end
|
|
|
|
# This is broken since the Runner was added.
|
|
describe 'POST #submit', skip: true 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) }
|
|
|
|
before do
|
|
FactoryBot.create(:lti_parameter, external_user: user, exercise: exercise)
|
|
allow_any_instance_of(Submission).to receive(:normalized_score).and_return(1)
|
|
allow(controller).to receive(:collect_test_results).and_return([{score: 1, weight: 1}])
|
|
allow(controller).to receive(:score_submission).and_call_original
|
|
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_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 do
|
|
allow(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 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_score)
|
|
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 { 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_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) { nil }
|
|
|
|
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) { nil }
|
|
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
|