PAYATHON 2026

Rails Tutorial 密码重置测试的登录断言失败

排障赵师傅

结论

从给出的 PasswordResetsController、User 和 is_logged_in? 代码看,没有明显错误。assert is_logged_in? 失败表示测试会话中没有 session[:user_id]。

排查时先确认两件事:

  1. 提交有效密码后,@user.update_attributes(user_params) 是否返回 true。
  2. SessionsHelper#log_in 是否执行了 session[:user_id] = user.id。

这次失败与 test_helper.rb 中的 log_in_as 无关。密码重置控制器调用的是 log_in @user,不会调用 log_in_as @user。

正常的执行路径

最后一次 PATCH 应按以下顺序执行:

PATCH /password_resets/:id
  → get_user
  → valid_user
  → check_expiration
  → update
  → @user.update_attributes(...)
  → log_in @user
  → session[:user_id] = @user.id
  → redirect_to @user

如果 update_attributes 失败,后面的 log_in @user 不会执行:

elsif @user.update_attributes(user_params)
  log_in @user
  flash[:success] = "Password has been reset."
  redirect_to @user
else
  render 'edit'
end

因此,登录断言失败可能是用户记录没有成功更新造成的。

先确认请求是否进入成功分支

把响应断言移到登录断言前面:

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

assert_redirected_to user
assert is_logged_in?
assert_not flash.empty?

如果 assert_redirected_to user 也失败,且响应状态为 200,说明控制器执行了:

render 'edit'

此时 update_attributes 返回了 false,需要检查测试环境中的验证错误:

updated_user = assigns(:user)

assert updated_user.errors.empty?,
       updated_user.errors.full_messages.join(', ')

临时调试时也可以这样写:

assert assigns(:user).update_attributes(
  password: "foobaz",
  password_confirmation: "foobaz"
), assigns(:user).errors.full_messages.join(', ')

更合适的方式是在控制器请求完成后检查 assigns(:user).errors,这样不会让测试再次修改记录。

还要核对 User 模型中没有展示的验证规则,例如:

validates :password, length: { minimum: 6 }, allow_nil: true

"foobaz" 有 6 个字符,符合 Rails Tutorial 中常见的规则。如果项目后来增加了数字、大小写或更长的长度要求,这个密码可能无法通过验证。

检查控制器使用的 log_in

控制器调用的 log_in 通常定义在 app/helpers/sessions_helper.rb:

module SessionsHelper
  def log_in(user)
    session[:user_id] = user.id
  end
end

同时确认 ApplicationController 能访问该方法。Rails Tutorial 中常见的配置是:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

session 键名必须完全一致。以下写法都会导致当前测试失败:

session[:userid] = user.id
session[:user] = user.id
session['userid'] = user.id

测试读取的是:

session[:user_id]

登录方法也要写入这个键:

session[:user_id] = user.id

如果控制器根本无法调用 log_in,通常会出现 NoMethodError。当前只有断言失败,更可能是代码没有进入 log_in,或者该方法写入了错误的 session 键。

test_helper.rb 与这次调用无关

下面的方法只会在测试主动调用 log_in_as(user) 时使用:

def log_in_as(user, options = {})
  if integration_test?
    post login_path, session: {
      email: user.email,
      password: password,
      remember_me: remember_me
    }
  else
    session[:user_id] = user.id
  end
end

密码重置成功后,控制器执行的是:

log_in @user

修改 log_in_as 或 integration_test? 无法修复这条失败的断言。

is_logged_in? 的实现没有问题:

def is_logged_in?
  !session[:user_id].nil?
end

也可以直接写成:

def is_logged_in?
  session[:user_id].present?
end

两种写法的效果相同,不会影响问题的原因。

使用更精确的断言

当前测试先检查登录状态,失败时只显示 Failed assertion, no message given,无法看出请求在哪一步中断。可以改成:

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

assert_redirected_to user
assert_equal user.id, session[:user_id],
             "Password reset succeeded, but session[:user_id] was not set"
assert_not flash.empty?

如果要单独识别模型验证失败,可以在重定向断言前加入:

unless response.redirect?
  reset_user = assigns(:user)
  flunk "Password update failed: #{reset_user.errors.full_messages.join(', ')}"
end

完整的测试片段可以写成:

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

unless response.redirect?
  reset_user = assigns(:user)
  flunk "Password update failed: #{reset_user.errors.full_messages.join(', ')}"
end

assert_redirected_to user
assert_equal user.id, session[:user_id]
assert_not flash.empty?

控制器中还需留意的细节

check_expiration 假定 @user 一定存在:

def check_expiration
  if @user.password_reset_expired?
    # ...
  end
end

get_user 找不到用户时,valid_user 会执行重定向。在这类旧版 Rails 代码中,不宜依赖 redirect_to 自动终止后续过滤器。可以显式返回,并让过期检查处理 @user 为 nil 的情况:

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

def check_expiration
  return unless @user

  if @user.password_reset_expired?
    flash[:danger] = "Password reset has expired."
    redirect_to new_password_reset_url
  end
end

这不太可能是当前登录断言失败的直接原因,但可以避免无效邮箱路径触发其他异常。

排查顺序

可以按以下顺序检查:

  1. 将 assert_redirected_to user 放在 assert is_logged_in? 前面。
  2. 如果响应没有重定向,输出 assigns(:user).errors.full_messages,找出并修复导致 update_attributes 失败的验证。
  3. 如果重定向正常,检查 SessionsHelper#log_in。
  4. 确认该方法写入 session[:user_id],同时确认 ApplicationController 包含 SessionsHelper。
  5. 用 assert_equal user.id, session[:user_id] 替换没有说明的布尔断言,以获得更明确的失败信息。

现有代码没有展示完整的模型验证规则和 log_in 定义,因此还无法确定是哪一条验证导致了失败。排查范围可以确定在两处:成功更新分支是否执行,以及 SessionsHelper#log_in 是否写入了正确的 session 键。重启 Rails server、修改 log_in_as 或检查 Minitest 本身无法解决这个问题。

备注:内容仅供参考。