Rails 3.0.9 + Devise + Cucumber + Capybara the infamous “No route matches /users/sign_out”

capybaracucumberdeviserspecruby-on-rails-3

I am using devise 1.4.2 with rails 3.0.9, cucumber-rails 1.0.2, capybara 1.0.0. I got No route matches "/users/sign_out" error when I clicked logout. I added :method => :delete to link_to tag after going through this so question ( no-route-matches-users-sign-out-devise-rails-3 ).

Since I replaced prototype with jquery, I also had to change

config.action_view.javascript_expansions[:defaults] = %w(jquery rails)

to

config.action_view.javascript_expansions[:defaults] = %w(jquery jquery_ujs)

to get around rails.js not found error.

Although with above changes I am able to successfully sign out and redirected to root, when I look at response of localhost:3000/users/sign_out request in FireBug it shows the same routing error message click here to see the screenshot with notes

After successfully implementing authentication to rails 3 app through devise, When I added feature and specs using Cucumber + Capybara + RSpec following this tutorial (github.com/RailsApps/rails3-devise-rspec-cucumber/wiki/Tutorial), I got following error

When I sign in as "user@test.com/please"                              # features/step_definitions/user_steps.rb:41
Then I should be signed in                                            # features/step_definitions/user_steps.rb:49
And I sign out                                                        # features/step_definitions/user_steps.rb:53
  No route matches "/users/sign_out" (ActionController::RoutingError)
  <internal:prelude>:10:in `synchronize'
  ./features/step_definitions/user_steps.rb:55:in `/^I sign out$/'
  features/users/sign_out.feature:10:in `And I sign out'
And I should see "Signed out"                                         # features/step_definitions/web_steps.rb:105
When I return next time                                               # features/step_definitions/user_steps.rb:60
Then I should be signed out  

with the following step_definition for 'I sign out'

Then /^I sign out$/ do
    visit('/users/sign_out')
end

I searched a lot and found that this is because of unobrusive javascript in Rails 3 being used for 'data-method' attributes, but I also read somewhere that Capybara does check for data-method attributes and behaves accordingly. But it did not work for me, so following this post Capybara attack: rack-test, lost sessions and http request methods I changed my step definition to following:

Then /^I sign out$/ do
    rack_test_session_wrapper = Capybara.current_session.driver
    rack_test_session_wrapper.process :delete, '/users/sign_out'
end

but I got undefined method process for Capybara::RackTest::Driver (NoMethodError).

Following this lead I changed the above step definition as following:

Then /^I sign out$/ do
    rack_test_session_wrapper = Capybara.current_session.driver
    rack_test_session_wrapper.delete '/users/sign_out'
end

This at least passed the 'I sign out' step, but it did not redirected to the home page after signing out and the next step failed:

And I should see "Signed out"                                         # features/step_definitions/web_steps.rb:105
  expected there to be content "Signed out" in "YasPiktochart\n\n  \n      Signed in as user@test.com. Not you?\n      Logout\n  \n\n    Signed in successfully.\n\n  Home\n  User: user@test.com\n\n\n\n" (RSpec::Expectations::ExpectationNotMetError)
  ./features/step_definitions/web_steps.rb:107:in `/^(?:|I )should see "([^"]*)"$/'
  features/users/sign_out.feature:11:in `And I should see "Signed out"'

After all this I had to resort to adding 'GET' method for logout in the routes file:

devise_for :users do get 'logout' => 'devise/sessions#destroy' end

modified my view from

<%= link_to "Logout", destroy_user_session_path, :method => :delete %>

to

<%= link_to "Logout", logout_path %>

and changed my step definition to following:

Then /^I sign out$/ do
    visit('/logout')
end

This obviously solved all the problems, all the tests passed and firebug did not show any error on sign_out. But I know that using 'get' request for destroying sessions is not a good practice, because it's a state-changing behavior.

Could this be due to particular version or Rails, Devise, Cucumber-Rails, or Capybara I am using? I want to use Devise's default sign_out route instead of overriding it with get method and be able to do BDD using Cucumber and RSpec. I am new to using Cucumber+Capybara, does there exists another method to send POST request instead of using "visit('/users/sign_out')", which only uses GET method?

Best Answer

So I have found that

<%= link_to "Logout", destroy_user_session_path, :method => :delete %>

rails helper generates following html

<a rel="nofollow" data-method="delete" href="/users/sign_out">Sign out</a>

and jquery_ujs.js has following method to convert the links with data-method="delete" attribute to a form and submit at runtime:

// Handles "data-method" on links such as:
// <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
handleMethod: function(link) {
var href = link.attr('href'),
method = link.data('method'),
csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content'),
form = $('<form method="post" action="' + href + '"></form>'),
metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';
if (csrf_param !== undefined && csrf_token !== undefined) {
metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
}
form.hide().append(metadata_input).appendTo('body');
form.submit();
}

And Capybara helper visit('/users/sign_out') simply clicks the link and send a GET request to the server which does not have any route for this request.

As opposed to link_to helper the button_to helper adds the required form within the html when page is rendered instead of relying on javascript:

<%= button_to "Logout", destroy_user_session_path, :method => :delete %>

generates following html

<form class="button_to" action="/users/sign_out" method="post">
    <div>
        <input type="hidden" name="_method" value="delete">
        <input type="submit" value="Logout">
        <input type="hidden" name="authenticity_token" value="0Il8D+7hRcWYfl7A1MjNPenDixLYZUkMBL4OOoryeJs=">
    </div>
</form>

with this I can easily use Capybara helper click_button('Logout') in my 'I sign out' step definition.

"link_to with a method anything other than GET is actually a bad idea, as links can be right clicked and opened in a new tab/window, and because this just copies the url (and not the method) it will break for non-get links..."

As Max Will explained right clicking and opening the link_to link with non-get data-method in new tab results in a broken link.

Some more useful discussion on link_to helper with ':method => :delete' and capybara issue can be found on this link

For now I would stick to simple link_to helper without :method attribute, and would prefer using button_to if I want to switch to non-get method for deleting.

At the same time I think there should be a capybara helper equivalent to Visit to cater for data-method attribute to send post request, so that one could avoid using javascript based driver for integration testing. Or may be there already is one which I am not aware of. Correct me if I am wrong.

Related Topic