specs for services
This commit is contained in:
1
Gemfile
1
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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$/) }
|
||||
|
@ -329,7 +329,7 @@ en:
|
||||
dialogtitle: Export to Codeharbor
|
||||
successfully_exported: 'Exercise has been successfully exported.<br>ID: %{id}<br>Title: %{title}'
|
||||
export_failed: 'Export has failed.<br>ID: %{id}<br>Title: %{title}<br><br>Error: %{error}'
|
||||
error: 'An error occurred while contacting Codeharbor<br>Error: %{message}'
|
||||
error: 'An error occurred while contacting Codeharbor'
|
||||
checking_codeharbor: Checking if the exercise exists on Codeharbor.
|
||||
buttons:
|
||||
retry: Retry
|
||||
|
@ -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
|
||||
|
76
spec/services/exercise_service/check_external_spec.rb
Normal file
76
spec/services/exercise_service/check_external_spec.rb
Normal file
@ -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
|
64
spec/services/exercise_service/push_external_spec.rb
Normal file
64
spec/services/exercise_service/push_external_spec.rb
Normal file
@ -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
|
198
spec/services/proforma_service/convert_exercise_to_task_spec.rb
Normal file
198
spec/services/proforma_service/convert_exercise_to_task_spec.rb
Normal file
@ -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
|
377
spec/services/proforma_service/convert_task_to_exercise_spec.rb
Normal file
377
spec/services/proforma_service/convert_task_to_exercise_spec.rb
Normal file
@ -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
|
41
spec/services/proforma_service/export_task_spec.rb
Normal file
41
spec/services/proforma_service/export_task_spec.rb
Normal file
@ -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
|
190
spec/services/proforma_service/import_spec.rb
Normal file
190
spec/services/proforma_service/import_spec.rb
Normal file
@ -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
|
42
spec/support/expectations/equal_exercise.rb
Normal file
42
spec/support/expectations/equal_exercise.rb
Normal file
@ -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
|
Reference in New Issue
Block a user