rails callbacks and order or events - ruby-on-rails-3

I have three different API calls happening when a record is created.
1) Call to create a bitly URL for the record.
2) Call to post to facebook with the bitly URL
3) Call to post to twitter with the bitly URL
My current process is as follows:
On record Create and Update
respond_to do |format|
if #dealer.save
call_bitly_api
end
end
in my model:
after_save :social_media_posting
def social_media_posting
if (self.bitly_url.present? && self.posted_to_facebook == false)
call_facebook_post
end
if (self.bitly_url.present? && self.posted_to_twitter == false)
call_twitter_post
end
end
The issue that I am facing is that the facebook and twitter post is being called on first save where the bitly_url is not yet created.
Need help is figuring out how to add these calls that they can still happen yet they happen in order and wait for the bitly_url to be present before the call the facebook and twitter is made? Major thing worth mentioning I am using sidekiq to make the calls and send the actual call to the sidekiq worker to work in the background.

In controller:
CallBitlyWorker.perform_async(dealer.id)
In your worker:
class CallBitlyWorker
include Sidekiq::Worker
def perform(dealer_id)
dealer = Dealer.find(dealer_id)
# make bitly call
dealer.update_attribute(:bitly_url, some_url)
# make the social media calls in parallel
CallFacebook.perform_async(dealer_id)
CallTwitter.perform_async(dealer_id)
end
end
ActiveRecord callbacks should be avoided where possible. They just make your code more opaque.

Related

Flask - How to require login only for post requests

I'm working in Flask and I want to allow users to enter information into a form without logging in but be required to login if they submit the form. After logging in, it should be as though a user just submitted the form (they shouldn't have to re-enter any information).
To store their information, I've used sessions like this. It works well:
if request.method == "POST":
if "arg1" not in session.keys() and "arg2" not in session.keys():
session["arg1"] = request.form.get('arg1')
session["arg2"] = request.form.get('arg2')
However, I'm having trouble with the login required part. I know I can use #login_required on the whole route but I just want #login_required to apply if the request is a post method. I've tried simply adding #login_required after checking if the method is a post request but it doesn't work.
My login route looks like this:
#app.route("/login", methods = ["POST", "GET"])
def login():
#log user in
return redirect(request.args.get("next") or url_for('index'))
It seems as though I need two things.
1: To apply #login_required solely to a post request.
2: To have request.args.get("next") call a post request, not get request
How could I go about doing these two things and achieve my goal?
Thank you!
Break out your routes. 1 for GET and 1 for POST
#app.route("/login", methods = ["GET"])
def get_login():
return stuff
#app.route("/login", methods = ["POST"])
#login_required
def post_login():
return stuff
There are a couple patterns that could be used here but this one is the most straight forward.

How to use Pundit with Actioncable (Rails 5)

I am wondering how to limit the connection to a channel or the streaming of messages over a channel in rails5. Currently I groups with users in the groups working with pundit and the connection to the websocket happens within that group. If a malicious user randomly guessed groups they could potentially read a message over a socket they shouldn't.
When you create a new message the following code is run in my controller:
if message.save
ActionCable.server.broadcast(
"messages_{message.groupchat_id}_channel",
message: message.content,
user: message.user.email
)
head :ok
end
I have no idea what I'm doing.
Here's the solution I found to use Pundit with Activecable.
First we need access to the user model. You can do that by following the instructions in the Action Cable Overview - Connection Setup. Mainly you need to change the code in connection.rb
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
Note: you may need to use a different way of finding your user. I'm using devise_token_auth and so needed to pass the uid, token, and client_id to the connection.rb and then got the user via this code:
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
I mention this just because how you get your user may vary. The main thing is that you need to use identified_by current_user and set it.
Another thing which I did not immediately find in the documentation, is that the current_user is now accessible by your channels. Since the user name may be different than your pundit_user name, I found it easiest to manually pass the user to Pundit at that point. So in subscribing with my channel file, I had this code:
def subscribed
message = MessagePolicy::Scope.new(self.current_user, Project).resolve.find(params[:message])
stream_for message
end
You could of course also manually authorize this way, instead of using Scope:
MessagePolicy.new(self.current_user, message).show?
You can add another layer of security for actioncable connections in app/channels/application_cable/connection.rb file.
You can follow this tutorial. I think this give you some idea:
https://www.learnenough.com/action-cable-tutorial#sec-login_protection

Devise log after auth failure

I need to write a log when somebody failes to log in to my app (to track bruteforce attempts). Also I decided to log successful authentications.
So I created a SessionsController < Devise::SessionsController and tried to override the sessions#create method like that: https://gist.github.com/3884693
The first part works perfectly, but when the auth failes rails throws some kind of an exception and never reaches the if statement. So I don't know what to do.
This answer to a previous SO question - Devise: Registering log in attempts has the answer.
The create action in the devise controller calls warden.authenticate!, which attempts to authenticate the user with the supplied params. If authentication fails then authenticate! will call the devise failure app, which then runs the SessionsController#new action. Note, any filters you have for the create action will not run if authentication fails.
So the solution is to add a filter after the new action which checks the contents of env["warden.options"] and takes the appropriate action.
I tried out the suggestion, and was able to log both the successful & failed login attempts. Here is the relevant controller code:
class SessionsController < Devise::SessionsController
after_filter :log_failed_login, :only => :new
def create
super
::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
end
private
def log_failed_login
::Rails.logger.info "\n***\nFailed login with email_id : #{request.filtered_parameters["user"]}\n***\n" if failed_login?
end
def failed_login?
(options = env["warden.options"]) && options[:action] == "unauthenticated"
end
end
The log has the following entries:
For a successful login
Started POST "/users/sign_in"
...
...
***
Successful login with email_id : {"email"=>...
***
...
...
Completed 302 Found
For a failed login
Started POST "/users/sign_in"
...
...
Completed 401 Unauthorized
Processing by SessionsController#new as HTML
...
...
***
Failed login with email_id : {"email"=>...
***
...
...
Completed 302 Found
Prakash's answer is helpful, but it's not ideal to rely on SessionsController#new to be run as a side effect. I believe this is cleaner:
class LogAuthenticationFailure < Devise::FailureApp
def respond
if request.env.dig('warden.options', :action) == 'unauthenticated'
Rails.logger.info('...')
end
super
end
end
...
Devise.setup do |config|
config.warden do |manager|
manager.failure_app = LogAuthenticationFailure
end
Check out Graeme's answer if you'd prefer to hook into Warden's callbacks (Devise is implemented using Warden).
I had the same question but was unable to resolve it using the "warden.options" since, in my case, these were being cleared before redirecting to the sessions#new action. After looking into a few alternatives that I judged to be too brittle (because they involved extending some Devise classes and aliasing existing methods), I wound up using some callbacks provided by Warden. It works better for me because the callback is invoked inside the current request-response cycle and the parameters are all preserved in the env object.
These callbacks are named and appear to be designed to solve this and related problems. And they are documented!
Warden supports the following callbacks as of warden-1.2.3:
after_set_user
after_authentication (useful for logging successful sign ins)
after_fetch (alias for after_set_user)
before_failure (useful for logging failed sign ins - example below)
after_failed_fetch
before_logout
on_request
Each callback is set directly on the Warden::Manager class (may be inside config/initializers/devise.rb). To track a failed authentication attempt I added this:
Warden::Manager.before_failure do |env, opts|
email = env["action_dispatch.request.request_parameters"][:user] &&
env["action_dispatch.request.request_parameters"][:user][:email]
# unfortunately, the User object has been lost by the time
# we get here; so we take a db hit because I care to see
# if the email matched a user account in our system
user_exists = User.where(email: email).exists?
if opts[:message] == :unconfirmed
# this is a special case for me because I'm using :confirmable
# the login was correct, but the user hasn't confirmed their
# email address yet
::Rails.logger.info "*** Login Failure: unconfirmed account access: #{email}"
elsif opts[:action] == "unauthenticated"
# "unauthenticated" indicates a login failure
if !user_exists
# bad email:
# no user found by this email address
::Rails.logger.info "*** Login Failure: bad email address given: #{email}"
else
# the user exists in the db, must have been a bad password
::Rails.logger.info "*** Login Failure: email-password mismatch: #{email}"
end
end
end
I expect that you could use the before_logout callback to track logout actions as well, but I haven't tested it. There appear to be prepend_ variants of the callbacks as well.
For logout logging, you need to catch the destroy event, so add the following to the Session controller (from the above answer):
before_filter :log_logout, :only => :destroy #add this at the top with the other filters
def log_logout
::Rails.logger.info "*** Logging out : #{current_user.email} ***\n"
end
I've found another way to do this, if you want, for example, display a custom message if login fails.
In my job, if login fails we check the activity status (custom logic) and display a message, no matter if the login was correct or not.
After debug a little bit and read warden docs I know this now: Warden executes a throw(:warden, opts), so, according to ruby docs, a throw must be captured inside a catch block.
def create
flash.clear
login_result = catch(:warden) { super }
return unless login_failed?(login_result)
email = params[:user][:email]
flash[:alert] = # here I call my service that calculates the message
redirect_to new_user_session_path
end
def login_failed?(login_result)
login_result.is_a?(Hash) && login_result.key?(:scope) && login_result.key?(:recall)
end
throw docs:
https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-throw
catch docs:
https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-catch
Building on Prakash Murty's answer, I think the approach in this answer (https://stackoverflow.com/a/34816998/891359) is a cleaner way to log a succesfull login attempt. Instead of calling super, Devise offers a way to pass a block that is yielded before the view is rendered.
So instead of doing this:
class SessionsController < Devise::SessionsController
def create
super
::Rails.logger.info "\n***\nSuccessful login with email_id : #{request.filtered_parameters["user"]}\n***\n"
end
end
It is cleaner to do:
class SessionsController < Devise::SessionsController
def create
super do |user|
::Rails.logger.info "\n***\nSuccessful login with email_id : #{user.email}\n***\n"
end
end
end

Test response of Rails 3 background job (API-Request) with VCR in Cucumber feature

I have a Rails 3 background job (delayed_job) which sends a hipchat / Campfire message to their API and I want to check the response in my Cucumber feature. Is there a way to get the last HTTP response(s) which VCR have recorded?
The feature looks like this
#vcr
Scenario: Send hipchat message when task created
Given an hipchat_sample integration exists with app: app "teamway"
When I create an "ActionMailer::Error" task to "Teamway"
And all jobs are worked off # invoke Delayed::Worker.new.work_off
Then a hipchat message should be sent "ActionMailer::Error"
In my step definition I want to check the response body:
Then /^a hipchat message should be sent "(.*?)"$/ do |arg1|
# Like this:
# VCR::Response.body.should == arg1
end
VCR already records the request and response, but I do not know how to take them. I think of something similar to catching the emails sent with Pickle's steps. Does anybody have an idea how to do this?
I use rails 3.2.8, cucumber-rails 1.3 and vcr 2.2.4 (with webmock).
Best regards
Torsten
You can use VCR.current_cassette to get the current cassette, and then interrogate that to get the [VCR::HTTPInteraction][1] object you're looking for, but it'll be a bit complex--the VCR cassette stores the newly recorded HTTP interactions separately from the ones it has available for playback and from the ones it has already played back...so you'll need some complex conditionals to ensure things work properly both when your tests are recording and when they are playing back.
Instead, I recommend you use an after_http_request hook:
module HipmunkHelpers
extend self
attr_accessor :last_http_response
end
Before { HipmunkHelpers.last_http_response = nil }
VCR.configure do |c|
c.after_http_request(lambda { |req| URI(req.uri).host == 'hipmunk.com' }) do |request, response|
HipmunkHelpers.last_http_response = response
end
end
Then, in your cucumber step, you can access HipmunkHelpers.last_http_response.
For more details on the after_http_request hook, check out the relish docs.

How to test with RSpec if an email is delivered

I'd like to test if an email is delivered if I call a controller method with :post. I'll use email_spec so I tried this snipped here: http://rubydoc.info/gems/email_spec/1.2.1/file/README.rdoc#Testing_In_Isolation
But it doesn't work, because I pass an instance of the model-object to the delivery-method and the instance is saved before the delivery.
I tried to create an other instance of the model-object, but then the id isn't the same.
My controller-method looks like this:
def create
#params = params[:reservation]
#reservation = Reservation.new(#params)
if #reservation.save
ReservationMailer.confirm_email(#reservation).deliver
redirect_to success_path
else
#title = "Reservation"
render 'new'
end
end
Do you have any idea to solve this?
Assuming your test environment is set up in the usual fashion (that is, you have config.action_mailer.delivery_method = :test), then delivered emails are inserted into the global array ActionMailer::Base.deliveries as Mail::Message instances. You can read that from your test case and ensure the email is as expected. See here.
Configure your test environment to accumulate sent mails in ActionMailer::Base.deliveries.
# config/environments/test.rb
config.action_mailer.delivery_method = :test
Then something like this should allow you to test that the mail was sent.
# Sample parameters you would expect for POST #create.
def reservation_params
{ "reservation" => "Drinks for two at 8pm" }
end
describe MyController do
describe "#create" do
context "when a reservation is saved" do
it "sends a confirmation email" do
expect { post :create, reservation_params }.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
end
end
Note that my example uses RSpec 3 syntax.
I know I'm late to the party with this one, but for future Googlers...
I think a better solution to this problem is answered here
The previously accepted answer is testing the Mailer itself (inside the controller spec). All you should be testing for here is that the Mailer gets told to deliver something with the right parameters.
You can then test the Mailer elsewhere to make sure it responds to those parameters correctly.
ReservationMailer.should_receive(:confirm_email).with(an_instance_of(Reservation))
This is way how to test that Mailer is called with right arguments. You can use this code in feature, controller or mailer spec:
delivery = double
expect(delivery).to receive(:deliver_now).with(no_args)
expect(ReservationMailer).to receive(:confirm_email)
.with('reservation')
.and_return(delivery)
Anyone using rspec +3.4 and ActiveJob to send async emails, try with:
expect {
post :create, params
}.to have_enqueued_job.on_queue('mailers')
To add a little more, make sure if you're going to stub out a call using should_receive that you have an integration test elsewhere testing that you're actually calling the method correctly.
I've been bit a few times by changing a method that was tested elsewhere with should_receive and having tests still pass when the method call was broken.
If you prefer to test the outcome rather than using should_receive, shoulda has a nice matcher that works like the following:
it { should have_sent_email.with_subject(/is spam$/) }
Shoulda documentation
More information on using Shoulda Matchers with rSpec
If you're using Capybara with Capybara Email and you sent an email to test#example.com, you can also use this method:
email = open_email('test#example.com')
And then you can test it like this:
expect(email.subject).to eq('SUBJECT')
expect(email.to).to eq(['test#example.com'])
Try email-spec
describe "POST /signup (#signup)" do
it "should deliver the signup email" do
# expect
expect(UserMailer).to(receive(:deliver_signup).with("email#example.com", "Jimmy Bean"))
# when
post :signup, "Email" => "email#example.com", "Name" => "Jimmy Bean"
end
end
more examples here: https://github.com/email-spec/email-spec#testing-in-isolation