canvas-lms/spec/controllers/files_controller_spec.rb

1820 lines
66 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require_relative "../helpers/k5_common"
def new_valid_tool(course)
tool = course.context_external_tools.new(
name: "bob",
consumer_key: "bob",
shared_secret: "bob",
tool_id: "some_tool",
privacy_level: "public"
)
tool.url = "http://www.example.com/basic_lti"
tool.resource_selection = {
url: "http://#{HostUrl.default_host}/selection_test",
selection_width: 400,
selection_height: 400
}
tool.save!
tool
end
describe FilesController do
include K5Common
def course_folder
@folder = @course.folders.create!(name: "a folder", workflow_state: "visible")
end
def io
fixture_file_upload("docs/doc.doc", "application/msword", true)
end
def course_file
@file = factory_with_protected_attributes(@course.attachments, uploaded_data: io)
end
def user_file
@file = factory_with_protected_attributes(@user.attachments, uploaded_data: io)
end
def user_html_file
@file = factory_with_protected_attributes(@user.attachments, uploaded_data: fixture_file_upload("test.html", "text/html", false))
end
def account_js_file
@file = factory_with_protected_attributes(@account.attachments, uploaded_data: fixture_file_upload("test.js", "text/javascript", false))
end
def folder_file
@file = @folder.active_file_attachments.build(uploaded_data: io)
@file.context = @course
@file.save!
@file
end
def file_in_a_module
@module = @course.context_modules.create!(name: "module")
@tag = @module.add_item({ type: "attachment", id: @file.id })
@module.reload
hash = {}
hash[@tag.id.to_s] = { type: "must_view" }
@module.completion_requirements = hash
@module.save!
end
def file_with_path(path)
components = path.split("/")
folder = nil
while components.size > 1
component = components.shift
folder = @course.folders.where(name: component).first
folder ||= @course.folders.create!(name: component, workflow_state: "visible", parent_folder: folder)
end
filename = components.shift
@file = folder.active_file_attachments.build(filename: filename, uploaded_data: io)
@file.context = @course
@file.save!
@file
end
before :once do
@other_user = user_factory(active_all: true)
course_with_teacher active_all: true
student_in_course active_all: true
end
describe "GET 'quota'" do
it "requires authorization" do
get "quota", params: { course_id: @course.id }
assert_unauthorized
end
it "assigns variables for course quota" do
user_session(@teacher)
get "quota", params: { course_id: @course.id }
expect(assigns[:quota]).not_to be_nil
expect(response).to be_successful
end
it "assigns variables for user quota" do
user_session(@student)
get "quota", params: { user_id: @student.id }
expect(assigns[:quota]).not_to be_nil
expect(response).to be_successful
end
it "assigns variables for group quota" do
user_session(@teacher)
group_model(context: @course)
get "quota", params: { group_id: @group.id }
expect(assigns[:quota]).not_to be_nil
expect(response).to be_successful
end
it "allows changing group quota" do
user_session(@teacher)
group_model(context: @course, storage_quota: 500.megabytes)
get "quota", params: { group_id: @group.id }
expect(assigns[:quota]).to eq 500.megabytes
expect(response).to be_successful
end
end
describe "GET 'index'" do
it "requires authorization" do
get "index", params: { course_id: @course.id }
assert_unauthorized
end
it "redirects 'disabled', if disabled by the teacher" do
user_session(@student)
@course.update_attribute(:tab_configuration, [{ "id" => 11, "hidden" => true }])
get "index", params: { course_id: @course.id }
expect(response).to be_redirect
expect(flash[:notice]).to match(/That page has been disabled/)
end
it "assigns variables" do
user_session(@teacher)
get "index", params: { course_id: @course.id }
expect(response).to be_successful
expect(assigns[:contexts]).not_to be_nil
expect(assigns[:contexts][0]).to eql(@course)
end
it "works for a user context, too" do
user_session(@student)
get "index", params: { user_id: @student.id }
expect(response).to be_successful
end
it "works for a group context, too" do
group_with_user_logged_in(group_context: Account.default)
get "index", params: { group_id: @group.id }
expect(response).to be_successful
end
it "refuses for a non-html format" do
group_with_user_logged_in(group_context: Account.default)
get "index", params: { group_id: @group.id }, format: :js
expect(response.body).to include("endpoint does not support js")
expect(response.code.to_i).to eq(400)
end
it "does not show external tools in a group context" do
group_with_user_logged_in(group_context: Account.default)
new_valid_tool(@course)
user_file
@file.context = @group
get "index", params: { group_id: @group.id }
expect(assigns[:js_env][:FILES_CONTEXTS][0][:file_menu_tools]).to eq []
end
context "file menu tool visibility" do
before do
course_factory(active_all: true)
@tool = @course.context_external_tools.create!(name: "a", url: "http://google.com", consumer_key: "12345", shared_secret: "secret")
@tool.file_menu = {
visibility: "admins"
}
@tool.save!
end
before do
user_factory(active_all: true)
user_session(@user)
end
it "shows restricted external tools to teachers" do
@course.enroll_teacher(@user).accept!
get "index", params: { course_id: @course.id }
expect(assigns[:js_env][:FILES_CONTEXTS][0][:file_menu_tools].count).to eq 1
end
it "does not show restricted external tools to students" do
course_file
@course.enroll_student(@user).accept!
get "index", params: { course_id: @course.id }
expect(assigns[:js_env][:FILES_CONTEXTS][0][:file_menu_tools]).to eq []
end
end
describe "across shards" do
specs_require_sharding
before :once do
@shard2.activate do
user_factory(active_all: true)
end
end
before do
user_session(@user)
end
it "authorizes users on a remote shard" do
get "index", params: { user_id: @user.global_id }
expect(response).to be_successful
end
end
end
describe "GET 'show'" do
before :once do
course_file
end
it "requires authorization" do
get "show", params: { course_id: @course.id, id: @file.id }
assert_unauthorized
end
it "respects user context" do
skip("investigate cause for failures beginning 05/05/21 FOO-1950")
user_session(@teacher)
assert_page_not_found do
get "show", params: { user_id: @user.id, id: @file.id }, format: "html"
end
end
it "doesn't allow an assignment_id to bypass other auth checks" do
assignment1 = @course.assignments.create!(name: "an assignment")
attachment_model(context: @teacher, uploaded_data: stub_file_data("test.m4v", "asdf", "video/mp4"))
user_session(@student)
get "show", params: { id: @attachment.id }, format: :json
expect(response).not_to be_ok
get "show", params: { assignment_id: assignment1.id, id: @attachment.id }, format: :json
expect(response).not_to be_ok
end
describe "with verifiers" do
it "allows public access with legacy verifier" do
allow_any_instance_of(Attachment).to receive(:canvadoc_url).and_return "stubby"
get "show", params: { course_id: @course.id, id: @file.id, verifier: @file.uuid }, format: "json"
expect(response).to be_successful
expect(json_parse["attachment"]).to_not be_nil
expect(json_parse["attachment"]["canvadoc_session_url"]).to eq "stubby"
expect(json_parse["attachment"]["md5"]).to be_nil
end
it "allows public access with new verifier" do
verifier = Attachments::Verification.new(@file).verifier_for_user(nil)
get "show", params: { course_id: @course.id, id: @file.id, verifier: verifier }, format: "json"
expect(response).to be_successful
expect(json_parse["attachment"]).to_not be_nil
expect(json_parse["attachment"]["md5"]).to be_nil
end
it "does not redirect to terms-acceptance page" do
user_session(@teacher)
session[:require_terms] = true
verifier = Attachments::Verification.new(@file).verifier_for_user(@teacher)
get "show", params: { course_id: @course.id, id: @file.id, verifier: verifier }, format: "json"
expect(response).to be_successful
end
it "emits an asset_accessed live event" do
allow_any_instance_of(Attachment).to receive(:canvadoc_url).and_return "stubby"
expect(Canvas::LiveEvents).to receive(:asset_access).with(@file, "files", nil, nil)
get "show", params: { course_id: @course.id, id: @file.id, verifier: @file.uuid, download: 1 }, format: "json"
end
end
it "assigns variables" do
user_session(@teacher)
get "show", params: { course_id: @course.id, id: @file.id }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment]).to eql(@file)
end
it "redirects for download" do
user_session(@teacher)
# k5_mode hooks don't run because we never render
expect(allow_any_instantiation_of(@course)).not_to receive(:elementary_subject_course?)
get "show", params: { course_id: @course.id, id: @file.id, download: 1 }
expect(response).to be_redirect
end
it "forces download when download_frd is set" do
user_session(@teacher)
# this call should happen inside of FilesController#send_attachment
expect_any_instance_of(FilesController).to receive(:send_stored_file).with(@file, false)
get "show", params: { course_id: @course.id, id: @file.id, download: 1, verifier: @file.uuid, download_frd: 1 }
end
it "remembers most recent valid sf_verifier in session" do
user1 = user_factory(active_all: true)
file1 = user_file
verifier1 = Users::AccessVerifier.generate(user: user1)
user2 = user_factory(active_all: true)
file2 = user_file
verifier2 = Users::AccessVerifier.generate(user: user2)
# first verifier
user_session(user1)
get "show", params: verifier1.merge(id: file1.id)
expect(response).to be_successful
expect(session[:file_access_user_id]).to eq user1.global_id
expect(session[:file_access_expiration]).not_to be_nil
expect(session[:permissions_key]).not_to be_nil
permissions_key = session[:permissions_key]
# second verifier, should update session
get "show", params: verifier2.merge(id: file2.id)
expect(response).to be_successful
expect(session[:file_access_user_id]).to eq user2.global_id
expect(session[:file_access_expiration]).not_to be_nil
expect(session[:permissions_key]).not_to eq permissions_key
permissions_key = session[:permissions_key]
# repeat access, even without verifier, should extend expiration (though
# we can't assert that, because milliseconds) and thus change
# permissions_key
get "show", params: { id: file2.id }
expect(response).to be_successful
expect(session[:permissions_key]).not_to eq permissions_key
end
it "redirects without sf_verifier for inline_content files" do
user = user_factory(active_all: true)
file = user_html_file
verifier = Users::AccessVerifier.generate(user: user)
get "show", params: verifier.merge(id: file.id)
expect(response).to be_redirect
expect(response.headers["Location"]).to eq "http://test.host/files/#{file.id}"
end
it "ignores invalid sf_verifiers" do
user = user_factory(active_all: true)
file = user_file
verifier = Users::AccessVerifier.generate(user: user)
# first use to establish session
get "show", params: verifier.merge(id: file.id)
expect(response).to be_successful
permissions_key = session[:permissions_key]
# second use after verifier expiration but before session expiration.
# expired verifier should be ignored but session should still be extended
Timecop.freeze((Users::AccessVerifier::TTL_MINUTES + 1).minutes.from_now) do
get "show", params: verifier.merge(id: file.id)
end
expect(response).to be_successful
expect(session[:permissions_key]).not_to eq permissions_key
end
it "sets cache headers for non text files" do
get "show", params: { course_id: @course.id, id: @file.id, download: 1, verifier: @file.uuid, download_frd: 1 }
expect(response.header["Cache-Control"]).to include "private"
expect(response.header["Cache-Control"]).to include "max-age=#{1.day.seconds}"
expect(response.header["Cache-Control"]).not_to include "no-cache"
expect(response.header["Cache-Control"]).not_to include "no-store"
expect(response.header["Cache-Control"]).not_to include "must-revalidate"
expect(response.header).to include("Expires")
expect(response.header).not_to include("Pragma")
end
it "does not set cache headers for text files" do
@file.content_type = "text/html"
@file.save
get "show", params: { course_id: @course.id, id: @file.id, download: 1, verifier: @file.uuid, download_frd: 1 }
# rails will include private directive by default unless no-cache is provided
expect(response.header["Cache-Control"]).to include "no-store"
expect(response.header).not_to include("Expires")
expect(response.header).to include("Pragma")
end
it "allows concluded teachers to read and download files" do
user_session(@teacher)
@enrollment.conclude
get "show", params: { course_id: @course.id, id: @file.id }
expect(response).to be_successful
get "show", params: { course_id: @course.id, id: @file.id, download: 1 }
expect(response).to be_redirect
end
context "when the attachment has been overwritten" do
subject do
get "show", params: params
response
end
let(:old_file) do
old = @course.attachments.build(display_name: "old file")
old.file_state = "deleted"
old.replacement_attachment = file
old.save!
old
end
let(:file) { @file }
let(:params) { { course_id: @course.id, id: old_file.id, preview: 1 } }
before { user_session(@teacher) }
it "finds overwritten files" do
expect(subject).to be_redirect
expect(subject.location).to match(%r{/courses/#{@course.id}/files/#{file.id}})
end
context "and no context is given" do
let(:params) { { id: old_file.id, preview: 1 } }
it "does not find the file" do
expect(subject).to be_not_found
end
context "but a replacement_chain_context is given" do
let(:params) do
{
id: old_file.id,
preview: 1,
replacement_chain_context_type: "course",
replacement_chain_context_id: @course.id
}
end
it "find the new file" do
expect(subject).to be_redirect
location = URI.parse(subject.location)
query = CGI.parse(location.query)
expect(location.path).to eq "/files/#{file.id}/download"
expect(query["download_frd"]).to eq ["1"]
expect(query["sf_verifier"]).to be_present
end
end
end
end
context "after user merge" do
before :once do
@merge_user_1 = student_in_course(name: "Merge User 1", active_all: true).user
@merge_user_2 = student_in_course(name: "Merge User 2", active_all: true).user
@user_1_file = attachment_model(context: @merge_user_1, md5: "hi")
end
before do
user_session(@teacher)
end
it "finds file in merged-to user's context" do
UserMerge.from(@merge_user_1).into(@merge_user_2)
UserMerge.from(@merge_user_2).into(@student)
run_jobs
get "show", params: { user_id: @merge_user_1.id, id: @user_1_file.id, verifier: @user_1_file.uuid }
expect(response).to be_successful
expect(@user_1_file.reload.context_type).to eq "User"
expect(@user_1_file.context_id).to eq @student.id
end
it "finds file in merged-from user's context when merged-to user already had the file" do
@user_2_file = attachment_model(context: @merge_user_2, md5: "hi")
UserMerge.from(@merge_user_1).into(@merge_user_2)
UserMerge.from(@merge_user_2).into(@student)
run_jobs
get "show", params: { user_id: @merge_user_1.id, id: @user_1_file.id, verifier: @user_1_file.uuid }
expect(response).to be_successful
expect(@user_1_file.reload.context_type).to eq "User"
expect(@user_1_file.context_id).to eq @merge_user_1.id
end
context "with sharding" do
specs_require_sharding
it "finds file in intermediate user's context if merge has happened cross-shard" do
@shard1.activate do
account = Account.create!
course_with_student(account: account)
end
UserMerge.from(@merge_user_1).into(@merge_user_2)
UserMerge.from(@merge_user_2).into(@student)
run_jobs
get "show", params: { user_id: @merge_user_1.id, id: @user_1_file.id, verifier: @user_1_file.uuid }
expect(response).to be_successful
expect(@user_1_file.reload.context_type).to eq "User"
expect(@user_1_file.context_id).to eq @merge_user_2.id
end
it "finds files correctly when given a non-native user ID" do
@shard1.activate do
account = Account.create!
course_with_student(account: account)
end
UserMerge.from(@merge_user_1).into(@merge_user_2)
UserMerge.from(@merge_user_2).into(@student)
run_jobs
@shard1.activate do
get "show", params: { user_id: @merge_user_1.id, id: @user_1_file.id, verifier: @user_1_file.uuid }
expect(response).to be_successful
expect(@user_1_file.reload.context_type).to eq "User"
expect(@user_1_file.context_id).to eq @merge_user_2.id
end
end
end
end
describe "as a student" do
before do
user_session(@student)
end
describe "with a module item ID" do
let(:params) do
{
course_id: @course.id,
id: @file.id,
module_item_id: 1
}
end
it "logs asset access for the attachment" do
expect(controller).to receive(:log_asset_access).with(
@file,
"files",
"files"
)
get "show", params: params
end
end
it "allows concluded students to read and download files" do
@enrollment.conclude
get "show", params: { course_id: @course.id, id: @file.id }
expect(response).to be_successful
get "show", params: { course_id: @course.id, id: @file.id, download: 1 }
expect(response).to be_redirect
end
it "marks files as viewed for module progressions if the file is previewed inline" do
file_in_a_module
get "show", params: { course_id: @course.id, id: @file.id, inline: 1 }
expect(json_parse).to eq({ "ok" => true })
@module.reload
expect(@module.evaluate_for(@student).state).to eql(:completed)
end
it "marks files as viewed for module progressions if the file is downloaded" do
file_in_a_module
get "show", params: { course_id: @course.id, id: @file.id, download: 1 }
@module.reload
expect(@module.evaluate_for(@student).state).to eql(:completed)
end
it "marks files as viewed for module progressions if the file data is requested and is canvadocable" do
file_in_a_module
allow_any_instance_of(Attachment).to receive(:canvadocable?).and_return true
get "show", params: { course_id: @course.id, id: @file.id }, format: :json
@module.reload
expect(@module.evaluate_for(@student).state).to eql(:completed)
end
it "marks media files viewed when rendering html with file_preview" do
@file = attachment_model(context: @course, uploaded_data: stub_file_data("test.m4v", "asdf", "video/mp4"))
file_in_a_module
get "show", params: { course_id: @course.id, id: @file.id }, format: :html
@module.reload
expect(@module.evaluate_for(@student).state).to eql(:completed)
end
it "redirects to the user's files URL when browsing to an attachment with the same path as a deleted attachment" do
owned_file = course_file
owned_file.display_name = "holla"
owned_file.user_id = @student.id
owned_file.save
owned_file.destroy
get "show", params: { course_id: @course.id, id: owned_file.id }
expect(response).to be_redirect
expect(flash[:notice]).to match(/has been deleted/)
expect(URI.parse(response["Location"]).path).to eq "/courses/#{@course.id}/files"
end
it "displays a new file without incident" do
new_file = course_file
new_file.display_name = "holla"
new_file.save
get "show", params: { course_id: @course.id, id: new_file.id }
expect(response).to be_successful
expect(assigns(:attachment)).to eq new_file
end
it "does not leak the name of unowned deleted files" do
unowned_file = @file
unowned_file.display_name = "holla"
unowned_file.save
unowned_file.destroy
get "show", params: { course_id: @course.id, id: unowned_file.id }
expect(response.status).to eq(404)
expect(assigns(:not_found_message)).to eq("This file has been deleted")
end
it "does not blow up for logged out users" do
unowned_file = @file
unowned_file.display_name = "holla"
unowned_file.save
unowned_file.destroy
remove_user_session
get "show", params: { course_id: @course.id, id: unowned_file.id }
expect(response.status).to eq(404)
expect(assigns(:not_found_message)).to eq("This file has been deleted")
end
it "views file when student's submission was deleted" do
@assignment = @course.assignments.create!(title: "upload_assignment", submission_types: "online_upload")
attachment_model context: @student
@assignment.submit_homework @student, attachments: [@attachment]
# create an orphaned attachment_association
@assignment.all_submissions.delete_all
get "show", params: { user_id: @student.id, id: @attachment.id, download_frd: 1 }
expect(response).to be_successful
end
it "hides the left side if in K5 mode" do
toggle_k5_setting(@course.account)
expect(controller).to receive(:set_k5_mode).and_call_original
get "show", params: { course_id: @course.id, id: @file.id }
expect(response).to be_successful
expect(assigns[:show_left_side]).to be false
end
end
describe "as a teacher" do
before do
user_session @teacher
end
it "works for quiz_statistics" do
quiz_model
file = @quiz.statistics_csv("student_analysis").csv_attachment
get "show", params: { quiz_statistics_id: file.reload.context.id,
file_id: file.id, download: "1", verifier: file.uuid }
expect(response).to be_redirect
end
it "records the inline view when a teacher previews a student's submission" do
@assignment = @course.assignments.create!(title: "upload_assignment", submission_types: "online_upload")
attachment_model context: @student
@assignment.submit_homework @student, attachments: [@attachment]
get "show", params: { user_id: @student.id, id: @attachment.id, inline: 1 }
expect(response).to be_successful
end
it "is successful when viewing as an admin even if locked" do
@file.locked = true
@file.save!
get "show", params: { course_id: @course.id, id: @file.id }
expect(response).to be_successful
end
describe "with a module item ID" do
let(:params) do
{
course_id: @course.id,
id: @file.id,
module_item_id: 1
}
end
it "logs asset access for the attachment" do
expect(controller).to receive(:log_asset_access).with(
@file,
"files",
"files"
)
get "show", params: params
end
end
end
describe "canvadoc_session_url" do
before do
user_session(@student)
allow(Canvadocs).to receive(:enabled?).and_return true
@file = canvadocable_attachment_model
end
it "is included if :download is allowed" do
get "show", params: { course_id: @course.id, id: @file.id }, format: "json"
expect(json_parse["attachment"]["canvadoc_session_url"]).to be_present
end
it "is not included if locked" do
@file.lock_at = 1.month.ago
@file.save!
get "show", params: { course_id: @course.id, id: @file.id }, format: "json"
expect(json_parse["attachment"]["canvadoc_session_url"]).to be_nil
end
it "is included in newly uploaded files" do
user_session(@teacher)
attachment = factory_with_protected_attributes(Attachment, context: @course, file_state: "deleted", filename: "doc.doc")
attachment.uploaded_data = io
attachment.save!
get "api_create_success", params: { id: attachment.id, uuid: attachment.uuid }, format: "json"
expect(json_parse["canvadoc_session_url"]).to be_present
end
end
end
describe "GET 'api_create_success'" do
before do
category = group_category
@group = category.groups.create(context: @course)
@group.add_user(@student)
user_session(@student)
end
it "treats attachments that live in the special 'submissions' folder as quota exempt" do
attachment = Attachment.create!(
context: @group,
uploaded_data: StringIO.new("my file"),
folder: @group.submissions_folder,
filename: "my-great-file.txt",
file_state: "deleted"
)
attachment.update_attribute(:size, 51.megabytes)
get "api_create_success", params: { id: attachment.id, uuid: attachment.uuid }, format: "json"
expect(response).to be_successful
end
it "does not give quota exemption to files not in the special 'submissions' folder" do
attachment = Attachment.create!(
context: @group,
uploaded_data: StringIO.new("my file"),
filename: "my-great-file.txt",
file_state: "deleted"
)
attachment.update_attribute(:size, 51.megabytes)
get "api_create_success", params: { id: attachment.id, uuid: attachment.uuid }, format: "json"
expect(json_parse.fetch("message")).to eq "file size exceeds quota limits"
end
end
describe "GET 'show_relative'" do
before(:once) do
course_file
file_in_a_module
end
context "as student" do
before do
user_session(@student)
end
it "finds files by relative path" do
get "show_relative", params: { course_id: @course.id, file_path: @file.full_display_path }
expect(response).to be_redirect
get "show_relative", params: { course_id: @course.id, file_path: @file.full_path }
expect(response).to be_redirect
def test_path(path)
file_with_path(path)
get "show_relative", params: { course_id: @course.id, file_path: @file.full_display_path }
expect(response).to be_redirect
get "show_relative", params: { course_id: @course.id, file_path: @file.full_path }
expect(response).to be_redirect
end
test_path("course files/unfiled/test1.txt")
test_path("course files/blah")
test_path("course files/a/b/c%20dude/d/e/f.gif")
end
it "renders unauthorized access page if the file path doesn't match" do
get "show_relative", params: { course_id: @course.id, file_path: @file.full_display_path + "blah" }
expect(response).to render_template("shared/errors/file_not_found")
get "show_relative", params: { file_id: @file.id, course_id: @course.id, file_path: @file.full_display_path + "blah" }
expect(response).to render_template("shared/errors/file_not_found")
end
it "renders file_not_found even if the format is non-html" do
get "show_relative", params: { file_id: @file.id, course_id: @course.id, file_path: @file.full_display_path + ".css" }, format: "css"
expect(response).to render_template("shared/errors/file_not_found")
end
it "ignores bad file_ids" do
get "show_relative", params: { file_id: @file.id + 1, course_id: @course.id, file_path: @file.full_display_path }
expect(response).to be_redirect
get "show_relative", params: { file_id: "blah", course_id: @course.id, file_path: @file.full_display_path }
expect(response).to be_redirect
end
it "renders inline for html files" do
s3_storage!
allow(HostUrl).to receive(:file_host).and_return("files.test")
request.host = "files.test"
@file.update_attribute(:content_type, "text/html")
handle = double(read: "hello")
allow_any_instantiation_of(@file).to receive(:open).and_return(handle)
get "show_relative", params: { file_id: @file.id, course_id: @course.id, file_path: @file.full_display_path, inline: 1, download: 1 }
expect(response).to be_successful
expect(response.body).to eq "hello"
expect(response.media_type).to eq "text/html"
end
it "redirects for large html files" do
s3_storage!
allow(HostUrl).to receive(:file_host).and_return("files.test")
request.host = "files.test"
@file.update_attribute(:content_type, "text/html")
@file.update_attribute(:size, 1024 * 1024)
allow_any_instance_of(FileAuthenticator).to receive(:inline_url).and_return("https://s3/myfile")
get "show_relative", params: { file_id: @file.id, course_id: @course.id, file_path: @file.full_display_path, inline: 1, download: 1 }
expect(response).to redirect_to("https://s3/myfile")
end
it "redirects for image files" do
s3_storage!
allow(HostUrl).to receive(:file_host).and_return("files.test")
request.host = "files.test"
@file.update_attribute(:content_type, "image/jpeg")
allow_any_instance_of(FileAuthenticator).to receive(:inline_url).and_return("https://s3/myfile")
get "show_relative", params: { file_id: @file.id, course_id: @course.id, file_path: @file.full_display_path, inline: 1, download: 1 }
expect(response).to redirect_to("https://s3/myfile")
end
it "redirects for non-html files" do
s3_storage!
allow(HostUrl).to receive(:file_host).and_return("files.test")
request.host = "files.test"
# it's a .doc file
allow_any_instance_of(FileAuthenticator).to receive(:download_url).and_return("https://s3/myfile")
get "show_relative", params: { file_id: @file.id, course_id: @course.id, file_path: @file.full_display_path, inline: 1, download: 1 }
expect(response).to redirect_to("https://s3/myfile")
end
it "prioritizes matches on display name vs. filename" do
display_name = "file.txt"
# make a file with an original filename matching the other file's display_name
Attachment.create!(context: @course, uploaded_data: StringIO.new("blah1"), folder: Folder.root_folders(@course).first,
filename: display_name, display_name: "something_else.txt")
file2 = Attachment.create!(context: @course, uploaded_data: StringIO.new("blah2"), folder: Folder.root_folders(@course).first,
filename: "still_something_else.txt", display_name: display_name)
other_file = Attachment.create!(context: @course, uploaded_data: StringIO.new("blah3"), folder: Folder.root_folders(@course).first,
filename: "totallydifferent.html")
get "show_relative", params: { file_id: other_file.id, course_id: @course.id, file_path: file2.full_display_path }
expect(assigns[:attachment]).to eq file2
end
end
context "unauthenticated user" do
it "renders unauthorized if the file exists" do
get "show_relative", params: { course_id: @course.id, file_path: @file.full_display_path }
assert_unauthorized
end
it "renders unauthorized if the file doesn't exist" do
get "show_relative", params: { course_id: @course.id, file_path: "course files/nope" }
assert_unauthorized
end
end
context "after user merge" do
before :once do
@merge_user_1 = student_in_course(name: "Merge User 1", active_all: true).user
@user_1_file = attachment_model(context: @merge_user_1, md5: "hi")
end
before do
user_session(@teacher)
end
context "with sharding" do
specs_require_sharding
it "allows access to files from a user who was merged into another user (happens with cross-shard merge)" do
@shard1.activate do
account = Account.create!
course_with_student(account: account)
end
UserMerge.from(@merge_user_1).into(@student)
run_jobs
get "show_relative", params: { user_id: @merge_user_1.id, file_id: @user_1_file.id, file_path: @user_1_file.full_path, verifier: @user_1_file.uuid }
expect(response).to be_redirect
end
end
end
context "account-context files" do
before :once do
@account = account_model
end
before do
allow(HostUrl).to receive(:file_host).and_return("files.test")
request.host = "files.test"
user_session(@teacher)
end
it "skips verification for an account-context file" do
account_js_file
file_verifier = Attachments::Verification.new(@file).verifier_for_user(nil)
user_verifier = Users::AccessVerifier.generate(user: @teacher)
other_params = { download: 1, inline: 1, verifier: file_verifier, account_id: @account.id, file_id: @file.id, file_path: @file.full_path }
get "show_relative", params: user_verifier.merge(other_params)
expect(response).to be_redirect
get "show_relative", params: other_params
expect(response).to be_successful
end
it "enforces verification for contexts other than account" do
course_file
file_verifier = Attachments::Verification.new(@file).verifier_for_user(nil)
user_verifier = Users::AccessVerifier.generate(user: @teacher)
other_params = { download: 1, inline: 1, verifier: file_verifier, account_id: @account.id, file_id: @file.id, file_path: @file.full_path }
get "show_relative", params: user_verifier.merge(other_params)
assert_unauthorized
end
end
end
describe "PUT 'update'" do
before :once do
course_file
end
it "requires authorization" do
put "update", params: { course_id: @course.id, id: @file.id }
assert_unauthorized
end
it "updates file" do
user_session(@teacher)
put "update", params: { course_id: @course.id, id: @file.id, attachment: { display_name: "new name", uploaded_data: nil } }
expect(response).to be_redirect
expect(assigns[:attachment]).to eql(@file)
expect(assigns[:attachment].display_name).to eql("new name")
expect(assigns[:attachment].user_id).to be_nil
end
it "moves file into a folder" do
user_session(@teacher)
course_folder
put "update", params: { course_id: @course.id, id: @file.id, attachment: { folder_id: @folder.id } }, format: "json"
expect(response).to be_successful
@file.reload
expect(@file.folder).to eql(@folder)
end
context "submissions folder" do
before(:once) do
@student = user_model
@root_folder = Folder.root_folders(@student).first
@file = attachment_model(context: @user, uploaded_data: default_uploaded_data, folder: @root_folder)
@sub_folder = @student.submissions_folder
@sub_file = attachment_model(context: @user, uploaded_data: default_uploaded_data, folder: @sub_folder)
end
it "does not move a file into a submissions folder" do
user_session(@student)
put "update", params: { user_id: @student.id, id: @file.id, attachment: { folder_id: @sub_folder.id } }, format: "json"
expect(response.status).to eq 401
end
it "does not move a file out of a submissions folder" do
user_session(@student)
put "update", params: { user_id: @student.id, id: @sub_file.id, attachment: { folder_id: @root_folder.id } }, format: "json"
expect(response.status).to eq 401
end
end
it "replaces content and update user_id" do
course_with_teacher_logged_in(active_all: true)
course_file
new_content = default_uploaded_data
put "update", params: { course_id: @course.id, id: @file.id, attachment: { uploaded_data: new_content } }
expect(response).to be_redirect
expect(assigns[:attachment]).to eql(@file)
@file.reload
expect(@file.size).to eql new_content.size
expect(@file.user).to eql @teacher
end
context "usage_rights_required" do
before do
@course.usage_rights_required = true
@course.save!
user_session(@teacher)
@file.update_attribute(:locked, true)
end
it "does not publish if usage_rights unset" do
put "update", params: { course_id: @course.id, id: @file.id, attachment: { locked: "false" } }
expect(@file.reload).to be_locked
end
it "publishes if usage_rights set" do
@file.usage_rights = @course.usage_rights.create! use_justification: "public_domain"
@file.save!
put "update", params: { course_id: @course.id, id: @file.id, attachment: { locked: "false" } }
expect(@file.reload).not_to be_locked
end
end
end
describe "DELETE 'destroy'" do
context "authorization" do
before :once do
course_file
end
it "requires authorization" do
delete "destroy", params: { course_id: @course.id, id: @file.id }
expect(response.body).to eql("{\"message\":\"Unauthorized to delete this file\"}")
expect(assigns[:attachment].file_state).to eq "available"
end
it "deletes file" do
user_session(@teacher)
delete "destroy", params: { course_id: @course.id, id: @file.id }
expect(response).to be_redirect
expect(assigns[:attachment]).to eql(@file)
expect(assigns[:attachment].file_state).to eq "deleted"
end
end
it "refuses to delete a file in a submissions folder" do
file = @student.attachments.create! display_name: "blah", uploaded_data: default_uploaded_data, folder: @student.submissions_folder
delete "destroy", params: { user_id: @student.id, id: file.id }
expect(response.status).to eq 401
end
context "file that has been submitted" do
def submit_file
assignment = @course.assignments.create!(title: "some assignment", submission_types: "online_upload")
@file = attachment_model(context: @user, uploaded_data: stub_file_data("test.txt", "asdf", "text/plain"))
assignment.submit_homework(@student, attachments: [@file])
end
before do
submit_file
user_session(@student)
end
it "does not delete" do
delete "destroy", params: { id: @file.id }
expect(response.body).to eql("{\"message\":\"Cannot delete a file that has been submitted as part of an assignment\"}")
expect(assigns[:attachment].file_state).to eq "available"
end
end
end
describe "POST 'create_pending'" do
it "requires authorization" do
user_session(@other_user)
post "create_pending", params: { attachment: { context_code: @course.asset_string } }
assert_unauthorized
end
it "requires a pseudonym" do
post "create_pending", params: { attachment: { context_code: @course.asset_string } }
expect(response).to redirect_to login_url
end
it "creates file placeholder (in local mode)" do
local_storage!
user_session(@teacher)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].id).not_to be_nil
expect(assigns[:attachment][:user_id]).not_to be_nil
json = json_parse
expect(json).not_to be_nil
expect(json["upload_url"]).not_to be_nil
expect(json["upload_params"]).not_to be_nil
expect(json["upload_params"]).not_to be_empty
end
it "creates file placeholder (in s3 mode)" do
s3_storage!
user_session(@teacher)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].id).not_to be_nil
expect(assigns[:attachment][:user_id]).not_to be_nil
json = json_parse
expect(json).not_to be_nil
expect(json["upload_url"]).not_to be_nil
expect(json["upload_params"]).to be_present
expect(json["upload_params"]["x-amz-credential"]).to start_with("stub_id")
end
it "allows specifying a content_type" do
# the API does, and the files page sends it based on the browser's detection
s3_storage!
user_session(@teacher)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
filename: "something.rb",
content_type: "text/magical-incantation"
} }
expect(response).to be_successful
expect(assigns[:attachment].content_type).to eq "text/magical-incantation"
end
it "does not allow going over quota for file uploads" do
s3_storage!
user_session(@student)
Setting.set("user_default_quota", -1)
post "create_pending", params: { attachment: {
context_code: @student.asset_string,
filename: "bob.txt",
size: 1
} }
expect(response).to be_bad_request
expect(assigns[:quota_used]).to be > assigns[:quota]
end
it "allows going over quota for homework submissions" do
s3_storage!
user_session(@student)
@assignment = @course.assignments.create!(title: "upload_assignment", submission_types: "online_upload")
Setting.set("user_default_quota", -1)
post "create_pending", params: { attachment: {
context_code: @assignment.context_code,
asset_string: @assignment.asset_string,
intent: "submit",
filename: "bob.txt"
}, format: :json }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].id).not_to be_nil
json = json_parse
expect(json).not_to be_nil
expect(json["upload_url"]).not_to be_nil
expect(json["upload_params"]).to be_present
expect(json["upload_params"]["x-amz-credential"]).to start_with("stub_id")
end
# This test verifies that an attachment on a graded discussion will not affect the files quota
it "allows going over quota for graded discussions submissions" do
s3_storage!
user_session(@student)
@assignment = @course.assignments.create!(title: "discussion assignment", submission_types: "discussion_topic")
Setting.set("user_default_quota", -1)
post "create_pending", params: { attachment: {
context_code: @assignment.context_code,
asset_string: @assignment.asset_string,
intent: "submit",
filename: "bob.txt"
}, format: :json }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].id).not_to be_nil
json = json_parse
expect(json).not_to be_nil
expect(json["upload_url"]).not_to be_nil
expect(json["upload_params"]).to be_present
expect(json["upload_params"]["x-amz-credential"]).to start_with("stub_id")
end
it "associates assignment submission for a group assignment with the group" do
user_session(@student)
category = group_category
assignment = @course.assignments.create(group_category: category, submission_types: "online_upload")
group = category.groups.create(context: @course)
group.add_user(@student)
user_session(@student)
# assignment.grants_right?(@student, :submit).should be_true
# assignment.grants_right?(@student, :nothing).should be_true
s3_storage!
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
asset_string: assignment.asset_string,
intent: "submit",
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].context).to eq group
end
it "creates the file in unlocked state if :usage_rights_required is disabled" do
@course.usage_rights_required = false
@course.save!
user_session(@teacher)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment].locked).to be_falsy
end
it "creates the file in locked state if :usage_rights_required is enabled" do
@course.usage_rights_required = true
@course.save!
user_session(@teacher)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment].locked).to be_truthy
end
it "refuses to create a file in a submissions folder" do
user_session(@student)
post "create_pending", params: { attachment: {
context_code: @student.asset_string,
filename: "test.txt",
folder_id: @student.submissions_folder.id
} }
expect(response.status).to eq 401
end
it "creates a file in the submissions folder if intent=='submit'" do
user_session(@student)
assignment = @course.assignments.create!(submission_types: "online_upload")
post "create_pending", params: { attachment: {
context_code: assignment.context_code,
asset_string: assignment.asset_string,
filename: "test.txt",
intent: "submit"
} }
f = assigns[:attachment].folder
expect(f.submission_context_code).to eq @course.asset_string
end
it "uses a submissions folder for group assignments" do
user_session(@student)
category = group_category
assignment = @course.assignments.create(group_category: category, submission_types: "online_upload")
group = category.groups.create(context: @course)
group.add_user(@student)
user_session(@student)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
asset_string: assignment.asset_string,
intent: "submit",
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].context).to eq group
expect(assigns[:attachment].folder).to be_for_submissions
end
it "does not require usage rights for group submissions to be visible to students" do
@course.usage_rights_required = true
@course.save!
user_session(@student)
category = group_category
assignment = @course.assignments.create(group_category: category, submission_types: "online_upload")
group = category.groups.create(context: @course)
group.add_user(@student)
user_session(@student)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
asset_string: assignment.asset_string,
intent: "submit",
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment]).not_to be_locked
end
context "sharding" do
specs_require_sharding
it "creates the attachment on the context's shard" do
local_storage!
@shard1.activate do
account = Account.create!
course_with_teacher_logged_in(active_all: true, account: account)
end
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].id).not_to be_nil
expect(assigns[:attachment].shard).to eq @shard1
json = json_parse
expect(json).not_to be_nil
expect(json["upload_url"]).not_to be_nil
expect(json["upload_params"]).not_to be_nil
expect(json["upload_params"]).not_to be_empty
end
it "creates the attachment on the user's shard when submitting" do
local_storage!
account = Account.create!
@shard1.activate do
@student = user_factory(active_user: true)
end
course_factory(active_all: true, account: account)
@course.enroll_user(@student, "StudentEnrollment").accept!
@assignment = @course.assignments.create!(title: "upload_assignment", submission_types: "online_upload")
user_session(@student)
post "create_pending", params: { attachment: {
context_code: @course.asset_string,
asset_string: @assignment.asset_string,
intent: "submit",
filename: "bob.txt"
} }
expect(response).to be_successful
expect(assigns[:attachment]).not_to be_nil
expect(assigns[:attachment].id).not_to be_nil
expect(assigns[:attachment].shard).to eq @shard1
json = json_parse
expect(json).not_to be_nil
expect(json["upload_url"]).not_to be_nil
expect(json["upload_params"]).not_to be_nil
expect(json["upload_params"]).not_to be_empty
end
end
end
describe "POST 'api_create'" do
before :once do
# this endpoint does not need a logged-in user or api token auth, it's
# based completely on the policy signature
pseudonym(@teacher)
@attachment = factory_with_protected_attributes(Attachment, context: @course, file_state: "deleted", workflow_state: "unattached", filename: "test.txt", content_type: "text")
end
before do
@content = Rack::Test::UploadedFile.new(File.join(RSpec.configuration.fixture_path, "courses.yml"), "")
request.env["CONTENT_TYPE"] = "multipart/form-data"
enable_forgery_protection
end
it "accepts the upload data if the policy and attachment are acceptable" do
local_storage!
params = @attachment.ajax_upload_params("", "")
post "api_create", params: params[:upload_params].merge(file: @content)
expect(response).to be_redirect
@attachment.reload
# the file is not available until the third api call is completed
expect(@attachment.file_state).to eq "deleted"
expect(@attachment.open.read).to eq File.read(File.join(RSpec.configuration.fixture_path, "courses.yml"))
end
it "opens up cors headers" do
params = @attachment.ajax_upload_params("", "")
request.headers["Origin"] = "http://canvas.docker"
post "api_create", params: params[:upload_params].merge(file: @content)
expect(response.header["Access-Control-Allow-Origin"]).to eq "http://canvas.docker"
end
it "has a preflight point for options requests (mostly safari)" do
process :api_create_success_cors, method: "OPTIONS", params: { id: "" }
expect(response.header["Access-Control-Allow-Headers"]).to eq("Origin, X-Requested-With, Content-Type, Accept, Authorization, Accept-Encoding")
end
it "rejects a blank policy" do
post "api_create", params: { file: @content }
assert_status(400)
end
it "rejects an expired policy" do
params = @attachment.ajax_upload_params("", "", expiration: -60.seconds)
post "api_create", params: params[:upload_params].merge({ file: @content })
assert_status(400)
end
it "rejects a modified policy" do
params = @attachment.ajax_upload_params("", "")
params[:upload_params]["Policy"] << "a"
post "api_create", params: params[:upload_params].merge({ file: @content })
assert_status(400)
end
it "rejects a good policy if the attachment data is already uploaded" do
params = @attachment.ajax_upload_params("", "")
@attachment.uploaded_data = @content
@attachment.save!
post "api_create", params: params[:upload_params].merge(file: @content)
assert_status(400)
end
it "forwards params[:success_include] to the api_create_success redirect as params[:include] if present" do
local_storage!
params = @attachment.ajax_upload_params("", "")
post "api_create", params: params[:upload_params].merge(file: @content, success_include: "foo")
expect(response).to be_redirect
expect(response.location).to include("include%5B%5D=foo") # include[]=foo, url encoded
end
it "adds 'include=avatar' to the api_create_success redirect for profile pictures" do
profile_pic = factory_with_protected_attributes(
Attachment,
user: @teacher,
context: @teacher,
folder: @teacher.profile_pics_folder,
file_state: "deleted",
workflow_state: "unattached",
filename: "profile.png",
content_type: "image/png"
)
local_storage!
params = profile_pic.ajax_upload_params("", "")
post "api_create", params: params[:upload_params].merge(file: @content)
expect(response).to be_redirect
expect(response.location).to include("include%5B%5D=avatar") # include[]=avatar, url encoded
end
end
describe "POST api_capture" do
before do
allow(InstFS).to receive(:enabled?).and_return(true)
allow(InstFS).to receive(:jwt_secrets).and_return(["jwt signing key"])
@token = Canvas::Security.create_jwt({}, nil, InstFS.jwt_secret)
end
it "rejects if InstFS integration is disabled" do
allow(InstFS).to receive(:enabled?).and_return(false)
post "api_capture", params: { id: 1 }
assert_status(404)
end
it "rejects if JWT is excluded or improperly formed" do
wrong_token = Canvas::Security.create_jwt({}, nil, "the wrong key")
post "api_capture", params: { id: 1, token: wrong_token }
assert_status(403)
end
it "rejects if required params aren't included" do
post "api_capture", params: { id: 1, user_id: 1, context_type: "Course", token: @token }
# `context_id` is excluded
assert_status(400)
end
context "with a course" do
let(:course) { Course.create }
let(:user) { User.create!(name: "me") }
let(:folder) { Folder.create!(name: "test", context: course) }
let(:params) do
{
id: 1,
user_id: user.id,
context_type: "Course",
context_id: course.id,
token: @token,
name: "test.txt",
size: 42,
content_type: "text/plain",
instfs_uuid: 1,
folder_id: folder.id,
}
end
it "creates a new attachment" do
post "api_capture", params: params
assert_status(201)
expect(folder.attachments.first).not_to be_nil
end
it "populates the md5 column with the instfs sha512" do
post "api_capture", params: params.merge(sha512: "deadbeef")
assert_status(201)
expect(folder.attachments.first.md5).to eq "deadbeef"
end
it "includes the attachment json in the response" do
post "api_capture", params: params
assert_status(201)
attachment = folder.attachments.first
data = json_parse
expect(data["id"]).to eql attachment.id
expect(data["filename"]).to eql "test.txt"
expect(data["url"]).not_to be_nil
end
it "works with a ContentMigration as the context" do
migration = course.content_migrations.create!
request_params = params.merge(
context_id: migration.id,
context_type: "ContentMigration"
)
post "api_capture", params: request_params
assert_status(201)
end
it "works with a Quizzes::QuizSubmission as the context" do
quiz = course.quizzes.create!
submission = quiz.quiz_submissions.create!(user: user)
request_params = params.merge(
context_type: "Quizzes::QuizSubmission",
context_id: submission.id
)
post "api_capture", params: request_params
assert_status(201)
end
context "with Submission, Assignment, and Progress" do
let(:assignment) { course.assignments.create! }
let(:submission) { assignment.submissions.create!(user: @student) }
let(:assignment_params) do
params.merge(
context_type: "Assignment",
context_id: assignment.id
)
end
let(:attachment) do
Attachment.create!(
context: assignment,
user: @student,
filename: "cats.jpg",
uploaded_data: StringIO.new("meow?")
)
end
let(:progress) do
::Progress
.new(context: assignment, user: user, tag: :test)
.tap(&:start)
.tap(&:save!)
end
let!(:homework_service) { Services::SubmitHomeworkService.new(attachment, progress) }
before do
allow(Mailer).to receive(:deliver)
allow(Services::SubmitHomeworkService).to(receive(:new)).and_return(homework_service)
end
it "works with an Assignment as the context" do
post "api_capture", params: assignment_params
assert_status(201)
end
context "with progress_id param" do
let(:progress_params) do
assignment_params.merge(
progress_id: progress.id
)
end
let(:request) do
post "api_capture", params: progress_params
progress.reload
end
it "completes the Progress object" do
request
expect(progress).to be_completed
end
it "sets the attachment id in the Progress#results" do
request
expect(progress.results["id"]).not_to be_nil
end
it "returns a 201 http status" do
request
assert_status(201)
end
it "does not submit the attachment" do
expect(homework_service).not_to receive(:submit)
request
end
end
context "with Progress tagged as :upload_via_url" do
let(:progress) do
::Progress
.new(context: assignment, user: user, tag: :upload_via_url)
.tap(&:start)
.tap(&:save!)
end
let(:progress_params) do
assignment_params.merge(
progress_id: progress.id,
comment: comment,
eula_agreement_timestamp: eula_agreement_timestamp
)
end
let(:eula_agreement_timestamp) { "1522419910" }
let(:comment) { "my assignment comment" }
let(:request) { post "api_capture", params: progress_params }
before do
allow(homework_service).to receive(:queue_email)
end
it "submits the attachment if the submit_assignment flag is not provided" do
expect(homework_service).to receive(:submit).with(eula_agreement_timestamp, comment)
request
end
it "submits the attachment if the submit_assignment param is set to true" do
expect(homework_service).to receive(:submit).with(eula_agreement_timestamp, comment)
post "api_capture", params: progress_params.merge(submit_assignment: true)
end
it "does not submit the attachment if the submit_assignment param is set to false" do
expect(homework_service).not_to receive(:submit)
post "api_capture", params: progress_params.merge(submit_assignment: false)
end
it "saves the eula_agreement_timestamp" do
request
submission = Submission.where(assignment_id: assignment.id)
expect(submission.first.turnitin_data[:eula_agreement_timestamp]).to eq(eula_agreement_timestamp)
end
it "saves the comment" do
request
submission = Submission.where(assignment_id: assignment.id)
expect(submission.first.submission_comments.first.comment).to eq(comment)
end
it "returns a 201 http status" do
request
assert_status(201)
end
it "marks the progress as completed" do
request
expect(progress.reload.workflow_state).to eq "completed"
end
it "sends a failure email" do
expect(homework_service).to receive(:submit).and_raise("error")
expect(homework_service).to receive(:failure_email)
request
expect(progress.reload.workflow_state).to eq "failed"
end
end
end
end
context "sharding" do
specs_require_sharding
it "creates the attachment on the context's shard" do
user = @shard1.activate { User.create!(name: "me") }
post "api_capture", params: {
user_id: user.global_id,
context_type: "User",
context_id: user.global_id,
token: @token,
name: "test.txt",
size: 42,
content_type: "text/plain",
instfs_uuid: 1,
folder_id: user.profile_pics_folder.global_id,
}
assert_status(201)
attachment = assigns[:attachment]
expect(attachment).not_to be_nil
expect(attachment.shard).to eq @shard1
end
end
end
describe "public_url" do
before :once do
assignment_model course: @course, submission_types: %w[online_upload]
attachment_model context: @student
@submission = @assignment.submit_homework @student, attachments: [@attachment]
end
context "with direct rights" do
before do
user_session @student
end
it "gives a download url" do
get "public_url", params: { id: @attachment.id }
expect(response).to be_successful
data = json_parse
expect(data).to eq({ "public_url" => @attachment.public_url(secure: false) })
end
end
context "without direct rights" do
before do
user_session @teacher
end
it "fails if no submission_id is given" do
get "public_url", params: { id: @attachment.id }
assert_unauthorized
end
it "allows a teacher to download a student's submission" do
get "public_url", params: { id: @attachment.id, submission_id: @submission.id }
expect(response).to be_successful
data = json_parse
expect(data).to eq({ "public_url" => @attachment.public_url(secure: false) })
end
it "verifies that the requested file belongs to the submission" do
otherfile = attachment_model
get "public_url", params: { id: otherfile, submission_id: @submission.id }
assert_unauthorized
end
it "allows downloading an attachment to a previous version" do
old_file = @attachment
new_file = attachment_model(context: @student)
@assignment.submit_homework @student, attachments: [new_file]
get "public_url", params: { id: old_file.id, submission_id: @submission.id }
expect(response).to be_successful
data = json_parse
expect(data).to eq({ "public_url" => old_file.public_url(secure: false) })
end
end
end
describe "GET 'image_thumbnail'" do
let(:image) { factory_with_protected_attributes(@teacher.attachments, uploaded_data: stub_png_data, instfs_uuid: "1234") }
it "returns default 'no_pic' thumbnail if attachment not found" do
user_session @teacher
get "image_thumbnail", params: { uuid: "bad uuid", id: "bad id" }
expect(response).to be_redirect
end
it "returns the same jwt if requested twice" do
enable_cache do
user_session @teacher
locations = Array.new(2) do
get("image_thumbnail", params: { uuid: image.uuid, id: image.id }).location
end
expect(locations[0]).to eq(locations[1])
end
end
it "returns the different jwts if no_cache is passed" do
enable_cache do
user_session @teacher
locations = Array.new(2) do
get("image_thumbnail", params: { uuid: image.uuid, id: image.id, no_cache: true }).location
end
expect(locations[0]).not_to eq(locations[1])
end
end
end
end