我正在为使用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
中的自定义匹配器或辅助方法中,但在每个控制器的每个规范中调用它也似乎是重复的。关于如何以干燥方式实现这一目标的任何想法?
如果您想知道,Pundit的政策类会单独测试,如this post所示。
答案 0 :(得分:1)
我觉得你可以在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
答案 1 :(得分:1)
下面是一个使用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
答案 2 :(得分:1)
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
答案 3 :(得分:0)
我发布了最新尝试的代码。
请注意:
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