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/
I solved this same problem using the following:
describe "should not be able to delete themselves" do
it { expect { delete user_path(admin) }.not_to change(User, :count) }
end
CallumD's solution worked for me, and seemed the most consistent with the techniques recommended in the rest of Michael Hartl's tutorial. But I wanted to tighten up the syntax a little to make it more consistent with the other specs in the same tutorial:
it "should not be able to delete itself" do
expect { delete user_path(admin) }.not_to change(User, :count)
end
You are confusing rspec-rails request specs which are integration tests and are executed in a simulated browser and controller specs which test controller in isolation. delete(action, *args)
(and get
, post
and so on) - is a method simulating request from ActionController::TestCase, so it's not available in your test.
So your only option is to simulate a click in a browser. I don't know how you hide your delete link, if the html is there but hidden you should be able to click it. If it's not there (removed on the server side when generating view) you can use capybara's page.execute_script
(but you have to enable javascript for this example :js => true
). You can either add the link back:
page.execute_script("$('body').append("<a href="/users/1" data-method="delete" rel="nofollow">Destroy</a>")")
or make ajax call:
page.execute_script("$.ajax({type:'DELETE',url:'/users/1'})")
Didn't test but something like this should work.
This is what I ended up with (Rspec 3.2):
describe 'DELETE destroy' do
before :each do
delete :destroy, { id: current_partner_role }
end
it 'destroys role' do
expect(assigns(:role).destroyed?).to be true
end
"destroyed?" method itself is spec-ed by Rails so IMHO it should be ok to rely on it.
https://github.com/rails/rails/blob/5142d5411481c893f817c1431b0869be3745060f/activerecord/lib/active_record/persistence.rb#L91
Try this:
expect { delete :destroy, :id => admin }.to_not change(User, :count)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With