Rails 测试:确保在所有控制器和操作中强制执行授权 (Pundit)
Rails testing: ensure authorization (Pundit) is enforced in all controllers and actions
我正在为使用 Pundit 进行授权的 Rails 4.2 应用程序编写 RSpec 测试。
我想测试是否在所有控制器的所有操作中强制执行授权,以避免在开发人员忘记调用 policy_scope
(在 #index
动作)和 authorize
(在所有其他动作上)。
一种可能的解决方案是在所有控制器单元测试中模拟这些方法。类似于 expect(controller).to receive(:authorize).and_return(true)
和 expect(controller).to receive(:policy_scope).and_call_original
。但是,这会导致大量代码重复。此行可以放在 spec/support
中的自定义匹配器或辅助方法中,但在每个控制器的每个规范中调用它似乎也是重复的。关于如何以 DRY 方式实现此目标的任何想法?
如果您想知道,Pundit 的政策 类 是单独测试的,如 this post 所示。
我觉得你可以在 spec_helper 中使用类似的东西。请注意,我假设一个命名约定,其中索引级别答案中包含单词 "index",因此您的规范可能如下所示:
describe MyNewFeaturesController, :type => :controller do
describe "index" do
# all of the index tests under here have policy_scope applied
end
# and these other tests have authorize applied
describe 'show' do
end
describe 'destroy' do
end
end
这里是整体配置:
RSpec.configure do |config|
config.before(:each, :type => :controller) do |spec|
# if the spec description has "index" in the name, then use policy-level authorization
if spec.metadata[:full_description] =~ /\bindex\b/
expect(controller).to receive(:policy_scope).and_call_original
else
expect(controller).to receive(:authorize).and_call_original
end
end
end
这是一个使用 shared_examples、before :suite 挂钩和元编程的示例,可能会满足您的需要。
RSpec.configure do |config|
config.before(:suite, :type => :controller) do |spec|
it_should_behave_like("authorized_controller")
end
end
及 spec_helper
shared_examples_for "authorized_controller" do
# expects controller to define index_params, create_params, etc
describe "uses pundit" do
HTTP_VERB = {
:create => :post, :update=>:put, :destroy=>:delete
}
%i{ new create show edit index update destroy}.each do |action|
if controller.responds_to action
it "for #{action}" do
expect(controller).to receive(:policy_scope) if :action == :index
expect(controller).to receive(:authorize) unless :action == :index
send (HTTP_VERB[action]||:get), action
end
end
end
end
end
Pundit 已经提供了一种机制来保证开发人员在执行控制器操作期间不会忘记授权:
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
end
如果未执行身份验证,这会指示 Pundit raise
。只要您的所有控制器都经过测试,这将导致规范失败。
https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
我正在发布我最近一次尝试的代码。
请注意:
- 您可能不应该使用此代码,因为它感觉过于复杂和 hacky。
- 发生异常后调用
authorize
或policy_scope
无效。如果测试的操作调用 Active Record 方法,例如 find
、update
和 destroy
,但没有提供有效参数,则会发生异常。以下代码创建具有空值的假参数。空 ID 无效,将导致 ActiveRecord::RecordNotFound
异常。找到解决方案后将更新代码。
spec/controllers/all_controllers_spec.rb
# Test all descendants of this base controller controller
BASE_CONTROLLER = ApplicationController
# To exclude specific actions:
# "TasksController" => [:create, :new, :index]
# "API::V1::PostsController" => [:index]
#
# To exclude entire controllers:
# "TasksController" => nil
# "API::V1::PostsController" => nil
EXCLUDED = {
'TasksController' => nil
}
def expected_auth_method(action)
action == 'index' ? :policy_scope : :authorize
end
def create_fake_params(route)
# Params with non-nil values are required to "No route matches..." error
route.parts.map { |param| [param, ''] }.to_h
end
def extract_action(route)
route.defaults[:action]
end
def extract_http_method(route)
route.constraints[:request_method].to_s.delete("^A-Z")
end
def skip_controller?(controller)
EXCLUDED.key?(controller.name) && EXCLUDED[controller.name].nil?
end
def skip_action?(controller, action)
EXCLUDED.key?(controller.name) &&
EXCLUDED[controller.name].include?(action.to_sym)
end
def testable_controllers
Rails.application.eager_load!
BASE_CONTROLLER.descendants.reject {|controller| skip_controller?(controller)}
end
def testable_routes(controller)
Rails.application.routes.set.select do |route|
route.defaults[:controller] == controller.controller_path &&
!skip_action?(controller, extract_action(route))
end
end
# Do NOT name the loop variable "controller" or it will override the
# "controller" object available within RSpec controller specs.
testable_controllers.each do |tested_controller|
RSpec.describe tested_controller, :focus, type: :controller do
# login_user is implemented in spec/support/controller_macros.rb
login_user
testable_routes(tested_controller).each do |route|
action = extract_action(route)
http_method = extract_http_method(route)
describe "#{http_method} ##{action}" do
it 'enforces authorization' do
expect(controller).to receive(expected_auth_method(action)).and_return(true)
begin
process(action, http_method, create_fake_params(route))
rescue ActiveRecord::RecordNotFound
end
end
end
end
end
end
我正在为使用 Pundit 进行授权的 Rails 4.2 应用程序编写 RSpec 测试。
我想测试是否在所有控制器的所有操作中强制执行授权,以避免在开发人员忘记调用 policy_scope
(在 #index
动作)和 authorize
(在所有其他动作上)。
一种可能的解决方案是在所有控制器单元测试中模拟这些方法。类似于 expect(controller).to receive(:authorize).and_return(true)
和 expect(controller).to receive(:policy_scope).and_call_original
。但是,这会导致大量代码重复。此行可以放在 spec/support
中的自定义匹配器或辅助方法中,但在每个控制器的每个规范中调用它似乎也是重复的。关于如何以 DRY 方式实现此目标的任何想法?
如果您想知道,Pundit 的政策 类 是单独测试的,如 this post 所示。
我觉得你可以在 spec_helper 中使用类似的东西。请注意,我假设一个命名约定,其中索引级别答案中包含单词 "index",因此您的规范可能如下所示:
describe MyNewFeaturesController, :type => :controller do
describe "index" do
# all of the index tests under here have policy_scope applied
end
# and these other tests have authorize applied
describe 'show' do
end
describe 'destroy' do
end
end
这里是整体配置:
RSpec.configure do |config|
config.before(:each, :type => :controller) do |spec|
# if the spec description has "index" in the name, then use policy-level authorization
if spec.metadata[:full_description] =~ /\bindex\b/
expect(controller).to receive(:policy_scope).and_call_original
else
expect(controller).to receive(:authorize).and_call_original
end
end
end
这是一个使用 shared_examples、before :suite 挂钩和元编程的示例,可能会满足您的需要。
RSpec.configure do |config|
config.before(:suite, :type => :controller) do |spec|
it_should_behave_like("authorized_controller")
end
end
及 spec_helper
shared_examples_for "authorized_controller" do
# expects controller to define index_params, create_params, etc
describe "uses pundit" do
HTTP_VERB = {
:create => :post, :update=>:put, :destroy=>:delete
}
%i{ new create show edit index update destroy}.each do |action|
if controller.responds_to action
it "for #{action}" do
expect(controller).to receive(:policy_scope) if :action == :index
expect(controller).to receive(:authorize) unless :action == :index
send (HTTP_VERB[action]||:get), action
end
end
end
end
end
Pundit 已经提供了一种机制来保证开发人员在执行控制器操作期间不会忘记授权:
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
end
如果未执行身份验证,这会指示 Pundit raise
。只要您的所有控制器都经过测试,这将导致规范失败。
https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
我正在发布我最近一次尝试的代码。
请注意:
- 您可能不应该使用此代码,因为它感觉过于复杂和 hacky。
- 发生异常后调用
authorize
或policy_scope
无效。如果测试的操作调用 Active Record 方法,例如find
、update
和destroy
,但没有提供有效参数,则会发生异常。以下代码创建具有空值的假参数。空 ID 无效,将导致ActiveRecord::RecordNotFound
异常。找到解决方案后将更新代码。
spec/controllers/all_controllers_spec.rb
# Test all descendants of this base controller controller
BASE_CONTROLLER = ApplicationController
# To exclude specific actions:
# "TasksController" => [:create, :new, :index]
# "API::V1::PostsController" => [:index]
#
# To exclude entire controllers:
# "TasksController" => nil
# "API::V1::PostsController" => nil
EXCLUDED = {
'TasksController' => nil
}
def expected_auth_method(action)
action == 'index' ? :policy_scope : :authorize
end
def create_fake_params(route)
# Params with non-nil values are required to "No route matches..." error
route.parts.map { |param| [param, ''] }.to_h
end
def extract_action(route)
route.defaults[:action]
end
def extract_http_method(route)
route.constraints[:request_method].to_s.delete("^A-Z")
end
def skip_controller?(controller)
EXCLUDED.key?(controller.name) && EXCLUDED[controller.name].nil?
end
def skip_action?(controller, action)
EXCLUDED.key?(controller.name) &&
EXCLUDED[controller.name].include?(action.to_sym)
end
def testable_controllers
Rails.application.eager_load!
BASE_CONTROLLER.descendants.reject {|controller| skip_controller?(controller)}
end
def testable_routes(controller)
Rails.application.routes.set.select do |route|
route.defaults[:controller] == controller.controller_path &&
!skip_action?(controller, extract_action(route))
end
end
# Do NOT name the loop variable "controller" or it will override the
# "controller" object available within RSpec controller specs.
testable_controllers.each do |tested_controller|
RSpec.describe tested_controller, :focus, type: :controller do
# login_user is implemented in spec/support/controller_macros.rb
login_user
testable_routes(tested_controller).each do |route|
action = extract_action(route)
http_method = extract_http_method(route)
describe "#{http_method} ##{action}" do
it 'enforces authorization' do
expect(controller).to receive(expected_auth_method(action)).and_return(true)
begin
process(action, http_method, create_fake_params(route))
rescue ActiveRecord::RecordNotFound
end
end
end
end
end
end