Add ProgrammingGroup & ProgrammingGroupMembership

* User can create programming group with other users for exercise
* Submission is shared in a group
* Also adjust specs
This commit is contained in:
kiragrammel
2023-08-10 17:07:04 +02:00
committed by Sebastian Serth
parent 0234414bae
commit 319c3ab3b4
42 changed files with 715 additions and 276 deletions

View File

@ -25,7 +25,7 @@ describe FileParameters do
it 'new file' do
submission = create(:submission, exercise: hello_world, id: 1337)
controller.instance_variable_set(:@current_user, submission.contributor)
controller.instance_variable_set(:@current_contributor, submission.contributor)
new_file = create(:file, context: submission)
expect(file_accepted?(new_file)).to be true
@ -61,7 +61,7 @@ describe FileParameters do
submission_learner1 = create(:submission, exercise: hello_world, contributor: learner1)
_submission_learner2 = create(:submission, exercise: hello_world, contributor: learner2)
controller.instance_variable_set(:@current_user, learner2)
controller.instance_variable_set(:@current_contributor, learner2)
other_submissions_file = create(:file, context: submission_learner1)
expect(file_accepted?(other_submissions_file)).to be false
end

View File

@ -22,6 +22,7 @@ describe Lti do
expect(controller.session).to receive(:delete).with(:external_user_id)
expect(controller.session).to receive(:delete).with(:study_group_id)
expect(controller.session).to receive(:delete).with(:embed_options)
expect(controller.session).to receive(:delete).with(:pg_id)
controller.send(:clear_lti_session_data)
end
end

View File

@ -5,166 +5,243 @@ require 'rails_helper'
describe SubmissionsController do
render_views
let(:submission) { create(:submission) }
let(:contributor) { create(:admin) }
let(:exercise) { create(:math) }
let(:submission) { create(:submission, exercise:, contributor:) }
before { allow(controller).to receive(:current_user).and_return(contributor) }
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)
shared_examples 'a regular user' do |record_not_found_status_code|
describe 'POST #create' do
before do
controller.request.accept = 'application/json'
end
expect_json
expect_http_status(:created)
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
context 'with an invalid submission' do
before { post :create, params: {submission: {}} }
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_assigns(submission: Submission)
expect_json
expect_http_status(:unprocessable_entity)
end
end
expect_http_status(record_not_found_status_code)
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} }
context 'with a valid binary filename' do
let(:exercise) { create(:sql_select) }
let(:submission) { create(:submission, exercise:, contributor:) }
expect_http_status(:not_found)
end
before { get :download_file, params: {filename: file.name_with_extension, id: submission.id} }
context 'with a valid binary filename' do
let(:submission) { create(:submission, exercise: create(:sql_select)) }
context 'with a binary file' do
let(:file) { submission.collect_files.detect {|file| file.name == 'exercise' && file.file_type.file_extension == '.sql' } }
before { get :download_file, params: {filename: file.name_with_extension, id: submission.id} }
expect_assigns(file: :file)
expect_assigns(submission: :submission)
expect_content_type('application/octet-stream')
expect_http_status(:ok)
context 'with a binary file' do
let(:file) { submission.collect_files.detect {|file| file.name == 'exercise' && file.file_type.file_extension == '.sql' } }
it 'sets the correct filename' do
expect(response.headers['Content-Disposition']).to include("attachment; filename=\"#{file.name_with_extension}\"")
end
end
end
expect_assigns(file: :file)
expect_assigns(submission: :submission)
expect_content_type('application/octet-stream')
expect_http_status(:ok)
context 'with a valid filename' do
let(:exercise) { create(:audio_video) }
let(:submission) { create(:submission, exercise:, contributor:) }
it 'sets the correct filename' do
expect(response.headers['Content-Disposition']).to include("attachment; filename=\"#{file.name_with_extension}\"")
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
context 'with a valid filename' do
let(:submission) { create(:submission, exercise: create(:audio_video)) }
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'] }
before { get :download_file, params: {filename: file.name_with_extension, id: submission.id} }
context 'with an invalid filename' do
let(:filename) { SecureRandom.hex }
context 'with a binary file' do
let(:file) { submission.collect_files.detect {|file| file.file_type.file_extension == '.mp4' } }
before { get :render_file, params: {filename:, id: submission.id, token:} }
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
expect_http_status(record_not_found_status_code)
end
context 'with a non-binary file' do
let(:file) { submission.collect_files.detect {|file| file.file_type.file_extension == '.js' } }
context 'with a valid filename' do
let(:exercise) { create(:audio_video) }
let(:submission) { create(:submission, exercise:, contributor:) }
let(:filename) { file.name_with_extension }
expect_assigns(file: :file)
expect_assigns(submission: :submission)
expect_content_type('application/octet-stream')
expect_http_status(:ok)
before { get :render_file, params: {filename:, id: submission.id, token:} }
it 'sets the correct filename' do
expect(response.headers['Content-Disposition']).to include("attachment; filename=\"#{file.name_with_extension}\"")
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
end
describe 'GET #index' do
before do
create_pair(:submission)
get :index
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} }
expect_assigns(submissions: Submission.all)
expect_http_status(:ok)
expect_template(:index)
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(:not_found)
end
context 'with a valid filename' do
let(:submission) { create(:submission, exercise: create(:audio_video)) }
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
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
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
expect_assigns(file: :file)
expect_http_status(204)
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} }
describe 'GET #score' do
let(:perform_request) { proc { get :score, format: :json, params: {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
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
# Render views requested in controller tests in order to get json responses
# https://github.com/rails/jbuilder/issues/32
render_views
before { get :show, params: {id: submission.id}, format: :json }
expect_assigns(submission: :submission)
expect_http_status(:ok)
%i[run test].each do |action|
describe "##{action}_url" do
let(:url) { response.parsed_body.with_indifferent_access.fetch("#{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}.json")
end
end
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
describe '#score_url' do
let(:url) { response.parsed_body.with_indifferent_access.fetch('score_url') }
it 'corresponds to the score path' do
expect(url).to eq(Rails.application.routes.url_helpers.score_submission_path(submission, format: :json))
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)
@ -173,88 +250,57 @@ describe SubmissionsController do
end
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
# Render views requested in controller tests in order to get json responses
# https://github.com/rails/jbuilder/issues/32
render_views
before { get :show, params: {id: submission.id}, format: :json }
expect_assigns(submission: :submission)
expect_http_status(:ok)
%i[run test].each do |action|
describe "##{action}_url" do
let(:url) { response.parsed_body.with_indifferent_access.fetch("#{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}.json")
end
end
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))
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
it 'includes a token' do
expect(url).to include '?token='
end
end
describe '#score_url' do
let(:url) { response.parsed_body.with_indifferent_access.fetch('score_url') }
it 'corresponds to the score path' do
expect(url).to eq(Rails.application.routes.url_helpers.score_submission_path(submission, format: :json))
end
expect_redirect(:root)
end
end
describe 'GET #score' do
let(:perform_request) { proc { get :score, format: :json, params: {id: submission.id} } }
context 'with an admin user' do
let(:contributor) { create(:admin) }
before { allow(controller).to receive(:current_user).and_return(contributor) }
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]) }
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
allow(controller).to receive_messages(current_contributor: contributor, current_user: group_author)
end
expect_assigns(submission: :submission)
expect_http_status(204)
it_behaves_like 'a regular user', :unauthorized
it_behaves_like 'denies access for regular, non-admin users'
end
describe 'GET #test' do
let(:file) { submission.collect_files.detect(&:teacher_defined_assessment?) }
let(:output) { {} }
context 'with a learner' do
let(:contributor) { create(:external_user) }
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}
allow(controller).to receive_messages(current_user: contributor)
end
expect_assigns(submission: :submission)
expect_assigns(file: :file)
expect_http_status(204)
it_behaves_like 'a regular user', :unauthorized
it_behaves_like 'denies access for regular, non-admin users'
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
FactoryBot.define do
factory :programming_group do
exercise factory: :math
after(:build) do |programming_group|
# Do not change anything if users were provided explicitly
next if programming_group.users.present?
programming_group.users = build_list(:external_user, 2)
end
end
end

View File

@ -10,6 +10,11 @@ FactoryBot.define do
submission.exercise.files.editable.visible.each do |file|
submission.add_file(content: file.content, file_id: file.id)
end
# Do not change anything if a study group was provided explicitly or user has no study groups
next if submission.study_group.present? || submission.users.first.study_groups.blank?
submission.update!(study_group: submission.users.first.study_groups.first)
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'rails_helper'
describe ProgrammingGroupPolicy do
subject(:policy) { described_class }
let(:programming_group) { build(:programming_group) }
%i[new? create?].each do |action|
permissions(action) do
it 'grants access to everyone' do
%i[external_user teacher admin].each do |factory_name|
expect(policy).to permit(create(factory_name), programming_group)
end
end
end
end
end

View File

@ -15,13 +15,23 @@ describe SubmissionPolicy do
%i[download_file? render_file? run? score? show? statistics? stop? test?].each do |action|
permissions(action) do
let(:exercise) { build(:math) }
let(:group_author) { build(:external_user) }
let(:other_group_author) { build(:external_user) }
it 'grants access to admins' do
expect(policy).to permit(build(:admin), Submission.new)
end
it 'grants access to authors' do
contributor = create(:external_user)
expect(policy).to permit(contributor, build(:submission, exercise: Exercise.new, contributor:))
expect(policy).to permit(contributor, build(:submission, exercise:, contributor:))
end
it 'grants access to other authors of the programming group' do
contributor = build(:programming_group, exercise:, users: [group_author, other_group_author])
expect(policy).to permit(group_author, build(:submission, exercise:, contributor:))
expect(policy).to permit(other_group_author, build(:submission, exercise:, contributor:))
end
end
end

View File

@ -6,9 +6,12 @@ describe 'exercises/implement.html.slim' do
let(:exercise) { create(:fibonacci) }
let(:files) { exercise.files.visible }
let(:non_binary_files) { files.reject {|file| file.file_type.binary? } }
let(:user) { create(:admin) }
before do
allow(view).to receive(:current_user).and_return(create(:admin))
without_partial_double_verification do
allow(view).to receive_messages(current_user: user, current_contributor: user)
end
assign(:exercise, exercise)
assign(:files, files)
assign(:paths, [])