1
\$\begingroup\$

The following specs test the authorization setup of the Post resource in a Rails 4.2 application. The Pundit gem is used for authorization but I believe the specs be easily adapted to work with most role-based authorization setups. The code was inspired by this blog post.

The User model has_many :posts and the Post model belongs_to :user.

The code works as expected but it would be great to receive feedback and suggestions from more experienced developers.

Custom Matcher

RSpec::Matchers.define :permit do |action|
  match do |policy|
    policy.public_send("#{action}?")
  end

  failure_message do |policy|
    "#{policy.class} does not permit #{action} on #{policy.record} for #{policy.user.inspect}."
  end

  failure_message_when_negated do |policy|
    "#{policy.class} does not forbid #{action} on #{policy.record} for #{policy.user.inspect}."
  end
end

Specs

require 'rails_helper'
require 'support/pundit_matcher'

describe PostPolicy do
  describe 'scope' do
    # The following "let" and "subject" blocks use variables defined within
    # other "let" blocks (user and scope). That's possible due to lazy loading.
    subject(:policy_scope) { PostPolicy::Scope.new(user,Post.all).resolve }
    let(:associated_post) { FactoryGirl.create(:post, user: user) }
    let!(:unassociated_post) { FactoryGirl.create(:post, user: nil) }

    context "for an unauthenticated user" do
      let(:user) { nil }
      it "hides all posts" do
        # unassociated_post is eagerly created so it exists within this test.
        expect(policy_scope).to eq nil
      end
    end

    context "for an ordinary user" do
      let(:user) { FactoryGirl.create(:user) }
      it "only shows posts associated with the user" do
        expect(policy_scope).to eq [associated_post]
      end
    end

    context "for an admin user" do
      let(:user) { FactoryGirl.create(:admin_user) }
      it "shows unpublished post" do
        expect(policy_scope).to include(associated_post, unassociated_post)
      end
    end
  end

  describe 'policy' do
    subject { PostPolicy.new(user, post) }
    let(:post) { FactoryGirl.create(:post, user: nil) }

    context "for an unauthenticated user" do
      let(:user) { nil }

      it { is_expected.to permit(:show) }
      it { is_expected.not_to permit(:create) }
      it { is_expected.not_to permit(:new) }
      it { is_expected.not_to permit(:update) }
      it { is_expected.not_to permit(:edit) }
      it { is_expected.not_to permit(:destroy) }
    end

    context "for an ordinary user" do
      let(:user) { FactoryGirl.create(:user) }

      it { is_expected.to permit(:create) }
      it { is_expected.to permit(:new) }

      context "and a post that is NOT associated to that user" do
        it { is_expected.to permit(:show) }
        it { is_expected.not_to permit(:update) }
        it { is_expected.not_to permit(:edit) }
        it { is_expected.not_to permit(:destroy) }
      end

      context "and a post that IS associated to that user" do
        let(:post) { FactoryGirl.create(:post, user: user) }

        it { is_expected.to permit(:show) }
        it { is_expected.to permit(:update) }
        it { is_expected.to permit(:edit) }
        it { is_expected.to permit(:destroy) }
      end
    end

    context "for an admin" do
      let(:user) { FactoryGirl.create(:admin_user) }

      it { is_expected.to permit(:show) }
      it { is_expected.to permit(:create) }
      it { is_expected.to permit(:new) }
      it { is_expected.to permit(:update) }
      it { is_expected.to permit(:edit) }
      it { is_expected.to permit(:destroy) }
    end
  end
end
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.