Pundit Matchers

repository·main·Indexed 42 days ago

https://github.com/pundit-community/pundit-matchers

A set of RSpec matchers for simplifying the testing of Pundit authorization policies in Ruby on Rails applications. It provides specialized matchers to verify permitted and forbidden actions (e.g., permit_only_actions, forbid_action), as well as attribute mass assignment permissions including action-specific attribute testing via .for_action(). Requires Ruby 3.1+, Pundit 2+, and RSpec 3.12+.

Tokens
5.5K
Snippets
21
Records
24
Agent score
54%

What's inside Pundit Matchers

  1. How to structure Pundit policy specs

    main

    To use Pundit Matchers effectively, follow these structural patterns in your policy specs (e.g., spec/policies/article_policy_spec.rb):

    1. Declare a Subject: The subject should be a new instance of the policy class, initialized with the user and the record being authorized. subject { described_class.new(user, article) }
    2. Use let for Context: Use let statements to define the user and the record. This allows you to reassign them within different RSpec context blocks to test different authorization states.
    3. Implicit Subject: Matchers like is_expected.to permit_action(:index) implicitly reference the subject defined above.
    require 'rails_helper'
    
    RSpec.describe ArticlePolicy do
      subject { described_class.new(user, article) }
    
      let(:article) { Article.new }
    
      context 'with visitors' do
        let(:user) { nil }
    
        it { is_expected.to permit_only_actions(%i[index show]) }
      end
    
      context 'with administrators' do
        let(:user) { User.new(administrator: true) }
    
        it { is_expected.to permit_all_actions }
      end
    end
  2. Organize large policy specs by context

    main
    To avoid deeply nested context trees in large policy specs, split the specifications into multiple files based on the user context (e.g., visitor_context_spec.rb and administrator_context_spec.rb). This improves readability and allows you to group all expectations for a specific user type in one place.
  3. Best Practice: Use allow lists with `permit_only_actions`

    main

    The recommended testing strategy is to use an explicit allow list via permit_only_actions. This ensures that if a developer adds a new action to the policy, the test will fail unless that action is explicitly added to the allow list. This prevents 'broken access control' by ensuring no unintended actions are permitted.

    Example of testing an allow list for administrators:

    context 'with administrators' do
      let(:user) { User.new(administrator: true) }
    
      # This will fail if a new action like :publish is added to the policy but not included here
      it { is_expected.to permit_only_actions(%i[index show new create edit update]) }
    end
    context 'with administrators' do
      let(:user) { User.new(administrator: true) }
    
      it { is_expected.to permit_only_actions(%i[index show new create edit update]) }
    end
  4. Test resolved scopes in Policy::Scope

    main

    To test that a Policy::Scope correctly includes or excludes records based on user permissions, do not use Pundit Matchers. Instead, manually instantiate the scope class and use standard RSpec inclusion/exclusion expectations.

    Pattern:

    1. Define let(:resolved_scope) by calling described_class::Scope.new(user, Model.all).resolve.
    2. Use expect(resolved_scope).to include(record) or expect(resolved_scope).not_to include(record).
    RSpec.describe ArticlePolicy do
      subject { described_class.new(user, article) }
    
      let(:resolved_scope) do
        described_class::Scope.new(user, Article.all).resolve
      end
    
      let(:user) { nil }
    
      context 'when visitor accesses a published article' do
        let(:article) { Article.create(publish: true) }
    
        it 'includes article in resolved scope' do
          expect(resolved_scope).to include(article)
        end
      end
    end
  5. Configure user aliases for Pundit policies

    main

    Pundit Matchers assumes your policies have a user attribute. If your application uses a different name (e.g., account), you must configure a user alias in spec/spec_helper.rb or spec/rails_helper.rb using Pundit::Matchers.configure.

    You can set a global default or specific aliases for individual policy classes.

    Pundit::Matchers.configure do |config|
      # Change the default user alias
      config.default_user_alias = :account
    
      # Change the user alias for a specific policy
      config.user_aliases = { 'Post' => :account }
    end
  6. Test attributes for specific actions

    main

    If your policy uses permitted_attributes_for_{action} to define different permitted attributes based on the current action, you can use composable matchers to target specific actions.

    Matchers:

    • permit_attribute(:attribute_name).for_action(:action_name)
    • forbid_attribute(:attribute_name).for_action(:action_name)

    Warning: Pundit Matchers does not automatically check if an attribute is permitted by a permitted_attributes_for_{action} method when using general permit_attribute calls. You must explicitly test all implemented permitted_attributes_for_{action} methods to ensure coverage.

    it { is_expected.to permit_attribute(:slug).for_action(:create) }
    it { is_expected.to forbid_attribute(:slug).for_action(:update) }
  7. Test attribute authorization (mass assignment)

    main

    To test if specific attributes are permitted or forbidden via the permitted_attributes method, use the following matchers:

    • permit_attribute(:attribute_name)
    • forbid_attribute(:attribute_name)
    • permit_attributes(...) (for multiple attributes)
    • forbid_attributes(...) (for multiple attributes)

    permit_attributes and forbid_attributes accept attributes as an array, a list of parameters, a symbol array (%i[...]), or a hash.

    Testing Nested Attributes You can test nested attributes using standard nested syntax. There is no limit to the depth of nesting supported.

    it { is_expected.to permit_attribute(:name) }
    # Multiple attributes
    it { is_expected.to permit_attributes(:first_name, :last_name) }
    # Nested attributes
    it 'permits nested attributes for address' do
      is_expected.to(
        permit_attribute(address_attributes: [:country])
      )
    end
  8. Test new/create and edit/update action pairs

    main

    Since new/create and edit/update actions are often logically coupled in authorization policies, Pundit Matchers provides shortcut matchers to test both actions in a single expectation.

    Available shortcut matchers:

    • permit_new_and_create_actions
    • permit_edit_and_update_actions
    • forbid_new_and_create_actions
    • forbid_edit_and_update_actions
    RSpec.describe ArticlePolicy do
      subject { described_class.new(user, article) }
    
      context 'with visitors'
        let(:user) { nil }
        it { is_expected.to forbid_new_and_create_actions }
      end
    
      context 'with administrators'
        let(:user) { User.new(administrator: true) }
        it { is_expected.to permit_new_and_create_actions }
      end
    end
  9. Configure Pundit Matchers user aliases

    main

    Pundit Matchers allows you to define custom user aliases for specific policies. By default, the matcher assumes the user object in your policy is named :user. If your policies use a different name (e.g., :client or :admin), you can configure this globally or per-class.

    Use Pundit::Matchers.configure to set these aliases.

    • default_user_alias: The fallback symbol used if no specific alias is defined for a policy class.
    • user_aliases: A hash mapping policy class names (as Strings) to their specific user alias symbols.
    Pundit::Matchers.configure do |config|
      # Set the global default if it's not :user
      config.default_user_alias = :account
    
      # Map specific policy classes to different user aliases
      config.user_aliases = { 'Post' => :client }
    end
  10. Reference: Forbid Matchers

    main

    The following RSpec matchers test that specific actions or attributes are forbidden by a policy:

    • forbid_only_actions(%i[action1 action2]): Tests that the provided array of actions are the only actions forbidden.
    • forbid_all_actions: Tests that all actions in the policy are forbidden.
    • forbid_action(:action_name): Tests that a specific action is not permitted.
    • forbid_actions(%i[action1 action2]): Tests that an array of actions are not permitted.
    • forbid_new_and_create_actions: Tests that both new and create are not permitted.
    • forbid_edit_and_update_actions: Tests that both edit and update are not permitted.
    • forbid_attribute(:attr): Tests that an attribute is not permitted for mass assignment.
    • forbid_attributes(%i[attr1 attr2]): Tests that a list of attributes are not permitted for mass assignment.
    • forbid_mass_assignment_of: Backward compatibility alias for forbid_attributes.