Rails 教程(第 3 版)第 10 章 (10.54) 中 M.Hartl 的 Ruby 中的密码重置测试失败

Password Reset Test failing in M.Hartl's Ruby on Rails Tutorial (3rd edition), Chapter 10 (10.54)

到现在我的tests/assertions都顺利通过了。我非常有信心应用程序本身运行良好,但我在此测试中没有通过一个断言。我是一名 Rails 菜鸟,但我从其他编程经验中了解到,现在不解决这个问题可能会让小鬼恶化。

出于对 SO 成员时间的尊重(并认识到我的 Rails-新手状态),在问这个问题之前,我已经做了几乎所有可能的故障排除,包括:

  1. 重新启动我的本地 Rails 服务器(多次)。
  2. 在这里查看了有关 Rails 教程(及以后)中测试失败的所有其他问题。
  3. 深入研究 Minitest 文档以了解我遇到的错误。
  4. 用@Mhartl 的 Github 存储库中的代码替换了我的(密码重置)集成测试代码。
  5. 在我的测试中尝试 "Rails.logger.debug" 条消息通过日志消息进行调试。

断言失败消息:

FAIL["test_password_resets", PasswordResetsTest, 2015-07-30 13:42:42 -0400] test_password_resets#PasswordResetsTest (1438278162.33s)
Failed assertion, no message given.
    test/integration/password_resets_test.rb:57:in `block in <class:PasswordResetsTest>'

我的password_resets_test.rb(完整):

require 'test_helper'

class PasswordResetsTest < ActionDispatch::IntegrationTest
  def setup
    ActionMailer::Base.deliveries.clear
    @user = users(:michael)
  end

  test "password resets" do
    get new_password_reset_path
    assert_template 'password_resets/new'
    # Invalid email
    post password_resets_path, password_reset: { email: "" }
    assert_not flash.empty?
    assert_template 'password_resets/new'
    # Valid email
    post password_resets_path, password_reset: { email: @user.email }
    assert_not_equal @user.reset_digest, @user.reload.reset_digest
    assert_equal 1, ActionMailer::Base.deliveries.size
    assert_not flash.empty?
    assert_redirected_to root_url
    # Password reset form
    user = assigns(:user)
    # Wrong email
    get edit_password_reset_path(user.reset_token, email: "")
    assert_redirected_to root_url
    # Inactive user
    user.toggle!(:activated)
    get edit_password_reset_path(user.reset_token, email: user.email)
    assert_redirected_to root_url
    user.toggle!(:activated)
    # Right email, wrong token
    get edit_password_reset_path('wrong token', email: user.email)
    assert_redirected_to root_url
    # Right email, right token
    get edit_password_reset_path(user.reset_token, email: user.email)
    assert_template 'password_resets/edit'
    assert_select "input[name=email][type=hidden][value=?]", user.email
    # Invalid password & confirmation
    patch password_reset_path(user.reset_token),
          email: user.email,
          user: { password:              "foobaz",
                  password_confirmation: "barquux" }
    assert_select 'div#error_explanation'
    # Empty password
    patch password_reset_path(user.reset_token),
          email: user.email,
          user: { password:              "",
                  password_confirmation: "" }
    assert_not flash.empty?
    # Valid password & confirmation
    patch password_reset_path(user.reset_token),
          email: user.email,
          user: { password:              "foobaz",
                  password_confirmation: "foobaz" }
    assert is_logged_in? #<=== FAILING ASSERTION
    assert_not flash.empty?
    assert_redirected_to user
  end
end

第 57 行(失败的断言)是:

assert is_logged_in?

我的相关部分test_helper.rb:

ENV['RAILS_ENV'] ||= 'test'

  # Edited for brevity ...

  # Returns true if a test user is logged in.
  def is_logged_in?
    !session[:user_id].nil?
  end

  # Logs in a test user.
  def log_in_as(user, options = {})
    password    = options[:password]    || 'password'
    remember_me = options[:remember_me] || '1'
    if integration_test?
      post login_path, session: { email:       user.email,
                                  password:    password,
                                  remember_me: remember_me }
    else
      session[:user_id] = user.id
    end
  end

  private
    # Returns true inside an integration test.
    def integration_test?
      defined?(post_via_redirect)
    end
end

这是我的 password_resets_controller.rb:

class PasswordResetsController < ApplicationController
  before_action :get_user,   only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]
  before_action :check_expiration, only: [:edit, :update] # Listing 10.52

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
  end

  def update
    if params[:user][:password].empty? 
      flash.now[:danger] = "Password can't be empty"
      render 'edit'
    elsif @user.update_attributes(user_params)
      log_in @user
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'
    end
  end

  private

    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end

    # Before filters:

    def get_user
      @user = User.find_by(email: params[:email])
    end

    # Confirms a valid user.
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end

    # Checks expiration of reset token.
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end
end

这是我的 user.rb(已编辑):

class User < ActiveRecord::Base
  # Add tokens to class accessor:
  attr_accessor :remember_token, :activation_token, :reset_token

  # Edited for brevity ...

  # Returns true if the given token matches the digest.
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil? # ... implied else here ...
    BCrypt::Password.new(digest).is_password?(token)
  end

  # Edited for brevity ...

  # Sets the password reset attributes.
  def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest,  User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

  # Sends password reset email.
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

  # Returns true if a password reset has expired.
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

 # Edited for brevity ...

end

我的 Gemfile(已编辑):

source 'https://rubygems.org'
ruby '2.2.2'
gem 'rails',        '4.2.2'

# Edited for brevity ...

group :development, :test do
  gem 'sqlite3',     '1.3.9'
  gem 'byebug',      '3.4.0'
  gem 'web-console', '2.0.0.beta3'
  gem 'spring',      '1.1.3'
end

group :test do
  gem 'minitest-reporters', '1.0.5'
  gem 'mini_backtrace',     '0.1.3'
  gem 'guard-minitest',     '2.3.1'
end

# Edited for brevity ...

我从事软件开发已有很长时间了,这听起来像是一个经典的案例,绕着车轴转,试图找到一个微妙的问题,而忽略了一些明显的问题。我绝对知道我在这上面花的时间比合理的要多,而且我预计在此过程中我在我的代码中注入了一些废话。

在此先感谢您的帮助。

我认为这是一个非常简单的错误:

在您的测试中,您在第 53 行提交了密码重置表单来为用户选择一个新密码,但是您选择的新密码 ("foobaz") 只有 6 个字符长:

patch password_reset_path(user.reset_token),
      email: user.email,
      user: { password:              "foobaz",
              password_confirmation: "foobaz" }

但是在user.rb中你规定密码必须至少有8个字符:

validates :password, presence: true, length: { minimum: 8 }, allow_nil: true

这就是密码重置失败的原因。使用更长的密码应该没问题!

要解决这个问题,您可以在失败断言之前添加这一行:

puts html_document

这会将呈现的 HTML 转储到您的终端 window,您会在其中找到...

<div class="alert alert-danger">
  The form contains 1 error.
</div>
<ul>
  <li>Password is too short (minimum is 8 characters)</li>
</ul>