pundit 2.4.0 → 2.5.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +92 -57
  3. data/.rubocop.yml +18 -8
  4. data/.rubocop_ignore_git.yml +7 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +61 -42
  7. data/Gemfile +22 -2
  8. data/README.md +30 -0
  9. data/Rakefile +1 -0
  10. data/lib/generators/pundit/install/install_generator.rb +3 -1
  11. data/lib/generators/pundit/policy/policy_generator.rb +3 -1
  12. data/lib/generators/rspec/policy_generator.rb +4 -1
  13. data/lib/generators/test_unit/policy_generator.rb +4 -1
  14. data/lib/pundit/authorization.rb +152 -77
  15. data/lib/pundit/cache_store/legacy_store.rb +7 -0
  16. data/lib/pundit/cache_store/null_store.rb +9 -0
  17. data/lib/pundit/cache_store.rb +22 -0
  18. data/lib/pundit/context.rb +76 -26
  19. data/lib/pundit/policy_finder.rb +22 -1
  20. data/lib/pundit/railtie.rb +19 -0
  21. data/lib/pundit/rspec.rb +67 -6
  22. data/lib/pundit/version.rb +2 -1
  23. data/lib/pundit.rb +39 -14
  24. data/pundit.gemspec +8 -12
  25. data/spec/authorization_spec.rb +60 -3
  26. data/spec/policy_finder_spec.rb +5 -1
  27. data/spec/pundit/helper_spec.rb +18 -0
  28. data/spec/pundit_spec.rb +37 -11
  29. data/spec/rspec_dsl_spec.rb +81 -0
  30. data/spec/simple_cov_check_action_formatter.rb +79 -0
  31. data/spec/spec_helper.rb +22 -339
  32. data/spec/support/lib/controller.rb +38 -0
  33. data/spec/support/lib/custom_cache.rb +19 -0
  34. data/spec/support/lib/instance_tracking.rb +20 -0
  35. data/spec/support/models/article.rb +4 -0
  36. data/spec/support/models/article_tag.rb +7 -0
  37. data/spec/support/models/artificial_blog.rb +7 -0
  38. data/spec/support/models/blog.rb +4 -0
  39. data/spec/support/models/comment.rb +5 -0
  40. data/spec/support/models/comment_four_five_six.rb +5 -0
  41. data/spec/support/models/comment_scope.rb +13 -0
  42. data/spec/support/models/comments_relation.rb +15 -0
  43. data/spec/support/models/customer/post.rb +11 -0
  44. data/spec/support/models/default_scope_contains_error.rb +5 -0
  45. data/spec/support/models/dummy_current_user.rb +7 -0
  46. data/spec/support/models/foo.rb +4 -0
  47. data/spec/support/models/post.rb +25 -0
  48. data/spec/support/models/post_four_five_six.rb +9 -0
  49. data/spec/support/models/project_one_two_three/avatar_four_five_six.rb +7 -0
  50. data/spec/support/models/project_one_two_three/tag_four_five_six.rb +11 -0
  51. data/spec/support/models/wiki.rb +4 -0
  52. data/spec/support/policies/article_tag_other_name_policy.rb +13 -0
  53. data/spec/support/policies/base_policy.rb +23 -0
  54. data/spec/support/policies/blog_policy.rb +5 -0
  55. data/spec/support/policies/comment_policy.rb +11 -0
  56. data/spec/support/policies/criteria_policy.rb +5 -0
  57. data/spec/support/policies/default_scope_contains_error_policy.rb +10 -0
  58. data/spec/support/policies/denier_policy.rb +7 -0
  59. data/spec/support/policies/dummy_current_user_policy.rb +9 -0
  60. data/spec/support/policies/nil_class_policy.rb +17 -0
  61. data/spec/support/policies/post_policy.rb +36 -0
  62. data/spec/support/policies/project/admin/comment_policy.rb +15 -0
  63. data/spec/support/policies/project/comment_policy.rb +17 -0
  64. data/spec/support/policies/project/criteria_policy.rb +7 -0
  65. data/spec/support/policies/project/post_policy.rb +13 -0
  66. data/spec/support/policies/project_one_two_three/avatar_four_five_six_policy.rb +6 -0
  67. data/spec/support/policies/project_one_two_three/comment_four_five_six_policy.rb +6 -0
  68. data/spec/support/policies/project_one_two_three/criteria_four_five_six_policy.rb +6 -0
  69. data/spec/support/policies/project_one_two_three/post_four_five_six_policy.rb +6 -0
  70. data/spec/support/policies/project_one_two_three/tag_four_five_six_policy.rb +6 -0
  71. data/spec/support/policies/publication_policy.rb +13 -0
  72. data/spec/support/policies/wiki_policy.rb +8 -0
  73. metadata +62 -158
  74. data/spec/dsl_spec.rb +0 -30
  75. /data/lib/generators/pundit/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
  76. /data/lib/generators/pundit/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  77. /data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  78. /data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
data/lib/pundit/rspec.rb CHANGED
@@ -1,13 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Array#to_sentence
4
+ require "active_support/core_ext/array/conversions"
5
+
3
6
  module Pundit
7
+ # Namespace for Pundit's RSpec integration.
4
8
  module RSpec
9
+ # Namespace for Pundit's RSpec matchers.
5
10
  module Matchers
6
11
  extend ::RSpec::Matchers::DSL
7
12
 
13
+ # @!method description=(description)
8
14
  class << self
15
+ # Used to build a suitable description for the Pundit `permit` matcher.
16
+ # @api public
17
+ # @param value [String, Proc]
18
+ # @example
19
+ # Pundit::RSpec::Matchers.description = ->(user, record) do
20
+ # "permit user with role #{user.role} to access record with ID #{record.id}"
21
+ # end
9
22
  attr_writer :description
10
23
 
24
+ # Used to retrieve a suitable description for the Pundit `permit` matcher.
25
+ # @api private
26
+ # @private
11
27
  def description(user, record)
12
28
  return @description.call(user, record) if defined?(@description) && @description.respond_to?(:call)
13
29
 
@@ -32,15 +48,21 @@ module Pundit
32
48
  end
33
49
 
34
50
  failure_message_proc = lambda do |policy|
35
- was_were = @violating_permissions.count > 1 ? "were" : "was"
36
51
  "Expected #{policy} to grant #{permissions.to_sentence} on " \
37
- "#{record} but #{@violating_permissions.to_sentence} #{was_were} not granted"
52
+ "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} not granted"
38
53
  end
39
54
 
40
55
  failure_message_when_negated_proc = lambda do |policy|
41
- was_were = @violating_permissions.count > 1 ? "were" : "was"
42
56
  "Expected #{policy} not to grant #{permissions.to_sentence} on " \
43
- "#{record} but #{@violating_permissions.to_sentence} #{was_were} granted"
57
+ "#{record} but #{@violating_permissions.to_sentence} #{was_or_were} granted"
58
+ end
59
+
60
+ def was_or_were
61
+ if @violating_permissions.count > 1
62
+ "were"
63
+ else
64
+ "was"
65
+ end
44
66
  end
45
67
 
46
68
  description do
@@ -53,21 +75,57 @@ module Pundit
53
75
  failure_message(&failure_message_proc)
54
76
  failure_message_when_negated(&failure_message_when_negated_proc)
55
77
  else
78
+ # :nocov:
79
+ # Compatibility with RSpec < 3.0, released 2014-06-01.
56
80
  match_for_should(&match_proc)
57
81
  match_for_should_not(&match_when_negated_proc)
58
82
  failure_message_for_should(&failure_message_proc)
59
83
  failure_message_for_should_not(&failure_message_when_negated_proc)
84
+ # :nocov:
85
+ end
86
+
87
+ if ::RSpec.respond_to?(:current_example)
88
+ def current_example
89
+ ::RSpec.current_example
90
+ end
91
+ else
92
+ # :nocov:
93
+ # Compatibility with RSpec < 3.0, released 2014-06-01.
94
+ def current_example
95
+ example
96
+ end
97
+ # :nocov:
60
98
  end
61
99
 
62
100
  def permissions
63
- current_example = ::RSpec.respond_to?(:current_example) ? ::RSpec.current_example : example
64
- current_example.metadata[:permissions]
101
+ current_example.metadata.fetch(:permissions) do
102
+ raise KeyError, <<~ERROR.strip
103
+ No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`?
104
+ ERROR
105
+ end
65
106
  end
66
107
  end
67
108
  # rubocop:enable Metrics/BlockLength
68
109
  end
69
110
 
111
+ # Mixed in to all policy example groups to provide a DSL.
70
112
  module DSL
113
+ # @example
114
+ # describe PostPolicy do
115
+ # permissions :show?, :update? do
116
+ # it { is_expected.to permit(user, own_post) }
117
+ # end
118
+ # end
119
+ #
120
+ # @example focused example group
121
+ # describe PostPolicy do
122
+ # permissions :show?, :update?, :focus do
123
+ # it { is_expected.to permit(user, own_post) }
124
+ # end
125
+ # end
126
+ #
127
+ # @param list [Symbol, Array<Symbol>] a permission to describe
128
+ # @return [void]
71
129
  def permissions(*list, &block)
72
130
  metadata = { permissions: list, caller: caller }
73
131
 
@@ -81,6 +139,9 @@ module Pundit
81
139
  end
82
140
  end
83
141
 
142
+ # Mixed in to all policy example groups.
143
+ #
144
+ # @private not useful
84
145
  module PolicyExampleGroup
85
146
  include Pundit::RSpec::Matchers
86
147
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pundit
4
- VERSION = "2.4.0"
4
+ # The current version of Pundit.
5
+ VERSION = "2.5.0"
5
6
  end
data/lib/pundit.rb CHANGED
@@ -1,33 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
4
+
3
5
  require "pundit/version"
4
6
  require "pundit/policy_finder"
5
- require "active_support/concern"
6
- require "active_support/core_ext/string/inflections"
7
- require "active_support/core_ext/object/blank"
8
- require "active_support/core_ext/module/introspection"
9
- require "active_support/dependencies/autoload"
10
7
  require "pundit/authorization"
11
8
  require "pundit/context"
9
+ require "pundit/cache_store"
12
10
  require "pundit/cache_store/null_store"
13
11
  require "pundit/cache_store/legacy_store"
12
+ require "pundit/railtie" if defined?(Rails)
14
13
 
15
14
  # @api private
16
15
  # To avoid name clashes with common Error naming when mixing in Pundit,
17
16
  # keep it here with compact class style definition.
18
17
  class Pundit::Error < StandardError; end # rubocop:disable Style/ClassAndModuleChildren
19
18
 
19
+ # Hello? Yes, this is Pundit.
20
+ #
20
21
  # @api public
21
22
  module Pundit
22
- SUFFIX = "Policy"
23
+ # @api private
24
+ # @deprecated See {Pundit::PolicyFinder}
25
+ SUFFIX = Pundit::PolicyFinder::SUFFIX
23
26
 
24
27
  # @api private
28
+ # @private
25
29
  module Generators; end
26
30
 
27
31
  # Error that will be raised when authorization has failed
28
32
  class NotAuthorizedError < Error
29
- attr_reader :query, :record, :policy
30
-
33
+ # @see #initialize
34
+ attr_reader :query
35
+ # @see #initialize
36
+ attr_reader :record
37
+ # @see #initialize
38
+ attr_reader :policy
39
+
40
+ # @overload initialize(message)
41
+ # Create an error with a simple error message.
42
+ # @param [String] message A simple error message string.
43
+ #
44
+ # @overload initialize(options)
45
+ # Create an error with the specified attributes.
46
+ # @param [Hash] options The error options.
47
+ # @option options [String] :message Optional custom error message. Will default to a generalized message.
48
+ # @option options [Symbol] :query The name of the policy method that was checked.
49
+ # @option options [Object] :record The object that was being checked with the policy.
50
+ # @option options [Class] :policy The class of policy that was used for the check.
31
51
  def initialize(options = {})
32
52
  if options.is_a? String
33
53
  message = options
@@ -70,10 +90,11 @@ module Pundit
70
90
  end
71
91
 
72
92
  class << self
73
- # @see [Pundit::Context#authorize]
93
+ # @see Pundit::Context#authorize
74
94
  def authorize(user, record, query, policy_class: nil, cache: nil)
75
95
  context = if cache
76
- Context.new(user: user, policy_cache: cache)
96
+ policy_cache = CacheStore::LegacyStore.new(cache)
97
+ Context.new(user: user, policy_cache: policy_cache)
77
98
  else
78
99
  Context.new(user: user)
79
100
  end
@@ -81,29 +102,33 @@ module Pundit
81
102
  context.authorize(record, query: query, policy_class: policy_class)
82
103
  end
83
104
 
84
- # @see [Pundit::Context#policy_scope]
105
+ # @see Pundit::Context#policy_scope
85
106
  def policy_scope(user, *args, **kwargs, &block)
86
107
  Context.new(user: user).policy_scope(*args, **kwargs, &block)
87
108
  end
88
109
 
89
- # @see [Pundit::Context#policy_scope!]
110
+ # @see Pundit::Context#policy_scope!
90
111
  def policy_scope!(user, *args, **kwargs, &block)
91
112
  Context.new(user: user).policy_scope!(*args, **kwargs, &block)
92
113
  end
93
114
 
94
- # @see [Pundit::Context#policy]
115
+ # @see Pundit::Context#policy
95
116
  def policy(user, *args, **kwargs, &block)
96
117
  Context.new(user: user).policy(*args, **kwargs, &block)
97
118
  end
98
119
 
99
- # @see [Pundit::Context#policy!]
120
+ # @see Pundit::Context#policy!
100
121
  def policy!(user, *args, **kwargs, &block)
101
122
  Context.new(user: user).policy!(*args, **kwargs, &block)
102
123
  end
103
124
  end
104
125
 
126
+ # Rails view helpers, to allow a slightly different view-specific
127
+ # implementation of the methods in {Pundit::Authorization}.
128
+ #
105
129
  # @api private
106
130
  module Helper
131
+ # @see Pundit::Authorization#pundit_policy_scope
107
132
  def policy_scope(scope)
108
133
  pundit_policy_scope(scope)
109
134
  end
data/pundit.gemspec CHANGED
@@ -16,20 +16,16 @@ Gem::Specification.new do |gem|
16
16
 
17
17
  gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
18
18
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
19
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
19
  gem.require_paths = ["lib"]
21
20
 
22
- gem.metadata = { "rubygems_mfa_required" => "true" }
21
+ gem.metadata = {
22
+ "rubygems_mfa_required" => "true",
23
+ "bug_tracker_uri" => "https://github.com/varvet/pundit/issues",
24
+ "changelog_uri" => "https://github.com/varvet/pundit/blob/main/CHANGELOG.md",
25
+ "documentation_uri" => "https://github.com/varvet/pundit/blob/main/README.md",
26
+ "homepage_uri" => "https://github.com/varvet/pundit",
27
+ "source_code_uri" => "https://github.com/varvet/pundit"
28
+ }
23
29
 
24
30
  gem.add_dependency "activesupport", ">= 3.0.0"
25
- gem.add_development_dependency "actionpack", ">= 3.0.0"
26
- gem.add_development_dependency "activemodel", ">= 3.0.0"
27
- gem.add_development_dependency "bundler"
28
- gem.add_development_dependency "pry"
29
- gem.add_development_dependency "railties", ">= 3.0.0"
30
- gem.add_development_dependency "rake"
31
- gem.add_development_dependency "rspec", ">= 3.0.0"
32
- gem.add_development_dependency "rubocop"
33
- gem.add_development_dependency "simplecov", ">= 0.17.0"
34
- gem.add_development_dependency "yard"
35
31
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
+ require "action_controller/metal/strong_parameters"
4
5
 
5
6
  describe Pundit::Authorization do
6
7
  def to_params(*args, **kwargs, &block)
@@ -8,7 +9,7 @@ describe Pundit::Authorization do
8
9
  end
9
10
 
10
11
  let(:controller) { Controller.new(user, "update", to_params({})) }
11
- let(:user) { double }
12
+ let(:user) { double("user") }
12
13
  let(:post) { Post.new(user) }
13
14
  let(:comment) { Comment.new }
14
15
  let(:article) { Article.new }
@@ -157,7 +158,7 @@ describe Pundit::Authorization do
157
158
  end
158
159
 
159
160
  it "allows policy to be injected" do
160
- new_policy = OpenStruct.new
161
+ new_policy = double
161
162
  controller.policies[post] = new_policy
162
163
 
163
164
  expect(controller.policy(post)).to eq new_policy
@@ -182,7 +183,7 @@ describe Pundit::Authorization do
182
183
  end
183
184
 
184
185
  it "allows policy_scope to be injected" do
185
- new_scope = OpenStruct.new
186
+ new_scope = double
186
187
  controller.policy_scopes[Post] = new_scope
187
188
 
188
189
  expect(controller.policy_scope(Post)).to eq new_scope
@@ -271,4 +272,60 @@ describe Pundit::Authorization do
271
272
  expect(Controller.new(user, action, params).permitted_attributes(post, :revise).to_h).to eq("body" => "blah")
272
273
  end
273
274
  end
275
+
276
+ describe "#pundit_reset!" do
277
+ it "allows authorize to react to a user change" do
278
+ expect(controller.authorize(post)).to be_truthy
279
+
280
+ controller.current_user = double
281
+ controller.pundit_reset!
282
+ expect { controller.authorize(post) }.to raise_error(Pundit::NotAuthorizedError)
283
+ end
284
+
285
+ it "allows policy to react to a user change" do
286
+ expect(controller.policy(DummyCurrentUser).user).to be user
287
+
288
+ new_user = double("new user")
289
+ controller.current_user = new_user
290
+ controller.pundit_reset!
291
+ expect(controller.policy(DummyCurrentUser).user).to be new_user
292
+ end
293
+
294
+ it "allows policy scope to react to a user change" do
295
+ expect(controller.policy_scope(DummyCurrentUser)).to be user
296
+
297
+ new_user = double("new user")
298
+ controller.current_user = new_user
299
+ controller.pundit_reset!
300
+ expect(controller.policy_scope(DummyCurrentUser)).to be new_user
301
+ end
302
+
303
+ it "resets the pundit context" do
304
+ expect(controller.pundit.user).to be(user)
305
+
306
+ new_user = double
307
+ controller.current_user = new_user
308
+ expect { controller.pundit_reset! }.to change { controller.pundit.user }.from(user).to(new_user)
309
+ end
310
+
311
+ it "clears pundit_policy_authorized? flag" do
312
+ expect(controller.pundit_policy_authorized?).to be false
313
+
314
+ controller.skip_authorization
315
+ expect(controller.pundit_policy_authorized?).to be true
316
+
317
+ controller.pundit_reset!
318
+ expect(controller.pundit_policy_authorized?).to be false
319
+ end
320
+
321
+ it "clears pundit_policy_scoped? flag" do
322
+ expect(controller.pundit_policy_scoped?).to be false
323
+
324
+ controller.skip_policy_scope
325
+ expect(controller.pundit_policy_scoped?).to be true
326
+
327
+ controller.pundit_reset!
328
+ expect(controller.pundit_policy_scoped?).to be false
329
+ end
330
+ end
274
331
  end
@@ -2,13 +2,17 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- class Foo; end
6
5
  RSpec.describe Pundit::PolicyFinder do
7
6
  let(:user) { double }
8
7
  let(:post) { Post.new(user) }
9
8
  let(:comment) { CommentFourFiveSix.new }
10
9
  let(:article) { Article.new }
11
10
 
11
+ describe "SUFFIX" do
12
+ specify { expect(described_class::SUFFIX).to eq "Policy" }
13
+ specify { expect(Pundit::SUFFIX).to eq(described_class::SUFFIX) }
14
+ end
15
+
12
16
  describe "#scope" do
13
17
  subject { described_class.new(post) }
14
18
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Pundit::Helper do
6
+ let(:user) { double }
7
+ let(:controller) { Controller.new(user, "update", double) }
8
+ let(:view) { Controller::View.new(controller) }
9
+
10
+ describe "#policy_scope" do
11
+ it "doesn't flip pundit_policy_scoped?" do
12
+ scoped = view.policy_scope(Post)
13
+
14
+ expect(scoped).to be(Post.published)
15
+ expect(controller).not_to be_pundit_policy_scoped
16
+ end
17
+ end
18
+ end
data/spec/pundit_spec.rb CHANGED
@@ -43,15 +43,6 @@ RSpec.describe Pundit do
43
43
  expect(Pundit.authorize(user, Post, :show?)).to eq(Post)
44
44
  end
45
45
 
46
- it "can be given a different policy class" do
47
- expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy
48
- end
49
-
50
- it "can be given a different policy class using namespaces" do
51
- expect(PublicationPolicy).to receive(:new).with(user, comment).and_call_original
52
- expect(Pundit.authorize(user, [:project, comment], :create?, policy_class: PublicationPolicy)).to be_truthy
53
- end
54
-
55
46
  it "works with anonymous class policies" do
56
47
  expect(Pundit.authorize(user, article_tag, :show?)).to be_truthy
57
48
  expect { Pundit.authorize(user, article_tag, :destroy?) }.to raise_error(Pundit::NotAuthorizedError)
@@ -111,6 +102,41 @@ RSpec.describe Pundit do
111
102
  Pundit.authorize(user, wiki, :update?)
112
103
  end.to raise_error(Pundit::InvalidConstructorError, "Invalid #<WikiPolicy> constructor is called")
113
104
  end
105
+
106
+ context "when passed a policy class" do
107
+ it "uses the passed policy class" do
108
+ expect(Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy)).to be_truthy
109
+ end
110
+
111
+ # This is documenting past behaviour.
112
+ it "doesn't cache the policy class" do
113
+ cache = {}
114
+
115
+ expect do
116
+ Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache)
117
+ Pundit.authorize(user, post, :create?, policy_class: PublicationPolicy, cache: cache)
118
+ end.to change { PublicationPolicy.instances }.by(2)
119
+ end
120
+ end
121
+
122
+ context "when passed a policy class while simultaenously passing a namespace" do
123
+ it "uses the passed policy class" do
124
+ expect(PublicationPolicy).to receive(:new).with(user, comment).and_call_original
125
+ expect(Pundit.authorize(user, [:project, comment], :create?, policy_class: PublicationPolicy)).to be_truthy
126
+ end
127
+ end
128
+
129
+ context "when passed an explicit cache" do
130
+ it "uses the hash assignment interface on the cache" do
131
+ custom_cache = CustomCache.new
132
+
133
+ Pundit.authorize(user, post, :update?, cache: custom_cache)
134
+
135
+ expect(custom_cache.to_h).to match({
136
+ post => kind_of(PostPolicy)
137
+ })
138
+ end
139
+ end
114
140
  end
115
141
 
116
142
  describe ".policy_scope" do
@@ -154,8 +180,8 @@ RSpec.describe Pundit do
154
180
 
155
181
  it "raises an original error with a policy scope that contains error" do
156
182
  expect do
157
- Pundit.policy_scope(user, Thread)
158
- end.to raise_error(ArgumentError)
183
+ Pundit.policy_scope(user, DefaultScopeContainsError)
184
+ end.to raise_error(RuntimeError, "This is an arbitrary error that should bubble up")
159
185
  end
160
186
  end
161
187
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Pundit RSpec DSL" do
6
+ include Pundit::RSpec::PolicyExampleGroup
7
+
8
+ let(:fake_rspec) do
9
+ double = class_double(RSpec::ExampleGroups)
10
+ double.extend(::Pundit::RSpec::DSL)
11
+ double
12
+ end
13
+ let(:block) { proc { "block content" } }
14
+
15
+ let(:user) { double }
16
+ let(:other_user) { double }
17
+ let(:post) { Post.new(user) }
18
+ let(:policy) { PostPolicy }
19
+
20
+ it "calls describe with the correct metadata and without :focus" do
21
+ expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array) }
22
+ expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
23
+ expect(block.call).to eq("block content")
24
+ end
25
+
26
+ fake_rspec.permissions(:item1, :item2, &block)
27
+ end
28
+
29
+ it "calls describe with the correct metadata and with :focus" do
30
+ expected_metadata = { permissions: %i[item1 item2], caller: instance_of(Array), focus: true }
31
+ expect(fake_rspec).to receive(:describe).with("item1 and item2", match(expected_metadata)) do |&block|
32
+ expect(block.call).to eq("block content")
33
+ end
34
+
35
+ fake_rspec.permissions(:item1, :item2, :focus, &block)
36
+ end
37
+
38
+ describe "#permit" do
39
+ context "when not appropriately wrapped in permissions" do
40
+ it "raises a descriptive error" do
41
+ expect do
42
+ expect(policy).to permit(user, post)
43
+ end.to raise_error(KeyError, <<~MSG.strip)
44
+ No permissions in example metadata, did you forget to wrap with `permissions :show?, ...`?
45
+ MSG
46
+ end
47
+ end
48
+
49
+ permissions :edit?, :update? do
50
+ it "succeeds when action is permitted" do
51
+ expect(policy).to permit(user, post)
52
+ end
53
+
54
+ context "when it fails" do
55
+ it "fails with a descriptive error message" do
56
+ expect do
57
+ expect(policy).to permit(other_user, post)
58
+ end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
59
+ Expected PostPolicy to grant edit? and update? on Post but edit? and update? were not granted
60
+ MSG
61
+ end
62
+ end
63
+
64
+ context "when negated" do
65
+ it "succeeds when action is not permitted" do
66
+ expect(policy).not_to permit(other_user, post)
67
+ end
68
+
69
+ context "when it fails" do
70
+ it "fails with a descriptive error message" do
71
+ expect do
72
+ expect(policy).not_to permit(user, post)
73
+ end.to raise_error(RSpec::Expectations::ExpectationNotMetError, <<~MSG.strip)
74
+ Expected PostPolicy not to grant edit? and update? on Post but edit? and update? were granted
75
+ MSG
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov"
4
+ require "json"
5
+
6
+ class SimpleCovCheckActionFormatter
7
+ SourceFile = Data.define(:source_file) do
8
+ def covered_strength = source_file.covered_strength
9
+ def covered_percent = source_file.covered_percent
10
+
11
+ def to_json(*args)
12
+ {
13
+ filename: source_file.filename,
14
+ covered_percent: covered_percent.nan? ? 0.0 : covered_percent,
15
+ coverage: source_file.coverage_data,
16
+ covered_strength: covered_strength.nan? ? 0.0 : covered_strength,
17
+ covered_lines: source_file.covered_lines.count,
18
+ lines_of_code: source_file.lines_of_code
19
+ }.to_json(*args)
20
+ end
21
+ end
22
+
23
+ Result = Data.define(:result) do
24
+ def included?(source_file) = result.filenames.include?(source_file.filename)
25
+
26
+ def files
27
+ result.files.filter_map do |source_file|
28
+ next unless result.filenames.include? source_file.filename
29
+
30
+ SourceFile.new(source_file)
31
+ end
32
+ end
33
+
34
+ def to_json(*args) # rubocop:disable Metrics/AbcSize
35
+ {
36
+ timestamp: result.created_at.to_i,
37
+ command_name: result.command_name,
38
+ files: files,
39
+ metrics: {
40
+ covered_percent: result.covered_percent,
41
+ covered_strength: result.covered_strength.nan? ? 0.0 : result.covered_strength,
42
+ covered_lines: result.covered_lines,
43
+ total_lines: result.total_lines
44
+ }
45
+ }.to_json(*args)
46
+ end
47
+ end
48
+
49
+ FormatterWithOptions = Data.define(:formatter) do
50
+ def new = formatter
51
+ end
52
+
53
+ class << self
54
+ def with_options(...)
55
+ FormatterWithOptions.new(new(...))
56
+ end
57
+ end
58
+
59
+ def initialize(output_filename: "coverage.json", output_directory: SimpleCov.coverage_path)
60
+ @output_filename = output_filename
61
+ @output_directory = output_directory
62
+ end
63
+
64
+ attr_reader :output_filename, :output_directory
65
+
66
+ def output_filepath = File.join(output_directory, output_filename)
67
+
68
+ def format(result_data)
69
+ result = Result.new(result_data)
70
+ json = JSON.generate(result)
71
+ File.write(output_filepath, json)
72
+ puts output_message(result_data)
73
+ json
74
+ end
75
+
76
+ def output_message(result)
77
+ "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
78
+ end
79
+ end