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
Related
We are using Doorkeeper gem to authenticate our users through an API. Everything is working fine since we've implemented it few years ago, we are using the password grant flow as in the example:
resource_owner_from_credentials do |_routes|
user = User.active.find_for_database_authentication(email: params[:username])
if user&.valid_password?(params[:password])
sign_in(user, force: true)
user
end
end
Doorkeeper is coupled with Devise, which enable reconfirmable strategy. As you can see in the code above, we are only allowing active users (a.k.a users with a confirmed email) to connect:
User.active.find_.....
Problem
Our specifications changed and now we want to return a different error on login (against /oauth/token) depending if the user has confirmed its email or not.
Right now, if login fails, Doorkeeper is returning the following JSON:
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."
}
Ideally, we want to be able to return a custom description if and only if the current email trying to login is unconfirmed
We've checked the documentation on Doorkeeper but it does not seems to have an easy way (if any at all) to do this. The fact that resource_owner_from_credentials method is located in the config adds too much magic and not enough flexibility.
Any ideas ?
Ok so after digging a little bit, we found an easy way to work around this issue by overriding Doorkeeper::TokensController.
# frozen_string_literal: true
class TokensController < Doorkeeper::TokensController
before_action :check_if_account_is_pending, only: :create
private
def check_if_account_is_pending
user = User.find_by(email: params['username'])
render json: unconfirmed_account_error if user && !user.confirmed?
end
def unconfirmed_account_error
{ error: 'invalid', error_description: 'You must validate your email address before login' }
end
end
We also needed to make sure the routes were pointing to the custom controller:
use_doorkeeper do
controllers tokens: 'tokens'
end
Hope it can helps someone in the future
I found this great tutorial on Github about how to create a custom mailer from Devise. https://github.com/plataformatec/devise/wiki/How-To:-Use-custom-mailer
I implemented the code and the logic makes complete sense to me however when I deploy, I can't get the code to work when I set my config.mailer = 'MyMailer' in initialize/devise.rb. There are no errors in the terminal and it seems that everything got processed just like normal however the email never makes it to the inbox.
If I change config.mailer = 'Devise::Mailer', the email gets delivered correctly. However since MyMailer inherits from Devise::Mailer, shouldn't it work?
Is anyone else having this issue? Any ideas what I can do to troubleshoot?
Here is my "mailers/my_mailer.rb":
class MyMailer < Devise::Mailer
helper :application # gives access to all helpers defined within `application_helper`.
# helper :devise # gives access to all helpers defined within `devise_helper`.
#include Devise::Controllers # Optional. eg. `confirmation_url`
include Devise::Mailers::Helpers
def invoice_payment_failed_email(user, listing)
#user = user
#listing = listing
#url = 'http://example.com/login'
mail(to: #user.email, subject: 'Payment Failed')
end
end
Here is my "initializers/devise.rb":
Devise.setup do |config|
config.secret_key = ENV["DEVISE_SECRET_KEY"]
# Configure the class responsible to send e-mails.
config.mailer = 'MyMailer'
Here is my call to the deliver the mail:
# email customer
MyMailer.invoice_payment_failed_email(#user, #listing).deliver
*Update:
OMG... after a whole day of racking my brain trying to figure this out, it turns out that the code logic is fine but the problem was that my From: needed to match the Postmark sender accounts. This is why I wasn't getting any terminal errors. The deliver request was being sent correctly to Postmark but since my From address wasn't matching, postmark just never delivered the email!
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
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.
In my Rails app I want to use ActionMailer to send emails to multiple recipients. Below is the code I've written that works with a single attribute:
def new_call(medic, call)
#call = call
#medic = medic
mail to: #medic.medic_email, subject: "New Call: #{#call.incident_number}"
end
I want to include #medic.medic_sms to send the message to their phones. I tried the following code but it doesn't work.
def new_call(medic, call)
#call = call
#medic = medic
mail to: #medic.medic_email, #medic.medic_sms, subject: "New Call: #{#call.incident_number}"
end
Can someone suggest how to add the second attribute cleanly so it works?
Thanks in advance.
Putting the two attributes into an array solved the problem.
[#medic.medic_email, #medic.medic_sms]
You have a syntax error. If you want to pass an array to to:, you need to explicitly wrap it in []:
mail to: [#medic.medic_email, #medic.medic_sms], subject: ...
See ActionMailer Basics section 2.3.4: Sending Email To Multiple Recipients.