Rails 3.0.9 + Devise + Cucumber + Capybara 臭名昭著的“没有路线匹配 /users/sign_out”

Posted

技术标签:

【中文标题】Rails 3.0.9 + Devise + Cucumber + Capybara 臭名昭著的“没有路线匹配 /users/sign_out”【英文标题】:Rails 3.0.9 + Devise + Cucumber + Capybara the infamous "No route matches /users/sign_out" 【发布时间】:2011-09-28 14:40:52 【问题描述】:

我正在使用带有 rails 3.0.9、cucumber-rails 1.0.2、capybara 1.0.0 的 devise 1.4.2。单击注销时出现No route matches "/users/sign_out" 错误。在经历了这个问题(no-route-matches-users-sign-out-devise-rails-3)之后,我将:method => :delete 添加到了link_to 标签。

由于我用 jquery 替换了原型,我也不得不改变

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

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

绕过 rails.js not found 错误。

虽然通过上述更改,我能够成功退出并重定向到 root,但当我在 FireBug 中查看 localhost:3000/users/sign_out 请求的响应时,它会显示相同的路由错误消息 click here to see the screenshot with notes

在通过设计成功实现 Rails 3 应用程序的身份验证后,当我按照本教程 (github.com/RailsApps/rails3-devise-rspec-cucumber/wiki/Tutorial) 使用 Cucumber + Capybara + RSpec 添加功能和规格时,出现以下错误

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  

使用以下 step_definition 表示“我退出”

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

我搜索了很多,发现这是因为 Rails 3 中的不显眼的 javascript 被用于“数据方法”属性,但我还在某处读到 Capybara 确实检查数据方法属性并做出相应的行为。但这对我不起作用,所以在Capybara attack: rack-test, lost sessions and http request methods这个帖子之后,我将我的步骤定义更改为:

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

但我有未定义的方法 process 用于 Capybara::RackTest::Driver (NoMethodError)

根据这个线索,我将上述步骤定义更改如下:

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

这至少通过了“我退出”这一步,但退出后并没有重定向到首页,下一步失败:

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"'

在这一切之后,我不得不求助于在路由文件中添加 'GET' 方法来注销:

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

修改了我的观点

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

<%= link_to "Logout", logout_path %>

并将我的步骤定义更改为:

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

这显然解决了所有问题,所有测试都通过了,firebug 在sign_out 上没有显示任何错误。但我知道使用“获取”请求来销毁会话不是一个好习惯,因为它是一种改变状态的行为。

这可能是由于我使用的特定版本或 Rails、Devise、Cucumber-Rails 或 Capybara 造成的吗?我想使用 Devise 的默认 sign_out 路由,而不是用 get 方法覆盖它,并且能够使用 Cucumber 和 RSpec 进行 BDD。我是使用 Cucumber+Capybara 的新手,是否存在另一种发送 POST 请求的方法,而不是使用仅使用 GET 方法的“visit('/users/sign_out')”?

【问题讨论】:

【参考方案1】:

所以我发现了

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

rails helper 生成以下 html

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

jquery_ujs.js 有以下方法将带有 data-method="delete" 属性的链接转换为表单并在运行时提交:

// 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();

Capybara helper visit('/users/sign_out') 只需单击链接并向没有此请求的任何路由的服务器发送 GET 请求。

与 link_to 帮助器相反,button_to 帮助器在页面呈现时在 html 中添加所需的表单,而不是依赖于 javascript:

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

生成以下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>

有了这个,我可以在“我退出”步骤定义中轻松使用 Capybara 助手 click_button('Logout')。

"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..."

Max Will explained 右键单击​​并在新选项卡中使用非获取数据方法打开 link_to 链接会导致链接断开。

更多有用的关于':method => :delete' 和水豚问题的link_to helper 讨论可以在on this link

现在我会坚持使用没有 :method 属性的简单 link_to 助手,如果我想切换到非 get 方法进行删除,我更喜欢使用 button_to。

同时我认为应该有一个等效于 Visit 的 capybara helper 来满足 data-method 属性发送 post 请求,这样就可以避免使用基于 javascript 的驱动程序进行集成测试。或者可能已经有一个我不知道的。如果我错了,请纠正我。

【讨论】:

感谢您抽出宝贵时间调查和记录此问题。 您对我如何改进github.com/RailsApps/rails3-devise-rspec-cucumber/wiki/Tutorial 的教程有什么建议吗? 我认为重要的是要注意这里的基本问题与 jQuery 与 Prototype 或 Cumcumber/Capybara 无关。 rails3-devise-rspec-cucumber Start App 在“开箱即用”状态下被破坏。它被破坏了,因为指向 Sign Out 的链接是使用 link_to 'Logout' destroy_user_session_path 呈现的,这不适用于 Rails 3.1rc4 和 Devise 1.4.2 提供的路由。这适用于 Rails 3.0.6 和 Devise 1.3.3,因为 destroy_user_session_path 确实响应了 GET 请求。我认为首先要纠正这个问题。 你是完全正确的 Zeeshan,我认为你已经按照信息路径找到了这里的“正确”解决方案。 is 实际上类似于 Capybara 帮助器,但它内置在 cucumber-rails 而不是 Capybara 中——提供了一个 emulation mode(默认情况下启用)以避免使用慢速 javascript 驱动程序删除链接的常见情况(通常由 Rails 脚手架生成)。诀窍是您必须使用click_link 而不是visit 上面的空间不足,但只是想补充一点,虽然 cucumber-rails 对click_link 所做的有点隐藏的 JS 仿真是 Daniel 的教程中绿色特性的最快方法,而无需回避出于充分的理由对 Devise 进行的更改,我认为 button_to 最终是处理它的正确方法,因为您在原始问题中表达了关于使用 POST 更改状态的担忧。【参考方案2】:

纠正这个问题的最简单方法(尽管可能不是最正确的方法)是修改你的路由文件以匹配应用程序的其余部分。例如。使 destroy_user_session_path 的 GET 版本工作。您可以通过如下修改路由文件来做到这一点

删除:

devise_for :users

添加:

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

这有点脏。我确信 Devise 有充分的理由弃用了 GET 路线。但是,此时以任何其他方式修复它超出了我的 Cucumber 知识,因为该套件中的每个测试最终都依赖于访问('/users/logout'),而开箱即用的设计是不可能的路线。

更新

您也可以通过在 config/initialers/devise.rb 中注释掉以下内容来解决此问题

#config.sign_out_via = :delete

【讨论】:

很好的答案。我在找什么【参考方案3】:

Devise 1.4.1(2011 年 6 月 27 日)更改了注销请求的默认行为:

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

Jose Valim 解释了原因:“GET 请求不应该改变服务器的状态。当注销是 GET 请求时,CSRF 可用于自动注销您,并且预加载链接最终可能会错误地将您注销为好吧。”

Cucumber 想要测试 GET 请求而不是 DELETE 请求,用于 destroy_user_session_path。如果您打算将 Cucumber 与 Devise 一起使用,请将 Rails 测试环境的 Devise 默认值从 DELETE 更改为 GET,并将此更改为 config/initializers/devise.rb

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

不要试图调整 routes.rb 文件来进行修复。这是没有必要的。如果您不打算使用 Cucumber,请保留 Devise 的新默认设置 (DELETE)。

示例源码在这里:

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

现在包括对 Cucumber 的 Devise 初始化程序的更改。

这里是应用模板:

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

现在检测 Devise 和 Cucumber 之间的冲突,并根据需要更改 Devise 初始化程序。

这些更改已使用 Rails 3.1.0.rc4 进行了测试,但行为应该与 Rails 3.0.9 相同。如果问题未解决或您有更多信息,请在此处添加 cmets。

【讨论】:

【参考方案4】:

在设计的 wiki 页面中解释了解决此问题的正确方法:https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara

基本上,一旦你包含在你的 user_step.rb 文件中:

include Warden::Test::Helpers
Warden.test_mode!

您可以将visit '/users/sign_out' 替换为logout(:user)

【讨论】:

这是正确的答案。可悲的是,它很难超越投票率更高的解决方法。【参考方案5】:

我实际上遇到了同样的问题,但使用的是 Rails/Sinatra 应用程序。我已经为 Rails 设置了设计,并且注销工作正常。我有一个在 lib 中运行的 GuestApp Sinatra 应用程序,除了注销链接外,它运行良好。我正在尝试在 sinatra 注销链接上强制 data-method="delete" ,但我所做的任何事情都不会使请求成为删除请求。

我认为这对我来说可能是一个 sinatra 问题,但我认为任何传入的请求都会首先由 rails 路由处理,直到它们到达我的 sinatra 路由。我即将手动添加注销的 GET 路由,但我宁愿不必这样做。

这是我的设计路线:

devise_for :member, :path => '', :path_names => 
  :sign_in => "login",
  :sign_out => "logout",
  :sign_up => "register" 

这是我的链接:

%a:href => '/logout', :"data-method" => 'delete', :rel => 'nofollow'Log Out
<a href="/logout" data-method="delete" rel="nofollow">Log Out</a>

#- realized it should be method instead, but still not reaching routes.rb as delete
<a href="/logout" method="delete" rel="nofollow">Log Out</a>

【讨论】:

确保在 Sinatra 正在呈现的页面上包含 rails.js 不显眼的 Javascript 助手。您确实希望呈现的 HTML 中的属性是 data-method 而不是 method,但 Rails JS 帮助程序会查找此数据属性并在发送请求之前对其进行转换。 编辑: 如果您好奇的话,可以使用 is here 的代码(假设您使用的是 jQuery)。【参考方案6】:

当我需要在 test.env 中使用类似的东西时:

  visit destroy_user_session_path

这对我有用,但也许这不对)

config/init/devise.rb

  # The default HTTP method used to sign out a resource. Default is :delete.
  if Rails.env.test?
    config.sign_out_via = :get
  else
    config.sign_out_via = :delete
  end

【讨论】:

以上是关于Rails 3.0.9 + Devise + Cucumber + Capybara 臭名昭著的“没有路线匹配 /users/sign_out”的主要内容,如果未能解决你的问题,请参考以下文章

rails - 设计 - 处理 - devise_error_messages

无法在 Rails 中使用 Devise 销毁会话 [重复]

Heroku/Rails/Devise:你想要的改变被拒绝了

Rails & Devise:如何在没有布局的情况下呈现登录页面?

Rails 和 Devise 的强大参数

Rails 4、Devise 和 Mandrill 电子邮件