canvas-lms/spec/models/attachment_spec.rb

2221 lines
86 KiB
Ruby

# coding: utf-8
#
# 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 File.expand_path(File.dirname(__FILE__) + '/../sharding_spec_helper.rb')
describe Attachment do
context "validation" do
it "should create a new instance given valid attributes" do
attachment_model
end
it "should require a context" do
expect{attachment_model(:context => nil)}.to raise_error(ActiveRecord::RecordInvalid, /Context/)
end
end
context "default_values" do
before :once do
@course = course_model
end
it "should set the display name to the filename if it is nil" do
attachment_model(:display_name => nil)
expect(@attachment.display_name).to eql(@attachment.filename)
end
end
context "public_url" do
before :each do
local_storage!
end
before :once do
course_model
end
it "should return http as the protocol by default" do
attachment_with_context(@course)
expect(@attachment.public_url).to match(/^http:\/\//)
end
it "should return the protocol if specified" do
attachment_with_context(@course)
expect(@attachment.public_url(:secure => true)).to match(/^https:\/\//)
end
context "for a quiz submission upload" do
it "should return a routable url", :type => :routing do
quiz = @course.quizzes.create
submission = Quizzes::SubmissionManager.new(quiz).find_or_create_submission(user_model)
attachment = attachment_with_context(submission)
expect(get(attachment.public_url)).to be_routable
end
end
end
context "public_url InstFS storage" do
before :once do
user_model
end
before :each do
attachment_with_context(@user)
@attachment.instfs_uuid = 1
allow(InstFS).to receive(:enabled?).and_return true
allow(InstFS).to receive(:authenticated_url)
end
it "should get url from InstFS when attachment has instfs_uuid" do
@attachment.public_url
expect(InstFS).to have_received(:authenticated_url)
end
it "should still get url from InstFS when attachment has instfs_uuid and instfs is later disabled" do
allow(InstFS).to receive(:enabled?).and_return false
@attachment.public_url
expect(InstFS).to have_received(:authenticated_url)
end
it "should not get url from InstFS when instfs is enabled but attachment lacks instfs_uuid" do
@attachment.instfs_uuid = nil
@attachment.public_url
expect(InstFS).not_to have_received(:authenticated_url)
end
end
context "public_url s3_storage" do
before :each do
s3_storage!
end
it "should give back a signed s3 url" do
a = attachment_model
s3object = a.s3object
expect(a.public_url(expires_in: 1.day)).to match(/^https:\/\//)
a.destroy_permanently!
end
end
def configure_crocodoc
PluginSetting.create! :name => 'crocodoc',
:settings => { :api_key => "blahblahblahblahblah" }
allow_any_instance_of(Crocodoc::API).to receive(:upload).and_return 'uuid' => '1234567890'
end
def configure_canvadocs(opts = {})
ps = PluginSetting.where(name: "canvadocs").first_or_create
ps.update_attribute :settings, {
"api_key" => "blahblahblahblahblah",
"base_url" => "http://example.com",
"annotations_supported" => true
}.merge(opts)
end
context "crocodoc" do
include HmacHelper
let_once(:user) { user_model }
let_once(:course) { course_model }
let_once(:student) do
course.enroll_student(user_model).accept
@user
end
before { configure_crocodoc }
it "crocodocable?" do
crocodocable_attachment_model
expect(@attachment).to be_crocodocable
end
it "should include a whitelist of moderated_grading_whitelist in the url blob" do
crocodocable_attachment_model
moderated_grading_whitelist = [user, student].map { |u| u.moderated_grading_ids(true) }
@attachment.submit_to_crocodoc
url_opts = {
moderated_grading_whitelist: moderated_grading_whitelist
}
url = Rack::Utils.parse_nested_query(@attachment.crocodoc_url(user, url_opts).sub(/^.*\?{1}/, ""))
blob = extract_blob(url["hmac"], url["blob"],
"user_id" => user.id,
"type" => "crocodoc")
expect(blob["moderated_grading_whitelist"]).to include(user.moderated_grading_ids.as_json)
expect(blob["moderated_grading_whitelist"]).to include(student.moderated_grading_ids.as_json)
end
it "should always enable annotations when creating a crocodoc url" do
crocodocable_attachment_model
@attachment.submit_to_crocodoc
url = Rack::Utils.parse_nested_query(@attachment.crocodoc_url(user, {}).sub(/^.*\?{1}/, ""))
blob = extract_blob(url["hmac"], url["blob"],
"user_id" => user.id,
"type" => "crocodoc")
expect(blob["enable_annotations"]).to be(true)
end
it "should not modify the options reference given to create a crocodoc url" do
crocodocable_attachment_model
@attachment.submit_to_crocodoc
url_opts = {}
@attachment.crocodoc_url(user, url_opts)
expect(url_opts).to eql({})
end
it "should submit to crocodoc" do
crocodocable_attachment_model
expect(@attachment.crocodoc_available?).to be_falsey
@attachment.submit_to_crocodoc
expect(@attachment.crocodoc_available?).to be_truthy
expect(@attachment.crocodoc_document.uuid).to eq '1234567890'
end
it "should spawn delayed jobs to retry failed uploads" do
allow_any_instance_of(Crocodoc::API).to receive(:upload).and_return 'error' => 'blah'
crocodocable_attachment_model
attempts = 3
Setting.set('max_crocodoc_attempts', attempts)
track_jobs do
# first attempt
@attachment.submit_to_crocodoc
time = Time.now
# nth attempt won't create more jobs
attempts.times {
time += 1.hour
Timecop.freeze(time) do
run_jobs
end
}
end
expect(created_jobs.size).to eq attempts
end
it "should submit to canvadocs if crocodoc fails to convert" do
crocodocable_attachment_model
@attachment.submit_to_crocodoc
allow_any_instance_of(Crocodoc::API).to receive(:status).and_return [
{'uuid' => '1234567890', 'status' => 'ERROR'}
]
allow(Canvadocs).to receive(:enabled?).and_return true
expects_job_with_tag('Attachment.submit_to_canvadocs') {
CrocodocDocument.update_process_states
}
end
end
context "canvadocs" do
before :once do
configure_canvadocs
end
before :each do
allow_any_instance_of(Canvadocs::API).to receive(:upload).and_return "id" => 1234
end
it "should treat text files equally" do
a = attachment_model(:content_type => 'text/x-ruby-script')
allow(Canvadoc).to receive(:mime_types).and_return(['text/plain'])
expect(a.canvadocable?).to be_truthy
end
describe "submit_to_canvadocs" do
it "submits canvadocable documents" do
a = canvadocable_attachment_model
a.submit_to_canvadocs
expect(a.canvadoc.document_id).not_to be_nil
end
it "works from the bulk uploader" do
a1 = canvadocable_attachment_model
Attachment.submit_to_canvadocs([a1.id])
expect(a1.canvadoc.document_id).not_to be_nil
end
it "doesn't submit non-canvadocable documents" do
a = attachment_model
a.submit_to_canvadocs
expect(a.canvadoc).to be_nil
end
it "tries again later when upload fails" do
allow_any_instance_of(Canvadocs::API).to receive(:upload).and_return(nil)
expects_job_with_tag('Attachment#submit_to_canvadocs') {
canvadocable_attachment_model.submit_to_canvadocs
}
end
it "sends annotatable documents to canvadocs if supported" do
configure_crocodoc
a = crocodocable_attachment_model
a.submit_to_canvadocs 1, wants_annotation: true
expect(a.canvadoc).not_to be_nil
end
it "prefers crocodoc when annotation is requested and canvadocs can't annotate" do
configure_crocodoc
configure_canvadocs "annotations_supported" => false
Setting.set('canvadoc_mime_types',
(Canvadoc.mime_types << "application/blah").to_json)
crocodocable = crocodocable_attachment_model
canvadocable = canvadocable_attachment_model content_type: "application/blah"
crocodocable.submit_to_canvadocs 1, wants_annotation: true
run_jobs
expect(crocodocable.canvadoc).to be_nil
expect(crocodocable.crocodoc_document).not_to be_nil
canvadocable.submit_to_canvadocs 1, wants_annotation: true
expect(canvadocable.canvadoc).not_to be_nil
expect(canvadocable.crocodoc_document).to be_nil
end
end
end
it "should set the uuid" do
attachment_model
expect(@attachment.uuid).not_to be_nil
end
context "workflow" do
before :once do
attachment_model
end
it "should default to pending_upload" do
expect(@attachment.state).to eql(:pending_upload)
end
it "should be able to take a processing object and complete its process" do
attachment_model(:workflow_state => 'processing')
@attachment.process!
expect(@attachment.state).to eql(:processed)
end
it "should be able to take a new object and bypass upload with process" do
@attachment.process!
expect(@attachment.state).to eql(:processed)
end
it "should be able to recycle a processed object and re-upload it" do
attachment_model(:workflow_state => 'processed')
@attachment.recycle
expect(@attachment.state).to eql(:pending_upload)
end
end
context "named scopes" do
context "by_content_types" do
before :once do
course_model
@gif = attachment_model :context => @course, :content_type => 'image/gif'
@jpg = attachment_model :context => @course, :content_type => 'image/jpeg'
@weird = attachment_model :context => @course, :content_type => "%/what's this"
end
it "should match type" do
expect(@course.attachments.by_content_types(['image']).pluck(:id).sort).to eq [@gif.id, @jpg.id].sort
end
it "should match type/subtype" do
expect(@course.attachments.by_content_types(['image/gif']).pluck(:id)).to eq [@gif.id]
expect(@course.attachments.by_content_types(['image/gif', 'image/jpeg']).pluck(:id).sort).to eq [@gif.id, @jpg.id].sort
end
it "should escape sql and wildcards" do
expect(@course.attachments.by_content_types(['%']).pluck(:id)).to eq [@weird.id]
expect(@course.attachments.by_content_types(["%/what's this"]).pluck(:id)).to eq [@weird.id]
expect(@course.attachments.by_content_types(["%/%"]).pluck(:id)).to eq []
end
end
context "by_exclude_content_types" do
before :once do
course_model
@gif = attachment_model :context => @course, :content_type => 'image/gif'
@jpg = attachment_model :context => @course, :content_type => 'image/jpeg'
@txt = attachment_model :context => @course, :content_type => 'text/plain'
@pdf = attachment_model :context => @course, :content_type => 'application/pdf'
end
it "should match type" do
expect(@course.attachments.by_exclude_content_types(['image']).pluck(:id).sort).to eq [@txt.id, @pdf.id].sort
end
it "should match type/subtype" do
expect(@course.attachments.by_exclude_content_types(['image/gif']).pluck(:id).sort).to eq [@jpg.id, @txt.id, @pdf.id].sort
expect(@course.attachments.by_exclude_content_types(['image/gif', 'image/jpeg']).pluck(:id).sort).to eq [@txt.id, @pdf.id].sort
end
it "should escape sql and wildcards" do
@weird = attachment_model :context => @course, :content_type => "%/what's this"
expect(@course.attachments.by_exclude_content_types(['%']).pluck(:id).sort).to eq [@gif.id, @jpg.id, @txt.id, @pdf.id].sort
expect(@course.attachments.by_exclude_content_types(["%/what's this"]).pluck(:id).sort).to eq [@gif.id, @jpg.id, @txt.id, @pdf.id].sort
expect(@course.attachments.by_exclude_content_types(["%/%"]).pluck(:id).sort).to eq [@gif.id, @jpg.id, @txt.id, @pdf.id, @weird.id].sort
end
end
end
context "uploaded_data" do
it "should create with uploaded_data" do
a = attachment_model(:uploaded_data => default_uploaded_data)
expect(a.filename).to eql("doc.doc")
end
context "uploading and db transactions" do
before :once do
attachment_model(:context => Account.default.groups.create!, :filename => 'test.mp4', :content_type => 'video')
end
it "should delay upload until the #save transaction is committed" do
allow(Rails.env).to receive(:test?).and_return(false)
@attachment.uploaded_data = default_uploaded_data
expect(Attachment.connection).to receive(:after_transaction_commit).twice
expect(@attachment).to receive(:touch_context_if_appropriate).never
expect(@attachment).to receive(:ensure_media_object).never
@attachment.save
end
end
end
context "ensure_media_object" do
before :once do
@course = course_factory
@attachment = @course.attachments.build(:filename => 'foo.mp4')
@attachment.content_type = 'video'
end
it "should be called automatically upon creation" do
expect(@attachment).to receive(:ensure_media_object).once
@attachment.save!
end
it "should create a media object for videos" do
@attachment.update_attribute(:media_entry_id, 'maybe')
expect(@attachment).to receive(:build_media_object).once.and_return(true)
@attachment.save!
end
it "should delay the creation of the media object by attachment_build_media_object_delay_seconds" do
now = Time.now
allow(Time).to receive(:now).and_return(now)
allow(Setting).to receive(:get).and_return(nil)
expect(Setting).to receive(:get).with('attachment_build_media_object_delay_seconds', '10').once.and_return('25')
track_jobs do
@attachment.save!
end
expect(MediaObject.count).to eq 0
job = created_jobs.first
expect(job.tag).to eq 'MediaObject.add_media_files'
expect(job.run_at.to_i).to eq (now + 25.seconds).to_i
end
it "should not create a media object in a skip_media_object_creation block" do
Attachment.skip_media_object_creation do
expect(@attachment).to receive(:build_media_object).never
@attachment.save!
end
end
it "should not create a media object for images" do
@attachment.filename = 'foo.png'
@attachment.content_type = 'image/png'
expect(@attachment).to receive(:ensure_media_object).once
expect(@attachment).to receive(:build_media_object).never
@attachment.save!
end
it "should create a media object *after* a direct-to-s3 upload" do
allowed = false
expect(@attachment).to receive(:build_media_object) do
raise "not allowed" unless allowed
end
@attachment.workflow_state = 'unattached'
@attachment.file_state = 'deleted'
@attachment.save!
allowed = true
@attachment.workflow_state = nil
@attachment.file_state = 'available'
@attachment.save!
end
it "should disassociate but not delete the associated media object" do
@attachment.media_entry_id = '0_feedbeef'
@attachment.save!
media_object = @course.media_objects.build :media_id => '0_feedbeef'
media_object.attachment_id = @attachment.id
media_object.save!
@attachment.destroy
media_object.reload
expect(media_object).not_to be_deleted
expect(media_object.attachment_id).to be_nil
end
end
context "destroy" do
it "should not actually destroy" do
a = attachment_model(:uploaded_data => default_uploaded_data)
expect(a.filename).to eql("doc.doc")
a.destroy
expect(a).not_to be_frozen
expect(a).to be_deleted
end
it "should not probably be possible to actually destroy... somehow" do
a = attachment_model(:uploaded_data => default_uploaded_data)
expect(a.filename).to eql("doc.doc")
a.destroy
expect(a).not_to be_frozen
expect(a).to be_deleted
a.destroy_permanently!
expect(a).to be_frozen
end
it "should not show up in the context list after being destroyed" do
@course = course_factory
expect(@course).not_to be_nil
a = attachment_model(:uploaded_data => default_uploaded_data, :context => @course)
expect(a.filename).to eql("doc.doc")
expect(a.context).to eql(@course)
a.destroy
expect(a).not_to be_frozen
expect(a).to be_deleted
expect(@course.attachments).to be_include(a)
expect(@course.attachments.active).not_to be_include(a)
end
it "should still destroy without error if file data is lost" do
a = attachment_model(:uploaded_data => default_uploaded_data)
allow(a).to receive(:downloadable?).and_return(false)
a.destroy
expect(a).to be_deleted
end
it "should replace uploaded data on destroy_content_and_replace" do
a = attachment_model(uploaded_data: default_uploaded_data)
expect(a.content_type).to eq 'application/msword'
a.destroy_content_and_replace
expect(a.content_type).to eq 'application/pdf'
end
it "should also destroy thumbnails" do
a = attachment_model(uploaded_data: stub_png_data, content_type: 'image/png')
thumb = a.thumbnail
expect(thumb).not_to be_nil
expect(thumb).to receive(:destroy).once
a.destroy_content_and_replace
end
it "should destroy content and record on destroy_permanently_plus" do
a = attachment_model
a2 = attachment_model(root_attachment: a)
expect(a).to receive(:make_childless).once
expect(a).to receive(:destroy_content).once
expect(a2).to receive(:make_childless).never
expect(a2).to receive(:destroy_content).never
a2.destroy_permanently_plus
a.destroy_permanently_plus
expect { a.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { a2.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'should not delete s3objects if it is not production for destroy_content' do
allow(ApplicationController).to receive(:test_cluster?).and_return(true)
s3_storage!
a = attachment_model
allow(a).to receive(:s3object).and_return(double('s3object'))
s3object = a.s3object
expect(s3object).to receive(:delete).never
a.destroy_content
end
it 'should allow destroy_content_and_replace when s3object is already deleted' do
s3_storage!
a = attachment_model(uploaded_data: default_uploaded_data)
a.s3object.delete
a.destroy_content_and_replace
expect(Purgatory.where(attachment_id: a.id).exists?).to be_truthy
end
it 'should not do destroy_content_and_replace twice' do
a = attachment_model(uploaded_data: default_uploaded_data)
a.destroy_content_and_replace # works
expect(a).to receive(:send_to_purgatory).never
a.destroy_content_and_replace # returns because it already happened
end
it 'should destroy all crocodocs even from children attachments' do
local_storage!
configure_crocodoc
a = crocodocable_attachment_model(uploaded_data: default_uploaded_data)
a2 = attachment_model(root_attachment: a)
a2.submit_to_canvadocs 1, wants_annotation: true
a.submit_to_canvadocs 1, wants_annotation: true
run_jobs
expect(a.crocodoc_document).not_to be_nil
expect(a2.crocodoc_document).not_to be_nil
a.destroy_content_and_replace
expect(a.reload.crocodoc_document).to be_nil
expect(a2.reload.crocodoc_document).to be_nil
end
it 'should allow destroy_content_and_replace on children attachments' do
a = attachment_model(uploaded_data: default_uploaded_data)
a2 = attachment_model(root_attachment: a)
a2.destroy_content_and_replace
purgatory = Purgatory.where(attachment_id: [a.id, a2.id])
expect(purgatory.count).to eq 1
expect(purgatory.take.attachment_id).to eq a.id
end
context "inst-fs" do
before :each do
allow(InstFS).to receive(:enabled?).and_return(true)
allow(InstFS).to receive(:app_host).and_return("https://somehost.example")
end
it "should only upload the replacement file to inst-fs once" do
instfs_uuid = "1234-abcd"
expect(InstFS).to receive(:direct_upload).
with(hash_including(file_name: File.basename(Attachment.file_removed_path))).
and_return(instfs_uuid).exactly(1).times
2.times do
expect(Attachment.file_removed_base_instfs_uuid).to eq instfs_uuid
end
end
it "should set the instfs_uuid to a duplicate of the replacement file" do
base_uuid = "base-id"
allow(Attachment).to receive(:file_removed_base_instfs_uuid).and_return(base_uuid)
dup_uuid = "duplicate-id"
expect(InstFS).to receive(:duplicate_file).with(base_uuid).and_return(dup_uuid)
att = attachment_model(instfs_uuid: "old-id")
expect(att).to receive(:send_to_purgatory) # stub these out for now - test separately
expect(att).to receive(:destroy_content)
att.destroy_content_and_replace
expect(att.instfs_uuid).to eq dup_uuid
end
it "should actually destroy the content" do
uuid = "old-id"
att = attachment_model(instfs_uuid: uuid)
expect(InstFS).to receive(:delete_file).with(uuid)
att.destroy_content
end
it "should duplicate the file for purgatory and restore from there" do
old_uuid = "old-id"
att = attachment_model(instfs_uuid: old_uuid)
purgatory_uuid = "purgatory-id"
expect(InstFS).to receive(:duplicate_file).with(old_uuid).and_return(purgatory_uuid)
purgatory = att.send_to_purgatory
expect(purgatory.new_instfs_uuid).to eq purgatory_uuid
att.resurrect_from_purgatory
expect(att.instfs_uuid).to eq purgatory_uuid
end
end
shared_examples_for "purgatory" do
it 'should save file in purgatory and then restore and back again' do
a = attachment_model(uploaded_data: default_uploaded_data)
old_filename = a.filename
old_content_type = a.content_type
a.destroy_content_and_replace
purgatory = Purgatory.where(attachment_id: a).take
expect(purgatory.old_filename).to eq old_filename
expect(purgatory.old_display_name).to eq old_filename
expect(purgatory.old_content_type).to eq old_content_type
a.reload
expect(a.filename).to eq 'file_removed.pdf'
expect(a.display_name).to eq 'file_removed.pdf'
a.resurrect_from_purgatory
a.reload
expect(a.filename).to eq old_filename
expect(a.display_name).to eq old_filename
expect(a.content_type).to eq old_content_type
expect(purgatory.reload.workflow_state).to eq 'restored'
a.destroy_content_and_replace
expect(purgatory.reload.workflow_state).to eq 'active'
end
end
context "s3" do
include_examples "purgatory"
before { s3_storage! }
end
context "s3" do
include_examples "purgatory"
before { local_storage! }
end
end
context "restore" do
it "should restore to 'available' state" do
a = attachment_model(:uploaded_data => default_uploaded_data)
a.destroy
expect(a).to be_deleted
a.restore
expect(a).to be_available
end
end
context "destroy_permanently!" do
it "should not delete the s3 object, even here" do
s3_storage!
a = attachment_model
s3object = a.s3object
expect(s3object).to receive(:delete).never
a.destroy_permanently!
end
end
context "inferred display name" do
before do
s3_storage! # because we don't 'sanitize' filenames with the local backend
end
it "should take a normal filename and use it as a diplay name" do
a = attachment_model(:filename => 'normal_name.ppt')
expect(a.display_name).to eql('normal_name.ppt')
expect(a.filename).to eql('normal_name.ppt')
end
it "should preserve case" do
a = attachment_model(:filename => 'Normal_naMe.ppt')
expect(a.display_name).to eql('Normal_naMe.ppt')
expect(a.filename).to eql('Normal_naMe.ppt')
end
it "should truncate filenames to 255 characters (preserving extension)" do
a = attachment_model(:filename => 'My new study guide or case study on this evolution on monkeys even in that land of costa rica somewhere my own point of view going along with the field experiment I would say or try out is to put them not in wet areas like costa rico but try and put it so its not so long.docx')
expect(a.display_name).to eql("My new study guide or case study on this evolution on monkeys even in that land of costa rica somewhere my own point of view going along with the field experiment I would say or try out is to put them not in wet areas like costa rico but try and put.docx")
expect(a.filename).to eql("My+new+study+guide+or+case+study+on+this+evolution+on+monkeys+even+in+that+land+of+costa+rica+somewhere+my+own+point+of++view+going+along+with+the+field+experiment+I+would+say+or+try+out+is+to+put+them+not+in+wet+areas+like+costa+rico+but+try+and+put.docx")
end
it "should use no more than half of the 255 characters for the extension" do
a = attachment_model(:filename => ("A" * 150) + "." + ("B" * 150))
expect(a.display_name).to eql(("A" * 127) + "." + ("B" * 127))
expect(a.filename).to eql(("A" * 127) + "." + ("B" * 127))
end
it "should not split unicode characters when truncating" do
a = attachment_model(:filename => "\u2603" * 300)
expect(a.display_name).to eql("\u2603" * 255)
expect(a.filename.length).to eql(252)
expect(a.unencoded_filename).to be_valid_encoding
expect(a.unencoded_filename).to eql("\u2603" * 28)
end
it "should truncate thumbnail names" do
a = attachment_model(:filename => "#{"a" * 251}.png")
thumbname = a.thumbnail_name_for("thumb")
expect(thumbname.length).to eq 255
expect(thumbname).to eq "#{"a" * 245}_thumb.png"
end
it "should not double-escape a root attachment's filename" do
a = attachment_model(:filename => 'something with spaces.txt')
expect(a.filename).to eq 'something+with+spaces.txt'
a2 = Attachment.new
a2.root_attachment = a
expect(a2.sanitize_filename(nil)).to eq a.filename
end
end
context "clone_for" do
it "should clone to another context" do
a = attachment_model(:filename => "blech.ppt")
course_factory
new_a = a.clone_for(@course)
expect(new_a.context).not_to eql(a.context)
expect(new_a.filename).to eql(a.filename)
expect(new_a.read_attribute(:filename)).to be_nil
expect(new_a.root_attachment_id).to eql(a.id)
end
it "should clone to another root_account" do
c = course_factory
a = attachment_model(filename: "blech.ppt", context: c)
new_account = Account.create
c2 = course_factory(account: new_account)
allow(Attachment).to receive(:s3_storage?).and_return(true)
expect_any_instance_of(Attachment).to receive(:make_rootless).once
expect_any_instance_of(Attachment).to receive(:change_namespace).once
a2 = a.clone_for(c2)
end
it "should create thumbnails for images on clone" do
c = course_factory
a = attachment_model(filename: "blech.jpg", context: c, content_type: 'image/jpg')
new_account = Account.create
c2 = course_factory(account: new_account)
allow(Attachment).to receive(:s3_storage?).and_return(true)
expect_any_instance_of(Attachment).to receive(:copy_attachment_content).once
expect_any_instance_of(Attachment).to receive(:change_namespace).once
expect_any_instance_of(Attachment).to receive(:create_thumbnail_size).once
a2 = a.clone_for(c2)
end
it "should link the thumbnail" do
a = attachment_model(:uploaded_data => stub_png_data, :content_type => 'image/png')
expect(a.thumbnail).not_to be_nil
course_factory
new_a = a.clone_for(@course)
expect(new_a.thumbnail).not_to be_nil
expect(new_a.thumbnail_url).not_to be_nil
expect(new_a.thumbnail_url).to eq a.thumbnail_url
end
it "should not create root_attachment_id cycles or self-references" do
a = attachment_model(:uploaded_data => stub_png_data, :content_type => 'image/png')
expect(a.root_attachment_id).to be_nil
coursea = @course
@context = courseb = course_factory
b = a.clone_for(courseb, nil, :overwrite => true)
b.save
expect(b.context).to eq courseb
expect(b.root_attachment).to eq a
new_a = b.clone_for(coursea, nil, :overwrite => true)
expect(new_a).to eq a
expect(new_a.root_attachment_id).to be_nil
new_b = new_a.clone_for(courseb, nil, :overwrite => true)
expect(new_b.root_attachment_id).to eq a.id
new_b = b.clone_for(courseb, nil, :overwrite => true)
expect(new_b.root_attachment_id).to eq a.id
@context = coursec = course_factory
c = b.clone_for(coursec, nil, :overwrite => true)
expect(c.root_attachment).to eq a
new_a = c.clone_for(coursea, nil, :overwrite => true)
expect(new_a).to eq a
expect(new_a.root_attachment_id).to be_nil
# pretend b's content changed so it got disconnected
b.update_attribute(:root_attachment_id, nil)
new_b = b.clone_for(courseb, nil, :overwrite => true)
expect(new_b.root_attachment_id).to be_nil
end
it "should set correct namespace across clones" do
s3_storage!
a = attachment_model
expect(a.root_attachment_id).to be_nil
coursea = @course
@context = courseb = course_factory(account: Account.create)
b = a.clone_for(courseb, nil, overwrite: true)
expect(b.id).not_to be_nil
expect(b.filename).to eq a.filename
b.save
expect(b.root_attachment_id).to eq nil
expect(b.namespace).to eq courseb.root_account.file_namespace
new_a = b.clone_for(coursea, nil, overwrite: true)
new_a.save
expect(new_a).to eq a
expect(new_a.namespace).to eq coursea.root_account.file_namespace
end
end
context "adheres_to_policy" do
let_once(:user) { user_model }
let_once(:course) do
course_model
@course.offer
@course.update_attribute(:is_public, false)
@course
end
let_once(:student) do
course.enroll_student(user_model).accept
@user
end
let_once(:attachment) do
attachment_model(context: course)
end
it "should not allow unauthorized users to read files" do
a = attachment_model(context: course_model)
@course.update_attribute(:is_public, false)
expect(a.grants_right?(user, :read)).to eql(false)
end
it "should allow anonymous access for public contexts" do
a = attachment_model(context: course_model)
@course.update_attribute(:is_public, true)
expect(a.grants_right?(user, :read)).to eql(false)
end
it "should allow students to read files" do
a = attachment
a.reload
expect(a.grants_right?(student, :read)).to eql(true)
end
it "should allow students to download files" do
a = attachment
a.reload
expect(a.grants_right?(student, :download)).to eql(true)
end
it "should allow students to read (but not download) locked files" do
a = attachment
a.update_attribute(:locked, true)
a.reload
expect(a.grants_right?(student, :read)).to eql(true)
expect(a.grants_right?(student, :download)).to eql(false)
end
it "should allow user access based on 'file_access_user_id' and 'file_access_expiration' in the session" do
a = attachment
expect(a.grants_right?(nil, :read)).to eql(false)
expect(a.grants_right?(nil, :download)).to eql(false)
mock_session = {
'file_access_user_id' => student.id,
'file_access_expiration' => 1.hour.from_now.to_i,
'permissions_key' => SecureRandom.uuid
}.with_indifferent_access
expect(a.grants_right?(nil, mock_session, :read)).to eql(true)
expect(a.grants_right?(nil, mock_session, :download)).to eql(true)
end
it "should correctly deny user access based on 'file_access_user_id'" do
a = attachment_model(context: user)
other_user = user_model
mock_session = {
'file_access_user_id' => other_user.id,
'file_access_expiration' => 1.hour.from_now.to_i,
'permissions_key' => SecureRandom.uuid
}.with_indifferent_access
expect(a.grants_right?(nil, mock_session, :read)).to eql(false)
expect(a.grants_right?(nil, mock_session, :download)).to eql(false)
end
it "should allow user access to anyone if the course is public to auth users (with 'file_access_user_id' and 'file_access_expiration' in the session)" do
mock_session = {
'file_access_user_id' => user.id,
'file_access_expiration' => 1.hour.from_now.to_i,
'permissions_key' => SecureRandom.uuid
}.with_indifferent_access
a = attachment_model(context: course)
expect(a.grants_right?(nil, mock_session, :read)).to eql(false)
expect(a.grants_right?(nil, mock_session, :download)).to eql(false)
course.is_public_to_auth_users = true
course.save!
a.reload
AdheresToPolicy::Cache.clear
expect(a.grants_right?(nil, :read)).to eql(false)
expect(a.grants_right?(nil, :download)).to eql(false)
expect(a.grants_right?(nil, mock_session, :read)).to eql(true)
expect(a.grants_right?(nil, mock_session, :download)).to eql(true)
end
it "should not allow user access based on incorrect 'file_access_user_id' in the session" do
a = attachment
expect(a.grants_right?(nil, :read)).to eql(false)
expect(a.grants_right?(nil, :download)).to eql(false)
expect(a.grants_right?(nil, {'file_access_user_id' => 0, 'file_access_expiration' => 1.hour.from_now.to_i}, :read)).to eql(false)
end
it "should not allow user access based on incorrect 'file_access_expiration' in the session" do
a = attachment
expect(a.grants_right?(nil, :read)).to eql(false)
expect(a.grants_right?(nil, :download)).to eql(false)
expect(a.grants_right?(nil, {'file_access_user_id' => student.id, 'file_access_expiration' => 1.minute.ago.to_i}, :read)).to eql(false)
end
it "should allow students to download a file on an assessment question if it's part of a quiz they can read" do
@bank = @course.assessment_question_banks.create!(:title => "bank")
@a1 = attachment_with_context(@course, :display_name => "a1")
@a2 = attachment_with_context(@course, :display_name => "a2")
data1 = {'name' => "Hi", 'question_text' => "hey look <img src='/courses/#{@course.id}/files/#{@a1.id}/download'>", 'answers' => [{'id' => 1}, {'id' => 2}]}
@aquestion1 = @bank.assessment_questions.create!(:question_data => data1)
aq_att1 = @aquestion1.attachments.first
data2 = {'name' => "Hi", 'question_text' => "hey look <img src='/courses/#{@course.id}/files/#{@a2.id}/download'>", 'answers' => [{'id' => 1}, {'id' => 2}]}
@aquestion2 = @bank.assessment_questions.create!(:question_data => data2)
aq_att2 = @aquestion2.attachments.first
quiz = @course.quizzes.create!
AssessmentQuestion.find_or_create_quiz_questions([@aquestion1], quiz.id, nil)
quiz.publish!
expect(aq_att1.grants_right?(student, :download)).to eq true
expect(aq_att2.grants_right?(student, :download)).to eq false
end
end
context "duplicate handling" do
before :once do
course_model
@a1 = attachment_with_context(@course, :display_name => "a1")
@a2 = attachment_with_context(@course, :display_name => "a2")
@a = attachment_with_context(@course)
end
it "should handle overwriting duplicates" do
@a.display_name = 'a1'
deleted = @a.handle_duplicates(:overwrite)
expect(@a.file_state).to eq 'available'
@a1.reload
expect(@a1.file_state).to eq 'deleted'
expect(@a1.replacement_attachment).to eql @a
expect(deleted).to eq [ @a1 ]
end
it "should update replacement pointers to replaced files" do
@a.update_attribute(:display_name, 'a1')
@a.handle_duplicates(:overwrite)
expect(@a1.reload.replacement_attachment).to eql @a
again = attachment_with_context(@course, :display_name => 'a1')
again.handle_duplicates(:overwrite)
expect(@a1.reload.replacement_attachment).to eql again
end
it "should update replacement pointers to replaced-then-renamed files" do
@a.update_attribute(:display_name, 'a1')
@a.handle_duplicates(:overwrite)
expect(@a1.reload.replacement_attachment).to eql @a
@a.update_attribute(:display_name, 'renamed')
again = attachment_with_context(@course, :display_name => 'renamed')
again.handle_duplicates(:overwrite)
expect(@a1.reload.replacement_attachment).to eql again
end
it "should handle renaming duplicates" do
@a.display_name = 'a1'
deleted = @a.handle_duplicates(:rename)
expect(deleted).to be_empty
expect(@a.file_state).to eq 'available'
@a1.reload
expect(@a1.file_state).to eq 'available'
expect(@a.display_name).to eq 'a1-1'
end
it "rename itself after collision on restoration" do
@a1.destroy!
@a.display_name = @a1.display_name
@a.save!
@a1.restore
expect(@a1.reload.display_name).to eq "#{@a.display_name}-1"
end
it "should update ContentTags when overwriting" do
mod = @course.context_modules.create!(:name => "some module")
tag1 = mod.add_item(:id => @a1.id, :type => 'attachment')
tag2 = mod.add_item(:id => @a2.id, :type => 'attachment')
mod.save!
@a1.reload
expect(@a1.could_be_locked).to be_truthy
@a.display_name = 'a1'
@a.handle_duplicates(:overwrite)
tag1.reload
expect(tag1).to be_active
expect(tag1.content_id).to eq @a.id
@a.reload
expect(@a.could_be_locked).to be_truthy
@a2.destroy
tag2.reload
expect(tag2).to be_deleted
end
it "should find replacement file by id if name changes" do
@a.display_name = 'a1'
@a.handle_duplicates(:overwrite)
@a.display_name = 'renamed!!'
@a.save!
expect(@course.attachments.find(@a1.id)).to eql @a
end
it "should find replacement file by name if id isn't present" do
@a.display_name = 'a1'
@a.handle_duplicates(:overwrite)
@a1.update_attribute(:replacement_attachment_id, nil)
expect(@course.attachments.find(@a1.id)).to eql @a
end
it "preserves hidden state" do
@a1.update_attribute(:file_state, 'hidden')
@a.update_attribute(:display_name, 'a1')
@a.handle_duplicates(:overwrite)
expect(@a.reload.file_state).to eq 'hidden'
end
it "preserves unpublished state" do
@a1.update_attribute(:locked, true)
@a.update_attribute(:display_name, 'a1')
@a.handle_duplicates(:overwrite)
expect(@a.reload.locked).to eq true
end
it "preserves lock dates" do
@a1.unlock_at = Date.new(2016, 1, 1)
@a1.lock_at = Date.new(2016, 4, 1)
@a1.save!
@a.update_attribute(:display_name, 'a1')
@a.handle_duplicates(:overwrite)
expect(@a.reload.unlock_at).to eq @a1.reload.unlock_at
expect(@a.lock_at).to eq @a1.lock_at
end
it "preserves usage rights" do
usage_rights = @course.usage_rights.create! use_justification: 'creative_commons', legal_copyright: '(C) 2014 XYZ Corp', license: 'cc_by_nd'
@a1.usage_rights = usage_rights
@a1.save!
@a.update_attribute(:display_name, 'a1')
@a.handle_duplicates(:overwrite)
expect(@a.reload.usage_rights).to eq usage_rights
end
it "forces rename semantics in submissions folders" do
user_model
a1 = attachment_model context: @user, folder: @user.submissions_folder, filename: 'a1.txt'
a2 = attachment_model context: @user, folder: @user.submissions_folder, filename: 'a2.txt'
a2.display_name = 'a1.txt'
deleted = a2.handle_duplicates(:overwrite)
expect(deleted).to be_empty
a2.reload
expect(a2.display_name).not_to eq 'a1.txt'
expect(a2.display_name).not_to eq 'a2.txt'
end
context "sharding" do
specs_require_sharding
it "forms proper queries when run from a different shard" do
@shard1.activate do
@a.display_name = 'a1'
deleted = @a.handle_duplicates(:overwrite)
expect(@a.file_state).to eq 'available'
@a1.reload
expect(@a1.file_state).to eq 'deleted'
expect(@a1.replacement_attachment).to eql @a
expect(deleted).to eq [ @a1 ]
end
end
end
end
describe "make_unique_filename" do
it "should find a unique name for files" do
existing_files = %w(a.txt b.txt c.txt)
expect(Attachment.make_unique_filename("d.txt", existing_files)).to eq "d.txt"
expect(existing_files).not_to be_include(Attachment.make_unique_filename("b.txt", existing_files))
existing_files = %w(/a/b/a.txt /a/b/b.txt /a/b/c.txt)
expect(Attachment.make_unique_filename("/a/b/d.txt", existing_files)).to eq "/a/b/d.txt"
new_name = Attachment.make_unique_filename("/a/b/b.txt", existing_files)
expect(existing_files).not_to be_include(new_name)
expect(new_name).to match(%r{^/a/b/b[^.]+\.txt})
end
it "deals with missing extensions" do
expect(Attachment.make_unique_filename('blah', ['blah'])).to eq 'blah-1'
end
it "puts the uniquifier before double extensions" do
expect(Attachment.make_unique_filename('blah.tar.bz2', ['blah.tar.bz2'])).to eq 'blah-1.tar.bz2'
end
it "deals with extensions starting with a digit" do
expect(Attachment.make_unique_filename('blah.3dm', ['blah.3dm'])).to eq 'blah-1.3dm'
end
it "does not treat numbers after a decimal point as extensions" do
expect(Attachment.make_unique_filename('section 11.5.doc', ['section 11.5.doc'])).to eq 'section 11.5-1.doc'
expect(Attachment.make_unique_filename('3.3.2018 footage.mp4', ['3.3.2018 footage.mp4'])).to eq '3.3.2018 footage-1.mp4'
end
end
context "download/inline urls" do
before :once do
course_model
end
it "should work with s3 storage" do
s3_storage!
attachment = attachment_with_context(@course, :display_name => 'foo')
expect(attachment.public_download_url).to match(/response-content-disposition=attachment/)
expect(attachment.public_inline_url).to match(/response-content-disposition=inline/)
end
it 'should allow custom ttl for download_url' do
attachment = attachment_with_context(@course, :display_name => 'foo')
allow(attachment).to receive(:public_url) # allow other calls due to, e.g., save
expect(attachment).to receive(:public_url).with(include(:expires_in => 3600.seconds))
attachment.public_download_url
expect(attachment).to receive(:public_url).with(include(:expires_in => 2.days))
attachment.public_download_url(2.days)
end
it 'should allow custom ttl for root_account' do
attachment = attachment_with_context(@course, :display_name => 'foo')
root = @course.root_account
root.settings[:s3_url_ttl_seconds] = 3.days.seconds.to_s
root.save!
expect(attachment).to receive(:public_url).with(include(expires_in: 3.days.to_i.seconds))
attachment.public_download_url
end
it "should include response-content-disposition" do
attachment = attachment_with_context(@course, :display_name => 'foo')
allow(attachment).to receive(:authenticated_s3_url) # allow other calls due to, e.g., save
expect(attachment).to receive(:authenticated_s3_url).with(include(:response_content_disposition => %(attachment; filename="foo"; filename*=UTF-8''foo)))
attachment.public_download_url
expect(attachment).to receive(:authenticated_s3_url).with(include(:response_content_disposition => %(inline; filename="foo"; filename*=UTF-8''foo)))
attachment.public_inline_url
end
it "should use the display_name, not filename, in the response-content-disposition" do
attachment = attachment_with_context(@course, :filename => 'bar', :display_name => 'foo')
allow(attachment).to receive(:authenticated_s3_url) # allow other calls due to, e.g., save
expect(attachment).to receive(:authenticated_s3_url).with(include(:response_content_disposition => %(attachment; filename="foo"; filename*=UTF-8''foo)))
attachment.public_download_url
end
it "should http quote the filename in the response-content-disposition if necessary" do
attachment = attachment_with_context(@course, :display_name => 'fo"o')
allow(attachment).to receive(:authenticated_s3_url) # allow other calls due to, e.g., save
expect(attachment).to receive(:authenticated_s3_url).with(include(:response_content_disposition => %(attachment; filename="fo\\"o"; filename*=UTF-8''fo%22o)))
attachment.public_download_url
end
it "should transliterate filename with i18n" do
a = attachment_with_context(@course, :display_name => "糟糕.pdf")
sanitized_filename = I18n.transliterate(a.display_name, replacement: '_')
allow(a).to receive(:authenticated_s3_url)
expect(a).to receive(:authenticated_s3_url).with(include(:response_content_disposition => %(attachment; filename="#{sanitized_filename}"; filename*=UTF-8''%E7%B3%9F%E7%B3%95.pdf)))
a.public_download_url
end
it "should escape all non-alphanumeric characters in the utf-8 filename" do
attachment = attachment_with_context(@course, :display_name => '"This file[0] \'{has}\' \# awesome `^<> chars 100%,|<-pipe"')
allow(attachment).to receive(:authenticated_s3_url)
expect(attachment).to receive(:authenticated_s3_url).with(include(:response_content_disposition => %(attachment; filename="\\\"This file[0] '{has}' \\# awesome `^<> chars 100%,|<-pipe\\\""; filename*=UTF-8''%22This%20file%5B0%5D%20%27%7Bhas%7D%27%20%5C%23%20awesome%20%60%5E%3C%3E%20chars%20100%25%2C%7C%3C%2Dpipe%22)))
attachment.public_download_url
end
end
context "root_account_id" do
before :once do
account_model
course_model(:account => @account)
@a = attachment_with_context(@course)
end
it "should return account id for normal namespaces" do
@a.namespace = "account_#{@account.id}"
expect(@a.root_account_id).to eq @account.id
end
it "should return account id for localstorage namespaces" do
@a.namespace = "_localstorage_/#{@account.file_namespace}"
expect(@a.root_account_id).to eq @account.id
end
it "should immediately infer the namespace if not yet set" do
Attachment.current_root_account = nil
@a = Attachment.new(:context => @course)
expect(@a).to be_new_record
expect(@a.read_attribute(:namespace)).to be_nil
expect(@a.namespace).not_to be_nil
expect(@a.read_attribute(:namespace)).not_to be_nil
expect(@a.root_account_id).to eq @account.id
end
it "should not infer the namespace if it's not a new record" do
Attachment.current_root_account = nil
attachment_model(:context => submission_model)
expect(@attachment).not_to be_new_record
expect(@attachment.read_attribute(:namespace)).to be_nil
expect(@attachment.namespace).to be_nil
expect(@attachment.read_attribute(:namespace)).to be_nil
end
context "sharding" do
specs_require_sharding
it "stores a local id on the birth shard" do
Attachment.current_root_account = Account.default
att = Attachment.new
att.infer_namespace
expect(att.namespace).to eq Account.default.asset_string
expect(att.root_account_id).to eq Account.default.local_id
@shard1.activate do
expect(att.root_account_id).to eq Account.default.global_id
end
end
it "stores a global id on all other shards" do
a = nil
att = nil
@shard1.activate do
a = Account.create!
Attachment.current_root_account = a
att = Attachment.new
att.infer_namespace
expect(att.namespace).to eq a.global_asset_string
expect(att.root_account_id).to eq a.local_id
end
expect(att.root_account_id).to eq a.global_id
end
it "interprets root_account_id correctly, even when local on not the birth shard" do
a = nil
att = nil
@shard1.activate do
a = Account.create!
att = Attachment.new
att.namespace = a.asset_string
expect(att.root_account_id).to eq a.local_id
end
expect(att.root_account_id).to eq a.global_id
end
it "stores ID for a cross-shard attachment" do
Attachment.current_root_account = Account.default
att = nil
@shard1.activate do
att = Attachment.new
att.infer_namespace
expect(att.namespace).to eq Account.default.global_asset_string
expect(att.root_account_id).to eq Account.default.global_id
end
expect(att.root_account_id).to eq Account.default.local_id
end
end
end
context "encoding detection" do
it "should include the charset when appropriate" do
a = Attachment.new
a.content_type = 'text/html'
expect(a.content_type_with_encoding).to eq 'text/html'
a.encoding = ''
expect(a.content_type_with_encoding).to eq 'text/html'
a.encoding = 'UTF-8'
expect(a.content_type_with_encoding).to eq 'text/html; charset=UTF-8'
a.encoding = 'mycustomencoding'
expect(a.content_type_with_encoding).to eq 'text/html; charset=mycustomencoding'
end
it "should schedule encoding detection when appropriate" do
expects_job_with_tag('Attachment#infer_encoding', 0) do
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'image/png'), :content_type => 'image/png')
end
expects_job_with_tag('Attachment#infer_encoding', 1) do
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
end
expects_job_with_tag('Attachment#infer_encoding', 0) do
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html', :encoding => 'UTF-8')
end
end
it "should properly infer encoding" do
attachment_model(:uploaded_data => stub_png_data('blank.gif', "GIF89a\001\000\001\000\200\377\000\377\377\377\000\000\000,\000\000\000\000\001\000\001\000\000\002\002D\001\000;"))
expect(@attachment.encoding).to be_nil
@attachment.infer_encoding
# can't figure out GIF encoding
expect(@attachment.encoding).to eq ''
attachment_model(:uploaded_data => stub_png_data('blank.txt', "Hello World!"))
expect(@attachment.encoding).to be_nil
@attachment.infer_encoding
expect(@attachment.encoding).to eq 'UTF-8'
attachment_model(:uploaded_data => stub_png_data('blank.txt', "\xc2\xa9 2011"))
expect(@attachment.encoding).to be_nil
@attachment.infer_encoding
expect(@attachment.encoding).to eq 'UTF-8'
attachment_model(:uploaded_data => stub_png_data('blank.txt', "can't read me"))
allow(@attachment).to receive(:open).and_raise(IOError)
@attachment.infer_encoding
expect(@attachment.encoding).to eq nil
# work across split bytes
allow(Attachment).to receive(:read_file_chunk_size).and_return(1)
attachment_model(:uploaded_data => stub_png_data('blank.txt', "\xc2\xa9 2011"))
@attachment.infer_encoding
expect(@attachment.encoding).to eq 'UTF-8'
end
end
context "sharding" do
specs_require_sharding
it "grants rights to owning user even if the user is on a seperate shard" do
user = nil
attachments = []
@shard1.activate do
user = User.create!
expect(user.attachments.build.grants_right?(user, :read)).to be_truthy
end
@shard2.activate do
expect(user.attachments.build.grants_right?(user, :read)).to be_truthy
end
expect(user.attachments.build.grants_right?(user, :read)).to be_truthy
end
end
context "#change_namespace and #make_childless" do
before :once do
@old_account = account_model
@new_account = account_model
end
before :each do
s3_storage!
Attachment.current_root_account = @old_account
@root = attachment_model(filename: 'unknown 2.loser')
@child = attachment_model(:root_attachment => @root)
@old_object = double('old object')
@new_object = double('new object')
new_full_filename = @root.full_filename.sub(@root.namespace, @new_account.file_namespace)
allow(@root.bucket).to receive(:object).with(@root.full_filename).and_return(@old_object)
allow(@root.bucket).to receive(:object).with(new_full_filename).and_return(@new_object)
end
it "should fail for non-root attachments" do
expect(@old_object).to receive(:copy_to).never
expect { @child.change_namespace(@new_account.file_namespace) }.to raise_error('change_namespace must be called on a root attachment')
expect(@root.reload.namespace).to eq @old_account.file_namespace
expect(@child.reload.namespace).to eq @root.reload.namespace
end
it "should not copy if the destination exists" do
expect(@new_object).to receive(:exists?).and_return(true)
expect(@old_object).to receive(:copy_to).never
@root.change_namespace(@new_account.file_namespace)
expect(@root.namespace).to eq @new_account.file_namespace
expect(@child.reload.namespace).to eq @root.namespace
end
it "should rename root attachments and update children" do
expect(@new_object).to receive(:exists?).and_return(false)
expect(@old_object).to receive(:copy_to).with(@new_object, anything)
@root.change_namespace(@new_account.file_namespace)
expect(@root.namespace).to eq @new_account.file_namespace
expect(@child.reload.namespace).to eq @root.namespace
end
it 'should allow making a root_attachment childless' do
@child.update_attribute(:filename, 'invalid')
expect(@root.s3object).to receive(:exists?).and_return(true)
expect(@child).to receive(:s3object).and_return(@old_object)
expect(@old_object).to receive(:exists?).and_return(true)
@root.make_childless(@child)
expect(@root.reload.children).to eq []
expect(@child.reload.root_attachment_id).to eq nil
expect(@child.read_attribute(:filename)).to eq @root.filename
end
end
context "s3 storage with sharding" do
let(:sz) { "640x>" }
specs_require_sharding
before :each do
s3_storage!
attachment_model(:uploaded_data => stub_png_data, :filename => 'profile.png')
end
it "should have namespaced thumb" do
@shard1.activate do
@attachment.thumbnail || @attachment.build_thumbnail.save!
thumb = @attachment.thumbnail
# i can't seem to get a s3 url so I am just going to make sure the thumbnail namespace was inherited from the attachment
expect(thumb.namespace).to eq @attachment.namespace
expect(thumb.authenticated_s3_url).to be_include @attachment.namespace
end
end
it "shouldn't have namespaced thumb when namespace is nil" do
@shard1.activate do
@attachment.thumbnail || @attachment.build_thumbnail.save!
thumb = @attachment.thumbnail
# nil out namespace so we can make sure the url generating is working properly
thumb.namespace = nil
expect(thumb.authenticated_s3_url).not_to be_include @attachment.namespace
end
end
end
context "has_thumbnail?" do
context "non-instfs attachment" do
it "should be false when it doesn't have a thumbnail object (yet?)" do
attachment_model(uploaded_data: stub_png_data)
if @attachment.thumbnail
@attachment.thumbnail.destroy!
@attachment.thumbnail = nil
end
expect(@attachment.has_thumbnail?).to be false
end
it "should be false when it doesn't have a thumbnail object even if instfs is enabled" do
attachment_model(uploaded_data: stub_png_data)
if @attachment.thumbnail
@attachment.thumbnail.destroy!
@attachment.thumbnail = nil
end
allow(InstFS).to receive(:enabled?).and_return true
expect(@attachment.has_thumbnail?).to be false
end
it "should be true when it has a thumbnail object" do
attachment_model(uploaded_data: stub_png_data)
@attachment.thumbnail || @attachment.build_thumbnail.save!
expect(@attachment.has_thumbnail?).to be true
end
end
context "instfs attachment" do
before do
allow(InstFS).to receive(:enabled?).and_return true
allow(InstFS).to receive(:jwt_secret).and_return 'secret'
allow(InstFS).to receive(:app_host).and_return 'instfs'
end
it "should be false when not thumbnailable" do
attachment_model(instfs_uuid: 'abc', content_type: 'text/plain')
expect(@attachment.has_thumbnail?).to be false
end
it "should be true when thumbnailable" do
attachment_model(instfs_uuid: 'abc', content_type: 'image/png')
expect(@attachment.has_thumbnail?).to be true
end
it "should be true when thumbnailable and instfs is later disabled" do
attachment_model(instfs_uuid: 'abc', content_type: 'image/png')
allow(InstFS).to receive(:enabled?).and_return false
expect(@attachment.has_thumbnail?).to be true
end
end
end
context "thumbnail_url (non-instfs)" do
it "should be the thumbnail's url" do
attachment_model(uploaded_data: stub_png_data)
@attachment.thumbnail || @attachment.build_thumbnail.save!
expect(@attachment.thumbnail_url).to eq @attachment.thumbnail.cached_s3_url
end
end
context "dynamic thumbnails" do
let(:sz) { "640x>" }
before do
attachment_model(:uploaded_data => stub_png_data)
end
around do |example|
Timecop.freeze(Time.now.utc, &example)
end
it "should use the default size if an unknown size is passed in" do
@attachment.thumbnail || @attachment.build_thumbnail.save!
url = @attachment.thumbnail_url(:size => "100x100")
expect(url).to be_present
expect(url).to eq @attachment.thumbnail.authenticated_s3_url(expires_in: 144.hours)
end
it "should generate the thumbnail on the fly" do
thumb = @attachment.thumbnails.where(thumbnail: "640x>").first
expect(thumb).to eq nil
expect(@attachment).to receive(:create_or_update_thumbnail).with(anything, sz, sz) {
@attachment.thumbnails.create!(:thumbnail => "640x>", :uploaded_data => stub_png_data)
}
url = @attachment.thumbnail_url(:size => "640x>")
expect(url).to be_present
thumb = @attachment.thumbnails.where(thumbnail: "640x>").first
expect(thumb).to be_present
expect(url).to eq thumb.authenticated_s3_url(expires_in: 144.hours)
end
it "should use the existing thumbnail if present" do
expect(@attachment).to receive(:create_or_update_thumbnail).with(anything, sz, sz) {
@attachment.thumbnails.create!(:thumbnail => "640x>", :uploaded_data => stub_png_data)
}
url = @attachment.thumbnail_url(:size => "640x>")
expect(@attachment).to receive(:create_dynamic_thumbnail).never
url = @attachment.thumbnail_url(:size => "640x>")
thumb = @attachment.thumbnails.where(thumbnail: "640x>").first
expect(url).to be_present
expect(thumb).to be_present
expect(url).to eq thumb.authenticated_s3_url(expires_in: 144.hours)
end
end
describe '.allows_thumbnails_for_size' do
it 'inevitably returns false if there is no size provided' do
expect(Attachment.allows_thumbnails_of_size?(nil)).to be_falsey
end
it 'returns true if the provided size is in the configured dynamic sizes' do
expect(Attachment.allows_thumbnails_of_size?(Attachment::DYNAMIC_THUMBNAIL_SIZES.first)).to be_truthy
end
it 'returns false if the provided size is not in the configured dynamic sizes' do
expect(Attachment.allows_thumbnails_of_size?('nonsense')).to be_falsey
end
end
describe "thumbnail source image size limitation" do
before do
local_storage! # s3 attachment data is stubbed out, so there is no image to identify the size of
course_factory
end
it 'creates thumbnails for smaller images' do
att = @course.attachments.create! :uploaded_data => jpeg_data_frd, :filename => 'ok.jpg'
expect(att.thumbnail).not_to be_nil
expect(att.thumbnail.width).not_to be_nil
end
it 'does not create thumbnails for larger images' do
att = @course.attachments.create! :uploaded_data => one_hundred_megapixels_of_highly_compressed_png_data, :filename => '3vil.png'
expect(att.thumbnail).to be_nil
end
end
context "notifications" do
before :once do
course_model(:workflow_state => "available")
# ^ enrolls @teacher in @course
# create a student to receive notifications
@student = user_model
@student.register!
e = @course.enroll_student(@student).accept
@cc = @student.communication_channels.create(:path => "default@example.com")
@cc.confirm!
@student_ended = user_model
@student_ended.register!
@section_ended = @course.course_sections.create!(end_at: Time.zone.now - 1.day)
@course.enroll_student(@student_ended, :section => @section_ended).accept
@cc_ended = @student_ended.communication_channels.create(:path => "default2@example.com")
@cc_ended.confirm!
NotificationPolicy.create(:notification => Notification.create!(:name => 'New File Added'), :communication_channel => @cc, :frequency => "immediately")
NotificationPolicy.create(:notification => Notification.create!(:name => 'New Files Added'), :communication_channel => @cc, :frequency => "immediately")
NotificationPolicy.create(:notification => Notification.create!(:name => 'New File Added - ended'),
:communication_channel => @cc_ended, :frequency => "immediately")
NotificationPolicy.create(:notification => Notification.create!(:name => 'New Files Added - ended'),
:communication_channel => @cc_ended, :frequency => "immediately")
end
it "should send a single-file notification" do
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
expect(@attachment.need_notify).to be_truthy
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
@attachment.reload
expect(@attachment.need_notify).not_to be_truthy
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).not_to be_nil
end
it "should send a batch notification" do
att1 = attachment_model(:uploaded_data => stub_file_data('file1.txt', nil, 'text/html'), :content_type => 'text/html')
att2 = attachment_model(:uploaded_data => stub_file_data('file2.txt', nil, 'text/html'), :content_type => 'text/html')
att3 = attachment_model(:uploaded_data => stub_file_data('file3.txt', nil, 'text/html'), :content_type => 'text/html')
[att1, att2, att3].each {|att| expect(att.need_notify).to be_truthy}
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
[att1, att2, att3].each {|att| expect(att.reload.need_notify).not_to be_truthy}
expect(Message.where(user_id: @student, notification_name: 'New Files Added').first).not_to be_nil
end
it "should not notify before a file finishes uploading" do
# it's weird, but file_state is 'deleted' until the upload completes, when it is changed to 'available'
attachment_model(:file_state => 'deleted', :content_type => 'text/html')
expect(@attachment.need_notify).not_to be_truthy
end
it "should postpone notification of a batch judged to be in-progress" do
att1 = attachment_model(:uploaded_data => stub_file_data('file1.txt', nil, 'text/html'), :content_type => 'text/html')
att2 = attachment_model(:uploaded_data => stub_file_data('file2.txt', nil, 'text/html'), :content_type => 'text/html')
att3 = attachment_model(:uploaded_data => stub_file_data('file3.txt', nil, 'text/html'), :content_type => 'text/html')
[att1, att2, att3].each {|att| expect(att.need_notify).to be_truthy}
Timecop.freeze(2.minutes.from_now) { Attachment.do_notifications }
[att1, att2, att3].each {|att| expect(att.reload.need_notify).to be_truthy}
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).to be_nil
Timecop.freeze(6.minutes.from_now) { Attachment.do_notifications }
[att1, att2, att3].each {|att| expect(att.reload.need_notify).not_to be_truthy}
expect(Message.where(user_id: @student, notification_name: 'New Files Added').first).not_to be_nil
end
it "should discard really old pending notifications" do
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
expect(@attachment.need_notify).to be_truthy
Timecop.freeze(1.week.from_now) { Attachment.do_notifications }
@attachment.reload
expect(@attachment.need_notify).to be_falsey
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).to be_nil
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).to be_nil
end
it "should respect save_without_broadcasting" do
att1 = attachment_model(:file_state => 'deleted', :uploaded_data => stub_file_data('file1.txt', nil, 'text/html'), :content_type => 'text/html')
att2 = attachment_model(:file_state => 'deleted', :uploaded_data => stub_file_data('file2.txt', nil, 'text/html'), :content_type => 'text/html')
att3 = attachment_model(:file_state => 'deleted', :uploaded_data => stub_file_data('file2.txt', nil, 'text/html'), :content_type => 'text/html')
expect(att1.need_notify).not_to be_truthy
att1.file_state = 'available'
att1.save!
expect(att1.need_notify).to be_truthy
expect(att2.need_notify).not_to be_truthy
att2.file_state = 'available'
att2.save_without_broadcasting
expect(att2.need_notify).not_to be_truthy
expect(att3.need_notify).not_to be_truthy
att3.file_state = 'available'
att3.save_without_broadcasting!
expect(att3.need_notify).not_to be_truthy
end
it "should not send notifications to students if the file is uploaded to a locked folder" do
@teacher.register!
cc = @teacher.communication_channels.create!(:path => "default@example.com")
cc.confirm!
NotificationPolicy.create!(:notification => Notification.where(name: 'New File Added').first, :communication_channel => cc, :frequency => "immediately")
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
@attachment.folder.locked = true
@attachment.folder.save!
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
@attachment.reload
expect(@attachment.need_notify).not_to be_truthy
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).to be_nil
expect(Message.where(user_id: @teacher, notification_name: 'New File Added').first).not_to be_nil
end
it "should not send notifications to students if the file is unpublished because of usage rights" do
@teacher.register!
cc = @teacher.communication_channels.create!(:path => "default@example.com")
cc.confirm!
NotificationPolicy.create!(:notification => Notification.where(name: 'New File Added').first, :communication_channel => cc, :frequency => "immediately")
@course.usage_rights_required = true
@course.save!
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
@attachment.set_publish_state_for_usage_rights
@attachment.save!
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
@attachment.reload
expect(@attachment.need_notify).not_to be_truthy
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).to be_nil
expect(Message.where(user_id: @teacher, notification_name: 'New File Added').first).not_to be_nil
end
it "should not send notifications to students if the files navigation is hidden from student view" do
@teacher.register!
cc = @teacher.communication_channels.create!(:path => "default@example.com")
cc.confirm!
NotificationPolicy.create!(:notification => Notification.where(name: 'New File Added').first, :communication_channel => cc, :frequency => "immediately")
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
@course.tab_configuration = [{:id => Course::TAB_FILES, :hidden => true}]
@course.save!
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
@attachment.reload
expect(@attachment.need_notify).not_to be_truthy
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).to be_nil
expect(Message.where(user_id: @teacher, notification_name: 'New File Added').first).not_to be_nil
end
it "should not fail if the attachment context does not have participants" do
cm = ContentMigration.create!(:context => course_factory)
attachment_model(:context => cm, :uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
Attachment.where(:id => @attachment).update_all(:need_notify => true)
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
end
it "doesn't send notifications for a concluded course" do
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
@course.soft_conclude!
@course.save!
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
expect(Message.where(user_id: @student, notification_name: 'New File Added').first).to be_nil
end
it "doesn't send notifications for a concluded section in an active course" do
attachment_model(:uploaded_data => stub_file_data('file.txt', nil, 'text/html'), :content_type => 'text/html')
Timecop.freeze(10.minutes.from_now) { Attachment.do_notifications }
expect(Message.where(user_id: @student_ended, notification_name: 'New File Added').first).to be_nil
end
end
context "quota" do
it "should give small files a minimum quota size" do
course_model
attachment_model(:context => @course, :uploaded_data => stub_png_data, :size => 25)
quota = Attachment.get_quota(@course)
expect(quota[:quota_used]).to eq Attachment.minimum_size_for_quota
end
it "should not count attachments a student has used for submissions towards the quota" do
course_with_student(:active_all => true)
attachment_model(:context => @user, :uploaded_data => stub_png_data, :filename => "homework.png")
@attachment.update_attribute(:size, 1.megabyte)
quota = Attachment.get_quota(@user)
expect(quota[:quota_used]).to eq 1.megabyte
@assignment = @course.assignments.create!
sub = @assignment.submit_homework(@user, attachments: [@attachment])
attachment_model(:context => @user, :uploaded_data => stub_png_data, :filename => "otherfile.png")
@attachment.update_attribute(:size, 1.megabyte)
quota = Attachment.get_quota(@user)
expect(quota[:quota_used]).to eq 1.megabyte
end
it "should not count attachments a student has used for graded discussion replies towards the quota" do
course_with_student(:active_all => true)
attachment_model(:context => @user, :uploaded_data => stub_png_data, :filename => "homework.png")
@attachment.update_attribute(:size, 1.megabyte)
quota = Attachment.get_quota(@user)
expect(quota[:quota_used]).to eq 1.megabyte
assignment = @course.assignments.create!(:title => "asmt")
topic = @course.discussion_topics.create!(:title => 'topic', :assignment => assignment)
entry = topic.reply_from(:user => @student, :text => "entry")
entry.attachment = @attachment
entry.save!
attachment_model(:context => @user, :uploaded_data => stub_png_data, :filename => "otherfile.png")
@attachment.update_attribute(:size, 1.megabyte)
quota = Attachment.get_quota(@user)
expect(quota[:quota_used]).to eq 1.megabyte
end
it "should not count attachments in submissions folders toward the quota" do
user_model
attachment_model(:context => @user, :uploaded_data => stub_png_data, :filename => 'whatever.png', :folder => @user.submissions_folder)
@attachment.update_attribute(:size, 1.megabyte)
quota = Attachment.get_quota(@user)
expect(quota[:quota_used]).to eq 0
end
it "should not count attachments in group submissions folders toward the quota" do
group_model
attachment_model(:context => @group, :uploaded_data => stub_png_data, :filename => 'whatever.png', :folder => @group.submissions_folder)
@attachment.update_attribute(:size, 1.megabyte)
quota = Attachment.get_quota(@group)
expect(quota[:quota_used]).to eq 0
end
end
context "#open" do
include WebMock::API
context "instfs branch" do
before do
user_model
attachment_model(:context => @user)
public_url = 'http://www.example.com/foo'
allow(@attachment).to receive(:instfs_hosted?).and_return true
allow(@attachment).to receive(:public_url).and_return public_url
stub_request(:get, public_url).
to_return(status: 200, body: "test response body", headers: {})
end
it "should stream data to the block given" do
callback = false
@attachment.open do |data|
expect(data).to eq "test response body"
callback = true
end
expect(callback).to eq true
end
it "should stream to a tempfile without a block given" do
file = @attachment.open
expect(file).to be_a(Tempfile)
expect(file.read).to eq("test response body")
end
end
context "s3_storage" do
before do
s3_storage!
attachment_model
end
it "should stream data to the block given" do
callback = false
data = ["test", false]
tempfile = double
expect(tempfile).to receive(:binmode)
expect(tempfile).to receive(:rewind)
expect(tempfile).to receive(:path)
expect(Tempfile).to receive(:new).and_return(tempfile)
actual_file = double()
expect(actual_file).to receive(:read).twice { data.shift }
expect(File).to receive(:open).and_yield(actual_file)
expect_any_instance_of(@attachment.s3object.class).to receive(:get).with(include(:response_target))
@attachment.open { |data| expect(data).to eq "test"; callback = true }
expect(callback).to eq true
end
it "should stream to a tempfile without a block given" do
expect_any_instance_of(@attachment.s3object.class).to receive(:get).with(include(:response_target))
file = @attachment.open
expect(file).to be_a(Tempfile)
end
end
end
context "#process_s3_details!" do
before :once do
attachment_model(filename: 'new filename')
end
before :each do
allow(Attachment).to receive(:local_storage?).and_return(false)
allow(Attachment).to receive(:s3_storage?).and_return(true)
allow(@attachment).to receive(:s3object).and_return(double('s3object'))
allow(@attachment).to receive(:after_attachment_saved)
end
context "deduplication" do
before :once do
attachment = @attachment
@existing_attachment = attachment_model(filename: 'existing filename')
@child_attachment = attachment_model(root_attachment: @existing_attachment)
@attachment = attachment
end
before :each do
allow(@existing_attachment).to receive(:s3object).and_return(double('existing_s3object'))
allow(@attachment).to receive(:find_existing_attachment_for_md5).and_return(@existing_attachment)
end
context "existing attachment has s3object" do
before do
allow(@existing_attachment.s3object).to receive(:exists?).and_return(true)
allow(@attachment.s3object).to receive(:delete)
end
it "should delete the new (redundant) s3object" do
expect(@attachment.s3object).to receive(:delete).once
@attachment.process_s3_details!({})
end
it "should put the new attachment under the existing attachment" do
@attachment.process_s3_details!({})
expect(@attachment.reload.root_attachment).to eq @existing_attachment
end
it "should retire the new attachment's filename" do
@attachment.process_s3_details!({})
expect(@attachment.reload.filename).to eq @existing_attachment.filename
end
end
context "existing attachment is missing s3object" do
before do
allow(@existing_attachment.s3object).to receive(:exists?).and_return(false)
end
it "should not delete the new s3object" do
expect(@attachment.s3object).to receive(:delete).never
@attachment.process_s3_details!({})
end
it "should not put the new attachment under the existing attachment" do
@attachment.process_s3_details!({})
expect(@attachment.reload.root_attachment).to be_nil
end
it "should not retire the new attachment's filename" do
@attachment.process_s3_details!({})
@attachment.reload.filename == 'new filename'
end
it "should put the existing attachment under the new attachment" do
@attachment.process_s3_details!({})
expect(@existing_attachment.reload.root_attachment).to eq @attachment
end
it "should retire the existing attachment's filename" do
@attachment.process_s3_details!({})
expect(@existing_attachment.reload.read_attribute(:filename)).to be_nil
expect(@existing_attachment.filename).to eq @attachment.filename
end
it "should reparent the child attachment under the new attachment" do
@attachment.process_s3_details!({})
expect(@child_attachment.reload.root_attachment).to eq @attachment
end
end
end
end
context 'permissions' do
describe ':attach_to_submission_comment' do
it 'works for assignments if you own the attachment' do
@s1, @s2 = n_students_in_course(2)
@assignment = @course.assignments.create! name: 'blah'
@attachment = Attachment.create! context: @assignment,
filename: "foo.txt",
uploaded_data: StringIO.new("bar"),
user: @s1
expect(@attachment.grants_right?(@s1, :attach_to_submission_comment)).to be_truthy
expect(@attachment.grants_right?(@s2, :attach_to_submission_comment)).to be_falsey
end
end
end
describe "#full_path" do
it "shouldn't puke for things that don't have folders" do
attachment_obj_with_context(Account.default.default_enrollment_term)
@attachment.folder = nil
expect(@attachment.full_path).to eq "/#{@attachment.display_name}"
end
end
describe ".clone_url_strand" do
it "falls back for invalid URLs" do
expect(Attachment.clone_url_strand("")).to eq "file_download"
end
it "gives the host for 'local' host" do
expect(Attachment.clone_url_strand("http://localhost:9090/image.jpg")).to eq ["file_download", "localhost"]
end
it "gives the full host for simple domain" do
expect(Attachment.clone_url_strand("http://google.com/image.jpg")).to eq ["file_download", "google.com"]
end
it "strips subdomains" do
expect(Attachment.clone_url_strand("http://cdn.google.com/image.jpg")).to eq ["file_download", "google.com"]
end
it "accepts overrides" do
allow(Attachment).to receive(:clone_url_strand_overrides).and_return("cdn.google.com" => "cdn")
expect(Attachment.clone_url_strand("http://cdn.google.com/image.jpg")).to eq ["file_download", "cdn"]
end
end
describe ".clone_url_as_attachment" do
it "should reject invalid urls" do
expect { Attachment.clone_url_as_attachment("ftp://some/stuff") }.to raise_error(ArgumentError)
end
it "should not raise on non-200 responses" do
url = "http://example.com/test.png"
expect(CanvasHttp).to receive(:get).with(url).and_yield(double('code' => '401'))
expect { Attachment.clone_url_as_attachment(url) }.to raise_error(CanvasHttp::InvalidResponseCodeError)
end
it "should use an existing attachment if passed in" do
url = "http://example.com/test.png"
a = attachment_model
expect(CanvasHttp).to receive(:get).with(url).and_yield(FakeHttpResponse.new('200', 'this is a jpeg', 'content-type' => 'image/jpeg'))
Attachment.clone_url_as_attachment(url, :attachment => a)
a.save!
expect(a.open.read).to eq "this is a jpeg"
end
it "should not overwrite the content_type if already present" do
url = "http://example.com/test.png"
a = attachment_model(:content_type => 'image/jpeg')
expect(CanvasHttp).to receive(:get).with(url).and_yield(FakeHttpResponse.new('200', 'this is a jpeg', 'content-type' => 'application/octet-stream'))
Attachment.clone_url_as_attachment(url, :attachment => a)
a.save!
expect(a.open.read).to eq "this is a jpeg"
expect(a.content_type).to eq 'image/jpeg'
end
it "should detect the content_type from the body" do
url = "http://example.com/test.png"
expect(CanvasHttp).to receive(:get).with(url).and_yield(FakeHttpResponse.new('200', 'this is a jpeg', 'content-type' => 'image/jpeg'))
att = Attachment.clone_url_as_attachment(url)
expect(att).to be_present
expect(att).to be_new_record
expect(att.content_type).to eq 'image/jpeg'
att.context = Account.default
att.save!
expect(att.open.read).to eq 'this is a jpeg'
end
end
describe "infer_namespace" do
it "should infer the correct namespace from the root attachment" do
local_storage!
allow(Rails.env).to receive(:development?).and_return(true)
course_factory
a1 = attachment_model(context: @course, uploaded_data: default_uploaded_data)
a2 = attachment_model(context: @course, uploaded_data: default_uploaded_data)
expect(a2.root_attachment).to eql(a1)
expect(a2.namespace).to eql(a1.namespace)
end
end
it "should be able to add a hidden attachment as a context module item" do
course_factory
att = attachment_model(context: @course, uploaded_data: default_uploaded_data)
att.hidden = true
att.save!
mod = @course.context_modules.create!(:name => "some module")
tag1 = mod.add_item(:id => att.id, :type => 'attachment')
expect(tag1).not_to be_nil
end
it "should unlock files at the right time even if they're accessed shortly before" do
enable_cache do
course_with_student :active_all => true
attachment_model uploaded_data: default_uploaded_data, unlock_at: 30.seconds.from_now
expect(@attachment.grants_right?(@student, :download)).to eq false # prime cache
Timecop.freeze(@attachment.unlock_at + 1.second) do
run_jobs
expect(Attachment.find(@attachment.id).grants_right?(@student, :download)).to eq true
end
end
end
it "should not be locked_for soft-concluded admin users" do
term = Account.default.enrollment_terms.create!
term.set_overrides(Account.default, 'TeacherEnrollment' => {:end_at => 3.days.ago})
course_with_teacher(:active_all => true)
@course.enrollment_term = term
@course.save!
attachment_model uploaded_data: default_uploaded_data
@attachment.update_attribute(:locked, true)
@attachment.reload
expect(@attachment.locked_for?(@teacher, :check_policies => true)).to be_falsey
end
describe 'local storage' do
it 'should properly sanitie a filename containing a slash' do
local_storage!
course_factory
a = attachment_model(filename: 'ENGL_100_/_ENGL_200.csv')
expect(a.filename).to eql('ENGL_100___ENGL_200.csv')
end
it 'should still properly escape the same filename on s3' do
s3_storage!
course_factory
a = attachment_model(filename: 'ENGL_100_/_ENGL_200.csv')
expect(a.filename).to eql('ENGL_100_%2F_ENGL_200.csv')
end
end
describe '#ajax_upload_params' do
it 'returns the attachment filename in the upload params' do
attachment_model filename: 'test.txt'
pseudonym @user
json = @attachment.ajax_upload_params(@user.pseudonym, '', '')
expect(json[:upload_params]['Filename']).to eq 'test.txt'
end
end
describe 'copy_to_folder!' do
before(:once) do
attachment_model filename: 'test.txt'
@folder = @context.folders.create! name: 'over there'
end
it 'copies a file into a folder' do
dup = @attachment.copy_to_folder!(@folder)
expect(dup.root_attachment).to eq @attachment
expect(dup.display_name).to eq 'test.txt'
end
it "handles duplicates" do
attachment_model filename: 'test.txt', folder: @folder
dup = @attachment.copy_to_folder!(@folder)
expect(dup.root_attachment).to eq @attachment
expect(dup.display_name).not_to eq 'test.txt'
end
end
end