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:你想要的改变被拒绝了