Search code examples
ruby-on-railsrspecpundit

problems for Category Policy Spec with pundit gem


so I do the association of the categories and posts through categorizations. And i'm trying to restrict the access with pundit gem and testing with RSpec for to show by a category the posts. Because with this association I have something like: c = Category.first c.posts

and i'm showing the posts by a category.

And i need some help.

my categories_controller.rb

class CategoriesController < ApplicationController
  before_action :set_category, only: [:show]

  def show
    authorize @category, :show?
  end

  private

  def set_category
    @category = Category.find(params[:id])
  end
end

category.rb

class Category < ActiveRecord::Base
  has_many :categorizations, dependent: :destroy
  has_many :posts, through: :categorizations

  validates :name,
                  presence: true,
                  length: { minimum: 3, maximum: 30 },
                  uniqueness: true
end

app/views/categories/show.html.slim

= title("Category - #{@category.name}")
#category.text-center
  h1 Category: <small>#{@category.name}</small>

  - if @category.posts
    #category.posts
      - @category.posts.each do |post|
        .row.col-md-12
          .caption
            h2 = link_to post.title, post
            p = link_to post.subtitle, post
            small = "by : #{post.author}"
          hr

routes.rb

Rails.application.routes.draw do

  namespace :admin do
    root 'application#index'

    resources :posts, only: [:new, :create, :destroy]
    resources :categories

    resources :users do
      member do
        patch :archive
      end
    end
  end

  devise_for :users

  root "posts#index"

  resources :posts, only: [:index, :show, :edit, :update]
  resources :categories, only: [:show]
end

category_policy.rb

class CategoryPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      resolve
    end
  end

  def show?
    user.try(:admin?) || record.post.has_member?(user)
  end
end

category_policy_spec.rb

require 'rails_helper'

RSpec.describe CategoryPolicy do
  context "permissions" do
    subject        { CategoryPolicy.new(user, category) }
    let(:user)     { create(:user) }
    let(:post)     { create(:post) }
    let(:category) { create(:category) }

    context "for anonymous users" do
      let(:user) { nil }

      it { should_not permit_action :show }
    end

    context "for viewers of the post" do
      before { assign_role!(user, :viewer, post) }

      it { should permit_action :show }
    end

    context "for editors of the post" do
      before { assign_role!(user, :editor, post) }

      it { should permit_action :show }
    end

    context "for managers of the post" do
      before { assign_role!(user, :manager, post) }

      it { should permit_action :show }
    end

    context "for managers of other posts" do
      before do
         assign_role!(user, :manager, create(:post))
      end

      it { should_not permit_action :show }
    end

    context "for administrators" do
      let(:user) { create(:user, :admin) }

      it  { should permit_action :show }
    end
  end
end

I'm using this matcher. pundit_matcher.rb

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

  failure_message do |policy|
    "#{policy.class} does not allow #{policy.user || "nil"} to " +
      "perform :#{action}? on #{policy.record}."
  end

  failure_message_when_negated do |policy|
    "#{policy.class} does not forbid #{policy.user || "nil"} from " +
      "performing :#{action}? on #{policy.record}."
  end
end

post.rb

class Post < ActiveRecord::Base
  belongs_to :author, class_name: "User"
  has_many :roles, dependent: :delete_all
  has_many :categorizations, dependent: :destroy
  has_many :categories, through: :categorizations

  mount_base64_uploader :attachment, AttachmentUploader

  validates :title,
              presence: true,
              length: { minimum: 10, maximum: 100 },
              uniqueness: true
  validates :subtitle,
              length: { maximum: 100 }
  validates :content,
              presence: true,
              length: { minimum: 30 }
  validates :attachment, file_size: { less_than: 1.megabytes }


  def has_member?(user)
    roles.exists?(user_id: user)
  end

  [:manager, :editor, :viewer].each do |role|
    define_method "has_#{role}?" do |user|
      roles.exists?(user_id: user, role: role)
    end
  end

end

roler.rb

class Role < ActiveRecord::Base
  belongs_to :user
  belongs_to :post

  def self.available_roles
    %w(manager editor viewer)
  end
end

When I try to run the:

rspec spec/policies/category_policy_spec.rb

I get this error:

Failure/Error: user.try(:admin?) || record.post.has_member?(user)

     NoMethodError:
       undefined method `post' for #<Category:0x007fc61bfdbf40>
       Did you mean?  posts

It's ok show this posts by category on show, or I need to switch for index?


Solution

  • As the error states, your Category model doesn't have a post method, it only has posts. This is because Category has_many posts. Because of that record.post.has_member?(user) in your Category policy doesn't make any sense (record is a Category instance). Since Category doesn't appear to have a direct relationship to users, and your roles are associated with users and posts.

    It's unclear to me what the purpose of your Category policy actually is, unless it's to only allow a user to see categories if they already have a post that uses that category? If that's the case then you need to change the policy to actually check if any of those exist.