Back to Repositories

Testing Authorization Policy Resolution in Pundit

This test suite thoroughly validates the Pundit authorization gem’s core functionality, including policy resolution, scope handling, and authorization checks. It covers both standard and edge cases for policy enforcement in Ruby applications.

Test Coverage Overview

The test suite provides comprehensive coverage of Pundit’s authorization mechanisms, including policy inference, scope resolution, and error handling.

Key areas tested include:
  • Policy class resolution and instantiation
  • Authorization checks with different record types
  • Policy scope functionality
  • Namespace handling
  • Custom policy class overrides

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with extensive use of let blocks for test setup.

Notable implementation patterns include:
  • Nested describe blocks for logical grouping
  • Shared context setup
  • Exception testing with raise_error matcher
  • Mock objects with double

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • Mock objects for user simulation
  • Custom error classes for authorization failures
  • Nested policy class hierarchies
  • Policy scope implementations

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through comprehensive coverage and maintainable structure.

Notable practices include:
  • Isolated test cases
  • Clear test descriptions
  • Edge case coverage
  • Consistent naming conventions
  • Thorough error scenario testing

varvet/pundit

spec/pundit_spec.rb

            
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Pundit do
  let(:user) { double }
  let(:post) { Post.new(user) }
  let(:customer_post) { Customer::Post.new(user) }
  let(:post_four_five_six) { PostFourFiveSix.new(user) }
  let(:comment) { Comment.new }
  let(:comment_four_five_six) { CommentFourFiveSix.new }
  let(:article) { Article.new }
  let(:artificial_blog) { ArtificialBlog.new }
  let(:article_tag) { ArticleTag.new }
  let(:comments_relation) { CommentsRelation.new(empty: false) }
  let(:empty_comments_relation) { CommentsRelation.new(empty: true) }
  let(:tag_four_five_six) { ProjectOneTwoThree::TagFourFiveSix.new(user) }
  let(:avatar_four_five_six) { ProjectOneTwoThree::AvatarFourFiveSix.new }
  let(:wiki) { Wiki.new }

  describe ".authorize" do
    it "infers the policy and authorizes based on it" do
      expect(Pundit.authorize(user, post, :update?)).to be_truthy
    end

    it "returns the record on successful authorization" do
      expect(Pundit.authorize(user, post, :update?)).to eq(post)
    end

    it "returns the record when passed record with namespace " do
      expect(Pundit.authorize(user, [:project, comment], :update?)).to eq(comment)
    end

    it "returns the record when passed record with nested namespace " do
      expect(Pundit.authorize(user, [:project, :admin, comment], :update?)).to eq(comment)
    end

    it "returns the policy name symbol when passed record with headless policy" do
      expect(Pundit.authorize(user, :publication, :create?)).to eq(:publication)
    end

    it "returns the class when passed record not a particular instance" do
      expect(Pundit.authorize(user, Post, :show?)).to eq(Post)
    end

    it "works with anonymous class policies" do
      expect(Pundit.authorize(user, article_tag, :show?)).to be_truthy
      expect { Pundit.authorize(user, article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
    end

    it "raises an error with the policy, query and record" do
      # rubocop:disable Style/MultilineBlockChain
      expect do
        Pundit.authorize(user, post, :destroy?)
      end.to raise_error(Pundit::NotAuthorizedError, "not allowed to PostPolicy#destroy? this Post") do |error|
        expect(error.query).to eq :destroy?
        expect(error.record).to eq post
        expect(error.policy).to have_attributes(
          user: user,
          record: post
        )
        expect(error.policy).to be_a(PostPolicy)
      end
      # rubocop:enable Style/MultilineBlockChain
    end

    it "raises an error with the policy, query and record when the record is namespaced" do
      # rubocop:disable Style/MultilineBlockChain
      expect do
        Pundit.authorize(user, [:project, :admin, comment], :destroy?)
      end.to raise_error(Pundit::NotAuthorizedError,
                         "not allowed to Project::Admin::CommentPolicy#destroy? this Comment") do |error|
        expect(error.query).to eq :destroy?
        expect(error.record).to eq comment
        expect(error.policy).to have_attributes(
          user: user,
          record: comment
        )
        expect(error.policy).to be_a(Project::Admin::CommentPolicy)
      end
      # rubocop:enable Style/MultilineBlockChain
    end

    it "raises an error with the policy, query and the class name when a Class is given" do
      # rubocop:disable Style/MultilineBlockChain
      expect do
        Pundit.authorize(user, Post, :destroy?)
      end.to raise_error(Pundit::NotAuthorizedError, "not allowed to PostPolicy#destroy? Post") do |error|
        expect(error.query).to eq :destroy?
        expect(error.record).to eq Post
        expect(error.policy).to have_attributes(
          user: user,
          record: Post
        )
        expect(error.policy).to be_a(PostPolicy)
      end
      # rubocop:enable Style/MultilineBlockChain
    end

    it "raises an error with a invalid policy constructor" do
      expect do
        Pundit.authorize(user, wiki, :update?)
      end.to raise_error(Pundit::InvalidConstructorError, "Invalid #<WikiPolicy> constructor is called")
    end

    context "when passed a policy class" do
      it "uses the passed policy class" do
        expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy
      end

      # This is documenting past behaviour.
      it "doesn't cache the policy class" do
        cache = {}

        expect do
          Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache)
          Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache)
        end.to change { PublicationPolicy.instances }.by(2)
      end
    end

    context "when passed a policy class while simultaenously passing a namespace" do
      it "uses the passed policy class" do
        expect(PublicationPolicy).to receive(:new).with(user, comment).and_call_original
        expect(Pundit.authorize(user, [:project, comment], :create?, policy_class: PublicationPolicy)).to be_truthy
      end
    end

    context "when passed an explicit cache" do
      it "uses the hash assignment interface on the cache" do
        custom_cache = CustomCache.new

        Pundit.authorize(user, post, :update?, cache: custom_cache)

        expect(custom_cache.to_h).to match({
          post => kind_of(PostPolicy)
        })
      end
    end
  end

  describe ".policy_scope" do
    it "returns an instantiated policy scope given a plain model class" do
      expect(Pundit.policy_scope(user, Post)).to eq :published
    end

    it "returns an instantiated policy scope given an active model class" do
      expect(Pundit.policy_scope(user, Comment)).to eq CommentScope.new(Comment)
    end

    it "returns an instantiated policy scope given an active record relation" do
      expect(Pundit.policy_scope(user, comments_relation)).to eq CommentScope.new(comments_relation)
    end

    it "returns an instantiated policy scope given an empty active record relation" do
      expect(Pundit.policy_scope(user, empty_comments_relation)).to eq CommentScope.new(empty_comments_relation)
    end

    it "returns an instantiated policy scope given an array of a symbol and plain model class" do
      expect(Pundit.policy_scope(user, [:project, Post])).to eq :read
    end

    it "returns an instantiated policy scope given an array of a symbol and active model class" do
      expect(Pundit.policy_scope(user, [:project, Comment])).to eq Comment
    end

    it "returns nil if the given policy scope can't be found" do
      expect(Pundit.policy_scope(user, Article)).to be_nil
    end

    it "raises an exception if nil object given" do
      expect { Pundit.policy_scope(user, nil) }.to raise_error(Pundit::NotDefinedError)
    end

    it "raises an error with a invalid policy scope constructor" do
      expect do
        Pundit.policy_scope(user, Wiki)
      end.to raise_error(Pundit::InvalidConstructorError, "Invalid #<WikiPolicy::Scope> constructor is called")
    end

    it "raises an original error with a policy scope that contains error" do
      expect do
        Pundit.policy_scope(user, DefaultScopeContainsError)
      end.to raise_error(RuntimeError, "This is an arbitrary error that should bubble up")
    end
  end

  describe ".policy_scope!" do
    it "returns an instantiated policy scope given a plain model class" do
      expect(Pundit.policy_scope!(user, Post)).to eq :published
    end

    it "returns an instantiated policy scope given an active model class" do
      expect(Pundit.policy_scope!(user, Comment)).to eq CommentScope.new(Comment)
    end

    it "throws an exception if the given policy scope can't be found" do
      expect { Pundit.policy_scope!(user, Article) }.to raise_error(Pundit::NotDefinedError)
    end

    it "throws an exception if the given policy scope can't be found" do
      expect { Pundit.policy_scope!(user, ArticleTag) }.to raise_error(Pundit::NotDefinedError)
    end

    it "throws an exception if the given policy scope is nil" do
      expect do
        Pundit.policy_scope!(user, nil)
      end.to raise_error(Pundit::NotDefinedError, "Cannot scope NilClass")
    end

    it "returns an instantiated policy scope given an array of a symbol and plain model class" do
      expect(Pundit.policy_scope!(user, [:project, Post])).to eq :read
    end

    it "returns an instantiated policy scope given an array of a symbol and active model class" do
      expect(Pundit.policy_scope!(user, [:project, Comment])).to eq Comment
    end

    it "raises an error with a invalid policy scope constructor" do
      expect do
        Pundit.policy_scope(user, Wiki)
      end.to raise_error(Pundit::InvalidConstructorError, "Invalid #<WikiPolicy::Scope> constructor is called")
    end
  end

  describe ".policy" do
    it "returns an instantiated policy given a plain model instance" do
      policy = Pundit.policy(user, post)
      expect(policy.user).to eq user
      expect(policy.post).to eq post
    end

    it "returns an instantiated policy given an active model instance" do
      policy = Pundit.policy(user, comment)
      expect(policy.user).to eq user
      expect(policy.comment).to eq comment
    end

    it "returns an instantiated policy given a plain model class" do
      policy = Pundit.policy(user, Post)
      expect(policy.user).to eq user
      expect(policy.post).to eq Post
    end

    it "returns an instantiated policy given an active model class" do
      policy = Pundit.policy(user, Comment)
      expect(policy.user).to eq user
      expect(policy.comment).to eq Comment
    end

    it "returns an instantiated policy given a symbol" do
      policy = Pundit.policy(user, :criteria)
      expect(policy.class).to eq CriteriaPolicy
      expect(policy.user).to eq user
      expect(policy.criteria).to eq :criteria
    end

    it "returns an instantiated policy given an array of symbols" do
      policy = Pundit.policy(user, %i[project criteria])
      expect(policy.class).to eq Project::CriteriaPolicy
      expect(policy.user).to eq user
      expect(policy.criteria).to eq :criteria
    end

    it "returns an instantiated policy given an array of a symbol and plain model instance" do
      policy = Pundit.policy(user, [:project, post])
      expect(policy.class).to eq Project::PostPolicy
      expect(policy.user).to eq user
      expect(policy.post).to eq post
    end

    it "returns an instantiated policy given an array of a symbol and a model instance with policy_class override" do
      policy = Pundit.policy(user, [:project, customer_post])
      expect(policy.class).to eq Project::PostPolicy
      expect(policy.user).to eq user
      expect(policy.post).to eq customer_post
    end

    it "returns an instantiated policy given an array of a symbol and an active model instance" do
      policy = Pundit.policy(user, [:project, comment])
      expect(policy.class).to eq Project::CommentPolicy
      expect(policy.user).to eq user
      expect(policy.comment).to eq comment
    end

    it "returns an instantiated policy given an array of a symbol and a plain model class" do
      policy = Pundit.policy(user, [:project, Post])
      expect(policy.class).to eq Project::PostPolicy
      expect(policy.user).to eq user
      expect(policy.post).to eq Post
    end

    it "raises an error with a invalid policy constructor" do
      expect do
        Pundit.policy(user, Wiki)
      end.to raise_error(Pundit::InvalidConstructorError, "Invalid #<WikiPolicy> constructor is called")
    end

    it "returns an instantiated policy given an array of a symbol and an active model class" do
      policy = Pundit.policy(user, [:project, Comment])
      expect(policy.class).to eq Project::CommentPolicy
      expect(policy.user).to eq user
      expect(policy.comment).to eq Comment
    end

    it "returns an instantiated policy given an array of a symbol and a class with policy_class override" do
      policy = Pundit.policy(user, [:project, Customer::Post])
      expect(policy.class).to eq Project::PostPolicy
      expect(policy.user).to eq user
      expect(policy.post).to eq Customer::Post
    end

    it "returns correct policy class for an array of a multi-word symbols" do
      policy = Pundit.policy(user, %i[project_one_two_three criteria_four_five_six])
      expect(policy.class).to eq ProjectOneTwoThree::CriteriaFourFiveSixPolicy
    end

    it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model instance" do
      policy = Pundit.policy(user, [:project_one_two_three, post_four_five_six])
      expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy
    end

    it "returns correct policy class for an array of a multi-word symbol and a multi-word active model instance" do
      policy = Pundit.policy(user, [:project_one_two_three, comment_four_five_six])
      expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy
    end

    it "returns correct policy class for an array of a multi-word symbol and a multi-word plain model class" do
      policy = Pundit.policy(user, [:project_one_two_three, PostFourFiveSix])
      expect(policy.class).to eq ProjectOneTwoThree::PostFourFiveSixPolicy
    end

    it "returns correct policy class for an array of a multi-word symbol and a multi-word active model class" do
      policy = Pundit.policy(user, [:project_one_two_three, CommentFourFiveSix])
      expect(policy.class).to eq ProjectOneTwoThree::CommentFourFiveSixPolicy
    end

    it "returns correct policy class for a multi-word scoped plain model class" do
      policy = Pundit.policy(user, ProjectOneTwoThree::TagFourFiveSix)
      expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy
    end

    it "returns correct policy class for a multi-word scoped plain model instance" do
      policy = Pundit.policy(user, tag_four_five_six)
      expect(policy.class).to eq ProjectOneTwoThree::TagFourFiveSixPolicy
    end

    it "returns correct policy class for a multi-word scoped active model class" do
      policy = Pundit.policy(user, ProjectOneTwoThree::AvatarFourFiveSix)
      expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy
    end

    it "returns correct policy class for a multi-word scoped active model instance" do
      policy = Pundit.policy(user, avatar_four_five_six)
      expect(policy.class).to eq ProjectOneTwoThree::AvatarFourFiveSixPolicy
    end

    it "returns nil if the given policy can't be found" do
      expect(Pundit.policy(user, article)).to be_nil
      expect(Pundit.policy(user, Article)).to be_nil
    end

    it "returns the specified NilClassPolicy for nil" do
      expect(Pundit.policy(user, nil)).to be_a NilClassPolicy
    end

    describe "with .policy_class set on the model" do
      it "returns an instantiated policy given a plain model instance" do
        policy = Pundit.policy(user, artificial_blog)
        expect(policy.user).to eq user
        expect(policy.blog).to eq artificial_blog
      end

      it "returns an instantiated policy given a plain model class" do
        policy = Pundit.policy(user, ArtificialBlog)
        expect(policy.user).to eq user
        expect(policy.blog).to eq ArtificialBlog
      end

      it "returns an instantiated policy given a plain model instance providing an anonymous class" do
        policy = Pundit.policy(user, article_tag)
        expect(policy.user).to eq user
        expect(policy.tag).to eq article_tag
      end

      it "returns an instantiated policy given a plain model class providing an anonymous class" do
        policy = Pundit.policy(user, ArticleTag)
        expect(policy.user).to eq user
        expect(policy.tag).to eq ArticleTag
      end
    end
  end

  describe ".policy!" do
    it "returns an instantiated policy given a plain model instance" do
      policy = Pundit.policy!(user, post)
      expect(policy.user).to eq user
      expect(policy.post).to eq post
    end

    it "returns an instantiated policy given an active model instance" do
      policy = Pundit.policy!(user, comment)
      expect(policy.user).to eq user
      expect(policy.comment).to eq comment
    end

    it "returns an instantiated policy given a plain model class" do
      policy = Pundit.policy!(user, Post)
      expect(policy.user).to eq user
      expect(policy.post).to eq Post
    end

    it "returns an instantiated policy given an active model class" do
      policy = Pundit.policy!(user, Comment)
      expect(policy.user).to eq user
      expect(policy.comment).to eq Comment
    end

    it "returns an instantiated policy given a symbol" do
      policy = Pundit.policy!(user, :criteria)
      expect(policy.class).to eq CriteriaPolicy
      expect(policy.user).to eq user
      expect(policy.criteria).to eq :criteria
    end

    it "returns an instantiated policy given an array of symbols" do
      policy = Pundit.policy!(user, %i[project criteria])
      expect(policy.class).to eq Project::CriteriaPolicy
      expect(policy.user).to eq user
      expect(policy.criteria).to eq :criteria
    end

    it "throws an exception if the given policy can't be found" do
      expect { Pundit.policy!(user, article) }.to raise_error(Pundit::NotDefinedError)
      expect { Pundit.policy!(user, Article) }.to raise_error(Pundit::NotDefinedError)
    end

    it "returns the specified NilClassPolicy for nil" do
      expect(Pundit.policy!(user, nil)).to be_a NilClassPolicy
    end

    it "raises an error with a invalid policy constructor" do
      expect do
        Pundit.policy(user, Wiki)
      end.to raise_error(Pundit::InvalidConstructorError, "Invalid #<WikiPolicy> constructor is called")
    end
  end

  describe ".included" do
    it "includes Authorization module" do
      klass = Class.new

      expect do
        klass.include Pundit
      end.to output.to_stderr

      expect(klass).to include Pundit::Authorization
    end

    it "warns about deprecation" do
      klass = Class.new
      expect do
        klass.include Pundit
      end.to output(a_string_starting_with("'include Pundit' is deprecated")).to_stderr
    end
  end

  describe "Pundit::NotAuthorizedError" do
    it "can be initialized with a string as message" do
      error = Pundit::NotAuthorizedError.new("must be logged in")
      expect(error.message).to eq "must be logged in"
    end
  end
end