specs for services

This commit is contained in:
Karol
2019-12-09 20:35:49 +01:00
parent c89ee6c102
commit 46e7853465
16 changed files with 1029 additions and 13 deletions

View File

@ -72,6 +72,7 @@ group :test do
gem 'database_cleaner' gem 'database_cleaner'
gem 'nyan-cat-formatter' gem 'nyan-cat-formatter'
gem 'rspec-autotest' gem 'rspec-autotest'
gem 'rspec-collection_matchers'
gem 'rspec-rails' gem 'rspec-rails'
gem 'shoulda-matchers' gem 'shoulda-matchers'
gem 'simplecov', require: false gem 'simplecov', require: false

View File

@ -318,6 +318,8 @@ GEM
rspec-mocks (~> 3.9.0) rspec-mocks (~> 3.9.0)
rspec-autotest (1.0.2) rspec-autotest (1.0.2)
rspec-core (>= 2.99.0.beta1, < 4.0.0) 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-core (3.9.0)
rspec-support (~> 3.9.0) rspec-support (~> 3.9.0)
rspec-expectations (3.9.0) rspec-expectations (3.9.0)
@ -486,6 +488,7 @@ DEPENDENCIES
ransack ransack
rest-client rest-client
rspec-autotest rspec-autotest
rspec-collection_matchers
rspec-rails rspec-rails
rubocop rubocop
rubocop-rspec rubocop-rspec

View File

@ -16,8 +16,8 @@ module ExerciseService
response_hash = JSON.parse(response.body, symbolize_names: true) response_hash = JSON.parse(response.body, symbolize_names: true)
{error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right)) {error: false}.merge(response_hash.slice(:message, :exercise_found, :update_right))
rescue Faraday::Error => e rescue Faraday::Error, JSON::ParserError
{error: true, message: I18n.t('exercises.export_codeharbor.error', message: e.message)} {error: true, message: I18n.t('exercises.export_codeharbor.error')}
end end
private private

View File

@ -17,9 +17,9 @@ module ExerciseService
request.body = body request.body = body
end end
return response.success? ? nil : response.body response.success? ? nil : response.body
rescue StandardError => e rescue StandardError => e
return e.message e.message
end end
end end

View File

@ -22,11 +22,9 @@ module ProformaService
title: @exercise.title, title: @exercise.title,
description: @exercise.description, description: @exercise.description,
internal_description: @exercise.instructions, internal_description: @exercise.instructions,
# proglang: proglang,
files: task_files, files: task_files,
tests: tests, tests: tests,
uuid: uuid, uuid: uuid,
# parent_uuid: parent_uuid,
language: DEFAULT_LANGUAGE, language: DEFAULT_LANGUAGE,
model_solutions: model_solutions, model_solutions: model_solutions,
import_checksum: @exercise.import_checksum import_checksum: @exercise.import_checksum
@ -66,8 +64,6 @@ module ProformaService
files: test_file(file), files: test_file(file),
meta_data: { meta_data: {
'feedback-message' => file.feedback_message 'feedback-message' => file.feedback_message
# 'testing-framework' => test.testing_framework&.name,
# 'testing-framework-version' => test.testing_framework&.version
}.compact }.compact
) )
end end

View File

@ -22,7 +22,6 @@ module ProformaService
description: @task.description, description: @task.description,
instructions: @task.internal_description, instructions: @task.internal_description,
files: files, files: files,
uuid: @task.uuid,
import_checksum: @task.checksum import_checksum: @task.checksum
) )
end end
@ -56,10 +55,10 @@ module ProformaService
name: File.basename(file.filename, '.*'), name: File.basename(file.filename, '.*'),
read_only: file.usage_by_lms != 'edit', read_only: file.usage_by_lms != 'edit',
role: file.internal_description, role: file.internal_description,
path: File.dirname(file.filename) path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename)
}.tap do |params| }.tap do |params|
if file.binary 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 else
params[:content] = file.content params[:content] = file.content
end end

View File

@ -12,7 +12,7 @@ module ProformaService
importer = Proforma::Importer.new(@zip) importer = Proforma::Importer.new(@zip)
@task = importer.perform @task = importer.perform
exercise = Exercise.find_by(uuid: @task.uuid) exercise = base_exercise
exercise_files = exercise&.files&.to_a exercise_files = exercise&.files&.to_a
exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise: exercise) exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise: exercise)
@ -26,6 +26,17 @@ module ProformaService
private 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 def import_multi
Zip::File.open(@zip.path) do |zip_file| Zip::File.open(@zip.path) do |zip_file|
zip_files = zip_file.filter { |entry| entry.name.match?(/\.zip$/) } zip_files = zip_file.filter { |entry| entry.name.match?(/\.zip$/) }

View File

@ -329,7 +329,7 @@ en:
dialogtitle: Export to Codeharbor dialogtitle: Export to Codeharbor
successfully_exported: 'Exercise has been successfully exported.<br>ID: %{id}<br>Title: %{title}' 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}' 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. checking_codeharbor: Checking if the exercise exists on Codeharbor.
buttons: buttons:
retry: Retry retry: Retry

View File

@ -10,6 +10,24 @@ module CodeOcean
name { SecureRandom.hex } name { SecureRandom.hex }
read_only { false } read_only { false }
role { 'main_file' } 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 end
end end

View 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

View 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

View 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

View 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

View 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

View 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

View 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