Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 "[email protected]/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 [email protected]. Not you?\n      Logout\n  \n\n    Signed in successfully.\n\n  Home\n  User: [email protected]\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?

like image 688
Zeeshan Avatar asked Jul 05 '11 08:07

Zeeshan


3 Answers

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.

like image 152
Zeeshan Avatar answered Nov 17 '22 01:11

Zeeshan


The easiest way to correct this problem (albeit probably not the most correct one) is to modify your routes file to match the rest of the application. E.g. make the GET version of destroy_user_session_path work. You can do this by modifying the routes file as follows

Remove:

devise_for :users

Add:

devise_for :users do
  get "/users/sign_out" => "devise/sessions#destroy", :as => :destroy_user_session
end

This is a bit dirty. I'm sure that Devise deprecated the GET route for good reason. However, fixing it any other way is beyond my Cucumber knowledge at this point, as every test in that suite ultimately relies on visit('/users/logout') which just isn't possible with the out-of-the-box Devise routes.

UPDATE

You can also fix this by commenting out the following in config/initialers/devise.rb

#config.sign_out_via = :delete
like image 27
cailinanne Avatar answered Nov 17 '22 02:11

cailinanne


Devise 1.4.1 (27 June 2011) changed the default behavior for sign out requests:

https://github.com/plataformatec/devise/commit/adb127bb3e3b334cba903db2c21710e8c41c2b40

Jose Valim explained why: "GET requests should not change the state of the server. When sign out is a GET request, CSRF can be used to sign you out automatically and things that preload links can eventually sign you out by mistake as well."

Cucumber wants to test GET requests not DELETE requests for destroy_user_session_path. If you intend to use Cucumber with Devise, change the Devise default from DELETE to GET for the Rails test environment only with this change to config/initializers/devise.rb:

config.sign_out_via = Rails.env.test? ? :get : :delete

Don't try to tweak the routes.rb file to make the fix. It isn't necessary. If you're not going to use Cucumber, leave Devise's new default (DELETE) in place.

The example source code here:

https://github.com/RailsApps/rails3-devise-rspec-cucumber

now includes the change to the Devise initializer for Cucumber.

The application template here:

https://github.com/RailsApps/rails3-application-templates

now detects the collision between Devise and Cucumber and alters the Devise initializer as needed.

These changes were tested with with Rails 3.1.0.rc4 but the behavior should be the same with Rails 3.0.9. Please add comments here if the issue is unresolved or if you have more information.

like image 8
Daniel Kehoe Avatar answered Nov 17 '22 02:11

Daniel Kehoe