RSpec test destroy method (Rails Tutorial 3.2 Ch. 9, Ex. 10)

will_d picture will_d · Jun 12, 2012 · Viewed 19.5k times · Source

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/

Answer

CallumD picture CallumD · Jul 28, 2012

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