Search code examples
ruby-on-railsrubyminitest

Rails pundit user scope Minitest won't run


In my app User only with status == 'active' has access to all application pages. I've create a scope policy for that action and a separate intermediate controller in which I set all global policies. As follows:

class UserPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      return raise(Pundit::NotAuthorizedError) unless user.status == 'active'

      scope.all
    end
  end
end

class BaseController < ApplicationController
  before_action :authorized_user
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  def authorized_user
    policy_scope(current_user)
  end

  private

  def user_not_authorized
    flash[:alert] = 'You are not authorized to perform this action.'
    redirect_to(request.referrer || root_path)
  end
end

Now I want to have a MiniTest for this scope policy. This is what I've got:

require 'test_helper'

class UserPolicyTest < ActiveSupport::TestCase
  context 'when user is active' do
    def setup
      @user = user(:active)
      @properties = properties(:one)
    end

    def scope_test
      # user should have access to the show page in properties controller
      refute_equal properties, user
      assert permit(user, properties, :show)
    end
  end
end

What did I missed? when I run this it shows me:

Finished in 0.185689s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips

Solution

  • You got it all pretty backwards. Its not the job of a scope to control overall access.

    class ApplicationPolicy
    
      class Scope
        def resolve
          @user.active?
            scope.all
          else
            scope.none
          end
        end
      end
    
      # ...
    
      def show?
        @user.active?
      end 
    
      def index?
        @user.active?
      end
    
    
      private
    
      def active?
        user.active? # user.status == 'active' is iffy. Don't let the logic leak outside of the object
      end
    end
    

    If you want to authorize the the actions of a controller you would do:

    def show
      authorize(Thing.find(params[:id]))
    end
    
    def index
      authorize(Thing) # controlls overall access - often skipped
      @things = policy_scope(Thing)
    end
    

    If you really want to add a scope to the show method you would do:

    def show
      authorize(policy_scope(Thing).find(params[:id]))
    end
    

    The reason your scope should NOT be controlling overall access is that is does not know the context (which action is called). So while this may "work" right now its going to blow up in your face when stuff gets more complicated.

    If you then want to test a specific policy you would do:

    assert ThingPolicy.new(thing, user).show?
    

    If you want to test a scope you resolve the scope and test that the expected records are included/not included.