transferred Code Ocean from original repository to GitHub

This commit is contained in:
Hauke Klement
2015-01-22 09:51:49 +01:00
commit 4cbf9970b1
683 changed files with 11979 additions and 0 deletions

170
spec/concerns/lti_spec.rb Normal file
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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
View 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

View 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
View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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
View File

@ -0,0 +1 @@
puts 'Hello World'

View 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
View 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

View 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

View File

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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

View File

@ -0,0 +1,12 @@
class AnonymousController < ApplicationController
def flash
@flash ||= {}
end
def redirect_to(*options)
end
def session
@session ||= {}
end
end

View File

@ -0,0 +1,5 @@
module Authentication
def sign_in(user, password)
page.driver.post(sessions_url, email: user.email, password: password)
end
end

View 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

View 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
View 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
View 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

View 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

View 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

View 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