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
Related
I'm trying to debug my feature spec in RSpec. But I'm unable to get an exception. If I put a binding.pry before auth.save!, I'm able to break in. I then check if auth.valid? and it returns true. I also call auth.save manually with no exception being thrown. But if I put a pry after user.update_for_omniauth omniauth It doesn't get hit. The method update_from_omniauth is not actually being called because I am stubbing it. There is a rescue block at the end of my method. Placing a pry there doesn't trigger anything either. My spec is failing because I doesn't find an Authentication nor User in the database.
authentication controller
def create
user = merge_users! if current_user
auth = current_auth
user ||= auth&.user
email_user = User.has_email.where(email: omniauth.info.email).first_or_initialize
if user && email_user.persisted? && user != email_user
user.merge! email_user, auth
end
user ||= email_user
auth ||= user.authentications.build uid: omniauth.uid,
provider: omniauth.provider, image_url: omniauth.info.image
auth.token = omniauth.credentials.token
if Authentication::GOOGLE.include?(params[:provider].to_sym)
auth.token_expire = Time.at(omniauth.credentials.expires_at)
end
if omniauth.credentials.refresh_token
auth.refresh_token = omniauth.credentials.refresh_token
end
auth.refresh_facebook_token # get a longer running token
auth.save!
user.update_for_omniauth omniauth
user.save!
if params[:provider] == 'google_contacts'
params[:sync_status] = Contact.sync player, auth.token
end
sign_in_and_redirect user
rescue => e
if Rails.env.production?
Raven.capture_exception e, extra: omniauth
redirect_back fallback_location: new_user_session_path, flash: {error: e.message}
else
raise
end
spec version 1
it 'set organically login user as propertyuser' do
visit '/users/sign_in'
click_link 'Login via Facebook'
expect(Authentication.last.uid).to eq('654321')
end
spec version 2
it 'set organically login user as propertyuser' do
visit '/users/sign_in'
click_link 'Login via Facebook'
expect(User.last.email).to eq('facebookuser#mail.com')
end
more spec code
before do
setup_omniauth
application_controller_patch
allow(Facebook).to receive_message_chain(:oauth_for_app, :exchange_access_token_info).and_return('access_token' => '123', 'expires' => 500_000)
allow_any_instance_of(Authentication).to receive(:refresh_facebook_token) #.and_return(true)
allow_any_instance_of(User).to receive(:update_facebook_properties)# .and_return(true)
allow_any_instance_of(User).to receive(:update_for_omniauth)# .and_return(true)
end
def setup_omniauth
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:facebook_app_rewards] = OmniAuth::AuthHash.new(
'provider' => 'facebook',
'uid' => '654321',
'info' => {
'first_name' => 'Facebook',
'last_name' => 'User',
'email' => 'facebookuser#mail.com',
'image' => 'https://randomuser.me/api/portraits/med/men/65.jpg'
},
'credentials' => {
'token' => '123456',
'secret' => 'top_secret',
'expires_at' => 2.days.from_now
}
)
Rails.application.env_config['devise.mapping'] = Devise.mappings[:user]
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:facebook_app_rewards]
end
def application_controller_patch
ApplicationController.class_eval do
def omniauth
Rails.application.env_config['omniauth.auth']
end
end
end
The most likely reason your tests are failing is because actions triggered by click_link are not guaranteed to have completed when click_link returns. You can test that by adding a few second sleep after the click_link. If that fixes your test then you'll want to replace the sleep with an expectation of visual change on the page.
click_link 'Login via Facebook'
expect(page).to have_text('You have logged in!') # expectation for whatever happens on the page after a successful login
expect ... # rest of your test.
Note: Mocking/Stubbing methods on User in a feature test is generally a bad idea (mocking of anything but external services is generally a bad code smell in a feature test) as is direct DB access from feature tests. Feature tests are designed to be all about testing what a user experiences on the site, not about directly testing the implementation details.
I have this code to authenticate channel subscribers:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
if current_user = User.find_by(id: cookies.signed[:user_id])
current_user
else
reject_unauthorized_connection
end
end
end
end
And everything works fine. The problem is in feature tests.
When I run this test:
require 'rails_helper'
feature 'Chat room' do
scenario "send one message" do
user = create(:user)
login_as(user, :scope => :user)
expect {
fill_in 'message', with: 'hello friend'
click_button 'Send'
byebug
}.to change(Message, :count).by(1)
expect(current_path).to eq root_path
expect(page).to have_content 'hello friend'
end
end
The test log, says that the "An unauthorized connection attempt was rejected". Since the cookie is empty, it is not able to authenticate.
So how can I set the cookies in capybara tests?
I tried something do this cookies.signed[:user_id] = user.id in the test but it does not work.
How can I set the cookie like this cookies.signed[:user_id] = user.id in tests?
Assuming that the login_as you're calling is from the Warden test helpers, what it does is set up so that the next request sets the session cookie in the response. Because of this you probably need to visit a page after calling login_as. Additionally, since clicking 'Send' is asynchronous you need to wait for something to change before checking that Message.count has changed, and you really shouldn't be using with .eq with current_path if you want non-flaky tests. So all combined something like
#don't visit the page where you can fill in the message before calling login_as
scenario "send one message" do
user = create(:user)
login_as(user, :scope => :user)
visit 'the path to the page where you can fill in a message'
expect {
fill_in 'message', with: 'hello friend'
click_button 'Send'
expect(page).to have_css('#messages', text:'hello friend') # adjust selector depending on where the message is supposed to appear
expect(page).to have_current_path(root_path)
}.to change(Message, :count).by(1)
end
should work for you
Hello i have a Rspec/Capybara test im trying to make.
Im a logged in user that as admin user should be the only user to add Sizes to my app.
I just can't get the test to Login the user. Can anyone see why?
Please look at the sessions controller below. I do have a log_in method.
Error is
Failures:
1) adding size allow a admin user to add a size
Failure/Error: log_in(admin)
NoMethodError:
undefined method `log_in' for #<RSpec::ExampleGroups::AddingSize:0x007f8e4fa828e0>
# ./spec/features/sizes_features.rb:9:in `block (2 levels) in <top (required)>'
Test
require "rails_helper"
RSpec.feature "adding size" do
let(:size01) { FactoryGirl.build :size01 }
let(:user) { FactoryGirl.build :user }
let(:admin) { FactoryGirl.create(:user, admin: true) }
scenario "allow a admin user to add a size" do
log_in(admin)
size = create(:size01)
visit new_size_path
fill_in 'Title', with: "example"
click_button 'Create Size'
expect(current_path).to eql(sizes_path)
expect(page).to have_content("example")
end
scenario "user can't add size" do
log_in(user)
visit sizes_path
expect(current_path).to eql(root_path)
expect(page).to have_content("Rescricted Web Page")
end
scenario "vistor can't add size" do
visit sizes_path
expect(current_path).to eql(root_path)
end
end
FactoryGirl
FactoryGirl.define do
factory :user, :class => User do
username "example"
email "example#example.com"
admin "false"
password_digest "<%= User.digest('password') %>"
activated "true"
activated_at "<%= Time.zone.now %>"
end
end
Sessions controller.
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
#current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
#current_user = user
end
end
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
# Logs out the current user.
def log_out
session.delete(:user_id)
#current_user = nil
end
# Remembers a user in a persistent session.
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# Returns true if the given user is the current user.
def current_user?(user)
user == current_user
end
# Forgets a persistent session.
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
# Logs out the current user.
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
# Redirects to stored location (or to the default).
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# Stores the URL trying to be accessed.
def store_location
session[:forwarding_url] = request.url if request.get?
end
end
Here is why it's not working:
Access to session and request is not possible from the test, Access to
response is limited. Some drivers allow access to response headers and
HTTP status code, but this kind of functionality is not provided by
some drivers, such as Selenium.
source: Capybara documentation
You have two options:
If you're using Devise for authentication, Devise provides authentication helpers you should use.
Otherwise, here's how I would approach your situation:
Instead of trying to directly manipulate the session, create a shared context or a helper that logs in the user by interacting with the login form in the same way a user browsing your site would.
Here is one approach:
spec/support/when_authenticated.rb
RSpec.shared_context 'When authenticated' do
background do
authenticate
end
def authenticate
visit '/sessions/new'
within('form#session') do
fill_in 'Email', :with => 'user#example.com'
fill_in 'Password', :with => 'password'
end
click_button 'Sign in'
end
end
Then, in your feature spec:
RSpec.feature 'User does something' do
include_context 'When authenticated'
# examples
end
This has the effect of running the authentication procedure before each example in your spec.
I am new to programming, this is my first application.
While creating an application in Rails, i have two models. User&list,nested.
resources :users do
resources :lists
end
These are the following routes i obtain with this setting:
user_lists GET /users/:user_id/lists(.:format) lists#index
POST /users/:user_id/lists(.:format) lists#create
new_user_list GET /users/:user_id/lists/new(.:format) lists#new
edit_user_list GET /users/:user_id/lists/:id/edit(.:format)lists#edit
user_list GET /users/:user_id/lists/:id(.:format) lists#show
PUT /users/:user_id/lists/:id(.:format) lists#update
DELETE /users/:user_id/lists/:id(.:format) lists#destroy
With regards i have created the views with the following links.
<div class="stats">
<a href="<%= user_lists_path(current_user) %>">
<%= pluralize(current_user.lists.count, 'List') %>
</a>
</div>
<div class="list">
<%= link_to 'Create List', new_user_list_path(current_user) %>
</div>
These work as expected, however when i use the same url helpers in testing i get an error.
describe "List create page" do
let(:user) { FactoryGirl.create(:user) }
before do
user.save
visit new_user_list_path(user)
end
it { should have_selector('title', text: 'SocialTask | List') }
it { should have_selector('h1', text: 'Create list') }
describe "invalid list creation" do
before { click_button 'Create list' }
it { should have_content('Error in creating list') }
end
end
This causes the tests to have an error.
Failure/Error: visit new_user_list_path(user)
NoMethodError:
undefined method `lists' for nil:NilClass
I have tried playing around with the url that did not work.
I tried updating rspec/capybara that did not work either.
I have also checked the inclusion of
config.include Rails.application.routes.url_helpers
in the spec helper.
How do i get the helpers to work? Or am i missing some minor detail?
Thanks in advance.
Helper Methods.
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def signed_in?
!current_user.nil?
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
def current_user?(user)
current_user == user
end
end
The rspec helper to sign in.
support/utilities.rb
include ApplicationHelper
def sign_in(user)
visit signin_path
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Sign in"
cookies[:remember_token] = user.remember_token
end
Without seeing the stack trace, I think your problem is in the view on this line:
<%= pluralize(current_user.lists.count, 'List') %>
It seems like current_user is nil. Normally you should define some kind of helper method in your RSpec suite to simulate a user logging in. That way, current_user will return the user that you stub out in the test.
Here's an example:
# spec/support/session_helper.rb
module SessionHelper
def login(username = 'admin')
request.session[:user_id] = User.find_by_username(username).id
end
end
Yours will differ depending on how you authenticate your users. For example, Devise publishes its own set of test helpers, so you can simply include its helpers directly:
# spec/support/devise.rb
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
end
Seems it's getting an error because the user doesn't exists. Try to change user.save to user.save! then you'll catch the error on creation I think..
i have created an rspec test like :
it "should redirect to '/tavern' with an error if user already has a tavern quest" do
user = mock('User')
user.stub(:has_tavern_quest).and_return(true)
post :new_quest, :quest_type => 3
flash[:error].should_not be_nil
response.should redirect_to tavern_path
end
Then, i wrote the controller part :
# check if user already has a tavern quest
if current_user.has_tavern_quest?
flash[:error] = 'You already have a quest to finish !'
redirect_to tavern_path and return
end
And the model part :
def has_tavern_quest?
TavernQuest.exists?(self.id)
end
I would expect that the test succeeds, now but i get :
1) TavernController POST '/quest/' to get a new quest of quest_type == 3 should redirect to '/tavern' with an error if user already has a tavern quest
Failure/Error: flash[:error].should_not be_nil
expected: not nil
got: nil
# ./spec/controllers/tavern_controller_spec.rb:29
Do i have a mistake somewhere ?
THE MACRO FOR LOGIN USER :
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = :user
#user = Factory.create(:user)
sign_in #user
end
end
end
Untested:
it "should redirect to '/tavern' with an error if user already has a tavern quest" do
controller.stub_chain(:current_user,:has_tavern_quest?).and_return(true)
post :new_quest, :quest_type => 3
flash[:error].should_not be_nil
response.should redirect_to tavern_path
end
Your mock doesn't do anything... perhaps you meant to use it somewhere?
I personally dislike mocking in this case and feel it's obfuscation. If you are using Devise you could use their test helpers to sign in as a user.