require "spec_helper" describe Paperclip::Attachment do it "is not present when file not set" do rebuild_class dummy = Dummy.new expect(dummy.avatar).to be_blank expect(dummy.avatar).to_not be_present end it "is present when the file is set" do rebuild_class dummy = Dummy.new dummy.avatar = File.new(fixture_file("50x50.png"), "rb") expect(dummy.avatar).to_not be_blank expect(dummy.avatar).to be_present end it "processes :original style first" do file = File.new(fixture_file("50x50.png"), "rb") rebuild_class styles: { small: "100x>", original: "42x42#" } dummy = Dummy.new dummy.avatar = file dummy.save # :small avatar should be 42px wide (processed original), not 50px (preprocessed original) expect(`identify -format "%w" "#{dummy.avatar.path(:small)}"`.strip).to eq "42" file.close end it "does not delete styles that don't get reprocessed" do file = File.new(fixture_file("50x50.png"), "rb") rebuild_class styles: { small: "100x>", large: "500x>", original: "42x42#" } dummy = Dummy.new dummy.avatar = file dummy.save expect(dummy.avatar.path(:small)).to exist expect(dummy.avatar.path(:large)).to exist expect(dummy.avatar.path(:original)).to exist dummy.avatar.reprocess!(:small) expect(dummy.avatar.path(:small)).to exist expect(dummy.avatar.path(:large)).to exist expect(dummy.avatar.path(:original)).to exist end it "reprocess works with virtual content_type attribute" do rebuild_class styles: { small: "100x>" } modify_table { |t| t.remove :avatar_content_type } Dummy.send :attr_accessor, :avatar_content_type Dummy.validates_attachment_content_type( :avatar, content_type: %w(image/jpeg image/png) ) Dummy.create!(avatar: File.new(fixture_file("50x50.png"), "rb")) dummy = Dummy.first dummy.avatar.reprocess!(:small) expect(dummy.avatar.path(:small)).to exist end context "having a not empty hash as a default option" do before do @old_default_options = Paperclip::Attachment.default_options.dup @new_default_options = { convert_options: { all: "-background white" } } Paperclip::Attachment.default_options.merge!(@new_default_options) end after do Paperclip::Attachment.default_options.merge!(@old_default_options) end it "deep merges when it is overridden" do new_options = { convert_options: { thumb: "-thumbnailize" } } attachment = Paperclip::Attachment.new(:name, :instance, new_options) expect(Paperclip::Attachment.default_options.deep_merge(new_options)).to eq attachment.instance_variable_get("@options") end end it "handles a boolean second argument to #url" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new( :name, FakeModel.new, url_generator: mock_url_generator_builder ) attachment.url(:style_name, true) expect(mock_url_generator_builder.has_generated_url_with_options?(timestamp: true, escape: true)).to eq true attachment.url(:style_name, false) expect(mock_url_generator_builder.has_generated_url_with_options?(timestamp: false, escape: true)).to eq true end it "passes the style and options through to the URL generator on #url" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new( :name, FakeModel.new, url_generator: mock_url_generator_builder ) attachment.url(:style_name, options: :values) expect(mock_url_generator_builder.has_generated_url_with_options?(options: :values)).to eq true end it "passes default options through when #url is given one argument" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, url_generator: mock_url_generator_builder, use_timestamp: true) attachment.url(:style_name) assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true) end it "passes default style and options through when #url is given no arguments" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, default_style: "default style", url_generator: mock_url_generator_builder, use_timestamp: true) attachment.url assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true) assert mock_url_generator_builder.has_generated_url_with_style_name?("default style") end it "passes the option timestamp: true if :use_timestamp is true and :timestamp is not passed" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, url_generator: mock_url_generator_builder, use_timestamp: true) attachment.url(:style_name) assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true) end it "passes the option timestamp: false if :use_timestamp is false and :timestamp is not passed" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, url_generator: mock_url_generator_builder, use_timestamp: false) attachment.url(:style_name) assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: false) end it "does not change the :timestamp if :timestamp is passed" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, url_generator: mock_url_generator_builder, use_timestamp: false) attachment.url(:style_name, timestamp: true) assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true) end it "renders JSON as default style" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, default_style: "default style", url_generator: mock_url_generator_builder) attachment.as_json assert mock_url_generator_builder.has_generated_url_with_style_name?("default style") end it "passes the option escape: true if :escape_url is true and :escape is not passed" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, url_generator: mock_url_generator_builder, escape_url: true) attachment.url(:style_name) assert mock_url_generator_builder.has_generated_url_with_options?(escape: true) end it "passes the option escape: false if :escape_url is false and :escape is not passed" do mock_url_generator_builder = MockUrlGeneratorBuilder.new attachment = Paperclip::Attachment.new(:name, FakeModel.new, url_generator: mock_url_generator_builder, escape_url: false) attachment.url(:style_name) assert mock_url_generator_builder.has_generated_url_with_options?(escape: false) end it "returns the path based on the url by default" do @attachment = attachment url: "/:class/:id/:basename" @model = @attachment.instance @model.id = 1234 @model.avatar_file_name = "fake.jpg" assert_equal "#{Rails.root}/public/fake_models/1234/fake", @attachment.path end it "defaults to a path that scales" do avatar_attachment = attachment model = avatar_attachment.instance model.id = 1234 model.avatar_file_name = "fake.jpg" expected_path = "#{Rails.root}/public/system/fake_models/avatars/000/001/234/original/fake.jpg" assert_equal expected_path, avatar_attachment.path end it "renders JSON as the URL to the attachment" do avatar_attachment = attachment model = avatar_attachment.instance model.id = 1234 model.avatar_file_name = "fake.jpg" assert_equal attachment.url, attachment.as_json end it "renders JSON from the model when requested by :methods" do rebuild_model dummy = Dummy.new dummy.id = 1234 dummy.avatar_file_name = "fake.jpg" allow(dummy).to receive(:new_record?).and_return(false) expected_string = '{"avatar":"/system/dummies/avatars/000/001/234/original/fake.jpg"}' # active_model pre-3.2 checks only by calling any? on it, thus it doesn't work if it is empty assert_equal expected_string, dummy.to_json(only: [:dummy_key_for_old_active_model], methods: [:avatar]) end context "Attachment default_options" do before do rebuild_model @old_default_options = Paperclip::Attachment.default_options.dup @new_default_options = @old_default_options.merge( path: "argle/bargle", url: "fooferon", default_url: "not here.png" ) end after do Paperclip::Attachment.default_options.merge! @old_default_options end it "is overrideable" do Paperclip::Attachment.default_options.merge!(@new_default_options) @new_default_options.keys.each do |key| assert_equal @new_default_options[key], Paperclip::Attachment.default_options[key] end end context "without an Attachment" do before do rebuild_model default_url: "default.url" @dummy = Dummy.new end it "returns false when asked exists?" do assert !@dummy.avatar.exists? end it "#url returns the default_url" do expect(@dummy.avatar.url).to eq "default.url" end end context "on an Attachment" do before do @dummy = Dummy.new @attachment = @dummy.avatar end Paperclip::Attachment.default_options.keys.each do |key| it "is the default_options for #{key}" do assert_equal @old_default_options[key], @attachment.instance_variable_get("@options")[key], key.to_s end end context "when redefined" do before do Paperclip::Attachment.default_options.merge!(@new_default_options) @dummy = Dummy.new @attachment = @dummy.avatar end Paperclip::Attachment.default_options.keys.each do |key| it "is the new default_options for #{key}" do assert_equal @new_default_options[key], @attachment.instance_variable_get("@options")[key], key.to_s end end end end end context "An attachment with similarly named interpolations" do before do rebuild_model path: ":id.omg/:id-bbq/:idwhat/:id_partition.wtf" @dummy = Dummy.new allow(@dummy).to receive(:id).and_return(1024) @file = File.new(fixture_file("5k.png"), "rb") @dummy.avatar = @file end after { @file.close } it "makes sure that they are interpolated correctly" do assert_equal "1024.omg/1024-bbq/1024what/000/001/024.wtf", @dummy.avatar.path end end context "An attachment with :timestamp interpolations" do before do @file = StringIO.new("...") @zone = "UTC" allow(Time).to receive(:zone).and_return(@zone) @zone_default = "Eastern Time (US & Canada)" allow(Time).to receive(:zone_default).and_return(@zone_default) end context "using default time zone" do before do rebuild_model path: ":timestamp", use_default_time_zone: true @dummy = Dummy.new @dummy.avatar = @file end it "returns a time in the default zone" do assert_equal @dummy.avatar_updated_at.in_time_zone(@zone_default).to_s, @dummy.avatar.path end end context "using per-thread time zone" do before do rebuild_model path: ":timestamp", use_default_time_zone: false @dummy = Dummy.new @dummy.avatar = @file end it "returns a time in the per-thread zone" do assert_equal @dummy.avatar_updated_at.in_time_zone(@zone).to_s, @dummy.avatar.path end end end context "An attachment with :hash interpolations" do before do @file = File.open(fixture_file("5k.png")) end after do @file.close end it "raises if no secret is provided" do rebuild_model path: ":hash" @attachment = Dummy.new.avatar @attachment.assign @file assert_raises ArgumentError do @attachment.path end end context "when secret is set" do before do rebuild_model path: ":hash", hash_secret: "w00t", hash_data: ":class/:attachment/:style/:filename" @attachment = Dummy.new.avatar @attachment.assign @file end it "results in the correct interpolation" do assert_equal "dummies/avatars/original/5k.png", @attachment.send(:interpolate, @attachment.options[:hash_data]) assert_equal "dummies/avatars/thumb/5k.png", @attachment.send(:interpolate, @attachment.options[:hash_data], :thumb) end it "results in a correct hash" do assert_equal "0a59e9142bba11576de1d353d8747b1acad5ad34", @attachment.path assert_equal "b39a062c1e62e85a6c785ed00cf3bebf5f850e2b", @attachment.path(:thumb) end end end context "An attachment with a :rails_env interpolation" do before do @rails_env = "blah" @id = 1024 rebuild_model path: ":rails_env/:id.png" @dummy = Dummy.new allow(@dummy).to receive(:id).and_return(@id) @file = StringIO.new(".") @dummy.avatar = @file allow(Rails).to receive(:env).and_return(@rails_env) end it "returns the proper path" do assert_equal "#{@rails_env}/#{@id}.png", @dummy.avatar.path end end context "An attachment with a default style and an extension interpolation" do before do rebuild_model path: ":basename.:extension", styles: { default: ["100x100", :jpg] }, default_style: :default @attachment = Dummy.new.avatar @file = File.open(fixture_file("5k.png")) allow(@file).to receive(:original_filename).and_return("file.png") end it "returns the right extension for the path" do @attachment.assign(@file) assert_equal "file.jpg", @attachment.path end end context "An attachment with :convert_options" do before do rebuild_model styles: { thumb: "100x100", large: "400x400" }, convert_options: { all: "-do_stuff", thumb: "-thumbnailize" } @dummy = Dummy.new @dummy.avatar end it "reports the correct options when sent #extra_options_for(:thumb)" do assert_equal "-thumbnailize -do_stuff", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect end it "reports the correct options when sent #extra_options_for(:large)" do assert_equal "-do_stuff", @dummy.avatar.send(:extra_options_for, :large) end end context "An attachment with :source_file_options" do before do rebuild_model styles: { thumb: "100x100", large: "400x400" }, source_file_options: { all: "-density 400", thumb: "-depth 8" } @dummy = Dummy.new @dummy.avatar end it "reports the correct options when sent #extra_source_file_options_for(:thumb)" do assert_equal "-depth 8 -density 400", @dummy.avatar.send(:extra_source_file_options_for, :thumb), @dummy.avatar.source_file_options.inspect end it "reports the correct options when sent #extra_source_file_options_for(:large)" do assert_equal "-density 400", @dummy.avatar.send(:extra_source_file_options_for, :large) end end context "An attachment with :only_process" do before do rebuild_model styles: { thumb: "100x100", large: "400x400" }, only_process: [:thumb] @file = StringIO.new("...") @attachment = Dummy.new.avatar end it "only processes the provided style" do expect(@attachment).to receive(:post_process).with(:thumb) expect(@attachment).to receive(:post_process).with(:large).never @attachment.assign(@file) end end context "An attachment with :only_process that is a proc" do before do rebuild_model styles: { thumb: "100x100", large: "400x400" }, only_process: lambda { |_attachment| [:thumb] } @file = StringIO.new("...") @attachment = Dummy.new.avatar end it "only processes the provided style" do expect(@attachment).to receive(:post_process).with(:thumb) expect(@attachment).to receive(:post_process).with(:large).never @attachment.assign(@file) @attachment.save end end context "An attachment with :convert_options that is a proc" do before do rebuild_model styles: { thumb: "100x100", large: "400x400" }, convert_options: { all: lambda { |i| i.all }, thumb: lambda { |i| i.thumb } } Dummy.class_eval do def all; "-all"; end def thumb; "-thumb"; end end @dummy = Dummy.new @dummy.avatar end it "reports the correct options when sent #extra_options_for(:thumb)" do assert_equal "-thumb -all", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect end it "reports the correct options when sent #extra_options_for(:large)" do assert_equal "-all", @dummy.avatar.send(:extra_options_for, :large) end end context "An attachment with :path that is a proc" do before do rebuild_model path: lambda { |attachment| "path/#{attachment.instance.other}.:extension" } @file = File.new(fixture_file("5k.png"), "rb") @dummyA = Dummy.new(other: "a") @dummyA.avatar = @file @dummyB = Dummy.new(other: "b") @dummyB.avatar = @file end after { @file.close } it "returns correct path" do assert_equal "path/a.png", @dummyA.avatar.path assert_equal "path/b.png", @dummyB.avatar.path end end context "An attachment with :styles that is a proc" do before do rebuild_model styles: lambda { |_attachment| { thumb: "50x50#", large: "400x400" } } @attachment = Dummy.new.avatar end it "has the correct geometry" do assert_equal "50x50#", @attachment.styles[:thumb][:geometry] end end context "An attachment with conditional :styles that is a proc" do before do rebuild_model styles: lambda { |attachment| attachment.instance.other == "a" ? { thumb: "50x50#" } : { large: "400x400" } } @dummy = Dummy.new(other: "a") end it "has the correct styles for the assigned instance values" do assert_equal "50x50#", @dummy.avatar.styles[:thumb][:geometry] assert_nil @dummy.avatar.styles[:large] @dummy.other = "b" assert_equal "400x400", @dummy.avatar.styles[:large][:geometry] assert_nil @dummy.avatar.styles[:thumb] end end geometry_specs = [ [lambda { |_z| "50x50#" }, :png], lambda { |_z| "50x50#" }, { geometry: lambda { |_z| "50x50#" } } ] geometry_specs.each do |geometry_spec| context "An attachment geometry like #{geometry_spec}" do before do rebuild_model styles: { normal: geometry_spec } @attachment = Dummy.new.avatar end context "when assigned" do before do @file = StringIO.new(".") @attachment.assign(@file) end it "has the correct geometry" do assert_equal "50x50#", @attachment.styles[:normal][:geometry] end end end end context "An attachment with both 'normal' and hash-style styles" do before do rebuild_model styles: { normal: ["50x50#", :png], hash: { geometry: "50x50#", format: :png } } @dummy = Dummy.new @attachment = @dummy.avatar end [:processors, :whiny, :convert_options, :geometry, :format].each do |field| it "has the same #{field} field" do assert_equal @attachment.styles[:normal][field], @attachment.styles[:hash][field] end end end context "An attachment with :processors that is a proc" do before do class Paperclip::Test < Paperclip::Processor; end @file = StringIO.new("...") allow(Paperclip::Test).to receive(:make).and_return(@file) rebuild_model styles: { normal: "" }, processors: lambda { |_a| [:test] } @attachment = Dummy.new.avatar end context "when assigned" do before do @attachment.assign(StringIO.new(".")) end it "has the correct processors" do assert_equal [:test], @attachment.styles[:normal][:processors] end end end context "An attachment with erroring processor" do before do rebuild_model processor: [:thumbnail], styles: { small: "" }, whiny_thumbnails: true @dummy = Dummy.new @file = StringIO.new("...") allow(@file).to receive(:to_tempfile).and_return(@file) end context "when error is meaningful for the end user" do before do expect(Paperclip::Thumbnail).to receive(:make).and_raise( Paperclip::Errors::NotIdentifiedByImageMagickError, "cannot be processed." ) end it "correctly forwards processing error message to the instance" do @dummy.avatar = @file @dummy.valid? assert_contains( @dummy.errors.full_messages, "Avatar cannot be processed." ) end end context "when error is intended for the developer" do before do expect(Paperclip::Thumbnail).to receive(:make).and_raise( Paperclip::Errors::CommandNotFoundError ) end it "propagates the error" do assert_raises(Paperclip::Errors::CommandNotFoundError) do @dummy.avatar = @file end end end end context "An attachment with multiple processors" do before do class Paperclip::Test < Paperclip::Processor; end @style_params = { once: { one: 1, two: 2 } } rebuild_model processors: [:thumbnail, :test], styles: @style_params @dummy = Dummy.new @file = StringIO.new("...") allow(@file).to receive(:close) allow(Paperclip::Test).to receive(:make).and_return(@file) allow(Paperclip::Thumbnail).to receive(:make).and_return(@file) end context "when assigned" do it "calls #make on all specified processors" do @dummy.avatar = @file expect(Paperclip::Thumbnail).to have_received(:make) expect(Paperclip::Test).to have_received(:make) end it "calls #make with the right parameters passed as second argument" do expected_params = @style_params[:once].merge( style: :once, processors: [:thumbnail, :test], whiny: true, convert_options: "", source_file_options: "" ) @dummy.avatar = @file expect(Paperclip::Thumbnail).to have_received(:make).with(anything, expected_params, anything) end it "calls #make with attachment passed as third argument" do @dummy.avatar = @file expect(Paperclip::Test).to have_received(:make).with(anything, anything, @dummy.avatar) end it "calls #make and unlinks intermediary files afterward" do expect(@dummy.avatar).to receive(:unlink_files).with([@file, @file]) @dummy.avatar = @file end end end context "An attachment with a processor that returns original file" do before do class Paperclip::Test < Paperclip::Processor def make; @file; end end rebuild_model processors: [:test], styles: { once: "100x100" } @file = StringIO.new("...") allow(@file).to receive(:close) @dummy = Dummy.new end context "when assigned" do it "#calls #make and doesn't unlink the original file" do expect(@dummy.avatar).to receive(:unlink_files).with([]) @dummy.avatar = @file end end end it "includes the filesystem module when loading the filesystem storage" do rebuild_model storage: :filesystem @dummy = Dummy.new assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem) end it "includes the filesystem module even if capitalization is wrong" do rebuild_model storage: :FileSystem @dummy = Dummy.new assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem) rebuild_model storage: :Filesystem @dummy = Dummy.new assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem) end it "converts underscored storage name to camelcase" do rebuild_model storage: :not_here @dummy = Dummy.new exception = assert_raises(Paperclip::Errors::StorageMethodNotFound, /NotHere/) do @dummy.avatar end end it "raises an error if you try to include a storage module that doesn't exist" do rebuild_model storage: :not_here @dummy = Dummy.new assert_raises(Paperclip::Errors::StorageMethodNotFound) do @dummy.avatar end end context "An attachment with styles but no processors defined" do before do rebuild_model processors: [], styles: { something: "1" } @dummy = Dummy.new @file = StringIO.new("...") end it "raises when assigned to" do assert_raises(RuntimeError) { @dummy.avatar = @file } end end context "An attachment without styles and with no processors defined" do before do rebuild_model processors: [], styles: {} @dummy = Dummy.new @file = StringIO.new("...") end it "does not raise when assigned to" do @dummy.avatar = @file end end context "Assigning an attachment with post_process hooks" do before do rebuild_class styles: { something: "100x100#" } Dummy.class_eval do before_avatar_post_process :do_before_avatar after_avatar_post_process :do_after_avatar before_post_process :do_before_all after_post_process :do_after_all def do_before_avatar; end def do_after_avatar; end def do_before_all; end def do_after_all; end end @file = StringIO.new(".") allow(@file).to receive(:to_tempfile).and_return(@file) @dummy = Dummy.new allow(Paperclip::Thumbnail).to receive(:make).and_return(@file) @attachment = @dummy.avatar end it "calls the defined callbacks when assigned" do expect(@dummy).to receive(:do_before_avatar) expect(@dummy).to receive(:do_after_avatar) expect(@dummy).to receive(:do_before_all) expect(@dummy).to receive(:do_after_all) expect(Paperclip::Thumbnail).to receive(:make).and_return(@file) @dummy.avatar = @file end it "does not cancel the processing if a before_post_process returns nil" do expect(@dummy).to receive(:do_before_avatar).and_return(nil) expect(@dummy).to receive(:do_after_avatar) expect(@dummy).to receive(:do_before_all).and_return(nil) expect(@dummy).to receive(:do_after_all) expect(Paperclip::Thumbnail).to receive(:make).and_return(@file) @dummy.avatar = @file end it "cancels the processing if a before_post_process returns false" do expect(@dummy).to receive(:do_before_avatar).never expect(@dummy).to receive(:do_after_avatar).never expect(@dummy).to receive(:do_before_all).and_return(false) expect(@dummy).to receive(:do_after_all) expect(Paperclip::Thumbnail).not_to receive(:make) @dummy.avatar = @file end it "cancels the processing if a before_avatar_post_process returns false" do expect(@dummy).to receive(:do_before_avatar).and_return(false) expect(@dummy).to receive(:do_after_avatar) expect(@dummy).to receive(:do_before_all).and_return(true) expect(@dummy).to receive(:do_after_all) expect(Paperclip::Thumbnail).not_to receive(:make) @dummy.avatar = @file end it "should not call process hooks if validation fails" do Dummy.class_eval do validates_attachment_content_type :avatar, content_type: 'image/jpeg' end expect(@dummy).not_to receive(:do_before_avatar) expect(@dummy).not_to receive(:do_after_avatar) expect(@dummy).not_to receive(:do_before_all) expect(@dummy).not_to receive(:do_after_all) expect(Paperclip::Thumbnail).not_to receive(:make) @dummy.avatar = @file end it "should call process hooks if validation would fail but check validity flag is false" do Dummy.class_eval do validates_attachment_content_type :avatar, content_type: 'image/jpeg' end @dummy.avatar.options[:check_validity_before_processing] = false expect(@dummy).to receive(:do_before_avatar) expect(@dummy).to receive(:do_after_avatar) expect(@dummy).to receive(:do_before_all) expect(@dummy).to receive(:do_after_all) expect(Paperclip::Thumbnail).to receive(:make).and_return(@file) @dummy.avatar = @file end end context "Assigning an attachment" do before do rebuild_model styles: { something: "100x100#" } @file = File.new(fixture_file("5k.png"), "rb") @dummy = Dummy.new @dummy.avatar = @file end it "strips whitespace from original_filename field" do assert_equal "5k.png", @dummy.avatar.original_filename end it "strips whitespace from content_type field" do assert_equal "image/png", @dummy.avatar.instance.avatar_content_type end end context "Assigning an attachment" do before do rebuild_model styles: { something: "100x100#" } @file = File.new(fixture_file("5k.png"), "rb") @dummy = Dummy.new @dummy.avatar = @file end it "makes sure the content_type is a string" do assert_equal "image/png", @dummy.avatar.instance.avatar_content_type end end context "Attachment with strange letters" do before do rebuild_model @file = File.new(fixture_file("5k.png"), "rb") allow(@file).to receive(:original_filename).and_return("sheep_say_bæ.png") @dummy = Dummy.new @dummy.avatar = @file end it "does not remove strange letters" do assert_equal "sheep_say_bæ.png", @dummy.avatar.original_filename end end context "Attachment with reserved filename" do before do rebuild_model @file = Tempfile.new(["filename", "png"]) end after do @file.unlink end context "with default configuration" do "&$+,/:;=?@<>[]{}|\^~%# ".split(//).each do |character| context "with character #{character}" do context "at beginning of filename" do before do allow(@file).to receive(:original_filename).and_return("#{character}filename.png") @dummy = Dummy.new @dummy.avatar = @file end it "converts special character into underscore" do assert_equal "_filename.png", @dummy.avatar.original_filename end end context "at end of filename" do before do allow(@file).to receive(:original_filename).and_return("filename.png#{character}") @dummy = Dummy.new @dummy.avatar = @file end it "converts special character into underscore" do assert_equal "filename.png_", @dummy.avatar.original_filename end end context "in the middle of filename" do before do allow(@file).to receive(:original_filename).and_return("file#{character}name.png") @dummy = Dummy.new @dummy.avatar = @file end it "converts special character into underscore" do assert_equal "file_name.png", @dummy.avatar.original_filename end end end end end context "with specified regexp replacement" do before do @old_defaults = Paperclip::Attachment.default_options.dup end after do Paperclip::Attachment.default_options.merge! @old_defaults end context "as another regexp" do before do Paperclip::Attachment.default_options.merge! restricted_characters: /o/ allow(@file).to receive(:original_filename).and_return("goood.png") @dummy = Dummy.new @dummy.avatar = @file end it "matches and converts that character" do assert_equal "g___d.png", @dummy.avatar.original_filename end end context "as nil" do before do Paperclip::Attachment.default_options.merge! restricted_characters: nil allow(@file).to receive(:original_filename).and_return("goood.png") @dummy = Dummy.new @dummy.avatar = @file end it "ignores and returns the original file name" do assert_equal "goood.png", @dummy.avatar.original_filename end end end context "with specified cleaner" do before do @old_defaults = Paperclip::Attachment.default_options.dup end after do Paperclip::Attachment.default_options.merge! @old_defaults end it "calls the given proc and take the result as cleaned filename" do Paperclip::Attachment.default_options[:filename_cleaner] = lambda do |str| "from_proc_#{str}" end allow(@file).to receive(:original_filename).and_return("goood.png") @dummy = Dummy.new @dummy.avatar = @file assert_equal "from_proc_goood.png", @dummy.avatar.original_filename end it "calls the given object and take the result as the cleaned filename" do class MyCleaner def call(_filename) "foo" end end Paperclip::Attachment.default_options[:filename_cleaner] = MyCleaner.new allow(@file).to receive(:original_filename).and_return("goood.png") @dummy = Dummy.new @dummy.avatar = @file assert_equal "foo", @dummy.avatar.original_filename end end end context "Attachment with uppercase extension and a default style" do before do @old_defaults = Paperclip::Attachment.default_options.dup Paperclip::Attachment.default_options.merge!( path: ":rails_root/:attachment/:class/:style/:id/:basename.:extension" ) FileUtils.rm_rf("tmp") rebuild_model styles: { large: ["400x400", :jpg], medium: ["100x100", :jpg], small: ["32x32#", :jpg] }, default_style: :small @instance = Dummy.new allow(@instance).to receive(:id).and_return 123 @file = File.new(fixture_file("uppercase.PNG"), "rb") @attachment = @instance.avatar now = Time.now allow(Time).to receive(:now).and_return(now) @attachment.assign(@file) @attachment.save end after do @file.close Paperclip::Attachment.default_options.merge!(@old_defaults) end it "has matching to_s and url methods" do assert_equal @attachment.to_s, @attachment.url assert_equal @attachment.to_s(:small), @attachment.url(:small) end it "has matching expiring_url and url methods when using the filesystem storage" do assert_equal @attachment.expiring_url, @attachment.url end end context "An attachment" do before do @old_defaults = Paperclip::Attachment.default_options.dup Paperclip::Attachment.default_options.merge!( path: ":rails_root/:attachment/:class/:style/:id/:basename.:extension" ) FileUtils.rm_rf("tmp") rebuild_model @instance = Dummy.new allow(@instance).to receive(:id).and_return 123 # @attachment = Paperclip::Attachment.new(:avatar, @instance) @attachment = @instance.avatar @file = File.new(fixture_file("5k.png"), "rb") end after do @file.close Paperclip::Attachment.default_options.merge!(@old_defaults) end it "raises if there are not the correct columns when you try to assign" do @other_attachment = Paperclip::Attachment.new(:not_here, @instance) assert_raises(Paperclip::Error) do @other_attachment.assign(@file) end end it "clears out the previous assignment when assigned nil" do @attachment.assign(@file) @attachment.queued_for_write[:original] @attachment.assign(nil) assert_nil @attachment.queued_for_write[:original] end it "does not do anything when it is assigned an empty string" do @attachment.assign(@file) original_file = @attachment.queued_for_write[:original] @attachment.assign("") assert_equal original_file, @attachment.queued_for_write[:original] end it "returns nil as path when no file assigned" do assert_equal nil, @attachment.path assert_equal nil, @attachment.path(:blah) end context "with a file assigned but not saved yet" do it "clears out any attached files" do @attachment.assign(@file) assert @attachment.queued_for_write.present? @attachment.clear assert @attachment.queued_for_write.blank? end end context "with a file assigned in the database" do before do allow(@attachment).to receive(:instance_read).with(:file_name).and_return("5k.png") allow(@attachment).to receive(:instance_read).with(:content_type).and_return("image/png") allow(@attachment).to receive(:instance_read).with(:file_size).and_return(12345) dtnow = DateTime.now @now = Time.now allow(Time).to receive(:now).and_return(@now) allow(@attachment).to receive(:instance_read).with(:updated_at).and_return(dtnow) end it "returns the proper path when filename has a single .'s" do assert_equal File.expand_path("tmp/avatars/dummies/original/#{@instance.id}/5k.png"), File.expand_path(@attachment.path) end it "returns the proper path when filename has multiple .'s" do allow(@attachment).to receive(:instance_read).with(:file_name).and_return("5k.old.png") assert_equal File.expand_path("tmp/avatars/dummies/original/#{@instance.id}/5k.old.png"), File.expand_path(@attachment.path) end context "when expecting three styles" do before do rebuild_class styles: { large: ["400x400", :png], medium: ["100x100", :gif], small: ["32x32#", :jpg] } @instance = Dummy.new allow(@instance).to receive(:id).and_return 123 @file = File.new(fixture_file("5k.png"), "rb") @attachment = @instance.avatar end context "and assigned a file" do before do now = Time.now allow(Time).to receive(:now).and_return(now) @attachment.assign(@file) end it "is dirty" do assert @attachment.dirty? end context "and saved" do before do @attachment.save end it "commits the files to disk" do [:large, :medium, :small].each do |style| expect(@attachment.path(style)).to exist end end it "saves the files as the right formats and sizes" do [[:large, 400, 61, "PNG"], [:medium, 100, 15, "GIF"], [:small, 32, 32, "JPEG"]].each do |style| cmd = %[identify -format "%w %h %b %m" "#{@attachment.path(style.first)}"] out = `#{cmd}` width, height, _size, format = out.split(" ") assert_equal style[1].to_s, width.to_s assert_equal style[2].to_s, height.to_s assert_equal style[3].to_s, format.to_s end end context "and trying to delete" do before do @existing_names = @attachment.styles.keys.map do |style| @attachment.path(style) end end it "deletes the files after assigning nil" do expect(@attachment).to receive(:instance_write).with(:file_name, nil) expect(@attachment).to receive(:instance_write).with(:content_type, nil) expect(@attachment).to receive(:instance_write).with(:file_size, nil) expect(@attachment).to receive(:instance_write).with(:fingerprint, nil) expect(@attachment).to receive(:instance_write).with(:updated_at, nil) @attachment.assign nil @attachment.save @existing_names.each { |f| assert_file_not_exists(f) } end it "deletes the files when you call #clear and #save" do expect(@attachment).to receive(:instance_write).with(:file_name, nil) expect(@attachment).to receive(:instance_write).with(:content_type, nil) expect(@attachment).to receive(:instance_write).with(:file_size, nil) expect(@attachment).to receive(:instance_write).with(:fingerprint, nil) expect(@attachment).to receive(:instance_write).with(:updated_at, nil) @attachment.clear @attachment.save @existing_names.each { |f| assert_file_not_exists(f) } end it "deletes the files when you call #delete" do expect(@attachment).to receive(:instance_write).with(:file_name, nil) expect(@attachment).to receive(:instance_write).with(:content_type, nil) expect(@attachment).to receive(:instance_write).with(:file_size, nil) expect(@attachment).to receive(:instance_write).with(:fingerprint, nil) expect(@attachment).to receive(:instance_write).with(:updated_at, nil) @attachment.destroy @existing_names.each { |f| assert_file_not_exists(f) } end context "when keeping old files" do before do @attachment.options[:keep_old_files] = true end it "keeps the files after assigning nil" do expect(@attachment).to receive(:instance_write).with(:file_name, nil) expect(@attachment).to receive(:instance_write).with(:content_type, nil) expect(@attachment).to receive(:instance_write).with(:file_size, nil) expect(@attachment).to receive(:instance_write).with(:fingerprint, nil) expect(@attachment).to receive(:instance_write).with(:updated_at, nil) @attachment.assign nil @attachment.save @existing_names.each { |f| assert_file_exists(f) } end it "keeps the files when you call #clear and #save" do expect(@attachment).to receive(:instance_write).with(:file_name, nil) expect(@attachment).to receive(:instance_write).with(:content_type, nil) expect(@attachment).to receive(:instance_write).with(:file_size, nil) expect(@attachment).to receive(:instance_write).with(:fingerprint, nil) expect(@attachment).to receive(:instance_write).with(:updated_at, nil) @attachment.clear @attachment.save @existing_names.each { |f| assert_file_exists(f) } end it "keeps the files when you call #delete" do expect(@attachment).to receive(:instance_write).with(:file_name, nil) expect(@attachment).to receive(:instance_write).with(:content_type, nil) expect(@attachment).to receive(:instance_write).with(:file_size, nil) expect(@attachment).to receive(:instance_write).with(:fingerprint, nil) expect(@attachment).to receive(:instance_write).with(:updated_at, nil) @attachment.destroy @existing_names.each { |f| assert_file_exists(f) } end end end end end end end context "when trying a nonexistant storage type" do before do rebuild_model storage: :not_here end it "is not able to find the module" do assert_raises(Paperclip::Errors::StorageMethodNotFound) { Dummy.new.avatar } end end end context "An attachment with only a avatar_file_name column" do before do ActiveRecord::Base.connection.create_table :dummies, force: true do |table| table.column :avatar_file_name, :string end rebuild_class @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), "rb") end after { @file.close } it "does not error when assigned an attachment" do assert_nothing_raised { @dummy.avatar = @file } end it "does not return the time when sent #avatar_updated_at" do @dummy.avatar = @file assert_nil @dummy.avatar.updated_at end it "returns the right value when sent #avatar_file_size" do @dummy.avatar = @file assert_equal File.size(@file), @dummy.avatar.size end context "and avatar_created_at column" do before do ActiveRecord::Base.connection.add_column :dummies, :avatar_created_at, :timestamp rebuild_class @dummy = Dummy.new end it "does not error when assigned an attachment" do assert_nothing_raised { @dummy.avatar = @file } end it "returns the creation time when sent #avatar_created_at" do now = Time.now allow(Time).to receive(:now).and_return(now) @dummy.avatar = @file assert_equal now.to_i, @dummy.avatar.created_at end it "returns the creation time when sent #avatar_created_at and the entry has been updated" do creation = 2.hours.ago now = Time.now allow(Time).to receive(:now).and_return(creation) @dummy.avatar = @file allow(Time).to receive(:now).and_return(now) @dummy.avatar = @file assert_equal creation.to_i, @dummy.avatar.created_at assert_not_equal now.to_i, @dummy.avatar.created_at end it "sets changed? to true on attachment assignment" do @dummy.avatar = @file @dummy.save! @dummy.avatar = @file assert @dummy.changed? end end context "and avatar_updated_at column" do before do ActiveRecord::Base.connection.add_column :dummies, :avatar_updated_at, :timestamp rebuild_class @dummy = Dummy.new end it "does not error when assigned an attachment" do assert_nothing_raised { @dummy.avatar = @file } end it "returns the right value when sent #avatar_updated_at" do now = Time.now allow(Time).to receive(:now).and_return(now) @dummy.avatar = @file assert_equal now.to_i, @dummy.avatar.updated_at end end it "does not calculate fingerprint" do allow(Digest::MD5).to receive(:file) @dummy.avatar = @file expect(Digest::MD5).not_to have_received(:file) end it "does not assign fingerprint" do @dummy.avatar = @file assert_nil @dummy.avatar.fingerprint end context "and avatar_content_type column" do before do ActiveRecord::Base.connection.add_column :dummies, :avatar_content_type, :string rebuild_class @dummy = Dummy.new end it "does not error when assigned an attachment" do assert_nothing_raised { @dummy.avatar = @file } end it "returns the right value when sent #avatar_content_type" do @dummy.avatar = @file assert_equal "image/png", @dummy.avatar.content_type end end context "and avatar_file_size column" do before do ActiveRecord::Base.connection.add_column :dummies, :avatar_file_size, :bigint rebuild_class @dummy = Dummy.new end it "does not error when assigned an attachment" do assert_nothing_raised { @dummy.avatar = @file } end it "returns the right value when sent #avatar_file_size" do @dummy.avatar = @file assert_equal File.size(@file), @dummy.avatar.size end it "returns the right value when saved, reloaded, and sent #avatar_file_size" do @dummy.avatar = @file @dummy.save @dummy = Dummy.find(@dummy.id) assert_equal File.size(@file), @dummy.avatar.size end end context "and avatar_fingerprint column" do before do ActiveRecord::Base.connection.add_column :dummies, :avatar_fingerprint, :string rebuild_class @dummy = Dummy.new end it "does not error when assigned an attachment" do assert_nothing_raised { @dummy.avatar = @file } end context "with explicitly set digest" do before do rebuild_class adapter_options: { hash_digest: Digest::SHA256 } @dummy = Dummy.new end it "returns the right value when sent #avatar_fingerprint" do @dummy.avatar = @file assert_equal "734016d801a497f5579cdd4ef2ae1d020088c1db754dc434482d76dd5486520a", @dummy.avatar_fingerprint end it "returns the right value when saved, reloaded, and sent #avatar_fingerprint" do @dummy.avatar = @file @dummy.save @dummy = Dummy.find(@dummy.id) assert_equal "734016d801a497f5579cdd4ef2ae1d020088c1db754dc434482d76dd5486520a", @dummy.avatar_fingerprint end end context "with the default digest" do before do rebuild_class # MD5 is the default @dummy = Dummy.new end it "returns the right value when sent #avatar_fingerprint" do @dummy.avatar = @file assert_equal "aec488126c3b33c08a10c3fa303acf27", @dummy.avatar_fingerprint end it "returns the right value when saved, reloaded, and sent #avatar_fingerprint" do @dummy.avatar = @file @dummy.save @dummy = Dummy.find(@dummy.id) assert_equal "aec488126c3b33c08a10c3fa303acf27", @dummy.avatar_fingerprint end end end end context "an attachment with delete_file option set to false" do before do rebuild_model preserve_files: true @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), "rb") @dummy.avatar = @file @dummy.save! @attachment = @dummy.avatar @path = @attachment.path end after { @file.close } it "does not delete the files from storage when attachment is destroyed" do @attachment.destroy assert_file_exists(@path) end it "clears out attachment data when attachment is destroyed" do @attachment.destroy assert !@attachment.exists? assert_nil @dummy.avatar_file_name end it "does not delete the file when model is destroyed" do @dummy.destroy assert_file_exists(@path) end end context "An attached file" do before do rebuild_model @dummy = Dummy.new @file = File.new(fixture_file("5k.png"), "rb") @dummy.avatar = @file @dummy.save! @attachment = @dummy.avatar @path = @attachment.path end after { @file.close } it "is not deleted when the model fails to destroy" do allow(@dummy).to receive(:destroy).and_raise(Exception) assert_raises Exception do @dummy.destroy end assert_file_exists(@path) end it "is deleted when the model is destroyed" do @dummy.destroy assert_file_not_exists(@path) end it "is not deleted when transaction rollbacks after model is destroyed" do ActiveRecord::Base.transaction do @dummy.destroy raise ActiveRecord::Rollback end assert_file_exists(@path) end end end