pundit-matchers 1.7.0 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0da80bb866c35b8ab6548d017a9bc022831e3da0c5493b99aac0ba5c8c79cad
4
- data.tar.gz: 24bfacd140e3976e30c88204db5e33566c42dbd5e45d568c8d857a7a4205951d
3
+ metadata.gz: 0ce1e0e0fa90468774d5a482fd4d715af3e40b6e4a4fc6360f19c009d21d04a8
4
+ data.tar.gz: 463cb245dfd0a68f5c106e92248ff320fb7ef95d826cdf1a0111b3bddd0ba8bd
5
5
  SHA512:
6
- metadata.gz: 5c1fbddf259fce9fa65c6f0613ec5d46e271ab0c7212380bc86cb5fef6fb85a2f74b6c20a4fb6b4c0e1dacc6b8ad11e1c963f8827c997e88bb319cce157d305b
7
- data.tar.gz: a2363786904df0631c54b8d84094179b61607e300a16ba53cd6406b02272e51f1e21060832f5b64a3eeaaba1ff6dba7c8462d3308732f335dc363df666058120
6
+ metadata.gz: 7ebb5b5df0c13cb1bc0fa8a150c0f6a34cae3719bebd9ff11bdec03f30304678e1101e393c7e7b3a47a3efa83b0169732e96efe929c8c23d79682dd537f72c8d
7
+ data.tar.gz: 25026769eeb8f12294babbbecd5efcff201d7a648b843d4b71b8a49d8da19b88c18f7cc0298e10e95f9dd0e36fc1209824a650aa338904704dcd4bcb121b24a7
@@ -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,29 @@
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
+ include ::RSpec::Matchers::Composable
10
+
11
+ # Error message when an ambiguous negated matcher is used.
12
+ AMBIGUOUS_NEGATED_MATCHER_ERROR = <<~MSG
13
+ `expect().not_to %<name>s` is not supported since it creates ambiguity.
14
+ MSG
15
+
16
+ private
17
+
18
+ attr_reader :policy_info
19
+
20
+ def setup_policy_info!(policy)
21
+ @policy_info = Pundit::Matchers::Utils::PolicyInfo.new(policy)
22
+ end
23
+
24
+ def user_message
25
+ " for '#{policy_info.user}'"
26
+ end
27
+ end
28
+ end
29
+ 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.2
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-27 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