diff --git a/Gemfile b/Gemfile
index 4bdc5e8d..6ca05b58 100644
--- a/Gemfile
+++ b/Gemfile
@@ -72,6 +72,7 @@ group :test do
gem 'database_cleaner'
gem 'nyan-cat-formatter'
gem 'rspec-autotest'
+ gem 'rspec-collection_matchers'
gem 'rspec-rails'
gem 'shoulda-matchers'
gem 'simplecov', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index e3afa6a6..8ba86613 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -318,6 +318,8 @@ GEM
rspec-mocks (~> 3.9.0)
rspec-autotest (1.0.2)
rspec-core (>= 2.99.0.beta1, < 4.0.0)
+ rspec-collection_matchers (1.1.3)
+ rspec-expectations (>= 2.99.0.beta1)
rspec-core (3.9.0)
rspec-support (~> 3.9.0)
rspec-expectations (3.9.0)
@@ -486,6 +488,7 @@ DEPENDENCIES
ransack
rest-client
rspec-autotest
+ rspec-collection_matchers
rspec-rails
rubocop
rubocop-rspec
diff --git a/app/services/exercise_service/check_external.rb b/app/services/exercise_service/check_external.rb
index 1aac945b..a7add85d 100644
--- a/app/services/exercise_service/check_external.rb
+++ b/app/services/exercise_service/check_external.rb
@@ -16,8 +16,8 @@ module ExerciseService
response_hash = JSON.parse(response.body, symbolize_names: true)
{error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right))
- rescue Faraday::Error => e
- {error: true, message: I18n.t('exercises.export_codeharbor.error', message: e.message)}
+ rescue Faraday::Error, JSON::ParserError
+ {error: true, message: I18n.t('exercises.export_codeharbor.error')}
end
private
diff --git a/app/services/exercise_service/push_external.rb b/app/services/exercise_service/push_external.rb
index 93dacb81..3410a7ba 100644
--- a/app/services/exercise_service/push_external.rb
+++ b/app/services/exercise_service/push_external.rb
@@ -17,9 +17,9 @@ module ExerciseService
request.body = body
end
- return response.success? ? nil : response.body
+ response.success? ? nil : response.body
rescue StandardError => e
- return e.message
+ e.message
end
end
diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb
index e22aa709..7a44c5aa 100644
--- a/app/services/proforma_service/convert_exercise_to_task.rb
+++ b/app/services/proforma_service/convert_exercise_to_task.rb
@@ -22,11 +22,9 @@ module ProformaService
title: @exercise.title,
description: @exercise.description,
internal_description: @exercise.instructions,
- # proglang: proglang,
files: task_files,
tests: tests,
uuid: uuid,
- # parent_uuid: parent_uuid,
language: DEFAULT_LANGUAGE,
model_solutions: model_solutions,
import_checksum: @exercise.import_checksum
@@ -66,8 +64,6 @@ module ProformaService
files: test_file(file),
meta_data: {
'feedback-message' => file.feedback_message
- # 'testing-framework' => test.testing_framework&.name,
- # 'testing-framework-version' => test.testing_framework&.version
}.compact
)
end
diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb
index 0264494f..f87a9354 100644
--- a/app/services/proforma_service/convert_task_to_exercise.rb
+++ b/app/services/proforma_service/convert_task_to_exercise.rb
@@ -22,7 +22,6 @@ module ProformaService
description: @task.description,
instructions: @task.internal_description,
files: files,
- uuid: @task.uuid,
import_checksum: @task.checksum
)
end
@@ -56,10 +55,10 @@ module ProformaService
name: File.basename(file.filename, '.*'),
read_only: file.usage_by_lms != 'edit',
role: file.internal_description,
- path: File.dirname(file.filename)
+ path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename)
}.tap do |params|
if file.binary
- params[:native_file] = FileIO.new(file.content.force_encoding('UTF-8'), File.basename(file.filename))
+ params[:native_file] = FileIO.new(file.content.dup.force_encoding('UTF-8'), File.basename(file.filename))
else
params[:content] = file.content
end
diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb
index b8ec628b..4fa0676e 100644
--- a/app/services/proforma_service/import.rb
+++ b/app/services/proforma_service/import.rb
@@ -12,7 +12,7 @@ module ProformaService
importer = Proforma::Importer.new(@zip)
@task = importer.perform
- exercise = Exercise.find_by(uuid: @task.uuid)
+ exercise = base_exercise
exercise_files = exercise&.files&.to_a
exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise: exercise)
@@ -26,6 +26,17 @@ module ProformaService
private
+ def base_exercise
+ exercise = Exercise.find_by(uuid: @task.uuid)
+ if exercise
+ return exercise if ExercisePolicy.new(@user, exercise).update?
+
+ return Exercise.new(uuid: SecureRandom.uuid, unpublished: true)
+ end
+
+ Exercise.new(uuid: @task.uuid || SecureRandom.uuid, unpublished: true)
+ end
+
def import_multi
Zip::File.open(@zip.path) do |zip_file|
zip_files = zip_file.filter { |entry| entry.name.match?(/\.zip$/) }
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8a0707e1..6211c3e1 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -329,7 +329,7 @@ en:
dialogtitle: Export to Codeharbor
successfully_exported: 'Exercise has been successfully exported.
ID: %{id}
Title: %{title}'
export_failed: 'Export has failed.
ID: %{id}
Title: %{title}
Error: %{error}'
- error: 'An error occurred while contacting Codeharbor
Error: %{message}'
+ error: 'An error occurred while contacting Codeharbor'
checking_codeharbor: Checking if the exercise exists on Codeharbor.
buttons:
retry: Retry
diff --git a/spec/factories/code_ocean/file.rb b/spec/factories/code_ocean/file.rb
index 186ad1d1..64245e97 100644
--- a/spec/factories/code_ocean/file.rb
+++ b/spec/factories/code_ocean/file.rb
@@ -10,6 +10,24 @@ module CodeOcean
name { SecureRandom.hex }
read_only { false }
role { 'main_file' }
+
+ trait(:image) do
+ association :file_type, factory: :dot_png
+ name { 'poster' }
+ native_file { Rack::Test::UploadedFile.new(Rails.root.join('db', 'seeds', 'audio_video', 'poster.png'), 'image/png') }
+ end
+ end
+
+ factory :test_file, class: CodeOcean::File do
+ content { '' }
+ association :context, factory: :dummy
+ association :file_type, factory: :dot_rb
+ hidden { true }
+ name { SecureRandom.hex }
+ read_only { true }
+ role { 'teacher_defined_test' }
+ feedback_message { 'feedback_message' }
+ weight { 1 }
end
end
end
diff --git a/spec/services/exercise_service/check_external_spec.rb b/spec/services/exercise_service/check_external_spec.rb
new file mode 100644
index 00000000..ea7874c6
--- /dev/null
+++ b/spec/services/exercise_service/check_external_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ExerciseService::CheckExternal do
+ describe '.new' do
+ subject(:export_service) { described_class.new(uuid: uuid, codeharbor_link: codeharbor_link) }
+
+ let(:uuid) { SecureRandom.uuid }
+ let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) }
+
+ it 'assigns uuid' do
+ expect(export_service.instance_variable_get(:@uuid)).to be uuid
+ end
+
+ it 'assigns codeharbor_link' do
+ expect(export_service.instance_variable_get(:@codeharbor_link)).to be codeharbor_link
+ end
+ end
+
+ describe '#execute' do
+ subject(:check_external_service) { described_class.call(uuid: uuid, codeharbor_link: codeharbor_link) }
+
+ let(:uuid) { SecureRandom.uuid }
+ let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) }
+ let(:response) { {}.to_json }
+
+ before { stub_request(:post, codeharbor_link.check_uuid_url).to_return(body: response) }
+
+ it 'calls the correct url' do
+ expect(check_external_service).to have_requested(:post, codeharbor_link.check_uuid_url)
+ end
+
+ it 'submits the correct headers' do
+ expect(check_external_service).to have_requested(:post, codeharbor_link.check_uuid_url)
+ .with(headers: {content_type: 'application/json', authorization: "Bearer #{codeharbor_link.api_key}"})
+ end
+
+ it 'submits the correct body' do
+ expect(check_external_service).to have_requested(:post, codeharbor_link.check_uuid_url)
+ .with(body: {uuid: uuid}.to_json)
+ end
+
+ context 'when response contains a JSON with expected keys' do
+ let(:response) { {message: 'message', exercise_found: true, update_right: true}.to_json }
+
+ it 'returns the correct hash' do
+ expect(check_external_service).to eql(error: false, message: 'message', exercise_found: true, update_right: true)
+ end
+
+ context 'with different values' do
+ let(:response) { {message: 'message', exercise_found: false, update_right: false}.to_json }
+
+ it 'returns the correct hash' do
+ expect(check_external_service).to eql(error: false, message: 'message', exercise_found: false, update_right: false)
+ end
+ end
+ end
+
+ context 'when response does not contain JSON' do
+ let(:response) { 'foo' }
+
+ it 'returns the correct hash' do
+ expect(check_external_service).to eql(error: true, message: I18n.t('exercises.export_codeharbor.error'))
+ end
+ end
+
+ context 'when the request fails' do
+ before { allow(Faraday).to receive(:new).and_raise(Faraday::Error, 'error') }
+
+ it 'returns the correct hash' do
+ expect(check_external_service).to eql(error: true, message: I18n.t('exercises.export_codeharbor.error'))
+ end
+ end
+ end
+end
diff --git a/spec/services/exercise_service/push_external_spec.rb b/spec/services/exercise_service/push_external_spec.rb
new file mode 100644
index 00000000..20c07a0a
--- /dev/null
+++ b/spec/services/exercise_service/push_external_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ExerciseService::PushExternal do
+ describe '.new' do
+ subject(:push_external) { described_class.new(zip: zip, codeharbor_link: codeharbor_link) }
+
+ let(:zip) { ProformaService::ExportTask.call(exercise: FactoryBot.build(:dummy)) }
+ let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) }
+
+ it 'assigns zip' do
+ expect(push_external.instance_variable_get(:@zip)).to be zip
+ end
+
+ it 'assigns codeharbor_link' do
+ expect(push_external.instance_variable_get(:@codeharbor_link)).to be codeharbor_link
+ end
+ end
+
+ describe '#execute' do
+ subject(:push_external) { described_class.call(zip: zip, codeharbor_link: codeharbor_link) }
+
+ let(:zip) { ProformaService::ExportTask.call(exercise: FactoryBot.build(:dummy)) }
+ let(:codeharbor_link) { FactoryBot.build(:codeharbor_link) }
+ let(:status) { 200 }
+ let(:response) { '' }
+
+ before { stub_request(:post, codeharbor_link.push_url).to_return(status: status, body: response) }
+
+ it 'calls the correct url' do
+ expect(push_external).to have_requested(:post, codeharbor_link.push_url)
+ end
+
+ it 'submits the correct headers' do
+ expect(push_external).to have_requested(:post, codeharbor_link.push_url)
+ .with(headers: {content_type: 'application/zip',
+ authorization: "Bearer #{codeharbor_link.api_key}",
+ content_length: zip.string.length})
+ end
+
+ it 'submits the correct body' do
+ expect(push_external).to have_requested(:post, codeharbor_link.push_url)
+ .with(body: zip.string)
+ end
+
+ context 'when response status is success' do
+ it { is_expected.to be nil }
+
+ context 'when response status is 500' do
+ let(:status) { 500 }
+ let(:response) { 'an error occured' }
+
+ it { is_expected.to be response }
+ end
+ end
+
+ context 'when an error occurs' do
+ before { allow(Faraday).to receive(:new).and_raise(StandardError) }
+
+ it { is_expected.not_to be nil }
+ end
+ end
+end
diff --git a/spec/services/proforma_service/convert_exercise_to_task_spec.rb b/spec/services/proforma_service/convert_exercise_to_task_spec.rb
new file mode 100644
index 00000000..ae2976d1
--- /dev/null
+++ b/spec/services/proforma_service/convert_exercise_to_task_spec.rb
@@ -0,0 +1,198 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ProformaService::ConvertExerciseToTask do
+ describe '.new' do
+ subject(:convert_to_task) { described_class.new(exercise: exercise) }
+
+ let(:exercise) { FactoryBot.build(:dummy) }
+
+ it 'assigns exercise' do
+ expect(convert_to_task.instance_variable_get(:@exercise)).to be exercise
+ end
+ end
+
+ describe '#execute' do
+ subject(:task) { convert_to_task.execute }
+
+ let(:convert_to_task) { described_class.new(exercise: exercise) }
+ let(:exercise) do
+ FactoryBot.create(:dummy,
+ instructions: 'instruction',
+ uuid: SecureRandom.uuid,
+ files: files + tests)
+ end
+ let(:files) { [] }
+ let(:tests) { [] }
+
+ it 'creates a task with all basic attributes' do
+ expect(task).to have_attributes(
+ title: exercise.title,
+ description: exercise.description,
+ internal_description: exercise.instructions,
+ # proglang: {
+ # name: exercise.execution_environment.language,
+ # version: exercise.execution_environment.version
+ # },
+ uuid: exercise.uuid,
+ language: described_class::DEFAULT_LANGUAGE,
+ # parent_uuid: exercise.clone_relations.first&.origin&.uuid,
+ files: [],
+ tests: [],
+ model_solutions: [],
+ import_checksum: exercise.import_checksum
+ )
+ end
+
+ context 'when exercise has a mainfile' do
+ let(:files) { [file] }
+ let(:file) { FactoryBot.build(:file) }
+
+ it 'creates a task-file with the correct attributes' do
+ expect(task.files.first).to have_attributes(
+ id: file.id,
+ content: file.content,
+ filename: file.name_with_extension,
+ used_by_grader: true,
+ usage_by_lms: 'edit',
+ visible: 'yes',
+ binary: false,
+ internal_description: 'main_file'
+ )
+ end
+ end
+
+ context 'when exercise has a regular file' do
+ let(:files) { [file] }
+ let(:file) { FactoryBot.build(:file, role: 'regular_file', hidden: hidden, read_only: read_only) }
+ let(:hidden) { true }
+ let(:read_only) { true }
+
+ it 'creates a task-file with the correct attributes' do
+ expect(task.files.first).to have_attributes(
+ id: file.id,
+ content: file.content,
+ filename: file.name_with_extension,
+ used_by_grader: true,
+ usage_by_lms: 'display',
+ visible: 'no',
+ binary: false,
+ internal_description: 'regular_file'
+ )
+ end
+
+ context 'when file is not hidden' do
+ let(:hidden) { false }
+
+ it 'creates a task-file with the correct attributes' do
+ expect(task.files.first).to have_attributes(visible: 'yes')
+ end
+ end
+
+ context 'when file is not read_only' do
+ let(:read_only) { false }
+
+ it 'creates a task-file with the correct attributes' do
+ expect(task.files.first).to have_attributes(usage_by_lms: 'edit')
+ end
+ end
+
+ context 'when file has an attachment' do
+ let(:file) { FactoryBot.build(:file, :image, role: 'regular_file') }
+
+ it 'creates a task-file with the correct attributes' do
+ expect(task.files.first).to have_attributes(
+ used_by_grader: false,
+ binary: true,
+ mimetype: 'image/png'
+ )
+ end
+ end
+ end
+
+ context 'when exercise has a file with role reference implementation' do
+ let(:files) { [file] }
+ let(:file) { FactoryBot.build(:file, role: 'reference_implementation') }
+
+ it 'creates a task with one model-solution' do
+ expect(task.model_solutions).to have(1).item
+ end
+
+ it 'creates a model-solution with one file' do
+ expect(task.model_solutions.first).to have_attributes(
+ id: "ms-#{file.id}",
+ files: have(1).item
+ )
+ end
+
+ it 'creates a model-solution with one file with correct attributes' do
+ expect(task.model_solutions.first.files.first).to have_attributes(
+ id: file.id,
+ content: file.content,
+ filename: file.name_with_extension,
+ used_by_grader: false,
+ usage_by_lms: 'display',
+ visible: 'delayed',
+ binary: false,
+ internal_description: 'reference_implementation'
+ )
+ end
+ end
+
+ context 'when exercise has multiple files with role reference implementation' do
+ let(:files) { FactoryBot.build_list(:file, 2, role: 'reference_implementation') }
+
+ it 'creates a task with two model-solutions' do
+ expect(task.model_solutions).to have(2).items
+ end
+ end
+
+ context 'when exercise has a test' do
+ let(:tests) { [test_file] }
+ let(:test_file) { FactoryBot.build(:test_file) }
+ # let(:file) { FactoryBot.build(:codeharbor_test_file) }
+
+ it 'creates a task with one test' do
+ expect(task.tests).to have(1).item
+ end
+
+ it 'creates a test with one file' do
+ expect(task.tests.first).to have_attributes(
+ id: test_file.id,
+ title: test_file.name,
+ files: have(1).item,
+ meta_data: {'feedback-message' => test_file.feedback_message}
+ )
+ end
+
+ it 'creates a test with one file with correct attributes' do
+ expect(task.tests.first.files.first).to have_attributes(
+ id: test_file.id,
+ content: test_file.content,
+ filename: test_file.name_with_extension,
+ used_by_grader: true,
+ visible: 'no',
+ binary: false,
+ internal_description: 'teacher_defined_test'
+ )
+ end
+
+ context 'when exercise_file is not hidden' do
+ let(:test_file) { FactoryBot.create(:test_file, hidden: false) }
+
+ it 'creates the test file with the correct attribute' do
+ expect(task.tests.first.files.first).to have_attributes(visible: 'yes')
+ end
+ end
+ end
+
+ context 'when exercise has multiple tests' do
+ let(:tests) { FactoryBot.build_list(:test_file, 2) }
+
+ it 'creates a task with two tests' do
+ expect(task.tests).to have(2).items
+ end
+ end
+ end
+end
diff --git a/spec/services/proforma_service/convert_task_to_exercise_spec.rb b/spec/services/proforma_service/convert_task_to_exercise_spec.rb
new file mode 100644
index 00000000..4f0a3b8c
--- /dev/null
+++ b/spec/services/proforma_service/convert_task_to_exercise_spec.rb
@@ -0,0 +1,377 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ProformaService::ConvertTaskToExercise do
+ describe '.new' do
+ subject(:convert_to_exercise_service) { described_class.new(task: task, user: user, exercise: exercise) }
+
+ let(:task) { Proforma::Task.new }
+ let(:user) { FactoryBot.build(:teacher) }
+ let(:exercise) { FactoryBot.build(:dummy) }
+
+ it 'assigns task' do
+ expect(convert_to_exercise_service.instance_variable_get(:@task)).to be task
+ end
+
+ it 'assigns user' do
+ expect(convert_to_exercise_service.instance_variable_get(:@user)).to be user
+ end
+
+ it 'assigns exercise' do
+ expect(convert_to_exercise_service.instance_variable_get(:@exercise)).to be exercise
+ end
+ end
+
+ describe '#execute' do
+ subject(:convert_to_exercise_service) { described_class.call(task: task, user: user, exercise: exercise) }
+
+ before { FactoryBot.create(:dot_txt) }
+
+ let(:task) do
+ Proforma::Task.new(
+ title: 'title',
+ description: 'description',
+ internal_description: 'internal_description',
+ proglang: {name: 'proglang-name', version: 'proglang-version'},
+ uuid: 'uuid',
+ parent_uuid: 'parent_uuid',
+ language: 'language',
+ model_solutions: model_solutions,
+ files: files,
+ tests: tests,
+ import_checksum: 'import_checksum',
+ checksum: 'checksum'
+ )
+ end
+ let(:user) { FactoryBot.create(:teacher) }
+ let(:files) { [] }
+ let(:tests) { [] }
+ let(:model_solutions) { [] }
+ let(:exercise) {}
+
+ it 'creates an exercise with the correct attributes' do
+ expect(convert_to_exercise_service).to have_attributes(
+ title: 'title',
+ description: 'description',
+ instructions: 'internal_description',
+ execution_environment: be_blank,
+ uuid: be_blank,
+ unpublished: true,
+ user: user,
+ files: be_empty
+ )
+ end
+
+ context 'when task has a file' do
+ let(:files) { [file] }
+ let(:file) do
+ Proforma::TaskFile.new(
+ id: 'id',
+ content: content,
+ filename: 'filename.txt',
+ used_by_grader: 'used_by_grader',
+ visible: 'yes',
+ usage_by_lms: usage_by_lms,
+ binary: binary,
+ internal_description: 'regular_file',
+ mimetype: mimetype
+ )
+ end
+ let(:usage_by_lms) { 'display' }
+ let(:mimetype) { 'mimetype' }
+ let(:binary) { false }
+ let(:content) { 'content' }
+
+ it 'creates an exercise with a file that has the correct attributes' do
+ expect(convert_to_exercise_service.files.first).to have_attributes(
+ content: 'content',
+ name: 'filename',
+ role: 'regular_file',
+ hidden: false,
+ read_only: true,
+ file_type: be_a(FileType).and(have_attributes(file_extension: '.txt'))
+ )
+ end
+
+ it 'creates a new Exercise on save' do
+ expect { convert_to_exercise_service.save! }.to change(Exercise, :count).by(1)
+ end
+
+ context 'when file is very large' do
+ let(:content) { 'test' * 10**5 }
+
+ it 'creates an exercise with a file that has the correct attributes' do
+ expect(convert_to_exercise_service.files.first).to have_attributes(content: content)
+ end
+ end
+
+ context 'when file is binary' do
+ let(:mimetype) { 'image/png' }
+ let(:binary) { true }
+
+ it 'creates an exercise with a file with attachment and the correct attributes' do
+ expect(convert_to_exercise_service.files.first.native_file).to be_present
+ end
+ end
+
+ context 'when usage_by_lms is edit' do
+ let(:usage_by_lms) { 'edit' }
+
+ it 'creates an exercise with a file with correct attributes' do
+ expect(convert_to_exercise_service.files.first).to have_attributes(read_only: false)
+ end
+ end
+
+ context 'when file is a model-solution-placeholder (needed by proforma until issue #5 is resolved)' do
+ let(:file) { Proforma::TaskFile.new(id: 'ms-placeholder-file') }
+
+ it 'leaves exercise_files empty' do
+ expect(convert_to_exercise_service.files).to be_empty
+ end
+ end
+ end
+
+ context 'when task has a model-solution' do
+ let(:model_solutions) { [model_solution] }
+ let(:model_solution) do
+ Proforma::ModelSolution.new(
+ id: 'ms-id',
+ files: ms_files
+ )
+ end
+ let(:ms_files) { [ms_file] }
+ let(:ms_file) do
+ Proforma::TaskFile.new(
+ id: 'ms-file',
+ content: 'content',
+ filename: 'filename.txt',
+ used_by_grader: 'used_by_grader',
+ visible: 'yes',
+ usage_by_lms: 'display',
+ binary: false,
+ internal_description: 'reference_implementation'
+ )
+ end
+
+ it 'creates an exercise with a file with role Reference Implementation' do
+ expect(convert_to_exercise_service.files.first).to have_attributes(
+ role: 'reference_implementation'
+ )
+ end
+
+ context 'when task has two model-solutions' do
+ let(:model_solutions) { [model_solution, model_solution2] }
+ let(:model_solution2) do
+ Proforma::ModelSolution.new(
+ id: 'ms-id-2',
+ files: ms_files_2
+ )
+ end
+ let(:ms_files_2) { [ms_file_2] }
+ let(:ms_file_2) do
+ Proforma::TaskFile.new(
+ id: 'ms-file-2',
+ content: 'content',
+ filename: 'filename.txt',
+ used_by_grader: 'used_by_grader',
+ visible: 'yes',
+ usage_by_lms: 'display',
+ binary: false,
+ internal_description: 'reference_implementation'
+ )
+ end
+
+ it 'creates an exercise with two files with role Reference Implementation' do
+ expect(convert_to_exercise_service.files).to have(2).items.and(all(have_attributes(role: 'reference_implementation')))
+ end
+ end
+ end
+
+ context 'when task has a test' do
+ let(:tests) { [test] }
+ let(:test) do
+ Proforma::Test.new(
+ id: 'test-id',
+ title: 'title',
+ description: 'description',
+ internal_description: 'internal_description',
+ test_type: 'test_type',
+ files: test_files,
+ meta_data: {
+ 'feedback-message' => 'feedback-message',
+ 'testing-framework' => 'testing-framework',
+ 'testing-framework-version' => 'testing-framework-version'
+ }
+ )
+ end
+
+ let(:test_files) { [test_file] }
+ let(:test_file) do
+ Proforma::TaskFile.new(
+ id: 'test_file_id',
+ content: 'testfile-content',
+ filename: 'testfile.txt',
+ used_by_grader: 'yes',
+ visible: 'no',
+ usage_by_lms: 'display',
+ binary: false,
+ internal_description: 'teacher_defined_test'
+ )
+ end
+
+ it 'creates an exercise with a test' do
+ expect(convert_to_exercise_service.files.select { |file| file.role == 'teacher_defined_test' }).to have(1).item
+ end
+
+ it 'creates an exercise with a test with correct attributes' do
+ expect(convert_to_exercise_service.files.select { |file| file.role == 'teacher_defined_test' }.first).to have_attributes(
+ feedback_message: 'feedback-message',
+ content: 'testfile-content',
+ name: 'testfile',
+ role: 'teacher_defined_test',
+ hidden: true,
+ read_only: true,
+ file_type: be_a(FileType).and(have_attributes(file_extension: '.txt'))
+ )
+ end
+
+ context 'when task has multiple tests' do
+ let(:tests) { [test, test2] }
+ let(:test2) do
+ Proforma::Test.new(
+ files: test_files2,
+ meta_data: {
+ 'feedback-message' => 'feedback-message',
+ 'testing-framework' => 'testing-framework',
+ 'testing-framework-version' => 'testing-framework-version'
+ }
+ )
+ end
+ let(:test_files2) { [test_file2] }
+ let(:test_file2) do
+ Proforma::TaskFile.new(
+ id: 'test_file_id2',
+ content: 'testfile-content',
+ filename: 'testfile.txt',
+ used_by_grader: 'yes',
+ visible: 'no',
+ usage_by_lms: 'display',
+ binary: false,
+ internal_description: 'teacher_defined_test'
+ )
+ end
+
+ it 'creates an exercise with two test' do
+ expect(convert_to_exercise_service.files.select { |file| file.role == 'teacher_defined_test' }).to have(2).items
+ end
+ end
+ end
+
+ context 'when exercise is set' do
+ let(:exercise) do
+ FactoryBot.create(
+ :files,
+ title: 'exercise-title',
+ description: 'exercise-description',
+ instructions: 'exercise-instruction'
+ )
+ end
+
+ before { exercise.reload }
+
+ it 'assigns all values to given exercise' do
+ convert_to_exercise_service.save
+ expect(exercise.reload).to have_attributes(
+ id: exercise.id,
+ title: task.title,
+ description: task.description,
+ instructions: task.internal_description,
+ execution_environment: exercise.execution_environment,
+ uuid: exercise.uuid,
+ user: exercise.user,
+ files: be_empty
+ )
+ end
+
+ it 'does not create a new Exercise on save' do
+ expect { convert_to_exercise_service.save }.not_to change(Exercise, :count)
+ end
+
+ context 'with file, model solution and test' do
+ let(:files) { [file] }
+ let(:file) do
+ Proforma::TaskFile.new(
+ id: 'id',
+ content: 'content',
+ filename: 'filename.txt',
+ used_by_grader: 'used_by_grader',
+ visible: 'yes',
+ usage_by_lms: 'display',
+ binary: false,
+ internal_description: 'regular_file'
+ )
+ end
+ let(:tests) { [test] }
+ let(:test) do
+ Proforma::Test.new(
+ id: 'test-id',
+ title: 'title',
+ description: 'description',
+ internal_description: 'regular_file',
+ test_type: 'test_type',
+ files: test_files,
+ meta_data: {
+ 'feedback-message' => 'feedback-message',
+ 'testing-framework' => 'testing-framework',
+ 'testing-framework-version' => 'testing-framework-version'
+ }
+ )
+ end
+ let(:test_files) { [test_file] }
+ let(:test_file) do
+ Proforma::TaskFile.new(
+ id: 'test_file_id',
+ content: 'testfile-content',
+ filename: 'testfile.txt',
+ used_by_grader: 'yes',
+ visible: 'no',
+ usage_by_lms: 'display',
+ binary: false,
+ internal_description: 'teacher_defined_test'
+ )
+ end
+ let(:model_solutions) { [model_solution] }
+ let(:model_solution) do
+ Proforma::ModelSolution.new(
+ id: 'ms-id',
+ files: ms_files
+ )
+ end
+ let(:ms_files) { [ms_file] }
+ let(:ms_file) do
+ Proforma::TaskFile.new(
+ id: 'ms-file',
+ content: 'ms-content',
+ filename: 'filename.txt',
+ used_by_grader: 'used_by_grader',
+ visible: 'yes',
+ usage_by_lms: 'display',
+ binary: false,
+ internal_description: 'reference_implementation'
+ )
+ end
+
+ it 'assigns all values to given exercise' do
+ expect(convert_to_exercise_service).to have_attributes(
+ id: exercise.id,
+ files: have(3).items
+ .and(include(have_attributes(content: 'ms-content', role: 'reference_implementation')))
+ .and(include(have_attributes(content: 'content', role: 'regular_file')))
+ .and(include(have_attributes(content: 'testfile-content', role: 'teacher_defined_test')))
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/proforma_service/export_task_spec.rb b/spec/services/proforma_service/export_task_spec.rb
new file mode 100644
index 00000000..157094de
--- /dev/null
+++ b/spec/services/proforma_service/export_task_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ProformaService::ExportTask do
+ describe '.new' do
+ subject(:export_task) { described_class.new(exercise: exercise) }
+
+ let(:exercise) { FactoryBot.build(:dummy) }
+
+ it 'assigns exercise' do
+ expect(export_task.instance_variable_get(:@exercise)).to be exercise
+ end
+
+ context 'without exercise' do
+ subject(:export_task) { described_class.new }
+
+ it 'assigns exercise' do
+ expect(export_task.instance_variable_get(:@exercise)).to be nil
+ end
+ end
+ end
+
+ describe '#execute' do
+ subject(:export_task) { described_class.call(exercise: exercise) }
+
+ let(:task) { Proforma::Task.new }
+ let(:exercise) { FactoryBot.build(:dummy) }
+ let(:exporter) { instance_double('Proforma::Exporter', perform: 'zip') }
+
+ before do
+ allow(ProformaService::ConvertExerciseToTask).to receive(:call).with(exercise: exercise).and_return(task)
+ allow(Proforma::Exporter).to receive(:new).with(task).and_return(exporter)
+ end
+
+ it do
+ export_task
+ expect(exporter).to have_received(:perform)
+ end
+ end
+end
diff --git a/spec/services/proforma_service/import_spec.rb b/spec/services/proforma_service/import_spec.rb
new file mode 100644
index 00000000..118284f1
--- /dev/null
+++ b/spec/services/proforma_service/import_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ProformaService::Import do
+ describe '.new' do
+ subject(:import_service) { described_class.new(zip: zip, user: user) }
+
+ let(:zip) { Tempfile.new('proforma_test_zip_file') }
+ let(:user) { FactoryBot.build(:teacher) }
+
+ it 'assigns zip' do
+ expect(import_service.instance_variable_get(:@zip)).to be zip
+ end
+
+ it 'assigns user' do
+ expect(import_service.instance_variable_get(:@user)).to be user
+ end
+ end
+
+ describe '#execute' do
+ subject(:import_service) { described_class.call(zip: zip_file, user: import_user) }
+
+ let(:user) { FactoryBot.create(:teacher) }
+ let(:import_user) { user }
+ let(:zip_file) { Tempfile.new('proforma_test_zip_file') }
+ let(:exercise) do
+ FactoryBot.create(:dummy,
+ instructions: 'instruction',
+ execution_environment: execution_environment,
+ files: files + tests,
+ uuid: uuid,
+ user: user)
+ end
+
+ let(:uuid) {}
+ let(:execution_environment) { FactoryBot.build(:java) }
+ let(:files) { [] }
+ let(:tests) { [] }
+ let(:exporter) { ProformaService::ExportTask.call(exercise: exercise.reload).string }
+
+ before do
+ zip_file.write(exporter)
+ zip_file.rewind
+ end
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+
+ it 'sets the correct user as owner of the exercise' do
+ expect(import_service.user).to be user
+ end
+
+ it 'sets the uuid' do
+ expect(import_service.uuid).not_to be_blank
+ end
+
+ context 'when no exercise exists' do
+ before { exercise.destroy }
+
+ it { is_expected.to be_valid }
+
+ it 'sets the correct user as owner of the exercise' do
+ expect(import_service.user).to be user
+ end
+
+ it 'sets the uuid' do
+ expect(import_service.uuid).not_to be_blank
+ end
+
+ context 'when task has a uuid' do
+ let(:uuid) { SecureRandom.uuid }
+
+ it 'sets the uuid' do
+ expect(import_service.uuid).to eql uuid
+ end
+ end
+ end
+
+ context 'when exercise has a mainfile' do
+ let(:files) { [file] }
+ let(:file) { FactoryBot.build(:file) }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+
+ context 'when the mainfile is very large' do
+ let(:file) { FactoryBot.build(:file, content: 'test' * 10**5) }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+ end
+ end
+
+ context 'when exercise has a regular file' do
+ let(:files) { [file] }
+ let(:file) { FactoryBot.build(:file, role: 'regular_file') }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+
+ context 'when file has an attachment' do
+ let(:file) { FactoryBot.build(:file, :image, role: 'regular_file') }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+ end
+ end
+
+ context 'when exercise has a file with role reference implementation' do
+ let(:files) { [file] }
+ let(:file) { FactoryBot.build(:file, role: 'reference_implementation', read_only: true) }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+ end
+
+ context 'when exercise has multiple files with role reference implementation' do
+ let(:files) { FactoryBot.build_list(:file, 2, role: 'reference_implementation', read_only: true) }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+ end
+
+ context 'when exercise has a test' do
+ let(:tests) { [test] }
+ let(:test) { FactoryBot.build(:test_file) }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+ end
+
+ context 'when exercise has multiple tests' do
+ let(:tests) { FactoryBot.build_list(:test_file, 2) }
+
+ it { is_expected.to be_an_equal_exercise_as exercise }
+ end
+
+ # context 'when zip contains multiple tasks' do
+ # let(:exporter) { ProformaService::ExportTasks.call(exercises: [exercise, exercise2]).string }
+
+ # let(:exercise2) do
+ # FactoryBot.create(:dummy,
+ # instruction: 'instruction2',
+ # execution_environment: execution_environment,
+ # exercise_files: [],
+ # tests: [],
+ # user: user)
+ # end
+
+ # it 'imports the exercises from zip containing multiple zips' do
+ # expect(import_service).to all be_an(Exercise)
+ # end
+
+ # it 'imports the zip exactly how they were exported' do
+ # expect(import_service).to all be_an_equal_exercise_as(exercise).or be_an_equal_exercise_as(exercise2)
+ # end
+
+ # context 'when a exercise has files and tests' do
+ # let(:files) { [FactoryBot.build(:file), FactoryBot.build(:file, role: 'regular_file')] }
+ # let(:tests) { FactoryBot.build_list(:codeharbor_test, 2) }
+
+ # it 'imports the zip exactly how the were exported' do
+ # expect(import_service).to all be_an_equal_exercise_as(exercise).or be_an_equal_exercise_as(exercise2)
+ # end
+ # end
+ # end
+
+ context 'when task in zip has a different uuid' do
+ let(:uuid) { SecureRandom.uuid }
+ let(:new_uuid) { SecureRandom.uuid }
+
+ before do
+ exercise.update(uuid: new_uuid)
+ end
+
+ it 'creates a new Exercise' do
+ expect(import_service.id).not_to be exercise.id
+ end
+ end
+
+ context 'when task in zip has the same uuid and nothing has changed' do
+ let(:uuid) { SecureRandom.uuid }
+
+ it 'updates the old Exercise' do
+ expect(import_service.id).to be exercise.id
+ end
+
+ context 'when another user imports the exercise' do
+ let(:import_user) { FactoryBot.create(:teacher) }
+
+ it 'creates a new Exercise' do
+ expect(import_service.id).not_to be exercise.id
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/expectations/equal_exercise.rb b/spec/support/expectations/equal_exercise.rb
new file mode 100644
index 00000000..df21ec3d
--- /dev/null
+++ b/spec/support/expectations/equal_exercise.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rspec/expectations'
+
+RSpec::Matchers.define :be_an_equal_exercise_as do |exercise|
+ match do |actual|
+ equal?(actual, exercise)
+ end
+ failure_message do |actual|
+ "#{actual.inspect} is not equal to \n#{exercise.inspect}. \nLast checked attribute: #{@last_checked}"
+ end
+
+ def equal?(object, other)
+ return false unless object.class == other.class
+ return attributes_equal?(object, other) if object.is_a?(ApplicationRecord)
+ return array_equal?(object, other) if object.is_a?(Array) || object.is_a?(ActiveRecord::Associations::CollectionProxy)
+
+ object == other
+ end
+
+ def attributes_equal?(object, other)
+ other_attributes = attributes_and_associations(other)
+ attributes_and_associations(object).each do |k, v|
+ @last_checked = "#{k}: \n\"#{v}\" vs \n\"#{other_attributes[k]}\""
+ return false unless equal?(other_attributes[k], v)
+ end
+ true
+ end
+
+ def array_equal?(object, other)
+ return true if object == other # for []
+ return false if object.length != other.length
+
+ object.to_a.product(other.to_a).map { |k, v| equal?(k, v) }.any?
+ end
+
+ def attributes_and_associations(object)
+ object.attributes.dup.tap do |attributes|
+ attributes[:files] = object.files if defined? object.files
+ end.except('id', 'created_at', 'updated_at', 'exercise_id', 'uuid', 'import_checksum')
+ end
+end