pundit-matchers 2.2.0 → 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pundit/matchers/actions_matcher.rb +41 -0
  3. data/lib/pundit/matchers/attributes_matcher.rb +71 -0
  4. data/lib/pundit/matchers/base_matcher.rb +27 -0
  5. data/lib/pundit/matchers/forbid_all_actions_matcher.rb +43 -0
  6. data/lib/pundit/matchers/forbid_only_actions_matcher.rb +60 -0
  7. data/lib/pundit/matchers/permit_actions_matcher.rb +69 -0
  8. data/lib/pundit/matchers/permit_all_actions_matcher.rb +43 -0
  9. data/lib/pundit/matchers/permit_attributes_matcher.rb +65 -0
  10. data/lib/pundit/matchers/permit_only_actions_matcher.rb +60 -0
  11. data/lib/pundit/matchers/utils/policy_info.rb +67 -6
  12. data/lib/pundit/matchers.rb +120 -364
  13. metadata +16 -39
  14. data/lib/pundit/matchers/utils/all_actions/actions_matcher.rb +0 -39
  15. data/lib/pundit/matchers/utils/all_actions/error_message_formatter.rb +0 -47
  16. data/lib/pundit/matchers/utils/all_actions/forbidden_actions_error_formatter.rb +0 -26
  17. data/lib/pundit/matchers/utils/all_actions/forbidden_actions_matcher.rb +0 -20
  18. data/lib/pundit/matchers/utils/all_actions/permitted_actions_error_formatter.rb +0 -26
  19. data/lib/pundit/matchers/utils/all_actions/permitted_actions_matcher.rb +0 -20
  20. data/lib/pundit/matchers/utils/only_actions/actions_matcher.rb +0 -39
  21. data/lib/pundit/matchers/utils/only_actions/error_message_formatter.rb +0 -54
  22. data/lib/pundit/matchers/utils/only_actions/forbidden_actions_error_formatter.rb +0 -26
  23. data/lib/pundit/matchers/utils/only_actions/forbidden_actions_matcher.rb +0 -20
  24. data/lib/pundit/matchers/utils/only_actions/permitted_actions_error_formatter.rb +0 -26
  25. data/lib/pundit/matchers/utils/only_actions/permitted_actions_matcher.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '013960778421a77229a9e50388f5cb7073cae3c60024b482f48c39431de063af'
4
- data.tar.gz: 12a70d9a7ffdf1df5a2e584e555f3f934d36a971852605afa611c033a07c14dc
3
+ metadata.gz: 96b6768ee2dad9c91d75c12506f76d530b9b6052fc8d8b09c6fdaa56f88d1748
4
+ data.tar.gz: a2e24ef15671dec4fd9820ef5f61029499d16fd56a726be2dcdd5892dc02088f
5
5
  SHA512:
6
- metadata.gz: 4c00b1be800433a24203f2d964a810eaa7445ed98934598726e568b727db2ad6333886d1d7dffe56196a1bb445a982c6d17ea96cda737cca5a5ca7e801445ada
7
- data.tar.gz: 0fb59915496d8b6807b01b84ce6f70c2c350e5dd6db00851c1e013211fb6ab41c6a1cc2f21c11f0a10107f116dadd666ff71228bcf06eee1ed706a90b56d9550
6
+ metadata.gz: 0aa08b14749957cdeb61631b97712288eb9207201197e35f303892ab90730c5dd1fdd736f01f9d84e1c6665131d79d5a5bdc1028e2f6d4b34e875e5850e71871
7
+ data.tar.gz: 4fab87c430012868c5274767a5bc4d75da673e3c7637ea544d83eb244604548c17be7a21cea94617b940b42bfabc9b2f4c8fc6ab6682e18d55d2a527f5afba61
@@ -0,0 +1,41 @@
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
+
14
+ # Initializes a new instance of the ActionsMatcher class.
15
+ #
16
+ # @param expected_actions [Array<String, Symbol>] The expected actions to be checked.
17
+ #
18
+ # @raise [ArgumentError] If no actions are specified.
19
+ def initialize(*expected_actions)
20
+ raise ArgumentError, ARGUMENTS_REQUIRED_ERROR if expected_actions.empty?
21
+
22
+ super()
23
+ @expected_actions = expected_actions.flatten.map(&:to_sym).sort
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :expected_actions
29
+
30
+ def check_actions!
31
+ missing_actions = expected_actions - policy_info.actions
32
+ return if missing_actions.empty?
33
+
34
+ raise ArgumentError, format(
35
+ ACTIONS_NOT_IMPLEMENTED_ERROR,
36
+ policy: policy_info, actions: missing_actions
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,71 @@
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
+
12
+ # Initializes a new instance of the AttributesMatcher class.
13
+ #
14
+ # @param expected_attributes [Array<String, Symbol, Hash>] The list of attributes to be tested.
15
+ def initialize(*expected_attributes)
16
+ raise ArgumentError, ARGUMENTS_REQUIRED_ERROR if expected_attributes.empty?
17
+
18
+ super()
19
+ @expected_attributes = flatten_attributes(expected_attributes)
20
+ @options = {}
21
+ end
22
+
23
+ # Specifies the action to be tested.
24
+ #
25
+ # @param action [Symbol, String] The action to be tested.
26
+ # @return [AttributesMatcher] The current instance of the AttributesMatcher class.
27
+ def for_action(action)
28
+ @options[:action] = action
29
+ self
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :expected_attributes, :options
35
+
36
+ def permitted_attributes(policy)
37
+ @permitted_attributes ||=
38
+ if options.key?(:action)
39
+ flatten_attributes(policy.public_send(:"permitted_attributes_for_#{options[:action]}"))
40
+ else
41
+ flatten_attributes(policy.permitted_attributes)
42
+ end
43
+ end
44
+
45
+ def action_message
46
+ " when authorising the '#{options[:action]}' action"
47
+ end
48
+
49
+ # Flattens and sorts a hash or array of attributes into an array of symbols.
50
+ #
51
+ # This is a private method used internally by the `Matcher` class to convert
52
+ # attribute lists into a flattened, sorted array of symbols. The resulting
53
+ # array can be used to compare attribute lists.
54
+ #
55
+ # @param attributes [String, Symbol, Array, Hash] the attributes to be flattened.
56
+ # @return [Array<Symbol>] the flattened, sorted array of symbols.
57
+ def flatten_attributes(attributes)
58
+ case attributes
59
+ when String, Symbol
60
+ [attributes.to_sym]
61
+ when Array
62
+ attributes.flat_map { |item| flatten_attributes(item) }.sort
63
+ when Hash
64
+ attributes.flat_map do |key, value|
65
+ flatten_attributes(value).map { |item| :"#{key}[#{item}]" }
66
+ end.sort
67
+ end
68
+ end
69
+ end
70
+ end
71
+ 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
@@ -3,27 +3,88 @@
3
3
  module Pundit
4
4
  module Matchers
5
5
  module Utils
6
- # Collects all details about given policy class.
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.
7
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
+
8
25
  attr_reader :policy
9
26
 
27
+ # Initializes a new instance of PolicyInfo.
28
+ #
29
+ # @param policy [Class] The policy class to collect details about.
10
30
  def initialize(policy)
11
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)
12
47
  end
13
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.
14
54
  def actions
15
- @actions ||= begin
16
- policy_methods = @policy.public_methods - Object.instance_methods
17
- policy_methods.grep(/\?$/).sort.map { |policy_method| policy_method.to_s.delete_suffix('?').to_sym }
55
+ @actions ||= policy_public_methods.grep(/\?$/).sort.map do |policy_method|
56
+ policy_method.to_s.delete_suffix('?').to_sym
18
57
  end
19
58
  end
20
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.
21
63
  def permitted_actions
22
- @permitted_actions ||= actions.select { |action| policy.public_send("#{action}?") }
64
+ @permitted_actions ||= actions.select { |action| policy.public_send(:"#{action}?") }
23
65
  end
24
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.
25
70
  def forbidden_actions
26
- actions - permitted_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)
27
88
  end
28
89
  end
29
90
  end