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

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

Related

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

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.

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.

Rails + Cucumber/Capybara: How to set/retrieve cookies in tests?

I'm implementing a lazy login feature. My cucumber feature should describe it:
Feature: User log in
Scenario: Lazy login
Given I didn't log out the last time I was on the site
When I go to the homepage
Then I should automatically be logged in
And these are my step definitions:
Given(/^I didn't log out the last time I was on the site$/) do
user = FactoryGirl.create(:user)
visit new_user_session_path
fill_in('user[email]', with: user.email)
fill_in('user[password]', with: 'test123')
click_button('Sign in')
Capybara.reset_sessions!
end
When(/^I go to the homepage$/) do
visit root_path
end
Then(/^I should automatically be logged in$/) do #<-- Fails here
page.should have_content("Logout")
end
This is what happens when a user logs in: the cookies.signed[:auth_token] gets set. This will be used by a before filter in my ApplicationController so that users who open a fresh browser will be logged in automatically:
class SessionsController < Devise::SessionsController
def create
super
if user_signed_in?
puts 'yesssssss'
session[:user_id] = current_user.id
current_user.remember_me! if current_user.remember_token.blank?
cookies.signed[:auth_token] = {
:value => current_user.remember_token,
:domain => "mysite.com",
:secure => !(Rails.env.test? || Rails.env.development?)
}
puts "current_user.remember_token = #{current_user.remember_token}"
puts 'cookies:'
puts cookies.signed[:auth_token]
end
end
end
This is the before filter in my ApplicationController:
def sign_in_through_cookie
logger.info "logging in by cookie"
puts "logging in by cookie"
puts cookies.signed[:auth_token] #<-- PROBLEM: this returns nil.
return true if !current_user.nil?
if !cookies[:auth_token].nil? && cookies[:auth_token] != ''
user = User.find_by_remember_token(cookies.signed[:auth_token])
return false if user.blank?
sign_in(user)
puts 'success'
return true
else
return false
end
end
So the issue is that in the last step of my cucumber feature, cookies.signed[:auth_token] returns nil. I'm guessing this is just a capybara thing. So do I actually have to set a cookie in the test as opposed to using the one in my controller?
So eventually I figured it out after trying a lot of different things.
Given(/^I didn't log out the last time I was on the site$/) do
user = FactoryGirl.create(:user)
visit new_user_session_path
fill_in('user[email]', with: user.email)
fill_in('user[password]', with: 'test123')
click_button('Sign in')
Capybara.current_session.driver.request.cookies.[]('auth_token').should_not be_nil
auth_token_value = Capybara.current_session.driver.request.cookies.[]('auth_token')
Capybara.reset_sessions!
page.driver.browser.set_cookie("auth_token=#{auth_token_value}")
end
When(/^I go to the homepage$/) do
visit root_path
end
Then(/^I should automatically be logged in$/) do
page.should have_content("Logout")
end
UPDATE:
Here's what I use in case I'm using Selenium for some of the tests:
if Capybara.current_session.driver.class == Capybara::Selenium::Driver
auth_token = page.driver.browser.manage.cookie_named('auth_token')[:value]
page.driver.browser.manage.delete_all_cookies
page.driver.browser.manage.add_cookie(:name => "auth_token", :value => auth_token)
else
puts "cookies = #{Capybara.current_session.driver.request.cookies}"
Capybara.current_session.driver.request.cookies.[]('auth_token').should_not be_nil
auth_token_value = Capybara.current_session.driver.request.cookies.[]('auth_token')
Capybara.reset_sessions!
page.driver.browser.set_cookie("auth_token=#{auth_token_value}")
end
Use https://github.com/nruth/show_me_the_cookies which wraps the driver methods. It has methods for getting cookies, deleting cookies, and a method for creating cookies called create_cookie.
I needed just to test the cookie values
Inspiration taken from https://collectiveidea.com/blog/archives/2012/01/05/capybara-cucumber-and-how-the-cookie-crumbles
and ported to Rails 5.x
Create features/support/cookies.rb
With content
module Capybara
class Session
def cookies
#cookies ||= ActionDispatch::Request.new(Rails.application.env_config.deep_dup).cookie_jar
end
end
end
Before do
allow_any_instance_of(ActionDispatch::Request).to receive(:cookie_jar).and_return(page.cookies)
allow_any_instance_of(ActionDispatch::Request).to receive(:cookies).and_return(page.cookies)
end
Then the step for testing
Then('is set cookie {string} with value {string}') do |cookie, value|
expect(page.cookies.signed[cookie]).to eq value
end

Adding roles to an OmniAuth log in so that Cucumber doesn't fail a test

I've been working on an app for learning purposes which includes OmniAuth for Facebook logins, Cucumber for BDD and CanCan & Rolify for permissions and roles. No Devise is used so far. I'm trying to write a test that involves logging a user with admin role and then visiting a restricted path. Also, users that have been created with OmniAuth have simple attributes: If the user has been confirmed to use the site, he/she will have confirmed: 1 and confirmation_token: nil; otherwise it will be confirmed: 0 and confirmation_token: . The idea actually works if I'm not in a Cucumber environment, but inside it, the page gives me a CanCan::AccessDenied error. Oh, and I've also set OmniAuth test mode, that works fine, env["omniauth.auth"] returns a proper mock hash.
This is my test (I'm still learning so bear with me)
#omniauth_test
Given /^I am logged as an admin$/ do
#ensure no session is active
visit '/signout'
#user = FactoryGirl.create(:user, confirmed: 1, confirmation_token: nil)
#user.add_role :admin
visit root_url
click_link "log_in" #means going to '/auth/facebook/callback'
end
And /^I check the user list$/ do
visit users_path #fails
end
This is my Factory for user, nothing complicated:
FactoryGirl.define do
factory :user do |u|
u.email 'test#example.com'
end
end
This is my SessionsController:
class SessionsController < ApplicationController
def create
reset_session
service = Service.from_omniauth(env["omniauth.auth"])
session[:user_id] = service.user.id
session[:service_id] = service.id
session[:expires_at] = 5.minutes.from_now
if service.user.confirmed == 0
redirect_to edit_user_path(service.user)
elsif service.user.confirmed == 1
if service.user.has_role? :member
redirect_to root_url
elsif service.user.has_role? :admin
redirect_to users_path
else
redirect_to root_url, :notice => "Work in progress!"
end
end
end
And finally, Service.rb:
class Service < ActiveRecord::Base
attr_accessible :user_id, :provider, :uid, :name, :token, :updated_at
validates_uniqueness_of :uid, :scope => [:provider]
belongs_to :user
def self.from_omniauth(auth)
where(auth.slice(:provider, :uid)).first_or_initialize.tap do |service|
if !service.user
user = User.create(:email => auth.info.email)
service.user = user
#for some reason, instance variable #user created by FactoryGirl is nil at this point (test env)
user.add_role :signedup
end
service.provider = auth.provider
service.uid = auth.uid
service.name = auth.info.name
service.token = auth.credentials.token
service.save!
end
end
What I would like is to somehow use the OmniAuth hash and add the admin role and the confirmed attributes only for test mode, without messing too much with the production code (if possible), maybe adding helper methods to env.rb, and logging in with that user.
Silly me. This did the trick:
#omniauth_test
Given /^I am logged as an admin$/ do
#user = FactoryGirl.create(:user, confirmed: 1, confirmation_token: nil)
#user.add_role :admin
#service = FactoryGirl.create(:service, user: #user, uid: '1234567', provider: 'facebook')
end

How do I get Devise session#create test to pass

Here is my test:
require 'test_helper'
class SessionsControllerTest < ActionController::TestCase
setup do
#request.env["devise.mapping"] = Devise.mappings[:user]
#u = Factory :user, :password => :mypass, :password_confirmation => :mypass
end
test 'log in page loads' do
get :new
assert :success
end
test 'log in with devise password' do
post :create, :user => {:email => #u.email, :password => 'mypass'}
ap session
end
end
gives this output, indicating that the sign in failed:
Loaded suite test/functional/sessions_controller_test
Started
.{
"action" => "create",
"locale" => "en",
"controller" => "sessions",
"user" => {
"password" => "mypass",
"email" => "458286#email.com"
}
}
{
"flash" => {
:alert => "Invalid email or password."
}
}
.
Finished in 0.49123 seconds.
This is my session controller:
#this is an extension of the devise controller for sessions
class SessionsController < Devise::SessionsController
before_filter :set_title_h1, :only => :new
before_filter :debug, :only => :create
before_filter :old_password_system_fix, :only => :create
private
def set_title_h1
#layout[:show_h1] = false
title 'Sign in Or Register'
end
def after_sign_in_path_for(resource)
#override Devise default sign in path /opt/local/lib/ruby/gems/1.8/gems/devise-1.1.2/lib/devise/controllers/helpers.rb
#edit_user_registration_path
'/en/main/index' #forces locale to be defined
end
def after_sign_out_path_for(resource)
#override Devise default sign out path /opt/local/lib/ruby/gems/1.8/gems/devise-1.1.2/lib/devise/controllers/helpers.rb
main_index_path
end
def old_password_system_fix
#purpose is to bring old users into the new system by setting their old password to the new format
require 'digest/md5'
email = params[:user][:email]
pw = params[:user][:password]
#get user
u = User.find_by_email email
return if u.nil?
#if they don't have a devise-style pw, authenticate with old
if u.encrypted_password.blank? && u.old_password.present?
#if [params pw] == md5 [old pw] then create devise-style pw & salt, store it, and let them through to devise auth action
if u.old_password == Digest::MD5.hexdigest(pw)
set_devise_style_pw(u, pw)
#if no match, give "invalid email or pw" message.
else
#flash[:notice] = "Sign in failed."
flash[:notice] = t 'devise.failure.invalid'
#render :new
redirect_to new_user_session_path
end
end
end
def debug
ap params
end
end
What am I missing and how can I test a new session via a functional test?
Turns out you have to use an integration test, not a functional test. Don't ask me why...

Resources