servactory 2.16.1 → 3.0.0.rc1
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/README.md +38 -9
- data/config/locales/de.yml +134 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/es.yml +134 -0
- data/config/locales/fr.yml +134 -0
- data/config/locales/it.yml +134 -0
- data/config/locales/ru.yml +15 -0
- data/lib/generators/README.md +45 -0
- data/lib/generators/servactory/base.rb +82 -0
- data/lib/generators/servactory/extension/USAGE +54 -0
- data/lib/generators/servactory/extension/extension_generator.rb +41 -0
- data/lib/generators/servactory/extension/templates/extension.rb.tt +62 -0
- data/lib/generators/servactory/install/USAGE +27 -0
- data/lib/generators/servactory/install/install_generator.rb +94 -0
- data/lib/generators/servactory/{templates/services/application_service/base.rb → install/templates/application_service/base.rb.tt} +29 -19
- data/lib/generators/servactory/{templates/services/application_service/exceptions.rb → install/templates/application_service/exceptions.rb.tt} +1 -1
- data/lib/generators/servactory/{templates/services/application_service/result.rb → install/templates/application_service/result.rb.tt} +1 -1
- data/lib/generators/servactory/rspec/USAGE +46 -0
- data/lib/generators/servactory/rspec/rspec_generator.rb +95 -0
- data/lib/generators/servactory/rspec/templates/service_spec.rb.tt +58 -0
- data/lib/generators/servactory/service/USAGE +51 -0
- data/lib/generators/servactory/service/service_generator.rb +56 -0
- data/lib/generators/servactory/service/templates/service.rb.tt +22 -0
- data/lib/servactory/actions/collection.rb +56 -1
- data/lib/servactory/actions/dsl.rb +11 -11
- data/lib/servactory/actions/tools/rules.rb +1 -1
- data/lib/servactory/actions/tools/runner.rb +111 -28
- data/lib/servactory/base.rb +1 -7
- data/lib/servactory/configuration/actions/aliases/collection.rb +5 -0
- data/lib/servactory/configuration/actions/rescue_handlers/collection.rb +5 -0
- data/lib/servactory/configuration/actions/shortcuts/collection.rb +5 -0
- data/lib/servactory/configuration/collection_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/config.rb +36 -0
- data/lib/servactory/configuration/configurable.rb +95 -0
- data/lib/servactory/configuration/dsl.rb +3 -27
- data/lib/servactory/configuration/factory.rb +20 -20
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +5 -0
- data/lib/servactory/context/warehouse/inputs.rb +2 -2
- data/lib/servactory/context/workspace/inputs.rb +2 -2
- data/lib/servactory/context/workspace/internals.rb +2 -2
- data/lib/servactory/context/workspace/outputs.rb +2 -2
- data/lib/servactory/context/workspace.rb +11 -7
- data/lib/servactory/dsl.rb +10 -8
- data/lib/servactory/maintenance/attributes/tools/validation.rb +1 -1
- data/lib/servactory/result.rb +2 -2
- data/lib/servactory/stroma/dsl.rb +118 -0
- data/lib/servactory/stroma/entry.rb +32 -0
- data/lib/servactory/stroma/exceptions/base.rb +45 -0
- data/lib/servactory/stroma/exceptions/invalid_hook_type.rb +29 -0
- data/lib/servactory/stroma/exceptions/key_already_registered.rb +32 -0
- data/lib/servactory/stroma/exceptions/registry_frozen.rb +33 -0
- data/lib/servactory/stroma/exceptions/registry_not_finalized.rb +33 -0
- data/lib/servactory/stroma/exceptions/unknown_hook_target.rb +39 -0
- data/lib/servactory/stroma/hooks/applier.rb +63 -0
- data/lib/servactory/stroma/hooks/collection.rb +103 -0
- data/lib/servactory/stroma/hooks/factory.rb +80 -0
- data/lib/servactory/stroma/hooks/hook.rb +74 -0
- data/lib/servactory/stroma/registry.rb +94 -0
- data/lib/servactory/stroma/settings/collection.rb +90 -0
- data/lib/servactory/stroma/settings/registry_settings.rb +88 -0
- data/lib/servactory/stroma/settings/setting.rb +113 -0
- data/lib/servactory/stroma/state.rb +59 -0
- data/lib/servactory/test_kit/fake_type.rb +23 -0
- data/lib/servactory/test_kit/result.rb +45 -0
- data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
- data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
- data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
- data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
- data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
- data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
- data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
- data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
- data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
- data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
- data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
- data/lib/servactory/test_kit/utils/faker.rb +62 -2
- data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
- data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
- data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
- data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
- data/lib/servactory/tool_kit/dynamic_options/target.rb +252 -0
- data/lib/servactory/version.rb +4 -4
- data/lib/servactory.rb +4 -0
- metadata +73 -19
- data/lib/generators/servactory/install_generator.rb +0 -21
- data/lib/generators/servactory/rspec_generator.rb +0 -88
- data/lib/generators/servactory/service_generator.rb +0 -49
- data/lib/servactory/configuration/setup.rb +0 -97
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Backward-compatible API for mocking Servactory services in RSpec tests.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Provides legacy helper methods for mocking Servactory service calls.
|
|
12
|
+
# These methods are maintained for backward compatibility with existing tests.
|
|
13
|
+
# For new tests, consider using the fluent API via {Fluent} module.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# Include in RSpec configuration via the main Helpers module:
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# RSpec.configure do |config|
|
|
21
|
+
# config.include Servactory::TestKit::Rspec::Helpers, type: :service
|
|
22
|
+
# end
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# ## Available Methods
|
|
26
|
+
#
|
|
27
|
+
# - `allow_service_as_success!` - mock `.call!` method for success
|
|
28
|
+
# - `allow_service_as_success` - mock `.call` method for success
|
|
29
|
+
# - `allow_service_as_failure!` - mock `.call!` method for failure
|
|
30
|
+
# - `allow_service_as_failure` - mock `.call` method for failure
|
|
31
|
+
#
|
|
32
|
+
# ## Examples
|
|
33
|
+
#
|
|
34
|
+
# ```ruby
|
|
35
|
+
# # Mock call method (returns Result)
|
|
36
|
+
# allow_service_as_success(PaymentService) do
|
|
37
|
+
# { transaction_id: "txn_123" }
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # Mock call! method (raises on failure)
|
|
41
|
+
# allow_service_as_success!(PaymentService, with: { amount: 100 }) do
|
|
42
|
+
# { transaction_id: "txn_123" }
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# # Mock failure
|
|
46
|
+
# allow_service_as_failure(PaymentService) do
|
|
47
|
+
# {
|
|
48
|
+
# exception: ApplicationService::Exceptions::Failure.new(
|
|
49
|
+
# type: :base,
|
|
50
|
+
# message: "Failed"
|
|
51
|
+
# )
|
|
52
|
+
# }
|
|
53
|
+
# end
|
|
54
|
+
# ```
|
|
55
|
+
#
|
|
56
|
+
# @see Fluent for the recommended fluent API
|
|
57
|
+
module Legacy
|
|
58
|
+
# Mock the `call!` method to return a successful result.
|
|
59
|
+
#
|
|
60
|
+
# @param service_class [Class] The service class to mock
|
|
61
|
+
# @param with [Hash, nil] Expected arguments matcher
|
|
62
|
+
# @yield Block returning Hash of output attributes
|
|
63
|
+
# @return [void]
|
|
64
|
+
#
|
|
65
|
+
# @example Basic success mock
|
|
66
|
+
# allow_service_as_success!(PaymentService) do
|
|
67
|
+
# { transaction_id: "txn_123" }
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# @example With argument matcher
|
|
71
|
+
# allow_service_as_success!(PaymentService, with: { amount: 100 }) do
|
|
72
|
+
# { transaction_id: "txn_123" }
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
def allow_service_as_success!(service_class, with: nil, &block)
|
|
76
|
+
allow_service_legacy!(service_class, :as_success, with:, &block)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Mock the `call` method to return a successful result.
|
|
80
|
+
#
|
|
81
|
+
# @param service_class [Class] The service class to mock
|
|
82
|
+
# @param with [Hash, nil] Expected arguments matcher
|
|
83
|
+
# @yield Block returning Hash of output attributes
|
|
84
|
+
# @return [void]
|
|
85
|
+
#
|
|
86
|
+
# @example Basic success mock
|
|
87
|
+
# allow_service_as_success(PaymentService) do
|
|
88
|
+
# { transaction_id: "txn_123" }
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
def allow_service_as_success(service_class, with: nil, &block)
|
|
92
|
+
allow_service_legacy(service_class, :as_success, with:, &block)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Mock the `call!` method to raise a failure exception.
|
|
96
|
+
#
|
|
97
|
+
# @param service_class [Class] The service class to mock
|
|
98
|
+
# @param with [Hash, nil] Expected arguments matcher
|
|
99
|
+
# @yield Block returning an exception or Hash with `:exception` key
|
|
100
|
+
# @return [void]
|
|
101
|
+
#
|
|
102
|
+
# @example Failure mock
|
|
103
|
+
# allow_service_as_failure!(PaymentService) do
|
|
104
|
+
# ApplicationService::Exceptions::Failure.new(
|
|
105
|
+
# type: :payment_declined,
|
|
106
|
+
# message: "Card declined"
|
|
107
|
+
# )
|
|
108
|
+
# end
|
|
109
|
+
#
|
|
110
|
+
def allow_service_as_failure!(service_class, with: nil, &block)
|
|
111
|
+
allow_service_legacy!(service_class, :as_failure, with:, &block)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Mock the `call` method to return a failure result.
|
|
115
|
+
#
|
|
116
|
+
# @param service_class [Class] The service class to mock
|
|
117
|
+
# @param with [Hash, nil] Expected arguments matcher
|
|
118
|
+
# @yield Block returning an exception or Hash with `:exception` key
|
|
119
|
+
# @return [void]
|
|
120
|
+
#
|
|
121
|
+
# @example Failure mock with exception in hash
|
|
122
|
+
# allow_service_as_failure(PaymentService) do
|
|
123
|
+
# {
|
|
124
|
+
# exception: ApplicationService::Exceptions::Failure.new(
|
|
125
|
+
# type: :base,
|
|
126
|
+
# message: "Failed"
|
|
127
|
+
# )
|
|
128
|
+
# }
|
|
129
|
+
# end
|
|
130
|
+
#
|
|
131
|
+
def allow_service_as_failure(service_class, with: nil, &block)
|
|
132
|
+
allow_service_legacy(service_class, :as_failure, with:, &block)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# Builds legacy mock for .call! method.
|
|
138
|
+
#
|
|
139
|
+
# @param service_class [Class] The service class to mock
|
|
140
|
+
# @param result_type [Symbol] :as_success or :as_failure
|
|
141
|
+
# @param with [Hash, nil] Argument matcher
|
|
142
|
+
# @return [void]
|
|
143
|
+
def allow_service_legacy!(service_class, result_type, with: nil, &block)
|
|
144
|
+
allow_servactory_legacy(service_class, :call!, result_type, with:, &block)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Builds legacy mock for .call method.
|
|
148
|
+
#
|
|
149
|
+
# @param service_class [Class] The service class to mock
|
|
150
|
+
# @param result_type [Symbol] :as_success or :as_failure
|
|
151
|
+
# @param with [Hash, nil] Argument matcher
|
|
152
|
+
# @return [void]
|
|
153
|
+
def allow_service_legacy(service_class, result_type, with: nil, &block)
|
|
154
|
+
allow_servactory_legacy(service_class, :call, result_type, with:, &block)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Core legacy mock implementation.
|
|
158
|
+
#
|
|
159
|
+
# @param service_class [Class] The service class to mock
|
|
160
|
+
# @param method_call [Symbol] :call or :call!
|
|
161
|
+
# @param result_type [Symbol] :as_success or :as_failure
|
|
162
|
+
# @param with [Hash, nil] Argument matcher
|
|
163
|
+
# @return [void]
|
|
164
|
+
def allow_servactory_legacy(service_class, method_call, result_type, with: nil)
|
|
165
|
+
config = build_legacy_config(service_class, method_call, result_type, with, block_given? ? yield : nil)
|
|
166
|
+
|
|
167
|
+
Helpers::MockExecutor.new(
|
|
168
|
+
service_class:,
|
|
169
|
+
configs: [config],
|
|
170
|
+
rspec_context: self
|
|
171
|
+
).execute
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Builds ServiceMockConfig from legacy parameters.
|
|
175
|
+
#
|
|
176
|
+
# @param service_class [Class] The service class
|
|
177
|
+
# @param method_call [Symbol] :call or :call!
|
|
178
|
+
# @param result_type [Symbol] :as_success or :as_failure
|
|
179
|
+
# @param with_arg [Hash, nil] Argument matcher
|
|
180
|
+
# @param block_result [Hash, Object, nil] Block return value
|
|
181
|
+
# @return [ServiceMockConfig] Configured mock config
|
|
182
|
+
def build_legacy_config(service_class, method_call, result_type, with_arg, block_result)
|
|
183
|
+
config = Helpers::ServiceMockConfig.new(service_class:)
|
|
184
|
+
config.method_type = method_call.to_sym
|
|
185
|
+
config.result_type = result_type == :as_success ? :success : :failure
|
|
186
|
+
config.argument_matcher = with_arg
|
|
187
|
+
|
|
188
|
+
process_legacy_block_result(config, block_result) if block_result
|
|
189
|
+
|
|
190
|
+
config
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Processes block result into config outputs/exception.
|
|
194
|
+
#
|
|
195
|
+
# @param config [ServiceMockConfig] The config to update
|
|
196
|
+
# @param block_result [Hash, Object] Block return value
|
|
197
|
+
# @return [void]
|
|
198
|
+
def process_legacy_block_result(config, block_result) # rubocop:disable Metrics/MethodLength
|
|
199
|
+
validate_legacy_block_result!(block_result, config.success?)
|
|
200
|
+
|
|
201
|
+
if block_result.is_a?(Hash)
|
|
202
|
+
if config.success?
|
|
203
|
+
config.outputs = block_result
|
|
204
|
+
else
|
|
205
|
+
config.exception = block_result[:exception] if block_result[:exception]
|
|
206
|
+
config.outputs = block_result.except(:exception)
|
|
207
|
+
end
|
|
208
|
+
else
|
|
209
|
+
config.exception = block_result
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Validates block result format for legacy API.
|
|
214
|
+
#
|
|
215
|
+
# @param block_result [Object] The block return value
|
|
216
|
+
# @param is_success [Boolean] Whether this is a success mock
|
|
217
|
+
# @raise [ArgumentError] If success mock block doesn't return Hash
|
|
218
|
+
# @return [void]
|
|
219
|
+
def validate_legacy_block_result!(block_result, is_success)
|
|
220
|
+
return unless is_success && !block_result.is_a?(Hash)
|
|
221
|
+
|
|
222
|
+
raise ArgumentError, "Invalid value for block. Must be a Hash with attributes."
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Executes RSpec stubbing based on mock configurations.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# MockExecutor translates ServiceMockConfig objects into actual RSpec
|
|
12
|
+
# stub setups using `allow(...).to receive(...)`. It handles single
|
|
13
|
+
# and sequential call scenarios, applying appropriate return behaviors.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# Typically used internally by ServiceMockBuilder:
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# executor = MockExecutor.new(
|
|
21
|
+
# service_class: MyService,
|
|
22
|
+
# configs: [config1, config2],
|
|
23
|
+
# rspec_context: self
|
|
24
|
+
# )
|
|
25
|
+
# executor.execute
|
|
26
|
+
# ```
|
|
27
|
+
#
|
|
28
|
+
# ## Execution Strategies
|
|
29
|
+
#
|
|
30
|
+
# - **Single Config** - uses `and_return` or `and_raise` directly
|
|
31
|
+
# - **Sequential Returns** - uses `and_return(*values)` for multiple values
|
|
32
|
+
# - **Sequential with Raises** - uses `and_invoke(*callables)` for mixed behavior
|
|
33
|
+
#
|
|
34
|
+
# ## Architecture
|
|
35
|
+
#
|
|
36
|
+
# Works with:
|
|
37
|
+
# - ServiceMockConfig - provides configuration for each stub
|
|
38
|
+
# - ServiceMockBuilder - creates executor with configs
|
|
39
|
+
# - RSpec Context - provides allow/receive/etc. methods
|
|
40
|
+
class MockExecutor
|
|
41
|
+
include Concerns::ErrorMessages
|
|
42
|
+
|
|
43
|
+
# Creates a new mock executor.
|
|
44
|
+
#
|
|
45
|
+
# @param service_class [Class] The Servactory service class to stub
|
|
46
|
+
# @param configs [Array<ServiceMockConfig>] Configurations for each call
|
|
47
|
+
# @param rspec_context [Object] RSpec example context with stubbing methods
|
|
48
|
+
def initialize(service_class:, configs:, rspec_context:)
|
|
49
|
+
@service_class = service_class
|
|
50
|
+
@configs = configs
|
|
51
|
+
@rspec_context = rspec_context
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Executes the stub setup based on configurations.
|
|
55
|
+
#
|
|
56
|
+
# Validates all configs first, then applies appropriate stubbing
|
|
57
|
+
# strategy (single or sequential).
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @raise [ArgumentError] If any config is invalid
|
|
61
|
+
def execute
|
|
62
|
+
validate_configs!
|
|
63
|
+
|
|
64
|
+
if sequential?
|
|
65
|
+
execute_sequential
|
|
66
|
+
else
|
|
67
|
+
execute_single
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Checks if this is a sequential mock (multiple configs).
|
|
74
|
+
#
|
|
75
|
+
# @return [Boolean] True if more than one config
|
|
76
|
+
def sequential?
|
|
77
|
+
@configs.size > 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Executes a single-config stub.
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
83
|
+
def execute_single
|
|
84
|
+
config = @configs.first
|
|
85
|
+
method_name = config.method_type
|
|
86
|
+
arg_matcher = config.build_argument_matcher(@rspec_context)
|
|
87
|
+
|
|
88
|
+
message_expectation = @rspec_context.allow(@service_class).to(
|
|
89
|
+
@rspec_context.receive(method_name).with(arg_matcher)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
apply_return_behavior(message_expectation, config)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Executes a multi-config sequential stub.
|
|
96
|
+
#
|
|
97
|
+
# Chooses between and_return (for simple returns) and and_invoke
|
|
98
|
+
# (when exceptions need to be raised).
|
|
99
|
+
#
|
|
100
|
+
# @return [void]
|
|
101
|
+
def execute_sequential
|
|
102
|
+
method_name = @configs.first.method_type
|
|
103
|
+
arg_matcher = @configs.first.build_argument_matcher(@rspec_context)
|
|
104
|
+
|
|
105
|
+
if all_returns?
|
|
106
|
+
execute_sequential_returns(method_name, arg_matcher)
|
|
107
|
+
else
|
|
108
|
+
execute_sequential_invoke(method_name, arg_matcher)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Checks if all configs can use simple and_return.
|
|
113
|
+
#
|
|
114
|
+
# Returns false if any config is a failure with bang method,
|
|
115
|
+
# which requires raising an exception.
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean] True if all configs are simple returns
|
|
118
|
+
def all_returns?
|
|
119
|
+
@configs.none? { |config| config.failure? && config.bang_method? }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Executes sequential stub with and_return for all values.
|
|
123
|
+
#
|
|
124
|
+
# @param method_name [Symbol] The method being stubbed
|
|
125
|
+
# @param arg_matcher [Object] RSpec argument matcher
|
|
126
|
+
# @return [void]
|
|
127
|
+
def execute_sequential_returns(method_name, arg_matcher)
|
|
128
|
+
returns = @configs.map(&:build_result)
|
|
129
|
+
|
|
130
|
+
@rspec_context.allow(@service_class).to(
|
|
131
|
+
@rspec_context.receive(method_name)
|
|
132
|
+
.with(arg_matcher)
|
|
133
|
+
.and_return(*returns)
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Executes sequential stub with and_invoke for mixed behavior.
|
|
138
|
+
#
|
|
139
|
+
# Uses callables to handle both returns and raises.
|
|
140
|
+
#
|
|
141
|
+
# @param method_name [Symbol] The method being stubbed
|
|
142
|
+
# @param arg_matcher [Object] RSpec argument matcher
|
|
143
|
+
# @return [void]
|
|
144
|
+
def execute_sequential_invoke(method_name, arg_matcher)
|
|
145
|
+
callables = @configs.map { |config| build_callable(config) }
|
|
146
|
+
|
|
147
|
+
@rspec_context.allow(@service_class).to(
|
|
148
|
+
@rspec_context.receive(method_name)
|
|
149
|
+
.with(arg_matcher)
|
|
150
|
+
.and_invoke(*callables)
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Builds a callable lambda for and_invoke.
|
|
155
|
+
#
|
|
156
|
+
# @param config [ServiceMockConfig] The config to build callable for
|
|
157
|
+
# @return [Proc] Lambda that returns result or raises exception
|
|
158
|
+
def build_callable(config)
|
|
159
|
+
if config.failure? && config.bang_method?
|
|
160
|
+
->(*_args) { raise config.exception }
|
|
161
|
+
else
|
|
162
|
+
result = config.build_result
|
|
163
|
+
->(*_args) { result }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Applies return or raise behavior to a message expectation.
|
|
168
|
+
#
|
|
169
|
+
# @param message_expectation [Object] RSpec message expectation
|
|
170
|
+
# @param config [ServiceMockConfig] Configuration with result/exception
|
|
171
|
+
# @return [void]
|
|
172
|
+
def apply_return_behavior(message_expectation, config)
|
|
173
|
+
if config.failure? && config.bang_method?
|
|
174
|
+
message_expectation.and_raise(config.exception)
|
|
175
|
+
else
|
|
176
|
+
message_expectation.and_return(config.build_result)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Validates all configurations before executing.
|
|
181
|
+
#
|
|
182
|
+
# @return [void]
|
|
183
|
+
# @raise [ArgumentError] If any config is invalid
|
|
184
|
+
def validate_configs!
|
|
185
|
+
@configs.each { |config| validate_config!(config) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Validates a single configuration.
|
|
189
|
+
#
|
|
190
|
+
# @param config [ServiceMockConfig] The config to validate
|
|
191
|
+
# @return [void]
|
|
192
|
+
# @raise [ArgumentError] If config is invalid
|
|
193
|
+
def validate_config!(config)
|
|
194
|
+
validate_failure_has_exception!(config)
|
|
195
|
+
validate_exception_type!(config)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Validates that failure configs have an exception.
|
|
199
|
+
#
|
|
200
|
+
# @param config [ServiceMockConfig] The config to validate
|
|
201
|
+
# @return [void]
|
|
202
|
+
# @raise [ArgumentError] If failure config is missing exception
|
|
203
|
+
def validate_failure_has_exception!(config)
|
|
204
|
+
return unless config.failure? && config.exception.nil?
|
|
205
|
+
|
|
206
|
+
raise ArgumentError, missing_exception_for_failure_message(config.service_class)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Validates that exception is the correct type for the service.
|
|
210
|
+
#
|
|
211
|
+
# @param config [ServiceMockConfig] The config to validate
|
|
212
|
+
# @return [void]
|
|
213
|
+
# @raise [ArgumentError] If exception type is wrong
|
|
214
|
+
def validate_exception_type!(config)
|
|
215
|
+
return if config.exception.nil?
|
|
216
|
+
return if valid_exception_type?(config)
|
|
217
|
+
|
|
218
|
+
raise ArgumentError, invalid_exception_type_message(
|
|
219
|
+
service_class: config.service_class,
|
|
220
|
+
expected_class: failure_class_for(config),
|
|
221
|
+
actual_class: config.exception.class
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Checks if exception is the correct type.
|
|
226
|
+
#
|
|
227
|
+
# Uses different validation strategies based on method type:
|
|
228
|
+
# - For `.call` (non-bang): Relaxed validation - accepts any Servactory::Exceptions::Failure subclass
|
|
229
|
+
# because the exception is wrapped in Result and never raised, so type doesn't matter.
|
|
230
|
+
# - For `.call!` (bang): Strict validation - requires the service's configured failure_class
|
|
231
|
+
# because the exception IS raised and type matters for rescue clauses.
|
|
232
|
+
#
|
|
233
|
+
# @param config [ServiceMockConfig] The config to check
|
|
234
|
+
# @return [Boolean] True if exception is valid type
|
|
235
|
+
def valid_exception_type?(config)
|
|
236
|
+
if config.bang_method?
|
|
237
|
+
# Strict validation for call! - exception will be raised
|
|
238
|
+
config.exception.is_a?(failure_class_for(config))
|
|
239
|
+
else
|
|
240
|
+
# Relaxed validation for call - exception is only wrapped in Result
|
|
241
|
+
config.exception.is_a?(Servactory::Exceptions::Failure)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Returns the expected failure class for a service.
|
|
246
|
+
#
|
|
247
|
+
# @param config [ServiceMockConfig] The config with service class
|
|
248
|
+
# @return [Class] The service's failure class
|
|
249
|
+
def failure_class_for(config)
|
|
250
|
+
config.service_class.config.failure_class
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Validates mock output values against service definitions.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Ensures that mocked output values match the service's output
|
|
12
|
+
# definitions in terms of names and types. Helps catch configuration
|
|
13
|
+
# errors early in tests.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# Called automatically when `succeeds()` is used on builder:
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# allow_service(MyService)
|
|
21
|
+
# .succeeds(user: user)
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# Can also be called directly:
|
|
25
|
+
#
|
|
26
|
+
# ```ruby
|
|
27
|
+
# OutputValidator.validate!(
|
|
28
|
+
# service_class: MyService,
|
|
29
|
+
# outputs: { user: user }
|
|
30
|
+
# )
|
|
31
|
+
# ```
|
|
32
|
+
#
|
|
33
|
+
# ## Validations
|
|
34
|
+
#
|
|
35
|
+
# 1. **Output names** - all provided outputs must be defined in service
|
|
36
|
+
# 2. **Output types** - values must match expected types (if defined)
|
|
37
|
+
class OutputValidator
|
|
38
|
+
include Concerns::ErrorMessages
|
|
39
|
+
|
|
40
|
+
# Error raised when validation fails
|
|
41
|
+
class ValidationError < StandardError; end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# Validates outputs and raises on failure.
|
|
45
|
+
#
|
|
46
|
+
# @param service_class [Class] The service class
|
|
47
|
+
# @param outputs [Hash] Output values to validate
|
|
48
|
+
# @raise [ValidationError] If validation fails
|
|
49
|
+
# @return [void]
|
|
50
|
+
def validate!(service_class:, outputs:)
|
|
51
|
+
new(service_class:, outputs:).validate!
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Creates a new validator instance.
|
|
56
|
+
#
|
|
57
|
+
# @param service_class [Class] The service class
|
|
58
|
+
# @param outputs [Hash] Output values to validate
|
|
59
|
+
# @return [OutputValidator] New validator
|
|
60
|
+
def initialize(service_class:, outputs:)
|
|
61
|
+
@service_class = service_class
|
|
62
|
+
@outputs = outputs
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Runs all validations.
|
|
66
|
+
#
|
|
67
|
+
# @raise [ValidationError] If any validation fails
|
|
68
|
+
# @return [void]
|
|
69
|
+
def validate!
|
|
70
|
+
validate_output_names!
|
|
71
|
+
validate_output_types!
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Validates all output names are defined in service.
|
|
77
|
+
#
|
|
78
|
+
# @raise [ValidationError] If unknown output names provided
|
|
79
|
+
# @return [void]
|
|
80
|
+
def validate_output_names!
|
|
81
|
+
defined_outputs = @service_class.info.outputs.keys
|
|
82
|
+
provided_outputs = @outputs.keys
|
|
83
|
+
|
|
84
|
+
unknown_outputs = provided_outputs - defined_outputs
|
|
85
|
+
|
|
86
|
+
return if unknown_outputs.empty?
|
|
87
|
+
|
|
88
|
+
raise ValidationError, unknown_outputs_message(
|
|
89
|
+
service_class: @service_class,
|
|
90
|
+
unknown_outputs:,
|
|
91
|
+
defined_outputs:
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Validates output values match expected types.
|
|
96
|
+
#
|
|
97
|
+
# @raise [ValidationError] If type mismatch found
|
|
98
|
+
# @return [void]
|
|
99
|
+
def validate_output_types! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
100
|
+
@outputs.each do |name, value|
|
|
101
|
+
output_info = @service_class.info.outputs[name]
|
|
102
|
+
next unless output_info
|
|
103
|
+
|
|
104
|
+
expected_types = output_info[:types]
|
|
105
|
+
next if value.nil?
|
|
106
|
+
next if expected_types.nil? || expected_types.empty?
|
|
107
|
+
next if expected_types.any? { |type| value.is_a?(type) }
|
|
108
|
+
|
|
109
|
+
raise ValidationError, type_mismatch_message(
|
|
110
|
+
service_class: @service_class,
|
|
111
|
+
output_name: name,
|
|
112
|
+
expected_types:,
|
|
113
|
+
actual_value: value
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|