I've got a pretty simple Pundit policy with a scope for different user roles. I can't figure out how to test it in Rspec. Specifically, I don't know how to tell the scope what user is logged in before accessing the scope.
Here is what I've tried:
let(:records) { policy_scope(Report) }
context 'admin user' do
before(:each) { sign_in(admin_user) }
it { expect(reports.to_a).to match_array([account1_report, account2_report]) }
end
context 'client user' do
before(:each) { sign_in(account2_user) }
it { expect(reports.to_a).to match_array([account2_report]) }
end
When I run Rspec, I get:
NoMethodError: undefined method `sign_in' for #<RSpec::ExampleGroups::ReportPolicy::Scope:0x00007f93241c67b8>
I use sign_in
extensively in controller tests, but I guess that doesn't apply in a Policy test.
The Pundit docs says only:
Pundit does not provide a DSL for testing scopes. Just test it like a regular Ruby class!
So...does anyone have an example of testing a Pundit scope for a specific user? How do I tell the scope what current_user is?
FWIW, here's the essence of my policy:
class ReportPolicy < ApplicationPolicy
def index?
true
end
class Scope < Scope
def resolve
if user.role == 'admin'
scope.all
else
scope.where(account_id: user.account_id)
end
end
end
end
In my controller, I call it as follows. I've confirmed that this works correctly in the real world, with admins seeing all reports, and other users only seeing reports for their account:
reports = policy_scope(Report)
You can instantiate a policy scope with:
Pundit.policy_scope!(user, Report)
Which is short for:
ReportPolicy::Scope.new(user, Report).resolve
Note that you don't need to do any actual steps of signing the user in. user
is just an object that your policy scope takes as an initializer argument. Pundit is after all just plain old OOP.
class ApplicationPolicy
# ...
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
scope.all
end
end
end
As to the actual spec I would write it as:
require 'rails_helper'
require 'pundit/rspec' # optional - but includes some nice matchers for policies
RSpec.describe ReportPolicy, type: :policy do
let(:user) { User.new }
let(:scope) { Pundit.policy_scope!(user, Report) }
# ... setup account1_report etc
describe "Scope" do
context 'client user' do
it 'allows a limited subset' do
expect(scope.to_a).to match_array([account2_report])
end
end
context 'admin user' do
let(:user) { User.new(role: 'admin') }
it 'allows access to all the reports' do
expect(scope.to_a).to match_array([account1_report, account2_report])
end
end
end
end
Avoid constructs such as it { expect ... }
and use it blocks that describe the actual behaviour that you are testing or you will end up with really cryptic failure messages and tests that are hard to understand. The one-liner syntax it { is_expected.to ... }
should only be used to help avoid duplication in situations where the doc string and the matcher used in the example mirror each other exactly.