有没有办法在请求规范中模拟 Pundit 策略授权?

Is there any way to mock Pundit policy authorize in request specs?

我正在使用 Pundit 对我的 Rails 应用程序进行授权,并且正在对我的请求进行单元测试。 我已经成功测试了该策略,但现在我想验证我的请求是否正在使用该策略。 我想以一种通用的方式来做,我可以在任何请求规范中使用(无论控制器、操作和策略如何)。 老实说,在这一点上,我会遵循一种对所有请求通用的说法 "expect any policy to receive authorize"。

对于索引操作(使用策略范围)很简单:

在请求描述中我说

RSpec.describe 'GET /companies', type: :request do
  include_context 'invokes policy scope'
end

然后我将共享上下文定义为:

RSpec.shared_context 'invokes policy scope', shared_context: :metadata do
  before do
    expect(Pundit).to receive(:policy_scope!) do |user_context, relation|
      expect(user_context.user).to eq(user)
      expect(user_context.current_group).to eq(group)

      relation
    end
  end
end

但是我如何为授权方法做同样的事情。我不知道哪个具体策略会接收它,也不知道哪个具体控制器会调用 authorize

我不明白专家为什么不提供自定义匹配器来验证某个请求是否确实调用了给定策略,或者 "simulate" authorized/unauthorized 场景,所以我测试我的请求 returns正确的状态码。

有什么想法吗?

首先,您需要单独测试您的 pundit 政策。 其次,要通过 pundit 政策,您可以存根满足 pundit 政策

的对象

那么,我有几点意见...

期望(expect 语句)应该在示例 (it) 块内,而不是在 before 块内。 before 块中的内容是允许语句(例如,allow(ClassX).to receive(:method) { object })、无法在测试变量声明中完成的数据修改或请求触发器。有关示例,请参阅 http://www.betterspecs.org/。 TL;DR,共享上下文不是合适的测试方法。

测试 policy_scope 是否被特定参数调用的方法是:

# You can put something generic like this in a shared context and then
# define 'params' and 'scoped_result' as let vars in the specs that include
# the shared context 
let(:request)       { get '/companies' }
let(:params)        { user_context or whatever }
let(:scoped_result) { relation }
# By using abstract variable names here, we make this reusable

it 'calls policy scope' do
  expect(Pundit).to receive(:policy_scope!).with(params)
  request
end

it 'scopes result' do 
  expect(Pundit.policy_scope!(params)).to eq(scoped_result) 
end

要模拟它并存根它的响应,您可以这样做:

before do
  # This ensures Pundit.policy_scope!(context) always returns scoped_result
  allow(Pundit).to receive(:policy_scope!).with(context) { scoped_result }
end

...但是 这些是非常 bad/brittle 测试 ,尤其是因为它与请求规范有关。您的 Pundit 策略应该已经在策略规范文件中进行了测试(参见 https://github.com/varvet/pundit#rspec),因此 您真正想要做的是测试您的端点 returns 正确的输出(范围响应) 给定特定输入(经过身份验证的策略管理对象)。尝试通过模拟响应来覆盖 Pundit 的功能是一个坏主意,因为如果您对策略代码进行重大更改,端点的规范将继续通过。您在这里要做的是设置测试变量以适应导致成功请求的情况,但要确保所有内容都是通用的,以便可以重复使用。对于请求规范,您可以执行以下操作:

# Shared context stuff
let(:json)    { JSON.parse(response.body) }
let(:headers) { ...define the headers to use across requests...}

before { request }

shared_examples_for 'success' do
  it { expect(response).to have_http_status(:success) }
  it { expect(json).to eq(expected) } # or something
end


# User spec that includes shared context
include_context 'above stuff'

let(:request) { get '/companies', params: params, headers: headers }
let(:params)  { { user_id: user.id } } # or something
let!(:admin_thing) { 
  ...something that should be excluded by the pundit policy used by endpoint... 
}

context 'restricted' do
  let!(:user)    { create :user, :restricted }
  let(:expected) { ...stuff scoped to restricted user... }

  it_behaves_like 'success'
end

context 'manager' do
  let!(:user)    { create :user, :manager }
  let(:expected) { ...stuff scoped to manager user... }

  it_behaves_like 'success'
end

context 'superuser' do
  let!(:user)   { create :user, :superuser }
  let(expected) { ...unscoped stuff visible to super user... }

  it_behaves_like 'success'
end

请注意,在更高级别(共享上下文),名称和功能是通用的。在较低级别(声明许可用户的规范),规范将抽象名称转换为特定于被测试端点的值。该规范还创建了一个 不应 由策略范围返回的附加对象(通过确认该对象已从结果中排除来实质上测试范围)。希望对您有所帮助。

当您在控制器中包含 Pundit 模块时,policy_scopeautherize 方法将在您的控制器中作为 public 方法可用。

因此,当您向控制器发送 getpost 请求时,rails 在后台创建了一个控制器实例 ControllerClass.new 因此您需要的是在实例化的控制器对象上模拟 authorize 方法。

模拟你需要知道的那个对象的方法,或者在你的测试中有那个对象,这是不可能的。但希望您可以提前在任何 class 实例上模拟 public 方法。

所以要模拟 authorize 方法,您将编写:

expect_any_instance_of(described_class).to receive(:authorize).with(any_params_you_want).and_return(nil)

expect_any_instance_of是rspec提供的mock某个class的任意实例化对象的方法。 click here to learn more

所以不再需要在测试中引用 Pundit class。实际上,这会在我们的测试中创建对 gem 的 class 名称的依赖性,您不需要它,因为您可以按上述说明测试这两种方法。