Test remote image url with carrierwave using rspec - ruby-on-rails

I want to speed up my spec tests for my remote image url upload using the carrierwave gem. In my model I will only allow an image upload by remote url not by upload. For testing the model I use FactoryGirl with the remote_image_url field and a placeholder image.
My tests run successful but it takes a lot of time to for each test (approx. 3-4 sec) because of downloading the image. I know I can stub my tests to avoid these external http requests but I do not have an idea how to do it with factory girl. Can anyone help?
Model:
class Store < ActiveRecord::Base
mount_uploader :image, StoreUploader
attr_accessible :name, :remote_image_url
validates :remote_image_url, presence: true
end
Factory:
FactoryGirl.define do
factory :store do
name "Corner store"
remote_image_url "http://placehold.it/800x600"
end
end
RspecController:
describe Api::StoreController, type: :api do
let!(:store) { create :store }
before(:each) do
get :show, id: store.id
end
it "returns a successful response" do
expect(response).to be_success
end
it "each store has the correct fields" do
expect(json).to match("name", "image")
end
end

I think you can use some public image in your dropbox, but if you want to have testing isolated and able to run offline then you can look at gem "webmock".
Other option start rack web-app in parallel thread and serve image from there.
require 'sinatra/base'
require 'webrick'
module TestingPurposeServer
def self.run!(port = nil)
if port.nil?
# ask system to pick free port for us
server = TCPServer.new('127.0.0.1', 0)
port = server.addr[1]
server.close
end
thread = Thread.new do
begin
options = { :Port => port, :BindAddress => '127.0.0.1', :AccessLog => [], :Logger => WEBrick::Log.new("/dev/null"), :OutputBufferSize => 5 } #WEBrick::Log::new("/dev/null") }
server = ::WEBrick::HTTPServer.new(options)
server.mount "/", Rack::Handler::WEBrick, VtDirectServer::Server
server.start
rescue Exception => e
puts e.message
puts e.backtrace
end
end
# wait for opening port
while port_open?('127.0.0.1', port, 1)
sleep 0.01
end
sleep 0.1
port
end
def self.port_open?(ip, port, seconds = 1)
Timeout::timeout(seconds) do
begin
TCPSocket.new(ip, port).close
true
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
false
end
end
rescue Timeout::Error
false
end
class Server < ::Sinatra::Base
# your sinatra app there
end
end
port = TestingPurposeServer.run!
local_server_url = "http://127.0.0.1:#{port}/"

Related

ruby on rails - fixture_file_upload not working with controller tests for CarrierWave

I'm having a problem with my controller integration tests on my ROR 5.2 project using CarrierWave and Minitest. I'm using fixture_file_upload to "upload" a file, which works correctly in my model tests, but fails in my controller integration tests due to the presence validation of the CarrierWave property in my model. It always fails on the create action. The update action occasionally and randomly fails too, even though I didn't update the CarrierWave property.
I used byebug to check the value of the property on the create action and it returns nothing; the property is never set. I also checked the errors of the newly created model and they are: "Icon can't be blank".
fixture_file_upload works fine in my model tests, and doing the creating/updating/uploading manually (not in tests) works fine too.
I've Googled for many hours trying to figure out what I'm doing wrong, but everything I find says to use fixture_file_upload, and I haven't found anything related to the problem I'm having.
CarrierWave initializer:
CarrierWave.configure do |config|
#To let CarrierWave work on heroku
config.root = Rails.root.join('tmp')
config.cache_dir = 'uploads/tmp'
if Rails.env.test? || Rails.env.development?
config.storage = :file
#config for tests is done in test/test_helper.rb
else
config.storage = :fog
config.fog_credentials = { #Configuration for Amazon S3
provider: 'AWS',
aws_access_key_id: Rails.application.credentials.aws[:access_key_id],
aws_secret_access_key: Rails.application.credentials.aws[:secret_access_key],
region: Rails.application.credentials.aws[:region]
}
config.fog_public = false
config.fog_directory = Rails.application.credentials.aws[:bucket_name]
config.fog_host = "#{Rails.application.credentials.aws[:asset_url]}/#{Rails.application.credentials.aws[:bucket_name]}"
end
end
Test helper:
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
include ActionDispatch::TestProcess #for fixture_file_upload
module UsersHelper
def login_as(user)
get login_user_url
assert_response :success
post user_login_url(session: { username: user.username, password: 'test1234' }) #have to hard code password here since passwords are stored encrypted
assert_redirected_to root_url, 'Login did not redirect'
end
def logout
get user_logout
end
end
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
class ActionDispatch::IntegrationTest
include UsersHelper
end
#setup carrierwave for tests
carrierwave_root = Rails.root.join('tmp', 'test', 'support', 'carrierwave')
carrierwave_template = Rails.root.join('test', 'fixtures', 'files')
CarrierWave.configure do |config|
config.root = carrierwave_root
config.cache_dir = carrierwave_root.join('carrierwave_cache')
config.enable_processing = false
end
#copy carrierwave fixture files to carrierwave root
puts 'Copying CarrierWave fixture files..'
puts carrierwave_template.join('uploads')
puts carrierwave_root
FileUtils.cp_r carrierwave_template.join('uploads'), carrierwave_root
Minitest.after_run do
#remove carrierwave files
puts 'Deleting CarrerWave fixture files...'
Dir.glob(Pathname.new(carrierwave_root).join('*')).each do |dir|
FileUtils.remove_entry(dir)
end
puts 'Cleaning CarrierWave cached files...'
CarrierWave.clean_cached_files!(0)
end
Model:
class Category < ApplicationRecord
mount_uploader :icon, IconUploader, dependent: :destroy
validates :name, length: { minimum: 2, maximum: 30 }, uniqueness: { case_sensitive: false }
validates :slug, length: { minimum: 2, maximum: 30 }, uniqueness: { case_sensitive: false }
validates :icon, presence: true
end
IconUploader:
class IconUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
after :remove, :delete_empty_upstream_dirs
def store_dir
"#{base_store_dir}/#{model.id}"
end
def base_store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}"
end
#override file name, for uniqueness
def filename
random_token = SecureRandom.hex(6/2) #length of 6 characters
token_var = "##{mounted_as}_secure_token" #get token variable name
token = model.instance_variable_get(token_var) #get token from token variable name
token ||= model.instance_variable_set(token_var, random_token) #if token isn't already set, set it
#name ||= "#{token}_#{super}" if original_filename.present? and super.present? #create name, using instance variable so token isn't changed (function is called multiple times)
end
#set size limits
def size_range
1.kilobyte..256.kilobytes #1 kilobyte to 256 kilobytes
end
#resize image if width or height is greater than 256px, add padding if needed
process resize_and_pad: [256, 256] #don't use resize_to_fit, as it adds a white background at least to SVG images
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_whitelist
%w(jpg jpeg png svg)
end
#whitelist of content types
def content_type_whitelist
/image\// #whitelist images
end
private
#delete directory if it's empty
def delete_empty_upstream_dirs
path = ::File.expand_path(store_dir, root)
Dir.delete(path) #fails if path not empty dir
path = ::File.expand_path(base_store_dir, root)
Dir.delete(path) #fails if path not empty dir
rescue SystemCallError => e
Rails.logger.error(e.message) #log error
true #nothing, the dir is not empty
end
end
Controller create action:
def create
data = params.require(:category).permit([ :name, :icon, :icon_cache ])
#category = Category.new(data)
if #category.save
flash.notice = 'Category successfully created.'
redirect_to categories_path
else
render :add #show errors
end
end
Controller test:
test "should post category_create when admin" do
login_as(users(:admin))
get add_category_url
assert_response :success
icon = fixture_file_upload(Rails.root.join('test', 'fixtures', 'files', 'category_icon.svg'))
#fails: validation error: "Icon can't be blank"
post category_create_url(category: { name: 'test901', icon: icon, icon_cache: '' })
assert_redirected_to categories_url
assert_equal 'Category successfully created.', flash[:notice]
end
Model test:
test "should save when all details correct" do
category = Category.new(name: 'tools',
icon: fixture_file_upload(Rails.root.join('test', 'fixtures', 'files', 'category_icon.svg')))
#succeeds
assert category.save, 'Not saved when all details correct: ' + category.errors.full_messages.to_s
end
post category_create_url(category: { name: 'test901', icon: icon, icon_cache: '' })
should be
post category_create_url, params: {category: { name: 'test901', icon: icon, icon_cache: '' }}
The first is sending params to the route_helper and results in an attempt to pass the file through query string parameters which won't work.
The second sends params to the post method which correctly posts the params as multipart/form data which will correctly post the file object to the controller.

Ruby on Rails - Get user email from database in a Rake Task

I have that Mailer method and a Rake task to later scheduled with Cron Job and automate with Gem Whenever for send a email to a users:
# Mailer:
class MailCourseWarnMailer < ActionMailer::Base
def course_available(user)
#user = user
mail(to: #user.email, subject: "Curso disponível") # ... email sending logic goes here
end
end
# Task
require 'rake'
desc 'send digest email'
task :send_warn_course, [:user_email] => :environment do |t, args|
user = MailCourseWarn.find_by_email args[:user_email]
MailCourseWarnMailer.course_available(user).deliver!
end
# Model
class MailCourseWarn < ActiveRecord::Base
belongs_to :course
validates :name, :email, presence: true
end
I am currently run the task like this: rake send_warn_course['user#email.com'] but I need that this be automatic, in a way that when run rake send_warn_course the Rake Task send to all of users in my DB.
Any ideas How can I do that? Thanks.
Find the user(s) that need the email and iterate over them. Surely you're indicating which users need the email in the database somehow, so you'll just want to query all those user records, iterate over them, and then send the email.
task :send_warn_course, [:user_email] => :environment do |t, args|
MailCourseWarn.where(needs_warned: true).each do |user|
MailCourseWarnMailer.course_available(user).deliver!
end
end

Request Spec failing on Association - Rails 5 API

I've got a request spec failing on a model with an association; when I binding.pry into it I can manually create the record with the same parameters, but when I pass them through the post '/path/model' request, it fails and says the association should exist. That's good - I want the association to be required. But the post action seems to be unable to pass.
# job model
belongs_to :worker
validates :title, presence: true
# worker model
has_many :jobs
# jobs controller
# POST /jobs
def create
#job = Job.create!(job_params)
json_response(#job, :created)
end
# jobs request spec
describe "POST /v1/jobs" do
context "when the request is valid" do
before {
post '/v1/jobs', params: {
title: "Whatever",
worker_id: Worker.first.id,
}
}
it "creates a job" do
puts request.body.read
puts response.body
expect(json["title"]).to eq("Whatever")
end
it "returns status code 201" do
expect(response).to have_http_status(201)
end
end
end
These tests both fail. The result of the puts statements above is:
title=Whatever&worker_id=21
{"message":"Validation failed: Worker must exist"}
If I put a binding.pry there instead, the following successfully creates a Job:
Job.create(title: "Whatever", worker_id: Worker.first.id)
My migrations are:
# jobs migration
create_table :jobs do |t|
t.references :worker, index: true
t.text :title
end
# worker migration
create_table :workers do |t|
t.text :first_name
...
end
What am I missing here? I know that the belongs_to association is now non-optional in Rails 5, but again, I want that. So I don't want to flag it optional just to get my tests to pass. Any ideas?
It looks like you haven't created a Worker for the spec to work with. Remember that your database is empty in each spec, so Worker.first.id will fail, because there is no workers in the database.
In your spec you want to create a worker (I use FactoryBot, the same concept applies to fixtures or whatever you're using):
context "when the request is valid" do
let!(:worker) { create :worker }
before {
post '/v1/jobs', params: {
title: "Whatever",
worker_id: worker.id,
}
}
it "creates a job" do
puts request.body.read
puts response.body
expect(json["title"]).to eq("Whatever")
end
it "returns status code 201" do
expect(response).to have_http_status(201)
end
end
I used let! because that will force the record to be created before the spec is run. Now Worker.first.id will find that worker, and you're on your way!
You're correct in that it's caused by belongs_to associations being required by default in Rails 5.
What I usually do is make it optional for creates: belongs_to :worker, optional: :new_record? and then write a test to ensure that it did get created.
Turns out this was a problem in the controller; I'm using strict parameters and didn't have the worker_id in the list of permitted params. Easy to overlook but it's a trap! Hope this helps someone else, especially since all the advice about the belongs_to association for Rails 5 says, "just make it optional."

How can I test ActionCable using RSpec?

This is my NotificationChannel
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_from "notification_user_#{user.id}"
end
def unsubscribed
stop_all_streams
end
end
How can I write test for this ActionCable channels
This is my Rspec
require 'rails_helper'
require_relative 'stubs/test_connection'
RSpec.describe NotificationChannel, type: :channel do
before do
#user = create(:user)
#connection = TestConnection.new(#user)
#channel = NotificationChannel.new #connection, {}
#action_cable = ActionCable.server
end
let(:data) do
{
"category" => "regular",
"region" => "us"
}
end
it 'notify user' do
#error is in below line
expect(#action_cable).to receive(:broadcast).with("notification_user_#{#user.id}")
#channel.perform_action(data)
end
end
when I run this spec it gives error
Wrong number of arguments. Expected 2, got 1
I used this link to write code for stub and this file.
Rails version - 5.0.0.1
Ruby version - 2.3.1
expect(#action_cable).to receive(:broadcast).with("notification_user_#{#user.id}")
Looking closely broadcast needs two parameters so
expect(#action_cable).to receive(:broadcast).with("notification_user_#{#user.id}", data)
I cant guess what is going on however one issue is
let(:data) do
{
"action" => 'action_name',
"category" => "regular",
"region" => "us"
}
end
You need an action for perform_action.
However you dont have any action defined in NotificationsChannel.
Otherwise you can try
NotificationChannel.broadcast_to("notification_user_#{#user.id}", data )

Rails scheduled SMS sender

I want send an SMS each 5 minutes to my users. At the moment, my application sends an SMS during the creation of an account.
# users_controller.rb
def create
#user = User.new(user_params)
if #user.save
#user.send_activation_email
#user.send_daily_sms
flash[:info] = "Veuillez contrôler votre boîte mail pour activer votre compte."
redirect_to root_url
else
render 'new'
end
end
# user.rb
def send_daily_sms
# put your own credentials here
account_sid = '**********************'
auth_token = '**********************'
# set up a client to talk to the Twilio REST API
#client = Twilio::REST::Client.new account_sid, auth_token
#client.account.messages.create({
:from => '**********',
:to => '***********',
:body => 'Salut',
})
end
I already have scheduled mails working in my project by doing this :
# schedule.rb
every :day, :at => '12pm' do
rake "email_sender_daily"
end
# My task
task :email_sender_daily => :environment do |_, args|
User.find_each do |user|
UserMailer.daily_mail(user).deliver_now if user.daily == true
end
end
# My UserMailer
def daily_mail(user)
#user = user
mail to: user.email, subject: "Mail journalier"
end
I'm showing you this because, with the UserMailer, I know how to access it from an other file. Here, I'd like to do the exactly the same for SMS, but how can I access the method that is in my Model ? If not, where can I put this method to be able to access it from my rake task ?
Twilio developer evangelist here.
It looks to me like you have all the parts you need. If send_daily_sms is a method in your User class then all you require is a rake task like so:
task :sms_sender_daily => :environment do |_, args|
User.find_each do |user|
user.send_daily_sms if user.daily == true
end
end
And then your schedule.rb would look like:
every :day, :at => '12pm' do
rake "email_sender_daily"
rake "sms_sender_daily"
end
I would warn that sending sms messages to all your users via one method that calls the API over and over again is somewhat fragile. If one message fails to send because of a timeout or some other error then the task will throw an error and not be able to complete sending all the messages.
I'd suggest sending both emails and sms messages by workers using a background queue, like Rails's ActiveJob. If you are on the latest Rails 4.2 then you can use a gem called Textris that works much like ActionMailer and then you could define a UserTexter class like this:
class UserTexter < Textris::Base
default :from => YOUR_NUMBER
def daily_sms(user)
#user = user
text :to => #user.phone_number
end
end
Then your tasks could look like this:
task :email_sender_daily => :environment do |_, args|
User.find_each do |user|
UserMailer.daily_mail(user).deliver_later if user.daily == true
end
end
task :sms_sender_daily => :environment do |_, args|
User.find_each do |user|
UserTexter.daily_sms(user).deliver_later if user.daily == true
end
end
Check out the Textris documentation for more on how to use the gem.
Let me know if this helps at all!

Resources