canvas-lms/spec/models/attachment_spec.rb

1238 lines
47 KiB
Ruby

# coding: utf-8
#
# Copyright (C) 2011 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
lambda{attachment_model(:context => nil)}.should raise_error(ActiveRecord::RecordInvalid, /Validation failed: Context can't be blank/)
end
end
context "default_values" do
it "should set the display name to the filename if it is nil" do
attachment_model(:display_name => nil)
@attachment.display_name.should eql(@attachment.filename)
end
context "scribd_mime_type_id" do
it "should get set given extension" do
Attachment.clear_cached_mime_ids
scribd_mime_type_model(:extension => 'pdf')
@course = course_model
@attachment = @course.attachments.build(:filename => 'some_file.pdf')
@attachment.content_type = ''
@attachment.save!
@attachment.scribd_mime_type.should eql(@scribd_mime_type)
end
it "should get set given content_type" do
Attachment.clear_cached_mime_ids
scribd_mime_type_model(:name => 'application/pdf')
@course = course_model
@attachment = @course.attachments.build(:filename => 'some_file')
@attachment.content_type = 'application/pdf'
@attachment.save!
@attachment.scribd_mime_type.should eql(@scribd_mime_type)
end
it "should prefer using content_type over extension" do
Attachment.clear_cached_mime_ids
mime_type_pdf = scribd_mime_type_model(:name => 'application/pdf')
mime_type_doc = scribd_mime_type_model(:extension => 'doc')
@course = course_model
@attachment = @course.attachments.build(:filename => 'some_file.doc')
@attachment.content_type = 'application/pdf'
@attachment.save!
@attachment.scribd_mime_type.should eql(mime_type_pdf)
end
it "should not get set for html content despite extension" do
['text/html', 'application/xhtml+xml', 'application/xml', 'text/xml'].each do |content_type|
# make sure mime type exists so we'd otherwise have a chance to set it
mime_type_doc = scribd_mime_type_model(:extension => 'doc')
mime_type_html = scribd_mime_type_model(:name => content_type)
@course = course_model
@attachment = @course.attachments.build(:filename => 'some_file.doc')
@attachment.content_type = content_type
@attachment.save!
@attachment.scribd_mime_type.should be_nil
end
end
end
it "should create a ScribdAccount if one isn't present" do
scribd_mime_type_model(:extension => 'pdf')
course_model
@course.scribd_account.should be_nil
attachment_obj_with_context(@course, :content_type => 'application/pdf')
@attachment.context.should eql(@course)
@attachment.context.scribd_account.should be_nil
expect {
@attachment.save!
@attachment.context.scribd_account.should_not be_nil
@attachment.context.scribd_account.should be_is_a(ScribdAccount)
}.to change(ScribdAccount, :count).by(1)
end
it "should set the attachment.scribd_account to the context scribd_account" do
scribdable_attachment_model
@attachment.scribd_account.should eql(@attachment.context.scribd_account)
end
end
it "should be scribdable if scribd_mime_type_id is set" do
scribdable_attachment_model
@attachment.should be_scribdable
end
context "authenticated_s3_url" do
before do
local_storage!
end
it "should return http as the protocol by default" do
course_model
attachment_with_context(@course)
@attachment.authenticated_s3_url.should match(/^http:\/\//)
end
it "should return the protocol if specified" do
course_model
attachment_with_context(@course)
@attachment.authenticated_s3_url(:secure => true).should match(/^https:\/\//)
end
end
context "scribdable_context" do
it "should be a scribdable_context if the context is Course" do
course_model
attachment_with_context(@course)
@attachment.send(:scribdable_context?).should be_true
end
it "should be a scribdable_context if the context is Group" do
group_model
attachment_with_context(@group)
@attachment.send(:scribdable_context?).should be_true
end
it "should be a scribdable_context if the context is User" do
user_model
attachment_with_context(@user)
@attachment.context = @user
@attachment.context.should be_is_a(User)
@attachment.send(:scribdable_context?).should be_true
end
it "should not be a scribdable_context for non-scribdable contexts (like an Account, for example)" do
account_model
attachment_with_context(@account)
@attachment.context = @account
@attachment.context.should be_is_a(Account)
@attachment.send(:scribdable_context?).should be_false
end
end
context "crocodoc" do
before do
PluginSetting.create! :name => 'crocodoc',
:settings => { :api_key => "blahblahblahblahblah" }
Crocodoc::API.any_instance.stubs(:upload).returns 'uuid' => '1234567890'
end
it "crocodocable?" do
crocodocable_attachment_model
@attachment.should be_crocodocable
end
it "should not submit to auto-submit to scribd if a crocodoc is present" do
expects_job_with_tag('Attachment#submit_to_scribd!', 0) do
attachment_model(:content_type => 'application/pdf', :submission_attachment => true)
@attachment.after_attachment_saved
end
expects_job_with_tag('Attachment#submit_to_scribd!') do
scribd_mime_type_model(:extension => 'odt', :name => 'openoffice')
attachment_model(:content_type => 'openoffice')
@attachment.crocodocable?.should_not be_true
@attachment.after_attachment_saved
end
end
it "should submit to crocodoc" do
crocodocable_attachment_model
@attachment.crocodoc_available?.should be_false
@attachment.submit_to_crocodoc
@attachment.crocodoc_available?.should be_true
@attachment.crocodoc_document.uuid.should == '1234567890'
end
it "should spawn a delayed job to retry failed uploads (once)" do
Crocodoc::API.any_instance.stubs(:upload).returns 'error' => 'blah'
crocodocable_attachment_model
expects_job_with_tag('Attachment#submit_to_crocodoc', 1) do
@attachment.submit_to_crocodoc
end
expects_job_with_tag('Attachment#submit_to_crocodoc', 0) do
@attachment.submit_to_crocodoc(2)
end
end
it "should submit to scribd if crocodoc fails to convert" do
crocodocable_attachment_model
@attachment.submit_to_crocodoc
Crocodoc::API.any_instance.stubs(:status).returns [
{'uuid' => '1234567890', 'status' => 'ERROR'}
]
expects_job_with_tag('Attachment.submit_to_scribd') {
CrocodocDocument.update_process_states
}
end
end
it "should set the uuid" do
attachment_model
@attachment.uuid.should_not be_nil
end
context "workflow" do
before do
attachment_model
end
it "should default to pending_upload" do
@attachment.state.should eql(:pending_upload)
end
it "should be able to upload and record the submitted_to_scribd_at" do
time = Time.now
@attachment.upload!
@attachment.submitted_to_scribd_at.to_i.should be_close(time.to_i, 2)
@attachment.state.should eql(:processing)
end
it "should be able to take a processing object and complete its process" do
attachment_model(:workflow_state => 'processing')
@attachment.process!
@attachment.state.should eql(:processed)
end
it "should be able to take a new object and bypass upload with process" do
@attachment.process!
@attachment.state.should eql(:processed)
end
it "should be able to recycle a processed object and re-upload it" do
attachment_model(:workflow_state => 'processed')
@attachment.recycle
@attachment.state.should eql(:pending_upload)
end
end
context "submit_to_scribd!" do
before do
ScribdAPI.stubs(:set_user).returns(true)
ScribdAPI.stubs(:upload).returns(UUIDSingleton.instance.generate)
end
describe "submit_to_scribd job" do
it "should queue for scribdable types" do
expects_job_with_tag('Attachment#submit_to_scribd!') do
scribdable_attachment_model
@attachment.after_attachment_saved
end
@attachment.should be_pending_upload
end
it "should not queue for non-scribdable types" do
expects_job_with_tag('Attachment#submit_to_scribd!', 0) do
attachment_model
@attachment.after_attachment_saved
end
@attachment.should be_processed
end
describe "scribd submit filtering" do
it "should still submit if the attachment is tagged" do
Attachment.stubs(:filtering_scribd_submits?).returns(true)
expects_job_with_tag('Attachment#submit_to_scribd!') do
scribd_mime_type_model(:extension => 'pdf')
attachment_model(:content_type => 'application/pdf', :submission_attachment => true)
@attachment.after_attachment_saved
end
@attachment.should be_pending_upload
end
it "should skip submit if the attachment isn't tagged" do
Attachment.stubs(:filtering_scribd_submits?).returns(true)
expects_job_with_tag('Attachment#submit_to_scribd!', 0) do
scribdable_attachment_model
@attachment.after_attachment_saved
end
@attachment.should be_processed
end
end
end
it "should upload scribdable attachments" do
scribdable_attachment_model
@doc_obj = Scribd::Document.new
ScribdAPI.expects(:upload).returns(@doc_obj)
@doc_obj.stubs(:thumbnail).returns("the url to the scribd doc thumbnail")
@attachment.submit_to_scribd!.should be_true
@attachment.scribd_doc.should eql(@doc_obj)
@attachment.state.should eql(:processing)
end
it "should bypass non-scridbable attachments" do
attachment_model
@attachment.should_not be_scribdable
ScribdAPI.expects(:set_user).never
ScribdAPI.expects(:upload).never
@attachment.submit_to_scribd!.should be_true
@attachment.state.should eql(:processed)
end
it "should not mess with attachments outside the pending_upload state" do
ScribdAPI.expects(:set_user).never
ScribdAPI.expects(:upload).never
attachment_model(:workflow_state => 'processing')
@attachment.submit_to_scribd!.should be_false
attachment_model(:workflow_state => 'processed')
@attachment.submit_to_scribd!.should be_false
end
it "should use the root attachment scribd doc" do
a1 = attachment_model(:workflow_state => 'processing')
a2 = attachment_model(:workflow_state => 'processing', :root_attachment => a1)
a2.root_attachment.should == a1
a1.scribd_doc = doc = Scribd::Document.new
a2.scribd_doc.should == doc
a2.destroy
a1.scribd_doc.should == doc
end
it "should not send the secret password via to_json" do
attachment_model
@attachment.scribd_doc = Scribd::Document.new
@attachment.scribd_doc.doc_id = 'asdf'
@attachment.scribd_doc.secret_password = 'password'
res = JSON.parse(@attachment.to_json)
res['attachment'].should_not be_nil
res['attachment']['scribd_doc'].should_not be_nil
res['attachment']['scribd_doc']['attributes'].should_not be_nil
res['attachment']['scribd_doc']['attributes']['doc_id'].should eql('asdf')
res['attachment']['scribd_doc']['attributes']['secret_password'].should eql('')
@attachment.scribd_doc.doc_id.should eql('asdf')
@attachment.scribd_doc.secret_password.should eql('password')
end
end
context "conversion_status" do
before(:each) do
ScribdAPI.stubs(:get_status).returns(:status_from_scribd)
ScribdAPI.stubs(:set_user).returns(true)
ScribdAPI.stubs(:upload).returns(Scribd::Document.new)
end
it "should have a default conversion_status of :not_submitted for attachments that haven't been submitted" do
attachment_model
@attachment.conversion_status.should eql('NOT SUBMITTED')
end
it "should ask Scribd for the status" do
ScribdAPI.expects(:get_status).returns(:status_from_scribd)
scribdable_attachment_model
@doc_obj = Scribd::Document.new
ScribdAPI.expects(:upload).returns(@doc_obj)
@doc_obj.stubs(:thumbnail).returns("the url to the scribd doc thumbnail")
@attachment.submit_to_scribd!
@attachment.query_conversion_status!
end
it "should not ask Scribd for the status" do
ScribdAPI.expects(:get_status).never
scribdable_attachment_model
@doc_obj = Scribd::Document.new
ScribdAPI.expects(:upload).returns(@doc_obj)
@doc_obj.stubs(:thumbnail).returns("the url to the scribd doc thumbnail")
@attachment.submit_to_scribd!
@attachment.conversion_status.should == "PROCESSING"
end
end
context "download_url" do
before do
ScribdAPI.stubs(:set_user).returns(true)
@doc = mock('Scribd Document', :download_url => 'some url')
Scribd::Document.stubs(:find).returns(@doc)
end
end
context "named scopes" do
it "should have a scope for all scribdable attachments, regardless their state" do
(1..3).each { attachment_model }
(1..3).each { scribdable_attachment_model }
Attachment.scribdable?.size.should eql(3)
Attachment.all.size.should eql(6)
Attachment.scribdable?.each {|m| m.should be_scribdable}
end
it "should have a scope for uploadable models, all models that are in the pending_upload state" do
attachment_model
attachments = [@attachment]
@attachment.submit_to_scribd!
attachment_model
attachments << @attachment
scribdable_attachment_model
attachments << @attachment
Attachment.all.size.should eql(3)
Attachment.uploadable.size.should eql(2)
Attachment.uploadable.should be_include(Attachment.find(attachments[1].id))
Attachment.uploadable.should be_include(Attachment.find(attachments[2].id))
end
end
context "uploaded_data" do
it "should create with uploaded_data" do
a = attachment_model(:uploaded_data => default_uploaded_data)
a.filename.should eql("doc.doc")
end
context "uploading and db transactions" do
self.use_transactional_fixtures = false
before do
attachment_model(:context => Group.create!, :filename => 'test.mp4', :content_type => 'video')
end
after do
truncate_table(Attachment)
truncate_table(Folder)
truncate_table(Group)
end
it "should delay upload until the #save transaction is committed" do
@attachment.uploaded_data = default_uploaded_data
@attachment.connection.expects(:after_transaction_commit).once
@attachment.expects(:touch_context_if_appropriate).never
@attachment.expects(:build_media_object).never
@attachment.save
end
it "should upload immediately when in a non-joinable transaction" do
Attachment.connection.transaction(:joinable => false) do
@attachment.uploaded_data = default_uploaded_data
Attachment.connection.expects(:after_transaction_commit).never
@attachment.expects(:touch_context_if_appropriate)
@attachment.expects(:build_media_object)
@attachment.save
end
end
end
end
context "build_media_object" do
before :each do
@course = course
@attachment = @course.attachments.build(:filename => 'foo.mp4')
@attachment.content_type = 'video'
@attachment.stubs(:downloadable?).returns(true)
end
it "should be called automatically upon creation" do
@attachment.expects(:build_media_object).once
@attachment.save!
end
it "should create a media object for videos" do
MediaObject.expects(:send_later_enqueue_args).once
@attachment.save!
end
it "should delay the creation of the media object by attachment_build_media_object_delay_seconds" do
now = Time.now
Time.stubs(:now).returns(now)
Setting.stubs(:get).returns(nil)
Setting.expects(:get).with('attachment_build_media_object_delay_seconds', '10').once.returns('25')
track_jobs do
@attachment.save!
end
MediaObject.count.should == 0
job = created_jobs.first
job.tag.should == 'MediaObject.add_media_files'
job.run_at.to_i.should == (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
MediaObject.expects(:send_later_enqueue_args).times(0)
@attachment.save!
end
end
it "should not create a media object for images" do
@attachment.filename = 'foo.png'
@attachment.content_type = 'image/png'
@attachment.expects(:build_media_object).once
MediaObject.expects(:send_later_enqueue_args).times(0)
@attachment.save!
end
it "should create a media object *after* a direct-to-s3 upload" do
MediaObject.expects(:send_later_enqueue_args).never
@attachment.workflow_state = 'unattached'
@attachment.file_state = 'deleted'
@attachment.save!
MediaObject.expects(:send_later_enqueue_args).once
@attachment.workflow_state = nil
@attachment.file_state = 'available'
@attachment.save!
end
end
context "destroy" do
it "should not actually destroy" do
a = attachment_model(:uploaded_data => default_uploaded_data)
a.filename.should eql("doc.doc")
a.destroy
a.should_not be_frozen
a.should be_deleted
end
it "should not probably be possible to actually destroy... somehow" do
a = attachment_model(:uploaded_data => default_uploaded_data)
a.filename.should eql("doc.doc")
a.destroy
a.should_not be_frozen
a.should be_deleted
a.destroy!
a.should be_frozen
end
it "should not show up in the context list after being destroyed" do
@course = course
@course.should_not be_nil
a = attachment_model(:uploaded_data => default_uploaded_data, :context => @course)
a.filename.should eql("doc.doc")
a.context.should eql(@course)
a.destroy
a.should_not be_frozen
a.should be_deleted
@course.attachments.should be_include(a)
@course.attachments.active.should_not be_include(a)
end
end
context "inferred display name" do
it "should take a normal filename and use it as a diplay name" do
a = attachment_model(:filename => 'normal_name.ppt')
a.display_name.should eql('normal_name.ppt')
end
it "should take a normal filename with spaces and convert the underscores to spaces" do
a = attachment_model(:filename => 'normal_name.ppt')
a.display_name.should eql('normal_name.ppt')
end
it "should preserve case" do
a = attachment_model(:filename => 'Normal_naMe.ppt')
a.display_name.should eql('Normal_naMe.ppt')
end
it "should split long names with dashes" do
a = attachment_model(:filename => 'this is a long name, over 30 characters long.ppt')
a.display_name.should eql('this is a long name, over 30 characters long.ppt')
end
it "shouldn't try to break up very large words" do
a = attachment_model(:filename => 'A long Bulgarian word is neprotifconstitutiondeistveiteneprotifconstitutiondeistveite')
a.display_name.should eql('A long Bulgarian word is neprotifconstitutiondeistveiteneprotifconstitutiondeistveite')
end
it "should truncate filenames that are just too freaking big" do
fn = Attachment.new.sanitize_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')
fn.should eql("My+new+study+guide+or+case+study+on+this+evolution+on+monkeys+even+in+that+land+of+costa+rica+somewhere+my+own.docx")
end
end
context "clone_for" do
it "should clone to another context" do
a = attachment_model(:filename => "blech.ppt")
course
new_a = a.clone_for(@course)
new_a.context.should_not eql(a.context)
new_a.filename.should eql(a.filename)
new_a.root_attachment_id.should eql(a.id)
end
it "should link the thumbnail" do
a = attachment_model(:uploaded_data => stub_png_data, :content_type => 'image/png')
a.thumbnail.should_not be_nil
course
new_a = a.clone_for(@course)
new_a.thumbnail.should_not be_nil
new_a.thumbnail_url.should_not be_nil
new_a.thumbnail_url.should == 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')
a.root_attachment_id.should be_nil
coursea = @course
@context = courseb = course
b = a.clone_for(courseb, nil, :overwrite => true)
b.save
b.context.should == courseb
b.root_attachment.should == a
new_a = b.clone_for(coursea, nil, :overwrite => true)
new_a.should == a
new_a.root_attachment_id.should be_nil
new_b = new_a.clone_for(courseb, nil, :overwrite => true)
new_b.root_attachment_id.should == a.id
new_b = b.clone_for(courseb, nil, :overwrite => true)
new_b.root_attachment_id.should == a.id
@context = coursec = course
c = b.clone_for(coursec, nil, :overwrite => true)
c.root_attachment.should == a
new_a = c.clone_for(coursea, nil, :overwrite => true)
new_a.should == a
new_a.root_attachment_id.should 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)
new_b.root_attachment_id.should be_nil
end
end
context "adheres_to_policy" do
it "should not allow unauthorized users to read files" do
user = user_model
a = attachment_model
@course.update_attribute(:is_public, false)
a.grants_right?(user, nil, :read).should eql(false)
end
it "should allow anonymous access for public contexts" do
user = user_model
a = attachment_model
@course.update_attribute(:is_public, true)
a.grants_right?(user, nil, :read).should eql(false)
end
it "should allow students to read files" do
a = attachment_model
@course.update_attribute(:is_public, false)
user = user_model
@course.offer
@course.enroll_student(user).accept
a.reload
a.grants_right?(user, nil, :read).should eql(true)
end
it "should allow students to download files" do
a = attachment_model
@course.offer
@course.update_attribute(:is_public, false)
user = user_model
@course.enroll_student(user).accept
a.reload
a.grants_right?(user, nil, :download).should eql(true)
end
it "should allow students to read (but not download) locked files" do
a = attachment_model
a.update_attribute(:locked, true)
@course.offer
@course.update_attribute(:is_public, false)
user = user_model
@course.enroll_student(user).accept
a.reload
a.grants_right?(user, nil, :read).should eql(true)
a.grants_right?(user, nil, :download).should eql(false)
end
it "should allow user access based on 'file_access_user_id' and 'file_access_expiration' in the session" do
a = attachment_model
@course.offer
@course.update_attribute(:is_public, false)
user = user_model
@course.enroll_student(user).accept
a.reload
a.grants_right?(nil, nil, :read).should eql(false)
a.grants_right?(nil, nil, :read).should eql(false)
a.grants_right?(nil, {'file_access_user_id' => user.id, 'file_access_expiration' => 1.hour.from_now.to_i}, :read).should eql(true)
a.grants_right?(nil, {'file_access_user_id' => user.id, 'file_access_expiration' => 1.hour.from_now.to_i}, :download).should eql(true)
end
it "should not allow user access based on incorrect 'file_access_user_id' in the session" do
a = attachment_model
@course.offer
@course.update_attribute(:is_public, false)
user = user_model
@course.enroll_student(user).accept
a.reload
a.grants_right?(nil, nil, :read).should eql(false)
a.grants_right?(nil, nil, :read).should eql(false)
a.grants_right?(nil, {'file_access_user_id' => 0, 'file_access_expiration' => 1.hour.from_now.to_i}, :read).should eql(false)
end
it "should not allow user access based on incorrect 'file_access_expiration' in the session" do
a = attachment_model
@course.offer
@course.update_attribute(:is_public, false)
user = user_model
@course.enroll_student(user).accept
a.reload
a.grants_right?(nil, nil, :read).should eql(false)
a.grants_right?(nil, nil, :read).should eql(false)
a.grants_right?(nil, {'file_access_user_id' => user.id, 'file_access_expiration' => 1.minute.ago.to_i}, :read).should eql(false)
end
end
context "duplicate handling" do
before(:each) 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)
@a.file_state.should == 'available'
@a1.reload
@a1.file_state.should == 'deleted'
deleted.should == [ @a1 ]
end
it "should handle renaming duplicates" do
@a.display_name = 'a1'
deleted = @a.handle_duplicates(:rename)
deleted.should be_empty
@a.file_state.should == 'available'
@a1.reload
@a1.file_state.should == 'available'
@a.display_name.should == 'a1-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!
@a.display_name = 'a1'
@a.handle_duplicates(:overwrite)
tag1.reload
tag1.should be_active
tag1.content_id.should == @a.id
@a2.destroy
tag2.reload
tag2.should be_deleted
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)
Attachment.make_unique_filename("d.txt", existing_files).should == "d.txt"
existing_files.should_not 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)
Attachment.make_unique_filename("/a/b/d.txt", existing_files).should == "/a/b/d.txt"
new_name = Attachment.make_unique_filename("/a/b/b.txt", existing_files)
existing_files.should_not be_include(new_name)
new_name.should match(%r{^/a/b/b[^.]+\.txt})
end
end
context "cacheable s3 urls" do
before(:each) do
course_model
end
it "should include response-content-disposition" do
attachment = attachment_with_context(@course, :display_name => 'foo')
attachment.expects(:authenticated_s3_url).at_least(0) # allow other calls due to, e.g., save
attachment.expects(:authenticated_s3_url).with(has_entry(:response_content_disposition => %(attachment; filename="foo"; filename*=UTF-8''foo)))
attachment.expects(:authenticated_s3_url).with(has_entry(:response_content_disposition => %(inline; filename="foo"; filename*=UTF-8''foo)))
attachment.cacheable_s3_inline_url
attachment.cacheable_s3_download_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')
attachment.expects(:authenticated_s3_url).at_least(0) # allow other calls due to, e.g., save
attachment.expects(:authenticated_s3_url).with(has_entry(:response_content_disposition => %(attachment; filename="foo"; filename*=UTF-8''foo)))
attachment.cacheable_s3_inline_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')
attachment.expects(:authenticated_s3_url).at_least(0) # allow other calls due to, e.g., save
attachment.expects(:authenticated_s3_url).with(has_entry(:response_content_disposition => %(attachment; filename="fo\\"o"; filename*=UTF-8''fo%22o)))
attachment.cacheable_s3_inline_url
end
it "should sanitize filename with iconv" do
a = attachment_with_context(@course, :display_name => "糟糕.pdf")
sanitized_filename = Iconv.conv("ASCII//TRANSLIT//IGNORE", "UTF-8", a.display_name)
a.expects(:authenticated_s3_url).at_least(0)
a.expects(:authenticated_s3_url).with(has_entry(:response_content_disposition => %(attachment; filename="#{sanitized_filename}"; filename*=UTF-8''%E7%B3%9F%E7%B3%95.pdf)))
a.cacheable_s3_inline_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"')
attachment.expects(:authenticated_s3_url).at_least(0) # allow other calls due to, e.g., save
attachment.expects(:authenticated_s3_url).with(has_entry(: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.cacheable_s3_inline_url
end
end
context "root_account_id" do
before 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}"
@a.root_account_id.should == @account.id
end
it "should return account id for localstorage namespaces" do
@a.namespace = "_localstorage_/#{@account.file_namespace}"
@a.root_account_id.should == @account.id
end
it "should immediately infer the namespace if not yet set" do
Attachment.domain_namespace = nil
@a = Attachment.new(:context => @course)
@a.should be_new_record
@a.read_attribute(:namespace).should be_nil
@a.namespace.should_not be_nil
@a.read_attribute(:namespace).should_not be_nil
@a.root_account_id.should == @account.id
end
it "should not infer the namespace if it's not a new record" do
Attachment.domain_namespace = nil
attachment_model(:context => submission_model)
@attachment.should_not be_new_record
@attachment.read_attribute(:namespace).should be_nil
@attachment.namespace.should be_nil
@attachment.read_attribute(:namespace).should be_nil
end
end
context "encoding detection" do
it "should include the charset when appropriate" do
a = Attachment.new
a.content_type = 'text/html'
a.content_type_with_encoding.should == 'text/html'
a.encoding = ''
a.content_type_with_encoding.should == 'text/html'
a.encoding = 'UTF-8'
a.content_type_with_encoding.should == 'text/html; charset=UTF-8'
a.encoding = 'mycustomencoding'
a.content_type_with_encoding.should == '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;"))
@attachment.encoding.should be_nil
@attachment.infer_encoding
# can't figure out GIF encoding
@attachment.encoding.should == ''
attachment_model(:uploaded_data => stub_png_data('blank.txt', "Hello World!"))
@attachment.encoding.should be_nil
@attachment.infer_encoding
@attachment.encoding.should == 'UTF-8'
attachment_model(:uploaded_data => stub_png_data('blank.txt', "\xc2\xa9 2011"))
@attachment.encoding.should be_nil
@attachment.infer_encoding
@attachment.encoding.should == 'UTF-8'
end
end
context "sharding" do
specs_require_sharding
it "should infer scribd mime type regardless of shard" do
scribd_mime_type_model(:extension => 'pdf')
attachment_model(:content_type => 'application/pdf')
@attachment.should be_scribdable
Attachment.clear_cached_mime_ids
@shard1.activate do
# need to create a context on this shard
@context = course_model(:account => Account.create!)
attachment_model(:content_type => 'application/pdf')
@attachment.should be_scribdable
end
end
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!
user.attachments.build.grants_right?(user, nil, :read).should be_true
end
@shard2.activate do
user.attachments.build.grants_right?(user, nil, :read).should be_true
end
user.attachments.build.grants_right?(user, nil, :read).should be_true
end
end
context "#change_namespace" do
before do
s3_storage!
@old_account = account_model
Attachment.domain_namespace = @old_account.file_namespace
@root = attachment_model
@child = attachment_model(:root_attachment => @root)
@new_account = account_model
end
it "should fail for non-root attachments" do
AWS::S3::S3Object.any_instance.expects(:rename_to).never
expect { @child.change_namespace(@new_account.file_namespace) }.to raise_error
@root.reload.namespace.should == @old_account.file_namespace
@child.reload.namespace.should == @root.reload.namespace
end
it "should rename root attachments and update children" do
AWS::S3::S3Object.any_instance.expects(:rename_to).with(@root.full_filename.sub(@old_account.id.to_s, @new_account.id.to_s), anything)
@root.change_namespace(@new_account.file_namespace)
@root.namespace.should == @new_account.file_namespace
@child.reload.namespace.should == @root.namespace
end
end
context "dynamic thumbnails" do
let(:sz) { CollectionItemData::THUMBNAIL_SIZE }
before do
attachment_model(:uploaded_data => stub_png_data)
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")
url.should be_present
url.should == @attachment.thumbnail.authenticated_s3_url
end
it "should generate the thumbnail on the fly" do
thumb = @attachment.thumbnails.find_by_thumbnail("640x>")
thumb.should == nil
@attachment.expects(:create_or_update_thumbnail).with(anything, sz, sz).returns { @attachment.thumbnails.create!(:thumbnail => "640x>", :uploaded_data => stub_png_data) }
url = @attachment.thumbnail_url(:size => "640x>")
url.should be_present
thumb = @attachment.thumbnails.find_by_thumbnail("640x>")
thumb.should be_present
url.should == thumb.authenticated_s3_url
end
it "should use the existing thumbnail if present" do
@attachment.expects(:create_or_update_thumbnail).with(anything, sz, sz).returns { @attachment.thumbnails.create!(:thumbnail => "640x>", :uploaded_data => stub_png_data) }
url = @attachment.thumbnail_url(:size => "640x>")
@attachment.expects(:create_dynamic_thumbnail).never
url = @attachment.thumbnail_url(:size => "640x>")
thumb = @attachment.thumbnails.find_by_thumbnail("640x>")
url.should be_present
thumb.should be_present
url.should == thumb.authenticated_s3_url
end
describe 'when its a scribd document' do
before do
@attachment.scribd_doc = Scribd::Document.new
ScribdAPI.expects(:enabled?).times(0)
end
it 'returns the cached thumbnail if present' do
@attachment.cached_scribd_thumbnail = "THUMBNAIL_URL"
@attachment.thumbnail_url.should == "THUMBNAIL_URL"
end
it 'just returns nil if there is no cached thumbnail' do
@attachment.thumbnail_url.should be_nil
end
end
end
describe '.allows_thumbnails_for_size' do
it 'inevitably returns false if there is no size provided' do
Attachment.allows_thumbnails_of_size?(nil).should be_false
end
it 'returns true if the provided size is in the configured dynamic sizes' do
Attachment.allows_thumbnails_of_size?(Attachment::DYNAMIC_THUMBNAIL_SIZES.first).should be_true
end
it 'returns false if the provided size is not in the configured dynamic sizes' do
Attachment.allows_thumbnails_of_size?('nonsense').should be_false
end
end
context "notifications" do
before :each 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!
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")
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')
@attachment.need_notify.should be_true
new_time = Time.now + 10.minutes
Time.stubs(:now).returns(new_time)
Attachment.do_notifications
@attachment.reload
@attachment.need_notify.should_not be_true
Message.find_by_user_id_and_notification_name(@student.id, 'New File Added').should_not 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| att.need_notify.should be_true}
new_time = Time.now + 10.minutes
Time.stubs(:now).returns(new_time)
Attachment.do_notifications
[att1, att2, att3].each {|att| att.reload.need_notify.should_not be_true}
Message.find_by_user_id_and_notification_name(@student.id, 'New Files Added').should_not 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')
@attachment.need_notify.should_not be_true
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| att.need_notify.should be_true}
new_time = Time.now + 2.minutes
Time.stubs(:now).returns(new_time)
Attachment.do_notifications
[att1, att2, att3].each {|att| att.reload.need_notify.should be_true}
Message.find_by_user_id_and_notification_name(@student.id, 'New Files Added').should be_nil
new_time = Time.now + 4.minutes
Time.stubs(:now).returns(new_time)
Attachment.do_notifications
[att1, att2, att3].each {|att| att.reload.need_notify.should_not be_true}
Message.find_by_user_id_and_notification_name(@student.id, 'New Files Added').should_not 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')
@attachment.need_notify.should be_true
new_time = Time.now + 1.week
Time.stubs(:now).returns(new_time)
Attachment.do_notifications
@attachment.reload
@attachment.need_notify.should be_false
Message.find_by_user_id_and_notification_name(@student.id, 'New Files Added').should be_nil
Message.find_by_user_id_and_notification_name(@student.id, 'New File Added').should 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')
att1.need_notify.should_not be_true
att1.file_state = 'available'
att1.save!
att1.need_notify.should be_true
att2.need_notify.should_not be_true
att2.file_state = 'available'
att2.save_without_broadcasting
att2.need_notify.should_not be_true
att3.need_notify.should_not be_true
att3.file_state = 'available'
att3.save_without_broadcasting!
att3.need_notify.should_not be_true
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.find_by_name('New File Added'), :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!
new_time = Time.now + 10.minutes
Time.stubs(:now).returns(new_time)
Attachment.do_notifications
@attachment.reload
@attachment.need_notify.should_not be_true
Message.find_by_user_id_and_notification_name(@student.id, 'New File Added').should be_nil
Message.find_by_user_id_and_notification_name(@teacher.id, 'New File Added').should_not 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.find_by_name('New File Added'), :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!
new_time = Time.now + 10.minutes
Time.stubs(:now).returns(new_time)
Attachment.do_notifications
@attachment.reload
@attachment.need_notify.should_not be_true
Message.find_by_user_id_and_notification_name(@student.id, 'New File Added').should be_nil
Message.find_by_user_id_and_notification_name(@teacher.id, 'New File Added').should_not 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)
quota[:quota_used].should == Attachment.minimum_size_for_quota
end
end
context "#open" do
context "s3_storage" do
before do
s3_storage!
attachment_model
@attachment.s3object.class.any_instance.expects(:read).yields("test")
end
it "should stream data to the block given" do
callback = false
@attachment.open { |data| data.should == "test"; callback = true }
callback.should == true
end
it "should stream to a tempfile without a block given" do
file = @attachment.open
file.should be_a(Tempfile)
file.read.should == "test"
end
end
end
end
def processing_model
ScribdAPI.stubs(:get_status).returns(:status_from_scribd)
ScribdAPI.stubs(:set_user).returns(true)
ScribdAPI.stubs(:upload).returns(Scribd::Document.new)
scribdable_attachment_model
@attachment.submit_to_scribd!
end
# Makes sure we have a value in scribd_mime_types and that the attachment model points to that.
def scribdable_attachment_model
scribd_mime_type_model(:extension => 'pdf')
attachment_model(:content_type => 'application/pdf')
end
def crocodocable_attachment_model
attachment_model(:content_type => 'application/pdf')
end