Carrierwave + RSpec testing - ruby-on-rails

I'm testing that my file uploads are correct with models with RSpec by setting Model#filename = File.open(etc)
When my specs run, all is grand. But the files still remain in my public/uploads directory after the testsuite finishes.
How do I get the files to delete on the end of running along with the records?
Thanks!

https://github.com/jnicklas/carrierwave/wiki/How-to:-Cleanup-after-your-Rspec-tests

One way is to delete them at the end of a test using an instance method exposed by carrierwave. An example where your instance is #user and your carrierwave file attribute is avatar would be: #user.remove_avatar!

Related

CarrierWave Multiple File Upload Rails Unit Testing

I have been banging my head with Carrierwave. I finally was able to get carrier wave to upload multiple files by adding the master branch for Carrierwave to my Gemfile:
gem 'carrierwave', github:'carrierwaveuploader/carrierwave'
I used it for a model called Product and I did this.
Product.rb
class Product < ActiveRecord::Base
mount_uploaders :avatar, ProductUploader
end
And I essentially followed the rules and was able to upload multiple files all while having each file iterate through the create method in my products_controller.rb and creating a new Product instance for each file being uploaded.
Now. Here comes the Testing. Before, when only one file was being uploaded I was able to use,
test "should create product" do
login
excel_filename = 'files/product_create_test.xls'
file = fixture_file_upload(excel_filename, 'application/excel')
assert_difference('Product.count') do
post :create, product: {:file_url => file}
end
But now, after adding the ability to upload multiple files, it seems like fixture_file_upload is not working properly.
I am getting this error:
ProductsControllerTest#test_should_create_product:
ArgumentError: wrong number of arguments (2 for 0)
test/controllers/products_controller_test.rb:54:in `block (2 levels) in <class:ProductsControllerTest>'
test/controllers/products_controller_test.rb:53:in `block in <class:ProductsControllerTest>'
I am not sure how to go around this. Like I said, when I had
gem 'carrierwave'
the previous test worked fine. Has anyone ever encountered this?
Here's what the problem was. In order to make it so that my fixture file uploads did not actually save in my test enviroment, I followed this article, http://jeffkreeftmeijer.com/2014/using-test-fixtures-with-carrierwave/#comment_768.
Here it tells you to add the following to your test_helper file
class CarrierWave::Mount::Mounter
def store!
# Not storing uploads in the tests
end
end
This essentially tells your testing environment not to store the uploaded fixtures anywhere. Anyway, this piece of code does not fly well with the addition of the carrier wave master branch for multiple file uploads. I ended up uncommenting it and despite the fact that is stores the uploaded files, my tests pass.

Stubbing Paperclip downloads from S3 in RSpec

I am using Paperclip/RSpec and StackOverflow has helped me successfully stub file uploads to S3 using this code:
spec/rails_helper.rb
config.before(:each) do
allow_any_instance_of(Paperclip::Attachment).to receive(:save).and_return(true)
end
This is working great.
On my model I have two Paperclip fields:
class MyModel < ActiveRecord::Base
has_attached_file :pdf
has_attached_file :resource
end
My code uses the #copy_to_local_file method (Docs) to retrieve a file from S3.
#copy_to_local_file takes two params: the style (:original, :thumbnail, etc) and the local file path to copy to.
Example:
MyModel.resource.copy_to_local_file(:original, local_file.path)
When the system under test tries to access MyModel#pdf#copy_to_local_file or MyModel#resource#copy_to_local_file, I originally got errors like the following:
No Such Key - cannot copy /email_receipts/pdfs/000/000/001/original/email_receipt.eml.pdf to local file /var/folders/4p/1mm86g0n58x7d9rvpy88_s9h0000gn/T/receipt20150917-4906-13evk95.pdf
No Such Key - cannot copy /email_receipts/resources/000/000/001/original/email_receipt.eml to local file /var/folders/4p/1mm86g0n58x7d9rvpy88_s9h0000gn/T/resource20150917-4906-1ysbwr3.eml
I realize these errors were happening because uploads to S3 are stubbed, so when it encounters MyModel#pdf#copy_to_local_file or MyModel#resource#copy_to_local_file it tries to grab a file in S3 that isn't there.
Current Solution:
I've managed to quash the errors above, but I feel it's not a complete solution and gives my tests a false sense of security. My half-solution is to stub this method in the following way:
spec/rails_helper.rb
before(:each) do
allow_any_instance_of(Paperclip::Storage::S3).to receive(:copy_to_local_file)
end
While this does stub out the #copy_to_local_file method and removes the errors, it doesn't actually write any content to the local file that is provided as the second argument to #copy_to_local_file, so it doesn't quite simulate the file being downloaded from S3.
Question:
Is there a way to stub #copy_to_local_file AND have it write the contents of a canned file in my spec/factories/files directory to the local file (its second argument)?
Or am I overthinking this? Is this something I shouldn't be worrying about?
You don't need to worry about whether the 'downloaded' files actually exist in your tests. You've decided to stub out Paperclip, so do it completely, by stubbing out both #save and #copy_to_file. You may also need to stub out reads of downloaded files from the filesystem.
All this stubbing raises the possibility of integration errors, so you should probably write a feature spec (using a captive browser like poltergeist) that actually uploads and downloads something and reads it from the filesystem.
That said, you can do anything you want in an RSpec stub by passing it a block:
allow_any_instance_of(Paperclip::Storage::S3).to receive(:copy_to_local_file) do |style, local_dest_path|
# write a file here, or do anything you like
end

Paperclip 4, Amazon S3 & rspec: how to stub file upload?

I'm writing tests with rspec and am a bit struggling with Paperclip 4. At the moment I'm using webmock to stub requests but image processing is slowing tests down.
Everything I read suggest to use stubs like so:
Profile.any_instance.stub(:save_attached_files).and_return(true)
It doesn't work since :save_attached_files disappeared with Paperclip 4 as far as I can tell.
What's the proper way to do it now ?
Thanks
Add this to your rails_helper.rb in your config block:
RSpec.configure do |config|
# Stub saving of files to S3
config.before(:each) do
allow_any_instance_of(Paperclip::Attachment).to receive(:save).and_return(true)
end
end
It doesn't exactly answer my question, but I found a way to run faster specs thanks to this dev blog, so I'm posting it if it can help someone else.
Just add this piece of code at the beginning of your spec file or in a helper. My tests are running 3x faster now.
# We stub some Paperclip methods - so it won't call shell slow commands
# This allows us to speedup paperclip tests 3-5x times.
module Paperclip
def self.run cmd, params = "", expected_outcodes = 0
cmd == 'convert' ? nil : super
end
end
class Paperclip::Attachment
def post_process
end
end
Paperclip > 3.5.2
For newer versions of Paperclip, use the following:
module Paperclip
def self.run cmd, arguments = "", interpolation_values = {}, local_options = {}
cmd == 'convert' ? nil : super
end
end
class Paperclip::Attachment
def post_process
end
end
I had a heckuva time dealing with this. I didn't want to test Paperclip per se -- rather, I needed a file to exist on my model so I can test a very sensitive custom class.
None of the other answers worked for me (Rails 5, Rspec 3.5) so I gave up on stubbing AWS or using the Paperclip::Shoulda::Matchers. Nothing worked and it ate up an entire day! Finally I gave up on Paperclip altogether and went this route:
list = build(:list)
allow(list).to receive_message_chain("file.url") { "#{Rails.root}/spec/fixtures/guess_fullname.csv" }
importer = Importer.new(list)
expect(importer.set_key_mapping).to eq({nil=>:email_address, :company=>:company, :fullname=>:full_name})
Where my models/list.rb has has_attached_file :file and Importer is a class I wrote that takes the file, which was already uploaded to S3 by paperclip before the class is initialized, and processes it. The file.url would otherwise be the S3 URL, so I give it the path in my repo to a testing csv file. set_key_mapping is a method in my class that I can use to verify the processing part worked.
Hope this saves somebody a few hours...
What about adding AWS.stub! in your spec/spec_helper.rb? Give that a try.

What's the proper way to copy a carrierwave file from one record to another?

I need to copy a file from one carrier wave object to another. They are different tables and different types of uploaders.
I started with:
user.avatar = image.content
(where user and image are model instances, avatar and content are the carrierwave mounted uploaders) which worked sometimes. It seems to work all the time locally, with a file storage, but intermittent when using fog and s3.
In a mailing list post I found this code:
user.avatar = image.content.file
that again worked sometimes.
My working solution so far is:
require "open-uri"
begin
user.avatar = open(image.url)
rescue Errno::ENOENT => e
begin
user.avatar = open(image.path)
rescue Errno::ENOENT => e
# Ok, whatever.
end
end
which is not only ugly, but fails to pass the extension validation because the opening of a remote file doesn't maintain the extension (jpg, png, etc.).
Perhaps one way you can do it is to set a remote image URL as per the Carrierwave gem documentation?
user.remote_avatar_url = image.url
From solutions discussed here I created simple CopyCarrierwaveFile gem to do this
usage is something like this:
original_resource = User.last
new_resource = User.new
CopyCarrierwaveFile::CopyFileService.new(original_resource, new_resource, :avatar).set_file
new_resource.save
nev_resource.avatar.url # https://...image.jpg
Here's a (albeit hacky) solution to that doesn't require an HTTP request to fetch the image:
module UploadCopier
def self.copy(old, new)
new.instance_variable_set('#_mounters', nil)
old.class.uploaders.each do |column, uploader|
new.send("#{column}=", old.send(column))
end
end
end
old_user = User.last
new_user = User.new
UploadCopier.copy(old_user, new_user)
new_user.save
I needed to copy a reference from one model to another model and I was successfully able to do so by doing the following:
my_new_model.update_column('attachment', my_other_model.attributes["attachment"]);
In this scenario, I did not care to actually make a copy of the file, nor did I care that 2 records were now linked to the same file (my system never deletes or modifies files after uploaded).
This may be useful to anyone who wants to just copy the reference to a file from one model to another model using the same uploader.
You can do this by copying files.
store_path is a carrierwave method from Uploader class. It returns uploaded file's folder relative path.
this clone file method should be called after model record is saved.
If record not saved, store_path may return wrong path if you specify store_dir with model id in uploader.
def clone_carrierwave_file(column_name)
origin_files = Dir[File.join(Rails.root, 'public', original_record.send(column_name).store_path, '*')]
return if origin_files.blank?
new_file_folder = File.join(Rails.root, 'public', send(column_name).store_path)
FileUtils.mkdir new_file_folder if !Dir.exist? new_file_folder
FileUtils.cp(origin_files, new_file_folder)
end
Hope it works.
I just wanted to copy an avatar reference from one object to another, and what worked for me was:
objectB.avatar.retrieve_from_store!(objectA.avatar.identifier)
objectB.save

How to make carrierwave delete the file when destroying a record?

I'm using the carrierwave gem to upload files.
I have built a system for users to flag images as inappropriate and for admins to remove the images. From what I can tell, calling destroy on the image will only remove the path name from the table.
Is there a way to have carrierwave actually remove the file itself? Or should rails automatically remove the file when I destroy the image path?
Like #mu_is_too_short said, you can use File#delete.
Here's a code snippet you could use as a helper with a little tweaking in your rails app.
def remove_file(file)
File.delete(file)
end
or if you just have the filename stored in file
def remove_file(file)
File.delete("./path/to/#{file}")
end
Not sure what CarrierWave offers for this, but you could use FileUtils in the Ruby standard library with an ActiveRecord callback.
For instance,
require 'FileUtils'
before_destroy :remove_hard_image
def remove_hard_image
FileUtils.rm(path_to_image)
end
Sidenote: This code is from memory.
If one wants to delete a file but does not want to specify the full filename you can use the below.
Can also be used to delete many files or all files in a directory with a specific extension...
file = Rails.root.join("tmp", "foo*")
or
file = Rails.root.join("tmp", ".pdf")
files = Dir.glob(file) #will build an array of the full filepath & filename(s)
files.each do |f|
File.delete(f)
end

Resources