Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test Pundit policies with Minitest?

Gemfile

gem 'pundit', '~> 0.2.1'

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  include Pundit
  ...

app/policies/application_policy.rb

class ApplicationPolicy < Struct.new(:user, :record)
  def index?  ; false;                              end
  def show?   ; scope.where(id: record.id).exists?; end
  def create? ; false;                              end
  def new?    ; create?;                            end
  def update? ; false;                              end
  def edit?   ; update?;                            end
  def destroy?; false;                              end
  def scope
    Pundit.policy_scope!(user, record.class)
  end
end

app/policies/book_policy.rb

class BookPolicy < ApplicationPolicy

  def create?
    record.new_record?
  end

  def new?
    create?      end

  def show?
    record.published? || user == record.user || user.is?(:admin)
  end

end

app/controllers/books_controller.rb

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, except: [:show]

  after_action :verify_authorized, except: :index
  after_action :verify_policy_scoped, only: :index

  # GET /books/1
  def show
    authorize(@book)
  end

  # GET /books/new
  def new
    @book = Book.new
    authorize(@book)
  end

  # POST /books
  def create
    @book = current_user.books.build(book_params)
    authorize(@book)

    if @book.save
      redirect_to @book, notice: 'Your book was successfully created.'
    else
      render action: 'new'
    end
  end

private
    def set_book
      @book = Book.find(params[:id])
    end

    def book_params
      params.require(:book).permit(:title, :description)
    end
end

test/factories/factories.rb

FactoryGirl.define do

  factory :user do
    sequence(:email) { |n| "email#{n}@x.com" }
    password  '12345678'
    password_confirmation  '12345678'
  end

  factory :book do
    title  'xx'
    user
  end

end
like image 855
user664833 Avatar asked Nov 28 '13 02:11

user664833


4 Answers

First try

As per the Minitest docs I tried < Minitest::Test, but got gems/minitest-4.7.5/lib/minitest/unit.rb:19:in 'const_missing': uninitialized constant MiniTest::Test (NameError) which led me to discover that the docs in master are for Minitest 5. So I hunted down the Minitest docs before the version 5 commit and found that we should be subclassing MiniTest::Unit::TestCase.

test/policies/book_policy_test.rb

require 'test_helper'
class BookPolicyTest < Minitest::Test
  ...
end

Second try (correct superclass)

test/policies/book_policy_test.rb

require 'test_helper'

class BookPolicyTest < Minitest::Unit::TestCase

  def test_new
    user = FactoryGirl.create(:user)
    book_policy = BookPolicy.new(user, Book.new)
    assert book_policy.new?
  end

  def test_create
    book = FactoryGirl.create(:book)
    book_policy = BookPolicy.new(book.user, book)
    assert !book_policy.create?
  end

end

Refactor one (create 'permit' method)

test/policies/book_policy_test.rb

require 'test_helper'

class BookPolicyTest < Minitest::Unit::TestCase

  def test_new
    user = FactoryGirl.create(:user)
    assert permit(user, Book.new, :new)
  end

  def test_create
    book = FactoryGirl.create(:book)
    assert !permit(book.user, book, :create)
  end

private

    def permit(current_user, record, action)
      self.class.to_s.gsub(/Test/, '').constantize.new(current_user, record).public_send("#{action.to_s}?")
    end

end

Refactor two (create 'PolicyTest' class)

test/test_helper.rb

class PolicyTest < Minitest::Unit::TestCase

  def permit(current_user, record, action)
    self.class.to_s.gsub(/Test/, '').constantize.new(current_user, record).public_send("#{action.to_s}?")
  end

end

test/policies/book_policy_test.rb

require 'test_helper'

class BookPolicyTest < PolicyTest

  def test_new
    user = FactoryGirl.create(:user)
    assert permit(user, Book.new, :new)
  end

  def test_create
    book = FactoryGirl.create(:book)
    assert !permit(book.user, book, :create)
  end

end

Refactor three (create 'forbid' method)

test/test_helper.rb

class PolicyTest < Minitest::Unit::TestCase

  def permit(current_user, record, action)
    self.class.to_s.gsub(/Test/, '').constantize.new(current_user, record).public_send("#{action.to_s}?")
  end

  def forbid(current_user, record, action)
    !permit(current_user, record, action)
  end

end

test/policies/book_policy_test.rb

require 'test_helper'

class BookPolicyTest < PolicyTest

  def test_new
    user = FactoryGirl.create(:user)
    assert permit(user, Book.new, :new)
  end

  def test_create
    book = FactoryGirl.create(:book)
    assert forbid(book.user, book, :create)
  end

end

Add full set of policy tests

require 'test_helper'

class BookPolicyTest < PolicyTest

  def test_new
    assert permit(User.new, Book.new, :new)

    book = FactoryGirl.create(:book)
    assert forbid(book.user, book, :new)
  end

  def test_create
    assert permit(User.new, Book.new, :create)

    book = FactoryGirl.create(:book)
    assert forbid(book.user, book, :create)
  end

  def test_show
    # a stranger should be able to see a published book
    stranger = FactoryGirl.build(:user)
    book = FactoryGirl.create(:book, published: true)
    refute_equal stranger, book.user
    assert permit(stranger, book, :show)

    # but not if it's NOT published
    book.published = false
    assert forbid(stranger, book, :show)

    # but the book owner still should
    assert permit(book.user, book, :show)

    # and so should the admin
    admin = FactoryGirl.build(:admin)
    assert permit(admin, book, :show)
  end

end
like image 57
user664833 Avatar answered Oct 23 '22 19:10

user664833


I created a gem for testing pundit with minitest called policy-assertions. Here's what your test looks like.

class ArticlePolicyTest < PolicyAssertions::Test
  def test_index_and_show
    assert_permit nil, Article
  end

  def test_new_and_create
    assert_permit users(:staff), Article
  end

  def test_destroy
    refute_permit users(:regular), articles(:instructions)
  end
end
like image 31
Kevin Avatar answered Oct 23 '22 20:10

Kevin


Pundit now provides Pundit#authorize(https://github.com/elabs/pundit/pull/227). So user664833's permit method can be updated as below:

def permit(current_context, record, action)
  Pundit.authorize(current_context, record, action)
rescue Pundit::NotAuthorizedError
  false
end
like image 3
puneet.sutar Avatar answered Oct 23 '22 20:10

puneet.sutar


Building on user664833's answer I use the following to test Pundit's policy_scope:

def permit_index(user, record)
  (record.class.to_s + 'Policy::Scope').constantize.new(user, record.class).resolve.include?(record)
end

For example:

assert permit_index(@book.user, @book)
like image 2
andorov Avatar answered Oct 23 '22 18:10

andorov