Rails 6, minitest system test: password reset fails in tests, but works when I manually change it - capybara

I'm using Rails 6 and minitest with the built-in system tests (which use Capybara I think) and using FactoryBot as well to generate my test records.
I have a pretty standard password rest feature I'm trying to implement.
I've verified that when I go to the pages in the browser and fill out the information it does indeed change the user's password, but for some reason the password is never changed in the test.
It's almost like the #user object is being cached in the tests and won't reload in the test, but I have no idea why that would be.
Anyone know why this test would fail but the functionality works in "real life" when I manually change a password?
# test/system/password_resets_test.rb
require "application_system_test_case"
class PasswordResetsTest < ApplicationSystemTestCase
test "change password" do
original_password = "password"
new_password = "new-password"
#user = create(:user, password: original_password, password_reset_token_sent_at: Time.current)
visit password_reset_path(#user.password_reset_token)
fill_in "user[password]", with: new_password
click_on "Update Password"
assert_equal(#user.reload.password, new_password)
end
end
# app/views/password_resets/show.html.erb
<%= form_with model: #user, url: password_reset_path(#user.password_reset_token), method: :put do |form| %>
<div class="field">
<%= form.label :password, "Password" %><br />
<%= form.password_field :password, autofocus: true, required: true %>
</div>
<div class="field">
<%= form.submit "Update Password" %>
</div>
<% end %>
# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
def show
if #user = User.find_by(password_reset_token: params[:id])
if #user.password_reset_token_expired?
flash[:error] = "Your password reset has expired"
redirect_to new_password_reset_path
end
else
flash[:error] = "Invalid password reset token"
redirect_to new_password_reset_path
end
end
def update
#user = User.find_by(password_reset_token: params[:id])
new_password = password_reset_params[:password]
# Automatically set `#password_confirmation` so user does not have
# to enter in password twice on reset page.
if #user&.update(password: new_password, password_confirmation: new_password)
let_user_in(#user)
else
render :show
end
end
private
def password_reset_params
params.require(:user).permit(:password)
end
# app/models/user.rb
class User < ApplicationRecord
PASSWORD_RESET_TIME_LIMIT_IN_HOURS = 4.freeze
has_secure_password
has_secure_token :password_reset_token
validates :password,
presence: true,
length: { minimum: 8 },
allow_nil: true
def password_reset_token_expired?
return true if password_reset_token_sent_at.blank?
password_reset_token_sent_at < PASSWORD_RESET_TIME_LIMIT_IN_HOURS.hours.ago
end
end

click_on doesn't guarantee any actions triggered by the click have happened when it returns. This is because Capybara has no way of knowing what (if any) actions would have been triggered by that click. This means your assertion of the new password is probably happening before the page has even submitted. To fix that you need to use one of the Capybara provided retrying assertions (which assert_equal is not) to check for something visible on the page that indicates the update has occurred.
Something along the lines of
click_on "Update Password"
assert_text "Password Updated!" # whatever message your page shows to indicate successful password update
assert_equal(#user.reload.password, new_password)
should fix your issue.

Related

How do I have a create a signed in user before running an Rspec feature test?

I'm trying to write a feature test (using Rspec 3 and Capybara) that tests a user adding an address (string) and getting the coordinates in response. Users need to be signed in before they can do this, so how can I create a user and then perform this function? I get the following error:
Failure/Error: fill_in 'text_field_tag', with: q
Capybara::ElementNotFound:
Unable to find field "text_field_tag" that is not disabled
Here is the code I have so far.
find_coordinates_spec.rb
feature 'find coordinates' do
scenario 'with valid place name' do
user = User.create(email: 'test#test.com', password: "password", password_confirmation: "password")
sign_in user
geocode('London')
expect(page).to have_content('51.5073219, -0.1276474')
end
def geocode(q)
visit locations_path
fill_in 'text_field_tag', with: q
click_button 'geocode'
end
end
locations_controller.rb
class LocationsController < ApplicationController
before_action :authenticate_user!
def index
if params[:q].blank?
#message = 'Please enter an address in the field!'
return
end
token = Rails.application.credentials.locationiq_key
search = LocationiqApi.new(token).find_place(params[:q])
# Hash#dig will return nil if ANY part of the lookup fails
latitude = search.dig('searchresults', 'place', 'lat')
longitude = search.dig('searchresults', 'place', 'lon')
if latitude.nil? || longitude.nil?
# Output an error message if lat or lon is nil
#coordinates = "We couldn't find a place by this name. Please enter a valid place name."
else
#coordinates = "Coordinates: " + "#{latitude}, #{longitude}"
end
end
end
locations.html.erb
<main>
<h1>Location Search</h1>
<!-- devise flash messages -->
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<!-- Button to search coordinates -->
<%= form_tag(locations_path, method: :get) do %>
<%= text_field_tag(:q) %>
<%= submit_tag("geocode") %>
<%= #message %>
<% end %><br>
<%= #coordinates %>
</main>
Your error is not because of failure to create a user.
Just to be sure that authentication went ok you can add after visit locations_path:
expect(page).to have_content('Please enter an address in the field')
Actual error is that your input field is called q and not text_field_tag:
fill_in "q", with: q
Your are getting this error because of this line:
fill_in 'text_field_tag', with: q
According to Capybara documentation on #fill_in:
The field can be found via its name, id, Capybara.test_id attribute, or label text
text_field_tag is not an html attribute but a rails view helper. You should change if with the id, label or name of the text_field_tag

ActionMailer not delivering mail in development

I am trying to build a password reset email. I am Using Rails 3.2.16, and Ruby 1.9.3p327, I have read a ton of answers to this question and tried pretty much everything. I have also gone through the action mailer basics guide, and as far as i can see this should be working but its just not going well. Ill do a step by step guide of how i set it up.
firstly since i am trying to get this working in development, within development.rb Note: I have reset the application each time i edited the development.rb file.
#this is all the action mailer settings i have defined in development.rb
config.action_mailer.raise_delivery_errors = true # Set to true so that errors would be visible.
config.action_mailer.perform_deliveries = true # I read about this possible fix on SO
config.action_mailer.default_url_options = {
host: "boogle.dev"
}
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
:address => "smtp.office365.com",
:port => 587,
:domain => "mpowered.co.za",
:user_name => "support#mpowered.co.za",
:password => "password",
:authentication => :login,
:enable_starttls_auto => true
}
My notifier class which inherits from ActionMailer
class Notifier < ActionMailer::Base
default from: "Mpowered - BEEtoolkit <support#mpowered.co.za>"
def deliver_password_reset_email(user)
#edit_password_reset_url = edit_password_reset_url(user.perishable_token)
#name = user.name
mail(
subject: "Password Reset Instructions",
to: user.email,
date: Time.now,
content_type: "text/html")
end
end
Within my User model i have set up the method which will send the mail along with setting up of a perishable_token
def deliver_password_reset_instructions!
reset_perishable_token!
Notifier.deliver_password_reset_email(self)
end
The Passwords reset controller is set up like this:
class PasswordResetsController < ApplicationController
before_filter :require_no_user
before_filter :load_user_using_perishable_token, :only => [ :edit, :update ]
def new
end
def create
#user = User.find_by_email(params[:email])
if #user
#user.deliver_password_reset_instructions!
flash[:notice] = "Instructions to reset your password have been emailed to you"
render action: :new
else
flash.now[:error] = "No user was found with email address #{params[:email]}"
render action: :new
end
end
def edit
end
def update
#user.password = params[:password]
# Only if your are using password confirmation
#user.password_confirmation = params[:password]
# Use #user.save_without_session_maintenance instead if you
# don't want the user to be signed in automatically.
if #user.save
flash[:success] = "Your password was successfully updated"
redirect_to #user
else
render action: :edit
end
end
private
def load_user_using_perishable_token
#user = User.find_using_perishable_token(params[:id])
unless #user
flash[:error] = "We're sorry, but we could not locate your account"
redirect_to root_url
end
end
end
I added resources to my routes:
resources :password_resets, :only => [ :new, :create, :edit, :update ]
My views are simple:
in app/views/password_resets/new.haml.html
%br
= form_tag password_resets_path, class: 'form-inline' do
%legend Forgotten Password
%p Enter your email address and instructions to reset your password will be emailed to you:
%span.span1
= label_tag :email, 'Email'
= text_field_tag :email
= submit_tag 'Reset my password', class: 'btn'
%br
So this should send the mail once you submit a valid email.
You should then receive an email with this content: app/views/notifier/password_reset_instructions.html.haml
%h1 Password Reset Instructions
%p
A request to reset your password has been made. If you did not make
this request, simply ignore this email. If you did make this
request, please follow the link below.
= link_to "Reset Password!", #edit_password_reset_url
The link should bring you to a form where you can then save a new password and password confirmation.
app/views/password_resets/edit.html.haml
- if #user
= form_for #user, :url => password_reset_path, :method => :put do |f|
%legend Change My Password
%p Please select a new password for your account
.span8
= f.field :password, :field_type => :password_field, :label => "New password"
= f.field :password_confirmation, :field_type => :password_field
.clearfix
= f.submit "Update my password", class: 'btn'
- else
%h3 We couldn't identify your reset code
%p We're sorry, but we could not locate any accounts with reset codes that matched yours.
%p If you are having difficulties resetting your password, try copying and pasting the URL from your password reset email into your browser or restarting the password reset process.
to which you can save your new password and then login once more.. this is what i have set up in the app. but everytime i try send it by following the system, it says the email was sent but nothing ever comes. I have also tried loading up a user in the console and then running u.deliver_password_reset_instructions!
and i get this:
But still no email in my inbox. I have currently set the email address in the notifier to my own personal one so no matter what valid email address is requested, the email should come to me.
I have been hitting walls for the last 12 hours and have no idea where to turn. i am hoping i have made a balls up that a fresh pair of eyes can catch.
You need to add .deliver when calling Mailer method like this
def deliver_password_reset_instructions!
reset_perishable_token!
Notifier.deliver_password_reset_email(self).deliver
end
Hope this helps

Testing devise account_update sanitizer

After following the Hartl Tutorial I'm trying to change the authentication to use the devise gem. My sample application site seems to be working again but some of the specs still fail because some of the routes and user controller actions have changed. So I'm in the process of fixing those and stuck on one that checks to make sure the user can't give themselves admin access.
describe "update user with forbidden attributes", type: request do
FactoryGirl.create(:user)
let(:params) do
{ "user[name]" => "new name",
"user[email]" => user.email,
"user[current_password]" => user.password,
"admin" => true }
end
before do
post user_session_path, 'user[email]' => user.email, 'user[password]' => user.password
patch user_registration_path(user), params
user.reload
end
its(:name) { should eql "new name" } # passes, and should.
its(:admin?) { should be false } # can't get to fail.
specify { expect(response).to be_success } # fails, gets response 406.
end
This test passes, but it passes because I can't get it to fail. I'm trying to do the usual Red-Green-Refactor and I can't make it go red, even if I add admin to the list of devise acceptable parameters. I want to make sure that this would change admin if the permissions were screwed up.
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: devise_controller?
after_action :print_permitted_parameters, if: devise_controller?
def configure_permitted_parameters
...
devise_parameter_sanitizer.for(:account_update) do |u|
u.permit(:name, :email, :password, :password_confirmation, :current_password, :admin)
end
end
def print_configured_parameters
puts "sign_up: " + devise_parameter_sanitizer.for(:sign_up).join(' ')
#prints "sign_up: email password password_confirmation"
puts "sign_in: " + devise_parameter_sanitizer.for(:sign_in).join(' ')
#prints "sign_in: email password remember_me"
puts "account_update: " + devise_parameter_sanitizer.for(:account_update).join(' ')
#prints "account_update: email password password_confirmation current_password"
end
end
The strange thing is that user's name and email do update, so something is working. But the response I get is always 406 for "Not Acceptable". So my question is why can I not get the admin tests to fail? And are the 406 errors related?
printing the permitted parameters suggests the parameters aren't being configured for any actions, it's just the default list. And I can sign_in with an existing user but if I just click "sign_in" with no fields it complains of an umpermitted parameter: "remember_me" despite that being on the list. Similarly if I try to sign_up a new user, which used to work, it complains that password_confirmation is unpermitted.
Thanks for your help, I appreciate it.

Why is rspec not updating the database record with a password reset token?

I'm running into a bit of a problem with RSpec when running a rails test. The test is as follows:
it "allows a user to request a password reset" do
user = FactoryGirl.create(:user, :name => "John Smith", :email => "john#test.org")
identity = Identity.create!(email: user.email, password: "please", password_confirmation: "please")
visit "/"
click_on "Forgotten password?"
fill_in "email", :with => "john#test.org"
click_on "Reset Password"
ActionMailer::Base.deliveries.last.to.should include(user.email)
ActionMailer::Base.deliveries.last.body.should include(identity.password_reset_token)
visit '/password_resets/' + identity.password_reset_token.to_s + '/edit'
^ **** ERROR HERE : password_reset_token is nil! ****
fill_in "identity_password", :with => "newpass123"
fill_in "identity_password_confirmation", :with => "newpass123"
click_on "Update Password"
within page.all('div#loginModal')[0] do
fill_in "auth_key", :with => "john#test.org"
fill_in "password", :with => "newpass123"
click_button "Log in"
end
page.should have_content("Hi John")
end
You can see I've outlined the error I'm getting back, which is strange, because when I inspect the email sent, it does in fact generate a password_reset_token.
For information, the code I use to do this is:
class PasswordResetsController < ApplicationController
def create
identity = Identity.find_by_email(params[:email])
identity.send_password_reset if identity
redirect_to root_url, :notice => "Email sent with password reset instructions."
end
def edit
#identity = Identity.find_by_password_reset_token!(params[:id])
end
def update
#identity = Identity.find_by_password_reset_token!(params[:id])
if #identity.password_reset_sent_at < 2.hours.ago
redirect_to new_password_reset_path, :alert => "Password reset has expired."
elsif #identity.update_attributes(params[:identity])
redirect_to root_url, :notice => "Password has been reset."
else
render :edit
end
end
end
and...
class Identity < OmniAuth::Identity::Models::ActiveRecord
attr_accessible :email, :password, :password_confirmation
validates_uniqueness_of :email
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
def send_password_reset
generate_token(:password_reset_token)
self.password_reset_sent_at = Time.zone.now
self.save!
UserMailer.password_reset(self).deliver
end
private
def generate_token(column)
begin
self[column] = SecureRandom.urlsafe_base64
end while Identity.exists?(column => self[column])
end
end
Can anyone suggest why this might be happening? Is it something to do with the database record not being saved / accessed or cached somewhere in my test environment?
Any help appreciated.
Just a guess: Don't you need to update your identity variable, like find it by email, in order to update the object with the new parameters?
Before you visit 'password_resets', try to load the identity with
Identity.find_by_email(user.email)
You may be running into timing issues. After the click_on 'Reset Password' step, check that the page comes back with the expected value before moving on to the visit 'passwords_reset...' step.

Is there any way to check if rails set permanent cookie?

I'm using TestUnit and I want to test the remember me functionality (when user login).
the cookies variable(and also require/response .cookies) contain only the cookie value without the expire time.
Rails somehow tell the web-browser when the cookie should be expire, so I assume there must be a way to check the cookie expire time.
EDIT
test "set permanent cookie" do
post :create, email: 'email', password: 'password', remember_me: true
# cookies[:auth_token] = random_string
# #request.cookies[:auth_token] = also_random_string
# #response.cookies[:auth_token] = also_random_string
end
the problem is that I can get only the values of the cookies and not the hash that contain the expire time.
As you've noticed, the cookies Hash only contains the values, not the expiration times, when you inspect it after your post call (this has been the behavior since at least Rails 2.3).
You have two options:
First, you could inspect the response.headers["Set-Cookie"] item instead. It will include the expiration time in there. However, the Set-Cookie value is just a single string, which you would need to then parse. For example, cookies["foo"] = {:value => "bar", :expires => Time.now + 10.years } would give you:
response.headers["Set-Cookie"]
# => "foo=bar; path=/; expires=Mon, 16-Aug-2021 21:54:30 GMT"
The other option (taken from This Question/Answer), would be to stub the cookie jar and make sure it is sent an expires value:
stub_cookie_jar = HashWithIndifferentAccess.new
controller.stub(:cookies) { stub_cookie_jar }
post :create, email: 'email', password: 'password', remember_me: true
expiring_cookie = stub_cookie_jar['expiring_cookie']
expiring_cookie[:expires].to_i.should be_within(1).of(1.hour.from_now.to_i)
Unfortunately, I couldn't get the solutions presented or linked in #DylanMarkow's answer working, so here is how I tested that a "permanent" cookie was being set when a Remember Me checkbox was checked (the tests are influenced/blatantly copied from the Test::Unit tests that DHH made in the commit that added cookies.permanent to Rails).
Tests use RSpec and FactoryGirl.
spec/requests/authentication_requests_spec.rb
require 'spec_helper'
describe "Authentication Requests" do
# ...
describe "authorization" do
# ...
describe "cookies" do
let(:user) { FactoryGirl.create(:user) }
subject { response.headers["Set-Cookie"] }
context "when remember me is set" do
before { sign_in_request(user) }
it { should =~ %r(.+expires.+#{20.years.from_now.year}) }
end
context "when remember me is not set" do
before { sign_in_request(user, remember_me: false) }
it { should_not =~ %r(expires) }
end
end
end
end
spec/utilities.rb
def sign_in_request(user, remember_me: "true")
post session_path(
session: {
email: user.email,
password: user.password,
remember_me: remember_me
}
)
end
App code snippets below use i18n, Haml, Bootstrap, and Simple Form syntax:
app/views/sessions/new.html.haml
= simple_form_for :session, url: session_path, html: {class: 'form-vertical' } do |f|
= f.input :email
= f.input :password
.checkbox
= f.input :remember_me, as: :boolean, label: false do
= check_box_tag :remember_me, 1, true
= label_tag :remember_me
= f.submit t('.signin'), class: "btn btn-large btn-primary"
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
# ...
def create
if user = User.authenticate(params[:session][:email],
params[:session][:password])
sign_in user
flash[:success] = t('flash.successful_signin')
redirect_to root_url
else
flash.now[:error] = t('flash.invalid_credentials')
render 'new'
end
end
# ...
end
app/models/user.rb
class User < ActiveRecord::Base
has_secure_password
# ...
before_create :generate_authentication_token
def self.authenticate(email, password)
find_by_email(email).try(:authenticate, password)
end
private
def generate_authentication_token
begin
self.authentication_token = SecureRandom.urlsafe_base64
end while User.exists?(authentication_token: self.authentication_token)
end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
private
# A cookie that does not have an expiry will automatically be expired by
# the browser when browser's session is finished.
# cookies.permanent sets the expiry to 20 years
# Booleans seem to get passed from forms as strings
def sign_in(user)
if remember_me
cookies.permanent[:authentication_token] = user.authentication_token
else
cookies[:authentication_token] = user.authentication_token
end
self.current_user = user
end
helper_method :sign_in
def remember_me
params[:session].try(:[], :remember_me) == "true" ||
params[:remember_me] == "true"
end
# ...
end

Resources