pundit 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/SECURITY.md ADDED
@@ -0,0 +1,19 @@
1
+ # Security Policy
2
+
3
+ Please do not file an issue on GitHub, or send a PR addressing the issue.
4
+
5
+ ## Supported versions
6
+
7
+ Most recent major version only.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Contact one of the maintainers directly:
12
+
13
+ * [@Burgestrand](https://github.com/Burgestrand)
14
+ * [@dgmstuart](https://github.com/dgmstuart)
15
+ * [@varvet](https://github.com/varvet)
16
+
17
+ You can report vulnerabilities on GitHub too: https://github.com/varvet/pundit/security
18
+
19
+ Thank you!
@@ -0,0 +1,5 @@
1
+ RSpec:
2
+ Language:
3
+ ExampleGroups:
4
+ Regular:
5
+ - permissions
@@ -43,7 +43,7 @@ class ApplicationPolicy
43
43
  end
44
44
 
45
45
  def resolve
46
- raise NotImplementedError, "You must define #resolve in #{self.class}"
46
+ raise NoMethodError, "You must define #resolve in #{self.class}"
47
47
  end
48
48
 
49
49
  private
@@ -1,6 +1,12 @@
1
1
  <% module_namespacing do -%>
2
2
  class <%= class_name %>Policy < ApplicationPolicy
3
- class Scope < Scope
3
+ # NOTE: Up to Pundit v2.3.1, the inheritance was declared as
4
+ # `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`.
5
+ # In most cases the behavior will be identical, but if updating existing
6
+ # code, beware of possible changes to the ancestors:
7
+ # https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5
8
+
9
+ class Scope < ApplicationPolicy::Scope
4
10
  # NOTE: Be explicit about which records you allow access to!
5
11
  # def resolve
6
12
  # scope.all
@@ -1,4 +1,4 @@
1
- require '<%= File.exists?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
1
+ require '<%= File.exist?('spec/rails_helper.rb') ? 'rails_helper' : 'spec_helper' %>'
2
2
 
3
3
  RSpec.describe <%= class_name %>Policy, type: :policy do
4
4
  let(:user) { User.new }
@@ -15,6 +15,14 @@ module Pundit
15
15
 
16
16
  protected
17
17
 
18
+ # @return [Pundit::Context] a new instance of {Pundit::Context} with the current user
19
+ def pundit
20
+ @pundit ||= Pundit::Context.new(
21
+ user: pundit_user,
22
+ policy_cache: Pundit::CacheStore::LegacyStore.new(policies)
23
+ )
24
+ end
25
+
18
26
  # @return [Boolean] whether authorization has been performed, i.e. whether
19
27
  # one {#authorize} or {#skip_authorization} has been called
20
28
  def pundit_policy_authorized?
@@ -64,7 +72,7 @@ module Pundit
64
72
 
65
73
  @_pundit_policy_authorized = true
66
74
 
67
- Pundit.authorize(pundit_user, record, query, policy_class: policy_class, cache: policies)
75
+ pundit.authorize(record, query: query, policy_class: policy_class)
68
76
  end
69
77
 
70
78
  # Allow this action not to perform authorization.
@@ -98,9 +106,9 @@ module Pundit
98
106
  #
99
107
  # @see https://github.com/varvet/pundit#policies
100
108
  # @param record [Object] the object we're retrieving the policy for
101
- # @return [Object, nil] instance of policy class with query methods
109
+ # @return [Object] instance of policy class with query methods
102
110
  def policy(record)
103
- policies[record] ||= Pundit.policy!(pundit_user, record)
111
+ pundit.policy!(record)
104
112
  end
105
113
 
106
114
  # Retrieves a set of permitted attributes from the policy by instantiating
@@ -162,7 +170,7 @@ module Pundit
162
170
  private
163
171
 
164
172
  def pundit_policy_scope(scope)
165
- policy_scopes[scope] ||= Pundit.policy_scope!(pundit_user, scope)
173
+ policy_scopes[scope] ||= pundit.policy_scope!(scope)
166
174
  end
167
175
  end
168
176
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module CacheStore
5
+ # @api private
6
+ class LegacyStore
7
+ def initialize(hash = {})
8
+ @store = hash
9
+ end
10
+
11
+ def fetch(user:, record:)
12
+ _ = user
13
+ @store[record] ||= yield
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module CacheStore
5
+ # @api private
6
+ class NullStore
7
+ @instance = new
8
+
9
+ class << self
10
+ attr_reader :instance
11
+ end
12
+
13
+ def fetch(*, **)
14
+ yield
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ class Context
5
+ def initialize(user:, policy_cache: CacheStore::NullStore.instance)
6
+ @user = user
7
+ @policy_cache = policy_cache
8
+ end
9
+
10
+ attr_reader :user
11
+
12
+ # @api private
13
+ attr_reader :policy_cache
14
+
15
+ # Retrieves the policy for the given record, initializing it with the
16
+ # record and user and finally throwing an error if the user is not
17
+ # authorized to perform the given action.
18
+ #
19
+ # @param user [Object] the user that initiated the action
20
+ # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
21
+ # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
22
+ # @param policy_class [Class] the policy class we want to force use of
23
+ # @raise [NotAuthorizedError] if the given query method returned false
24
+ # @return [Object] Always returns the passed object record
25
+ def authorize(possibly_namespaced_record, query:, policy_class:)
26
+ record = pundit_model(possibly_namespaced_record)
27
+ policy = if policy_class
28
+ policy_class.new(user, record)
29
+ else
30
+ policy!(possibly_namespaced_record)
31
+ end
32
+
33
+ raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
34
+
35
+ record
36
+ end
37
+
38
+ # Retrieves the policy scope for the given record.
39
+ #
40
+ # @see https://github.com/varvet/pundit#scopes
41
+ # @param user [Object] the user that initiated the action
42
+ # @param scope [Object] the object we're retrieving the policy scope for
43
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
44
+ # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
45
+ def policy_scope(scope)
46
+ policy_scope_class = policy_finder(scope).scope
47
+ return unless policy_scope_class
48
+
49
+ begin
50
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
51
+ rescue ArgumentError
52
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
53
+ end
54
+
55
+ policy_scope.resolve
56
+ end
57
+
58
+ # Retrieves the policy scope for the given record. Raises if not found.
59
+ #
60
+ # @see https://github.com/varvet/pundit#scopes
61
+ # @param user [Object] the user that initiated the action
62
+ # @param scope [Object] the object we're retrieving the policy scope for
63
+ # @raise [NotDefinedError] if the policy scope cannot be found
64
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
65
+ # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
66
+ def policy_scope!(scope)
67
+ policy_scope_class = policy_finder(scope).scope!
68
+ return unless policy_scope_class
69
+
70
+ begin
71
+ policy_scope = policy_scope_class.new(user, pundit_model(scope))
72
+ rescue ArgumentError
73
+ raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
74
+ end
75
+
76
+ policy_scope.resolve
77
+ end
78
+
79
+ # Retrieves the policy for the given record.
80
+ #
81
+ # @see https://github.com/varvet/pundit#policies
82
+ # @param user [Object] the user that initiated the action
83
+ # @param record [Object] the object we're retrieving the policy for
84
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
85
+ # @return [Object, nil] instance of policy class with query methods
86
+ def policy(record)
87
+ cached_find(record, &:policy)
88
+ end
89
+
90
+ # Retrieves the policy for the given record. Raises if not found.
91
+ #
92
+ # @see https://github.com/varvet/pundit#policies
93
+ # @param user [Object] the user that initiated the action
94
+ # @param record [Object] the object we're retrieving the policy for
95
+ # @raise [NotDefinedError] if the policy cannot be found
96
+ # @raise [InvalidConstructorError] if the policy constructor called incorrectly
97
+ # @return [Object] instance of policy class with query methods
98
+ def policy!(record)
99
+ cached_find(record, &:policy!)
100
+ end
101
+
102
+ private
103
+
104
+ def cached_find(record)
105
+ policy_cache.fetch(user: user, record: record) do
106
+ klass = yield policy_finder(record)
107
+ next unless klass
108
+
109
+ model = pundit_model(record)
110
+
111
+ begin
112
+ klass.new(user, model)
113
+ rescue ArgumentError
114
+ raise InvalidConstructorError, "Invalid #<#{klass}> constructor is called"
115
+ end
116
+ end
117
+ end
118
+
119
+ def policy_finder(record)
120
+ PolicyFinder.new(record)
121
+ end
122
+
123
+ def pundit_model(record)
124
+ record.is_a?(Array) ? record.last : record
125
+ end
126
+ end
127
+ end
@@ -56,7 +56,7 @@ module Pundit
56
56
 
57
57
  # @return [String] the name of the key this object would have in a params hash
58
58
  #
59
- def param_key
59
+ def param_key # rubocop:disable Metrics/AbcSize
60
60
  model = object.is_a?(Array) ? object.last : object
61
61
 
62
62
  if model.respond_to?(:model_name)
data/lib/pundit/rspec.rb CHANGED
@@ -5,6 +5,16 @@ module Pundit
5
5
  module Matchers
6
6
  extend ::RSpec::Matchers::DSL
7
7
 
8
+ class << self
9
+ attr_writer :description
10
+
11
+ def description(user, record)
12
+ return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call)
13
+
14
+ @description
15
+ end
16
+ end
17
+
8
18
  # rubocop:disable Metrics/BlockLength
9
19
  matcher :permit do |user, record|
10
20
  match_proc = lambda do |policy|
@@ -33,6 +43,10 @@ module Pundit
33
43
  "#{record} but #{@violating_permissions.to_sentence} #{was_were} granted"
34
44
  end
35
45
 
46
+ description do
47
+ Pundit::RSpec::Matchers.description(user, record) || super()
48
+ end
49
+
36
50
  if respond_to?(:match_when_negated)
37
51
  match(&match_proc)
38
52
  match_when_negated(&match_when_negated_proc)
@@ -55,7 +69,15 @@ module Pundit
55
69
 
56
70
  module DSL
57
71
  def permissions(*list, &block)
58
- describe(list.to_sentence, permissions: list, caller: caller) { instance_eval(&block) }
72
+ metadata = { permissions: list, caller: caller }
73
+
74
+ if list.last == :focus
75
+ list.pop
76
+ metadata[:focus] = true
77
+ end
78
+
79
+ description = list.to_sentence
80
+ describe(description, metadata) { instance_eval(&block) }
59
81
  end
60
82
  end
61
83
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
- VERSION = "2.2.0"
4
+ VERSION = "2.4.0"
5
5
  end
data/lib/pundit.rb CHANGED
@@ -8,6 +8,9 @@ require "active_support/core_ext/object/blank"
8
8
  require "active_support/core_ext/module/introspection"
9
9
  require "active_support/dependencies/autoload"
10
10
  require "pundit/authorization"
11
+ require "pundit/context"
12
+ require "pundit/cache_store/null_store"
13
+ require "pundit/cache_store/legacy_store"
11
14
 
12
15
  # @api private
13
16
  # To avoid name clashes with common Error naming when mixing in Pundit,
@@ -33,7 +36,10 @@ module Pundit
33
36
  @record = options[:record]
34
37
  @policy = options[:policy]
35
38
 
36
- message = options.fetch(:message) { "not allowed to #{query} this #{record.class}" }
39
+ message = options.fetch(:message) do
40
+ record_name = record.is_a?(Class) ? record.to_s : "this #{record.class}"
41
+ "not allowed to #{policy.class}##{query} #{record_name}"
42
+ end
37
43
  end
38
44
 
39
45
  super(message)
@@ -55,111 +61,44 @@ module Pundit
55
61
  class NotDefinedError < Error; end
56
62
 
57
63
  def self.included(base)
58
- ActiveSupport::Deprecation.warn <<~WARNING.strip_heredoc
64
+ location = caller_locations(1, 1).first
65
+ warn <<~WARNING
59
66
  'include Pundit' is deprecated. Please use 'include Pundit::Authorization' instead.
67
+ (called from #{location.label} at #{location.path}:#{location.lineno})
60
68
  WARNING
61
69
  base.include Authorization
62
70
  end
63
71
 
64
72
  class << self
65
- # Retrieves the policy for the given record, initializing it with the
66
- # record and user and finally throwing an error if the user is not
67
- # authorized to perform the given action.
68
- #
69
- # @param user [Object] the user that initiated the action
70
- # @param possibly_namespaced_record [Object, Array] the object we're checking permissions of
71
- # @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
72
- # @param policy_class [Class] the policy class we want to force use of
73
- # @param cache [#[], #[]=] a Hash-like object to cache the found policy instance in
74
- # @raise [NotAuthorizedError] if the given query method returned false
75
- # @return [Object] Always returns the passed object record
76
- def authorize(user, possibly_namespaced_record, query, policy_class: nil, cache: {})
77
- record = pundit_model(possibly_namespaced_record)
78
- policy = if policy_class
79
- policy_class.new(user, record)
73
+ # @see [Pundit::Context#authorize]
74
+ def authorize(user, record, query, policy_class: nil, cache: nil)
75
+ context = if cache
76
+ Context.new(user: user, policy_cache: cache)
80
77
  else
81
- cache[possibly_namespaced_record] ||= policy!(user, possibly_namespaced_record)
78
+ Context.new(user: user)
82
79
  end
83
80
 
84
- raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
85
-
86
- record
87
- end
88
-
89
- # Retrieves the policy scope for the given record.
90
- #
91
- # @see https://github.com/varvet/pundit#scopes
92
- # @param user [Object] the user that initiated the action
93
- # @param scope [Object] the object we're retrieving the policy scope for
94
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
95
- # @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
96
- def policy_scope(user, scope)
97
- policy_scope_class = PolicyFinder.new(scope).scope
98
- return unless policy_scope_class
99
-
100
- begin
101
- policy_scope = policy_scope_class.new(user, pundit_model(scope))
102
- rescue ArgumentError
103
- raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
104
- end
105
-
106
- policy_scope.resolve
81
+ context.authorize(record, query: query, policy_class: policy_class)
107
82
  end
108
83
 
109
- # Retrieves the policy scope for the given record.
110
- #
111
- # @see https://github.com/varvet/pundit#scopes
112
- # @param user [Object] the user that initiated the action
113
- # @param scope [Object] the object we're retrieving the policy scope for
114
- # @raise [NotDefinedError] if the policy scope cannot be found
115
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
116
- # @return [Scope{#resolve}] instance of scope class which can resolve to a scope
117
- def policy_scope!(user, scope)
118
- policy_scope_class = PolicyFinder.new(scope).scope!
119
- return unless policy_scope_class
120
-
121
- begin
122
- policy_scope = policy_scope_class.new(user, pundit_model(scope))
123
- rescue ArgumentError
124
- raise InvalidConstructorError, "Invalid #<#{policy_scope_class}> constructor is called"
125
- end
126
-
127
- policy_scope.resolve
84
+ # @see [Pundit::Context#policy_scope]
85
+ def policy_scope(user, *args, **kwargs, &block)
86
+ Context.new(user: user).policy_scope(*args, **kwargs, &block)
128
87
  end
129
88
 
130
- # Retrieves the policy for the given record.
131
- #
132
- # @see https://github.com/varvet/pundit#policies
133
- # @param user [Object] the user that initiated the action
134
- # @param record [Object] the object we're retrieving the policy for
135
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
136
- # @return [Object, nil] instance of policy class with query methods
137
- def policy(user, record)
138
- policy = PolicyFinder.new(record).policy
139
- policy&.new(user, pundit_model(record))
140
- rescue ArgumentError
141
- raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
89
+ # @see [Pundit::Context#policy_scope!]
90
+ def policy_scope!(user, *args, **kwargs, &block)
91
+ Context.new(user: user).policy_scope!(*args, **kwargs, &block)
142
92
  end
143
93
 
144
- # Retrieves the policy for the given record.
145
- #
146
- # @see https://github.com/varvet/pundit#policies
147
- # @param user [Object] the user that initiated the action
148
- # @param record [Object] the object we're retrieving the policy for
149
- # @raise [NotDefinedError] if the policy cannot be found
150
- # @raise [InvalidConstructorError] if the policy constructor called incorrectly
151
- # @return [Object] instance of policy class with query methods
152
- def policy!(user, record)
153
- policy = PolicyFinder.new(record).policy!
154
- policy.new(user, pundit_model(record))
155
- rescue ArgumentError
156
- raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
94
+ # @see [Pundit::Context#policy]
95
+ def policy(user, *args, **kwargs, &block)
96
+ Context.new(user: user).policy(*args, **kwargs, &block)
157
97
  end
158
98
 
159
- private
160
-
161
- def pundit_model(record)
162
- record.is_a?(Array) ? record.last : record
99
+ # @see [Pundit::Context#policy!]
100
+ def policy!(user, *args, **kwargs, &block)
101
+ Context.new(user: user).policy!(*args, **kwargs, &block)
163
102
  end
164
103
  end
165
104
 
data/pundit.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
8
8
  gem.name = "pundit"
9
9
  gem.version = Pundit::VERSION
10
10
  gem.authors = ["Jonas Nicklas", "Varvet AB"]
11
- gem.email = ["jonas.nicklas@gmail.com", "dev@elabs.se"]
11
+ gem.email = ["jonas.nicklas@gmail.com", "info@varvet.com"]
12
12
  gem.description = "Object oriented authorization for Rails applications"
13
13
  gem.summary = "OO authorization for Rails"
14
14
  gem.homepage = "https://github.com/varvet/pundit"
@@ -19,6 +19,8 @@ Gem::Specification.new do |gem|
19
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
20
  gem.require_paths = ["lib"]
21
21
 
22
+ gem.metadata = { "rubygems_mfa_required" => "true" }
23
+
22
24
  gem.add_dependency "activesupport", ">= 3.0.0"
23
25
  gem.add_development_dependency "actionpack", ">= 3.0.0"
24
26
  gem.add_development_dependency "activemodel", ">= 3.0.0"
@@ -27,7 +29,7 @@ Gem::Specification.new do |gem|
27
29
  gem.add_development_dependency "railties", ">= 3.0.0"
28
30
  gem.add_development_dependency "rake"
29
31
  gem.add_development_dependency "rspec", ">= 3.0.0"
30
- gem.add_development_dependency "rubocop", "1.24.0"
32
+ gem.add_development_dependency "rubocop"
31
33
  gem.add_development_dependency "simplecov", ">= 0.17.0"
32
34
  gem.add_development_dependency "yard"
33
35
  end
@@ -3,10 +3,13 @@
3
3
  require "spec_helper"
4
4
 
5
5
  describe Pundit::Authorization do
6
- let(:controller) { Controller.new(user, "update", {}) }
6
+ def to_params(*args, **kwargs, &block)
7
+ ActionController::Parameters.new(*args, **kwargs, &block)
8
+ end
9
+
10
+ let(:controller) { Controller.new(user, "update", to_params({})) }
7
11
  let(:user) { double }
8
12
  let(:post) { Post.new(user) }
9
- let(:customer_post) { Customer::Post.new(user) }
10
13
  let(:comment) { Comment.new }
11
14
  let(:article) { Article.new }
12
15
  let(:article_tag) { ArticleTag.new }
@@ -166,7 +169,7 @@ describe Pundit::Authorization do
166
169
  expect(controller.policy_scope(Post)).to eq :published
167
170
  end
168
171
 
169
- it "allows policy scope class to be overriden" do
172
+ it "allows policy scope class to be overridden" do
170
173
  expect(controller.policy_scope(Post, policy_scope_class: PublicationPolicy::Scope)).to eq :published
171
174
  end
172
175
 
@@ -188,7 +191,7 @@ describe Pundit::Authorization do
188
191
 
189
192
  describe "#permitted_attributes" do
190
193
  it "checks policy for permitted attributes" do
191
- params = ActionController::Parameters.new(
194
+ params = to_params(
192
195
  post: {
193
196
  title: "Hello",
194
197
  votes: 5,
@@ -206,7 +209,8 @@ describe Pundit::Authorization do
206
209
  end
207
210
 
208
211
  it "checks policy for permitted attributes for record of a ActiveModel type" do
209
- params = ActionController::Parameters.new(
212
+ customer_post = Customer::Post.new(user)
213
+ params = to_params(
210
214
  customer_post: {
211
215
  title: "Hello",
212
216
  votes: 5,
@@ -224,11 +228,23 @@ describe Pundit::Authorization do
224
228
  "votes" => 5
225
229
  )
226
230
  end
231
+
232
+ it "goes through the policy cache" do
233
+ params = to_params(post: { title: "Hello" })
234
+ user = double
235
+ post = Post.new(user)
236
+ controller = Controller.new(user, "update", params)
237
+
238
+ expect do
239
+ expect(controller.permitted_attributes(post)).to be_truthy
240
+ expect(controller.permitted_attributes(post)).to be_truthy
241
+ end.to change { PostPolicy.instances }.by(1)
242
+ end
227
243
  end
228
244
 
229
245
  describe "#permitted_attributes_for_action" do
230
246
  it "is checked if it is defined in the policy" do
231
- params = ActionController::Parameters.new(
247
+ params = to_params(
232
248
  post: {
233
249
  title: "Hello",
234
250
  body: "blah",
@@ -242,7 +258,7 @@ describe Pundit::Authorization do
242
258
  end
243
259
 
244
260
  it "can be explicitly set" do
245
- params = ActionController::Parameters.new(
261
+ params = to_params(
246
262
  post: {
247
263
  title: "Hello",
248
264
  body: "blah",
data/spec/dsl_spec.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Pundit RSpec DSL" do
6
+ let(:fake_rspec) do
7
+ double = class_double(RSpec::ExampleGroups)
8
+ double.extend(::Pundit::RSpec::DSL)
9
+ double
10
+ end
11
+ let(:block) { proc { "block content" } }
12
+
13
+ it "calls describe with the correct metadata and without :focus" do
14
+ expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array) }
15
+ expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
16
+ expect(block.call).to eq("block content")
17
+ end
18
+
19
+ fake_rspec.permissions(:item1, :item2, &block)
20
+ end
21
+
22
+ it "calls describe with the correct metadata and with :focus" do
23
+ expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array), focus: true }
24
+ expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
25
+ expect(block.call).to eq("block content")
26
+ end
27
+
28
+ fake_rspec.permissions(:item1, :item2, :focus, &block)
29
+ end
30
+ end
@@ -35,7 +35,7 @@ RSpec.describe "generators" do
35
35
  describe "#resolve" do
36
36
  it "raises a descriptive error" do
37
37
  scope = WidgetPolicy::Scope.new(double("User"), double("User.all"))
38
- expect { scope.resolve }.to raise_error(NotImplementedError, /WidgetPolicy::Scope/)
38
+ expect { scope.resolve }.to raise_error(NoMethodError, /WidgetPolicy::Scope/)
39
39
  end
40
40
  end
41
41
  end
@@ -18,5 +18,32 @@ RSpec.describe PostPolicy do
18
18
  should permit(user, other_post)
19
19
  end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
20
20
  end
21
+
22
+ it "uses the default description if not overridden" do
23
+ expect(permit(user, own_post).description).to eq("permit #{user.inspect} and #{own_post.inspect}")
24
+ end
25
+
26
+ context "when the matcher description is overridden" do
27
+ after do
28
+ Pundit::RSpec::Matchers.description = nil
29
+ end
30
+
31
+ it "sets a custom matcher description with a Proc" do
32
+ allow(user).to receive(:role).and_return("default_role")
33
+ allow(own_post).to receive(:id).and_return(1)
34
+
35
+ Pundit::RSpec::Matchers.description = lambda { |user, record|
36
+ "permit user with role #{user.role} to access record with ID #{record.id}"
37
+ }
38
+
39
+ description = permit(user, own_post).description
40
+ expect(description).to eq("permit user with role default_role to access record with ID 1")
41
+ end
42
+
43
+ it "sets a custom matcher description with a string" do
44
+ Pundit::RSpec::Matchers.description = "permit user"
45
+ expect(permit(user, own_post).description).to eq("permit user")
46
+ end
47
+ end
21
48
  end
22
49
  end