Note: I've read this question and the answer, but for some reason the code isn't working for me. (see below for the error I'm getting)
Exercise 10 from Chapter 9 of the Rails Tutorial asks you to: Modify the destroy action [for users] to prevent admin users from destroying themselves. (Write a test first.)
The tricky part here is testing it, because the application already hides the "delete" link for the current user, so you have to do the http request directly.
I got the code working, and tested it by removing the piece of code that hides the delete link for the current user. Sure enough, if I click on the delete link for the currently logged-in user, it redirects me and gives me the notice message.
From users_controller.rb
def destroy
@user = User.find(params[:id])
if current_user?(@user)
redirect_to users_path, notice: "You can't destroy yourself."
else
@user.destroy
flash[:success] = "User destroyed."
redirect_to users_path
end
end
The problem I'm having is in writing the tests for this that will send the delete request and call the destroy method. I tried the solution from Rspec test for destroy if no delete link, which I'm copying here:
From user_pages_spec.rb
describe "destroy" do
let(:admin) { FactoryGirl.create(:admin) }
it "should not allow the admin to delete herself" do
sign_in admin
#expect { delete user_path(admin), method: :delete }.should change(User, :count)
expect { delete :destroy, :id => admin.id }.should_not change(User, :count)
end
end
But when I run this, I get this error from RSpec
Failures:
1) User Pages destroy should not allow the admin to delete herself
Failure/Error: expect { delete :destroy, :id => admin.id }.should_not change(User, :count)
ArgumentError:
bad argument (expected URI object or URI string)
# ./spec/requests/user_pages_spec.rb:180:in `block (4 levels) in <top (required)>'
# ./spec/requests/user_pages_spec.rb:180:in `block (3 levels) in <top (required)>'
So, my questions are: 1) Why is this code above failing? 2) How do I simulate a "delete" in order to call the destroy action in my controller?
Environment: Mac OSX ruby 1.9.3p194 Rails 3.2.3
Gems for testing:
group :test do
gem 'rspec-rails', '2.9.0'
gem 'capybara', '1.1.2'
gem 'rb-fsevent', '0.4.3.1', :require => false
gem 'growl', '1.0.3'
gem 'guard-spork', '0.3.2'
gem 'spork', '0.9.0'
gem 'factory_girl_rails', '1.4.0'
end
More Info I have tried a ton of ways to try to simulate clicking on the delete link and none seem to work. I've been using the debugger gem to see if the destroy method is even being called. In the test that clicks on the link to delete a different user, the destroy method gets called and it works fine:
it "should be able to delete another user" do
expect { click_link('delete') }.to change(User, :count).by(-1)
end
But nothing I have tried to generate the delete request directly has worked to call the destroy method.
Thanks for your help!
Will
** UPDATE **
I tried DVG's suggestion:
describe "destroy" do
let(:admin) { FactoryGirl.create(:admin) }
it "should not allow the admin to delete herself" do
sign_in admin
#expect { delete user_path(admin), method: :delete }.should change(User, :count)
expect { delete :destroy, :id => admin }.to_not change(User, :count)
end
end
And got this error:
6) User Pages destroy should not allow the admin to delete herself
Failure/Error: expect { delete :destroy, :id => admin }.to_not change(User, :count)
ArgumentError:
bad argument (expected URI object or URI string)
# ./spec/requests/user_pages_spec.rb:190:in `block (4 levels) in <top (required)>'
# ./spec/requests/user_pages_spec.rb:190:in `block (3 levels) in <top (required)>'
SOLUTION
I figured it out after FOREVER.
I had to use Rack::Test to issue the DELETE request, but Capybara and Rack::Test don't share the same MockSession, so I had to pull in the :remember_token and :!sample_app_session cookies and put them into the DELETE request manually. Here is what worked. (the other problem I was having, listed below, was that I had a force_ssl statement that was not letting my destroy action get called.
describe "destroy" do
let!(:admin) { FactoryGirl.create(:admin) }
before do
sign_in admin
end
it "should delete a normal user" do
user = FactoryGirl.create(:user)
expect { delete user_path(user), {},
'HTTP_COOKIE' => "remember_token=#{admin.remember_token},
#{Capybara.current_session.driver.response.headers["Set-Cookie"]}" }.
to change(User, :count).by(-1)
end
it "should not allow the admin to delete herself" do
expect { delete user_path(admin), {},
'HTTP_COOKIE' => "remember_token=#{admin.remember_token},
#{Capybara.current_session.driver.response.headers["Set-Cookie"]}" }.
to_not change(User, :count)
end
end
I had a force_ssl statement after my before_filters in my users_controller.rb and this was somehow throwing things off so I never got to the destroy action.
class UsersController < ApplicationController
before_filter :signed_in_user, only: [:edit, :update, :index]
before_filter :existing_user, only: [:new, :create]
before_filter :correct_user, only: [:edit, :update]
before_filter :admin_user, only: :destroy
#force_ssl
def index
@users = User.paginate(page: params[:page])
end
def show
@user = User.find(params[:id])
@microposts = @user.microposts.paginate(page: params[:page])
end
def destroy
@user = User.find(params[:id])
if current_user?(@user)
redirect_to users_path, notice: "You can't destroy yourself."
else
@user.destroy
flash[:success] = "User destroyed."
redirect_to users_path
end
end
These were helpful in getting to a solution
https://gist.github.com/484787
http://collectiveidea.com/blog/archives/2012/01/05/capybara-cucumber-and-how-the-cookie-crumbles/