ruby-on-railsrubyrspecscopepundit

How to test Pundit Scopes in Rspec?


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)

Solution

  • 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.