I'm working through http://railstutorial.org (currently working on section 10.4.2 - destroying users).
My User model has a boolean admin attribute, and in the Users controller there is some simple code making use of this attribute:
def admin_user
redirect_to(root_path) unless current_user.admin?
end
The RSpec test:
describe "as a non-signed-in user" do
it "should deny access" do
delete :destroy, :id => #user
response.should redirect_to(signin_path)
end
end
And the error this test produces:
NoMethodError in 'UsersController DELETE 'destroy' as a non-signed-in user should deny access'
undefined method `admin?' for nil:NilClass
What is nil:NilClass? Why am I getting this?
edit: j., this might help:
def sign_in(user)
user.remember_me!
cookies[:remember_token] = { :value => user.remember_token,
:expires => 20.years.from_now.utc }
self.current_user = user
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= user_from_remember_token
end
Good news! Your rspec test found a bug!
Your controller method
def admin_user
redirect_to(root_path) unless current_user.admin?
end
is being called directly or indirectly by your controller's delete action.
And admin_user is being called when current_user is nil. So the admin_user method is failing with the error
undefined method `admin?' for nil:NilClass
fix: depends on how admin_user is being called and used. Is it in a filter?
Could be changed to either of
def admin_user
redirect_to(root_path) unless current_user && current_user.admin?
end
# or
def admin_user
redirect_to(root_path) if current_user && !current_user.admin?
end
depending on the situation....
It looks like your current_user object is nil and it should be nil.
As you are not signed in then surely your current _user is nil and then you are trying to get
nil.admin? so that's why it give No method error.
Probabily you should rewrite your method something like
def admin_user
redirect_to(root_path) unless current_user || current_user.admin?
end
Did you complete section 9.3.4 Current User of the tutorial? This creates a current_user variable within the SessionsController via the inclusion of the SessionsHelper module.
I had this exact problem, and I found that the underlying problem was that I'd failed to add :destroy to the before_filter :authenticate in app/controllers/users_controller.rb, as below (from listing 10.41:
before_filter :authenticate, :only => [:index, :edit, :update, :destroy]
Related
This is my first foray into app development, and I have been following Michael Hartl's tutorial pretty much verbatim. All went fine until towards the end of chapter 12, when my tests started failing. The strange thing is that the tests which fail seem to be unrelated to the changes I made in chapter 12.
There are three failures and one error, and they are also duplicated by the local server when manually checked in the browser. Basically the logged_in_user before action doesn't seem to work, but only with some of the model actions, not all. I've tried to trace it with the debugger and tried to revert back to pre-chapter 12 commits (which all definitely worked) but I'm having trouble with database migrations and stuff and only introducing more and more errors. Below is the error log, my users controller, application controller and sessions helper. I feel like the error must be in one of those.
Many many thanks in advance for your help!
FAIL["test_should_redirect_update_when_not_logged_in", UsersControllerTest, 2016-03-06 05:58:45 -0800]
test_should_redirect_update_when_not_logged_in#UsersControllerTest
(1457272725.92s)
Expected true to be nil or false
test/controllers/users_controller_test.rb:27:in `block in '
ERROR["test_should_redirect_destroy_when_not_logged_in", UsersControllerTest, 2016-03-06 05:58:45 -0800]
test_should_redirect_destroy_when_not_logged_in#UsersControllerTest
(1457272725.96s) NoMethodError: NoMethodError: undefined
method admin?' for nil:NilClass
app/controllers/users_controller.rb:77:inadmin_user'
test/controllers/users_controller_test.rb:47:in block (2 levels) in <class:UsersControllerTest>'
test/controllers/users_controller_test.rb:46:inblock in '
app/controllers/users_controller.rb:77:in admin_user'
test/controllers/users_controller_test.rb:47:inblock (2 levels) in '
test/controllers/users_controller_test.rb:46:in `block in '
FAIL["test_should_redirect_edit_when_not_logged_in", UsersControllerTest, 2016-03-06 05:58:46 -0800]
test_should_redirect_edit_when_not_logged_in#UsersControllerTest
(1457272726.03s)
Expected true to be nil or false
test/controllers/users_controller_test.rb:21:in `block in '
FAIL["test_successful_edit_with_friendly_forwarding", UsersEditTest, 2016-03-06 05:58:46 -0800]
test_successful_edit_with_friendly_forwarding#UsersEditTest
(1457272726.07s)
Expected response to be a redirect to <[removed link]/users/633107804/edit> but was a redirect to <[removed
link]/users/633107804>.
Expected "[removed link]users/633107804/edit" to be === "[removed link]/users/633107804".
test/integration/users_edit_test.rb:22:in `block in '
class UsersController < ApplicationController
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to_users_url
end
def show
#user = User.find(params[:id])
#microposts = #user.microposts.paginate(page: params[:page])
end
def new
#user = User.new
end
def index
#users = User.paginate(page: params[:page])
end
def create
#user = User.new(user_params)
if #user.save
#user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
def edit
#user = User.find(params[:id])
end
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to #user
else
render 'edit'
end
end
def following
#title = "Following"
#user = User.find(params[:id])
#users = #user.following.paginate(page: params[:page])
render 'show_follow'
end
def followers
#title = "Followers"
#user = User.find(params[:id])
#users = #user.followers.paginate(page: params[:page])
render 'show_follow'
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user)
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
end
And the application controller:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
include SessionsHelper
private
#confirms a logged in user. Now placed in the application controller so both teh user controller and microposts controller can use it
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
And the sessions helper:
module SessionsHelper
#logs in the given user
def log_in(user)
session[:user_id] = user.id
end
# returns the current logged-in user (if any)
def current_user
if (user_id = session[:user_id])
#current_user ||= User.find_by(id: session[: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, and false otherwise
def logged_in?
!current_user.nil?
end
#Saves a user id and remember token digest to cookies
def remember(user)
user.remember
#Code to save cookies with 20 year expiry ("permanent") and "signed" so it can't be accessed by third party
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# runs the forget method and removes remember tokens from cookies
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
# logs out the given user
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
def current_user?(user)
user == current_user
end
# Redirects to stored location after trying to edit while logged out and then logging in
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
Userscontrollertest:
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
#user = users(:Joe)
#other_user = users(:Jane)
end
test "should redirect to index when not logged in" do
get :index
assert_redirected_to login_url
end
test "should get new" do
get :new
assert_response :success
end
test "should redirect edit when not logged in" do
get :edit, id: #user
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch :update, id: #user, user: { name: #user.name, email: #user.email }
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect edit when logged in as wrong user" do
log_in_as(#other_user)
get :edit, id: #user
assert flash.empty?
assert_redirected_to root_url
end
test "should redirect update when logged in as the wrong user" do
log_in_as(#other_user)
patch :update, id: #user, user: { name: #user.name, email: #user.email }
assert flash.empty?
assert_redirected_to root_url
end
test "should redirect destroy when not logged in" do
assert_no_difference 'User.count' do
delete :destroy, id: #user
end
assert_redirected_to login_url
end
test "should redirect destroy when logged in as a non-admin" do
log_in_as(#other_user)
assert_no_difference 'User.count' do
delete :destroy, id: #user
end
assert_redirected_to root_url
end
end
After comparing your code to what I have in my tutorial code, try changing the order of the before_action statements in your UsersController and see if that works:
Before:
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
After:
before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
All the before_action statements are run in the order they are defined in the controller, so for the update action, you're currently running the correct_user filter first, which calls current_user?(#user), which calls current_user:
def current_user
if (user_id = session[:user_id])
#current_user ||= User.find_by(id: session[: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
At that point in your tests, since you haven't logged in a user yet, you don't have a session[:user_id], so that if statement fails, and you also don't have a cookies.signed[:user_id], so the elsif statement fails, leaving you with a nil return value and #current_user not being set.
At some point in your request in the test, you seem to be finding its way over to the admin_user method, which is calling current_user.admin?, and since at this point current_user is nil, you're getting the NoMethodError: undefined method admin?' for nil:NilClass that you're seeing.
I used the M. Hartl Rails Tutorial to create my app. So I have a User model, and all current_user and signed_in_user methods.
I want to make the following test pass :
describe "submitting a PATCH request to the Users#update action" do
before do
be_signed_in_as FactoryGirl.create(:user)
patch user_path(FactoryGirl.create(:user))
end
specify { expect(response).to redirect_to(root_path) }
end
But the test fails :
Failure/Error: specify { expect(response).to redirect_to(root_path) }
Expected response to be a redirect to <http://www.example.com/> but was a redirect to <http://www.example.com/signin>.
Expected "http://www.example.com/" to be === "http://www.example.com/signin".
So here is a part of the User controller
class UsersController < ApplicationController
before_action :signed_in_user, only: [:index, :edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
.
.
.
.
private
def signed_in_user
unless !current_user.nil?
store_url
redirect_to signin_url, notice: t('sign.in.please')
end
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_path) unless current_user?(#user)
end
def admin_user
redirect_to(root_path) unless current_user.admin?
end
end
If I remove the before_create :signed_in_user... line, the test pass.
But why is that ? The be_signed_in_as spec method is working in all the others test (~ 1k) so the reason must lie within the specify { expect(response) thing.
Your test is going to a user_path for a different user than you're logging in as, so you're getting redirected to the root by your correct_user filter. You need to save the user that you're logging in as and use that for your user_path.
Each time you call FactoryGirl.create(:user), you're creating an additional user. The code you list is creating two separate user records in the database. So unless you're intending on creating two different users for this test, you should probably have a line before the before block like:
let(:user) { FactoryGirl.create(:user) }
Then just refer to user everywhere you want the one user record.
I'm trying to get before_filter to work on the actions that requires the user to be logged in, however something must be wrong because it's not.
I use a helper file called 'session_helper.rb' for login/logout as well as for checking if the user is logged in (signed_in?). That works fine if used inside an action or in the view, however while using it with the before_filer it's not working. If I log out the user and try to access '/projects/new' it's possible to do that, while it shouldn't be.
What am I doing wrong?
project controller:
class ProjectsController < ApplicationController
before_filter :signed_in?, :except => [:index] // <-- doesn't prevent e.g. the action "new" to be executed
def new
#project = Project.new
#users = (current_user.blank? ? User.all : User.find(:all, :conditions => ["id != ?", current_user.id]))
end
def index
#projects = Project.all
if signed_in? // <-- works as it should
#users_projects = Project.where(:user_id => current_user.id)
end
end
... other actions ...
end
sessions_helper.rb
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
end
So, before_filter is a slightly misleading name. It is not really a filter. It isn't that it'll filter out the other actions and prevent them occurring if you return a falsey value, and allow them if you return a truthy one. It's really a way of calling a method before anything else. Think of it as 'before calling the action that the route has triggered, call the following method'.
Indeed, in Rails 4 they are renaming before_filter to before_action and that should alleviate the confusion moving forward.
You're just returning T/F from signed_in? So it's checking that and moving on, as you haven't told it to do anything special based on the results of that check.
So rather than calling signed_in? Something like this would work:
before_filter :authorize, :except => [:index]
def authorize
redirect_to login_url, alert: "Not authorized" if !signed_in?
end
Hop that helps.
I've always seen the before_filter raise an exception or redirect to another page when there is no current login. I am not sure that returning false will prevent the page from rendering.
I can't get my tests to work and was wondering if anyone have any pointers. I am testing my user edit page such that you have to be logged in to be able to see and edit the user profile. Following the authlogic documentation, here are the relevant codes:
class ApplicationController < ActionController::Base
helper_method :current_user_session, :current_user
private
def current_user_session
return #current_user_session if defined?(#current_user_session)
#current_user_session = UserSession.find
end
def current_user
return #current_user if defined?(#current_user)
#current_user = current_user_session && current_user_session.user
end
def require_current_user
unless current_user
flash[:error] = 'You must be logged in'
redirect_to sign_in_path
end
end
end
class UsersController < ApplicationController
before_filter :require_current_user, :only => [:edit, :update]
...
end
In my users_controller_spec
describe "GET 'edit'" do
before(:each) do
#user = Factory(:user)
UserSession.create(#user)
end
it "should be successful" do
# test for /users/id/edit
get :edit, :id => #user.id
response.should be_success
end
I tested it using the browser and it works. You have to be logged in to be able to edit your profile but it doesn't work in my rspec test.
I have a feeling it has something to do with mock controller but I can't figure out how. I've also read the test case but still can't get it to work.
Please help and much thanks!!!
You can stub the current_user method on the ApplicationController like so:
fake_user = controller.stub(:current_user).and_return(#user) #you could use a let(:user) { FactoryGirl.create(:user) } instead of before_each
get :edit, :id => fake_user.id
response.should be_success
I'm working through the railstutorial.org book, chapter 9. When running the test suite, I can't get rid of the following failure. All the code has been attempted by first typing it in and then by copying and pasting from the book.
...............F............................................
Failures:
1) SessionsController DELETE 'destroy' should sign a user out
Failure/Error: controller.should_not be_signed_in
expected signed_in? to return false, got true
# ./spec/controllers/sessions_controller_spec.rb:69:in `block (3 levels) in <top (required)>'
Finished in 8.28 seconds
60 examples, 1 failure
Here's the code for the test in sessions_controller_spec.rb:
describe "DELETE 'destroy'" do
it "should sign a user out" do
test_sign_in(Factory(:user))
delete :destroy
controller.should_not be_signed_in
response.should redirect_to(root_path)
end
end
The (I think) relevant portions of sessions_controller.rb:
def destroy
sign_out
redirect_to root_path
end
The sign_out method is found in sessions_helper.rb
def current_user=(user)
#current_user ||= user_from_remember_token
end
def current_user
#current_user
end
def signed_in?
!current_user.nil?
end
def sign_out
cookies.delete(:remember_token)
self.current_user = nil
end
So, if I understand this correctly, the test, after signing in a factory user, is calling the SessionsController's destroy method, which calls sign_out (from SessionsHelper), which is explicitly setting self.current_user to nil. Then the test checks signed_in? with the line be_signed_in. Since the code sets self.current_user to nil, current_user.nil? should return true, !current_user.nil? should return false, which is what the test wants (controller.should_not be_signed_in).
Any help would be appreciated. I'm learning Ruby, Rails and TDD all at the same time, so I'm not sure where my problem lies.
The problem is how you are setting current user - you are only ever setting it to login from the remember token, you aren't ever allowing it to be set to nil. You might want to try the following:
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= user_from_remember_token
end