light-services 2.2.1 → 3.0.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 +4 -4
- data/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +1 -0
- data/.rubocop.yml +77 -7
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +76 -13
- data/docs/arguments.md +267 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +168 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +250 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +135 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +100 -0
- data/docs/recipes.md +14 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +337 -0
- data/docs/summary.md +19 -0
- data/docs/testing.md +549 -0
- data/lib/generators/light_services/install/USAGE +15 -0
- data/lib/generators/light_services/install/install_generator.rb +41 -0
- data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/light_services/service/USAGE +21 -0
- data/lib/generators/light_services/service/service_generator.rb +68 -0
- data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
- data/lib/light/services/base.rb +23 -113
- data/lib/light/services/callbacks.rb +103 -0
- data/lib/light/services/collection.rb +97 -0
- data/lib/light/services/concerns/execution.rb +76 -0
- data/lib/light/services/concerns/parent_service.rb +34 -0
- data/lib/light/services/concerns/state_management.rb +30 -0
- data/lib/light/services/config.rb +4 -18
- data/lib/light/services/constants.rb +97 -0
- data/lib/light/services/dsl/arguments_dsl.rb +84 -0
- data/lib/light/services/dsl/outputs_dsl.rb +80 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +132 -0
- data/lib/light/services/exceptions.rb +7 -2
- data/lib/light/services/messages.rb +19 -31
- data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
- data/lib/light/services/rspec/matchers/define_output.rb +147 -0
- data/lib/light/services/rspec/matchers/define_step.rb +225 -0
- data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
- data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
- data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
- data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
- data/lib/light/services/rspec.rb +15 -0
- data/lib/light/services/settings/field.rb +86 -0
- data/lib/light/services/settings/step.rb +31 -16
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/light-services.gemspec +6 -8
- metadata +54 -26
- data/lib/light/services/class_based_collection/base.rb +0 -86
- data/lib/light/services/class_based_collection/mount.rb +0 -33
- data/lib/light/services/collection/arguments.rb +0 -34
- data/lib/light/services/collection/base.rb +0 -59
- data/lib/light/services/collection/outputs.rb +0 -16
- data/lib/light/services/settings/argument.rb +0 -68
- data/lib/light/services/settings/output.rb +0 -34
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module RSpec
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for testing step execution on a service instance
|
|
8
|
+
# NOTE: This matcher requires the service to track executed steps.
|
|
9
|
+
# Add a callback in your service to track execution:
|
|
10
|
+
#
|
|
11
|
+
# after_step_run do |service, step_name|
|
|
12
|
+
# service.executed_steps << step_name
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage (requires executed_steps tracking)
|
|
16
|
+
# expect(service).to execute_step(:validate)
|
|
17
|
+
#
|
|
18
|
+
# @example Check step was skipped
|
|
19
|
+
# expect(service).to skip_step(:notify)
|
|
20
|
+
#
|
|
21
|
+
# @example Check multiple steps executed
|
|
22
|
+
# expect(service).to execute_steps(:validate, :process, :save)
|
|
23
|
+
#
|
|
24
|
+
# @example Check execution order
|
|
25
|
+
# expect(service).to execute_steps_in_order(:validate, :process, :save)
|
|
26
|
+
def execute_step(name)
|
|
27
|
+
ExecuteStepMatcher.new(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def skip_step(name)
|
|
31
|
+
SkipStepMatcher.new(name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def execute_steps(*names)
|
|
35
|
+
ExecuteStepsMatcher.new(names, ordered: false)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def execute_steps_in_order(*names)
|
|
39
|
+
ExecuteStepsMatcher.new(names, ordered: true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ExecuteStepMatcher
|
|
43
|
+
def initialize(name)
|
|
44
|
+
@name = name
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def matches?(service)
|
|
48
|
+
@service = service
|
|
49
|
+
|
|
50
|
+
return false unless service_tracks_steps?
|
|
51
|
+
return false unless step_executed?
|
|
52
|
+
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def failure_message
|
|
57
|
+
return tracking_not_available_message unless service_tracks_steps?
|
|
58
|
+
|
|
59
|
+
"expected service to execute step :#{@name}, " \
|
|
60
|
+
"but executed steps were: #{executed_steps.inspect}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def failure_message_when_negated
|
|
64
|
+
"expected service not to execute step :#{@name}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def description
|
|
68
|
+
"execute step :#{@name}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def service_tracks_steps?
|
|
74
|
+
@service.respond_to?(:executed_steps)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def executed_steps
|
|
78
|
+
@service.executed_steps
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def step_executed?
|
|
82
|
+
executed_steps.include?(@name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def tracking_not_available_message
|
|
86
|
+
"cannot verify step execution because service does not track executed steps. " \
|
|
87
|
+
"Add `after_step_run { |service, step| service.executed_steps << step }` to your service."
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class SkipStepMatcher
|
|
92
|
+
def initialize(name)
|
|
93
|
+
@name = name
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def matches?(service)
|
|
97
|
+
@service = service
|
|
98
|
+
|
|
99
|
+
return false unless service_tracks_steps?
|
|
100
|
+
return false unless step_skipped?
|
|
101
|
+
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def failure_message
|
|
106
|
+
return tracking_not_available_message unless service_tracks_steps?
|
|
107
|
+
|
|
108
|
+
"expected service to skip step :#{@name}, but it was executed. " \
|
|
109
|
+
"Executed steps: #{executed_steps.inspect}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def failure_message_when_negated
|
|
113
|
+
"expected service not to skip step :#{@name} (expected it to execute)"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def description
|
|
117
|
+
"skip step :#{@name}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def service_tracks_steps?
|
|
123
|
+
@service.respond_to?(:executed_steps)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def executed_steps
|
|
127
|
+
@service.executed_steps
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def step_skipped?
|
|
131
|
+
!executed_steps.include?(@name)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def tracking_not_available_message
|
|
135
|
+
"cannot verify step execution because service does not track executed steps. " \
|
|
136
|
+
"Add `after_step_run { |service, step| service.executed_steps << step }` to your service."
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class ExecuteStepsMatcher
|
|
141
|
+
def initialize(names, ordered:)
|
|
142
|
+
@names = names
|
|
143
|
+
@ordered = ordered
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def matches?(service)
|
|
147
|
+
@service = service
|
|
148
|
+
@missing_steps = []
|
|
149
|
+
|
|
150
|
+
return false unless service_tracks_steps?
|
|
151
|
+
return false unless all_steps_executed?
|
|
152
|
+
return false unless order_matches?
|
|
153
|
+
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def failure_message
|
|
158
|
+
return tracking_not_available_message unless service_tracks_steps?
|
|
159
|
+
return missing_steps_failure_message unless all_steps_executed?
|
|
160
|
+
return order_failure_message unless order_matches?
|
|
161
|
+
|
|
162
|
+
""
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def failure_message_when_negated
|
|
166
|
+
if @ordered
|
|
167
|
+
"expected service not to execute steps #{@names.inspect} in that order"
|
|
168
|
+
else
|
|
169
|
+
"expected service not to execute steps #{@names.inspect}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def description
|
|
174
|
+
if @ordered
|
|
175
|
+
"execute steps #{@names.inspect} in order"
|
|
176
|
+
else
|
|
177
|
+
"execute steps #{@names.inspect}"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def service_tracks_steps?
|
|
184
|
+
@service.respond_to?(:executed_steps)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def executed_steps
|
|
188
|
+
@service.executed_steps
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def all_steps_executed?
|
|
192
|
+
@missing_steps = @names.reject { |name| executed_steps.include?(name) }
|
|
193
|
+
@missing_steps.empty?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def order_matches?
|
|
197
|
+
return true unless @ordered
|
|
198
|
+
|
|
199
|
+
# Check if the expected steps appear in the same order in executed steps
|
|
200
|
+
last_index = -1
|
|
201
|
+
@names.all? do |name|
|
|
202
|
+
current_index = executed_steps.index(name)
|
|
203
|
+
return false unless current_index
|
|
204
|
+
return false unless current_index > last_index
|
|
205
|
+
|
|
206
|
+
last_index = current_index
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def missing_steps_failure_message
|
|
212
|
+
"expected service to execute steps #{@names.inspect}, " \
|
|
213
|
+
"but missing: #{@missing_steps.inspect}. " \
|
|
214
|
+
"Executed steps: #{executed_steps.inspect}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def order_failure_message
|
|
218
|
+
"expected service to execute steps #{@names.inspect} in that order, " \
|
|
219
|
+
"but actual execution order was: #{executed_steps.inspect}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def tracking_not_available_message
|
|
223
|
+
"cannot verify step execution because service does not track executed steps. " \
|
|
224
|
+
"Add `after_step_run { |service, step| service.executed_steps << step }` to your service."
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module RSpec
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for testing errors on a service instance
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# expect(service).to have_error_on(:name)
|
|
11
|
+
#
|
|
12
|
+
# @example With specific message
|
|
13
|
+
# expect(service).to have_error_on(:name).with_message("can't be blank")
|
|
14
|
+
#
|
|
15
|
+
# @example With message matching regex
|
|
16
|
+
# expect(service).to have_error_on(:base).with_message(/invalid/)
|
|
17
|
+
#
|
|
18
|
+
# @example Check multiple error keys
|
|
19
|
+
# expect(service).to have_errors_on(:name, :email)
|
|
20
|
+
def have_error_on(key)
|
|
21
|
+
HaveErrorOnMatcher.new(key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def have_errors_on(*keys)
|
|
25
|
+
HaveErrorsOnMatcher.new(keys)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class HaveErrorOnMatcher
|
|
29
|
+
def initialize(key)
|
|
30
|
+
@key = key
|
|
31
|
+
@expected_message = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with_message(message)
|
|
35
|
+
@expected_message = message
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def matches?(service)
|
|
40
|
+
@service = service
|
|
41
|
+
|
|
42
|
+
return false unless has_error_key?
|
|
43
|
+
return false unless message_matches?
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def failure_message
|
|
49
|
+
unless has_error_key?
|
|
50
|
+
return "expected service to have error on :#{@key}, but errors were: #{errors_summary}"
|
|
51
|
+
end
|
|
52
|
+
return message_failure_message unless message_matches?
|
|
53
|
+
|
|
54
|
+
""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def failure_message_when_negated
|
|
58
|
+
if @expected_message
|
|
59
|
+
"expected service not to have error on :#{@key} with message #{@expected_message.inspect}"
|
|
60
|
+
else
|
|
61
|
+
"expected service not to have error on :#{@key}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def description
|
|
66
|
+
desc = "have error on :#{@key}"
|
|
67
|
+
desc += " with message #{@expected_message.inspect}" if @expected_message
|
|
68
|
+
desc
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def has_error_key?
|
|
74
|
+
@service.errors.key?(@key)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def message_matches?
|
|
78
|
+
return true if @expected_message.nil?
|
|
79
|
+
|
|
80
|
+
error_messages = @service.errors[@key].map(&:to_s)
|
|
81
|
+
|
|
82
|
+
case @expected_message
|
|
83
|
+
when Regexp
|
|
84
|
+
error_messages.any? { |msg| msg.match?(@expected_message) }
|
|
85
|
+
else
|
|
86
|
+
error_messages.include?(@expected_message.to_s)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def message_failure_message
|
|
91
|
+
actual_messages = @service.errors[@key].map(&:to_s)
|
|
92
|
+
"expected service error on :#{@key} to include message #{@expected_message.inspect}, " \
|
|
93
|
+
"but messages were: #{actual_messages.inspect}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def errors_summary
|
|
97
|
+
if @service.errors.empty?
|
|
98
|
+
"empty"
|
|
99
|
+
else
|
|
100
|
+
@service.errors.to_h.inspect
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class HaveErrorsOnMatcher
|
|
106
|
+
def initialize(keys)
|
|
107
|
+
@keys = keys
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def matches?(service)
|
|
111
|
+
@service = service
|
|
112
|
+
@missing_keys = []
|
|
113
|
+
|
|
114
|
+
@keys.each do |key|
|
|
115
|
+
@missing_keys << key unless @service.errors.key?(key)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@missing_keys.empty?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def failure_message
|
|
122
|
+
"expected service to have errors on #{@keys.inspect}, " \
|
|
123
|
+
"but missing errors on: #{@missing_keys.inspect}. " \
|
|
124
|
+
"Actual errors: #{errors_summary}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def failure_message_when_negated
|
|
128
|
+
"expected service not to have errors on #{@keys.inspect}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def description
|
|
132
|
+
"have errors on #{@keys.inspect}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def errors_summary
|
|
138
|
+
if @service.errors.empty?
|
|
139
|
+
"empty"
|
|
140
|
+
else
|
|
141
|
+
@service.errors.to_h.inspect
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module RSpec
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for testing warnings on a service instance
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# expect(service).to have_warning_on(:name)
|
|
11
|
+
#
|
|
12
|
+
# @example With specific message
|
|
13
|
+
# expect(service).to have_warning_on(:name).with_message("was replaced")
|
|
14
|
+
#
|
|
15
|
+
# @example With message matching regex
|
|
16
|
+
# expect(service).to have_warning_on(:base).with_message(/deprecated/)
|
|
17
|
+
#
|
|
18
|
+
# @example Check multiple warning keys
|
|
19
|
+
# expect(service).to have_warnings_on(:name, :email)
|
|
20
|
+
def have_warning_on(key)
|
|
21
|
+
HaveWarningOnMatcher.new(key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def have_warnings_on(*keys)
|
|
25
|
+
HaveWarningsOnMatcher.new(keys)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class HaveWarningOnMatcher
|
|
29
|
+
def initialize(key)
|
|
30
|
+
@key = key
|
|
31
|
+
@expected_message = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with_message(message)
|
|
35
|
+
@expected_message = message
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def matches?(service)
|
|
40
|
+
@service = service
|
|
41
|
+
|
|
42
|
+
return false unless has_warning_key?
|
|
43
|
+
return false unless message_matches?
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def failure_message
|
|
49
|
+
unless has_warning_key?
|
|
50
|
+
return "expected service to have warning on :#{@key}, but warnings were: #{warnings_summary}"
|
|
51
|
+
end
|
|
52
|
+
return message_failure_message unless message_matches?
|
|
53
|
+
|
|
54
|
+
""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def failure_message_when_negated
|
|
58
|
+
if @expected_message
|
|
59
|
+
"expected service not to have warning on :#{@key} with message #{@expected_message.inspect}"
|
|
60
|
+
else
|
|
61
|
+
"expected service not to have warning on :#{@key}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def description
|
|
66
|
+
desc = "have warning on :#{@key}"
|
|
67
|
+
desc += " with message #{@expected_message.inspect}" if @expected_message
|
|
68
|
+
desc
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def has_warning_key?
|
|
74
|
+
@service.warnings.key?(@key)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def message_matches?
|
|
78
|
+
return true if @expected_message.nil?
|
|
79
|
+
|
|
80
|
+
warning_messages = @service.warnings[@key].map(&:to_s)
|
|
81
|
+
|
|
82
|
+
case @expected_message
|
|
83
|
+
when Regexp
|
|
84
|
+
warning_messages.any? { |msg| msg.match?(@expected_message) }
|
|
85
|
+
else
|
|
86
|
+
warning_messages.include?(@expected_message.to_s)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def message_failure_message
|
|
91
|
+
actual_messages = @service.warnings[@key].map(&:to_s)
|
|
92
|
+
"expected service warning on :#{@key} to include message #{@expected_message.inspect}, " \
|
|
93
|
+
"but messages were: #{actual_messages.inspect}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def warnings_summary
|
|
97
|
+
if @service.warnings.empty?
|
|
98
|
+
"empty"
|
|
99
|
+
else
|
|
100
|
+
@service.warnings.to_h.inspect
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class HaveWarningsOnMatcher
|
|
106
|
+
def initialize(keys)
|
|
107
|
+
@keys = keys
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def matches?(service)
|
|
111
|
+
@service = service
|
|
112
|
+
@missing_keys = []
|
|
113
|
+
|
|
114
|
+
@keys.each do |key|
|
|
115
|
+
@missing_keys << key unless @service.warnings.key?(key)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@missing_keys.empty?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def failure_message
|
|
122
|
+
"expected service to have warnings on #{@keys.inspect}, " \
|
|
123
|
+
"but missing warnings on: #{@missing_keys.inspect}. " \
|
|
124
|
+
"Actual warnings: #{warnings_summary}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def failure_message_when_negated
|
|
128
|
+
"expected service not to have warnings on #{@keys.inspect}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def description
|
|
132
|
+
"have warnings on #{@keys.inspect}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def warnings_summary
|
|
138
|
+
if @service.warnings.empty?
|
|
139
|
+
"empty"
|
|
140
|
+
else
|
|
141
|
+
@service.warnings.to_h.inspect
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module RSpec
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matcher for testing callback execution on a service instance
|
|
8
|
+
# NOTE: This matcher requires the service to track callback execution.
|
|
9
|
+
# Add tracking in your callbacks:
|
|
10
|
+
#
|
|
11
|
+
# before_service_run do |service|
|
|
12
|
+
# service.callback_log << :before_service_run
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# after_step_run do |service, step_name|
|
|
16
|
+
# service.callback_log << [:after_step_run, step_name]
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Basic callback check (requires callback_log tracking)
|
|
20
|
+
# expect(service).to trigger_callback(:before_service_run)
|
|
21
|
+
#
|
|
22
|
+
# @example Step-specific callback
|
|
23
|
+
# expect(service).to trigger_callback(:after_step_run).for_step(:validate)
|
|
24
|
+
#
|
|
25
|
+
# @example Check callback was not triggered
|
|
26
|
+
# expect(service).not_to trigger_callback(:on_service_failure)
|
|
27
|
+
def trigger_callback(callback_name)
|
|
28
|
+
TriggerCallbackMatcher.new(callback_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class TriggerCallbackMatcher
|
|
32
|
+
VALID_CALLBACKS = [
|
|
33
|
+
:before_service_run,
|
|
34
|
+
:after_service_run,
|
|
35
|
+
:around_service_run,
|
|
36
|
+
:on_service_success,
|
|
37
|
+
:on_service_failure,
|
|
38
|
+
:before_step_run,
|
|
39
|
+
:after_step_run,
|
|
40
|
+
:around_step_run,
|
|
41
|
+
:on_step_success,
|
|
42
|
+
:on_step_failure,
|
|
43
|
+
:on_step_crash,
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
STEP_CALLBACKS = [
|
|
47
|
+
:before_step_run,
|
|
48
|
+
:after_step_run,
|
|
49
|
+
:around_step_run,
|
|
50
|
+
:on_step_success,
|
|
51
|
+
:on_step_failure,
|
|
52
|
+
:on_step_crash,
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
def initialize(callback_name)
|
|
56
|
+
@callback_name = callback_name
|
|
57
|
+
@step_name = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def for_step(step_name)
|
|
61
|
+
@step_name = step_name
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def matches?(service)
|
|
66
|
+
@service = service
|
|
67
|
+
|
|
68
|
+
return false unless service_tracks_callbacks?
|
|
69
|
+
return false unless callback_triggered?
|
|
70
|
+
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def failure_message
|
|
75
|
+
return tracking_not_available_message unless service_tracks_callbacks?
|
|
76
|
+
|
|
77
|
+
if @step_name
|
|
78
|
+
"expected service to trigger callback :#{@callback_name} for step :#{@step_name}, " \
|
|
79
|
+
"but callback log was: #{callback_log.inspect}"
|
|
80
|
+
else
|
|
81
|
+
"expected service to trigger callback :#{@callback_name}, " \
|
|
82
|
+
"but callback log was: #{callback_log.inspect}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def failure_message_when_negated
|
|
87
|
+
if @step_name
|
|
88
|
+
"expected service not to trigger callback :#{@callback_name} for step :#{@step_name}"
|
|
89
|
+
else
|
|
90
|
+
"expected service not to trigger callback :#{@callback_name}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def description
|
|
95
|
+
desc = "trigger callback :#{@callback_name}"
|
|
96
|
+
desc += " for step :#{@step_name}" if @step_name
|
|
97
|
+
desc
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def service_tracks_callbacks?
|
|
103
|
+
@service.respond_to?(:callback_log)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def callback_log
|
|
107
|
+
@service.callback_log
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def callback_triggered?
|
|
111
|
+
if @step_name
|
|
112
|
+
# Look for step-specific callback entry like [:after_step_run, :validate]
|
|
113
|
+
callback_log.include?([@callback_name, @step_name])
|
|
114
|
+
else
|
|
115
|
+
# Look for service-level callback or any occurrence of the callback name
|
|
116
|
+
callback_log.any? do |entry|
|
|
117
|
+
case entry
|
|
118
|
+
when Symbol
|
|
119
|
+
entry == @callback_name
|
|
120
|
+
when Array
|
|
121
|
+
entry.first == @callback_name
|
|
122
|
+
else
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def tracking_not_available_message
|
|
130
|
+
"cannot verify callback execution because service does not track callbacks. " \
|
|
131
|
+
"Add callback tracking to your service, e.g.: " \
|
|
132
|
+
"`before_service_run { |service| service.callback_log << :before_service_run }`"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "light/services"
|
|
4
|
+
|
|
5
|
+
require_relative "rspec/matchers/define_argument"
|
|
6
|
+
require_relative "rspec/matchers/define_output"
|
|
7
|
+
require_relative "rspec/matchers/define_step"
|
|
8
|
+
require_relative "rspec/matchers/have_error_on"
|
|
9
|
+
require_relative "rspec/matchers/have_warning_on"
|
|
10
|
+
require_relative "rspec/matchers/execute_step"
|
|
11
|
+
require_relative "rspec/matchers/trigger_callback"
|
|
12
|
+
|
|
13
|
+
RSpec.configure do |config|
|
|
14
|
+
config.include Light::Services::RSpec::Matchers
|
|
15
|
+
end
|