
Previously, the filename was URL-encoded, thus each / was replaced with %2F. This caused issues with some Apache2 configuration, smartly mingling with the URL to either encode it a second time (resulting in %252F) or decoding it (generating a real /). However, for authenticated file downloads with the JWT, we hardly require a byte-by-byte matching. With these changes, the URL parameter is no longer URL-encoded, so that Apache2 won't break our implementation any longer. Further, we use this opportunity to get rid of the unnecessary .json extension for those filename routes, simplifying the routes generated and doing some further cleanup.
372 lines
13 KiB
Ruby
372 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe SubmissionsController do
|
|
render_views
|
|
|
|
let(:exercise) { create(:math) }
|
|
let(:cause) { 'save' }
|
|
let(:submission) { create(:submission, exercise:, contributor:, cause:) }
|
|
|
|
shared_examples 'a regular user' do |record_not_found_status_code|
|
|
describe 'POST #create' do
|
|
before do
|
|
controller.request.accept = 'application/json'
|
|
end
|
|
|
|
context 'with a valid submission' do
|
|
let(:exercise) { create(:hello_world) }
|
|
let(:perform_request) { proc { post :create, format: :json, params: {submission: attributes_for(:submission, exercise_id: exercise.id)} } }
|
|
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(submission: Submission)
|
|
|
|
it 'creates the submission' do
|
|
expect { perform_request.call }.to change(Submission, :count).by(1)
|
|
end
|
|
|
|
expect_json
|
|
expect_http_status(:created)
|
|
end
|
|
|
|
context 'with an invalid submission' do
|
|
before { post :create, params: {submission: {}} }
|
|
|
|
expect_assigns(submission: Submission)
|
|
expect_json
|
|
expect_http_status(:unprocessable_entity)
|
|
end
|
|
end
|
|
|
|
describe 'GET #download' do
|
|
let(:perform_request) { proc { get :download, params: {id: submission.id} } }
|
|
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(submission: :submission)
|
|
expect_http_status(:ok)
|
|
end
|
|
|
|
describe 'GET #download_file' do
|
|
context 'with an invalid filename' do
|
|
before { get :download_file, params: {filename: SecureRandom.hex, id: submission.id, format: :json} }
|
|
|
|
expect_http_status(record_not_found_status_code)
|
|
end
|
|
|
|
context 'with a valid binary filename' do
|
|
let(:exercise) { create(:sql_select) }
|
|
let(:submission) { create(:submission, exercise:, contributor:) }
|
|
|
|
before { get :download_file, params: {filename: file.name_with_extension, id: submission.id} }
|
|
|
|
context 'with a binary file' do
|
|
let(:file) { submission.collect_files.detect {|file| file.name == 'exercise' && file.file_type.file_extension == '.sql' } }
|
|
|
|
expect_assigns(file: :file)
|
|
expect_assigns(submission: :submission)
|
|
expect_content_type('application/octet-stream')
|
|
expect_http_status(:ok)
|
|
|
|
it 'sets the correct filename' do
|
|
expect(response.headers['Content-Disposition']).to include("attachment; filename=\"#{file.name_with_extension}\"")
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with a valid filename' do
|
|
let(:exercise) { create(:audio_video) }
|
|
let(:submission) { create(:submission, exercise:, contributor:) }
|
|
|
|
before { get :download_file, params: {filename: file.name_with_extension, id: submission.id} }
|
|
|
|
context 'with a binary file' do
|
|
let(:file) { submission.collect_files.detect {|file| file.file_type.file_extension == '.mp4' } }
|
|
|
|
expect_assigns(file: :file)
|
|
expect_assigns(submission: :submission)
|
|
|
|
it 'sets the correct redirect' do
|
|
expect(response.location).to eq protected_upload_url(id: file, filename: file.filepath)
|
|
end
|
|
end
|
|
|
|
context 'with a non-binary file' do
|
|
let(:file) { submission.collect_files.detect {|file| file.file_type.file_extension == '.js' } }
|
|
|
|
expect_assigns(file: :file)
|
|
expect_assigns(submission: :submission)
|
|
expect_content_type('application/octet-stream')
|
|
expect_http_status(:ok)
|
|
|
|
it 'sets the correct filename' do
|
|
expect(response.headers['Content-Disposition']).to include("attachment; filename=\"#{file.name_with_extension}\"")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET #finalize' do
|
|
let(:perform_request) { proc { get :finalize, params: {id: submission.id} } }
|
|
let(:cause) { 'assess' }
|
|
|
|
context 'when the request is performed' do
|
|
before { perform_request.call }
|
|
|
|
expect_assigns(submission: :submission)
|
|
expect_redirect
|
|
end
|
|
|
|
it 'updates cause to submit' do
|
|
expect { perform_request.call && submission.reload }.to change(submission, :cause).from('assess').to('submit')
|
|
end
|
|
|
|
context 'when contributing to a community solution is possible' do
|
|
let!(:community_solution) { CommunitySolution.create(exercise:) }
|
|
|
|
before do
|
|
allow(Java21Study).to receive(:allow_redirect_to_community_solution?).and_return(true)
|
|
perform_request.call
|
|
end
|
|
|
|
expect_redirect { edit_community_solution_path(community_solution, lock_id: CommunitySolutionLock.last) }
|
|
end
|
|
|
|
context 'when sharing exercise feedback is desired' do
|
|
before do
|
|
uef&.save!
|
|
allow_any_instance_of(Submission).to receive(:redirect_to_feedback?).and_return(true)
|
|
perform_request.call
|
|
end
|
|
|
|
context 'without any previous feedback' do
|
|
let(:uef) { nil }
|
|
|
|
expect_redirect { new_exercise_user_exercise_feedback_path(exercise_id: submission.exercise) }
|
|
end
|
|
|
|
context 'with a previous feedback for the same exercise' do
|
|
let(:uef) { create(:user_exercise_feedback, exercise:, user: current_user) }
|
|
|
|
expect_redirect { edit_exercise_user_exercise_feedback_path(uef, exercise_id: exercise) }
|
|
end
|
|
end
|
|
|
|
context 'with an RfC' do
|
|
before do
|
|
rfc.save!
|
|
allow_any_instance_of(Submission).to receive(:redirect_to_feedback?).and_return(false)
|
|
perform_request.call
|
|
end
|
|
|
|
context 'when an own RfC is unsolved' do
|
|
let(:rfc) { create(:rfc, user: current_user, exercise:, submission:) }
|
|
|
|
expect_flash_message(:notice, I18n.t('exercises.editor.exercise_finished_redirect_to_own_rfc'))
|
|
expect_redirect { request_for_comment_url(rfc) }
|
|
end
|
|
|
|
context 'when another RfC is unsolved' do
|
|
let(:rfc) { create(:rfc, exercise:) }
|
|
|
|
expect_flash_message(:notice, I18n.t('exercises.editor.exercise_finished_redirect_to_rfc'))
|
|
expect_redirect { request_for_comment_url(rfc) }
|
|
end
|
|
end
|
|
|
|
context 'when neither a community solution, feedback nor RfC is available' do
|
|
before do
|
|
allow_any_instance_of(Submission).to receive(:redirect_to_feedback?).and_return(false)
|
|
perform_request.call
|
|
end
|
|
|
|
expect_redirect { lti_return_path(submission_id: submission.id) }
|
|
end
|
|
end
|
|
|
|
describe 'GET #render_file' do
|
|
let(:file) { submission.files.first }
|
|
let(:signed_url) { AuthenticatedUrlHelper.sign(render_submission_url(submission, filename), submission) }
|
|
let(:token) { Rack::Utils.parse_nested_query(URI.parse(signed_url).query)['token'] }
|
|
|
|
context 'with an invalid filename' do
|
|
let(:filename) { SecureRandom.hex }
|
|
|
|
before { get :render_file, params: {filename:, id: submission.id, token:} }
|
|
|
|
expect_http_status(record_not_found_status_code)
|
|
end
|
|
|
|
context 'with a valid filename' do
|
|
let(:exercise) { create(:audio_video) }
|
|
let(:submission) { create(:submission, exercise:, contributor:) }
|
|
let(:filename) { file.name_with_extension }
|
|
|
|
before { get :render_file, params: {filename:, id: submission.id, token:} }
|
|
|
|
context 'with a binary file' do
|
|
let(:file) { submission.collect_files.detect {|file| file.file_type.file_extension == '.mp4' } }
|
|
let(:signed_url_video) { AuthenticatedUrlHelper.sign(render_protected_upload_url(id: file, filename: file.filepath), file) }
|
|
|
|
expect_assigns(file: :file)
|
|
expect_assigns(submission: :submission)
|
|
|
|
it 'sets the correct redirect' do
|
|
expect(response.location).to eq signed_url_video
|
|
end
|
|
end
|
|
|
|
context 'with a non-binary file' do
|
|
let(:file) { submission.collect_files.detect {|file| file.file_type.file_extension == '.js' } }
|
|
|
|
expect_assigns(file: :file)
|
|
expect_assigns(submission: :submission)
|
|
expect_content_type('text/javascript')
|
|
expect_http_status(:ok)
|
|
|
|
it 'renders the file content' do
|
|
expect(response.body).to eq(file.content)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET #run' do
|
|
let(:file) { submission.collect_files.detect(&:main_file?) }
|
|
let(:perform_request) { get :run, format: :json, params: {filename: file.filepath, id: submission.id} }
|
|
|
|
context 'when no errors occur during execution' do
|
|
before do
|
|
allow_any_instance_of(described_class).to receive(:hijack)
|
|
allow_any_instance_of(described_class).to receive(:close_client_connection)
|
|
allow_any_instance_of(Submission).to receive(:run).and_return({})
|
|
allow_any_instance_of(described_class).to receive(:save_testrun_output)
|
|
perform_request
|
|
end
|
|
|
|
expect_assigns(submission: :submission)
|
|
expect_assigns(file: :file)
|
|
expect_http_status(204)
|
|
end
|
|
end
|
|
|
|
describe 'GET #score' do
|
|
let(:perform_request) { proc { get :score, format: :json, params: {id: submission.id} } }
|
|
|
|
before do
|
|
allow_any_instance_of(described_class).to receive(:hijack)
|
|
allow_any_instance_of(described_class).to receive(:kill_client_socket)
|
|
perform_request.call
|
|
end
|
|
|
|
expect_assigns(submission: :submission)
|
|
expect_http_status(204)
|
|
end
|
|
|
|
describe 'GET #show' do
|
|
before { get :show, params: {id: submission.id} }
|
|
|
|
expect_assigns(submission: :submission)
|
|
expect_http_status(:ok)
|
|
expect_template(:show)
|
|
end
|
|
|
|
describe 'GET #show.json' do
|
|
before { get :show, params: {id: submission.id}, format: :json }
|
|
|
|
expect_assigns(submission: :submission)
|
|
expect_http_status(:ok)
|
|
|
|
it 'includes the desired fields' do
|
|
expect(response.parsed_body.keys).to include('id', 'files')
|
|
expect(response.parsed_body['files'].first.keys).to include('id', 'file_id')
|
|
end
|
|
|
|
describe '#render_url' do
|
|
let(:supported_urls) { response.parsed_body.with_indifferent_access.fetch('render_url') }
|
|
let(:file) { submission.collect_files.detect(&:main_file?) }
|
|
let(:url) { supported_urls.find {|hash| hash[:filepath] == file.filepath }['url'] }
|
|
|
|
it 'starts like the render path' do
|
|
expect(url).to start_with(Rails.application.routes.url_helpers.render_submission_url(submission, file.filepath, host: request.host))
|
|
end
|
|
|
|
it 'includes a token' do
|
|
expect(url).to include '?token='
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET #test' do
|
|
let(:file) { submission.collect_files.detect(&:teacher_defined_assessment?) }
|
|
let(:output) { {} }
|
|
|
|
before do
|
|
file.update(hidden: false)
|
|
allow_any_instance_of(described_class).to receive(:hijack)
|
|
allow_any_instance_of(described_class).to receive(:kill_client_socket)
|
|
get :test, params: {filename: "#{file.filepath}.json", id: submission.id}
|
|
end
|
|
|
|
expect_assigns(submission: :submission)
|
|
expect_assigns(file: :file)
|
|
expect_http_status(204)
|
|
end
|
|
end
|
|
|
|
shared_examples 'denies access for regular, non-admin users' do # rubocop:disable RSpec/SharedContext
|
|
describe 'GET #index' do
|
|
before do
|
|
create_pair(:submission, contributor:, exercise:)
|
|
get :index
|
|
end
|
|
|
|
expect_redirect(:root)
|
|
end
|
|
end
|
|
|
|
context 'with an admin user' do
|
|
let(:contributor) { create(:admin) }
|
|
let(:current_user) { contributor }
|
|
|
|
before { allow(controller).to receive_messages(current_user:) }
|
|
|
|
describe 'GET #index' do
|
|
before do
|
|
create_pair(:submission, contributor:, exercise:)
|
|
get :index
|
|
end
|
|
|
|
expect_assigns(submissions: Submission.all)
|
|
expect_http_status(:ok)
|
|
expect_template(:index)
|
|
end
|
|
|
|
it_behaves_like 'a regular user', :not_found
|
|
end
|
|
|
|
context 'with a programming group' do
|
|
let(:group_author) { create(:external_user) }
|
|
let(:other_group_author) { create(:external_user) }
|
|
let(:contributor) { create(:programming_group, exercise:, users: [group_author, other_group_author]) }
|
|
let(:current_user) { group_author }
|
|
|
|
before { allow(controller).to receive_messages(current_contributor: contributor, current_user:) }
|
|
|
|
it_behaves_like 'a regular user', :unauthorized
|
|
it_behaves_like 'denies access for regular, non-admin users'
|
|
end
|
|
|
|
context 'with a learner' do
|
|
let(:contributor) { create(:external_user) }
|
|
let(:current_user) { contributor }
|
|
|
|
before { allow(controller).to receive_messages(current_user:) }
|
|
|
|
it_behaves_like 'a regular user', :unauthorized
|
|
it_behaves_like 'denies access for regular, non-admin users'
|
|
end
|
|
end
|