You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用RSpec测试带JWT认证的Ruby on Rails API控制器?

问题背景

我使用rspec-rails对Ruby on Rails(RoR)的API控制器进行单元测试,所有控制器均继承自V1::ProtectedController,该类通过拦截请求并验证Authorization请求头实现JWT认证,核心逻辑如下:

class V1::ProtectedController < V1::BaseController
  include JsonWebTokenAuthentication

  attr_reader :current_user
  before_action :validate_token!
  after_action :verify_authorized

  private

  def validate_token!
    authorization_header = request.headers['Authorization'] || ''
    puts "header: #{request.headers['Authorization']}"
    jwt = JsonWebToken.new(authorization_header.gsub('Bearer ', ''))
    if payload = jwt.verify
      account = Account.find_by(id: payload[:user][:id])
      if account.nil? || account.token_checksum == payload[:user][:token_checksum]
        @current_user = Account.from_jwt_data(payload[:user])
      end
    end
    return if @current_user

    raise JsonWebTokenAuthentication::Unauthorized, 'invalid token'
  end
end

为模拟认证流程,我编写了AuthHelper模块获取管理员JWT令牌:

module AuthHelper
  include Rack::Test::Methods

  def app
    Rails.application
  end
  def admin_login
    params = {
      grant_type: 'password',
      email: 'admin@arime.com',
      password: 'preci666'
    }
    post '/api/v1/auth/token', params
    expect(last_response.status).to eq(200)

    auth_response = JSON.parse(last_response.body)
    @access_token = auth_response['access_token']
  end
end

但在Workers控制器测试中,携带令牌的请求始终返回401状态码:

  • 调试发现ProtectedController中request.headers['Authorization']为空,但request.inspect能看到该请求头
  • 尝试直接赋值request.headers['Authorization'] = auth时,触发错误:
1) V1::WorkersController GET #index assigns @workers
 Failure/Error: request.headers['Authorization'] = auth
 
 ArgumentError:
   wrong number of arguments (given 0, expected 1..2)

环境版本:Ruby 2.7.7、Rails 7.0.4.2、rspec-rails 6.0.1


解决方案

方案一:修复请求头传递问题

在Rails 7+的RSpec控制器测试中,request对象的headers操作方式发生变化,不能直接使用request.headers['key'] = value,需改用以下方式:

request.headers.merge!('Authorization' => "Bearer #{@access_token}")

完整测试用例示例:

RSpec.describe V1::WorkersController, type: :controller do
  include AuthHelper

  before do
    admin_login
    request.headers.merge!('Authorization' => "Bearer #{@access_token}")
  end

  describe 'GET #index' do
    it 'assigns @workers' do
      get :index
      expect(response).to have_http_status(:ok)
      # 添加其他业务断言
    end
  end
end

如果仍出现request.headers['Authorization']为空的情况,可直接操作请求env:

request.env['HTTP_AUTHORIZATION'] = "Bearer #{@access_token}"

方案二:更优的模拟认证方案(绕过真实JWT验证)

直接在测试中模拟current_user,跳过真实的JWT解析与验证流程,提升测试速度与稳定性:

  1. 修改ProtectedController,添加测试环境的跳过逻辑:
class V1::ProtectedController < V1::BaseController
  # 原有代码不变

  private

  def validate_token!
    # 测试环境下直接使用预设的current_user
    return if Rails.env.test? && @current_user.present?

    # 原有JWT验证逻辑...
  end
end
  1. 更新AuthHelper,添加模拟登录方法:
module AuthHelper
  def sign_in_as_admin
    # 优先查找已有管理员,无则创建(假设使用FactoryBot生成测试数据)
    @current_user = Account.find_by(email: 'admin@arime.com') || create(:admin_account)
    controller.instance_variable_set(:@current_user, @current_user)
  end
end
  1. 在测试用例中使用模拟登录:
RSpec.describe V1::WorkersController, type: :controller do
  include AuthHelper

  before do
    sign_in_as_admin
  end

  describe 'GET #index' do
    it 'assigns @workers' do
      get :index
      expect(response).to have_http_status(:ok)
      # 添加其他业务断言
    end
  end
end

这种方式无需调用真实认证接口,也不用处理JWT传递问题,测试效率更高,同时避免了对外部认证服务的依赖。


内容的提问来源于stack exchange,提问作者Safouane Jelassi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 22:05:19