pundit-matchers 1.7.0 → 3.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0da80bb866c35b8ab6548d017a9bc022831e3da0c5493b99aac0ba5c8c79cad
4
- data.tar.gz: 24bfacd140e3976e30c88204db5e33566c42dbd5e45d568c8d857a7a4205951d
3
+ metadata.gz: 379030415885d5cd2557f7540639e6c835f53cfa5a88895fd3e5f7cbbdf1b88f
4
+ data.tar.gz: 5a5e5f7586d31f4328e3e0b5bf722d3465a681ef77340a90d44b6517f04eb700
5
5
  SHA512:
6
- metadata.gz: 5c1fbddf259fce9fa65c6f0613ec5d46e271ab0c7212380bc86cb5fef6fb85a2f74b6c20a4fb6b4c0e1dacc6b8ad11e1c963f8827c997e88bb319cce157d305b
7
- data.tar.gz: a2363786904df0631c54b8d84094179b61607e300a16ba53cd6406b02272e51f1e21060832f5b64a3eeaaba1ff6dba7c8462d3308732f335dc363df666058120
6
+ metadata.gz: 5c513efb4f063cd3bd15d43c0a68d001bb4ca4ef806861b27cc0de39f7102d6aab8be0ee2b64213b83f8991d38d9e3f62ae6f7b0c03f0ae87f2f1f71fba87b0c
7
+ data.tar.gz: a32970e872d608219955154cdd43812b36a4d0d37363e09d0e17e341f297c23afb0e947756711b1df3d1c0aad3bcc48a43935663072b87656f196bfdb049afab
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This is the base action matcher class. Matchers related to actions should inherit from this class.
8
+ class ActionsMatcher < BaseMatcher
9
+ # Error message when actions are not implemented in a policy.
10
+ ACTIONS_NOT_IMPLEMENTED_ERROR = "'%<policy>s' does not implement %<actions>s"
11
+ # Error message when at least one action must be specified.
12
+ ARGUMENTS_REQUIRED_ERROR = 'At least one action must be specified'
13
+ # Error message when only one action may be specified.
14
+ ONE_ARGUMENT_REQUIRED_ERROR = 'Only one action may be specified'
15
+
16
+ # Initializes a new instance of the ActionsMatcher class.
17
+ #
18
+ # @param expected_actions [Array<String, Symbol>] The expected actions to be checked.
19
+ #
20
+ # @raise [ArgumentError] If no actions are specified.
21
+ def initialize(*expected_actions)
22
+ raise ArgumentError, ARGUMENTS_REQUIRED_ERROR if expected_actions.empty?
23
+
24
+ super()
25
+ @expected_actions = expected_actions.flatten.map(&:to_sym).sort
26
+ end
27
+
28
+ # Ensures that only one action is specified.
29
+ #
30
+ # @raise [ArgumentError] If more than one action is specified.
31
+ #
32
+ # @return [ActionsMatcher] The object itself.
33
+ def ensure_single_action!
34
+ raise ArgumentError, ONE_ARGUMENT_REQUIRED_ERROR if expected_actions.size > 1
35
+
36
+ self
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :expected_actions
42
+
43
+ def check_actions!
44
+ non_explicit_actions = (expected_actions - policy_info.actions)
45
+ missing_actions = non_explicit_actions.reject { |action| policy_info.policy.respond_to?(:"#{action}?") }
46
+ return if missing_actions.empty?
47
+
48
+ raise ArgumentError, format(
49
+ ACTIONS_NOT_IMPLEMENTED_ERROR,
50
+ policy: policy_info, actions: missing_actions
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # The AttributesMatcher class is used to test whether a Pundit policy allows or denies access to certain attributes.
8
+ class AttributesMatcher < BaseMatcher
9
+ # Error message to be raised when no attributes are specified.
10
+ ARGUMENTS_REQUIRED_ERROR = 'At least one attribute must be specified'
11
+ # Error message to be raised when only one attribute may be specified.
12
+ ONE_ARGUMENT_REQUIRED_ERROR = 'Only one attribute may be specified'
13
+
14
+ # Initializes a new instance of the AttributesMatcher class.
15
+ #
16
+ # @param expected_attributes [Array<String, Symbol, Hash>] The list of attributes to be tested.
17
+ def initialize(*expected_attributes)
18
+ raise ArgumentError, ARGUMENTS_REQUIRED_ERROR if expected_attributes.empty?
19
+
20
+ super()
21
+ @expected_attributes = flatten_attributes(expected_attributes)
22
+ @options = {}
23
+ end
24
+
25
+ # Specifies the action to be tested.
26
+ #
27
+ # @param action [Symbol, String] The action to be tested.
28
+ # @return [AttributesMatcher] The current instance of the AttributesMatcher class.
29
+ def for_action(action)
30
+ @options[:action] = action
31
+ self
32
+ end
33
+
34
+ # Ensures that only one attribute is specified.
35
+ #
36
+ # @raise [ArgumentError] If more than one attribute is specified.
37
+ #
38
+ # @return [AttributesMatcher] The object itself.
39
+ def ensure_single_attribute!
40
+ raise ArgumentError, ONE_ARGUMENT_REQUIRED_ERROR if expected_attributes.size > 1
41
+
42
+ self
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :expected_attributes, :options
48
+
49
+ def permitted_attributes(policy)
50
+ @permitted_attributes ||=
51
+ if options.key?(:action)
52
+ flatten_attributes(policy.public_send(:"permitted_attributes_for_#{options[:action]}"))
53
+ else
54
+ flatten_attributes(policy.permitted_attributes)
55
+ end
56
+ end
57
+
58
+ def action_message
59
+ " when authorising the '#{options[:action]}' action"
60
+ end
61
+
62
+ # Flattens and sorts a hash or array of attributes into an array of symbols.
63
+ #
64
+ # This is a private method used internally by the `Matcher` class to convert
65
+ # attribute lists into a flattened, sorted array of symbols. The resulting
66
+ # array can be used to compare attribute lists.
67
+ #
68
+ # @param attributes [String, Symbol, Array, Hash] the attributes to be flattened.
69
+ # @return [Array<Symbol>] the flattened, sorted array of symbols.
70
+ def flatten_attributes(attributes)
71
+ case attributes
72
+ when String, Symbol
73
+ [attributes.to_sym]
74
+ when Array
75
+ attributes.flat_map { |item| flatten_attributes(item) }.sort
76
+ when Hash
77
+ attributes.flat_map do |key, value|
78
+ flatten_attributes(value).map { |item| :"#{key}[#{item}]" }
79
+ end.sort
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils/policy_info'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This is the base class for all matchers in the Pundit Matchers library.
8
+ class BaseMatcher
9
+ # Error message when an ambiguous negated matcher is used.
10
+ AMBIGUOUS_NEGATED_MATCHER_ERROR = <<~MSG
11
+ `expect().not_to %<name>s` is not supported since it creates ambiguity.
12
+ MSG
13
+
14
+ private
15
+
16
+ attr_reader :policy_info
17
+
18
+ def setup_policy_info!(policy)
19
+ @policy_info = Pundit::Matchers::Utils::PolicyInfo.new(policy)
20
+ end
21
+
22
+ def user_message
23
+ " for '#{policy_info.user}'"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy forbids all actions.
8
+ class ForbidAllActionsMatcher < BaseMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] Description of the matcher.
12
+ def description
13
+ 'forbid all actions'
14
+ end
15
+
16
+ # Checks if the given policy forbids all actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy forbids all actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+
23
+ policy_info.permitted_actions.empty?
24
+ end
25
+
26
+ # Raises a NotImplementedError
27
+ # @raise NotImplementedError
28
+ # @return [void]
29
+ def does_not_match?(_policy)
30
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'forbid_all_actions')
31
+ end
32
+
33
+ # Returns a failure message if the matcher fails.
34
+ #
35
+ # @return [String] Failure message.
36
+ def failure_message
37
+ message = +"expected '#{policy_info}' to forbid all actions,"
38
+ message << " but permitted #{policy_info.permitted_actions}"
39
+ message << user_message
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'actions_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy forbids only the expected actions.
8
+ class ForbidOnlyActionsMatcher < ActionsMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] Description of the matcher.
12
+ def description
13
+ "forbid only #{expected_actions}"
14
+ end
15
+
16
+ # Checks if the given policy forbids only the expected actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy forbids only the expected actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+ check_actions!
23
+
24
+ @actual_actions = policy_info.forbidden_actions - expected_actions
25
+ @extra_actions = policy_info.permitted_actions & expected_actions
26
+
27
+ actual_actions.empty? && extra_actions.empty?
28
+ end
29
+
30
+ # Raises a NotImplementedError
31
+ # @raise NotImplementedError
32
+ # @return [void]
33
+ def does_not_match?(_policy)
34
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'forbid_only_actions')
35
+ end
36
+
37
+ # The failure message when the expected actions and the forbidden actions do not match.
38
+ #
39
+ # @return [String] A failure message when the expected actions and the forbidden actions do not match.
40
+ def failure_message
41
+ message = +"expected '#{policy_info}' to forbid only #{expected_actions},"
42
+ message << " but forbade #{actual_actions}" unless actual_actions.empty?
43
+ message << extra_message unless extra_actions.empty?
44
+ message << user_message
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :actual_actions, :extra_actions
50
+
51
+ def extra_message
52
+ if actual_actions.empty?
53
+ " but permitted #{extra_actions}"
54
+ else
55
+ " and permitted #{extra_actions}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'actions_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits or forbids the expected actions.
8
+ class PermitActionsMatcher < ActionsMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] Description of the matcher.
12
+ def description
13
+ "permit #{expected_actions}"
14
+ end
15
+
16
+ # Checks if the given policy permits the expected actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits the expected actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+ check_actions!
23
+
24
+ @actual_actions = expected_actions.reject do |action|
25
+ policy.public_send(:"#{action}?")
26
+ end
27
+
28
+ actual_actions.empty?
29
+ end
30
+
31
+ # Checks if the given policy forbids the expected actions.
32
+ #
33
+ # @param policy [Object] The policy to test.
34
+ # @return [Boolean] True if the policy forbids the expected actions, false otherwise.
35
+ def does_not_match?(policy)
36
+ setup_policy_info! policy
37
+ check_actions!
38
+
39
+ @actual_actions = expected_actions.select do |action|
40
+ policy.public_send(:"#{action}?")
41
+ end
42
+
43
+ actual_actions.empty?
44
+ end
45
+
46
+ # Returns a failure message when the expected actions are forbidden.
47
+ #
48
+ # @return [String] A failure message when the expected actions are not forbidden.
49
+ def failure_message
50
+ message = +"expected '#{policy_info}' to permit #{expected_actions},"
51
+ message << " but forbade #{actual_actions}"
52
+ message << user_message
53
+ end
54
+
55
+ # Returns a failure message when the expected actions are permitted.
56
+ #
57
+ # @return [String] A failure message when the expected actions are permitted.
58
+ def failure_message_when_negated
59
+ message = +"expected '#{policy_info}' to forbid #{expected_actions},"
60
+ message << " but permitted #{actual_actions}"
61
+ message << user_message
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :actual_actions
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits all actions.
8
+ class PermitAllActionsMatcher < BaseMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] A description of the matcher.
12
+ def description
13
+ 'permit all actions'
14
+ end
15
+
16
+ # Checks if the given policy permits all actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits all actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+
23
+ policy_info.forbidden_actions.empty?
24
+ end
25
+
26
+ # Raises a NotImplementedError
27
+ # @raise NotImplementedError
28
+ # @return [void]
29
+ def does_not_match?(_policy)
30
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'permit_all_actions')
31
+ end
32
+
33
+ # Returns a failure message when the policy does not permit all actions.
34
+ #
35
+ # @return [String] A failure message when the policy does not permit all actions.
36
+ def failure_message
37
+ message = +"expected '#{policy_info}' to permit all actions,"
38
+ message << " but forbade #{policy_info.forbidden_actions}"
39
+ message << user_message
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attributes_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits or forbids the mass assignment of the expected attributes.
8
+ class PermitAttributesMatcher < AttributesMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] A description of the matcher.
12
+ def description
13
+ "permit the mass assignment of #{expected_attributes}"
14
+ end
15
+
16
+ # Checks if the given policy permits the mass assignment of the expected attributes.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits the mass assignment of the expected attributes, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+
23
+ @actual_attributes = expected_attributes - permitted_attributes(policy)
24
+
25
+ actual_attributes.empty?
26
+ end
27
+
28
+ # Checks if the given policy forbids the mass assignment of the expected attributes.
29
+ #
30
+ # @param policy [Object] The policy to test.
31
+ # @return [Boolean] True if the policy forbids the mass assignment of the expected attributes, false otherwise.
32
+ def does_not_match?(policy)
33
+ setup_policy_info! policy
34
+
35
+ @actual_attributes = expected_attributes & permitted_attributes(policy)
36
+
37
+ actual_attributes.empty?
38
+ end
39
+
40
+ # The failure message when the expected attributes are forbidden.
41
+ #
42
+ # @return [String] A failure message when the expected attributes are not permitted.
43
+ def failure_message
44
+ message = +"expected '#{policy_info}' to permit the mass assignment of #{expected_attributes}"
45
+ message << action_message if options.key?(:action)
46
+ message << ", but forbade the mass assignment of #{actual_attributes}"
47
+ message << user_message
48
+ end
49
+
50
+ # The failure message when the expected attributes are permitted.
51
+ #
52
+ # @return [String] A failure message when the expected attributes are forbidden.
53
+ def failure_message_when_negated
54
+ message = +"expected '#{policy_info}' to forbid the mass assignment of #{expected_attributes}"
55
+ message << action_message if options.key?(:action)
56
+ message << ", but permitted the mass assignment of #{actual_attributes}"
57
+ message << user_message
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :actual_attributes
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'actions_matcher'
4
+
5
+ module Pundit
6
+ module Matchers
7
+ # This matcher tests whether a policy permits only the expected actions.
8
+ class PermitOnlyActionsMatcher < ActionsMatcher
9
+ # A description of the matcher.
10
+ #
11
+ # @return [String] A description of the matcher.
12
+ def description
13
+ "permit only #{expected_actions}"
14
+ end
15
+
16
+ # Checks if the given policy permits only the expected actions.
17
+ #
18
+ # @param policy [Object] The policy to test.
19
+ # @return [Boolean] True if the policy permits only the expected actions, false otherwise.
20
+ def matches?(policy)
21
+ setup_policy_info! policy
22
+ check_actions!
23
+
24
+ @actual_actions = policy_info.permitted_actions - expected_actions
25
+ @extra_actions = policy_info.forbidden_actions & expected_actions
26
+
27
+ actual_actions.empty? && extra_actions.empty?
28
+ end
29
+
30
+ # Raises a NotImplementedError
31
+ # @raise NotImplementedError
32
+ # @return [void]
33
+ def does_not_match?(_policy)
34
+ raise NotImplementedError, format(AMBIGUOUS_NEGATED_MATCHER_ERROR, name: 'permit_only_actions')
35
+ end
36
+
37
+ # The failure message when the expected actions and the permitted actions do not match.
38
+ #
39
+ # @return [String] A failure message when the expected actions and the permitted actions do not match.
40
+ def failure_message
41
+ message = +"expected '#{policy_info}' to permit only #{expected_actions},"
42
+ message << " but permitted #{actual_actions}" unless actual_actions.empty?
43
+ message << extra_message unless extra_actions.empty?
44
+ message << user_message
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :actual_actions, :extra_actions
50
+
51
+ def extra_message
52
+ if actual_actions.empty?
53
+ " but forbade #{extra_actions}"
54
+ else
55
+ " and forbade #{extra_actions}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pundit
4
+ module Matchers
5
+ module Utils
6
+ # This class provides methods to retrieve information about a policy class,
7
+ # such as the actions it defines and which of those actions are permitted
8
+ # or forbidden. It also provides a string representation of the policy class name
9
+ # and the user object associated with the policy.
10
+ class PolicyInfo
11
+ # Error message when policy does not respond to `user_alias`.
12
+ USER_NOT_IMPLEMENTED_ERROR = <<~MSG
13
+ '%<policy>s' does not implement '%<user_alias>s'. You may want to
14
+ configure an alias, which you can do as follows:
15
+
16
+ Pundit::Matchers.configure do |config|
17
+ # Alias for all policies
18
+ config.default_user_alias = :%<user_alias>s
19
+
20
+ # Per-policy alias
21
+ config.user_aliases = { '%<policy>s' => :%<user_alias>s }
22
+ end
23
+ MSG
24
+
25
+ attr_reader :policy
26
+
27
+ # Initializes a new instance of PolicyInfo.
28
+ #
29
+ # @param policy [Class] The policy class to collect details about.
30
+ def initialize(policy)
31
+ @policy = policy
32
+ check_user_alias!
33
+ end
34
+
35
+ # Returns a string representation of the policy class name.
36
+ #
37
+ # @return [String] A string representation of the policy class name.
38
+ def to_s
39
+ policy.class.name
40
+ end
41
+
42
+ # Returns the user object associated with the policy.
43
+ #
44
+ # @return [Object] The user object associated with the policy.
45
+ def user
46
+ @user ||= policy.public_send(user_alias)
47
+ end
48
+
49
+ # Returns an array of all actions defined in the policy class.
50
+ #
51
+ # It assumes that actions are defined as public instance methods that end with a question mark.
52
+ #
53
+ # @return [Array<Symbol>] An array of all actions defined in the policy class.
54
+ def actions
55
+ @actions ||= policy_public_methods.grep(/\?$/).sort.map do |policy_method|
56
+ policy_method.to_s.delete_suffix('?').to_sym
57
+ end
58
+ end
59
+
60
+ # Returns an array of all permitted actions defined in the policy class.
61
+ #
62
+ # @return [Array<Symbol>] An array of all permitted actions defined in the policy class.
63
+ def permitted_actions
64
+ @permitted_actions ||= actions.select { |action| policy.public_send(:"#{action}?") }
65
+ end
66
+
67
+ # Returns an array of all forbidden actions defined in the policy class.
68
+ #
69
+ # @return [Array<Symbol>] An array of all forbidden actions defined in the policy class.
70
+ def forbidden_actions
71
+ @forbidden_actions ||= actions - permitted_actions
72
+ end
73
+
74
+ private
75
+
76
+ def policy_public_methods
77
+ @policy_public_methods ||= policy.public_methods - Object.instance_methods
78
+ end
79
+
80
+ def user_alias
81
+ @user_alias ||= Pundit::Matchers.configuration.user_alias(policy)
82
+ end
83
+
84
+ def check_user_alias!
85
+ return if policy.respond_to?(user_alias)
86
+
87
+ raise ArgumentError, format(USER_NOT_IMPLEMENTED_ERROR, policy: self, user_alias: user_alias)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,373 +1,186 @@
1
- require 'rspec/core'
2
-
3
- module Pundit
4
- module Matchers
5
- class Configuration
6
- attr_accessor :user_alias
7
-
8
- def initialize
9
- @user_alias = :user
10
- end
11
- end
12
-
13
- class << self
14
- def configure
15
- yield(configuration)
16
- end
17
-
18
- def configuration
19
- @configuration ||= Pundit::Matchers::Configuration.new
20
- end
21
- end
22
-
23
- RSpec::Matchers.define :forbid_action do |action, *args|
24
- match do |policy|
25
- if args.any?
26
- !policy.public_send("#{action}?", *args)
27
- else
28
- !policy.public_send("#{action}?")
29
- end
30
- end
1
+ # frozen_string_literal: true
31
2
 
32
- failure_message do |policy|
33
- "#{policy.class} does not forbid #{action} for " +
34
- policy.public_send(Pundit::Matchers.configuration.user_alias)
35
- .inspect + '.'
36
- end
37
-
38
- failure_message_when_negated do |policy|
39
- "#{policy.class} does not permit #{action} for " +
40
- policy.public_send(Pundit::Matchers.configuration.user_alias)
41
- .inspect + '.'
42
- end
43
- end
44
- end
45
-
46
- RSpec::Matchers.define :forbid_actions do |*actions|
47
- actions.flatten!
48
- match do |policy|
49
- return false if actions.count < 1
50
- @allowed_actions = actions.select do |action|
51
- policy.public_send("#{action}?")
52
- end
53
- @allowed_actions.empty?
54
- end
55
-
56
- attr_reader :allowed_actions
57
-
58
- zero_actions_failure_message = 'At least one action must be ' \
59
- 'specified when using the forbid_actions matcher.'
60
-
61
- failure_message do |policy|
62
- if actions.count.zero?
63
- zero_actions_failure_message
64
- else
65
- "#{policy.class} expected to forbid #{actions}, but allowed " \
66
- "#{allowed_actions} for " +
67
- policy.public_send(Pundit::Matchers.configuration.user_alias)
68
- .inspect + '.'
69
- end
70
- end
71
-
72
- failure_message_when_negated do |policy|
73
- if actions.count.zero?
74
- zero_actions_failure_message
75
- else
76
- "#{policy.class} expected to permit #{actions}, but forbade " \
77
- "#{allowed_actions} for " +
78
- policy.public_send(Pundit::Matchers.configuration.user_alias)
79
- .inspect + '.'
80
- end
81
- end
82
- end
83
-
84
- RSpec::Matchers.define :forbid_edit_and_update_actions do
85
- match do |policy|
86
- !policy.edit? && !policy.update?
87
- end
88
-
89
- failure_message do |policy|
90
- "#{policy.class} does not forbid the edit or update action for " +
91
- policy.public_send(Pundit::Matchers.configuration.user_alias)
92
- .inspect + '.'
93
- end
3
+ require 'rspec/core'
94
4
 
95
- failure_message_when_negated do |policy|
96
- "#{policy.class} does not permit the edit or update action for " +
97
- policy.public_send(Pundit::Matchers.configuration.user_alias)
98
- .inspect + '.'
99
- end
100
- end
5
+ require_relative 'matchers/permit_actions_matcher'
101
6
 
102
- RSpec::Matchers.define :forbid_mass_assignment_of do |attributes|
103
- # Map single object argument to an array, if necessary
104
- attributes = attributes.is_a?(Array) ? attributes : [attributes]
7
+ require_relative 'matchers/permit_attributes_matcher'
105
8
 
106
- match do |policy|
107
- return false if attributes.count < 1
9
+ require_relative 'matchers/forbid_all_actions_matcher'
10
+ require_relative 'matchers/forbid_only_actions_matcher'
108
11
 
109
- @allowed_attributes = attributes.select do |attribute|
110
- if defined? @action
111
- policy.send("permitted_attributes_for_#{@action}").include? attribute
112
- else
113
- policy.permitted_attributes.include? attribute
114
- end
115
- end
12
+ require_relative 'matchers/permit_all_actions_matcher'
13
+ require_relative 'matchers/permit_only_actions_matcher'
116
14
 
117
- @allowed_attributes.empty?
118
- end
15
+ module Pundit
16
+ # Matchers module provides a set of RSpec matchers for testing Pundit policies.
17
+ module Matchers
18
+ # A Proc that negates the description of a matcher.
19
+ NEGATED_DESCRIPTION = ->(description) { description.gsub(/^permit/, 'forbid') }
119
20
 
120
- attr_reader :allowed_attributes
21
+ # Configuration class for Pundit Matchers.
22
+ class Configuration
23
+ # The default user object value
24
+ DEFAULT_USER_ALIAS = :user
121
25
 
122
- chain :for_action do |action|
123
- @action = action
124
- end
26
+ # The default user object in policies.
27
+ # @return [Symbol|String]
28
+ attr_accessor :default_user_alias
125
29
 
126
- zero_attributes_failure_message = 'At least one attribute must be ' \
127
- 'specified when using the forbid_mass_assignment_of matcher.'
30
+ # Policy-specific user objects.
31
+ #
32
+ # @example Use +:client+ as user alias for class +Post+
33
+ # config.user_aliases = { 'Post' => :client }
34
+ #
35
+ # @return [Hash]
36
+ attr_accessor :user_aliases
128
37
 
129
- failure_message do |policy|
130
- if attributes.count.zero?
131
- zero_attributes_failure_message
132
- elsif defined? @action
133
- "#{policy.class} expected to forbid the mass assignment of the " \
134
- "attributes #{attributes} when authorising the #{@action} action, " \
135
- 'but allowed the mass assignment of the attributes ' \
136
- "#{allowed_attributes} for " +
137
- policy.public_send(Pundit::Matchers.configuration.user_alias)
138
- .inspect + '.'
139
- else
140
- "#{policy.class} expected to forbid the mass assignment of the " \
141
- "attributes #{attributes}, but allowed the mass assignment of " \
142
- "the attributes #{allowed_attributes} for " +
143
- policy.public_send(Pundit::Matchers.configuration.user_alias)
144
- .inspect + '.'
38
+ def initialize
39
+ @default_user_alias = DEFAULT_USER_ALIAS
40
+ @user_aliases = {}
145
41
  end
146
- end
147
42
 
148
- failure_message_when_negated do |policy|
149
- if attributes.count.zero?
150
- zero_attributes_failure_message
151
- elsif defined? @action
152
- "#{policy.class} expected to permit the mass assignment of the " \
153
- "attributes #{attributes} when authorising the #{@action} action, " \
154
- 'but permitted the mass assignment of the attributes ' \
155
- "#{allowed_attributes} for " +
156
- policy.public_send(Pundit::Matchers.configuration.user_alias)
157
- .inspect + '.'
158
- else
159
- "#{policy.class} expected to permit the mass assignment of the " \
160
- "attributes #{attributes}, but permitted the mass assignment of " \
161
- "the attributes #{allowed_attributes} for " +
162
- policy.public_send(Pundit::Matchers.configuration.user_alias)
163
- .inspect + '.'
43
+ # Returns the user object for the given policy.
44
+ #
45
+ # @return [Symbol]
46
+ def user_alias(policy)
47
+ user_aliases.fetch(policy.class.name, default_user_alias)
164
48
  end
165
49
  end
166
- end
167
-
168
- RSpec::Matchers.define :forbid_new_and_create_actions do
169
- match do |policy|
170
- !policy.new? && !policy.create?
171
- end
172
-
173
- failure_message do |policy|
174
- "#{policy.class} does not forbid the new or create action for " +
175
- policy.public_send(Pundit::Matchers.configuration.user_alias)
176
- .inspect + '.'
177
- end
178
-
179
- failure_message_when_negated do |policy|
180
- "#{policy.class} does not permit the new or create action for " +
181
- policy.public_send(Pundit::Matchers.configuration.user_alias)
182
- .inspect + '.'
183
- end
184
- end
185
50
 
186
- RSpec::Matchers.define :permit_action do |action, *args|
187
- match do |policy|
188
- if args.any?
189
- policy.public_send("#{action}?", *args)
190
- else
191
- policy.public_send("#{action}?")
51
+ class << self
52
+ # Configures Pundit Matchers.
53
+ #
54
+ # @yieldparam [Configuration] configuration the configuration object to be modified.
55
+ def configure
56
+ yield(configuration)
192
57
  end
193
- end
194
58
 
195
- failure_message do |policy|
196
- "#{policy.class} does not permit #{action} for " +
197
- policy.public_send(Pundit::Matchers.configuration.user_alias)
198
- .inspect + '.'
199
- end
200
-
201
- failure_message_when_negated do |policy|
202
- "#{policy.class} does not forbid #{action} for " +
203
- policy.public_send(Pundit::Matchers.configuration.user_alias)
204
- .inspect + '.'
205
- end
206
- end
207
-
208
- RSpec::Matchers.define :permit_actions do |*actions|
209
- actions.flatten!
210
- match do |policy|
211
- return false if actions.count < 1
212
- @forbidden_actions = actions.reject do |action|
213
- policy.public_send("#{action}?")
59
+ # Returns the configuration object for Pundit Matchers.
60
+ #
61
+ # @return [Configuration] the configuration object.
62
+ def configuration
63
+ @configuration ||= Pundit::Matchers::Configuration.new
214
64
  end
215
- @forbidden_actions.empty?
216
65
  end
217
66
 
218
- match_when_negated do |policy|
219
- ::Kernel.warn 'Using expect { }.not_to permit_actions could produce \
220
- confusing results. Please use `.to forbid_actions` instead. To \
221
- clarify, `.not_to permit_actions` will look at all of the actions and \
222
- checks if ANY actions fail, not if all actions fail. Therefore, you \
223
- could result in something like this: \
224
-
225
- it { is_expected.to permit_actions([:new, :create, :edit]) } \
226
- it { is_expected.not_to permit_actions([:edit, :destroy]) } \
227
-
228
- In this case, edit would be true and destroy would be false, but both \
229
- tests would pass.'
230
-
231
- return true if actions.count < 1
232
- @forbidden_actions = actions.reject do |action|
233
- policy.public_send("#{action}?")
234
- end
235
- !@forbidden_actions.empty?
67
+ # Creates a matcher that tests if the policy permits a given action.
68
+ #
69
+ # @param [String|Symbol] action the action to be tested.
70
+ # @return [PermitActionsMatcher] the matcher object.
71
+ def permit_action(action)
72
+ PermitActionsMatcher.new(action).ensure_single_action!
236
73
  end
237
74
 
238
- attr_reader :forbidden_actions
239
-
240
- zero_actions_failure_message = 'At least one action must be specified ' \
241
- 'when using the permit_actions matcher.'
75
+ # @!macro [attach] RSpec::Matchers.define_negated_matcher
76
+ # @!method $1
77
+ #
78
+ # The negated matcher of {$2}.
79
+ #
80
+ # Same as +expect(policy).not_to $2(*args)+.
81
+ RSpec::Matchers.define_negated_matcher :forbid_action, :permit_action, &NEGATED_DESCRIPTION
242
82
 
243
- failure_message do |policy|
244
- if actions.count.zero?
245
- zero_actions_failure_message
246
- else
247
- "#{policy.class} expected to permit #{actions}, but forbade " \
248
- "#{forbidden_actions} for " +
249
- policy.public_send(Pundit::Matchers.configuration.user_alias)
250
- .inspect + '.'
251
- end
83
+ # Creates a matcher that tests if the policy permits a set of actions.
84
+ #
85
+ # @param [Array<String, Symbol>] actions the actions to be tested.
86
+ # @return [PermitActionsMatcher] the matcher object.
87
+ def permit_actions(*actions)
88
+ PermitActionsMatcher.new(*actions)
252
89
  end
253
90
 
254
- failure_message_when_negated do |policy|
255
- if actions.count.zero?
256
- zero_actions_failure_message
257
- else
258
- "#{policy.class} expected to forbid #{actions}, but allowed " \
259
- "#{forbidden_actions} for " +
260
- policy.public_send(Pundit::Matchers.configuration.user_alias)
261
- .inspect + '.'
262
- end
263
- end
264
- end
91
+ RSpec::Matchers.define_negated_matcher :forbid_actions, :permit_actions, &NEGATED_DESCRIPTION
265
92
 
266
- RSpec::Matchers.define :permit_edit_and_update_actions do
267
- match do |policy|
268
- policy.edit? && policy.update?
93
+ # Creates a matcher that tests if the policy permits all actions.
94
+ #
95
+ # @note The negative form +not_to permit_all_actions+ is not supported
96
+ # since it creates ambiguity. Instead use +to forbid_all_actions+.
97
+ #
98
+ # @return [PermitAllActionsMatcher] the matcher object.
99
+ def permit_all_actions
100
+ PermitAllActionsMatcher.new
269
101
  end
270
102
 
271
- failure_message do |policy|
272
- "#{policy.class} does not permit the edit or update action for " +
273
- policy.public_send(Pundit::Matchers.configuration.user_alias)
274
- .inspect + '.'
103
+ # Creates a matcher that tests if the policy forbids all actions.
104
+ #
105
+ # @note The negative form +not_to forbid_all_actions+ is not supported
106
+ # since it creates ambiguity. Instead use +to permit_all_actions+.
107
+ #
108
+ # @return [ForbidAllActionsMatcher] the matcher object.
109
+ def forbid_all_actions
110
+ ForbidAllActionsMatcher.new
275
111
  end
276
112
 
277
- failure_message_when_negated do |policy|
278
- "#{policy.class} does not forbid the edit or update action for " +
279
- policy.public_send(Pundit::Matchers.configuration.user_alias)
280
- .inspect + '.'
113
+ # Creates a matcher that tests if the policy permits the edit and update actions.
114
+ #
115
+ # @return [PermitActionsMatcher] the matcher object.
116
+ def permit_edit_and_update_actions
117
+ PermitActionsMatcher.new(:edit, :update)
281
118
  end
282
- end
283
-
284
- RSpec::Matchers.define :permit_mass_assignment_of do |attributes|
285
- # Map single object argument to an array, if necessary
286
- attributes = attributes.is_a?(Array) ? attributes : [attributes]
287
119
 
288
- match do |policy|
289
- return false if attributes.count < 1
120
+ RSpec::Matchers.define_negated_matcher :forbid_edit_and_update_actions, :permit_edit_and_update_actions,
121
+ &NEGATED_DESCRIPTION
290
122
 
291
- @forbidden_attributes = attributes.select do |attribute|
292
- if defined? @action
293
- !policy.send("permitted_attributes_for_#{@action}").include? attribute
294
- else
295
- !policy.permitted_attributes.include? attribute
296
- end
297
- end
298
-
299
- @forbidden_attributes.empty?
123
+ # Creates a matcher that tests if the policy permits the new and create actions.
124
+ #
125
+ # @return [PermitActionsMatcher] the matcher object.
126
+ def permit_new_and_create_actions
127
+ PermitActionsMatcher.new(:new, :create)
300
128
  end
301
129
 
302
- attr_reader :forbidden_attributes
130
+ RSpec::Matchers.define_negated_matcher :forbid_new_and_create_actions, :permit_new_and_create_actions,
131
+ &NEGATED_DESCRIPTION
303
132
 
304
- chain :for_action do |action|
305
- @action = action
133
+ # Creates a matcher that tests if the policy permits only a set of actions.
134
+ #
135
+ # @note The negative form +not_to permit_only_actions+ is not supported
136
+ # since it creates ambiguity. Instead use +to forbid_only_actions+.
137
+ #
138
+ # @param [Array<String, Symbol>] actions the actions to be tested.
139
+ # @return [PermitOnlyActionsMatcher] the matcher object.
140
+ def permit_only_actions(*actions)
141
+ PermitOnlyActionsMatcher.new(*actions)
306
142
  end
307
143
 
308
- zero_attributes_failure_message = 'At least one attribute must be ' \
309
- 'specified when using the permit_mass_assignment_of matcher.'
310
-
311
- failure_message do |policy|
312
- if attributes.count.zero?
313
- zero_attributes_failure_message
314
- elsif defined? @action
315
- "#{policy.class} expected to permit the mass assignment of the " \
316
- "attributes #{attributes} when authorising the #{@action} action, " \
317
- 'but forbade the mass assignment of the attributes ' \
318
- "#{forbidden_attributes} for " +
319
- policy.public_send(Pundit::Matchers.configuration.user_alias)
320
- .inspect + '.'
321
- else
322
- "#{policy.class} expected to permit the mass assignment of the " \
323
- "attributes #{attributes}, but forbade the mass assignment of the " \
324
- "attributes #{forbidden_attributes} for " +
325
- policy.public_send(Pundit::Matchers.configuration.user_alias)
326
- .inspect + '.'
327
- end
144
+ # Creates a matcher that tests if the policy forbids only a set of actions.
145
+ #
146
+ # @note The negative form +not_to forbid_only_actions+ is not supported
147
+ # since it creates ambiguity. Instead use +to permit_only_actions+.
148
+ #
149
+ # @param [Array<String, Symbol>] actions the actions to be tested.
150
+ # @return [ForbidOnlyActionsMatcher] the matcher object.
151
+ def forbid_only_actions(*actions)
152
+ ForbidOnlyActionsMatcher.new(*actions)
328
153
  end
329
154
 
330
- failure_message_when_negated do |policy|
331
- if attributes.count.zero?
332
- zero_attributes_failure_message
333
- elsif defined? @action
334
- "#{policy.class} expected to forbid the mass assignment of the " \
335
- "attributes #{attributes} when authorising the #{@action} action, " \
336
- 'but forbade the mass assignment of the attributes ' \
337
- "#{forbidden_attributes} for " +
338
- policy.public_send(Pundit::Matchers.configuration.user_alias)
339
- .inspect + '.'
340
- else
341
- "#{policy.class} expected to forbid the mass assignment of the " \
342
- "attributes #{attributes}, but forbade the mass assignment of the " \
343
- "attributes #{forbidden_attributes} for " +
344
- policy.public_send(Pundit::Matchers.configuration.user_alias)
345
- .inspect + '.'
346
- end
155
+ # Creates a matcher that tests if the policy permits mass assignment of an attribute.
156
+ #
157
+ # @param [String, Symbol, Hash] attribute the attribute to be tested.
158
+ # @return [PermitAttributesMatcher] the matcher object.
159
+ def permit_attribute(attribute)
160
+ PermitAttributesMatcher.new(attribute).ensure_single_attribute!
347
161
  end
348
- end
349
162
 
350
- RSpec::Matchers.define :permit_new_and_create_actions do
351
- match do |policy|
352
- policy.new? && policy.create?
353
- end
354
-
355
- failure_message do |policy|
356
- "#{policy.class} does not permit the new or create action for " +
357
- policy.public_send(Pundit::Matchers.configuration.user_alias)
358
- .inspect + '.'
359
- end
163
+ RSpec::Matchers.define_negated_matcher :forbid_attribute, :permit_attribute, &NEGATED_DESCRIPTION
360
164
 
361
- failure_message_when_negated do |policy|
362
- "#{policy.class} does not forbid the new or create action for " +
363
- policy.public_send(Pundit::Matchers.configuration.user_alias)
364
- .inspect + '.'
365
- end
165
+ # Creates a matcher that tests if the policy permits mass assignment of a set of attributes.
166
+ #
167
+ # @param [Array<String, Symbol, Hash>] attributes the attributes to be tested.
168
+ # @return [PermitAttributesMatcher] the matcher object.
169
+ def permit_attributes(*attributes)
170
+ PermitAttributesMatcher.new(*attributes)
171
+ end
172
+
173
+ RSpec::Matchers.define_negated_matcher :forbid_attributes, :permit_attributes, &NEGATED_DESCRIPTION
174
+
175
+ # @!macro [attach] RSpec::Matchers.alias_matcher
176
+ # @!method $1
177
+ #
178
+ # An alias matcher for {$2}.
179
+ RSpec::Matchers.alias_matcher :permit_mass_assignment_of, :permit_attributes
180
+ RSpec::Matchers.alias_matcher :forbid_mass_assignment_of, :forbid_attributes
366
181
  end
367
182
  end
368
183
 
369
- if defined?(Pundit)
370
- RSpec.configure do |config|
371
- config.include Pundit::Matchers
372
- end
184
+ RSpec.configure do |config|
185
+ config.include Pundit::Matchers
373
186
  end
metadata CHANGED
@@ -1,49 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pundit-matchers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Alley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-04 00:00:00.000000000 Z
11
+ date: 2023-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rspec-rails
14
+ name: rspec-core
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 3.0.0
19
+ version: '3.12'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 3.0.0
26
+ version: '3.12'
27
27
  - !ruby/object:Gem::Dependency
28
- name: pundit
28
+ name: rspec-expectations
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.1'
34
- - - ">="
33
+ version: '3.12'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-mocks
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
35
46
  - !ruby/object:Gem::Version
36
- version: 1.1.0
37
- type: :development
47
+ version: '3.12'
48
+ type: :runtime
38
49
  prerelease: false
39
50
  version_requirements: !ruby/object:Gem::Requirement
40
51
  requirements:
41
52
  - - "~>"
42
53
  - !ruby/object:Gem::Version
43
- version: '1.1'
44
- - - ">="
54
+ version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-support
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
45
67
  - !ruby/object:Gem::Version
46
- version: 1.1.0
68
+ version: '3.12'
47
69
  description: A set of RSpec matchers for testing Pundit authorisation policies
48
70
  email: chris@chrisalley.info
49
71
  executables: []
@@ -51,10 +73,21 @@ extensions: []
51
73
  extra_rdoc_files: []
52
74
  files:
53
75
  - lib/pundit/matchers.rb
54
- homepage: http://github.com/chrisalley/pundit-matchers
76
+ - lib/pundit/matchers/actions_matcher.rb
77
+ - lib/pundit/matchers/attributes_matcher.rb
78
+ - lib/pundit/matchers/base_matcher.rb
79
+ - lib/pundit/matchers/forbid_all_actions_matcher.rb
80
+ - lib/pundit/matchers/forbid_only_actions_matcher.rb
81
+ - lib/pundit/matchers/permit_actions_matcher.rb
82
+ - lib/pundit/matchers/permit_all_actions_matcher.rb
83
+ - lib/pundit/matchers/permit_attributes_matcher.rb
84
+ - lib/pundit/matchers/permit_only_actions_matcher.rb
85
+ - lib/pundit/matchers/utils/policy_info.rb
86
+ homepage: https://github.com/punditcommunity/pundit-matchers
55
87
  licenses:
56
88
  - MIT
57
- metadata: {}
89
+ metadata:
90
+ rubygems_mfa_required: 'true'
58
91
  post_install_message:
59
92
  rdoc_options: []
60
93
  require_paths:
@@ -63,14 +96,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
96
  requirements:
64
97
  - - ">="
65
98
  - !ruby/object:Gem::Version
66
- version: '0'
99
+ version: '3.0'
67
100
  required_rubygems_version: !ruby/object:Gem::Requirement
68
101
  requirements:
69
102
  - - ">="
70
103
  - !ruby/object:Gem::Version
71
104
  version: '0'
72
105
  requirements: []
73
- rubygems_version: 3.2.15
106
+ rubygems_version: 3.4.12
74
107
  signing_key:
75
108
  specification_version: 4
76
109
  summary: RSpec matchers for Pundit policies