Rails 3.0.9 + Devise + Cucumber + Capybara the infamous "No route matches /users/sign_out"
capybara, cucumber, devise, rspec, ruby-on-rails-3
Solution
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.
Problem
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?