Testing Pundit Policies
CanCan has been an awesome standby for authorization in many of the projects I’ve been involved with; but I’ve had my eye on Pundit and really appreciate the ‘just plain old Ruby objects’ approach. So on a couple of recent projects I made the jump. So far, it’s been awesome; but that’s a post for another day :-).
One of the first challenges that I ran into was developing a strategy for testing my policies. Both of my projects are using MiniTest, one uses spec via the ‘minitest-spec-rails’ gem and the other is just plain vanilla with the additions that Rails provides. FactoryGirl is used in both cases.
General Structure
In each policy test, the basic structure I’ve begun using is
# MiniTest
require 'test_helper'
class SomethingPolicyTest < ActiveSupport::TestCase
setup do
# Setup needed for all tests... maybe build different record for testing
# against
end
test 'a user can' do
# test the things a user can do
end
test 'a user can not' do
# test the things a user can not do
end
test 'user scope' do
# test the scope of a user
end
test 'an admin can' do
# test the things an admin can do
end
# ...
end
#MiniTest::Spec
require 'test_helper'
class SomethingPolicyTest < ActiveSupport::TestCase
let(:something) { build :something }
let(:another_something) { build :another_something }
describe 'user' do
let(:user) { build :user }
it 'can' do
# test what a user can do
# More on `must_permit` and `wont_permit` later :-)
something.must_permit user, [:new?, :create?, :edit?, :update?]
end
it 'can not' do
# test what a user can't do
something.wont_permit user, :destroy?
end
describe 'scope' do
it 'includes something' do
# test things about the user scope
end
end
end
describe 'admin' do
let(:admin) { build :admin }
# same stuff for an admin or other role
end
end
This has been kind of a nice starting point, but I frequently deviate from this pattern if it makes sense or as the list of things they can or can not do gets longer and more complex.
Custom Assertions
How do we make assertions about a policy? Well, a policy is just a Ruby class… so all we have to do is instantiate the policy and call it’s instance methods. Pundit provides a nice class method .policy
for instantiating a policy. It infers the name of the policy from the class of the given record and then instantiates the policy using the user and record passed in. So in order to test our policy, we could make assertions using the result of calling methods on the policy.
# ...
test 'user can' do
assert Pundit.policy(user, record).show?
end
# or spec
it 'can' do
Pundit.policy(user, record).show?.must_equal true
end
But this isn’t going to give meaningful error messages and contains a lot of boilerplate code. To help make things clearer, lets write custom MiniTest assertions to help test our policy. I’ve defined these in test/helpers/pundit.rb
and then required that file in test_helper.rb
module MiniTest
module Assertions
##
# Fails unless there is a Pundit policy for the user and record and permits
# the action.
# user: instance of User
# record: instance or class of object being authorized
# action: string or symbol of the action being authorized or array of
# strings/ symbols
def assert_permit(user, record, actions)
actions = Array(actions)
actions.each do |action|
msg = "User #{user.inspect} should be permitted to #{action} #{record}, but isn't permitted"
assert Pundit.policy!(user, record).public_send(action), msg
end
end
##
# Same as `assert_permit` except it fails if user IS permitted for the
# record and action
def refute_permit(user, record, actions)
actions = Array(actions)
actions.each do |action|
msg = "User #{user.inspect} should NOT be permitted to #{action} #{record}, but is permitted"
refute Pundit.policy!(user, record).public_send(action), msg
end
end
end
end
This opens up the MiniTest::Assertions module and defines two newly minted assertions. Under the hood it uses Pundit.policy
like we were before. I chose to use the ‘bang’ version, .policy!
, so that if the policy is not yet defined, we get a meaningful error message. Otherwise we would simply get a nil class error. #public_send
is used to dynamically call the given ‘action’ method on the policy. #public_send
is used because I only want to test the public methods available in the policy, not any private helper stuff I might have defined.
We now have a much more descriptive error message for when our assertion fails. I used #.inspect
with user
in the error message so that you can see the attributes of the user when you are debugging a test… although if this is too much info you could play around with what makes most sense for you. Array()
is used to convert whatever we pass into the ‘action’ parameter to an array, if possible; this way we can choose to pass in a single action or an array of actions.
With these new assertions, we can now use them like
test 'a user can' do
assert_permit user, record, :show?
# or
assert_permit user, record, [:new?, :create?, :edit?, :update?]
# or
assert_permit user, Record, :new?
end
and then you would get a helpful error message like:
User #<User id: nil, email: "test558484455@example.com", password_digest: "$2a$04$u0rOr5nwXzYHcydrVJZq.ONO5bKnZj2AoSIjE6Touw/...", role: 2, organization_id: 1> should be permitted to update? #<Something:0x007fc71ca58ff8>, but isn't permitted
But we don’t want to leave the MiniTest:Spec-ers without an expectation to hide these assertions in. Fortunately, it is wonderfully easy to turn an assertion into an expectation. At the bottom of test/helpers/pundit.rb
we’ll add the following
module Minitest
module Expectations
##
# See `assert_permit`
# Use like `record.must_permit user, action`
infect_an_assertion :assert_permit, :must_permit
##
# See `refute_permit`
# Use like `record.wont_permit user, action`
infect_an_assertion :refute_permit, :wont_permit
end
end
and that’s it! We can now use our expectations in our tests:
it 'can' do
something.must_permit user, :show?
# or
something.must_permit user, [:new?, :create?, :edit?, :update?]
# or
Something.must_permit user, :new?
end
Testing Scope
Testing scope is pretty straight forward. Pundit provides .policy_scope
for retrieving records using the scope, so all we have to do is make sure that we only get the records we expect to get and not the others. For example:
describe 'scope' do
attr_accessor :another_thing, :result, :thing
let(:user) { create :user }
before do
@thing = create :thing, user: user
@another_thing = create :another_thing
@result = Pundit.policy_scope!(user, Thing)
end
it 'includes a thing' do
result.must_include thing
end
it 'wont include another thing' do
result.wont_include another_thing
end
end
The End!
This pretty much sums up how I test my Pundit policies. I’ll leave you with a complete copy of my test/helpers/pundit.rb
. Happy testing!
module MiniTest
module Assertions
##
# Fails unless there is a Pundit policy for the user and record and permits
# the action.
# user: instance of User
# record: instance or class of object being authorized
# action: string or symbol of the action being authorized or array of
# strings/ symbols
def assert_permit(user, record, actions)
actions = Array(actions)
actions.each do |action|
msg = "User #{user.inspect} should be permitted to #{action} #{record}, but isn't permitted"
assert Pundit.policy!(user, record).public_send(action), msg
end
end
##
# Same as `assert_permit` except it fails if user IS permitted for the
# record and action
def refute_permit(user, record, actions)
actions = Array(actions)
actions.each do |action|
msg = "User #{user.inspect} should NOT be permitted to #{action} #{record}, but is permitted"
refute Pundit.policy!(user, record).public_send(action), msg
end
end
end
end
module MiniTest
module Expectations
##
# See `assert_permit`
# Use like `record.must_permit user, action`
infect_an_assertion :assert_permit, :must_permit
##
# See `refute_permit`
# Use like `record.wont_permit user, action`
infect_an_assertion :refute_permit, :wont_permit
end
end