Search code examples
ruby-on-railsruby-on-rails-4rolespundit

Permissions for create comments on comment_policy_spec for a post with Pundit


[UPDATED:]

I'm using Pundit and I'm facing problems when I try to use for a user who is permitted with a role(like manager) on posts to create a comment.

I'm practicing to do in this way testing with RSpec and polymorphic association and I want to know if I doing correct, and how to do for pass this error. The sample for do a polymorphic association I'm using like the gorails tutorial.

I have this:

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.text :content
      t.references :commentable, polymorphic: true, index: true
      t.references :author, index: true

      t.timestamps null: false
    end

    add_foreign_key :comments, :users, column: :author_id
  end
end

Comment.rb

class Comment < ActiveRecord::Base
  belongs_to :author, class_name: "User"
  belongs_to :commentable, polymorphic: true

  validates :content, presence: true

  default_scope -> { order(created_at: "desc") }
  scope :persisted, lambda { where.not(id: nil) }

end

CommentsController

class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @commentable.comments.new(comment_params)
    @comment.author = current_user
    authorize @comment, :create?

    if @comment.save
     flash[:notice] = "Comment has been created."
     redirect_to @commentable
    else
     flash.now[:alert] = "Comment has not been created."
     render "posts/show"
    end
 end


  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def comment_params
    params.require(:comment).permit(:content)
  end
end

app/views/posts/show.html.slim

.
.
.
#comments
  = render partial: "comments/form", locals: {commentable: @post}

  - if @post.comments.persisted.any?
    h4
      = t(:available_comments, count: @post.comments.count)
    = render partial: "comments/comments", locals: {commentable: @post}
  - else
    p
      There are no comments for this post.

comments/_form.html.slim

    .header
      h3 New Comment

    = simple_form_for [commentable, Comment.new] do |f|
      .form-group
        = f.input :content, label: "Comment", placeholder: "Add a comment", input_html: { rows: 6 }
      = f.submit class: "btn btn-primary"
    <br>

routes

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

  resources :posts, only: [] do
    resources :comments, only: [:create], module: :posts
  end

app/controllers/post/comments_controller.rb

class Posts::CommentsController < CommentsController
  before_action :set_commentable

  private

  def set_commentable
    @commentable = Post.find(params[:post_id])
  end
end

role.rb

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

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

spec/factories/comment_factory.rb

FactoryGirl.define do
  factory :comment do
    content { "A comment!" }

    trait :post_comment do
      association :commentable, factory: :post
      #commentable_type 'Post'
      association :author_id, factory: :user
    end
  end
end

CommentPolicy

    class CommentPolicy < ApplicationPolicy
  class Scope < Scope
   def resolve
     scope
   end
  end

 def create?
    user.try(:admin?) || record.commentable.has_manager?(user)
 end
end

spec/policies/comment_policy_spec.rb

    require 'rails_helper'    
    RSpec.describe CommentPolicy do
      context "permissions" do
        subject { CommentPolicy.new(user, comment) }

        let(:user) { create(:user) }
        let(:post) { create(:post)}
        let(:comment) { create(:comment, post: post)}

        context "for anonymous users" do
          let(:user) { nil }
          it { should_not permit_action :create }
        end

        context "for viewers of the post_comment" do
          before { assign_role!(user, :viewer, post) }
          it { should_not permit_action :create }
        end

        context "for editors of the post" do
          before { assign_role!(user, :editor, post) }
          it { should permit_action :create }
        end

        context "for managers of the post" do
          before { assign_role!(user, :manager, post) }
          it { should permit_action :create }
        end

        context "for managers of other post" do
          before do
            assign_role!(user, :manager, create(:post))
          end
          it { should_not permit_action :create }
        end

        context "for administrators" do
          let(:user) { create(:user, :admin) }
          it { should permit_action :create }
        end
      end

    end

When I run I have:

`rspec spec/policies/comment_policy_spec.rb`
Run options: exclude {:slow=>true}
FFFFFF

Failure/Error: subject { CommentPolicy.new(user, comment) }

     NameError:
       undefined local variable or method `comment' for #<RSpec::ExampleGroups::CommentPolicy::Permissions::.....

I tryed to put in the place of comment( like commentable, comments) and gets the same error. And I tryed to put post like: subject { CommentPolicy.new(user, post) } and work's, but was complained another errors: rspec spec/policies/comment_policy_spec.rb

Run options: exclude {:slow=>true}
FFFFF.
Failure/Error: user.try(:admin?) || record.commentable.has_manager?(user)

     NoMethodError:
       undefined method `commentable' for #<Post:0x007f9cc3dba328>
       Did you mean?  comments

I put on CommentPolicy the subject { CommentPolicy.new(user, comment) } and I run the application on localhost and try to create a comment with different users like(admin, manager, editor). As expected the application work's fine.

The admin and manager was able to create a comment, and the editor receive the message "You aren't allowed to do that." and wasn't able to create the comment as expected. So the problem is something on the RSpec that I don't know yet.


Solution

  • Giving that you are using polymorphism (because you want to use the Comment model to be associated to more than one model), you defined the model Comment as:

    class Comment < ActiveRecord::Base
      belongs_to :commentable, polymorphic: true
      #etc...
    end
    

    The belongs_to :commentable polymorphic: true allows this model to belong to more than one model. It will create two fields in the table:

    1. commentable_id

    2. commentable_type

    commentable_id will be a foreign key to another model/table and commentable_type will be the model name. This allows to comment posts, or any other model you may add.

    This definition is complemented by the associated model, Post, using:

    class Post < ActiveRecord::Base
      has_many :comments, as: :commentable, dependent: :destroy
      #etc...
    end
    

    has_many :comments, as: :commentable lets ActiveRecord know this is one of the models associated to Comment using polymorphism.

    However, in your Comment model you associated the comment to a Post in two ways:

    belongs_to :post
    belongs_to :commentable, polymorphic: true
    

    This is why your table has post_id (because of the first line) and commentable_id (because of the second line). Only commentable_id is set when the record is created, as it is done through:

    @post.comments.build
    #this will set comment.commentable_id = @post.id, and comment.commentable_type = "Post"
    #but will not set comment.post_id
    

    For all said, you have a post_id (null, causing the error) and a commentable_id (not null). belongs_to :post in Comment model makes ActiveRecord search the related Post using post_id (in record.post.has_manager?(user)), instead of using commentable_id. As post_id is null, the Post is not found. The error only happens if the user is not admin, because if he is an admin, user.try(:admin?) returns true and the rest of the sentence is not evaluated. Change record.post.has_manager?(user) for record.commentable.has_manager?(user)

    Besides, I think you have an error in PostPolicy

      scope.joins(:roles).where(roles: {user_id: user}
    

    Should be

      scope.joins(:roles).where(roles: {user_id: user.id}