Rails Tutorial 密码重置测试的登录断言失败
结论
从给出的 PasswordResetsController、User 和 is_logged_in? 代码看,没有明显错误。assert is_logged_in? 失败表示测试会话中没有 session[:user_id]。
排查时先确认两件事:
- 提交有效密码后,
@user.update_attributes(user_params)是否返回true。 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
这不太可能是当前登录断言失败的直接原因,但可以避免无效邮箱路径触发其他异常。
排查顺序
可以按以下顺序检查:
- 将
assert_redirected_to user放在assert is_logged_in?前面。 - 如果响应没有重定向,输出
assigns(:user).errors.full_messages,找出并修复导致update_attributes失败的验证。 - 如果重定向正常,检查
SessionsHelper#log_in。 - 确认该方法写入
session[:user_id],同时确认ApplicationController包含SessionsHelper。 - 用
assert_equal user.id, session[:user_id]替换没有说明的布尔断言,以获得更明确的失败信息。
现有代码没有展示完整的模型验证规则和 log_in 定义,因此还无法确定是哪一条验证导致了失败。排查范围可以确定在两处:成功更新分支是否执行,以及 SessionsHelper#log_in 是否写入了正确的 session 键。重启 Rails server、修改 log_in_as 或检查 Minitest 本身无法解决这个问题。
备注:内容仅供参考。
