Back to Repositories

Validating Authorization Policy Enforcement in Pundit

This RSpec test suite comprehensively validates the Pundit authorization framework’s core functionality and behavior. It covers authorization verification, policy scoping, and permission handling across different contexts and use cases.

Test Coverage Overview

The test suite provides extensive coverage of Pundit’s authorization mechanisms, including:

  • Authorization verification and policy scoping checks
  • Policy inference and authorization handling
  • Permission checking and attribute filtering
  • Cache management and user context handling
  • Error cases and edge conditions

Implementation Analysis

The testing approach employs RSpec’s behavior-driven development patterns with detailed context specifications. It utilizes doubles for user simulation and implements comprehensive assertion chains to validate authorization outcomes.

The tests leverage RSpec’s let statements for efficient object setup and employ shared contexts for related test scenarios.

Technical Details

Key technical components include:

  • RSpec as the testing framework
  • ActionController::Parameters for parameter handling
  • Mock objects and doubles for isolation testing
  • Custom helper methods for parameter conversion
  • Nested describe blocks for organized test structure

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear separation of concerns
  • Comprehensive error case coverage
  • Consistent test structure and naming
  • Efficient test setup and teardown
  • Clear documentation of expected behaviors

varvet/pundit

spec/authorization_spec.rb

            
# frozen_string_literal: true

require "spec_helper"
require "action_controller/metal/strong_parameters"

describe Pundit::Authorization do
  def to_params(*args, **kwargs, &block)
    ActionController::Parameters.new(*args, **kwargs, &block)
  end

  let(:controller) { Controller.new(user, "update", to_params({})) }
  let(:user) { double("user") }
  let(:post) { Post.new(user) }
  let(:comment) { Comment.new }
  let(:article) { Article.new }
  let(:article_tag) { ArticleTag.new }
  let(:wiki) { Wiki.new }

  describe "#verify_authorized" do
    it "does nothing when authorized" do
      controller.authorize(post)
      controller.verify_authorized
    end

    it "raises an exception when not authorized" do
      expect { controller.verify_authorized }.to raise_error(Pundit::AuthorizationNotPerformedError)
    end
  end

  describe "#verify_policy_scoped" do
    it "does nothing when policy_scope is used" do
      controller.policy_scope(Post)
      controller.verify_policy_scoped
    end

    it "raises an exception when policy_scope is not used" do
      expect { controller.verify_policy_scoped }.to raise_error(Pundit::PolicyScopingNotPerformedError)
    end
  end

  describe "#pundit_policy_authorized?" do
    it "is true when authorized" do
      controller.authorize(post)
      expect(controller.pundit_policy_authorized?).to be true
    end

    it "is false when not authorized" do
      expect(controller.pundit_policy_authorized?).to be false
    end
  end

  describe "#pundit_policy_scoped?" do
    it "is true when policy_scope is used" do
      controller.policy_scope(Post)
      expect(controller.pundit_policy_scoped?).to be true
    end

    it "is false when policy scope is not used" do
      expect(controller.pundit_policy_scoped?).to be false
    end
  end

  describe "#authorize" do
    it "infers the policy name and authorizes based on it" do
      expect(controller.authorize(post)).to be_truthy
    end

    it "returns the record on successful authorization" do
      expect(controller.authorize(post)).to eq(post)
    end

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

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

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

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

    it "can be given a different permission to check" do
      expect(controller.authorize(post, :show?)).to be_truthy
      expect { controller.authorize(post, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
    end

    it "can be given a different policy class" do
      expect(controller.authorize(post, :create?, policy_class: PublicationPolicy)).to be_truthy
    end

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

    it "throws an exception when the permission check fails" do
      expect { controller.authorize(Post.new) }.to raise_error(Pundit::NotAuthorizedError)
    end

    it "throws an exception when a policy cannot be found" do
      expect { controller.authorize(Article) }.to raise_error(Pundit::NotDefinedError)
    end

    it "caches the policy" do
      expect(controller.policies[post]).to be_nil
      controller.authorize(post)
      expect(controller.policies[post]).not_to be_nil
    end

    it "raises an error when the given record is nil" do
      expect { controller.authorize(nil, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
    end

    it "raises an error with a invalid policy constructor" do
      expect { controller.authorize(wiki, :destroy?) }.to raise_error(Pundit::InvalidConstructorError)
    end
  end

  describe "#skip_authorization" do
    it "disables authorization verification" do
      controller.skip_authorization
      expect { controller.verify_authorized }.not_to raise_error
    end
  end

  describe "#skip_policy_scope" do
    it "disables policy scope verification" do
      controller.skip_policy_scope
      expect { controller.verify_policy_scoped }.not_to raise_error
    end
  end

  describe "#pundit_user" do
    it "returns the same thing as current_user" do
      expect(controller.pundit_user).to eq controller.current_user
    end
  end

  describe "#policy" do
    it "returns an instantiated policy" do
      policy = controller.policy(post)
      expect(policy.user).to eq user
      expect(policy.post).to eq post
    end

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

    it "raises an error with a invalid policy constructor" do
      expect { controller.policy(wiki) }.to raise_error(Pundit::InvalidConstructorError)
    end

    it "allows policy to be injected" do
      new_policy = double
      controller.policies[post] = new_policy

      expect(controller.policy(post)).to eq new_policy
    end
  end

  describe "#policy_scope" do
    it "returns an instantiated policy scope" do
      expect(controller.policy_scope(Post)).to eq :published
    end

    it "allows policy scope class to be overridden" do
      expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published
    end

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

    it "raises an error with a invalid policy scope constructor" do
      expect { controller.policy_scope(Wiki) }.to raise_error(Pundit::InvalidConstructorError)
    end

    it "allows policy_scope to be injected" do
      new_scope = double
      controller.policy_scopes[Post] = new_scope

      expect(controller.policy_scope(Post)).to eq new_scope
    end
  end

  describe "#permitted_attributes" do
    it "checks policy for permitted attributes" do
      params = to_params(
        post: {
          title: "Hello",
          votes: 5,
          admin: true
        }
      )

      action = "update"

      expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq(
        "title" => "Hello",
        "votes" => 5
      )
      expect(Controller.new(double, action, params).permitted_attributes(post).to_h).to eq("votes" => 5)
    end

    it "checks policy for permitted attributes for record of a ActiveModel type" do
      customer_post = Customer::Post.new(user)
      params = to_params(
        customer_post: {
          title: "Hello",
          votes: 5,
          admin: true
        }
      )

      action = "update"

      expect(Controller.new(user, action, params).permitted_attributes(customer_post).to_h).to eq(
        "title" => "Hello",
        "votes" => 5
      )
      expect(Controller.new(double, action, params).permitted_attributes(customer_post).to_h).to eq(
        "votes" => 5
      )
    end

    it "goes through the policy cache" do
      params = to_params(post: { title: "Hello" })
      user = double
      post = Post.new(user)
      controller = Controller.new(user, "update", params)

      expect do
        expect(controller.permitted_attributes(post)).to be_truthy
        expect(controller.permitted_attributes(post)).to be_truthy
      end.to change { PostPolicy.instances }.by(1)
    end
  end

  describe "#permitted_attributes_for_action" do
    it "is checked if it is defined in the policy" do
      params = to_params(
        post: {
          title: "Hello",
          body: "blah",
          votes: 5,
          admin: true
        }
      )

      action = "revise"
      expect(Controller.new(user, action, params).permitted_attributes(post).to_h).to eq("body" => "blah")
    end

    it "can be explicitly set" do
      params = to_params(
        post: {
          title: "Hello",
          body: "blah",
          votes: 5,
          admin: true
        }
      )

      action = "update"
      expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah")
    end
  end

  describe "#pundit_reset!" do
    it "allows authorize to react to a user change" do
      expect(controller.authorize(post)).to be_truthy

      controller.current_user = double
      controller.pundit_reset!
      expect { controller.authorize(post) }.to raise_error(Pundit::NotAuthorizedError)
    end

    it "allows policy to react to a user change" do
      expect(controller.policy(DummyCurrentUser).user).to be user

      new_user = double("new user")
      controller.current_user = new_user
      controller.pundit_reset!
      expect(controller.policy(DummyCurrentUser).user).to be new_user
    end

    it "allows policy scope to react to a user change" do
      expect(controller.policy_scope(DummyCurrentUser)).to be user

      new_user = double("new user")
      controller.current_user = new_user
      controller.pundit_reset!
      expect(controller.policy_scope(DummyCurrentUser)).to be new_user
    end

    it "resets the pundit context" do
      expect(controller.pundit.user).to be(user)

      new_user = double
      controller.current_user = new_user
      expect { controller.pundit_reset! }.to change { controller.pundit.user }.from(user).to(new_user)
    end

    it "clears pundit_policy_authorized? flag" do
      expect(controller.pundit_policy_authorized?).to be false

      controller.skip_authorization
      expect(controller.pundit_policy_authorized?).to be true

      controller.pundit_reset!
      expect(controller.pundit_policy_authorized?).to be false
    end

    it "clears pundit_policy_scoped? flag" do
      expect(controller.pundit_policy_scoped?).to be false

      controller.skip_policy_scope
      expect(controller.pundit_policy_scoped?).to be true

      controller.pundit_reset!
      expect(controller.pundit_policy_scoped?).to be false
    end
  end
end