diff --git a/.gitignore b/.gitignore index 600b1d718e0..a134e75feb4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,12 @@ # Ignore user-specific VSCode project files .vscode +# Ignore credentials files which may contain sensitive information +/config/credentials/*.key +/config/credentials/*.yml.enc +!/config/credentials/test.key +!/config/credentials/test.yml.enc + # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal diff --git a/Gemfile b/Gemfile index c6eb5b2d2d4..996eca97736 100644 --- a/Gemfile +++ b/Gemfile @@ -199,7 +199,7 @@ gem 'filename' # Required by CarrierWave, for image resizing gem 'mini_magick' # Library for reading and writing zip files -gem 'rubyzip', require: 'zip' +gem 'rubyzip', '~> 3.0', require: 'zip' # Manipulating XML files, needed for programming evaluation test report parsing. gem 'nokogiri', '>= 1.18.8' diff --git a/Gemfile.lock b/Gemfile.lock index 70c61aee7c6..284afe31de7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -216,9 +216,9 @@ GEM docker-api (2.4.0) excon (>= 0.64.0) multi_json - docx (0.9.1) + docx (0.10.0) nokogiri (~> 1.13, >= 1.13.0) - rubyzip (~> 2.0) + rubyzip (>= 2.0, < 4) domain_name (0.6.20240107) dotenv (3.1.7) dotenv-rails (3.1.7) @@ -608,16 +608,16 @@ GEM ruby-vips (2.2.2) ffi (~> 1.12) logger - rubyzip (2.4.1) + rubyzip (3.2.2) sanitize (7.0.0) crass (~> 1.0.2) nokogiri (>= 1.16.8) securerandom (0.4.1) - selenium-webdriver (4.22.0) + selenium-webdriver (4.43.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) should_not (1.1.0) shoulda-matchers (7.0.1) @@ -779,7 +779,7 @@ DEPENDENCIES rubocop-rails ruby-oembed ruby-openai - rubyzip + rubyzip (~> 3.0) rwordnet! sanitize (>= 4.6.3) settings_on_rails! diff --git a/app/services/course/assessment/submission/base_zip_download_service.rb b/app/services/course/assessment/submission/base_zip_download_service.rb index a409d419b38..e5bce21ed70 100644 --- a/app/services/course/assessment/submission/base_zip_download_service.rb +++ b/app/services/course/assessment/submission/base_zip_download_service.rb @@ -42,7 +42,7 @@ def zip_file_path # # @return [String] The path to the zip file. def zip_base_dir - Zip::File.open(zip_file_path, Zip::File::CREATE) do |zip_file| + Zip::File.open(zip_file_path, create: true) do |zip_file| Dir["#{@base_dir}/**/**"].each do |file| zip_file.add(file.sub(File.join("#{@base_dir}/"), ''), file) end diff --git a/app/services/course/assessment/submission/ssid_plagiarism_service.rb b/app/services/course/assessment/submission/ssid_plagiarism_service.rb index 7b3a3f2ee50..8a33a9f2326 100644 --- a/app/services/course/assessment/submission/ssid_plagiarism_service.rb +++ b/app/services/course/assessment/submission/ssid_plagiarism_service.rb @@ -32,7 +32,7 @@ def fetch_plagiarism_result(limit, offset) end def download_submission_pair_result(submission_pair_id) - ssid_api_service = SsidAsyncApiService.new( + ssid_api_service = SsidApiService.new( "submission-pairs/#{submission_pair_id}/report", {} ) response_status, response_body = ssid_api_service.get @@ -52,7 +52,7 @@ def share_assessment_result end def fetch_plagiarism_check_result - ssid_api_service = SsidAsyncApiService.new("folders/#{@main_assessment.ssid_folder_id}/plagiarism-checks", {}) + ssid_api_service = SsidApiService.new("folders/#{@main_assessment.ssid_folder_id}/plagiarism-checks", {}) response_status, response_body = ssid_api_service.get raise SsidError, { status: response_status, body: response_body } unless response_status == 200 @@ -67,13 +67,28 @@ def create_ssid_folders end end + def prepare_ssid_presigned_request(assessment) + ssid_api_service = SsidApiService.new("folders/#{assessment.ssid_folder_id}/uploads", {}) + response_status, response_body = ssid_api_service.post + raise SsidError, { status: response_status, body: response_body } unless response_status == 200 + + upload_url = response_body['payload']['data']['url'] + unless SsidApiService.upload_whitelist.match?(upload_url) + raise SsidError, { status: 500, body: 'Invalid presigned URL' } + end + + [upload_url, response_body['payload']['data']['fields']] + end + def run_upload_answers @linked_assessments.each do |assessment| + upload_url, upload_fields = prepare_ssid_presigned_request(assessment) service = Course::Assessment::Submission::SsidZipDownloadService.new(assessment) zip_files = service.download_and_zip - ssid_api_service = SsidAsyncApiService.new("folders/#{assessment.ssid_folder_id}/submissions", {}) + ssid_api_service = SsidApiService.new('', {}, upload_url) zip_files.each do |zip_file| - response_status, response_body = ssid_api_service.post_multipart(zip_file) + form_data = upload_fields.merge('file' => Faraday::Multipart::FilePart.new(zip_file, 'application/zip')) + response_status, response_body = ssid_api_service.post_multipart(form_data) raise SsidError, { status: response_status, body: response_body } unless response_status == 204 end ensure @@ -82,7 +97,7 @@ def run_upload_answers end def send_plagiarism_check_request - ssid_api_service = SsidAsyncApiService.new("folders/#{@main_assessment.ssid_folder_id}/plagiarism-checks", { + ssid_api_service = SsidApiService.new("folders/#{@main_assessment.ssid_folder_id}/plagiarism-checks", { comparedFolderIds: @linked_assessments.pluck(:ssid_folder_id) }) response_status, response_body = ssid_api_service.post @@ -94,7 +109,7 @@ def ssid_submission_to_submission_id(ssid_submission) end def fetch_ssid_submission_pair_data(limit, offset) - ssid_api_service = SsidAsyncApiService.new( + ssid_api_service = SsidApiService.new( "folders/#{@main_assessment.ssid_folder_id}/plagiarism-checks/latest/submission-pairs", { limit: limit, offset: offset } ) @@ -105,7 +120,7 @@ def fetch_ssid_submission_pair_data(limit, offset) end def create_ssid_shared_resource_link(resource_type, resource_id) - ssid_api_service = SsidAsyncApiService.new('shared-resources', { + ssid_api_service = SsidApiService.new('shared-resources', { resourceType: resource_type, resourceId: resource_id }) diff --git a/app/services/course/assessment/submission/ssid_zip_download_service.rb b/app/services/course/assessment/submission/ssid_zip_download_service.rb index bd52d5f0843..1c40c2713ef 100644 --- a/app/services/course/assessment/submission/ssid_zip_download_service.rb +++ b/app/services/course/assessment/submission/ssid_zip_download_service.rb @@ -137,7 +137,7 @@ def zip_base_dir answer_partitions = partition_answers_by_size(answer_size_hash) @zip_files = answer_partitions.map.with_index do |partition, index| output_file = "#{@base_dir}_#{index}.zip" - Zip::File.open(output_file, Zip::File::CREATE) do |zip_file| + Zip::File.open(output_file, create: true) do |zip_file| partition.each do |answer_dir| Dir["#{answer_dir}/**/**"].each do |file| zip_file.add(file.sub(File.join("#{@base_dir}/"), ''), file) diff --git a/app/services/course/material/zip_download_service.rb b/app/services/course/material/zip_download_service.rb index 1918fc2a77b..3fa4f5fa98d 100644 --- a/app/services/course/material/zip_download_service.rb +++ b/app/services/course/material/zip_download_service.rb @@ -39,7 +39,7 @@ def download_to_base_dir # # @return [String] The path to the zip file. def zip_base_dir - Zip::File.open(zip_file_path, Zip::File::CREATE) do |zip_file| + Zip::File.open(zip_file_path, create: true) do |zip_file| Dir["#{@base_dir}/**/**"].each do |file| zip_file.add(file.sub(File.join("#{@base_dir}/"), ''), file) end diff --git a/app/services/course/ssid_folder_service.rb b/app/services/course/ssid_folder_service.rb index 6ca20587ef8..7a8dac86a1e 100644 --- a/app/services/course/ssid_folder_service.rb +++ b/app/services/course/ssid_folder_service.rb @@ -5,7 +5,7 @@ def initialize(folder_name, parent_folder_id = nil) end def run_create_ssid_folder_service - ssid_api_service = SsidAsyncApiService.new('folders', @folder_object) + ssid_api_service = SsidApiService.new('folders', @folder_object) response_status, response_body = ssid_api_service.post # If id is lost in our DB somehow, we can recover it if SSID returns a 409 diff --git a/app/services/ssid_async_api_service.rb b/app/services/ssid_api_service.rb similarity index 64% rename from app/services/ssid_async_api_service.rb rename to app/services/ssid_api_service.rb index 7c735aa44c1..ad9dc1b3fcc 100644 --- a/app/services/ssid_async_api_service.rb +++ b/app/services/ssid_api_service.rb @@ -1,13 +1,20 @@ # frozen_string_literal: true -class SsidAsyncApiService - def config - ENV.fetch('SSID_URL') +class SsidApiService + def self.api_url + Rails.application.credentials.ssid.url end - def initialize(api_namespace, payload) + # Validate that the URL is in the whitelist before allowing uploads to it. + # This is to prevent leaking data in case the SSID response is intercepted. + def self.upload_whitelist + Regexp.new(Rails.application.credentials.ssid.upload_whitelist_pattern) + end + + def initialize(api_namespace, payload, url = nil) @api_namespace = api_namespace @payload = payload + @url = url || self.class.api_url end def post @@ -20,8 +27,7 @@ def post [500, nil] end - def post_multipart(file_path) - form_data = { 'file' => Faraday::Multipart::FilePart.new(file_path, 'application/zip') } + def post_multipart(form_data) response = connection.post(@api_namespace) do |req| req.body = form_data end @@ -42,8 +48,10 @@ def get private def connection - @connection ||= Faraday.new(url: config) do |builder| - builder.request :authorization, 'Bearer', -> { ENV.fetch('SSID_API_KEY', nil) } + @connection ||= Faraday.new(url: @url) do |builder| + if @url == self.class.api_url + builder.request :authorization, 'Bearer', -> { Rails.application.credentials.ssid.api_key } + end builder.request :multipart end end diff --git a/config/credentials/README.md b/config/credentials/README.md index 900c0961c4c..f6dd29a9094 100644 --- a/config/credentials/README.md +++ b/config/credentials/README.md @@ -11,7 +11,7 @@ Each environment has two files: - `.key`, the decryption key that allows reading the `.yml.enc` file. **Keys from deployed environments (staging and production) must NEVER BE COMMITTED.** Once leaked, all secrets in the environment will be compromised. - `.yml.enc`, an encrypted YAML file containing sensitive environment data (e.g. API keys). While theoretically safe to commit because the data is unreadable without the decryption key, we currently do not commit files containing real sensitive data as an additional safety measure. -The `development` and `test` key files are checked into this repository, so local development works out of the box. The sample credentials use the same structure as staging/production but with redacted values, so external API integrations (e.g. Codaveri, AWS) will not function. +The `test` key files are checked into this repository. The sample credentials use the same structure as other environments but with redacted values, so external API integrations (e.g. Codaveri, AWS) will not function. If you are a Coursemology team member who needs working credentials, contact current staff for the appropriate credentials file. diff --git a/config/credentials/development.key b/config/credentials/development.key deleted file mode 100644 index 9a9a1030d0e..00000000000 --- a/config/credentials/development.key +++ /dev/null @@ -1 +0,0 @@ -921054b69df2e11efe48fcc33e5e6c3b \ No newline at end of file diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc deleted file mode 100644 index 41c2502d1c5..00000000000 --- a/config/credentials/development.yml.enc +++ /dev/null @@ -1 +0,0 @@ -ggx1zYTbsx1qW4tVbDVtzWhUaBWhEzCQupNuLn566FFhyKb88iHP3NVoRZ/ikLbbikowkQ74yFLrw9vRg2A3XCvEbwkU2skv86YpcYNZJAczeE2Iwc+WOGDgAtMqoYGHdmi/nXoJcTGrD3H03IObGKXU89/YZP6iARJxvofjSxieOCoD24DwlzGm3/SEROrdm7qwIhYVXYWa8jQOQc2GbI1mCQX7InRxKTjz+0YDjDb6AVwQBF1084JJuqxRlxavWrKLTl0OZPR/dHhCxS2pH8Kt27QjBcF7nYCKF+dTLsGeExF4J7DUgsi6i0ZKeB0f7XaDHTXMF46DkzHW7ggCdjDRzTOrQ1aDm1jpK2MsMk4OFnW0s7gQ4AZT+0+1b6Z6zCtWRsROlGx+EPv2b8adbI6vvHJMnMe7ycWkXlIbAcMhIZKwT1cqPvMmOls1Pw36wV0ez0CB8r0E8qLkKb4z4q7xoJLXfx2bLtBskTcDHbdPn/kUCXh7DsZF3Brvqym1+JeCpjNv1dKBOW1swwcTektvdkSusOH3EQVII3H5Qim7D/Qq15i94sPG3rkQioLl3BkOJJYnqEsd/z2W9zsH8jnY5o12jUOa+CvsJKVRA1gRWfrS7tzieYN+XkvCa1QPr5jDzC+r9e77eRH7b9FXZsQ/UhG3WchWZWPIlYCq+BK+BJFLZDxtkKUJvoIanjdBybw8xTJdPgQGbE8qb2gw9otCVN9LW/UttYlUChk=--GbjPmjAra2SfumvH--8U9MPKnGuIO5ow1+bZvChw== \ No newline at end of file diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc index 269d0894a34..b6fe35a163b 100644 --- a/config/credentials/test.yml.enc +++ b/config/credentials/test.yml.enc @@ -1 +1 @@ -22UauTnIrniRSCH/x4uVoj8HBcZJHb1ET04fxlhsYVsXPZ5vXG5mr+V5D5+F5iKv4pHNpAA7Osg6b/jWiG6t5uKPvlCwRL5j4aBi03bMIc5AMBH/8s2gEfvpWSZUC0viSNT4zwfJgKdgwHs4QhBg3EfkYxLe6S3R7W/RwX/hgW8pQB5Hw3bZhpbOtDuJeP/4Of4xg51PWP2hEz4K4IF9kTMKtjiGpprIoUQ0WFhe7l8cDBWsXBZdusCH4azQHkKo0A6YtuABe+Yaq5gBb7J2GqnVss89IJRp3TILfjgpFy8CrjGx1DpiY6csHFlvqbiOxJUjk+B0oqp2qIFOrGabcNEaGCT7tvNUEJwKb9WPYLTwfNW2EW4VgXAO7hM=--bb5c6OI9HMr/h3Jd--0p6ur3vWAdKRI3SYxGArog== \ No newline at end of file +ONmuV4CWoclCzxsWAbR+B0P9g7iT9wGrBGZBzNLfxp0S6lQ+kiJJrw8wV66vKDYxiODYdTFA4W+vL//7Dd3HAdMy1s+3qFxtEu0GvbFbHJDbu46uCQd/4UiVEpjHa0KeBwd2n6SpIUAZDmBbrtHoGKzxz1lP3M6vyd1g9D2vZguiExuH4JVDbF4jMYd4DQdlqLZL2Poa3r1Gwp/70mt4u2J+rNI1JshK0LFov9P3sAqWIDDbodaQXRDmp92eSNtdui5AmaeGtQjYryygE8njKE4OWhWAGmoRAk20hpiOZvvIc8DMtFxQQemrXZWFRdOkq2g/Lyy2MOMMSSM9dhoAOcPEjCEcSNDmkXddrefJPkEfWp+sXWC6590pbyx7Lvterl+u/bHMALhnUGdVkkKxrNVWAJ9xt1TOfR4TM6ThBZOPF8/x+VEnd0arhh3zW1pe8FJ2mZqFxGbL8VuOA/VPRzAszEq3u3bW5WY36AMIg7xgBplyBqQXDsTW7BCbSFHpX7Y+VwpEOj1lSyhoVGd11s/oWXXHZD/W9C/QbBiWHOe5cYJ/Ugvryhkob/6gkR1B5u6DWLOsQrokF5yMN33h3MwrKbhFYUcaH/D9hECJweVcW+zKUKBpPbI7smlaigBCFP1kUn4D4XoV4Lmr1A7Na+Cn2c3p7aVarPC7GJic4/pA0ZQKDL3/CKsTv24z44j0G4mDpT7Rp6SSYuIkb2mqufp3lqSgC/e7MkDHJ1lx5g3BmCCga0SeNaXxXvRNs05eDCewP69m58rFTS+JubhA8XrXl5Az8xovn3W9QgYR5Zp60gC7cVWigzCZTqpNBGranw3HecQ1N12hxZ/41u3ZLLz9WSIgWPI6WBQ5wXhyuFUBAD261Q==--RTxTAhlW3voVXY8u--+DWAPJUB5NgmSl6FAyhUYg== \ No newline at end of file diff --git a/lib/autoload/course/assessment/programming_package.rb b/lib/autoload/course/assessment/programming_package.rb index 4c7bcd549c8..c3a2f82021f 100644 --- a/lib/autoload/course/assessment/programming_package.rb +++ b/lib/autoload/course/assessment/programming_package.rb @@ -50,16 +50,16 @@ class Course::Assessment::ProgrammingPackage # # @overload initialize(path) # @param [String|Pathname] path The path to the package on disk. - # @overload initialize(stream) - # @param [IO] stream The stream to the file. - def initialize(path_or_stream) - case path_or_stream + # @overload initialize(file) + # @param [File] file The file object. + def initialize(path_or_file) + case path_or_file when String, Pathname - @path = path_or_stream - when IO - @stream = path_or_stream + @path = path_or_file + when File + @file_stream_obj = path_or_file else - raise ArgumentError, 'Invalid path or stream object' + raise ArgumentError, 'Invalid path or File object' end end @@ -72,8 +72,8 @@ def path @file.name elsif @path @path.to_s - elsif @stream.is_a?(File) - @stream.path + elsif @file_stream_obj + @file_stream_obj.path end end @@ -170,8 +170,8 @@ def unzip_file(destination) ensure_file_open! @file.each do |entry| entry_path = File.join(destination, entry.name) - FileUtils.mkdir_p(File.dirname(entry_path)) - @file.extract(entry, entry_path) unless File.exist?(entry_path) + FileUtils.mkdir_p(destination) + @file.extract(entry, destination_directory: destination) unless File.exist?(entry_path) end end @@ -219,10 +219,8 @@ def ensure_file_open! if @path @file = Zip::File.open(@path.to_s) - elsif @stream - @file = Zip::File.new(@stream&.path, true) - @file.read_from_stream(@stream) - @file.instance_variable_set(:@stored_entries, @file.instance_variable_get(:@entry_set).dup) + elsif @file_stream_obj + @file = Zip::File.open_buffer(@file_stream_obj) end raise IllegalStateError unless @file end diff --git a/spec/libraries/course/assessment/programming_package_spec.rb b/spec/libraries/course/assessment/programming_package_spec.rb index 499fd6903bd..aa7da54eba5 100644 --- a/spec/libraries/course/assessment/programming_package_spec.rb +++ b/spec/libraries/course/assessment/programming_package_spec.rb @@ -11,13 +11,9 @@ 'empty_programming_question_template.zip') def temp_package_path - temp_package_stream.tap(&:close).path - end - - def temp_package_stream package_path = Rails.application.config.x.temp_folder.join('spec/packages') FileUtils.mkdir_p(package_path) unless Dir.exist?(package_path) - Tempfile.create('programming_package', package_path) + Tempfile.create('programming_package', package_path).tap(&:close).path end def open_package(path) @@ -34,14 +30,8 @@ def open_package(path) end end - context 'when a file stream is specified' do - let(:package_stream) { temp_package_stream } - subject { open_package(package_stream) } - before do - IO.copy_stream(self.class::PACKAGE_PATH, package_stream) - package_stream.seek(0) - end - + context 'when a File object is specified' do + let(:package_path) { File.new(self.class::PACKAGE_PATH, 'rb') } it 'opens the file' do expect(subject.submission_files).not_to be_empty end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 07aabdacbdc..5e54f2c6ec8 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,7 +13,6 @@ # We use a dummy value since leaving it as null raises an error, and potentially # in the future we can use it in stub logic to differentiate from other external APIs. ENV['CODAVERI_URL'] ||= 'http://localhost:53896' -ENV['SSID_URL'] ||= 'http://localhost:53897' require 'spec_helper' require 'rspec/rails' diff --git a/spec/services/course/assessment/submission/ssid_plagiarism_service_spec.rb b/spec/services/course/assessment/submission/ssid_plagiarism_service_spec.rb index 116e01c6812..1fa1ae4094f 100644 --- a/spec/services/course/assessment/submission/ssid_plagiarism_service_spec.rb +++ b/spec/services/course/assessment/submission/ssid_plagiarism_service_spec.rb @@ -20,7 +20,8 @@ end before do - allow_any_instance_of(SsidAsyncApiService).to receive(:connection).and_return(connection) + allow(SsidApiService).to receive(:api_url).and_return('http://localhost:53897') + allow_any_instance_of(SsidApiService).to receive(:connection).and_return(connection) allow_any_instance_of(Course::Assessment::Submission::SsidZipDownloadService).to receive(:download_and_zip). and_return([File.join(Rails.root, 'spec/fixtures/course/ssid/submissions.zip')]) end @@ -38,36 +39,66 @@ end describe '#start_plagiarism_check' do - before do - stubs.post('/folders') do - [Ssid::ApiStubs::CREATE_FOLDER_SUCCESS[:status], - { 'Content-Type': 'application/json' }, - Ssid::ApiStubs::CREATE_FOLDER_SUCCESS[:body]] - end - stubs.post(/folders\/.*\/submissions/) do |env| - expect(env[:url].to_s).to include("/folders/#{assessment.ssid_folder_id}/submissions") - [Ssid::ApiStubs::UPLOAD_ANSWERS_SUCCESS[:status], - { 'Content-Type': 'application/json' }, - Ssid::ApiStubs::UPLOAD_ANSWERS_SUCCESS[:body]] + context 'when presigned URL matches whitelist' do + before do + allow(SsidApiService).to receive(:upload_whitelist).and_return(/.*/) + stubs.post('/folders') do + [Ssid::ApiStubs::CREATE_FOLDER_SUCCESS[:status], + { 'Content-Type': 'application/json' }, + Ssid::ApiStubs::CREATE_FOLDER_SUCCESS[:body]] + end + stubs.post(/folders\/.*\/uploads/) do |env| + expect(env[:url].to_s).to include("/folders/#{assessment.ssid_folder_id}/uploads") + [Ssid::ApiStubs::GET_PRESIGNED_UPLOAD_URL_SUCCESS[:status], + { 'Content-Type': 'application/json' }, + Ssid::ApiStubs::GET_PRESIGNED_UPLOAD_URL_SUCCESS[:body]] + end + stubs.post('/') do + [Ssid::ApiStubs::MULTIPART_UPLOAD_SUCCESS[:status], + {}, + Ssid::ApiStubs::MULTIPART_UPLOAD_SUCCESS[:body]] + end + stubs.post(/folders\/.*\/plagiarism-checks/) do |env| + expect(env[:url].to_s).to include("/folders/#{assessment.ssid_folder_id}/plagiarism-checks") + [Ssid::ApiStubs::SEND_PLAGIARISM_CHECK_SUCCESS[:status], + { 'Content-Type': 'application/json' }, + Ssid::ApiStubs::SEND_PLAGIARISM_CHECK_SUCCESS[:body]] + end + stubs.get(/folders\/.*\/plagiarism-checks/) do |env| + expect(env[:url].to_s).to include("/folders/#{assessment.ssid_folder_id}/plagiarism-checks") + [Ssid::ApiStubs::FETCH_PLAGIARISM_CHECK_SUCCESSFUL[:status], + { 'Content-Type': 'application/json' }, + Ssid::ApiStubs::FETCH_PLAGIARISM_CHECK_SUCCESSFUL[:body]] + end end - stubs.post(/folders\/.*\/plagiarism-checks/) do |env| - expect(env[:url].to_s).to include("/folders/#{assessment.ssid_folder_id}/plagiarism-checks") - [Ssid::ApiStubs::SEND_PLAGIARISM_CHECK_SUCCESS[:status], - { 'Content-Type': 'application/json' }, - Ssid::ApiStubs::SEND_PLAGIARISM_CHECK_SUCCESS[:body]] - end - stubs.get(/folders\/.*\/plagiarism-checks/) do |env| - expect(env[:url].to_s).to include("/folders/#{assessment.ssid_folder_id}/plagiarism-checks") - [Ssid::ApiStubs::FETCH_PLAGIARISM_CHECK_SUCCESSFUL[:status], - { 'Content-Type': 'application/json' }, - Ssid::ApiStubs::FETCH_PLAGIARISM_CHECK_SUCCESSFUL[:body]] + + it 'completes similarity check successfully' do + subject.start_plagiarism_check + response = subject.fetch_plagiarism_check_result + expect(response['status']).to eq('successful') end end - it 'completes similarity check successfully' do - subject.start_plagiarism_check - response = subject.fetch_plagiarism_check_result - expect(response['status']).to eq('successful') + context 'when presigned URL does not match whitelist' do + before do + allow(SsidApiService).to receive(:upload_whitelist).and_return(/^https:\/\/trusted\.s3\.amazonaws\.com/) + stubs.post('/folders') do + [Ssid::ApiStubs::CREATE_FOLDER_SUCCESS[:status], + { 'Content-Type': 'application/json' }, + Ssid::ApiStubs::CREATE_FOLDER_SUCCESS[:body]] + end + stubs.post(/folders\/.*\/uploads/) do + [Ssid::ApiStubs::GET_PRESIGNED_UPLOAD_URL_SUCCESS[:status], + { 'Content-Type': 'application/json' }, + Ssid::ApiStubs::GET_PRESIGNED_UPLOAD_URL_SUCCESS[:body]] + end + end + + it 'raises SsidError before uploading' do + expect_any_instance_of(Course::Assessment::Submission::SsidZipDownloadService). + not_to receive(:download_and_zip) + expect { subject.start_plagiarism_check }.to raise_error(SsidError) + end end end diff --git a/spec/services/course/ssid_folder_service_spec.rb b/spec/services/course/ssid_folder_service_spec.rb index fdae7fd4d7e..b04daeebcb5 100644 --- a/spec/services/course/ssid_folder_service_spec.rb +++ b/spec/services/course/ssid_folder_service_spec.rb @@ -18,7 +18,7 @@ end before do - allow_any_instance_of(SsidAsyncApiService).to receive(:connection).and_return(connection) + allow_any_instance_of(SsidApiService).to receive(:connection).and_return(connection) end after do diff --git a/spec/support/stubs/ssid/api_stubs.rb b/spec/support/stubs/ssid/api_stubs.rb index 89f48c9ea80..1e4b9522a21 100644 --- a/spec/support/stubs/ssid/api_stubs.rb +++ b/spec/support/stubs/ssid/api_stubs.rb @@ -15,7 +15,19 @@ module Ssid::ApiStubs # rubocop:disable Metrics/ModuleLength }.to_json }.freeze - UPLOAD_ANSWERS_SUCCESS = { + GET_PRESIGNED_UPLOAD_URL_SUCCESS = { + status: 200, + body: { + payload: { + data: { + url: 'http://localhost:53897/', + fields: { 'key' => 'uploads/test.zip', 'policy' => 'base64encodedpolicy' } + } + } + }.to_json + }.freeze + + MULTIPART_UPLOAD_SUCCESS = { status: 204, body: nil }.freeze