polleverywhere/moat

View on GitHub
lib/moat/rspec.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module Moat
  module RSpec
    module PolicyMatchers
      extend ::RSpec::Matchers::DSL

      matcher :permit_all_authorizations do
        match do |policy_class|
          @incorrectly_denied = policy_authorizations - permitted_authorizations(policy_class)
          @incorrectly_denied.empty?
        end
        failure_message do
          generate_failure_message(incorrectly_denied: @incorrectly_denied)
        end

        match_when_negated do
          false
        end
        failure_message_when_negated do
          "Cannot negate `permit_all_authorizations`: Use `only_permit_authorizations` instead"
        end
      end

      matcher :deny_all_authorizations do
        match do |policy_class|
          @incorrectly_permitted = permitted_authorizations(policy_class)
          @incorrectly_permitted.empty?
        end
        failure_message do
          generate_failure_message(incorrectly_permitted: @incorrectly_permitted)
        end

        match_when_negated do
          false
        end
        failure_message_when_negated do
          "Cannot negate `deny_all_authorizations`: Use `only_permit_authorizations` instead"
        end
      end

      matcher :only_permit_authorizations do |*authorizations_to_permit|
        match do |policy_class|
          permitted_authorizations = permitted_authorizations(policy_class)
          @incorrectly_permitted = permitted_authorizations - authorizations_to_permit
          @incorrectly_denied = authorizations_to_permit - permitted_authorizations
          @incorrectly_permitted.empty? && @incorrectly_denied.empty?
        end
        failure_message do
          generate_failure_message(
            incorrectly_permitted: @incorrectly_permitted,
            incorrectly_denied: @incorrectly_denied
          )
        end

        match_when_negated do
          false
        end
        failure_message_when_negated do
          "Cannot negate `only_permit_authorizations`: Specify all permitted authorizations instead"
        end
      end

      matcher :permit_through_all_filters do
        match do |policy_class|
          @incorrectly_denied = policy_filters - permitted_through_filters(policy_class)
          @incorrectly_denied.empty?
        end
        failure_message do
          generate_failure_message(incorrectly_denied: @incorrectly_denied)
        end

        match_when_negated do
          false
        end
        failure_message_when_negated do
          "Cannot negate `permit_through_all_filters`: Use `only_permit_through_filters` instead"
        end
      end

      matcher :deny_through_all_filters do
        match do |policy_class|
          @incorrectly_permitted = permitted_through_filters(policy_class)
          @incorrectly_permitted.empty?
        end
        failure_message do
          generate_failure_message(incorrectly_permitted: @incorrectly_permitted)
        end

        match_when_negated do
          false
        end
        failure_message_when_negated do
          "Cannot negate `deny_through_all_filters`: Use `only_permit_through_filters` instead"
        end
      end

      matcher :only_permit_through_filters do |*filter_whitelist|
        match do |policy_class|
          permitted_through_filters = permitted_through_filters(policy_class)
          @incorrectly_permitted = permitted_through_filters - filter_whitelist
          @incorrectly_denied = filter_whitelist - permitted_through_filters
          @incorrectly_permitted.empty? && @incorrectly_denied.empty?
        end
        failure_message do
          generate_failure_message(
            incorrectly_denied: @incorrectly_denied,
            incorrectly_permitted: @incorrectly_permitted
          )
        end

        match_when_negated do
          false
        end
        failure_message_when_negated do
          "Cannot negate `only_permit_through_filters`: Specify permitted filters instead"
        end
      end

      def generate_failure_message(incorrectly_permitted: [], incorrectly_denied: [])
        failure_descriptions = []
        unless incorrectly_permitted.empty?
          failure_descriptions << "Incorrectly permitted to #{role}: #{incorrectly_permitted.to_sentence}"
        end
        unless incorrectly_denied.empty?
          failure_descriptions << "Incorrectly denied to #{role}: #{incorrectly_denied.to_sentence}"
        end
        failure_descriptions.join("\n")
      end

      def role
        ::RSpec.current_example.metadata.fetch(:role)
      end

      def current_role
        public_send(role)
      end

      def permitted_authorizations(policy_class)
        policy_instance = policy_class::Authorization.new(current_role, policy_example_resource)
        policy_authorizations.select do |authorization|
          policy_instance.public_send(authorization)
        end
      end

      def permitted_through_filters(policy_class)
        policy_instance = policy_class::Filter.new(current_role, policy_example_scope)
        policy_filters.select do |filter|
          policy_instance.public_send(filter).include?(policy_example_resource)
        end
      end
    end

    module PolicyExampleGroup
      include Moat::RSpec::PolicyMatchers

      def self.included(base_class)
        base_class.metadata[:type] = :policy

        class << base_class
          def roles(*roles, &block)
            roles.each do |role|
              describe(role.to_s, role: role, caller: caller) { instance_eval(&block) }
            end
          end
          alias_method :role, :roles

          def resource(&block)
            fail ArgumentError, "#{__method__} called without a block" unless block
            let(:policy_example_resource) { instance_eval(&block) }
          end

          def scope(&block)
            fail ArgumentError, "#{__method__} called without a block" unless block
            let(:policy_example_scope) { instance_eval(&block) }
          end

          def policy_filters(*filters)
            let(:policy_filters) { filters }
          end

          def policy_authorizations(*authorizations)
            let(:policy_authorizations) { authorizations }
          end
        end

        base_class.class_eval do
          subject { described_class }

          let(:policy_authorizations) do
            fail NotImplementedError, "List of policy_authorizations undefined"
          end

          let(:policy_filters) do
            fail NotImplementedError, "List of policy_filters undefined"
          end

          let(:policy_example_resource) do
            fail NotImplementedError, "A resource has not been defined"
          end

          # a scope that contains at least the resource
          let(:policy_example_scope) do
            if policy_example_resource.class.respond_to?(:all)
              policy_example_resource.class.all
            else
              [policy_example_resource]
            end
          end
        end
      end
    end
  end
end

RSpec.configure do |config|
  config.include(
    Moat::RSpec::PolicyExampleGroup,
    type: :policy,
    file_path: %r{spec/policies}
  )
end