transferred Code Ocean from original repository to GitHub
This commit is contained in:
170
spec/concerns/lti_spec.rb
Normal file
170
spec/concerns/lti_spec.rb
Normal file
@ -0,0 +1,170 @@
|
||||
require 'rails_helper'
|
||||
|
||||
class Controller < AnonymousController
|
||||
include Lti
|
||||
end
|
||||
|
||||
describe Lti do
|
||||
let(:controller) { Controller.new }
|
||||
let(:session) { double }
|
||||
|
||||
describe '#build_tool_provider' do
|
||||
it 'instantiates a tool provider' do
|
||||
expect(IMS::LTI::ToolProvider).to receive(:new)
|
||||
controller.send(:build_tool_provider, consumer: FactoryGirl.build(:consumer), parameters: {})
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clear_lti_session_data' do
|
||||
it 'clears the session' do
|
||||
expect(controller.session).to receive(:delete).with(:consumer_id)
|
||||
expect(controller.session).to receive(:delete).with(:external_user_id)
|
||||
expect(controller.session).to receive(:delete).with(:lti_parameters)
|
||||
controller.send(:clear_lti_session_data)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#external_user_name' do
|
||||
let(:first_name) { 'Jane' }
|
||||
let(:full_name) { 'John Doe' }
|
||||
let(:last_name) { 'Doe' }
|
||||
let(:provider) { double }
|
||||
|
||||
context 'when a full name is provided' do
|
||||
it 'returns the full name' do
|
||||
expect(provider).to receive(:lis_person_name_full).twice.and_return(full_name)
|
||||
expect(controller.send(:external_user_name, provider)).to eq(full_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first and last name are provided' do
|
||||
it 'returns the concatenated names' do
|
||||
expect(provider).to receive(:lis_person_name_full)
|
||||
expect(provider).to receive(:lis_person_name_given).twice.and_return(first_name)
|
||||
expect(provider).to receive(:lis_person_name_family).twice.and_return(last_name)
|
||||
expect(controller.send(:external_user_name, provider)).to eq("#{first_name} #{last_name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only partial information is provided' do
|
||||
it 'returns the first available name' do
|
||||
expect(provider).to receive(:lis_person_name_full)
|
||||
expect(provider).to receive(:lis_person_name_given).twice.and_return(first_name)
|
||||
expect(provider).to receive(:lis_person_name_family)
|
||||
expect(controller.send(:external_user_name, provider)).to eq(first_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refuse_lti_launch' do
|
||||
it 'returns to the tool consumer' do
|
||||
message = I18n.t('sessions.oauth.invalid_consumer')
|
||||
expect(controller).to receive(:return_to_consumer).with(lti_errorlog: message, lti_errormsg: I18n.t('sessions.oauth.failure'))
|
||||
controller.send(:refuse_lti_launch, message: message)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#return_to_consumer' do
|
||||
context 'with a return URL' do
|
||||
let(:consumer_return_url) { 'http://example.org' }
|
||||
before(:each) { expect(controller).to receive(:params).and_return({launch_presentation_return_url: consumer_return_url}) }
|
||||
|
||||
it 'redirects to the tool consumer' do
|
||||
expect(controller).to receive(:redirect_to).with(consumer_return_url)
|
||||
controller.send(:return_to_consumer)
|
||||
end
|
||||
|
||||
it 'passes messages to the consumer' do
|
||||
message = I18n.t('sessions.oauth.failure')
|
||||
expect(controller).to receive(:redirect_to).with("#{consumer_return_url}?lti_errorlog=#{CGI.escape(message)}")
|
||||
controller.send(:return_to_consumer, lti_errorlog: message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a return URL' do
|
||||
before(:each) do
|
||||
expect(controller).to receive(:params).and_return({})
|
||||
expect(controller).to receive(:redirect_to).with(:root)
|
||||
end
|
||||
|
||||
it 'redirects to the root URL' do
|
||||
controller.send(:return_to_consumer)
|
||||
end
|
||||
|
||||
it 'displays alerts' do
|
||||
message = I18n.t('sessions.oauth.failure')
|
||||
controller.send(:return_to_consumer, lti_errormsg: message)
|
||||
end
|
||||
|
||||
it 'displays notices' do
|
||||
message = I18n.t('sessions.oauth.success')
|
||||
controller.send(:return_to_consumer, lti_msg: message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_score' do
|
||||
let(:consumer) { FactoryGirl.create(:consumer) }
|
||||
let(:score) { 0.5 }
|
||||
|
||||
before(:each) do
|
||||
controller.session[:consumer_id] = consumer.id
|
||||
controller.session[:lti_parameters] = {}
|
||||
end
|
||||
|
||||
context 'when grading is not supported' do
|
||||
it 'returns a corresponding status' do
|
||||
expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(false)
|
||||
expect(controller.send(:send_score, score)[:status]).to eq('unsupported')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when grading is supported' do
|
||||
let(:response) { double }
|
||||
|
||||
before(:each) do
|
||||
expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:outcome_service?).and_return(true)
|
||||
expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:post_replace_result!).with(score).and_return(response)
|
||||
expect(response).to receive(:response_code).at_least(:once).and_return(200)
|
||||
expect(response).to receive(:post_response).and_return(response)
|
||||
expect(response).to receive(:body).at_least(:once).and_return('')
|
||||
expect(response).to receive(:code_major).at_least(:once).and_return('success')
|
||||
end
|
||||
|
||||
it 'sends the score' do
|
||||
controller.send(:send_score, score)
|
||||
end
|
||||
|
||||
it 'returns code, message, and status' do
|
||||
result = controller.send(:send_score, score)
|
||||
expect(result[:code]).to eq(response.response_code)
|
||||
expect(result[:message]).to eq(response.body)
|
||||
expect(result[:status]).to eq(response.code_major)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#store_lti_session_data' do
|
||||
let(:parameters) { {} }
|
||||
before(:each) { controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user)) }
|
||||
after(:each) { controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters) }
|
||||
|
||||
it 'stores data in the session' do
|
||||
expect(controller.session).to receive(:[]=).with(:consumer_id, anything)
|
||||
expect(controller.session).to receive(:[]=).with(:external_user_id, anything)
|
||||
expect(controller.session).to receive(:[]=).with(:lti_parameters, kind_of(Hash))
|
||||
end
|
||||
|
||||
it 'stores only selected tuples' do
|
||||
expect(parameters).to receive(:slice).with(*Lti::SESSION_PARAMETERS)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#store_nonce' do
|
||||
it 'adds a nonce to the nonce store' do
|
||||
nonce = SecureRandom.hex
|
||||
expect(NonceStore).to receive(:add).with(nonce)
|
||||
controller.send(:store_nonce, nonce)
|
||||
end
|
||||
end
|
||||
end
|
36
spec/concerns/submission_scoring_spec.rb
Normal file
36
spec/concerns/submission_scoring_spec.rb
Normal file
@ -0,0 +1,36 @@
|
||||
require 'rails_helper'
|
||||
|
||||
class Controller < AnonymousController
|
||||
include SubmissionScoring
|
||||
end
|
||||
|
||||
describe SubmissionScoring do
|
||||
let(:controller) { Controller.new }
|
||||
let(:submission) { FactoryGirl.create(:submission, cause: 'submit') }
|
||||
before(:each) { controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user)) }
|
||||
|
||||
describe '#score_submission', docker: true do
|
||||
let(:score_submission) { Proc.new { controller.score_submission(submission) } }
|
||||
before(:each) { score_submission.call }
|
||||
|
||||
it 'assigns @assessor' do
|
||||
expect(controller.instance_variable_get(:@assessor)).to be_an(Assessor)
|
||||
end
|
||||
|
||||
it 'assigns @docker_client' do
|
||||
expect(controller.instance_variable_get(:@docker_client)).to be_a(DockerClient)
|
||||
end
|
||||
|
||||
it 'executes the teacher-defined test cases' do
|
||||
submission.collect_files.select(&:teacher_defined_test?).each do |file|
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_test_command).with(submission, file.name_with_extension).and_call_original
|
||||
end
|
||||
score_submission.call
|
||||
end
|
||||
|
||||
it 'updates the submission' do
|
||||
expect(submission).to receive(:update).with(score: anything)
|
||||
score_submission.call
|
||||
end
|
||||
end
|
||||
end
|
45
spec/controllers/application_controller_spec.rb
Normal file
45
spec/controllers/application_controller_spec.rb
Normal file
@ -0,0 +1,45 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ApplicationController do
|
||||
describe '#current_user' do
|
||||
context 'with an external user' do
|
||||
let(:external_user) { FactoryGirl.create(:external_user) }
|
||||
before(:each) { session[:external_user_id] = external_user.id }
|
||||
|
||||
it 'returns the external user' do
|
||||
expect(controller.current_user).to eq(external_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without an external user' do
|
||||
let(:internal_user) { FactoryGirl.create(:teacher) }
|
||||
before(:each) { login_user(internal_user) }
|
||||
|
||||
it 'returns the internal user' do
|
||||
expect(controller.current_user).to eq(internal_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#render_not_authorized' do
|
||||
let(:render_not_authorized) { controller.send(:render_not_authorized) }
|
||||
|
||||
it 'displays a flash message' do
|
||||
expect(controller).to receive(:redirect_to)
|
||||
render_not_authorized
|
||||
expect(flash[:danger]).to eq(I18n.t('application.not_authorized'))
|
||||
end
|
||||
|
||||
it 'redirects to the root URL' do
|
||||
expect(controller).to receive(:redirect_to).with(:root)
|
||||
render_not_authorized
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #welcome' do
|
||||
before(:each) { get :welcome }
|
||||
|
||||
expect_status(200)
|
||||
expect_template(:welcome)
|
||||
end
|
||||
end
|
21
spec/controllers/code_ocean/files_controller_spec.rb
Normal file
21
spec/controllers/code_ocean/files_controller_spec.rb
Normal file
@ -0,0 +1,21 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe CodeOcean::FilesController do
|
||||
let(:user) { FactoryGirl.build(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let(:exercise) { FactoryGirl.create(:fibonacci) }
|
||||
let(:request) { Proc.new { delete :destroy, id: exercise.files.first.id } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(file: CodeOcean::File)
|
||||
|
||||
it 'destroys the file' do
|
||||
exercise = FactoryGirl.create(:fibonacci)
|
||||
expect { request.call }.to change(CodeOcean::File, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
end
|
93
spec/controllers/consumers_controller_spec.rb
Normal file
93
spec/controllers/consumers_controller_spec.rb
Normal file
@ -0,0 +1,93 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ConsumersController do
|
||||
let(:consumer) { FactoryGirl.create(:consumer) }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'with a valid consumer' do
|
||||
let(:request) { Proc.new { post :create, consumer: FactoryGirl.attributes_for(:consumer) } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(consumer: Consumer)
|
||||
|
||||
it 'creates the consumer' do
|
||||
expect { request.call }.to change(Consumer, :count).by(1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid consumer' do
|
||||
before(:each) { post :create, consumer: {} }
|
||||
|
||||
expect_assigns(consumer: Consumer)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before(:each) { delete :destroy, id: consumer.id }
|
||||
|
||||
expect_assigns(consumer: Consumer)
|
||||
|
||||
it 'destroys the consumer' do
|
||||
consumer = FactoryGirl.create(:consumer)
|
||||
expect { delete :destroy, id: consumer.id }.to change(Consumer, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect(:consumers)
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
before(:each) { get :edit, id: consumer.id }
|
||||
|
||||
expect_assigns(consumer: Consumer)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:consumers) { FactoryGirl.create_pair(:consumer) }
|
||||
before(:each) { get :index }
|
||||
|
||||
expect_assigns(consumers: Consumer.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
before(:each) { get :new }
|
||||
|
||||
expect_assigns(consumer: Consumer)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, id: consumer.id }
|
||||
|
||||
expect_assigns(consumer: :consumer)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'with a valid consumer' do
|
||||
before(:each) { put :update, consumer: FactoryGirl.attributes_for(:consumer), id: consumer.id }
|
||||
|
||||
expect_assigns(consumer: Consumer)
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid consumer' do
|
||||
before(:each) { put :update, consumer: {name: ''}, id: consumer.id }
|
||||
|
||||
expect_assigns(consumer: Consumer)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
85
spec/controllers/errors_controller_spec.rb
Normal file
85
spec/controllers/errors_controller_spec.rb
Normal file
@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ErrorsController do
|
||||
let(:error) { FactoryGirl.create(:error) }
|
||||
let(:execution_environment) { error.execution_environment }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'with a valid error' do
|
||||
let(:request) { Proc.new { post :create, execution_environment_id: FactoryGirl.build(:error).execution_environment.id, error: FactoryGirl.attributes_for(:error), format: :json } }
|
||||
|
||||
context 'when a hint can be matched' do
|
||||
let(:hint) { FactoryGirl.build(:ruby_syntax_error).message }
|
||||
|
||||
before(:each) do
|
||||
expect_any_instance_of(Whistleblower).to receive(:generate_hint).and_return(hint)
|
||||
request.call
|
||||
end
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
|
||||
it 'does not create the error' do
|
||||
allow_any_instance_of(Whistleblower).to receive(:generate_hint).and_return(hint)
|
||||
expect { request.call }.not_to change(Error, :count)
|
||||
end
|
||||
|
||||
it 'returns the hint' do
|
||||
expect(response.body).to eq({hint: hint}.to_json)
|
||||
end
|
||||
|
||||
expect_content_type('application/json')
|
||||
expect_status(200)
|
||||
end
|
||||
|
||||
context 'when no hint can be matched' do
|
||||
before(:each) do
|
||||
expect_any_instance_of(Whistleblower).to receive(:generate_hint).and_return(nil)
|
||||
request.call
|
||||
end
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
|
||||
it 'creates the error' do
|
||||
allow_any_instance_of(Whistleblower).to receive(:generate_hint)
|
||||
expect { request.call }.to change(Error, :count).by(1)
|
||||
end
|
||||
|
||||
expect_content_type('application/json')
|
||||
expect_status(201)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid error' do
|
||||
before(:each) { post :create, execution_environment_id: FactoryGirl.build(:error).execution_environment.id, error: {}, format: :json }
|
||||
|
||||
expect_assigns(error: Error)
|
||||
expect_content_type('application/json')
|
||||
expect_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:errors) { FactoryGirl.create_pair(:error) }
|
||||
before(:each) { get :index, execution_environment_id: execution_environment.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
|
||||
it 'aggregates errors by message' do
|
||||
expect(errors.count).to be_a(Numeric)
|
||||
end
|
||||
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, execution_environment_id: execution_environment.id, id: error.id }
|
||||
|
||||
expect_assigns(error: :error)
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
end
|
152
spec/controllers/execution_environments_controller_spec.rb
Normal file
152
spec/controllers/execution_environments_controller_spec.rb
Normal file
@ -0,0 +1,152 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExecutionEnvironmentsController do
|
||||
let(:execution_environment) { FactoryGirl.create(:ruby) }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
before(:each) { expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([]) }
|
||||
|
||||
context 'with a valid execution environment' do
|
||||
let(:request) { Proc.new { post :create, execution_environment: FactoryGirl.attributes_for(:ruby) } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(docker_images: Array)
|
||||
expect_assigns(execution_environment: ExecutionEnvironment)
|
||||
|
||||
it 'creates the execution environment' do
|
||||
expect { request.call }.to change(ExecutionEnvironment, :count).by(1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid execution environment' do
|
||||
before(:each) { post :create, execution_environment: {} }
|
||||
|
||||
expect_assigns(execution_environment: ExecutionEnvironment)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before(:each) { delete :destroy, id: execution_environment.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
|
||||
it 'destroys the execution environment' do
|
||||
execution_environment = FactoryGirl.create(:ruby)
|
||||
expect { delete :destroy, id: execution_environment.id }.to change(ExecutionEnvironment, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect(:execution_environments)
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
before(:each) do
|
||||
expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([])
|
||||
get :edit, id: execution_environment.id
|
||||
end
|
||||
|
||||
expect_assigns(docker_images: Array)
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
|
||||
describe 'POST #execute_command' do
|
||||
let(:command) { 'which ruby' }
|
||||
|
||||
before(:each) do
|
||||
expect(DockerClient).to receive(:new).with(execution_environment: execution_environment, user: user).and_call_original
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_command).with(command)
|
||||
post :execute_command, command: command, id: execution_environment.id
|
||||
end
|
||||
|
||||
expect_assigns(docker_client: DockerClient)
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_content_type('application/json')
|
||||
expect_status(200)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:execution_environments) { FactoryGirl.create_pair(:ruby) }
|
||||
before(:each) { get :index }
|
||||
|
||||
expect_assigns(execution_environments: ExecutionEnvironment.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
before(:each) do
|
||||
expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([])
|
||||
get :new
|
||||
end
|
||||
|
||||
expect_assigns(docker_images: Array)
|
||||
expect_assigns(execution_environment: ExecutionEnvironment)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
|
||||
describe '#set_docker_images', docker: true do
|
||||
context 'when Docker is unavailable' do
|
||||
let(:error_message) { 'Docker is unavailable' }
|
||||
|
||||
before(:each) do
|
||||
expect(DockerClient).to receive(:check_availability!).at_least(:once).and_raise(DockerClient::Error.new(error_message))
|
||||
controller.send(:set_docker_images)
|
||||
end
|
||||
|
||||
it 'fails gracefully' do
|
||||
expect { controller.send(:set_docker_images) }.not_to raise_error
|
||||
end
|
||||
|
||||
expect_assigns(docker_images: Array)
|
||||
|
||||
it 'displays a flash message' do
|
||||
expect(flash[:warning]).to eq(error_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #shell' do
|
||||
before(:each) { get :shell, id: execution_environment.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_status(200)
|
||||
expect_template(:shell)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, id: execution_environment.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'with a valid execution environment' do
|
||||
before(:each) do
|
||||
expect(DockerClient).to receive(:image_tags).at_least(:once).and_return([])
|
||||
put :update, execution_environment: FactoryGirl.attributes_for(:ruby), id: execution_environment.id
|
||||
end
|
||||
|
||||
expect_assigns(docker_images: Array)
|
||||
expect_assigns(execution_environment: ExecutionEnvironment)
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid execution environment' do
|
||||
before(:each) { put :update, execution_environment: {name: ''}, id: execution_environment.id }
|
||||
|
||||
expect_assigns(execution_environment: ExecutionEnvironment)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
213
spec/controllers/exercises_controller_spec.rb
Normal file
213
spec/controllers/exercises_controller_spec.rb
Normal file
@ -0,0 +1,213 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExercisesController do
|
||||
let(:exercise) { FactoryGirl.create(:fibonacci) }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:exercise_attributes) { FactoryGirl.build(:fibonacci).attributes }
|
||||
|
||||
context 'with a valid exercise' do
|
||||
let(:request) { Proc.new { post :create, exercise: exercise_attributes } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(exercise: Exercise)
|
||||
|
||||
it 'creates the exercise' do
|
||||
expect { request.call }.to change(Exercise, :count).by(1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'when including a file' do
|
||||
let(:files_attributes) { {'0' => FactoryGirl.build(:file).attributes} }
|
||||
let(:request) { Proc.new { post :create, exercise: exercise_attributes.merge(files_attributes: files_attributes) } }
|
||||
|
||||
it 'creates the file' do
|
||||
expect { request.call }.to change(CodeOcean::File, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a file upload" do
|
||||
let(:files_attributes) { {'0' => FactoryGirl.build(:file, content: fixture_file_upload('upload.rb', 'text/x-ruby')).attributes} }
|
||||
let(:request) { Proc.new { post :create, exercise: exercise_attributes.merge(files_attributes: files_attributes) } }
|
||||
|
||||
it 'creates the file' do
|
||||
expect { request.call }.to change(CodeOcean::File, :count)
|
||||
end
|
||||
|
||||
it 'assigns the file content' do
|
||||
request.call
|
||||
file = File.new(Rails.root.join('spec', 'fixtures', 'upload.rb'), 'r')
|
||||
expect(Exercise.last.files.first.content).to eq(file.read)
|
||||
file.close
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid exercise' do
|
||||
before(:each) { post :create, exercise: {} }
|
||||
|
||||
expect_assigns(exercise: Exercise)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before(:each) { delete :destroy, id: exercise.id }
|
||||
|
||||
expect_assigns(exercise: :exercise)
|
||||
|
||||
it 'destroys the exercise' do
|
||||
exercise = FactoryGirl.create(:fibonacci)
|
||||
expect { delete :destroy, id: exercise.id }.to change(Exercise, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect(:exercises)
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
before(:each) { get :edit, id: exercise.id }
|
||||
|
||||
expect_assigns(exercise: :exercise)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
|
||||
describe 'GET #implement' do
|
||||
let(:request) { Proc.new { get :implement, id: exercise.id } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(exercise: :exercise)
|
||||
|
||||
context 'with an existing submission' do
|
||||
let!(:submission) { FactoryGirl.create(:submission, exercise_id: exercise.id, user_id: user.id, user_type: InternalUser.class.name) }
|
||||
|
||||
it "populates the editors with the submission's files' content" do
|
||||
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
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:exercises) { FactoryGirl.create_pair(:fibonacci) }
|
||||
let(:scope) { Pundit.policy_scope!(user, Exercise) }
|
||||
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
|
||||
before(:each) { get :show, id: exercise.id }
|
||||
|
||||
expect_assigns(exercise: :exercise)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'POST #submit' do
|
||||
let(:output) { {} }
|
||||
let(:request) { post :submit, format: :json, id: exercise.id, submission: {cause: 'submit', exercise_id: exercise.id} }
|
||||
|
||||
before(:each) do
|
||||
expect(controller).to receive(:execute_test_files).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'})
|
||||
request
|
||||
end
|
||||
|
||||
expect_assigns(exercise: :exercise)
|
||||
|
||||
it 'creates a submission' do
|
||||
expect(assigns(:submission)).to be_a(Submission)
|
||||
end
|
||||
|
||||
expect_content_type('application/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'})
|
||||
request
|
||||
end
|
||||
|
||||
expect_assigns(exercise: :exercise)
|
||||
|
||||
it 'creates a submission' do
|
||||
expect(assigns(:submission)).to be_a(Submission)
|
||||
end
|
||||
|
||||
expect_content_type('application/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)
|
||||
request
|
||||
end
|
||||
|
||||
expect_assigns(exercise: :exercise)
|
||||
|
||||
it 'creates a submission' do
|
||||
expect(assigns(:submission)).to be_a(Submission)
|
||||
end
|
||||
|
||||
expect_content_type('application/json')
|
||||
expect_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'with a valid exercise' do
|
||||
let(:exercise_attributes) { FactoryGirl.build(:fibonacci).attributes }
|
||||
before(:each) { put :update, exercise: exercise_attributes, id: exercise.id }
|
||||
|
||||
expect_assigns(exercise: Exercise)
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid exercise' do
|
||||
before(:each) { put :update, exercise: {title: ''}, id: exercise.id }
|
||||
|
||||
expect_assigns(exercise: Exercise)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
23
spec/controllers/external_users_controller_spec.rb
Normal file
23
spec/controllers/external_users_controller_spec.rb
Normal file
@ -0,0 +1,23 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExternalUsersController do
|
||||
let(:user) { FactoryGirl.build(:admin) }
|
||||
let!(:users) { FactoryGirl.create_pair(:external_user) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'GET #index' do
|
||||
before(:each) { get :index }
|
||||
|
||||
expect_assigns(users: ExternalUser.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, id: users.first.id }
|
||||
|
||||
expect_assigns(user: ExternalUser)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
end
|
99
spec/controllers/file_types_controller_spec.rb
Normal file
99
spec/controllers/file_types_controller_spec.rb
Normal file
@ -0,0 +1,99 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe FileTypesController do
|
||||
let(:file_type) { FactoryGirl.create(:dot_rb) }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'with a valid file type' do
|
||||
let(:request) { Proc.new { post :create, file_type: FactoryGirl.attributes_for(:dot_rb) } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(editor_modes: Array)
|
||||
expect_assigns(file_type: FileType)
|
||||
|
||||
it 'creates the file type' do
|
||||
expect { request.call }.to change(FileType, :count).by(1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid file type' do
|
||||
before(:each) { post :create, file_type: {} }
|
||||
|
||||
expect_assigns(editor_modes: Array)
|
||||
expect_assigns(file_type: FileType)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before(:each) { delete :destroy, id: file_type.id }
|
||||
|
||||
expect_assigns(file_type: FileType)
|
||||
|
||||
it 'destroys the file type' do
|
||||
file_type = FactoryGirl.create(:dot_rb)
|
||||
expect { delete :destroy, id: file_type.id }.to change(FileType, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect(:file_types)
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
before(:each) { get :edit, id: file_type.id }
|
||||
|
||||
expect_assigns(editor_modes: Array)
|
||||
expect_assigns(file_type: FileType)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:file_types) { FactoryGirl.create_pair(:dot_rb) }
|
||||
before(:each) { get :index }
|
||||
|
||||
expect_assigns(file_types: FileType.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
before(:each) { get :new }
|
||||
|
||||
expect_assigns(editor_modes: Array)
|
||||
expect_assigns(file_type: FileType)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, id: file_type.id }
|
||||
|
||||
expect_assigns(file_type: :file_type)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'with a valid file type' do
|
||||
before(:each) { put :update, file_type: FactoryGirl.attributes_for(:dot_rb), id: file_type.id }
|
||||
|
||||
expect_assigns(editor_modes: Array)
|
||||
expect_assigns(file_type: FileType)
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid file type' do
|
||||
before(:each) { put :update, file_type: {name: ''}, id: file_type.id }
|
||||
|
||||
expect_assigns(editor_modes: Array)
|
||||
expect_assigns(file_type: FileType)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
103
spec/controllers/hints_controller_spec.rb
Normal file
103
spec/controllers/hints_controller_spec.rb
Normal file
@ -0,0 +1,103 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe HintsController do
|
||||
let(:execution_environment) { FactoryGirl.create(:ruby) }
|
||||
let(:hint) { FactoryGirl.create(:ruby_syntax_error) }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'with a valid hint' do
|
||||
let(:request) { Proc.new { post :create, execution_environment_id: execution_environment.id, hint: FactoryGirl.attributes_for(:ruby_syntax_error) } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: Hint)
|
||||
|
||||
it 'creates the hint' do
|
||||
expect { request.call }.to change(Hint, :count).by(1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid hint' do
|
||||
before(:each) { post :create, execution_environment_id: execution_environment.id, hint: {} }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: Hint)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before(:each) { delete :destroy, execution_environment_id: execution_environment.id, id: hint.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: Hint)
|
||||
|
||||
it 'destroys the hint' do
|
||||
hint = FactoryGirl.create(:ruby_syntax_error)
|
||||
expect { delete :destroy, execution_environment_id: execution_environment.id, id: hint.id }.to change(Hint, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
before(:each) { get :edit, execution_environment_id: execution_environment.id, id: hint.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: Hint)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:hints) { FactoryGirl.create_pair(:ruby_syntax_error) }
|
||||
before(:each) { get :index, execution_environment_id: execution_environment.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hints: Hint.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
before(:each) { get :new, execution_environment_id: execution_environment.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: Hint)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, execution_environment_id: execution_environment.id, id: hint.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: :hint)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'with a valid hint' do
|
||||
before(:each) { put :update, execution_environment_id: execution_environment.id, hint: FactoryGirl.attributes_for(:ruby_syntax_error), id: hint.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: Hint)
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid hint' do
|
||||
before(:each) { put :update, execution_environment_id: execution_environment.id, hint: {name: ''}, id: hint.id }
|
||||
|
||||
expect_assigns(execution_environment: :execution_environment)
|
||||
expect_assigns(hint: Hint)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
212
spec/controllers/internal_users_controller_spec.rb
Normal file
212
spec/controllers/internal_users_controller_spec.rb
Normal file
@ -0,0 +1,212 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe InternalUsersController do
|
||||
let(:user) { FactoryGirl.build(:admin) }
|
||||
let!(:users) { FactoryGirl.create_pair(:teacher) }
|
||||
|
||||
describe 'GET #activate' do
|
||||
let(:user) { InternalUser.create(FactoryGirl.attributes_for(:teacher)) }
|
||||
|
||||
before(:each) do
|
||||
user.send(:setup_activation)
|
||||
user.save(validate: false)
|
||||
end
|
||||
|
||||
context 'without a valid activation token' do
|
||||
before(:each) { get :activate, id: user.id }
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an already activated user' do
|
||||
before(:each) do
|
||||
user.activate!
|
||||
get :activate, id: user.id, token: user.activation_token
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with valid preconditions' do
|
||||
before(:each) { get :activate, id: user.id, token: user.activation_token }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
expect_status(200)
|
||||
expect_template(:activate)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #activate' do
|
||||
let(:user) { InternalUser.create(FactoryGirl.attributes_for(:teacher)) }
|
||||
let(:password) { SecureRandom.hex }
|
||||
|
||||
before(:each) do
|
||||
user.send(:setup_activation)
|
||||
user.save(validate: false)
|
||||
expect(user.activation_token).to be_present
|
||||
end
|
||||
|
||||
context 'without a valid activation token' do
|
||||
before(:each) { put :activate, id: user.id }
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an already activated user' do
|
||||
before(:each) do
|
||||
user.activate!
|
||||
put :activate, id: user.id, internal_user: {activation_token: user.activation_token, password: password, password_confirmation: password}
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'without a password' do
|
||||
before(:each) { put :activate, id: user.id, internal_user: {activation_token: user.activation_token} }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
|
||||
it 'builds a user with errors' do
|
||||
expect(assigns(:user).errors).to be_present
|
||||
end
|
||||
|
||||
expect_template(:activate)
|
||||
end
|
||||
|
||||
context 'without a valid password confirmation' do
|
||||
before(:each) { put :activate, id: user.id, internal_user: {activation_token: user.activation_token, password: password, password_confirmation: ''} }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
|
||||
it 'builds a user with errors' do
|
||||
expect(assigns(:user).errors).to be_present
|
||||
end
|
||||
|
||||
expect_template(:activate)
|
||||
end
|
||||
|
||||
context 'with valid preconditions' do
|
||||
before(:each) { put :activate, id: user.id, internal_user: {activation_token: user.activation_token, password: password, password_confirmation: password} }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
|
||||
it 'activates the user' do
|
||||
expect(assigns[:user]).to be_activated
|
||||
end
|
||||
|
||||
expect_flash_message(:notice, :'internal_users.activate.success')
|
||||
expect_redirect
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
context 'with a valid internal user' do
|
||||
let(:request) { Proc.new { post :create, internal_user: FactoryGirl.attributes_for(:teacher) } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
|
||||
it 'creates the internal user' do
|
||||
expect { request.call }.to change(InternalUser, :count).by(1)
|
||||
end
|
||||
|
||||
it 'creates an inactive user' do
|
||||
expect(InternalUser.last).not_to be_activated
|
||||
end
|
||||
|
||||
it 'sets up an activation token' do
|
||||
expect(InternalUser.last.activation_token).to be_present
|
||||
end
|
||||
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid internal user' do
|
||||
before(:each) { post :create, internal_user: {} }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before(:each) do
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
delete :destroy, id: users.first.id
|
||||
end
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
|
||||
it 'destroys the internal user' do
|
||||
expect { delete :destroy, id: InternalUser.last.id }.to change(InternalUser, :count).by(-1)
|
||||
end
|
||||
|
||||
expect_redirect(:internal_users)
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
before(:each) do
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
get :edit, id: users.first.id
|
||||
end
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
before(:each) do
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
get :index
|
||||
end
|
||||
|
||||
expect_assigns(users: InternalUser.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
before(:each) do
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
get :new
|
||||
end
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
expect_status(200)
|
||||
expect_template(:new)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) do
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
get :show, id: users.first.id
|
||||
end
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
context 'with a valid internal user' do
|
||||
before(:each) { put :update, internal_user: FactoryGirl.attributes_for(:teacher), id: users.first.id }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with an invalid internal user' do
|
||||
before(:each) { put :update, internal_user: {email: ''}, id: users.first.id }
|
||||
|
||||
expect_assigns(user: InternalUser)
|
||||
expect_status(200)
|
||||
expect_template(:edit)
|
||||
end
|
||||
end
|
||||
end
|
145
spec/controllers/sessions_controller_spec.rb
Normal file
145
spec/controllers/sessions_controller_spec.rb
Normal file
@ -0,0 +1,145 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SessionsController do
|
||||
let(:consumer) { FactoryGirl.create(:consumer) }
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:password) { user_attributes[:password] }
|
||||
let(:user) { InternalUser.create(user_attributes) }
|
||||
let(:user_attributes) { FactoryGirl.attributes_for(:teacher) }
|
||||
|
||||
context 'with valid credentials' do
|
||||
before(:each) do
|
||||
user.activate!
|
||||
post :create, email: user.email, password: password, remember_me: 1
|
||||
end
|
||||
|
||||
expect_flash_message(:notice, :'sessions.create.success')
|
||||
expect_redirect
|
||||
end
|
||||
|
||||
context 'with invalid credentials' do
|
||||
before(:each) { post :create, 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) { FactoryGirl.create(:fibonacci) }
|
||||
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, 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, 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
|
||||
expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true)
|
||||
expect(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, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a valid exercise token' do
|
||||
it 'refuses the LTI launch' do
|
||||
expect_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, custom_token: '', oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid launch parameters' do
|
||||
let(:request) { post :create_through_lti, custom_token: exercise.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id }
|
||||
let(:user) { FactoryGirl.create(:external_user, consumer_id: consumer.id) }
|
||||
before(:each) { expect_any_instance_of(IMS::LTI::ToolProvider).to receive(:valid_request?).and_return(true) }
|
||||
|
||||
it 'assigns the current user' do
|
||||
request
|
||||
expect(assigns(:current_user)).to be_an(ExternalUser)
|
||||
expect(session[:external_user_id]).to eq(user.id)
|
||||
end
|
||||
|
||||
it 'assigns the exercise' do
|
||||
request
|
||||
expect(assigns(:exercise)).to eq(exercise)
|
||||
end
|
||||
|
||||
it 'stores LTI parameters in the session' do
|
||||
expect(controller).to receive(:store_lti_session_data)
|
||||
request
|
||||
end
|
||||
|
||||
it 'stores the OAuth nonce' do
|
||||
expect(controller).to receive(:store_nonce).with(nonce)
|
||||
request
|
||||
end
|
||||
|
||||
context 'when LTI outcomes are supported' do
|
||||
before(:each) do
|
||||
expect(controller).to receive(:lti_outcome_service?).and_return(true)
|
||||
request
|
||||
end
|
||||
|
||||
it 'displays a flash message' do
|
||||
expect(flash[:notice]).to eq(I18n.t('sessions.create_through_lti.session_with_outcome', consumer: consumer))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when LTI outcomes are not supported' do
|
||||
before(:each) do
|
||||
expect(controller).to receive(:lti_outcome_service?).and_return(false)
|
||||
request
|
||||
end
|
||||
|
||||
it 'displays a flash message' do
|
||||
expect(flash[:notice]).to eq(I18n.t('sessions.create_through_lti.session_without_outcome', consumer: consumer))
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects to the requested exercise' do
|
||||
request
|
||||
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #destroy_through_lti' do
|
||||
let(:request) { Proc.new { get :destroy_through_lti, consumer_id: consumer.id, submission_id: submission.id } }
|
||||
let(:submission) { FactoryGirl.create(:submission) }
|
||||
|
||||
before(:each) do
|
||||
session[:consumer_id] = consumer.id
|
||||
session[:lti_parameters] = {}
|
||||
end
|
||||
|
||||
before(:each) { request.call }
|
||||
|
||||
it 'clears the session' do
|
||||
expect(controller).to receive(:clear_lti_session_data)
|
||||
request.call
|
||||
end
|
||||
|
||||
expect_status(200)
|
||||
expect_template(:destroy_through_lti)
|
||||
end
|
||||
end
|
171
spec/controllers/submissions_controller_spec.rb
Normal file
171
spec/controllers/submissions_controller_spec.rb
Normal file
@ -0,0 +1,171 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SubmissionsController do
|
||||
let(:submission) { FactoryGirl.create(:submission) }
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||
|
||||
describe 'POST #create' do
|
||||
before(:each) do
|
||||
controller.request.accept = 'application/json'
|
||||
end
|
||||
|
||||
context 'with a valid submission' do
|
||||
let(:exercise) { FactoryGirl.create(:hello_world) }
|
||||
let(:request) { Proc.new { post :create, format: :json, submission: FactoryGirl.attributes_for(:submission, exercise_id: exercise.id) } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(submission: Submission)
|
||||
|
||||
it 'creates the submission' do
|
||||
expect { request.call }.to change(Submission, :count).by(1)
|
||||
end
|
||||
|
||||
expect_content_type('application/json')
|
||||
expect_status(201)
|
||||
end
|
||||
|
||||
context 'with an invalid submission' do
|
||||
before(:each) { post :create, submission: {} }
|
||||
|
||||
expect_assigns(submission: Submission)
|
||||
expect_content_type('application/json')
|
||||
expect_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #download_file' do
|
||||
context 'with an invalid filename' do
|
||||
before(:each) { get :download_file, filename: SecureRandom.hex, id: submission.id }
|
||||
|
||||
expect_status(404)
|
||||
end
|
||||
|
||||
context 'with a valid filename' do
|
||||
let(:file) { submission.files.first }
|
||||
before(:each) { get :download_file, filename: file.name_with_extension, id: submission.id }
|
||||
|
||||
expect_assigns(file: :file)
|
||||
expect_assigns(submission: :submission)
|
||||
expect_content_type('application/octet-stream')
|
||||
expect_status(200)
|
||||
|
||||
it 'sets the correct filename' do
|
||||
expect(response.headers['Content-Disposition']).to eq("attachment; filename=\"#{file.name_with_extension}\"")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
let!(:submissions) { FactoryGirl.create_pair(:submission) }
|
||||
before(:each) { get :index }
|
||||
|
||||
expect_assigns(submissions: Submission.all)
|
||||
expect_status(200)
|
||||
expect_template(:index)
|
||||
end
|
||||
|
||||
describe 'GET #render_file' do
|
||||
context 'with an invalid filename' do
|
||||
before(:each) { get :render_file, filename: SecureRandom.hex, id: submission.id }
|
||||
|
||||
expect_status(404)
|
||||
end
|
||||
|
||||
context 'with a valid filename' do
|
||||
let(:file) { submission.files.first }
|
||||
let(:request) { Proc.new { get :render_file, filename: file.name_with_extension, id: submission.id } }
|
||||
before(:each) { request.call }
|
||||
|
||||
expect_assigns(file: :file)
|
||||
expect_assigns(submission: :submission)
|
||||
expect_status(200)
|
||||
|
||||
it 'renders the file content' do
|
||||
expect(response.body).to eq(file.content)
|
||||
end
|
||||
|
||||
it 'sets the correct MIME type' do
|
||||
mime_type = Mime::Type.lookup_by_extension('css')
|
||||
expect(Mime::Type).to receive(:lookup_by_extension).at_least(:once).and_return(mime_type)
|
||||
request.call
|
||||
expect(response.headers['Content-Type']).to eq(mime_type.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #run' do
|
||||
let(:filename) { submission.collect_files.detect(&:main_file?).name_with_extension }
|
||||
|
||||
before(:each) do
|
||||
expect_any_instance_of(ActionController::Live::SSE).to receive(:write).at_least(3).times
|
||||
end
|
||||
|
||||
context 'when no errors occur during execution' do
|
||||
before(:each) do
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_run_command).with(submission, filename).and_return({})
|
||||
get :run, filename: filename, id: submission.id
|
||||
end
|
||||
|
||||
expect_assigns(docker_client: DockerClient)
|
||||
expect_assigns(server_sent_event: ActionController::Live::SSE)
|
||||
expect_assigns(submission: :submission)
|
||||
expect_content_type('text/event-stream')
|
||||
expect_status(200)
|
||||
end
|
||||
|
||||
context 'when an error occurs during execution' do
|
||||
let(:hint) { "Your object 'main' of class 'Object' does not understand the method 'foo'." }
|
||||
let(:stderr) { "undefined method `foo' for main:Object (NoMethodError)" }
|
||||
|
||||
before(:each) do
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_run_command).with(submission, filename).and_yield(:stderr, stderr)
|
||||
end
|
||||
|
||||
after(:each) { get :run, filename: filename, id: submission.id }
|
||||
|
||||
context 'when the error is covered by a hint' do
|
||||
before(:each) do
|
||||
expect_any_instance_of(Whistleblower).to receive(:generate_hint).with(stderr).and_return(hint)
|
||||
end
|
||||
|
||||
it 'does not store the error' do
|
||||
expect(Error).not_to receive(:create)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the error is not covered by a hint' do
|
||||
before(:each) do
|
||||
expect_any_instance_of(Whistleblower).to receive(:generate_hint).with(stderr)
|
||||
end
|
||||
|
||||
it 'stores the error' do
|
||||
expect(Error).to receive(:create).with(execution_environment_id: submission.exercise.execution_environment_id, message: stderr)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
before(:each) { get :show, id: submission.id }
|
||||
|
||||
expect_assigns(submission: :submission)
|
||||
expect_status(200)
|
||||
expect_template(:show)
|
||||
end
|
||||
|
||||
describe 'GET #test' do
|
||||
let(:filename) { submission.collect_files.detect(&:teacher_defined_test?).name_with_extension }
|
||||
let(:output) { {} }
|
||||
|
||||
before(:each) do
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_test_command).with(submission, filename)
|
||||
get :test, filename: filename, id: submission.id
|
||||
end
|
||||
|
||||
expect_assigns(docker_client: DockerClient)
|
||||
expect_assigns(submission: :submission)
|
||||
expect_content_type('application/json')
|
||||
expect_status(200)
|
||||
end
|
||||
end
|
15
spec/factories/code_ocean/file.rb
Normal file
15
spec/factories/code_ocean/file.rb
Normal file
@ -0,0 +1,15 @@
|
||||
require 'seeds_helper'
|
||||
|
||||
module CodeOcean
|
||||
FactoryGirl.define do
|
||||
factory :file, class: CodeOcean::File do
|
||||
content ''
|
||||
association :context, factory: :submission
|
||||
association :file_type, factory: :dot_rb
|
||||
hidden false
|
||||
name { SecureRandom.hex }
|
||||
read_only false
|
||||
role 'main_file'
|
||||
end
|
||||
end
|
||||
end
|
12
spec/factories/consumer.rb
Normal file
12
spec/factories/consumer.rb
Normal file
@ -0,0 +1,12 @@
|
||||
FactoryGirl.define do
|
||||
factory :consumer do
|
||||
name 'openHPI'
|
||||
oauth_key { SecureRandom.hex }
|
||||
oauth_secret { SecureRandom.hex }
|
||||
singleton_consumer
|
||||
end
|
||||
|
||||
trait :singleton_consumer do
|
||||
initialize_with { Consumer.where(name: name).first_or_create }
|
||||
end
|
||||
end
|
6
spec/factories/error.rb
Normal file
6
spec/factories/error.rb
Normal file
@ -0,0 +1,6 @@
|
||||
FactoryGirl.define do
|
||||
factory :error do
|
||||
association :execution_environment, factory: :ruby
|
||||
message "exercise.rb:4:in `<main>': undefined local variable or method `foo' for main:Object (NameError)"
|
||||
end
|
||||
end
|
114
spec/factories/execution_environment.rb
Normal file
114
spec/factories/execution_environment.rb
Normal file
@ -0,0 +1,114 @@
|
||||
FactoryGirl.define do
|
||||
factory :coffee_script, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-coffee:latest'
|
||||
help
|
||||
name 'CoffeeScript'
|
||||
permitted_execution_time 10.seconds
|
||||
run_command 'coffee'
|
||||
singleton_execution_environment
|
||||
end
|
||||
|
||||
factory :html, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-html:latest'
|
||||
help
|
||||
name 'HTML5'
|
||||
permitted_execution_time 10.seconds
|
||||
run_command 'touch'
|
||||
singleton_execution_environment
|
||||
test_command 'rspec %{filename} --format documentation'
|
||||
testing_framework 'RspecAdapter'
|
||||
end
|
||||
|
||||
factory :java, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-java:latest'
|
||||
help
|
||||
name 'Java 8'
|
||||
permitted_execution_time 10.seconds
|
||||
run_command 'make run'
|
||||
singleton_execution_environment
|
||||
test_command 'make test CLASS_NAME="%{class_name}" FILENAME="%{filename}"'
|
||||
testing_framework 'JunitAdapter'
|
||||
end
|
||||
|
||||
factory :jruby, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-jruby:latest'
|
||||
help
|
||||
name 'JRuby 1.7'
|
||||
permitted_execution_time 10.seconds
|
||||
run_command 'ruby %{filename}'
|
||||
singleton_execution_environment
|
||||
test_command 'rspec %{filename} --format documentation'
|
||||
testing_framework 'RspecAdapter'
|
||||
end
|
||||
|
||||
factory :node_js, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-node:latest'
|
||||
help
|
||||
name 'Node.js'
|
||||
permitted_execution_time 10.seconds
|
||||
run_command 'node %{filename}'
|
||||
singleton_execution_environment
|
||||
end
|
||||
|
||||
factory :python, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-python:latest'
|
||||
help
|
||||
name 'Python 2.7'
|
||||
permitted_execution_time 10.seconds
|
||||
run_command 'python %{filename}'
|
||||
singleton_execution_environment
|
||||
test_command 'python -m unittest --verbose %{module_name}'
|
||||
testing_framework 'PyUnitAdapter'
|
||||
end
|
||||
|
||||
factory :ruby, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-ruby:latest'
|
||||
help
|
||||
name 'Ruby 2.1'
|
||||
permitted_execution_time 10.seconds
|
||||
run_command 'ruby %{filename}'
|
||||
singleton_execution_environment
|
||||
test_command 'rspec %{filename} --format documentation'
|
||||
testing_framework 'RspecAdapter'
|
||||
end
|
||||
|
||||
factory :sinatra, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-sinatra:latest'
|
||||
exposed_ports '4567'
|
||||
help
|
||||
name 'Sinatra'
|
||||
permitted_execution_time 15.minutes
|
||||
run_command 'ruby %{filename}'
|
||||
singleton_execution_environment
|
||||
test_command 'rspec %{filename} --format documentation'
|
||||
testing_framework 'RspecAdapter'
|
||||
end
|
||||
|
||||
factory :sqlite, class: ExecutionEnvironment do
|
||||
created_by_teacher
|
||||
docker_image 'hklement/ubuntu-sqlite:latest'
|
||||
help
|
||||
name 'SQLite'
|
||||
permitted_execution_time 1.minute
|
||||
run_command 'sqlite3 /database.db -init %{filename} -html'
|
||||
singleton_execution_environment
|
||||
test_command 'ruby %{filename}'
|
||||
testing_framework 'SqlResultSetComparatorAdapter'
|
||||
end
|
||||
|
||||
trait :help do
|
||||
help { Forgery(:lorem_ipsum).words(Forgery(:basic).number(at_least: 50, at_most: 100)) }
|
||||
end
|
||||
|
||||
trait :singleton_execution_environment do
|
||||
initialize_with { ExecutionEnvironment.where(name: name).first_or_create }
|
||||
end
|
||||
end
|
174
spec/factories/exercise.rb
Normal file
174
spec/factories/exercise.rb
Normal file
@ -0,0 +1,174 @@
|
||||
require 'seeds_helper'
|
||||
|
||||
def create_seed_file(exercise, path, file_attributes = {})
|
||||
file_extension = File.extname(path)
|
||||
file_type = FactoryGirl.create(file_attributes[:file_type] || :"dot_#{file_extension.gsub('.', '')}")
|
||||
name = File.basename(path).gsub(file_extension, '')
|
||||
file_attributes.merge!(file_type: file_type, name: name, path: path.split('/')[1..-2].join('/'), role: file_attributes[:role] || 'regular_file')
|
||||
if file_type.binary?
|
||||
file_attributes.merge!(native_file: File.open(SeedsHelper.seed_file_path(path), 'r'))
|
||||
else
|
||||
file_attributes.merge!(content: SeedsHelper.read_seed_file(path))
|
||||
end
|
||||
file = exercise.add_file!(file_attributes)
|
||||
end
|
||||
|
||||
FactoryGirl.define do
|
||||
factory :audio_video, class: Exercise do
|
||||
created_by_teacher
|
||||
description "Try HTML's audio and video capabilities."
|
||||
association :execution_environment, factory: :html
|
||||
instructions "Build a simple website including an HTML <audio> and <video> element. Link the following media files: chai.ogg, devstories.mp4."
|
||||
title 'Audio & Video'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'audio_video/index.html', role: 'main_file')
|
||||
create_seed_file(exercise, 'audio_video/index.js')
|
||||
create_seed_file(exercise, 'audio_video/index.html_spec.rb', feedback_message: 'Your solution is not correct yet.', hidden: true, role: 'teacher_defined_test')
|
||||
create_seed_file(exercise, 'audio_video/chai.ogg', read_only: true)
|
||||
create_seed_file(exercise, 'audio_video/devstories.mp4', read_only: true)
|
||||
create_seed_file(exercise, 'audio_video/devstories.webm', read_only: true)
|
||||
create_seed_file(exercise, 'audio_video/poster.png', read_only: true)
|
||||
end
|
||||
end
|
||||
|
||||
factory :even_odd, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Implement two methods even and odd which return whether a given number is even or odd, respectively.'
|
||||
association :execution_environment, factory: :python
|
||||
instructions
|
||||
title 'Even/Odd'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'even_odd/exercise.py', role: 'main_file')
|
||||
create_seed_file(exercise, 'even_odd/exercise_tests.py', feedback_message: 'Your solution is not correct yet.', hidden: true, role: 'teacher_defined_test')
|
||||
create_seed_file(exercise, 'even_odd/reference.py', hidden: true, role: 'reference_implementation')
|
||||
end
|
||||
end
|
||||
|
||||
factory :fibonacci, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Implement a recursive function that calculates a requested Fibonacci number.'
|
||||
association :execution_environment, factory: :ruby
|
||||
instructions
|
||||
title 'Fibonacci Sequence'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'fibonacci/exercise.rb', role: 'main_file')
|
||||
create_seed_file(exercise, 'fibonacci/exercise_spec_1.rb', feedback_message: "The 'fibonacci' method is not defined correctly. Please take care that the method is called 'fibonacci', takes a single (integer) argument and returns an integer.", hidden: true, role: 'teacher_defined_test', weight: 1.5)
|
||||
create_seed_file(exercise, 'fibonacci/exercise_spec_2.rb', feedback_message: 'Your method does not work recursively. Please make sure that the method works in a divide-and-conquer fashion by calling itself for partial results.', hidden: true, role: 'teacher_defined_test', weight: 2)
|
||||
create_seed_file(exercise, 'fibonacci/exercise_spec_3.rb', feedback_message: 'Your method does not return the correct results for all tested input values. ', hidden: true, role: 'teacher_defined_test', weight: 3)
|
||||
create_seed_file(exercise, 'fibonacci/reference.rb', hidden: true, role: 'reference_implementation')
|
||||
end
|
||||
end
|
||||
|
||||
factory :files, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Learn how to work with files.'
|
||||
association :execution_environment, factory: :ruby
|
||||
instructions
|
||||
title 'Working with Files'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'files/data.txt', read_only: true)
|
||||
create_seed_file(exercise, 'files/exercise.rb', role: 'main_file')
|
||||
create_seed_file(exercise, 'files/exercise_spec.rb', feedback_message: 'Your solution is not correct yet.', hidden: true, role: 'teacher_defined_test')
|
||||
end
|
||||
end
|
||||
|
||||
factory :geolocation, class: Exercise do
|
||||
created_by_teacher
|
||||
description "Use the HTML5 Geolocation API to get the user's geographical position."
|
||||
association :execution_environment, factory: :html
|
||||
instructions
|
||||
title 'Geolocation'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'geolocation/index.html', role: 'main_file')
|
||||
create_seed_file(exercise, 'geolocation/index.js')
|
||||
end
|
||||
end
|
||||
|
||||
factory :hello_world, class: Exercise do
|
||||
created_by_teacher
|
||||
description "Write a simple 'Hello World' application."
|
||||
association :execution_environment, factory: :ruby
|
||||
instructions
|
||||
title 'Hello World'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'hello_world/exercise.rb', role: 'main_file')
|
||||
create_seed_file(exercise, 'hello_world/exercise_spec.rb', feedback_message: 'Your solution is not correct yet.', hidden: true, role: 'teacher_defined_test')
|
||||
end
|
||||
end
|
||||
|
||||
factory :math, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Implement a recursive math library.'
|
||||
association :execution_environment, factory: :java
|
||||
instructions
|
||||
title 'Math'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'math/Makefile', file_type: :makefile, hidden: true, role: 'regular_file')
|
||||
create_seed_file(exercise, 'math/org/example/RecursiveMath.java', role: 'main_file')
|
||||
create_seed_file(exercise, 'math/org/example/RecursiveMathTest1.java', feedback_message: "The 'power' method is not defined correctly. Please take care that the method is called 'power', takes two arguments and returns a double.", hidden: true, role: 'teacher_defined_test')
|
||||
create_seed_file(exercise, 'math/org/example/RecursiveMathTest2.java', feedback_message: 'Your solution yields wrong results.', hidden: true, role: 'teacher_defined_test')
|
||||
end
|
||||
end
|
||||
|
||||
factory :primes, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Write a function that prints the first n prime numbers.'
|
||||
association :execution_environment, factory: :node_js
|
||||
instructions
|
||||
title 'Primes'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'primes/exercise.js', role: 'main_file')
|
||||
end
|
||||
end
|
||||
|
||||
factory :sql_select, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Learn to use the SELECT statement.'
|
||||
association :execution_environment, factory: :sqlite
|
||||
instructions "Write a query which selects the full rows for all people with the last name 'Doe'."
|
||||
title 'SELECT'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'sql_select/exercise.sql', role: 'main_file')
|
||||
create_seed_file(exercise, 'sql_select/comparator.rb', feedback_message: 'Your solution is not correct yet.', hidden: true, role: 'teacher_defined_test')
|
||||
create_seed_file(exercise, 'sql_select/reference.sql', hidden: true, role: 'reference_implementation')
|
||||
end
|
||||
end
|
||||
|
||||
factory :tdd, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Learn to appreciate test-driven development.'
|
||||
association :execution_environment, factory: :ruby
|
||||
instructions SeedsHelper.read_seed_file('tdd/instructions.md')
|
||||
title 'Test-driven Development'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'tdd/exercise.rb', role: 'main_file')
|
||||
create_seed_file(exercise, 'tdd/exercise_spec.rb', role: 'user_defined_test')
|
||||
end
|
||||
end
|
||||
|
||||
factory :web_app, class: Exercise do
|
||||
created_by_teacher
|
||||
description 'Build a simple Web application with Sinatra.'
|
||||
association :execution_environment, factory: :sinatra
|
||||
instructions
|
||||
title 'A Simple Web Application'
|
||||
|
||||
after(:create) do |exercise|
|
||||
create_seed_file(exercise, 'web_app/app.rb', role: 'main_file')
|
||||
end
|
||||
end
|
||||
|
||||
trait :instructions do
|
||||
instructions { Forgery(:lorem_ipsum).words(Forgery(:basic).number(at_least: 50, at_most: 100)) }
|
||||
end
|
||||
end
|
9
spec/factories/external_user.rb
Normal file
9
spec/factories/external_user.rb
Normal file
@ -0,0 +1,9 @@
|
||||
FactoryGirl.define do
|
||||
factory :external_user do
|
||||
association :consumer
|
||||
generated_email
|
||||
external_id { SecureRandom.uuid }
|
||||
generated_user_name
|
||||
singleton_external_user
|
||||
end
|
||||
end
|
193
spec/factories/file_type.rb
Normal file
193
spec/factories/file_type.rb
Normal file
@ -0,0 +1,193 @@
|
||||
FactoryGirl.define do
|
||||
factory :dot_coffee, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/coffee'
|
||||
executable
|
||||
file_extension '.coffee'
|
||||
indent_size 2
|
||||
name 'CoffeeScript'
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_gif, class: FileType do
|
||||
binary
|
||||
created_by_admin
|
||||
file_extension '.gif'
|
||||
name 'GIF'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_html, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/html'
|
||||
file_extension '.html'
|
||||
indent_size 4
|
||||
name 'HTML'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_java, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/java'
|
||||
executable
|
||||
file_extension '.java'
|
||||
indent_size 4
|
||||
name 'Java'
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_jpg, class: FileType do
|
||||
binary
|
||||
created_by_admin
|
||||
file_extension '.jpg'
|
||||
name 'JPEG'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_js, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/javascript'
|
||||
executable
|
||||
file_extension '.js'
|
||||
indent_size 4
|
||||
name 'JavaScript'
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_json, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/javascript'
|
||||
file_extension '.json'
|
||||
indent_size 4
|
||||
name 'JSON'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_mp3, class: FileType do
|
||||
binary
|
||||
created_by_admin
|
||||
file_extension '.mp3'
|
||||
name 'MP3'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_mp4, class: FileType do
|
||||
binary
|
||||
created_by_admin
|
||||
file_extension '.mp4'
|
||||
name 'MPEG-4'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_ogg, class: FileType do
|
||||
binary
|
||||
created_by_admin
|
||||
file_extension '.ogg'
|
||||
name 'Ogg Vorbis'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_png, class: FileType do
|
||||
binary
|
||||
created_by_admin
|
||||
file_extension '.png'
|
||||
name 'PNG'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_py, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/python'
|
||||
executable
|
||||
file_extension '.py'
|
||||
indent_size 4
|
||||
name 'Python'
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_rb, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/ruby'
|
||||
executable
|
||||
file_extension '.rb'
|
||||
indent_size 2
|
||||
name 'Ruby'
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_svg, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/svg'
|
||||
file_extension '.svg'
|
||||
indent_size 4
|
||||
name 'SVG'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_sql, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/sql'
|
||||
executable
|
||||
file_extension '.sql'
|
||||
indent_size 4
|
||||
name 'SQL'
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_txt, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/plain_text'
|
||||
file_extension '.txt'
|
||||
indent_size 4
|
||||
name 'Plain Text'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_webm, class: FileType do
|
||||
binary
|
||||
created_by_admin
|
||||
file_extension '.webm'
|
||||
name 'WebM'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :dot_xml, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/xml'
|
||||
file_extension '.xml'
|
||||
indent_size 4
|
||||
name 'XML'
|
||||
renderable
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
factory :makefile, class: FileType do
|
||||
created_by_admin
|
||||
editor_mode 'ace/mode/makefile'
|
||||
executable
|
||||
indent_size 2
|
||||
name 'Makefile'
|
||||
singleton_file_type
|
||||
end
|
||||
|
||||
%w[binary executable renderable].each do |attribute|
|
||||
trait(attribute) do
|
||||
self.send(attribute, true)
|
||||
end
|
||||
end
|
||||
|
||||
trait :singleton_file_type do
|
||||
initialize_with { FileType.where(attributes).first_or_create }
|
||||
end
|
||||
end
|
101
spec/factories/hint.rb
Normal file
101
spec/factories/hint.rb
Normal file
@ -0,0 +1,101 @@
|
||||
FactoryGirl.define do
|
||||
factory :node_js_invalid_assignment, class: Hint do
|
||||
association :execution_environment, factory: :node_js
|
||||
english
|
||||
message 'There was an error with an assignment. Maybe you have to use the equality operator here.'
|
||||
name 'Invalid assignment'
|
||||
regular_expression 'Invalid left-hand side in assignment'
|
||||
end
|
||||
|
||||
factory :node_js_reference_error, class: Hint do
|
||||
association :execution_environment, factory: :node_js
|
||||
english
|
||||
message "'$1' is not defined."
|
||||
name 'ReferenceError'
|
||||
regular_expression 'ReferenceError: (\w+) is not defined'
|
||||
end
|
||||
|
||||
factory :node_js_syntax_error, class: Hint do
|
||||
association :execution_environment, factory: :node_js
|
||||
english
|
||||
message 'You seem to have made a typo.'
|
||||
name 'SyntaxError'
|
||||
regular_expression 'SyntaxError: Unexpected token (\w+)'
|
||||
end
|
||||
|
||||
factory :ruby_load_error, class: Hint do
|
||||
association :execution_environment, factory: :ruby
|
||||
english
|
||||
message "The file '$1' cannot be found."
|
||||
name 'LoadError'
|
||||
regular_expression 'cannot load such file -- (\w+) (LoadError)'
|
||||
end
|
||||
|
||||
factory :ruby_name_error_constant, class: Hint do
|
||||
association :execution_environment, factory: :ruby
|
||||
english
|
||||
message "The constant '$1' is not defined."
|
||||
name 'NameError (uninitialized constant)'
|
||||
regular_expression 'uninitialized constant (\w+) \(NameError\)'
|
||||
end
|
||||
|
||||
factory :ruby_name_error_variable, class: Hint do
|
||||
association :execution_environment, factory: :ruby
|
||||
english
|
||||
message "Your object '$2' of class '$3' does not know what '$1' is. Maybe you made a typo or still have to define '$1'."
|
||||
name 'NameError (undefined local variable or method)'
|
||||
regular_expression 'undefined local variable or method `(\w+)\' for (\w+):(\w+) \(NameError\)'
|
||||
end
|
||||
|
||||
factory :ruby_no_method_error, class: Hint do
|
||||
association :execution_environment, factory: :ruby
|
||||
english
|
||||
message "Your object '$2' of class '$3' does not understand the method '$1'. Maybe you made a typo or still have to implement that method."
|
||||
name 'NoMethodError'
|
||||
regular_expression 'undefined method `([\w\!\?=\[\]]+)\' for (\w+):(\w+) \(NoMethodError\)'
|
||||
end
|
||||
|
||||
factory :ruby_syntax_error, class: Hint do
|
||||
association :execution_environment, factory: :ruby
|
||||
english
|
||||
message 'You seem to have made a typo.'
|
||||
name 'SyntaxError'
|
||||
regular_expression 'syntax error'
|
||||
end
|
||||
|
||||
factory :ruby_system_stack_error, class: Hint do
|
||||
association :execution_environment, factory: :ruby
|
||||
english
|
||||
message 'You seem to have built an infinite loop or recursion.'
|
||||
name 'SystemStackError'
|
||||
regular_expression 'stack level too deep \(SystemStackError\)'
|
||||
end
|
||||
|
||||
factory :sqlite_no_such_column, class: Hint do
|
||||
association :execution_environment, factory: :sqlite
|
||||
english
|
||||
message "The column '$1' does not exist."
|
||||
name 'No Such Column'
|
||||
regular_expression 'no such column: (\w+)'
|
||||
end
|
||||
|
||||
factory :sqlite_no_such_table, class: Hint do
|
||||
association :execution_environment, factory: :sqlite
|
||||
english
|
||||
message "The table '$1' does not exist."
|
||||
name 'No Such Table'
|
||||
regular_expression 'no such table: (\w+)'
|
||||
end
|
||||
|
||||
factory :sqlite_syntax_error, class: Hint do
|
||||
association :execution_environment, factory: :sqlite
|
||||
english
|
||||
message "You seem to have made a typo near '$1'."
|
||||
name 'SyntaxError'
|
||||
regular_expression 'near "(\w+)": syntax error'
|
||||
end
|
||||
|
||||
trait :english do
|
||||
locale 'en'
|
||||
end
|
||||
end
|
24
spec/factories/internal_user.rb
Normal file
24
spec/factories/internal_user.rb
Normal file
@ -0,0 +1,24 @@
|
||||
FactoryGirl.define do
|
||||
factory :admin, class: InternalUser do
|
||||
activated_user
|
||||
email 'admin@example.org'
|
||||
generated_user_name
|
||||
password 'admin'
|
||||
role 'admin'
|
||||
singleton_internal_user
|
||||
end
|
||||
|
||||
factory :teacher, class: InternalUser do
|
||||
activated_user
|
||||
association :consumer
|
||||
generated_email
|
||||
generated_user_name
|
||||
password 'teacher'
|
||||
role 'teacher'
|
||||
singleton_internal_user
|
||||
end
|
||||
|
||||
trait :activated_user do
|
||||
after(:create, &:activate!)
|
||||
end
|
||||
end
|
21
spec/factories/shared_traits.rb
Normal file
21
spec/factories/shared_traits.rb
Normal file
@ -0,0 +1,21 @@
|
||||
FactoryGirl.define do
|
||||
%w[admin external_user teacher].each do |factory_name|
|
||||
trait :"created_by_#{factory_name}" do
|
||||
association :user, factory: factory_name
|
||||
end
|
||||
end
|
||||
|
||||
trait :generated_email do
|
||||
email { "#{name.underscore.gsub(' ', '.')}@example.org" }
|
||||
end
|
||||
|
||||
trait :generated_user_name do
|
||||
name { Forgery::Name.full_name }
|
||||
end
|
||||
|
||||
[ExternalUser, InternalUser].each do |klass|
|
||||
trait :"singleton_#{klass.name.underscore}" do
|
||||
initialize_with { klass.where(email: email).first_or_create }
|
||||
end
|
||||
end
|
||||
end
|
13
spec/factories/submission.rb
Normal file
13
spec/factories/submission.rb
Normal file
@ -0,0 +1,13 @@
|
||||
FactoryGirl.define do
|
||||
factory :submission do
|
||||
cause 'save'
|
||||
created_by_external_user
|
||||
association :exercise, factory: :fibonacci
|
||||
|
||||
after(:create) do |submission|
|
||||
submission.exercise.files.editable.visible.each do |file|
|
||||
submission.add_file(content: file.content, file_id: file.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
54
spec/features/authentication_spec.rb
Normal file
54
spec/features/authentication_spec.rb
Normal file
@ -0,0 +1,54 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Authentication' do
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
let(:password) { FactoryGirl.attributes_for(:admin)[:password] }
|
||||
|
||||
context 'when signed out' do
|
||||
before(:each) { visit(root_path) }
|
||||
|
||||
it 'displays a sign in link' do
|
||||
expect(page).to have_content(I18n.t('sessions.new.link'))
|
||||
end
|
||||
|
||||
context 'with valid credentials' do
|
||||
it 'allows to sign in' do
|
||||
click_link(I18n.t('sessions.new.link'))
|
||||
fill_in('Email', with: user.email)
|
||||
fill_in('Password', with: password)
|
||||
click_button(I18n.t('sessions.new.link'))
|
||||
expect(page).to have_content(I18n.t('sessions.create.success'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid credentials' do
|
||||
it 'does not allow to sign in' do
|
||||
click_link(I18n.t('sessions.new.link'))
|
||||
fill_in('Email', with: user.email)
|
||||
fill_in('Password', with: password.reverse)
|
||||
click_button(I18n.t('sessions.new.link'))
|
||||
expect(page).to have_content(I18n.t('sessions.create.failure'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before(:each) do
|
||||
sign_in(user, password)
|
||||
visit(root_path)
|
||||
end
|
||||
|
||||
it "displays the user's name" do
|
||||
expect(page).to have_content(user.name)
|
||||
end
|
||||
|
||||
it 'displays a sign out link' do
|
||||
expect(page).to have_content(I18n.t('sessions.destroy.link'))
|
||||
end
|
||||
|
||||
it 'allows to sign out' do
|
||||
click_link(I18n.t('sessions.destroy.link'))
|
||||
expect(page).to have_content(I18n.t('sessions.destroy.success'))
|
||||
end
|
||||
end
|
||||
end
|
34
spec/features/authorization_spec.rb
Normal file
34
spec/features/authorization_spec.rb
Normal file
@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Authorization' do
|
||||
context 'as an admin' do
|
||||
let(:user) { FactoryGirl.create(:admin) }
|
||||
before(:each) { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }
|
||||
|
||||
%w[consumer execution_environment exercise file_type internal_user].each do |model|
|
||||
expect_permitted_path(:"new_#{model}_path")
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an external user' do
|
||||
let(:user) { FactoryGirl.create(:external_user) }
|
||||
before(:each) { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }
|
||||
|
||||
%w[consumer execution_environment exercise file_type internal_user].each do |model|
|
||||
expect_forbidden_path(:"new_#{model}_path")
|
||||
end
|
||||
end
|
||||
|
||||
context 'as a teacher' do
|
||||
let(:user) { FactoryGirl.create(:teacher) }
|
||||
before(:each) { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }
|
||||
|
||||
%w[consumer internal_user].each do |model|
|
||||
expect_forbidden_path(:"new_#{model}_path")
|
||||
end
|
||||
|
||||
%w[execution_environment exercise file_type].each do |model|
|
||||
expect_permitted_path(:"new_#{model}_path")
|
||||
end
|
||||
end
|
||||
end
|
7
spec/features/factories_spec.rb
Normal file
7
spec/features/factories_spec.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'Factories' do
|
||||
it 'are all valid', permitted_execution_time: 30 do
|
||||
expect { FactoryGirl.lint }.not_to raise_error
|
||||
end
|
||||
end
|
1
spec/fixtures/upload.rb
vendored
Normal file
1
spec/fixtures/upload.rb
vendored
Normal file
@ -0,0 +1 @@
|
||||
puts 'Hello World'
|
103
spec/helpers/application_helper_spec.rb
Normal file
103
spec/helpers/application_helper_spec.rb
Normal file
@ -0,0 +1,103 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ApplicationHelper do
|
||||
describe '#code_tag' do
|
||||
context 'with code' do
|
||||
it "builds a 'pre' tag" do
|
||||
code = 'puts 42'
|
||||
expect(code_tag(code)).to eq("<pre><code>#{code}</code></pre>")
|
||||
end
|
||||
end
|
||||
|
||||
context 'without code' do
|
||||
it 'calls #empty' do
|
||||
expect(code_tag('')).to eq(empty)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#empty' do
|
||||
it "builds an 'i' tag" do
|
||||
expect(empty).to eq('<i class="empty fa fa-minus"></i>')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#label_column' do
|
||||
it 'translates the label' do
|
||||
expect(I18n).to receive(:translate).at_least(:once)
|
||||
label_column('foo')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#no' do
|
||||
it "builds an 'i' tag" do
|
||||
expect(no).to eq('<i class="glyphicon glyphicon-remove"></i>')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#row' do
|
||||
let(:html) { row(label: 'foo', value: 42) }
|
||||
|
||||
it "builds nested 'div' tags" do
|
||||
expect(html.scan(/<\/div>/).length).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#value_column' do
|
||||
context 'without a value' do
|
||||
let(:html) { value_column('') }
|
||||
|
||||
it "builds a 'div' tag" do
|
||||
expect(html).to start_with('<div')
|
||||
end
|
||||
|
||||
it 'calls #empty' do
|
||||
expect(html).to include(empty)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a 'false' value" do
|
||||
let(:html) { value_column(false) }
|
||||
|
||||
it "builds a 'div' tag" do
|
||||
expect(html).to start_with('<div')
|
||||
end
|
||||
|
||||
it 'calls #no' do
|
||||
expect(html).to include(no)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a 'true' value" do
|
||||
let(:html) { value_column(true) }
|
||||
|
||||
it "builds a 'div' tag" do
|
||||
expect(html).to start_with('<div')
|
||||
end
|
||||
|
||||
it 'calls #yes' do
|
||||
expect(html).to include(yes)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a non-boolean value' do
|
||||
let(:html) { value_column(value) }
|
||||
let(:value) { [42] }
|
||||
|
||||
it "builds a 'div' tag" do
|
||||
expect(html).to start_with('<div')
|
||||
end
|
||||
|
||||
it "uses the value's string representation" do
|
||||
expect(value).to receive(:to_s)
|
||||
html
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#yes' do
|
||||
it "builds an 'i' tag" do
|
||||
expect(yes).to eq('<i class="glyphicon glyphicon-ok"></i>')
|
||||
end
|
||||
end
|
||||
end
|
25
spec/lib/assessor_spec.rb
Normal file
25
spec/lib/assessor_spec.rb
Normal file
@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Assessor do
|
||||
describe '#calculate_score' do
|
||||
let(:count) { 42 }
|
||||
let(:passed) { 17 }
|
||||
let(:test_outcome) { {count: count, passed: passed} }
|
||||
|
||||
context 'with a testing framework adapter' do
|
||||
let(:assessor) { Assessor.new(execution_environment: FactoryGirl.build(:ruby)) }
|
||||
|
||||
it 'returns the correct score' do
|
||||
expect(assessor.send(:calculate_score, test_outcome)).to eq(passed.to_f / count.to_f)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a testing framework adapter' do
|
||||
let(:assessor) { Assessor.new(execution_environment: FactoryGirl.build(:execution_environment)) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { assessor.send(:calculate_score, test_outcome) }.to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
255
spec/lib/docker_client_spec.rb
Normal file
255
spec/lib/docker_client_spec.rb
Normal file
@ -0,0 +1,255 @@
|
||||
require 'rails_helper'
|
||||
require 'seeds_helper'
|
||||
|
||||
describe DockerClient, docker: true do
|
||||
let(:command) { 'whoami' }
|
||||
let(:docker_client) { DockerClient.new(execution_environment: FactoryGirl.build(:ruby), user: FactoryGirl.build(:admin)) }
|
||||
let(:image) { double }
|
||||
let(:submission) { FactoryGirl.create(:submission) }
|
||||
let(:workspace_path) { '/tmp' }
|
||||
|
||||
describe '#bound_folders' do
|
||||
context 'when executing a submission' do
|
||||
before(:each) { docker_client.instance_variable_set(:@submission, submission) }
|
||||
|
||||
it 'returns a submission-specific mapping' do
|
||||
mapping = docker_client.send(:bound_folders).first
|
||||
expect(mapping).to include(submission.id.to_s)
|
||||
expect(mapping).to end_with(DockerClient::CONTAINER_WORKSPACE_PATH)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when executing a single command' do
|
||||
it 'returns an empty mapping' do
|
||||
expect(docker_client.send(:bound_folders)).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.check_availability!' do
|
||||
context 'when a socket error occurs' do
|
||||
it 'raises an error' do
|
||||
expect(Docker).to receive(:version).and_raise(Excon::Errors::SocketError.new(StandardError.new))
|
||||
expect { DockerClient.check_availability! }.to raise_error(DockerClient::Error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a timeout occurs' do
|
||||
it 'raises an error' do
|
||||
expect(Docker).to receive(:version).and_raise(Timeout::Error)
|
||||
expect { DockerClient.check_availability! }.to raise_error(DockerClient::Error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clean_workspace' do
|
||||
it 'removes the submission-specific directory' do
|
||||
expect(docker_client).to receive(:local_workspace_path).and_return(workspace_path)
|
||||
expect(FileUtils).to receive(:rm_rf).with(workspace_path)
|
||||
docker_client.send(:clean_workspace)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_container' do
|
||||
let(:image_tag) { 'tag' }
|
||||
before(:each) { docker_client.instance_variable_set(:@image, image) }
|
||||
|
||||
it 'creates a container' do
|
||||
expect(image).to receive(:info).and_return({'RepoTags' => [image_tag]})
|
||||
expect(Docker::Container).to receive(:create).with('Cmd' => command, 'Image' => image_tag)
|
||||
docker_client.send(:create_container, command: command)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_workspace' do
|
||||
before(:each) { docker_client.instance_variable_set(:@submission, submission) }
|
||||
|
||||
it 'creates submission-specific directories' do
|
||||
expect(docker_client).to receive(:local_workspace_path).at_least(:once).and_return(workspace_path)
|
||||
expect(Dir).to receive(:mkdir).at_least(:once)
|
||||
docker_client.send(:create_workspace)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_workspace_file' do
|
||||
let(:file) { FactoryGirl.build(:file, content: 'puts 42') }
|
||||
let(:file_path) { File.join(workspace_path, file.name_with_extension) }
|
||||
|
||||
it 'creates a file' do
|
||||
expect(docker_client).to receive(:local_workspace_path).and_return(workspace_path)
|
||||
docker_client.send(:create_workspace_file, file: file)
|
||||
expect(File.exist?(file_path)).to be true
|
||||
expect(File.new(file_path, 'r').read).to eq(file.content)
|
||||
File.delete(file_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.destroy_container' do
|
||||
let(:container) { docker_client.send(:create_container, {command: command}) }
|
||||
after(:each) { DockerClient.destroy_container(container) }
|
||||
|
||||
it 'stops the container' do
|
||||
expect(container).to receive(:stop).and_return(container)
|
||||
end
|
||||
|
||||
it 'kills the container' do
|
||||
expect(container).to receive(:kill)
|
||||
end
|
||||
|
||||
it 'releases allocated ports' do
|
||||
expect(container).to receive(:json).at_least(:once).and_return({'HostConfig' => {'PortBindings' => {foo: [{'HostPort' => '42'}]}}})
|
||||
docker_client.send(:start_container, container)
|
||||
expect(PortPool).to receive(:release)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute_command' do
|
||||
after(:each) { docker_client.send(:execute_command, command) }
|
||||
|
||||
it 'creates a container' do
|
||||
expect(docker_client).to receive(:create_container).with(command: ['bash', '-c', command]).and_call_original
|
||||
end
|
||||
|
||||
it 'starts the container' do
|
||||
expect(docker_client).to receive(:start_container)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute_in_workspace' do
|
||||
let(:block) { Proc.new do; end }
|
||||
let(:execute_in_workspace) { docker_client.send(:execute_in_workspace, submission, &block) }
|
||||
after(:each) { execute_in_workspace }
|
||||
|
||||
it 'creates the workspace' do
|
||||
expect(docker_client).to receive(:create_workspace)
|
||||
end
|
||||
|
||||
it 'calls the block' do
|
||||
expect(block).to receive(:call)
|
||||
end
|
||||
|
||||
it 'cleans the workspace' do
|
||||
expect(docker_client).to receive(:clean_workspace)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute_run_command' do
|
||||
let(:block) { Proc.new {} }
|
||||
let(:filename) { submission.exercise.files.detect { |file| file.role == 'main_file' }.name_with_extension }
|
||||
after(:each) { docker_client.send(:execute_run_command, submission, filename, &block) }
|
||||
|
||||
it 'is executed in the workspace' do
|
||||
expect(docker_client).to receive(:execute_in_workspace)
|
||||
end
|
||||
|
||||
it 'executes the run command' do
|
||||
expect(docker_client).to receive(:execute_command).with(kind_of(String), &block)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute_test_command' do
|
||||
let(:filename) { submission.exercise.files.detect { |file| file.role == 'teacher_defined_test' }.name_with_extension }
|
||||
after(:each) { docker_client.send(:execute_test_command, submission, filename) }
|
||||
|
||||
it 'is executed in the workspace' do
|
||||
expect(docker_client).to receive(:execute_in_workspace)
|
||||
end
|
||||
|
||||
it 'executes the test command' do
|
||||
expect(docker_client).to receive(:execute_command).with(kind_of(String))
|
||||
end
|
||||
end
|
||||
|
||||
describe '.initialize_environment' do
|
||||
let(:config) { {connection_timeout: 3, host: 'tcp://8.8.8.8:2375', workspace_root: '/'} }
|
||||
|
||||
context 'with complete configuration' do
|
||||
before(:each) { expect(DockerClient).to receive(:config).at_least(:once).and_return(config) }
|
||||
|
||||
it 'does not raise an error' do
|
||||
expect { DockerClient.initialize_environment }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with incomplete configuration' do
|
||||
before(:each) { expect(DockerClient).to receive(:config).at_least(:once).and_return({}) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { DockerClient.initialize_environment }.to raise_error(DockerClient::Error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#local_workspace_path' do
|
||||
before(:each) { docker_client.instance_variable_set(:@submission, submission) }
|
||||
|
||||
it 'includes the correct workspace root' do
|
||||
expect(docker_client.send(:local_workspace_path)).to start_with(DockerClient::LOCAL_WORKSPACE_ROOT.to_s)
|
||||
end
|
||||
|
||||
it 'is submission-specific' do
|
||||
expect(docker_client.send(:local_workspace_path)).to end_with(submission.id.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#remote_workspace_path' do
|
||||
before(:each) { docker_client.instance_variable_set(:@submission, submission) }
|
||||
|
||||
it 'includes the correct workspace root' do
|
||||
expect(docker_client.send(:remote_workspace_path)).to start_with(DockerClient.config[:workspace_root])
|
||||
end
|
||||
|
||||
it 'is submission-specific' do
|
||||
expect(docker_client.send(:remote_workspace_path)).to end_with(submission.id.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#start_container' do
|
||||
let(:container) { docker_client.send(:create_container, command: command) }
|
||||
let(:start_container) { docker_client.send(:start_container, container) }
|
||||
|
||||
it 'configures bound folders' do
|
||||
expect(container).to receive(:start).with(hash_including('Binds' => kind_of(Array))).and_call_original
|
||||
start_container
|
||||
end
|
||||
|
||||
it 'configures bound ports' do
|
||||
expect(container).to receive(:start).with(hash_including('PortBindings' => kind_of(Hash))).and_call_original
|
||||
start_container
|
||||
end
|
||||
|
||||
it 'starts the container' do
|
||||
expect(container).to receive(:start).and_call_original
|
||||
start_container
|
||||
end
|
||||
|
||||
it 'waits for the container to terminate' do
|
||||
expect(container).to receive(:wait).with(kind_of(Numeric)).and_call_original
|
||||
start_container
|
||||
end
|
||||
|
||||
context 'when a timeout occurs' do
|
||||
before(:each) { expect(container).to receive(:wait).and_raise(Docker::Error::TimeoutError) }
|
||||
|
||||
it 'kills the container' do
|
||||
expect(container).to receive(:kill)
|
||||
start_container
|
||||
end
|
||||
|
||||
it 'returns a corresponding status' do
|
||||
expect(start_container[:status]).to eq(:timeout)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the container terminates timely' do
|
||||
it "returns the container's output" do
|
||||
expect(start_container[:stderr]).to be_blank
|
||||
expect(start_container[:stdout]).to start_with('root')
|
||||
end
|
||||
|
||||
it 'returns a corresponding status' do
|
||||
expect(start_container[:status]).to eq(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,39 @@
|
||||
require 'rails/generators'
|
||||
require 'generators/testing_framework_adapter_generator'
|
||||
require 'rails_helper'
|
||||
|
||||
describe TestingFrameworkAdapterGenerator do
|
||||
describe '#create_testing_framework_adapter' do
|
||||
let(:name) { 'TestUnit' }
|
||||
let(:path) { Rails.root.join('lib', "#{name.underscore}_adapter.rb") }
|
||||
let(:spec_path) { Rails.root.join('spec', 'lib', "#{name.underscore}_adapter_spec.rb") }
|
||||
|
||||
before(:each) do
|
||||
Rails::Generators.invoke('testing_framework_adapter', [name])
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
File.delete(path)
|
||||
File.delete(spec_path)
|
||||
end
|
||||
|
||||
it 'generates a correctly named file' do
|
||||
expect(File.exist?(path)).to be true
|
||||
end
|
||||
|
||||
it 'builds a correct class skeleton' do
|
||||
file_content = File.new(path, 'r').read
|
||||
expect(file_content).to start_with("class #{name}Adapter < TestingFrameworkAdapter")
|
||||
end
|
||||
|
||||
it 'generates a corresponding test' do
|
||||
expect(File.exist?(spec_path)).to be true
|
||||
end
|
||||
|
||||
it 'builds a correct test skeleton' do
|
||||
file_content = File.new(spec_path, 'r').read
|
||||
expect(file_content).to include("describe #{name}Adapter")
|
||||
expect(file_content).to include("describe '#parse_output'")
|
||||
end
|
||||
end
|
||||
end
|
26
spec/lib/junit_adapter_spec.rb
Normal file
26
spec/lib/junit_adapter_spec.rb
Normal file
@ -0,0 +1,26 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe JunitAdapter do
|
||||
let(:adapter) { JunitAdapter.new }
|
||||
|
||||
describe '#parse_output' do
|
||||
context 'with failed tests' do
|
||||
let(:count) { 42 }
|
||||
let(:failed) { 25 }
|
||||
let(:stdout) { "FAILURES!!!\nTests run: #{count}, Failures: #{failed}" }
|
||||
|
||||
it 'returns the correct numbers' do
|
||||
expect(adapter.parse_output(stdout: stdout)).to eq({count: count, failed: failed})
|
||||
end
|
||||
end
|
||||
|
||||
context 'without failed tests' do
|
||||
let(:count) { 42 }
|
||||
let(:stdout) { "OK (#{count} tests)" }
|
||||
|
||||
it 'returns the correct numbers' do
|
||||
expect(adapter.parse_output(stdout: stdout)).to eq({count: count, passed: count})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
spec/lib/nonce_store_spec.rb
Normal file
41
spec/lib/nonce_store_spec.rb
Normal file
@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe NonceStore do
|
||||
let(:nonce) { SecureRandom.hex }
|
||||
|
||||
describe '.add' do
|
||||
it 'stores a nonce in the cache' do
|
||||
expect(Rails.cache).to receive(:write)
|
||||
NonceStore.add(nonce)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.delete' do
|
||||
it 'deletes a nonce from the cache' do
|
||||
expect(Rails.cache).to receive(:write)
|
||||
NonceStore.add(nonce)
|
||||
NonceStore.delete(nonce)
|
||||
expect(NonceStore.has?(nonce)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '.has?' do
|
||||
it 'returns true for present nonces' do
|
||||
NonceStore.add(nonce)
|
||||
expect(NonceStore.has?(nonce)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for expired nonces' do
|
||||
Lti.send(:remove_const, 'MAXIMUM_SESSION_AGE')
|
||||
Lti::MAXIMUM_SESSION_AGE = 1
|
||||
NonceStore.add(nonce)
|
||||
expect(NonceStore.has?(nonce)).to be true
|
||||
sleep(Lti::MAXIMUM_SESSION_AGE)
|
||||
expect(NonceStore.has?(nonce)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for absent nonces' do
|
||||
expect(NonceStore.has?(nonce)).to be false
|
||||
end
|
||||
end
|
||||
end
|
55
spec/lib/port_pool_spec.rb
Normal file
55
spec/lib/port_pool_spec.rb
Normal file
@ -0,0 +1,55 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe PortPool do
|
||||
describe '.available_port' do
|
||||
it 'is synchronized' do
|
||||
expect(PortPool.instance_variable_get(:@mutex)).to receive(:synchronize)
|
||||
PortPool.available_port
|
||||
end
|
||||
|
||||
context 'when a port is available' do
|
||||
it 'returns the port' do
|
||||
expect(PortPool.available_port).to be_a(Numeric)
|
||||
end
|
||||
|
||||
it 'removes the port from the list of available ports' do
|
||||
port = PortPool.available_port
|
||||
expect(PortPool.instance_variable_get(:@available_ports)).not_to include(port)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no port is available' do
|
||||
it 'returns the port' do
|
||||
available_ports = PortPool.instance_variable_get(:@available_ports)
|
||||
PortPool.instance_variable_set(:@available_ports, [])
|
||||
expect(PortPool.available_port).to be_nil
|
||||
PortPool.instance_variable_set(:@available_ports, available_ports)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.release' do
|
||||
context 'when the port has been obtained earlier' do
|
||||
it 'adds the port to the list of available ports' do
|
||||
port = PortPool.available_port
|
||||
expect(PortPool.instance_variable_get(:@available_ports)).not_to include(port)
|
||||
PortPool.release(port)
|
||||
expect(PortPool.instance_variable_get(:@available_ports)).to include(port)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the port has not been obtained earlier' do
|
||||
it 'does not add the port to the list of available ports' do
|
||||
port = PortPool.instance_variable_get(:@available_ports).sample
|
||||
expect { PortPool.release(port) }.not_to change { PortPool.instance_variable_get(:@available_ports).length }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the port is not included in the port range' do
|
||||
it 'does not add the port to the list of available ports' do
|
||||
port = nil
|
||||
expect { PortPool.release(port) }.not_to change { PortPool.instance_variable_get(:@available_ports).length }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
spec/lib/py_unit_adapter_spec.rb
Normal file
14
spec/lib/py_unit_adapter_spec.rb
Normal file
@ -0,0 +1,14 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe PyUnitAdapter do
|
||||
let(:adapter) { PyUnitAdapter.new }
|
||||
let(:count) { 42 }
|
||||
let(:failed) { 25 }
|
||||
let(:stderr) { "Ran #{count} tests in 0.1s\n\nFAILED (failures=#{failed})" }
|
||||
|
||||
describe '#parse_output' do
|
||||
it 'returns the correct numbers' do
|
||||
expect(adapter.parse_output(stderr: stderr)).to eq({count: count, failed: failed})
|
||||
end
|
||||
end
|
||||
end
|
14
spec/lib/rspec_adapter_spec.rb
Normal file
14
spec/lib/rspec_adapter_spec.rb
Normal file
@ -0,0 +1,14 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe RspecAdapter do
|
||||
let(:adapter) { RspecAdapter.new }
|
||||
let(:count) { 42 }
|
||||
let(:failed) { 25 }
|
||||
let(:stdout) { "Finished in 0.1 seconds (files took 0.1 seconds to load)\n#{count} examples, #{failed} failures" }
|
||||
|
||||
describe '#parse_output' do
|
||||
it 'returns the correct numbers' do
|
||||
expect(adapter.parse_output(stdout: stdout)).to eq({count: count, failed: failed})
|
||||
end
|
||||
end
|
||||
end
|
31
spec/lib/sql_result_set_comparator_adapter_spec.rb
Normal file
31
spec/lib/sql_result_set_comparator_adapter_spec.rb
Normal file
@ -0,0 +1,31 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SqlResultSetComparatorAdapter do
|
||||
let(:adapter) { SqlResultSetComparatorAdapter.new }
|
||||
|
||||
describe '#parse_output' do
|
||||
context 'with missing tuples' do
|
||||
let(:stdout) { "Missing tuples: [1]\nUnexpected tuples: []" }
|
||||
|
||||
it 'considers the test as failed' do
|
||||
expect(adapter.parse_output(stdout: stdout)).to eq({count: 1, failed: 1})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unexpected tuples' do
|
||||
let(:stdout) { "Missing tuples: []\nUnexpected tuples: [1]" }
|
||||
|
||||
it 'considers the test as failed' do
|
||||
expect(adapter.parse_output(stdout: stdout)).to eq({count: 1, failed: 1})
|
||||
end
|
||||
end
|
||||
|
||||
context 'without missing or unexpected tuples' do
|
||||
let(:stdout) { "Missing tuples: []\nUnexpected tuples: []" }
|
||||
|
||||
it 'considers the test as passed' do
|
||||
expect(adapter.parse_output(stdout: stdout)).to eq({count: 1, passed: 1})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
spec/lib/testing_framework_adapter_spec.rb
Normal file
41
spec/lib/testing_framework_adapter_spec.rb
Normal file
@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe TestingFrameworkAdapter do
|
||||
let(:adapter) { TestingFrameworkAdapter.new }
|
||||
let(:count) { 42 }
|
||||
let(:failed) { 25 }
|
||||
let(:passed) { 17 }
|
||||
|
||||
describe '#augment_output' do
|
||||
context 'when missing the count of all tests' do
|
||||
it 'adds the count of all tests' do
|
||||
expect(adapter.send(:augment_output, failed: failed, passed: passed)).to include(count: count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when missing the count of failed tests' do
|
||||
it 'adds the count of failed tests' do
|
||||
expect(adapter.send(:augment_output, count: count, passed: passed)).to include(failed: failed)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when missing the count of passed tests' do
|
||||
it 'adds the count of passed tests' do
|
||||
expect(adapter.send(:augment_output, count: count, failed: failed)).to include(passed: passed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_output' do
|
||||
it 'requires subclasses to implement #parse_output' do
|
||||
expect { adapter.send(:parse_output, '') }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#test_outcome' do
|
||||
it 'calls the framework-specific implementation' do
|
||||
expect(adapter).to receive(:parse_output).and_return(count: count, failed: failed, passed: passed)
|
||||
adapter.test_outcome('')
|
||||
end
|
||||
end
|
||||
end
|
28
spec/lib/whistleblower_spec.rb
Normal file
28
spec/lib/whistleblower_spec.rb
Normal file
@ -0,0 +1,28 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Whistleblower do
|
||||
let(:hint) { FactoryGirl.create(:ruby_no_method_error) }
|
||||
let(:stderr) { "undefined method `foo' for main:Object (NoMethodError)" }
|
||||
let(:whistleblower) { Whistleblower.new(execution_environment: hint.execution_environment) }
|
||||
|
||||
describe '#find_hint' do
|
||||
let(:find_hint) { whistleblower.send(:find_hint, stderr) }
|
||||
|
||||
it 'finds the hint' do
|
||||
expect(find_hint).to eq(hint)
|
||||
end
|
||||
|
||||
it 'stores the matches' do
|
||||
find_hint
|
||||
expect(whistleblower.instance_variable_get(:@matches)).to be_a(MatchData)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#generate_hint' do
|
||||
it 'returns the customized hint message' do
|
||||
message = whistleblower.generate_hint(stderr)
|
||||
expect(message[0..9]).to eq(hint.message[0..9])
|
||||
expect(message[-10..-1]).to eq(hint.message[-10..-1])
|
||||
end
|
||||
end
|
||||
end
|
56
spec/mailers/user_mailer_spec.rb
Normal file
56
spec/mailers/user_mailer_spec.rb
Normal file
@ -0,0 +1,56 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe UserMailer do
|
||||
let(:user) { InternalUser.create(FactoryGirl.attributes_for(:teacher)) }
|
||||
|
||||
describe '#activation_needed_email' do
|
||||
let(:mail) { UserMailer.activation_needed_email(user) }
|
||||
|
||||
before(:each) do
|
||||
user.send(:setup_activation)
|
||||
user.save(validate: false)
|
||||
end
|
||||
|
||||
it 'sets the correct sender' do
|
||||
expect(mail.from).to include(CodeOcean::Application.config.action_mailer[:default_options][:from])
|
||||
end
|
||||
|
||||
it 'sets the correct subject' do
|
||||
expect(mail.subject).to eq(I18n.t('mailers.user_mailer.activation_needed.subject'))
|
||||
end
|
||||
|
||||
it 'sets the correct receiver' do
|
||||
expect(mail.to).to include(user.email)
|
||||
end
|
||||
|
||||
it 'includes the correct URL' do
|
||||
expect(mail.body).to include(activate_internal_user_url(user, token: user.activation_token))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#activation_success_email' do
|
||||
it 'does not raise an error' do
|
||||
expect { UserMailer.activation_success_email(user) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_password_email' do
|
||||
let(:mail) { UserMailer.reset_password_email(user) }
|
||||
|
||||
it 'sets the correct sender' do
|
||||
expect(mail.from).to include(CodeOcean::Application.config.action_mailer[:default_options][:from])
|
||||
end
|
||||
|
||||
it 'sets the correct subject' do
|
||||
expect(mail.subject).to eq(I18n.t('mailers.user_mailer.reset_password.subject'))
|
||||
end
|
||||
|
||||
it 'sets the correct receiver' do
|
||||
expect(mail.to).to include(user.email)
|
||||
end
|
||||
|
||||
it 'includes the correct URL' do
|
||||
expect(mail.body).to include(reset_password_internal_user_url(user, token: user.reset_password_token))
|
||||
end
|
||||
end
|
||||
end
|
52
spec/models/code_ocean/file_spec.rb
Normal file
52
spec/models/code_ocean/file_spec.rb
Normal file
@ -0,0 +1,52 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe CodeOcean::File do
|
||||
let(:file) { CodeOcean::File.create.tap { |file| file.update(content: nil, hidden: nil, read_only: nil) } }
|
||||
|
||||
it 'validates the presence of a file type' do
|
||||
expect(file.errors[:file_type_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of the hidden flag' do
|
||||
expect(file.errors[:hidden]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a name' do
|
||||
expect(file.errors[:name]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of the read-only flag' do
|
||||
expect(file.errors[:read_only]).to be_present
|
||||
end
|
||||
|
||||
context 'as a teacher-defined test' do
|
||||
before(:each) { file.update(role: 'teacher_defined_test') }
|
||||
|
||||
it 'validates the presence of a feedback message' do
|
||||
expect(file.errors[:feedback_message]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the numericality of a weight' do
|
||||
file.update(weight: 'heavy')
|
||||
expect(file.errors[:weight]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a weight' do
|
||||
expect(file.errors[:weight]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'with another file type' do
|
||||
before(:each) { file.update(role: 'regular_file') }
|
||||
|
||||
it 'validates the absence of a feedback message' do
|
||||
file.update(feedback_message: 'Your solution is not correct yet.')
|
||||
expect(file.errors[:feedback_message]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the absence of a weight' do
|
||||
file.update(weight: 1)
|
||||
expect(file.errors[:weight]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
22
spec/models/consumer_spec.rb
Normal file
22
spec/models/consumer_spec.rb
Normal file
@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Consumer do
|
||||
let(:consumer) { Consumer.create }
|
||||
|
||||
it 'validates the presence of a name' do
|
||||
expect(consumer.errors[:name]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of an OAuth key' do
|
||||
expect(consumer.errors[:oauth_key]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the uniqueness of the OAuth key' do
|
||||
consumer.update(oauth_key: FactoryGirl.create(:consumer).oauth_key)
|
||||
expect(consumer.errors[:oauth_key]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of an OAuth secret' do
|
||||
expect(consumer.errors[:oauth_secret]).to be_present
|
||||
end
|
||||
end
|
13
spec/models/error_spec.rb
Normal file
13
spec/models/error_spec.rb
Normal file
@ -0,0 +1,13 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Error do
|
||||
let(:error) { Error.create }
|
||||
|
||||
it 'validates the presence of an execution environment' do
|
||||
expect(error.errors[:execution_environment_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a message' do
|
||||
expect(error.errors[:message]).to be_present
|
||||
end
|
||||
end
|
87
spec/models/execution_environment_spec.rb
Normal file
87
spec/models/execution_environment_spec.rb
Normal file
@ -0,0 +1,87 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExecutionEnvironment do
|
||||
let(:execution_environment) { ExecutionEnvironment.create }
|
||||
|
||||
it 'validates that the Docker image works', docker: true do
|
||||
expect(execution_environment).to receive(:working_docker_image?).and_call_original
|
||||
expect(execution_environment).to receive(:validate_docker_image?).and_return(true)
|
||||
execution_environment.update(docker_image: 'invalid')
|
||||
expect(execution_environment.errors[:docker_image]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a Docker image name' do
|
||||
expect(execution_environment.errors[:docker_image]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a name' do
|
||||
expect(execution_environment.errors[:name]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the numericality of a permitted run time' do
|
||||
execution_environment.update(permitted_execution_time: Math::PI)
|
||||
expect(execution_environment.errors[:permitted_execution_time]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a permitted run time' do
|
||||
expect(execution_environment.errors[:permitted_execution_time]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a run command' do
|
||||
expect(execution_environment.errors[:run_command]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a user' do
|
||||
expect(execution_environment.errors[:user_id]).to be_present
|
||||
expect(execution_environment.errors[:user_type]).to be_present
|
||||
end
|
||||
|
||||
describe '#validate_docker_image?' do
|
||||
it 'is false in the test environment' do
|
||||
expect(execution_environment.send(:validate_docker_image?)).to be false
|
||||
end
|
||||
|
||||
it 'is false without a Docker image' do
|
||||
allow(Rails).to receive(:env).and_return('production')
|
||||
expect(execution_environment.send(:validate_docker_image?)).to be false
|
||||
end
|
||||
|
||||
it 'is true otherwise' do
|
||||
execution_environment.docker_image = DockerClient.image_tags.first
|
||||
expect(Rails).to receive(:env).and_return('production')
|
||||
expect(execution_environment.send(:validate_docker_image?)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#working_docker_image?', docker: true do
|
||||
let(:working_docker_image?) { execution_environment.send(:working_docker_image?) }
|
||||
before(:each) { expect_any_instance_of(DockerClient).to receive(:find_image_by_tag).and_return(Object.new) }
|
||||
|
||||
it 'instantiates a Docker client' do
|
||||
expect(DockerClient).to receive(:new).with(execution_environment: execution_environment).and_call_original
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_command).and_return({})
|
||||
working_docker_image?
|
||||
end
|
||||
|
||||
it 'executes the validation command' do
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_command).with(ExecutionEnvironment::VALIDATION_COMMAND).and_return({})
|
||||
working_docker_image?
|
||||
end
|
||||
|
||||
context 'when the command produces an error' do
|
||||
it 'adds an error' do
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_command).and_return({stderr: 'command not found'})
|
||||
working_docker_image?
|
||||
expect(execution_environment.errors[:docker_image]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the Docker client produces an error' do
|
||||
it 'adds an error' do
|
||||
expect_any_instance_of(DockerClient).to receive(:execute_command).and_raise(DockerClient::Error)
|
||||
working_docker_image?
|
||||
expect(execution_environment.errors[:docker_image]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
30
spec/models/exercise_spec.rb
Normal file
30
spec/models/exercise_spec.rb
Normal file
@ -0,0 +1,30 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Exercise do
|
||||
let(:exercise) { Exercise.create.tap { |exercise| exercise.update(public: nil, token: nil) } }
|
||||
|
||||
it 'validates the presence of a description' do
|
||||
expect(exercise.errors[:description]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of an execution environment' do
|
||||
expect(exercise.errors[:execution_environment_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of the public flag' do
|
||||
expect(exercise.errors[:public]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a title' do
|
||||
expect(exercise.errors[:title]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a token' do
|
||||
expect(exercise.errors[:token]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a user' do
|
||||
expect(exercise.errors[:user_id]).to be_present
|
||||
expect(exercise.errors[:user_type]).to be_present
|
||||
end
|
||||
end
|
25
spec/models/external_user_spec.rb
Normal file
25
spec/models/external_user_spec.rb
Normal file
@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExternalUser do
|
||||
let(:user) { ExternalUser.create }
|
||||
|
||||
it 'validates the presence of a consumer' do
|
||||
expect(user.errors[:consumer_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of an external ID' do
|
||||
expect(user.errors[:external_id]).to be_present
|
||||
end
|
||||
|
||||
describe '#admin?' do
|
||||
it 'is false' do
|
||||
expect(FactoryGirl.build(:external_user).admin?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#teacher?' do
|
||||
it 'is false' do
|
||||
expect(FactoryGirl.build(:external_user).teacher?).to be false
|
||||
end
|
||||
end
|
||||
end
|
50
spec/models/file_type_spec.rb
Normal file
50
spec/models/file_type_spec.rb
Normal file
@ -0,0 +1,50 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe FileType do
|
||||
let(:file_type) { FileType.create.tap { |file_type| file_type.update(binary: nil, executable: nil, renderable: nil) } }
|
||||
|
||||
it 'validates the presence of the binary flag' do
|
||||
expect(file_type.errors[:binary]).to be_present
|
||||
end
|
||||
|
||||
context 'when binary' do
|
||||
before(:each) { file_type.update(binary: true) }
|
||||
|
||||
it 'does not validate the presence of an editor mode' do
|
||||
expect(file_type.errors[:editor_mode]).not_to be_present
|
||||
end
|
||||
|
||||
it 'does not validate the presence of an indent size' do
|
||||
expect(file_type.errors[:indent_size]).not_to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not binary' do
|
||||
before(:each) { file_type.update(binary: false) }
|
||||
|
||||
it 'validates the presence of an editor mode' do
|
||||
expect(file_type.errors[:editor_mode]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of an indent size' do
|
||||
expect(file_type.errors[:indent_size]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'validates the presence of the executable flag' do
|
||||
expect(file_type.errors[:executable]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a name' do
|
||||
expect(file_type.errors[:name]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of the renderable flag' do
|
||||
expect(file_type.errors[:renderable]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a user' do
|
||||
expect(file_type.errors[:user_id]).to be_present
|
||||
expect(file_type.errors[:user_type]).to be_present
|
||||
end
|
||||
end
|
25
spec/models/hint_spec.rb
Normal file
25
spec/models/hint_spec.rb
Normal file
@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Hint do
|
||||
let(:user) { Hint.create }
|
||||
|
||||
it 'validates the presence of an execution environment' do
|
||||
expect(user.errors[:execution_environment_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a locale' do
|
||||
expect(user.errors[:locale]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a message' do
|
||||
expect(user.errors[:message]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a name' do
|
||||
expect(user.errors[:name]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a regular expression' do
|
||||
expect(user.errors[:regular_expression]).to be_present
|
||||
end
|
||||
end
|
65
spec/models/internal_user_spec.rb
Normal file
65
spec/models/internal_user_spec.rb
Normal file
@ -0,0 +1,65 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe InternalUser do
|
||||
let(:password) { SecureRandom.hex }
|
||||
let(:user) { InternalUser.create }
|
||||
|
||||
it 'validates the presence of an email address' do
|
||||
expect(user.errors[:email]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the uniqueness of the email address' do
|
||||
user.update(email: FactoryGirl.create(:admin).email)
|
||||
expect(user.errors[:email]).to be_present
|
||||
end
|
||||
|
||||
context 'when activated' do
|
||||
let(:user) { FactoryGirl.create(:teacher, activation_state: 'active') }
|
||||
|
||||
it 'does not validate the confirmation of the password' do
|
||||
user.update(password: password, password_confirmation: '')
|
||||
expect(user.errors[:password_confirmation]).not_to be_present
|
||||
end
|
||||
|
||||
it 'does not validate the presence of a password' do
|
||||
expect(user.errors[:password]).not_to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not activated' do
|
||||
let(:user) { InternalUser.create(FactoryGirl.attributes_for(:teacher, activation_state: 'pending', password: nil)) }
|
||||
|
||||
it 'validates the confirmation of the password' do
|
||||
user.update(password: password, password_confirmation: '')
|
||||
expect(user.errors[:password_confirmation]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a password' do
|
||||
user.update(name: Forgery::Name.full_name)
|
||||
expect(user.errors[:password]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'validates the domain of the role' do
|
||||
user.update(role: 'Foo')
|
||||
expect(user.errors[:role]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a role' do
|
||||
expect(user.errors[:role]).to be_present
|
||||
end
|
||||
|
||||
describe '#admin?' do
|
||||
it 'is only true for admins' do
|
||||
expect(FactoryGirl.build(:admin).admin?).to be true
|
||||
expect(FactoryGirl.build(:teacher).admin?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#teacher?' do
|
||||
it 'is only true for teachers' do
|
||||
expect(FactoryGirl.build(:admin).teacher?).to be false
|
||||
expect(FactoryGirl.build(:teacher).teacher?).to be true
|
||||
end
|
||||
end
|
||||
end
|
34
spec/models/submission_spec.rb
Normal file
34
spec/models/submission_spec.rb
Normal file
@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Submission do
|
||||
let(:submission) { Submission.create }
|
||||
|
||||
it 'validates the presence of a cause' do
|
||||
expect(submission.errors[:cause]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of an exercise' do
|
||||
expect(submission.errors[:exercise_id]).to be_present
|
||||
end
|
||||
|
||||
it 'validates the presence of a user' do
|
||||
expect(submission.errors[:user_id]).to be_present
|
||||
expect(submission.errors[:user_type]).to be_present
|
||||
end
|
||||
|
||||
%w[download render run test].each do |action|
|
||||
describe "##{action}_url" do
|
||||
let(:submission) { FactoryGirl.create(:submission) }
|
||||
let(:url) { submission.send(:"#{action}_url") }
|
||||
|
||||
it "starts like the #{action} path" do
|
||||
filename = File.basename(__FILE__)
|
||||
expect(url).to start_with(Rails.application.routes.url_helpers.send(:"#{action}_submission_path", submission, filename).sub(filename, ''))
|
||||
end
|
||||
|
||||
it 'ends with a placeholder' do
|
||||
expect(url).to end_with(Submission::FILENAME_URL_PLACEHOLDER)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
spec/policies/application_policy_spec.rb
Normal file
11
spec/policies/application_policy_spec.rb
Normal file
@ -0,0 +1,11 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ApplicationPolicy do
|
||||
describe '#initialize' do
|
||||
context 'without a user' do
|
||||
it 'raises an error' do
|
||||
expect { ApplicationPolicy.new(nil, nil) }.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
73
spec/policies/code_ocean/file_policy_spec.rb
Normal file
73
spec/policies/code_ocean/file_policy_spec.rb
Normal file
@ -0,0 +1,73 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe CodeOcean::FilePolicy do
|
||||
subject { CodeOcean::FilePolicy }
|
||||
|
||||
let(:file) { FactoryGirl.build(:file) }
|
||||
let(:exercise) { FactoryGirl.create(:fibonacci) }
|
||||
let(:submission) { FactoryGirl.create(:submission) }
|
||||
|
||||
permissions :create? do
|
||||
context 'as part of an exercise' do
|
||||
before(:each) { file.context = exercise }
|
||||
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), file)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(exercise.author, file)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as part of a submission' do
|
||||
before(:each) { file.context = submission }
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(submission.author, file)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:admin, :external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), file)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :destroy? do
|
||||
context 'as part of an exercise' do
|
||||
before(:each) { file.context = exercise }
|
||||
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), file)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(exercise.author, file)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as part of a submission' do
|
||||
before(:each) { file.context = submission }
|
||||
|
||||
it 'does not grant access to anyone' do
|
||||
[:admin, :external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), file)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
spec/policies/consumer_policy_spec.rb
Normal file
16
spec/policies/consumer_policy_spec.rb
Normal file
@ -0,0 +1,16 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ConsumerPolicy do
|
||||
subject { ConsumerPolicy }
|
||||
|
||||
[:create?, :destroy?, :edit?, :index?, :new?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins only' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), Consumer.new)
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), Consumer.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
37
spec/policies/error_policy_spec.rb
Normal file
37
spec/policies/error_policy_spec.rb
Normal file
@ -0,0 +1,37 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ErrorPolicy do
|
||||
subject { ErrorPolicy }
|
||||
|
||||
let(:error) { FactoryGirl.build(:error) }
|
||||
|
||||
permissions :index? do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), error)
|
||||
end
|
||||
|
||||
it 'grants access to teachers' do
|
||||
expect(subject).to permit(FactoryGirl.build(:teacher), error)
|
||||
end
|
||||
|
||||
it 'does not grant access to external users' do
|
||||
expect(subject).not_to permit(FactoryGirl.build(:external_user), error)
|
||||
end
|
||||
end
|
||||
|
||||
permissions :show? do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), error)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(error.execution_environment.author, error)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), error)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
spec/policies/execution_environment_policy_spec.rb
Normal file
41
spec/policies/execution_environment_policy_spec.rb
Normal file
@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExecutionEnvironmentPolicy do
|
||||
subject { ExecutionEnvironmentPolicy }
|
||||
|
||||
let(:execution_environment) { FactoryGirl.build(:ruby) }
|
||||
|
||||
[:create?, :index?, :new?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), execution_environment)
|
||||
end
|
||||
|
||||
it 'grants access to teachers' do
|
||||
expect(subject).to permit(FactoryGirl.build(:teacher), execution_environment)
|
||||
end
|
||||
|
||||
it 'does not grant access to external users' do
|
||||
expect(subject).not_to permit(FactoryGirl.build(:external_user), execution_environment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:destroy?, :edit?, :execute_command?, :shell?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), execution_environment)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(execution_environment.author, execution_environment)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), execution_environment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
99
spec/policies/exercise_policy_spec.rb
Normal file
99
spec/policies/exercise_policy_spec.rb
Normal file
@ -0,0 +1,99 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExercisePolicy do
|
||||
subject { ExercisePolicy }
|
||||
|
||||
let(:exercise) { FactoryGirl.build(:fibonacci) }
|
||||
|
||||
[:create?, :index?, :new?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), exercise)
|
||||
end
|
||||
|
||||
it 'grants access to teachers' do
|
||||
expect(subject).to permit(FactoryGirl.build(:teacher), exercise)
|
||||
end
|
||||
|
||||
it 'does not grant access to external users' do
|
||||
expect(subject).not_to permit(FactoryGirl.build(:external_user), exercise)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:destroy?, :edit?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), exercise)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(exercise.author, exercise)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), exercise)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:implement?, :submit?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to anyone' do
|
||||
[:admin, :external_user, :teacher].each do |factory_name|
|
||||
expect(subject).to permit(FactoryGirl.build(factory_name), Exercise.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ExercisePolicy::Scope do
|
||||
describe '#resolve' do
|
||||
let(:admin) { FactoryGirl.create(:admin) }
|
||||
let(:external_user) { FactoryGirl.create(:external_user) }
|
||||
let(:teacher) { FactoryGirl.create(:teacher) }
|
||||
|
||||
before(:each) do
|
||||
[admin, teacher].each do |user|
|
||||
[true, false].each do |public|
|
||||
FactoryGirl.create(:fibonacci, public: public, user_id: user.id, user_type: InternalUser.class.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for admins' do
|
||||
let(:scope) { Pundit.policy_scope!(admin, Exercise) }
|
||||
|
||||
it 'returns all exercises' do
|
||||
expect(scope.map(&:id)).to include(*Exercise.all.map(&:id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'for external users' do
|
||||
let(:scope) { Pundit.policy_scope!(external_user, Exercise) }
|
||||
|
||||
it 'returns only public exercises' do
|
||||
expect(scope.map(&:id)).to include(*Exercise.where(public: true).map(&:id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'for teachers' do
|
||||
let(:scope) { Pundit.policy_scope!(teacher, Exercise) }
|
||||
|
||||
it 'includes all public exercises' do
|
||||
expect(scope.map(&:id)).to include(*Exercise.where(public: true).map(&:id))
|
||||
end
|
||||
|
||||
it 'includes all authored non-public exercises' do
|
||||
expect(scope.map(&:id)).to include(*Exercise.where(public: false, user_id: teacher.id).map(&:id))
|
||||
end
|
||||
|
||||
it "does not include other authors' non-public exercises" do
|
||||
expect(scope.map(&:id)).not_to include(*Exercise.where(public: false).where("user_id <> #{teacher.id}").map(&:id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
spec/policies/external_user_policy_spec.rb
Normal file
16
spec/policies/external_user_policy_spec.rb
Normal file
@ -0,0 +1,16 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ExternalUserPolicy do
|
||||
subject { ExternalUserPolicy }
|
||||
|
||||
[:create?, :destroy?, :edit?, :index?, :new?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins only' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), ExternalUser.new)
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), ExternalUser.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
spec/policies/file_type_policy_spec.rb
Normal file
41
spec/policies/file_type_policy_spec.rb
Normal file
@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe FileTypePolicy do
|
||||
subject { FileTypePolicy }
|
||||
|
||||
let(:file_type) { FactoryGirl.build(:dot_rb) }
|
||||
|
||||
[:create?, :index?, :new?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), file_type)
|
||||
end
|
||||
|
||||
it 'grants access to teachers' do
|
||||
expect(subject).to permit(FactoryGirl.build(:teacher), file_type)
|
||||
end
|
||||
|
||||
it 'does not grant access to external users' do
|
||||
expect(subject).not_to permit(FactoryGirl.build(:external_user), file_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:destroy?, :edit?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), file_type)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(file_type.author, file_type)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), file_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
41
spec/policies/hint_policy_spec.rb
Normal file
41
spec/policies/hint_policy_spec.rb
Normal file
@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe HintPolicy do
|
||||
subject { HintPolicy }
|
||||
|
||||
let(:hint) { FactoryGirl.build(:ruby_no_method_error) }
|
||||
|
||||
[:create?, :index?, :new?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), hint)
|
||||
end
|
||||
|
||||
it 'grants access to teachers' do
|
||||
expect(subject).to permit(FactoryGirl.build(:teacher), hint)
|
||||
end
|
||||
|
||||
it 'does not grant access to external users' do
|
||||
expect(subject).not_to permit(FactoryGirl.build(:external_user), hint)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:destroy?, :edit?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), hint)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
expect(subject).to permit(hint.execution_environment.author, hint)
|
||||
end
|
||||
|
||||
it 'does not grant access to all other users' do
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), hint)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
35
spec/policies/internal_user_policy_spec.rb
Normal file
35
spec/policies/internal_user_policy_spec.rb
Normal file
@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe InternalUserPolicy do
|
||||
subject { InternalUserPolicy }
|
||||
|
||||
[:create?, :edit?, :index?, :new?, :show?, :update?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins only' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), InternalUser.new)
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), InternalUser.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :destroy? do
|
||||
context 'with an admin user' do
|
||||
it 'grants access to no one' do
|
||||
[:admin, :external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), FactoryGirl.build(:admin))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a non-admin user' do
|
||||
it 'grants access to admins only' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), InternalUser.new)
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), FactoryGirl.build(:teacher))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
35
spec/policies/submission_policy_spec.rb
Normal file
35
spec/policies/submission_policy_spec.rb
Normal file
@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SubmissionPolicy do
|
||||
subject { SubmissionPolicy }
|
||||
|
||||
permissions :create? do
|
||||
it 'grants access to anyone' do
|
||||
[:admin, :external_user, :teacher].each do |factory_name|
|
||||
expect(subject).to permit(FactoryGirl.build(factory_name), Submission.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[:download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?].each do |action|
|
||||
permissions(action) do
|
||||
it 'grants access to admins' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), Submission.new)
|
||||
end
|
||||
|
||||
it 'grants access to authors' do
|
||||
user = FactoryGirl.create(:external_user)
|
||||
expect(subject).to permit(user, FactoryGirl.build(:submission, user_id: user.id, user_type: user.class.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :index? do
|
||||
it 'grants access to admins only' do
|
||||
expect(subject).to permit(FactoryGirl.build(:admin), Submission.new)
|
||||
[:external_user, :teacher].each do |factory_name|
|
||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), Submission.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
52
spec/rails_helper.rb
Normal file
52
spec/rails_helper.rb
Normal file
@ -0,0 +1,52 @@
|
||||
MAXIMUM_EXECUTION_TIME = 15
|
||||
|
||||
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
||||
ENV["RAILS_ENV"] ||= 'test'
|
||||
require 'spec_helper'
|
||||
require File.expand_path("../../config/environment", __FILE__)
|
||||
require 'rspec/rails'
|
||||
require 'pundit/rspec'
|
||||
|
||||
# Requires supporting ruby files with custom matchers and macros, etc, in
|
||||
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
|
||||
# run as spec files by default. This means that files in spec/support that end
|
||||
# in _spec.rb will both be required and run as specs, causing the specs to be
|
||||
# run twice. It is recommended that you do not name files matching this glob to
|
||||
# end with _spec.rb. You can configure this pattern with with the --pattern
|
||||
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
||||
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
|
||||
|
||||
# Checks for pending migrations before tests are run.
|
||||
# If you are not using ActiveRecord, you can remove this line.
|
||||
ActiveRecord::Migration.maintain_test_schema!
|
||||
|
||||
Capybara.javascript_driver = :webkit
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.around(:each) do |example|
|
||||
Timeout::timeout(example.metadata[:permitted_execution_time] || MAXIMUM_EXECUTION_TIME) { example.run }
|
||||
end
|
||||
|
||||
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
||||
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
||||
|
||||
config.include(Authentication, type: :feature)
|
||||
config.include(WaitForAjax, type: :feature)
|
||||
config.include(Sorcery::TestHelpers::Rails::Controller, type: :controller)
|
||||
config.include(Sorcery::TestHelpers::Rails::Integration, type: :feature)
|
||||
|
||||
# RSpec Rails can automatically mix in different behaviours to your tests
|
||||
# based on their file location, for example enabling you to call `get` and
|
||||
# `post` in specs under `spec/controllers`.
|
||||
#
|
||||
# You can disable this behaviour by removing the line below, and instead
|
||||
# explicitly tag your specs with their type, e.g.:
|
||||
#
|
||||
# RSpec.describe UsersController, :type => :controller do
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# The different available types are documented in the features, such as in
|
||||
# https://relishapp.com/rspec/rspec-rails/docs
|
||||
config.infer_spec_type_from_file_location!
|
||||
end
|
78
spec/spec_helper.rb
Normal file
78
spec/spec_helper.rb
Normal file
@ -0,0 +1,78 @@
|
||||
# This file was generated by the `rails generate rspec:install` command. Conventionally, all
|
||||
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
||||
# The generated `.rspec` file contains `--require spec_helper` which will cause this
|
||||
# file to always be loaded, without a need to explicitly require it in any files.
|
||||
#
|
||||
# Given that it is always loaded, you are encouraged to keep this file as
|
||||
# light-weight as possible. Requiring heavyweight dependencies from this file
|
||||
# will add to the boot time of your test suite on EVERY test run, even for an
|
||||
# individual file that may not need all of that loaded. Instead, make a
|
||||
# separate helper file that requires this one and then use it only in the specs
|
||||
# that actually need it.
|
||||
#
|
||||
# The `.rspec` file also contains a few flags that are not defaults but that
|
||||
# users commonly want.
|
||||
#
|
||||
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
||||
|
||||
require 'simplecov'
|
||||
SimpleCov.start
|
||||
|
||||
RSpec.configure do |config|
|
||||
# These two settings work together to allow you to limit a spec run
|
||||
# to individual examples or groups you care about by tagging them with
|
||||
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
||||
# get run.
|
||||
config.filter_run :focus
|
||||
config.run_all_when_everything_filtered = true
|
||||
|
||||
# Many RSpec users commonly either run the entire suite or an individual
|
||||
# file, and it's useful to allow more verbose output when running an
|
||||
# individual spec file.
|
||||
if config.files_to_run.one?
|
||||
# Use the documentation formatter for detailed output,
|
||||
# unless a formatter has already been configured
|
||||
# (e.g. via a command-line flag).
|
||||
config.default_formatter = 'doc'
|
||||
end
|
||||
|
||||
# Print the 10 slowest examples and example groups at the
|
||||
# end of the spec run, to help surface which specs are running
|
||||
# particularly slow.
|
||||
config.profile_examples = 10
|
||||
|
||||
# Run specs in random order to surface order dependencies. If you find an
|
||||
# order dependency and want to debug it, you can fix the order by providing
|
||||
# the seed, which is printed after each run.
|
||||
# --seed 1234
|
||||
config.order = :random
|
||||
|
||||
# Seed global randomization in this process using the `--seed` CLI option.
|
||||
# Setting this allows you to use `--seed` to deterministically reproduce
|
||||
# test failures related to randomization by passing the same `--seed` value
|
||||
# as the one that triggered the failure.
|
||||
Kernel.srand config.seed
|
||||
|
||||
# rspec-expectations config goes here. You can use an alternate
|
||||
# assertion/expectation library such as wrong or the stdlib/minitest
|
||||
# assertions if you prefer.
|
||||
config.expect_with :rspec do |expectations|
|
||||
# Enable only the newer, non-monkey-patching expect syntax.
|
||||
# For more details, see:
|
||||
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
||||
expectations.syntax = :expect
|
||||
end
|
||||
|
||||
# rspec-mocks config goes here. You can use an alternate test double
|
||||
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
||||
config.mock_with :rspec do |mocks|
|
||||
# Enable only the newer, non-monkey-patching expect syntax.
|
||||
# For more details, see:
|
||||
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
||||
mocks.syntax = :expect
|
||||
|
||||
# Prevents you from mocking or stubbing a method that does not exist on
|
||||
# a real object. This is generally recommended.
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
end
|
12
spec/support/anonymous_controller.rb
Normal file
12
spec/support/anonymous_controller.rb
Normal file
@ -0,0 +1,12 @@
|
||||
class AnonymousController < ApplicationController
|
||||
def flash
|
||||
@flash ||= {}
|
||||
end
|
||||
|
||||
def redirect_to(*options)
|
||||
end
|
||||
|
||||
def session
|
||||
@session ||= {}
|
||||
end
|
||||
end
|
5
spec/support/authentication.rb
Normal file
5
spec/support/authentication.rb
Normal file
@ -0,0 +1,5 @@
|
||||
module Authentication
|
||||
def sign_in(user, password)
|
||||
page.driver.post(sessions_url, email: user.email, password: password)
|
||||
end
|
||||
end
|
52
spec/support/controllers.rb
Normal file
52
spec/support/controllers.rb
Normal file
@ -0,0 +1,52 @@
|
||||
def expect_assigns(pairs)
|
||||
pairs.each_pair do |key, value|
|
||||
it "assigns @#{key}" do
|
||||
if value.is_a?(Class)
|
||||
expect(assigns(key)).to be_a(value)
|
||||
else
|
||||
object = value.is_a?(Symbol) ? send(value) : value
|
||||
if object.is_a?(ActiveRecord::Relation) || object.is_a?(Array)
|
||||
expect(assigns(key)).to match_array(object)
|
||||
else
|
||||
expect(assigns(key)).to eq(object)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_content_type(content_type)
|
||||
it "responds with content type '#{content_type}'" do
|
||||
expect([response.content_type, response.headers['Content-Type']]).to include(content_type)
|
||||
end
|
||||
end
|
||||
|
||||
def expect_flash_message(type, message)
|
||||
it 'displays a flash message' do
|
||||
expect(flash[type]).to eq(message.is_a?(String) ? message : I18n.t(message))
|
||||
end
|
||||
end
|
||||
|
||||
def expect_redirect(path = nil)
|
||||
if path
|
||||
it "redirects to #{path}" do
|
||||
expect(controller).to redirect_to(path)
|
||||
end
|
||||
else
|
||||
it 'performs a redirect' do
|
||||
expect(response).to be_redirect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_status(status)
|
||||
it "responds with status #{status}" do
|
||||
expect(response.status).to eq(status)
|
||||
end
|
||||
end
|
||||
|
||||
def expect_template(template)
|
||||
it "renders the '#{template}' template" do
|
||||
expect(controller).to render_template(template)
|
||||
end
|
||||
end
|
12
spec/support/database_cleaner.rb
Normal file
12
spec/support/database_cleaner.rb
Normal file
@ -0,0 +1,12 @@
|
||||
require 'database_cleaner'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.before(:suite) do
|
||||
DatabaseCleaner.strategy = :transaction
|
||||
DatabaseCleaner.clean_with(:truncation)
|
||||
end
|
||||
|
||||
config.around(:each) do |example|
|
||||
DatabaseCleaner.cleaning { example.run }
|
||||
end
|
||||
end
|
12
spec/support/docker.rb
Normal file
12
spec/support/docker.rb
Normal file
@ -0,0 +1,12 @@
|
||||
IMAGE = Docker::Image.new(Docker::Connection.new('http://example.org', {}), 'id' => SecureRandom.hex)
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.before(:each) do |example|
|
||||
unless example.metadata[:docker]
|
||||
allow(DockerClient).to receive(:check_availability!).and_return(true)
|
||||
allow(DockerClient).to receive(:image_tags).and_return([IMAGE])
|
||||
allow_any_instance_of(DockerClient).to receive(:find_image_by_tag).and_return(IMAGE)
|
||||
allow_any_instance_of(ExecutionEnvironment).to receive(:working_docker_image?)
|
||||
end
|
||||
end
|
||||
end
|
17
spec/support/features.rb
Normal file
17
spec/support/features.rb
Normal file
@ -0,0 +1,17 @@
|
||||
def expect_forbidden_path(path_name)
|
||||
it "forbids to access the #{path_name.to_s.split('_').join(' ')}" do
|
||||
visit(send(path_name))
|
||||
expect_path('/')
|
||||
end
|
||||
end
|
||||
|
||||
def expect_path(path)
|
||||
expect(URI.parse(current_url).path).to eq(path)
|
||||
end
|
||||
|
||||
def expect_permitted_path(path_name)
|
||||
it "permits to access the #{path_name.to_s.split('_').join(' ')}" do
|
||||
visit(send(path_name))
|
||||
expect_path(send(path_name))
|
||||
end
|
||||
end
|
11
spec/support/wait_for_ajax.rb
Normal file
11
spec/support/wait_for_ajax.rb
Normal file
@ -0,0 +1,11 @@
|
||||
module WaitForAjax
|
||||
def wait_for_ajax
|
||||
Timeout.timeout(Capybara.default_wait_time) do
|
||||
loop until ajax_requests_finished?
|
||||
end
|
||||
end
|
||||
|
||||
def ajax_requests_finished?
|
||||
page.evaluate_script('jQuery.active').zero?
|
||||
end
|
||||
end
|
15
spec/views/execution_environments/shell.html.slim_spec.rb
Normal file
15
spec/views/execution_environments/shell.html.slim_spec.rb
Normal file
@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'execution_environments/shell.html.slim' do
|
||||
let(:execution_environment) { FactoryGirl.create(:ruby) }
|
||||
|
||||
before(:each) do
|
||||
assign(:execution_environment, execution_environment)
|
||||
render
|
||||
end
|
||||
|
||||
it 'contains the required data attributes' do
|
||||
expect(rendered).to have_css('#shell[data-message-timeout]')
|
||||
expect(rendered).to have_css("#shell[data-url='#{execute_command_execution_environment_path(execution_environment)}']")
|
||||
end
|
||||
end
|
41
spec/views/exercises/implement.html.slim_spec.rb
Normal file
41
spec/views/exercises/implement.html.slim_spec.rb
Normal file
@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'exercises/implement.html.slim' do
|
||||
let(:exercise) { FactoryGirl.create(:fibonacci) }
|
||||
let(:files) { exercise.files.visible }
|
||||
let(:non_binary_files) { files.reject { |file| file.file_type.binary? } }
|
||||
|
||||
before(:each) do
|
||||
assign(:exercise, exercise)
|
||||
assign(:files, files)
|
||||
render
|
||||
end
|
||||
|
||||
it 'contains the required editor data attributes' do
|
||||
expect(rendered).to have_css("#editor[data-errors-url='#{execution_environment_errors_path(exercise.execution_environment)}']")
|
||||
expect(rendered).to have_css("#editor[data-exercise-id='#{exercise.id}']")
|
||||
expect(rendered).to have_css('#editor[data-message-timeout]')
|
||||
expect(rendered).to have_css("#editor[data-submissions-url='#{submissions_path}']")
|
||||
end
|
||||
|
||||
it 'contains the required file tree data attributes' do
|
||||
expect(rendered).to have_css('#files[data-entries]')
|
||||
end
|
||||
|
||||
it 'contains a frame for every file' do
|
||||
expect(rendered).to have_css('.frame', count: files.length)
|
||||
end
|
||||
|
||||
it 'assigns the correct code to every editor' do
|
||||
non_binary_files.each do |file|
|
||||
expect(rendered).to include(file.content)
|
||||
end
|
||||
end
|
||||
|
||||
it 'assigns the correct data attributes to every frame' do
|
||||
non_binary_files.each do |file|
|
||||
expect(rendered).to have_css(".editor[data-file-id='#{file.id}'][data-indent-size='#{file.file_type.indent_size}'][data-mode='#{file.file_type.editor_mode}']")
|
||||
expect(rendered).to have_css(".frame[data-filename='#{file.name_with_extension}']")
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user