/
have_authorized_scope.rb
134 lines (110 loc) · 3.38 KB
/
have_authorized_scope.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# frozen_string_literal: true
require "action_policy/testing"
module ActionPolicy
module RSpec
# Implements `have_authorized_scope` matcher.
#
# Verifies that a block of code applies authorization scoping using specific policy.
#
# Example:
#
# # in controller/request specs
# subject { get :index }
#
# it "has authorized scope" do
# expect { subject }
# .to have_authorized_scope(:active_record_relation)
# .with(ProductPolicy)
# end
#
class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
:target_expectations, :context
def initialize(type)
@type = type
@name = :default
@scope_options = nil
end
def with(policy)
@policy = policy
self
end
def as(name)
@name = name
self
end
def with_scope_options(scope_options)
@scope_options = scope_options
self
end
def with_target(&block)
@target_expectations = block
self
end
def with_context(context)
@context = context
self
end
def match(_expected, actual)
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)
ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }
return false if matching_scopes.empty?
return true unless target_expectations
if matching_scopes.size > 1
raise "Too many matching scopings (#{matching_scopes.size}), " \
"you can run `.with_target` only when there is the only one match"
end
target_expectations.call(matching_scopes.first.target)
true
end
def does_not_match?(*)
raise "This matcher doesn't support negation"
end
def supports_block_expectations?() = true
def failure_message
"expected a scoping named :#{name} for type :#{type} " \
"#{scope_options_message} " \
"and #{context_message} " \
"from #{policy} to have been applied, " \
"but #{actual_scopes_message}"
end
def scope_options_message
if scope_options
if defined?(::RSpec::Matchers::Composable) &&
scope_options.is_a?(::RSpec::Matchers::Composable)
"with scope options #{scope_options.description}"
else
"with scope options #{scope_options}"
end
else
"without scope options"
end
end
def context_message
context.blank? ? "without context" : "with context: #{context}"
end
def actual_scopes_message
if actual_scopes.empty?
"no scopings have been made"
else
"the following scopings were encountered:\n" \
"#{formatted_scopings}"
end
end
def formatted_scopings
actual_scopes.map do
" - #{_1.inspect}"
end.join("\n")
end
end
end
end
RSpec.configure do |config|
config.include(Module.new do
def have_authorized_scope(type)
ActionPolicy::RSpec::HaveAuthorizedScope.new(type)
end
end)
end