diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ed27808..eec2515b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,9 +2,8 @@ name: Main on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] workflow_dispatch: permissions: @@ -28,74 +27,113 @@ jobs: fail-fast: false matrix: ruby-version: - - '3.1' - - '3.2' - - '3.3' - - 'jruby-9.3.10' # oldest supported jruby - - 'jruby' + - "3.1" + - "3.2" + - "3.3" + - "jruby-9.3.10" # oldest supported jruby + - "jruby" include: # HEAD-versions - - ruby-version: 'head' + - ruby-version: "head" allow-failure: true - - ruby-version: 'jruby-head' + - ruby-version: "jruby-head" allow-failure: true - - ruby-version: 'truffleruby-head' + - ruby-version: "truffleruby-head" allow-failure: true steps: - - uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - rubygems: latest - ruby-version: ${{ matrix.ruby-version }} - bundler-cache: ${{ !startsWith(matrix.ruby-version, 'jruby') }} - - name: Bundler install (JRuby workaround) - if: ${{ startsWith(matrix.ruby-version, 'jruby') }} - run: | - gem install psych - bundle install - - name: Run tests - run: bundle exec rspec + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + rubygems: latest + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: ${{ !startsWith(matrix.ruby-version, 'jruby') }} + - name: Bundler install (JRuby workaround) + if: ${{ startsWith(matrix.ruby-version, 'jruby') }} + run: | + gem install psych + bundle install + - name: Run tests + run: bundle exec rspec test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - rubygems: latest - ruby-version: 'ruby' - bundler-cache: true - - name: "Download cc-test-reporter from codeclimate.com" - run: | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - - name: "Report to Code Climate that we will send a coverage report." - run: ./cc-test-reporter before-build - - name: Run tests - run: bundle exec rspec - env: - COVERAGE: 1 - - name: Upload code coverage to Code Climate - run: | - ./cc-test-reporter after-build \ - --coverage-input-type simplecov \ - ./coverage/.resultset.json + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + rubygems: latest + ruby-version: "ruby" + bundler-cache: true + - name: "Download cc-test-reporter from codeclimate.com" + run: | + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + - name: "Report to Code Climate that we will send a coverage report." + run: ./cc-test-reporter before-build + - name: Run tests + run: bundle exec rspec + env: + COVERAGE: 1 + - name: Upload coverage results + uses: actions/upload-artifact@v4 + with: + include-hidden-files: true + name: coverage-results + path: coverage + retention-days: 1 + - name: Upload code coverage to Code Climate + run: | + ./cc-test-reporter after-build \ + --coverage-input-type simplecov \ + ./coverage/.resultset.json + + coverage-check: + permissions: + checks: write + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Download coverage results + uses: actions/download-artifact@v4 + with: + name: coverage-results + path: coverage + - uses: joshmfrankel/simplecov-check-action@be89e11889202cc59efb14aab2a7091622fa9aad + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + minimum_suite_coverage: 100 + minimum_file_coverage: 100 + coverage_json_path: coverage/simplecov-check-action.json rubocop: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - rubygems: default - ruby-version: 'ruby' - bundler-cache: false - - run: bundle install - - name: Run RuboCop - run: bundle exec rubocop + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + rubygems: default + ruby-version: "ruby" + bundler-cache: false + - run: bundle install + - name: Run RuboCop + run: bundle exec rubocop + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + rubygems: default + ruby-version: "ruby" + bundler-cache: false + - run: bundle install + - run: rake yard required-checks: runs-on: ubuntu-latest @@ -103,10 +141,11 @@ jobs: needs: - test - matrix-test + - docs - rubocop steps: - name: failure if: ${{ failure() || contains(needs.*.result, 'failure') }} run: exit 1 - name: success - run: exit 0 \ No newline at end of file + run: exit 0 diff --git a/.rubocop.yml b/.rubocop.yml index 007b2a28..4bc466f9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -36,6 +36,9 @@ Layout/CaseIndentation: - end IndentOneStep: true +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent + Layout/EndAlignment: EnforcedStyleAlignWith: variable diff --git a/.yardopts b/.yardopts index f03ab316..f723d9be 100644 --- a/.yardopts +++ b/.yardopts @@ -1 +1 @@ ---api public --hide-void-return --markup markdown +--no-private --private --protected --hide-void-return --markup markdown --fail-on-warning diff --git a/lib/generators/pundit/install/install_generator.rb b/lib/generators/pundit/install/install_generator.rb index de87aa94..643b8416 100644 --- a/lib/generators/pundit/install/install_generator.rb +++ b/lib/generators/pundit/install/install_generator.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true module Pundit + # @private module Generators + # @private class InstallGenerator < ::Rails::Generators::Base source_root File.expand_path("templates", __dir__) def copy_application_policy - template "application_policy.rb", "app/policies/application_policy.rb" + template "application_policy.rb.tt", "app/policies/application_policy.rb" end end end diff --git a/lib/generators/pundit/install/templates/application_policy.rb b/lib/generators/pundit/install/templates/application_policy.rb.tt similarity index 100% rename from lib/generators/pundit/install/templates/application_policy.rb rename to lib/generators/pundit/install/templates/application_policy.rb.tt diff --git a/lib/generators/pundit/policy/policy_generator.rb b/lib/generators/pundit/policy/policy_generator.rb index b4734fc5..1084f829 100644 --- a/lib/generators/pundit/policy/policy_generator.rb +++ b/lib/generators/pundit/policy/policy_generator.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true module Pundit + # @private module Generators + # @private class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_policy - template "policy.rb", File.join("app/policies", class_path, "#{file_name}_policy.rb") + template "policy.rb.tt", File.join("app/policies", class_path, "#{file_name}_policy.rb") end hook_for :test_framework diff --git a/lib/generators/pundit/policy/templates/policy.rb b/lib/generators/pundit/policy/templates/policy.rb.tt similarity index 100% rename from lib/generators/pundit/policy/templates/policy.rb rename to lib/generators/pundit/policy/templates/policy.rb.tt diff --git a/lib/generators/rspec/policy_generator.rb b/lib/generators/rspec/policy_generator.rb index 026e60d5..0c85dc89 100644 --- a/lib/generators/rspec/policy_generator.rb +++ b/lib/generators/rspec/policy_generator.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true +# @private module Rspec + # @private module Generators class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_policy_spec - template "policy_spec.rb", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb") + template "policy_spec.rb.tt", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb") end end end diff --git a/lib/generators/rspec/templates/policy_spec.rb b/lib/generators/rspec/templates/policy_spec.rb.tt similarity index 100% rename from lib/generators/rspec/templates/policy_spec.rb rename to lib/generators/rspec/templates/policy_spec.rb.tt diff --git a/lib/generators/test_unit/policy_generator.rb b/lib/generators/test_unit/policy_generator.rb index 89c375fa..cd0175a0 100644 --- a/lib/generators/test_unit/policy_generator.rb +++ b/lib/generators/test_unit/policy_generator.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true +# @private module TestUnit + # @private module Generators class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_policy_test - template "policy_test.rb", File.join("test/policies", class_path, "#{file_name}_policy_test.rb") + template "policy_test.rb.tt", File.join("test/policies", class_path, "#{file_name}_policy_test.rb") end end end diff --git a/lib/generators/test_unit/templates/policy_test.rb b/lib/generators/test_unit/templates/policy_test.rb.tt similarity index 100% rename from lib/generators/test_unit/templates/policy_test.rb rename to lib/generators/test_unit/templates/policy_test.rb.tt diff --git a/lib/pundit.rb b/lib/pundit.rb index a03f4d3a..a7263b39 100644 --- a/lib/pundit.rb +++ b/lib/pundit.rb @@ -22,6 +22,7 @@ module Pundit SUFFIX = "Policy" # @api private + # @private module Generators; end # Error that will be raised when authorization has failed @@ -70,7 +71,7 @@ def self.included(base) end class << self - # @see [Pundit::Context#authorize] + # @see Pundit::Context#authorize def authorize(user, record, query, policy_class: nil, cache: nil) context = if cache Context.new(user: user, policy_cache: cache) @@ -81,22 +82,22 @@ def authorize(user, record, query, policy_class: nil, cache: nil) context.authorize(record, query: query, policy_class: policy_class) end - # @see [Pundit::Context#policy_scope] + # @see Pundit::Context#policy_scope def policy_scope(user, *args, **kwargs, &block) Context.new(user: user).policy_scope(*args, **kwargs, &block) end - # @see [Pundit::Context#policy_scope!] + # @see Pundit::Context#policy_scope! def policy_scope!(user, *args, **kwargs, &block) Context.new(user: user).policy_scope!(*args, **kwargs, &block) end - # @see [Pundit::Context#policy] + # @see Pundit::Context#policy def policy(user, *args, **kwargs, &block) Context.new(user: user).policy(*args, **kwargs, &block) end - # @see [Pundit::Context#policy!] + # @see Pundit::Context#policy! def policy!(user, *args, **kwargs, &block) Context.new(user: user).policy!(*args, **kwargs, &block) end diff --git a/lib/pundit/authorization.rb b/lib/pundit/authorization.rb index bc4cc4f4..c0a77834 100644 --- a/lib/pundit/authorization.rb +++ b/lib/pundit/authorization.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true module Pundit + # Pundit DSL to include in your controllers to provide authorization helpers. + # + # @example + # class ApplicationController < ActionController::Base + # include Pundit::Authorization + # end + # @see #pundit + # @api public module Authorization extend ActiveSupport::Concern @@ -15,7 +23,13 @@ module Authorization protected - # @return [Pundit::Context] a new instance of {Pundit::Context} with the current user + # An instance of {Pundit::Context} initialized with the current user. + # + # @note this method is memoized and will return the same instance during the request. + # @api public + # @return [Pundit::Context] + # @see #pundit_user + # @see #policies def pundit @pundit ||= Pundit::Context.new( user: pundit_user, @@ -23,39 +37,16 @@ def pundit ) end - # @return [Boolean] whether authorization has been performed, i.e. whether - # one {#authorize} or {#skip_authorization} has been called - def pundit_policy_authorized? - !!@_pundit_policy_authorized - end - - # @return [Boolean] whether policy scoping has been performed, i.e. whether - # one {#policy_scope} or {#skip_policy_scope} has been called - def pundit_policy_scoped? - !!@_pundit_policy_scoped - end - - # Raises an error if authorization has not been performed, usually used as an - # `after_action` filter to prevent programmer error in forgetting to call - # {#authorize} or {#skip_authorization}. + # Hook method which allows customizing which user is passed to policies and + # scopes initialized by {#authorize}, {#policy} and {#policy_scope}. # - # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used - # @raise [AuthorizationNotPerformedError] if authorization has not been performed - # @return [void] - def verify_authorized - raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized? + # @see https://github.com/varvet/pundit#customize-pundit-user + # @return [Object] the user object to be used with pundit + def pundit_user + current_user end - # Raises an error if policy scoping has not been performed, usually used as an - # `after_action` filter to prevent programmer error in forgetting to call - # {#policy_scope} or {#skip_policy_scope} in index actions. - # - # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used - # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed - # @return [void] - def verify_policy_scoped - raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped? - end + # @!group Policies # Retrieves the policy for the given record, initializing it with the record # and current user and finally throwing an error if the user is not @@ -66,7 +57,9 @@ def verify_policy_scoped # If omitted then this defaults to the Rails controller action name. # @param policy_class [Class] the policy class we want to force use of # @raise [NotAuthorizedError] if the given query method returned false - # @return [Object] Always returns the passed object record + # @return [record] Always returns the passed object record + # @see Pundit::Context#authorize + # @see #verify_authorized def authorize(record, query = nil, policy_class: nil) query ||= "#{action_name}?" @@ -79,29 +72,45 @@ def authorize(record, query = nil, policy_class: nil) # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used # @return [void] + # @see #verify_authorized def skip_authorization @_pundit_policy_authorized = :skipped end - # Allow this action not to perform policy scoping. + # @return [Boolean] wether or not authorization has been performed + # @see #authorize + # @see #skip_authorization + def pundit_policy_authorized? + !!@_pundit_policy_authorized + end + + # Raises an error if authorization has not been performed. + # + # Usually used as an `after_action` filter to prevent programmer error in + # forgetting to call {#authorize} or {#skip_authorization}. # # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used + # @raise [AuthorizationNotPerformedError] if authorization has not been performed # @return [void] - def skip_policy_scope - @_pundit_policy_scoped = :skipped + # @see #authorize + # @see #skip_authorization + def verify_authorized + raise AuthorizationNotPerformedError, self.class unless pundit_policy_authorized? end - # Retrieves the policy scope for the given record. + # rubocop:disable Naming/MemoizedInstanceVariableName + + # Cache of policies. You should not rely on this method. # - # @see https://github.com/varvet/pundit#scopes - # @param scope [Object] the object we're retrieving the policy scope for - # @param policy_scope_class [Class] the policy scope class we want to force use of - # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope - def policy_scope(scope, policy_scope_class: nil) - @_pundit_policy_scoped = true - policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope) + # @api private + def policies + @_pundit_policies ||= {} end + # rubocop:enable Naming/MemoizedInstanceVariableName + + # @!endgroup + # Retrieves the policy for the given record. # # @see https://github.com/varvet/pundit#policies @@ -111,11 +120,78 @@ def policy(record) pundit.policy!(record) end - # Retrieves a set of permitted attributes from the policy by instantiating - # the policy class for the given record and calling `permitted_attributes` on - # it, or `permitted_attributes_for_{action}` if `action` is defined. It then infers - # what key the record should have in the params hash and retrieves the - # permitted attributes from the params hash under that key. + # @!group Policy Scopes + + # Retrieves the policy scope for the given record. + # + # @see https://github.com/varvet/pundit#scopes + # @param scope [Object] the object we're retrieving the policy scope for + # @param policy_scope_class [#resolve] the policy scope class we want to force use of + # @return [#resolve, nil] instance of scope class which can resolve to a scope + def policy_scope(scope, policy_scope_class: nil) + @_pundit_policy_scoped = true + policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope) + end + + # Allow this action not to perform policy scoping. + # + # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used + # @return [void] + # @see #verify_policy_scoped + def skip_policy_scope + @_pundit_policy_scoped = :skipped + end + + # @return [Boolean] wether or not policy scoping has been performed + # @see #policy_scope + # @see #skip_policy_scope + def pundit_policy_scoped? + !!@_pundit_policy_scoped + end + + # Raises an error if policy scoping has not been performed. + # + # Usually used as an `after_action` filter to prevent programmer error in + # forgetting to call {#policy_scope} or {#skip_policy_scope} in index + # actions. + # + # @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used + # @raise [AuthorizationNotPerformedError] if policy scoping has not been performed + # @return [void] + # @see #policy_scope + # @see #skip_policy_scope + def verify_policy_scoped + raise PolicyScopingNotPerformedError, self.class unless pundit_policy_scoped? + end + + # rubocop:disable Naming/MemoizedInstanceVariableName + + # Cache of policy scope. You should not rely on this method. + # + # @api private + def policy_scopes + @_pundit_policy_scopes ||= {} + end + + # rubocop:enable Naming/MemoizedInstanceVariableName + + # @api private + def pundit_policy_scope(scope) + policy_scopes[scope] ||= pundit.policy_scope!(scope) + end + private :pundit_policy_scope + + # @!endgroup + + # @!group Strong Parameters + + # Retrieves a set of permitted attributes from the policy. + # + # Done by instantiating the policy class for the given record and calling + # `permitted_attributes` on it, or `permitted_attributes_for_{action}` if + # `action` is defined. It then infers what key the record should have in the + # params hash and retrieves the permitted attributes from the params hash + # under that key. # # @see https://github.com/varvet/pundit#strong-parameters # @param record [Object] the object we're retrieving permitted attributes for @@ -140,37 +216,6 @@ def pundit_params_for(record) params.require(PolicyFinder.new(record).param_key) end - # Cache of policies. You should not rely on this method. - # - # @api private - # rubocop:disable Naming/MemoizedInstanceVariableName - def policies - @_pundit_policies ||= {} - end - # rubocop:enable Naming/MemoizedInstanceVariableName - - # Cache of policy scope. You should not rely on this method. - # - # @api private - # rubocop:disable Naming/MemoizedInstanceVariableName - def policy_scopes - @_pundit_policy_scopes ||= {} - end - # rubocop:enable Naming/MemoizedInstanceVariableName - - # Hook method which allows customizing which user is passed to policies and - # scopes initialized by {#authorize}, {#policy} and {#policy_scope}. - # - # @see https://github.com/varvet/pundit#customize-pundit-user - # @return [Object] the user object to be used with pundit - def pundit_user - current_user - end - - private - - def pundit_policy_scope(scope) - policy_scopes[scope] ||= pundit.policy_scope!(scope) - end + # @!endgroup end end diff --git a/lib/pundit/cache_store/legacy_store.rb b/lib/pundit/cache_store/legacy_store.rb index d8ca6e9b..bf41c5c4 100644 --- a/lib/pundit/cache_store/legacy_store.rb +++ b/lib/pundit/cache_store/legacy_store.rb @@ -2,6 +2,10 @@ module Pundit module CacheStore + # A cache store that uses only the record as a cache key, and ignores the user. + # + # The original cache mechanism used by Pundit. + # # @api private class LegacyStore def initialize(hash = {}) diff --git a/lib/pundit/cache_store/null_store.rb b/lib/pundit/cache_store/null_store.rb index 60f484e0..a75a23ce 100644 --- a/lib/pundit/cache_store/null_store.rb +++ b/lib/pundit/cache_store/null_store.rb @@ -2,11 +2,17 @@ module Pundit module CacheStore + # A cache store that does not cache anything. + # + # Use `NullStore.instance` to get the singleton instance, it is thread-safe. + # + # @see Pundit::Context#initialize # @api private class NullStore @instance = new class << self + # @return [NullStore] the singleton instance attr_reader :instance end diff --git a/lib/pundit/context.rb b/lib/pundit/context.rb index a5f86716..d906f74b 100644 --- a/lib/pundit/context.rb +++ b/lib/pundit/context.rb @@ -1,22 +1,52 @@ # frozen_string_literal: true module Pundit + # {Pundit::Context} is intended to be created once per request and user, and + # it is then used to perform authorization checks throughout the request. + # + # @example Using Sinatra + # helpers do + # def current_user = ... + # + # def pundit + # @pundit ||= Pundit::Context.new(user: current_user) + # end + # end + # + # get "/posts/:id" do |id| + # pundit.authorize(Post.find(id), query: :show?) + # end + # + # @example Using [Roda](https://roda.jeremyevans.net/index.html) + # route do |r| + # context = Pundit::Context.new(user:) + # + # r.get "posts", Integer do |id| + # context.authorize(Post.find(id), query: :show?) + # end + # end class Context + # @see Pundit::Authorization#pundit + # @param user later passed to policies and scopes + # @param policy_cache [#fetch] cache store for policies (see e.g. {CacheStore::NullStore}) def initialize(user:, policy_cache: CacheStore::NullStore.instance) @user = user @policy_cache = policy_cache end + # @api public + # @see #initialize attr_reader :user # @api private attr_reader :policy_cache + # @!group Policies + # Retrieves the policy for the given record, initializing it with the # record and user and finally throwing an error if the user is not # authorized to perform the given action. # - # @param user [Object] the user that initiated the action # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`) # @param policy_class [Class] the policy class we want to force use of @@ -35,10 +65,34 @@ def authorize(possibly_namespaced_record, query:, policy_class:) record end + # Retrieves the policy for the given record. + # + # @see https://github.com/varvet/pundit#policies + # @param record [Object] the object we're retrieving the policy for + # @raise [InvalidConstructorError] if the policy constructor called incorrectly + # @return [Object, nil] instance of policy class with query methods + def policy(record) + cached_find(record, &:policy) + end + + # Retrieves the policy for the given record, or raises if not found. + # + # @see https://github.com/varvet/pundit#policies + # @param record [Object] the object we're retrieving the policy for + # @raise [NotDefinedError] if the policy cannot be found + # @raise [InvalidConstructorError] if the policy constructor called incorrectly + # @return [Object] instance of policy class with query methods + def policy!(record) + cached_find(record, &:policy!) + end + + # @!endgroup + + # @!group Scopes + # Retrieves the policy scope for the given record. # # @see https://github.com/varvet/pundit#scopes - # @param user [Object] the user that initiated the action # @param scope [Object] the object we're retrieving the policy scope for # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope @@ -58,14 +112,12 @@ def policy_scope(scope) # Retrieves the policy scope for the given record. Raises if not found. # # @see https://github.com/varvet/pundit#scopes - # @param user [Object] the user that initiated the action # @param scope [Object] the object we're retrieving the policy scope for # @raise [NotDefinedError] if the policy scope cannot be found # @raise [InvalidConstructorError] if the policy constructor called incorrectly # @return [Scope{#resolve}] instance of scope class which can resolve to a scope def policy_scope!(scope) policy_scope_class = policy_finder(scope).scope! - return unless policy_scope_class begin policy_scope = policy_scope_class.new(user, pundit_model(scope)) @@ -76,31 +128,12 @@ def policy_scope!(scope) policy_scope.resolve end - # Retrieves the policy for the given record. - # - # @see https://github.com/varvet/pundit#policies - # @param user [Object] the user that initiated the action - # @param record [Object] the object we're retrieving the policy for - # @raise [InvalidConstructorError] if the policy constructor called incorrectly - # @return [Object, nil] instance of policy class with query methods - def policy(record) - cached_find(record, &:policy) - end - - # Retrieves the policy for the given record. Raises if not found. - # - # @see https://github.com/varvet/pundit#policies - # @param user [Object] the user that initiated the action - # @param record [Object] the object we're retrieving the policy for - # @raise [NotDefinedError] if the policy cannot be found - # @raise [InvalidConstructorError] if the policy constructor called incorrectly - # @return [Object] instance of policy class with query methods - def policy!(record) - cached_find(record, &:policy!) - end + # @!endgroup private + # @!group Private Helpers + def cached_find(record) policy_cache.fetch(user: user, record: record) do klass = yield policy_finder(record) diff --git a/lib/pundit/rspec.rb b/lib/pundit/rspec.rb index 1244031e..fb44ba60 100644 --- a/lib/pundit/rspec.rb +++ b/lib/pundit/rspec.rb @@ -5,9 +5,20 @@ module RSpec module Matchers extend ::RSpec::Matchers::DSL + # @!method description=(description) class << self + # Used to build a suitable description for the Pundit `permit` matcher. + # @api public + # @param value [String, Proc] + # @example + # Pundit::RSpec::Matchers.description = ->(user, record) do + # "permit user with role #{user.role} to access record with ID #{record.id}" + # end attr_writer :description + # Used to retrieve a suitable description for the Pundit `permit` matcher. + # @api private + # @private def description(user, record) return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call) @@ -32,15 +43,21 @@ def description(user, record) end failure_message_proc = lambda do |policy| - was_were = @violating_permissions.count > 1 ? "were" : "was" "Expected #{policy} to grant #{permissions.to_sentence} on " \ - "#{record} but #{@violating_permissions.to_sentence} #{was_were} not granted" + "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} not granted" end failure_message_when_negated_proc = lambda do |policy| - was_were = @violating_permissions.count > 1 ? "were" : "was" "Expected #{policy} not to grant #{permissions.to_sentence} on " \ - "#{record} but #{@violating_permissions.to_sentence} #{was_were} granted" + "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} granted" + end + + def was_or_were + if @violating_permissions.count > 1 + "were" + else + "was" + end end description do @@ -53,21 +70,53 @@ def description(user, record) failure_message(&failure_message_proc) failure_message_when_negated(&failure_message_when_negated_proc) else + # :nocov: + # Compatibility with RSpec < 3.0, released 2014-06-01. match_for_should(&match_proc) match_for_should_not(&match_when_negated_proc) failure_message_for_should(&failure_message_proc) failure_message_for_should_not(&failure_message_when_negated_proc) + # :nocov: + end + + if ::RSpec.respond_to?(:current_example) + def current_example + ::RSpec.current_example + end + else + # :nocov: + # Compatibility with RSpec < 3.0, released 2014-06-01. + def current_example + example + end + # :nocov: end def permissions - current_example = ::RSpec.respond_to?(:current_example) ? ::RSpec.current_example : example current_example.metadata[:permissions] end end # rubocop:enable Metrics/BlockLength end + # Mixed in to all policy example groups to provide a DSL. module DSL + # @example + # describe PostPolicy do + # permissions :show?, :update? do + # it { is_expected.to permit(user, own_post) } + # end + # end + # + # @example focused example group + # describe PostPolicy do + # permissions :show?, :update?, :focus do + # it { is_expected.to permit(user, own_post) } + # end + # end + # + # @param list [Symbol, Array] a permission to describe + # @return [void] def permissions(*list, &block) metadata = { permissions: list, caller: caller } @@ -81,6 +130,9 @@ def permissions(*list, &block) end end + # Mixed in to all policy example groups. + # + # @private not useful module PolicyExampleGroup include Pundit::RSpec::Matchers diff --git a/pundit.gemspec b/pundit.gemspec index 2a45e4b0..18a97bf7 100644 --- a/pundit.gemspec +++ b/pundit.gemspec @@ -32,4 +32,5 @@ Gem::Specification.new do |gem| gem.add_development_dependency "rubocop" gem.add_development_dependency "simplecov", ">= 0.17.0" gem.add_development_dependency "yard" + gem.add_development_dependency "zeitwerk" end diff --git a/spec/dsl_spec.rb b/spec/dsl_spec.rb deleted file mode 100644 index 97c936a2..00000000 --- a/spec/dsl_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "Pundit RSpec DSL" do - let(:fake_rspec) do - double = class_double(RSpec::ExampleGroups) - double.extend(::Pundit::RSpec::DSL) - double - end - let(:block) { proc { "block content" } } - - it "calls describe with the correct metadata and without :focus" do - expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array) } - expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block| - expect(block.call).to eq("block content") - end - - fake_rspec.permissions(:item1, :item2, &block) - end - - it "calls describe with the correct metadata and with :focus" do - expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array), focus: true } - expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block| - expect(block.call).to eq("block content") - end - - fake_rspec.permissions(:item1, :item2, :focus, &block) - end -end diff --git a/spec/policy_finder_spec.rb b/spec/policy_finder_spec.rb index 11eb25a8..c387b024 100644 --- a/spec/policy_finder_spec.rb +++ b/spec/policy_finder_spec.rb @@ -2,7 +2,6 @@ require "spec_helper" -class Foo; end RSpec.describe Pundit::PolicyFinder do let(:user) { double } let(:post) { Post.new(user) } diff --git a/spec/pundit/helper_spec.rb b/spec/pundit/helper_spec.rb new file mode 100644 index 00000000..cfb94555 --- /dev/null +++ b/spec/pundit/helper_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Pundit::Helper do + let(:user) { double } + let(:controller) { Controller.new(user, "update", double) } + let(:view) { Controller::View.new(controller) } + + describe "#policy_scope" do + it "doesn't flip pundit_policy_scoped?" do + scoped = view.policy_scope(Post) + + expect(scoped).to be(Post.published) + expect(controller).not_to be_pundit_policy_scoped + end + end +end diff --git a/spec/pundit_spec.rb b/spec/pundit_spec.rb index bcec8633..65d13b77 100644 --- a/spec/pundit_spec.rb +++ b/spec/pundit_spec.rb @@ -43,15 +43,6 @@ expect(Pundit.authorize(user, Post, :show?)).to eq(Post) end - it "can be given a different policy class" do - expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy - end - - it "can be given a different policy class using namespaces" do - expect(PublicationPolicy).to receive(:new).with(user, comment).and_call_original - expect(Pundit.authorize(user, [:project, comment], :create?, policy_class: PublicationPolicy)).to be_truthy - end - it "works with anonymous class policies" do expect(Pundit.authorize(user, article_tag, :show?)).to be_truthy expect { Pundit.authorize(user, article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError) @@ -111,6 +102,29 @@ Pundit.authorize(user, wiki, :update?) end.to raise_error(Pundit::InvalidConstructorError, "Invalid # constructor is called") end + + context "when passed a policy class" do + it "uses the passed policy class" do + expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy + end + + # This is documenting past behaviour. + it "doesn't cache the policy class" do + cache = {} + + expect do + Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache) + Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache) + end.to change { PublicationPolicy.instances }.by(2) + end + end + + context "when passed a policy class while simultaenously passing a namespace" do + it "uses the passed policy class" do + expect(PublicationPolicy).to receive(:new).with(user, comment).and_call_original + expect(Pundit.authorize(user, [:project, comment], :create?, policy_class: PublicationPolicy)).to be_truthy + end + end end describe ".policy_scope" do @@ -154,8 +168,8 @@ it "raises an original error with a policy scope that contains error" do expect do - Pundit.policy_scope(user, Thread) - end.to raise_error(ArgumentError) + Pundit.policy_scope(user, DefaultScopeContainsError) + end.to raise_error(RuntimeError, "This is an arbitrary error that should bubble up") end end diff --git a/spec/rspec_dsl_spec.rb b/spec/rspec_dsl_spec.rb new file mode 100644 index 00000000..c1f65dae --- /dev/null +++ b/spec/rspec_dsl_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Pundit RSpec DSL" do + include Pundit::RSpec::PolicyExampleGroup + + let(:fake_rspec) do + double = class_double(RSpec::ExampleGroups) + double.extend(::Pundit::RSpec::DSL) + double + end + let(:block) { proc { "block content" } } + + let(:user) { double } + let(:other_user) { double } + let(:post) { Post.new(user) } + let(:policy) { PostPolicy } + + it "calls describe with the correct metadata and without :focus" do + expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array) } + expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block| + expect(block.call).to eq("block content") + end + + fake_rspec.permissions(:item1, :item2, &block) + end + + it "calls describe with the correct metadata and with :focus" do + expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array), focus: true } + expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block| + expect(block.call).to eq("block content") + end + + fake_rspec.permissions(:item1, :item2, :focus, &block) + end + + describe "#permit" do + permissions :edit?, :update? do + it "succeeds when action is permitted" do + expect(policy).to permit(user, post) + end + + context "when it fails" do + it "fails with a descriptive error message" do + expect do + expect(policy).to permit(other_user, post) + end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip) + Expected PostPolicy to grant edit? and update? on Post but edit? and update? were not granted + MSG + end + end + + context "when negated" do + it "succeeds when action is not permitted" do + expect(policy).not_to permit(other_user, post) + end + + context "when it fails" do + it "fails with a descriptive error message" do + expect do + expect(policy).not_to permit(user, post) + end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip) + Expected PostPolicy not to grant edit? and update? on Post but edit? and update? were granted + MSG + end + end + end + end + end +end diff --git a/spec/simple_cov_check_action_formatter.rb b/spec/simple_cov_check_action_formatter.rb new file mode 100644 index 00000000..76f694ce --- /dev/null +++ b/spec/simple_cov_check_action_formatter.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "simplecov" +require "json" + +class SimpleCovCheckActionFormatter + SourceFile = Data.define(:source_file) do + def covered_strength = source_file.covered_strength + def covered_percent = source_file.covered_percent + + def to_json(*args) + { + filename: source_file.filename, + covered_percent: covered_percent.nan? ? 0.0 : covered_percent, + coverage: source_file.coverage_data, + covered_strength: covered_strength.nan? ? 0.0 : covered_strength, + covered_lines: source_file.covered_lines.count, + lines_of_code: source_file.lines_of_code + }.to_json(*args) + end + end + + Result = Data.define(:result) do + def included?(source_file) = result.filenames.include?(source_file.filename) + + def files + result.files.filter_map do |source_file| + next unless result.filenames.include? source_file.filename + + SourceFile.new(source_file) + end + end + + def to_json(*args) # rubocop:disable Metrics/AbcSize + { + timestamp: result.created_at.to_i, + command_name: result.command_name, + files: files, + metrics: { + covered_percent: result.covered_percent, + covered_strength: result.covered_strength.nan? ? 0.0 : result.covered_strength, + covered_lines: result.covered_lines, + total_lines: result.total_lines + } + }.to_json(*args) + end + end + + FormatterWithOptions = Data.define(:formatter) do + def new = formatter + end + + class << self + def with_options(...) + FormatterWithOptions.new(new(...)) + end + end + + def initialize(output_filename: "coverage.json", output_directory: SimpleCov.coverage_path) + @output_filename = output_filename + @output_directory = output_directory + end + + attr_reader :output_filename, :output_directory + + def output_filepath = File.join(output_directory, output_filename) + + def format(result_data) + result = Result.new(result_data) + json = JSON.generate(result) + File.write(output_filepath, json) + puts output_message(result_data) + json + end + + def output_message(result) + "Coverage report generated for #{result.command_name} to #{output_filepath}. #{result.covered_lines} / #{result.total_lines} LOC (#{result.covered_percent.round(2)}%) covered." # rubocop:disable Layout/LineLength + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ff70ac0d..6d63c514 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,8 +2,19 @@ if ENV["COVERAGE"] require "simplecov" + require "simplecov_json_formatter" + require_relative "simple_cov_check_action_formatter" + SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::JSONFormatter, + SimpleCovCheckActionFormatter.with_options( + output_filename: "simplecov-check-action.json" + ) + ]) SimpleCov.start do add_filter "/spec/" + enable_coverage :branch + primary_coverage :branch end end @@ -18,335 +29,11 @@ require "active_model/naming" require "action_controller/metal/strong_parameters" -module InstanceTracking - module ClassMethods - def instances - @instances || 0 - end - - attr_writer :instances - end - - def self.prepended(other) - other.extend(ClassMethods) - end - - def initialize(*args, **kwargs, &block) - self.class.instances += 1 - super(*args, **kwargs, &block) - end -end - -class BasePolicy - prepend InstanceTracking - - class BaseScope - prepend InstanceTracking - - def initialize(user, scope) - @user = user - @scope = scope - end - - attr_reader :user, :scope - end - - def initialize(user, record) - @user = user - @record = record - end - - attr_reader :user, :record -end - -class PostPolicy < BasePolicy - class Scope < BaseScope - def resolve - scope.published - end - end - - alias post record - - def update? - post.user == user - end - - def destroy? - false - end - - def show? - true - end - - def permitted_attributes - if post.user == user - %i[title votes] - else - [:votes] - end - end - - def permitted_attributes_for_revise - [:body] - end -end - -class Post - def initialize(user = nil) - @user = user - end - - attr_reader :user - - def self.published - :published - end - - def self.read - :read - end - - def to_s - "Post" - end - - def inspect - "#" - end -end - -module Customer - class Post < ::Post - def model_name - OpenStruct.new(param_key: "customer_post") - end - - def self.policy_class - PostPolicy - end - end -end - -class CommentScope - attr_reader :original_object - - def initialize(original_object) - @original_object = original_object - end - - def ==(other) - original_object == other.original_object - end -end - -class CommentPolicy < BasePolicy - class Scope < BaseScope - def resolve - CommentScope.new(scope) - end - end - - alias comment record -end - -class PublicationPolicy < BasePolicy - class Scope < BaseScope - def resolve - scope.published - end - end - - def create? - true - end -end - -class Comment - extend ActiveModel::Naming -end - -class CommentsRelation - def initialize(empty: false) - @empty = empty - end - - def blank? - @empty - end - - def self.model_name - Comment.model_name - end -end - -class Article; end - -class BlogPolicy < BasePolicy - alias blog record -end - -class Blog; end - -class ArtificialBlog < Blog - def self.policy_class - BlogPolicy - end -end - -class ArticleTagOtherNamePolicy < BasePolicy - def show? - true - end - - def destroy? - false - end - - alias tag record -end - -class ArticleTag - def self.policy_class - ArticleTagOtherNamePolicy - end -end - -class CriteriaPolicy < BasePolicy - alias criteria record -end - -module Project - class CommentPolicy < BasePolicy - class Scope < BaseScope - def resolve - scope - end - end - - def update? - true - end - - alias comment record - end - - class CriteriaPolicy < BasePolicy - alias criteria record - end - - class PostPolicy < BasePolicy - class Scope < BaseScope - def resolve - scope.read - end - end - - alias post record - end - - module Admin - class CommentPolicy < BasePolicy - def update? - true - end - - def destroy? - false - end - end - end -end - -class DenierPolicy < BasePolicy - def update? - false - end -end - -class Controller - include Pundit::Authorization - # Mark protected methods public so they may be called in test - # rubocop:disable Style/AccessModifierDeclarations - public(*Pundit::Authorization.protected_instance_methods) - # rubocop:enable Style/AccessModifierDeclarations - - attr_reader :current_user, :action_name, :params - - def initialize(current_user, action_name, params) - @current_user = current_user - @action_name = action_name - @params = params - end -end - -class NilClassPolicy < BasePolicy - class Scope - def initialize(*) - raise Pundit::NotDefinedError, "Cannot scope NilClass" - end - end - - def show? - false - end - - def destroy? - false - end -end - -class Wiki; end - -class WikiPolicy - class Scope - # deliberate typo method - def initalize; end - end -end - -class Thread - def self.all; end -end - -class ThreadPolicy < BasePolicy - class Scope < BaseScope - def resolve - # deliberate wrong usage of the method - scope.all(:unvalid, :parameters) - end - end -end - -class PostFourFiveSix - def initialize(user) - @user = user - end - - attr_reader(:user) -end - -class CommentFourFiveSix; extend ActiveModel::Naming; end - -module ProjectOneTwoThree - class CommentFourFiveSixPolicy < BasePolicy; end - - class CriteriaFourFiveSixPolicy < BasePolicy; end - - class PostFourFiveSixPolicy < BasePolicy; end - - class TagFourFiveSix - def initialize(user) - @user = user - end - - attr_reader(:user) - end - - class TagFourFiveSixPolicy < BasePolicy; end - - class AvatarFourFiveSix; extend ActiveModel::Naming; end - - class AvatarFourFiveSixPolicy < BasePolicy; end -end +# Load all supporting files: models, policies, etc. +require "zeitwerk" +loader = Zeitwerk::Loader.new +loader.push_dir(File.expand_path("support/models", __dir__)) +loader.push_dir(File.expand_path("support/policies", __dir__)) +loader.push_dir(File.expand_path("support/lib", __dir__)) +loader.setup +loader.eager_load diff --git a/spec/support/lib/controller.rb b/spec/support/lib/controller.rb new file mode 100644 index 00000000..4077de25 --- /dev/null +++ b/spec/support/lib/controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class Controller + attr_reader :current_user, :action_name, :params + + class View + def initialize(controller) + @controller = controller + end + + attr_reader :controller + end + + class << self + def helper(mod) + View.include(mod) + end + + def helper_method(method) + View.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, **kwargs, &block) + controller.send(:#{method}, *args, **kwargs, &block) + end + RUBY + end + end + + include Pundit::Authorization + # Mark protected methods public so they may be called in test + # rubocop:disable Style/AccessModifierDeclarations + public(*Pundit::Authorization.protected_instance_methods) + # rubocop:enable Style/AccessModifierDeclarations + + def initialize(current_user, action_name, params) + @current_user = current_user + @action_name = action_name + @params = params + end +end diff --git a/spec/support/lib/instance_tracking.rb b/spec/support/lib/instance_tracking.rb new file mode 100644 index 00000000..bf7cb763 --- /dev/null +++ b/spec/support/lib/instance_tracking.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module InstanceTracking + module ClassMethods + def instances + @instances || 0 + end + + attr_writer :instances + end + + def self.prepended(other) + other.extend(ClassMethods) + end + + def initialize(*args, **kwargs, &block) + self.class.instances += 1 + super(*args, **kwargs, &block) + end +end diff --git a/spec/support/models/article.rb b/spec/support/models/article.rb new file mode 100644 index 00000000..24252378 --- /dev/null +++ b/spec/support/models/article.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Article +end diff --git a/spec/support/models/article_tag.rb b/spec/support/models/article_tag.rb new file mode 100644 index 00000000..5ac8a4b8 --- /dev/null +++ b/spec/support/models/article_tag.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ArticleTag + def self.policy_class + ArticleTagOtherNamePolicy + end +end diff --git a/spec/support/models/artificial_blog.rb b/spec/support/models/artificial_blog.rb new file mode 100644 index 00000000..f554cf3a --- /dev/null +++ b/spec/support/models/artificial_blog.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ArtificialBlog < Blog + def self.policy_class + BlogPolicy + end +end diff --git a/spec/support/models/blog.rb b/spec/support/models/blog.rb new file mode 100644 index 00000000..8d9d1385 --- /dev/null +++ b/spec/support/models/blog.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Blog +end diff --git a/spec/support/models/comment.rb b/spec/support/models/comment.rb new file mode 100644 index 00000000..5479a7ea --- /dev/null +++ b/spec/support/models/comment.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Comment + extend ActiveModel::Naming +end diff --git a/spec/support/models/comment_four_five_six.rb b/spec/support/models/comment_four_five_six.rb new file mode 100644 index 00000000..74912d19 --- /dev/null +++ b/spec/support/models/comment_four_five_six.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CommentFourFiveSix + extend ActiveModel::Naming +end diff --git a/spec/support/models/comment_scope.rb b/spec/support/models/comment_scope.rb new file mode 100644 index 00000000..b5bee646 --- /dev/null +++ b/spec/support/models/comment_scope.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CommentScope + attr_reader :original_object + + def initialize(original_object) + @original_object = original_object + end + + def ==(other) + original_object == other.original_object + end +end diff --git a/spec/support/models/comments_relation.rb b/spec/support/models/comments_relation.rb new file mode 100644 index 00000000..081f9174 --- /dev/null +++ b/spec/support/models/comments_relation.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CommentsRelation + def initialize(empty: false) + @empty = empty + end + + def blank? + @empty + end + + def self.model_name + Comment.model_name + end +end diff --git a/spec/support/models/customer/post.rb b/spec/support/models/customer/post.rb new file mode 100644 index 00000000..0b08c654 --- /dev/null +++ b/spec/support/models/customer/post.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Customer + class Post < ::Post + def model_name + OpenStruct.new(param_key: "customer_post") + end + + def self.policy_class + PostPolicy + end + end +end diff --git a/spec/support/models/default_scope_contains_error.rb b/spec/support/models/default_scope_contains_error.rb new file mode 100644 index 00000000..d7a49d63 --- /dev/null +++ b/spec/support/models/default_scope_contains_error.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class DefaultScopeContainsError + def self.all; end +end diff --git a/spec/support/models/foo.rb b/spec/support/models/foo.rb new file mode 100644 index 00000000..ce2a8416 --- /dev/null +++ b/spec/support/models/foo.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Foo +end diff --git a/spec/support/models/post.rb b/spec/support/models/post.rb new file mode 100644 index 00000000..a2eb5ff0 --- /dev/null +++ b/spec/support/models/post.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Post + def initialize(user = nil) + @user = user + end + + attr_reader :user + + def self.published + :published + end + + def self.read + :read + end + + def to_s + "Post" + end + + def inspect + "#" + end +end diff --git a/spec/support/models/post_four_five_six.rb b/spec/support/models/post_four_five_six.rb new file mode 100644 index 00000000..db0852bd --- /dev/null +++ b/spec/support/models/post_four_five_six.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class PostFourFiveSix + def initialize(user) + @user = user + end + + attr_reader(:user) +end diff --git a/spec/support/models/project_one_two_three/avatar_four_five_six.rb b/spec/support/models/project_one_two_three/avatar_four_five_six.rb new file mode 100644 index 00000000..db91f851 --- /dev/null +++ b/spec/support/models/project_one_two_three/avatar_four_five_six.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ProjectOneTwoThree + class AvatarFourFiveSix + extend ActiveModel::Naming + end +end diff --git a/spec/support/models/project_one_two_three/tag_four_five_six.rb b/spec/support/models/project_one_two_three/tag_four_five_six.rb new file mode 100644 index 00000000..8e039410 --- /dev/null +++ b/spec/support/models/project_one_two_three/tag_four_five_six.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ProjectOneTwoThree + class TagFourFiveSix + def initialize(user) + @user = user + end + + attr_reader(:user) + end +end diff --git a/spec/support/models/wiki.rb b/spec/support/models/wiki.rb new file mode 100644 index 00000000..4dd9bbb2 --- /dev/null +++ b/spec/support/models/wiki.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Wiki +end diff --git a/spec/support/policies/article_tag_other_name_policy.rb b/spec/support/policies/article_tag_other_name_policy.rb new file mode 100644 index 00000000..ef84559b --- /dev/null +++ b/spec/support/policies/article_tag_other_name_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ArticleTagOtherNamePolicy < BasePolicy + def show? + true + end + + def destroy? + false + end + + alias tag record +end diff --git a/spec/support/policies/base_policy.rb b/spec/support/policies/base_policy.rb new file mode 100644 index 00000000..e63f0cfb --- /dev/null +++ b/spec/support/policies/base_policy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class BasePolicy + prepend InstanceTracking + + class BaseScope + prepend InstanceTracking + + def initialize(user, scope) + @user = user + @scope = scope + end + + attr_reader :user, :scope + end + + def initialize(user, record) + @user = user + @record = record + end + + attr_reader :user, :record +end diff --git a/spec/support/policies/blog_policy.rb b/spec/support/policies/blog_policy.rb new file mode 100644 index 00000000..c1412644 --- /dev/null +++ b/spec/support/policies/blog_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BlogPolicy < BasePolicy + alias blog record +end diff --git a/spec/support/policies/comment_policy.rb b/spec/support/policies/comment_policy.rb new file mode 100644 index 00000000..1ea45695 --- /dev/null +++ b/spec/support/policies/comment_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CommentPolicy < BasePolicy + class Scope < BaseScope + def resolve + CommentScope.new(scope) + end + end + + alias comment record +end diff --git a/spec/support/policies/criteria_policy.rb b/spec/support/policies/criteria_policy.rb new file mode 100644 index 00000000..dc0fd456 --- /dev/null +++ b/spec/support/policies/criteria_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CriteriaPolicy < BasePolicy + alias criteria record +end diff --git a/spec/support/policies/default_scope_contains_error_policy.rb b/spec/support/policies/default_scope_contains_error_policy.rb new file mode 100644 index 00000000..6be84817 --- /dev/null +++ b/spec/support/policies/default_scope_contains_error_policy.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class DefaultScopeContainsErrorPolicy < BasePolicy + class Scope < BaseScope + def resolve + # deliberate wrong usage of the method + raise "This is an arbitrary error that should bubble up" + end + end +end diff --git a/spec/support/policies/denier_policy.rb b/spec/support/policies/denier_policy.rb new file mode 100644 index 00000000..6d69e635 --- /dev/null +++ b/spec/support/policies/denier_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DenierPolicy < BasePolicy + def update? + false + end +end diff --git a/spec/support/policies/nil_class_policy.rb b/spec/support/policies/nil_class_policy.rb new file mode 100644 index 00000000..8bfcdb5b --- /dev/null +++ b/spec/support/policies/nil_class_policy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class NilClassPolicy < BasePolicy + class Scope + def initialize(*) + raise Pundit::NotDefinedError, "Cannot scope NilClass" + end + end + + def show? + false + end + + def destroy? + false + end +end diff --git a/spec/support/policies/post_policy.rb b/spec/support/policies/post_policy.rb new file mode 100644 index 00000000..b68fce3c --- /dev/null +++ b/spec/support/policies/post_policy.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class PostPolicy < BasePolicy + class Scope < BaseScope + def resolve + scope.published + end + end + + alias post record + + def update? + post.user == user + end + alias edit? update? + + def destroy? + false + end + + def show? + true + end + + def permitted_attributes + if post.user == user + %i[title votes] + else + [:votes] + end + end + + def permitted_attributes_for_revise + [:body] + end +end diff --git a/spec/support/policies/project/admin/comment_policy.rb b/spec/support/policies/project/admin/comment_policy.rb new file mode 100644 index 00000000..44f103f2 --- /dev/null +++ b/spec/support/policies/project/admin/comment_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Project + module Admin + class CommentPolicy < BasePolicy + def update? + true + end + + def destroy? + false + end + end + end +end diff --git a/spec/support/policies/project/comment_policy.rb b/spec/support/policies/project/comment_policy.rb new file mode 100644 index 00000000..2fe0980e --- /dev/null +++ b/spec/support/policies/project/comment_policy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Project + class CommentPolicy < BasePolicy + class Scope < BaseScope + def resolve + scope + end + end + + def update? + true + end + + alias comment record + end +end diff --git a/spec/support/policies/project/criteria_policy.rb b/spec/support/policies/project/criteria_policy.rb new file mode 100644 index 00000000..2216e92b --- /dev/null +++ b/spec/support/policies/project/criteria_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Project + class CriteriaPolicy < BasePolicy + alias criteria record + end +end diff --git a/spec/support/policies/project/post_policy.rb b/spec/support/policies/project/post_policy.rb new file mode 100644 index 00000000..33feb9df --- /dev/null +++ b/spec/support/policies/project/post_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Project + class PostPolicy < BasePolicy + class Scope < BaseScope + def resolve + scope.read + end + end + + alias post record + end +end diff --git a/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb b/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb new file mode 100644 index 00000000..a09cd013 --- /dev/null +++ b/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ProjectOneTwoThree + class AvatarFourFiveSixPolicy < BasePolicy + end +end diff --git a/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb b/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb new file mode 100644 index 00000000..ea965638 --- /dev/null +++ b/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ProjectOneTwoThree + class CommentFourFiveSixPolicy < BasePolicy + end +end diff --git a/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb b/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb new file mode 100644 index 00000000..fbaa22cf --- /dev/null +++ b/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ProjectOneTwoThree + class CriteriaFourFiveSixPolicy < BasePolicy + end +end diff --git a/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb b/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb new file mode 100644 index 00000000..56e67c86 --- /dev/null +++ b/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ProjectOneTwoThree + class PostFourFiveSixPolicy < BasePolicy + end +end diff --git a/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb b/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb new file mode 100644 index 00000000..8fdabef7 --- /dev/null +++ b/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ProjectOneTwoThree + class TagFourFiveSixPolicy < BasePolicy + end +end diff --git a/spec/support/policies/publication_policy.rb b/spec/support/policies/publication_policy.rb new file mode 100644 index 00000000..daa7f413 --- /dev/null +++ b/spec/support/policies/publication_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class PublicationPolicy < BasePolicy + class Scope < BaseScope + def resolve + scope.published + end + end + + def create? + true + end +end diff --git a/spec/support/policies/wiki_policy.rb b/spec/support/policies/wiki_policy.rb new file mode 100644 index 00000000..30c311b6 --- /dev/null +++ b/spec/support/policies/wiki_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class WikiPolicy + class Scope + # deliberate typo method + def initalize; end + end +end