I have a Rails 3.2 app with Devise and CanCan. User abilities are defined by roles. There are five Roles in the database with id values 1-5, and a User has_one role through a Membership. So, for example, Membership.create(user_id: 2, role_id: 4) will lead to User.find(2).role == Role.find(4) # => true
My ability file looks like
if user.has_role? :admin
can :manage, Model
can :manage, AnotherModel
# etc.
elsif user.has_role? :basic
can :read, Model
can :read, AnotherModel
elsif user.has_role? (etc. for other roles)
(etc.)
else
cannot :manage, :all
I want to kick out people who are attempting to access pages they are not authorized for. My application controller includes:
rescue_from CanCan::AccessDenied do |exception|
reset_session
redirect_to new_user_session_path
end
This behavior works properly when clicking around in development—-a basic user created via rails console gets kicked out when trying to do admin things, and a user with no roles (created in console, then User.last.membership.destroy so that User.last.role # => nil) cannot log in to the dashboard--instead the user is immediately redirected to the login page (a User with no role cannot manage anything).
I'm having problems when trying to test this same behavior, specifically for users with no role:
# spec/support/login_helpers.rb
def login_via_sign_in_page(user)
visit new_user_session_path
fill_in :user_email, with: user.email
fill_in :user_password, with: user.password
click_button 'Sign in'
end
# spec/features/dashboard_spec.rb
describe "as a user with no role" do
let(:user) { FactoryGirl.create(:user) }
before { login_via_sign_in_page(user) }
it "should redirect to the sign_in page" do
current_path.should == new_user_session_path
end
end
The above test fails, with RSpec showing that the user successfully made it to the dashboard page instead of being redirected to the sign in page. However, the following specs do pass:
# spec/features/dashboard_spec.rb
describe "as a guest browser (no login whatsoever)" do
it "should redirect to the sign_in page" do
visit dashboard_index_path
current_path.should == new_user_session_path
end
end
describe "as an admin user" do
let(:user) { FactoryGirl.create(:admin_user) }
before { login_via_sign_in_page(admin_user) }
it "should go to the dashboard" do
visit dashboard_index_path
current_path.should == dashboard_index_path
end
end
...as well as things like basic user getting redirected to the sign_in page when attempting admin actions
I have read this post, which seems to be dealing with a similar issue, but the suggestions there have not helped.
Finally, I get confusing results when using debugger and save_and_open_page. The former will give me user # => <A legit User object> and user.role # => nil. However, save_and_open_page shows me logged in on the dashboard page.
Have you tried including the Warden::Test::Helpers, and the rest of the config required for testing Devise with Capybara? Take a look here How To: Test with Capybara. Which driver are you using?
Related
Having trouble with a rspec spec. The actual code seems to be working fine, but my spec is still failing. I'm using FactoryGirl, Rspec and Capybara. This is part of spec/features/admin/admin_users_spec.rb:
require 'spec_helper'
describe "Admin::Users" do
subject { page }
context "When admin signed in" do
let(:admin) { FactoryGirl.create(:admin) }
let(:user) { FactoryGirl.create(:user) }
before { sign_in admin }
context "when visiting admin users" do
before { visit admin_users_path }
it_should_behave_like "admin visiting admin page"
it { should have_content('- User Index') }
it { should have_content(user.email) } # This test is failing
end
end
end
The test that is failing has the comment.
Here is my factories for users.
require 'faker'
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
admin false
super_admin false
password "password"
password_confirmation "password"
confirmed_at Time.now
factory (:admin) do
admin true
end
factory (:super_admin) do
admin true
super_admin true
end
end
end
The idea that when you go to the index for users in the admin dashboard, admins should see a list of users. So in addition to the current admin, I create another user to populate the test database. Here is a sample failure message:
Failures:
1) Admin::Users When admin signed in when visiting admin users should text "christian.stark#rempel.com"
Failure/Error: it { should have_content(user.email) }
expected to find text "christian.stark#rempel.com" in "Under The Gun Theater Admin Edit profile Logout Dashboard Users Posts Pages Shows Classes People Photos Admin Dashboard - User Index Email Roles Last Logged In geovanni.marks#kaulke.com admin May 18, 2014 11:59 Logged in as geovanni.marks#kaulke.com"
# ./spec/features/admin/admin_users_spec.rb:53:in `block (4 levels) in <top (required)>'
As you can see, the table that is generated on the page only includes the admin's email address. The other user "christian.stark#rempel.com" is not found. Again, when I actually log into the app it works, so I'm not sure why this test is not working.
Use let! instead of let for user.
let is lazily evaluated, that is, user won't come into existence till something is called upon it. let! helps out in sorting this issue by eagerly evaluating the block.
let!(:user) { FactoryGirl.create(:user) }
Read more about let and let! here.
OR, you can create a user manually in before block as:
before do
#user = FactoryGirl.create(:user)
sign_in admin
end
#corresponding test:
it { should have_content(#user.email) } # This test should pass
Your user is not being created until after you've loaded the page. lets are lazy in RSpec. The user record gets created on this line:
it { should have_content(user.email) }
which will always fail since the user wasn't created before the time that the page was loaded. You can change let(:user) to let!(:user), which will run it eagerly before the test. You might want to consider a different structure though, which helps avoid these issues:
admin = create(:user, :admin)
user = create(:user)
sign_in admin
visit admin_users_path
expect(page).to have_content('- User Index')
expect(page).to have_content(user.email)
Stack: Rails '4.0.4', devise, rSpec, factory_girl, cappybara + selenium-webdriver, mySQL
I'm stll finding myself a little confused controlling user auth in my tests, but this patchwork from other examples is working for now. I have a file called request_helpers.rb in /support that contains:
require 'spec_helper'
include Warden::Test::Helpers
module RequestHelpers
class Login
def self.create_logged_in_user
user = FactoryGirl.create(:user)
login(user)
user
end
def self.login(user)
login_as user, scope: :user, run_callbacks: false
end
end
end
And this is an example of a passing test:
require "spec_helper"
feature "Story Management" do
let( :authorized_user ){ RequestHelpers::Login.create_logged_in_user }
scenario "has a valid factory" do
authorized_user.should be_an_instance_of( User )
end
scenario "Can visit root", js:true do
visit root_path( authorized_user )
page.should have_content( "Your Stories" )
end
end
My question is, How can I logout my authorized user, and log in a new authorized user? Every attempt to utilize devise logout method in my request helper hasn't worked.
Here is my attempt at testing this:
require 'spec_helper'
include Warden::Test::Helpers
Warden.test_mode!
module RequestHelpers
class Login
def self.create_logged_in_user
user = FactoryGirl.create(:user)
login(user)
user
end
def self.login(user)
login_as user, scope: :user, run_callbacks: false
end
def self.logout(user)
logout( user )
end
end
end
scenario "Two users can take turns adding 3 chapters each" do
chapter_string = ValidString.short
player1 = create(:user)
player2 = create(:user)
RequestHelpers::Login.login(player1)
visit new_story_path( player1 )
fill_in "story_title", with: ValidString.short
fill_in "co_author", with: player2.email
click_button "Create Story"
click_link "New Chapter"
fill_in "chapter_body", with: chapter_string
click_button "Create Chapter"
page.should have_content(chapter_string)
RequestHelpers::Login.logout(player1)
RequestHelpers::Login.login(player2)
fill_in "chapter_body", with: chapter_string
click_button "Create Chapter"
page.should have_content(chapter_string)
end
Failed test text:
1) Chapter Management Two users can take turns adding 3 chapters each
Failure/Error: Unable to find matching line from backtrace
SystemStackError:
stack level too deep
# ./spec/support/request_helpers.rb:16
I decided to stop trying to hack around devise here, and just fill in the forms like they suggested in the docs. Logging in and out works fine in this scenario, although slower.
From Devise docs:
These helpers are not going to work for integration tests driven by Capybara or Webrat. They are meant to be used with functional tests only. Instead, fill in the form or explicitly set the user in session.
I'm working on exercise 9 from chapter 9 in Rails Tutorial: http://ruby.railstutorial.org/chapters/updating-showing-and-deleting-users#fnref-9_9
"Modify the destroy action to prevent admin users from destroying themselves. (Write a test first.)"
I started with creating test:
users_controller_spec.rb
require 'spec_helper'
describe UsersController do
describe "admins-userscontroller" do
let(:admin) { FactoryGirl.create(:admin) }
let(:non_admin) { FactoryGirl.create(:user) }
it "should not be able to delete themself" do
sign_in admin
expect { delete :destroy, :id => admin.id }.not_to change(User, :count)
end
end
end
however, noticed that even if logic for prohibiting admin to delete himself is not implemented, test passes unless I change line
sign_in admin
to
sign_in admin, no_copybara: true
after this change test fails (as expected)
sign_in is in support\utilities.rb file and looks like this:
def sign_in(user, options={})
if options[:no_capybara]
# Sign in when not using Capybara.
remember_token = User.new_remember_token
cookies[:remember_token] = remember_token
user.update_attribute(:remember_token, User.hash(remember_token))
else
visit signin_path
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Sign in"
end
end
Does anyone know why it doesn't work with capybara? It looks like the "else" section of the above code fails/doesn't execute when using capybara but it doesn't return any error (e.g. that "Email" field is not found, so it looks like it's rendered)...
Other problem is that if I remove non_admin instead of admin:
expect { delete :destroy, :id => non_admin.id }.not_to change(User, :count)
test passes which means non_admin isn't deleted... why does it work for admin and not non_admin?
Question 2:
capybara is not supposed to work in request spec as of 2.0+, but Im using capybara 2.1 and rspec-rails 2.13.1 and it works just fine in request specs (actually that's even what tutorial tells us to do), doesn't even output any warning...
Rails newbie. Trying to follow Michael Hartl's tutorial.
Stuck trying to add a helper method to simulate log in an RSpec test:
describe "when the a user has logged in and attempts to visit the page" do
let(:user) { FactoryGirl.create :user }
before do
log_in user
end
it "should redirect the user to next page" do
specify { response.should redirect_to loggedin_path }
end
end
In my spec/support/utilities.rb:
def log_in user
visit root_path
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Log in"
cookies[:remember_token] = user.remember_token
end
Error:
Failure/Error: log_in user
NoMethodError:
undefined method `cookie_jar' for nil:NilClass
What gives?
Edit, full stack trace:
Index page when the a user has logged in and attempts to visit the page should redirect the user to next page
Failure/Error: log_in user
NoMethodError:
undefined method `cookie_jar' for nil:NilClass
# ./spec/support/utilities.rb:8:in `log_in'
# ./spec/features/pages/index_spec.rb:20:in `block (3 levels) in <top (required)>'
RSpec is very particular about the directory that you place your tests. If you put the test in the wrong directory, it won't automagically mix-in the various test helpers that setup different types of tests. It seems your setup is using spec/features which is not an approved default directory (spec/requests, spec/integration, or spec/api).
Based on the tutorial page, I'm not sure how they have the spec_helper.rb file setup. Though the examples so they are using spec/requests to hold the tests.
You can force RSpec to recognize another directory for request specs by using on of the following:
Manually add the proper module to the test file:
# spec/features/pages/index_spec.rb
require 'spec_helper'
describe "Visiting the index page" do
include RSpec::Rails::RequestExampleGroup
# Rest of your test code
context "when the a user has logged in and attempts to visit the page" do
let(:user) { FactoryGirl.create :user }
before do
log_in user
end
specify { response.should redirect_to loggedin_path }
end
end
Include this in your spec/spec_helper.rb file:
RSpec::configure do |c|
c.include RSpec::Rails::RequestExampleGroup, type: :request, example_group: {
file_path: c.escaped_path(%w[spec (features)])
}
end
Since this is a tutorial I'd recommend following the standard of including require 'spec_helper' at the top of the spec file and that your actual spec/spec_helper.rb file has require 'rspec/rails'
A minor note, you don't need to put a specify inside of an it block. They are aliases of each other, so just use one.
context "when the a user has logged in and attempts to visit the page" do
let(:user) { FactoryGirl.create :user }
before do
log_in user
end
# All of the following are the same
it "redirects the user to next page" do
response.should redirect_to loggedin_path
end
it { response.should redirect_to loggedin_path }
specify "redirects the user to next page" do
response.should redirect_to loggedin_path
end
specify { response.should redirect_to loggedin_path }
end
Note, according to the documentation for capybara, you should be able to put your capybara tests into spec/features. To make this work, ensure you are loading require 'capybara/rspec' in your spec_helper or test spec file directly.
However, looking at the source, I didn't see where they are automatically including that directory. You can also try adding the tag type: :feature to the outer describe block in your test file. Though the more likely solution is to use spec/requests.
Shouldn't you have the "user" argument of the method enclosed in parenthesis?
def log_in(user)
visit root_path
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Log in"
cookies[:remember_token] = user.remember_token
end
To have a mock cookie jar, you must have either rack-test or rspec-rails gem included in your Gemfile. I think maybe you have included just rspec and maybe missed out rspec-rails.
You also need to ensure you've configured the session store as follows:
config.session_store = :cookie_store
This must have been done in either config/application.rb or some file under config/initializers. If you have configured this in config/environments/development.rb or somewhere else, the Test environment will not be able to pick it up.
Inside a controllers test, I want to test that when logged in, the controller renders the request fine, else if not logged in, it redirects to the login_path.
The first test passes fine as expected, no user is logged in, so the request is redirected to the login_path. However I've tried a myriad of stub/stub_chain's but still can't get the test to fake a user being logged in and render the page okay.
I would appreciate some direction on getting this to work as expected.
The following classes and tests are the bare bones to keep the question terse.
ApplicationController
class ApplicationController < ActionController::Base
include SessionsHelper
private
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
end
SessionsHelper
module SessionsHelper
def logged_in?
redirect_to login_path, :notice => "Please log in before continuing..." unless current_user
end
end
AppsController
class AppsController < ApplicationController
before_filter :logged_in?
def index
#title = "apps"
end
end
apps_controller_spec.rb
require 'spec_helper'
describe AppsController do
before do
#user = FactoryGirl.create(:user)
end
describe "Visit apps_path" do
it "should redirect to login path if not logged in" do
visit apps_path
current_path.should eq(login_path)
end
it "should get okay if logged in" do
#stubs here, I've tried many variations but can't get any to work
#stubbing the controller/ApplicationController/helper
ApplicationController.stub(:current_user).and_return(#user)
visit apps_path
current_path.should eq(apps_path)
end
end
end
This is not working because you are stubbing the method current_user on the ApplicationController class, and not an instance of that class.
I would suggest stubbing it (correctly) on an instance of that class, but your test appears to be an integration test rather than a controller test.
What I would do instead then is as Art Shayderov mentioned is to emulate the sign-in action for a user before attempting to visit a place that requires an authenticated user.
visit sign_in_path
fill_in "Username", :with => "some_guy"
fill_in "Password", :with => "password"
click_button "Sign in"
page.should have_content("You have signed in successfully.")
In my applications, I've moved this into a helper method for my tests. This is placed into a file at spec/support/authentication_helpers.rb and looks like this:
module AuthenticationHelpers
def sign_in_as!(user)
visit sign_in_path
fill_in "Username", :with => user.username
fill_in "Password", :with => "password"
click_button "Sign in"
page.should have_content("You have signed in successfully.")
end
end
RSpec.configure do |c|
c.include AuthenticationHelpers, :type => :request
end
Then in my request specs, I simply call the method to sign in as that particular user:
sign_in_as(user)
Now if you want to sign in using a standard controller test, Devise already has helpers for this. I generally include these in the same file (spec/support/authentication_helpers.rb):
RSpec.configure do |c|
c.include Devise::TestHelpers, :type => :controller
end
Then you can sign in using the helpers like this:
before do
sign_in(:user, user)
end
it "performs an action" do
get :index
end
I would look at http://ruby.railstutorial.org/chapters/sign-in-sign-out#sec:a_working_sign_in_method.
The author describes how to write a sign_in method and use it in your rspec tests.
It doesn't look like controller test. It looks more like rspec-rails request spec which simulates browser. So stabbing controller won't work, you have to either simulate sign in (something like this)
visit sign_in
fill_in 'username', :with => 'username'
...
or manually add user_id to session.
If on the other hand you want to test controller in isolation your test should look like that:
get 'index'
response.should be_success