servactory 2.16.0 → 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 +3 -3
- data/lib/servactory.rb +4 -0
- metadata +73 -25
- 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,422 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Fluent builder for configuring Servactory service mocks in RSpec tests.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# ServiceMockBuilder provides a fluent API for stubbing Servactory service calls
|
|
12
|
+
# in tests. It handles both success and failure scenarios, output configuration,
|
|
13
|
+
# input argument matching, and sequential call responses.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# Basic success mock:
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# allow_service(MyService)
|
|
21
|
+
# .succeeds(result: "value")
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# Failure mock:
|
|
25
|
+
#
|
|
26
|
+
# ```ruby
|
|
27
|
+
# allow_service(MyService)
|
|
28
|
+
# .fails(type: :base, message: "Error")
|
|
29
|
+
# ```
|
|
30
|
+
#
|
|
31
|
+
# Failure mock with custom exception class:
|
|
32
|
+
#
|
|
33
|
+
# ```ruby
|
|
34
|
+
# allow_service(MyService)
|
|
35
|
+
# .fails(CustomException, type: :base, message: "Error")
|
|
36
|
+
# ```
|
|
37
|
+
#
|
|
38
|
+
# Sequential returns (first call succeeds, second fails):
|
|
39
|
+
#
|
|
40
|
+
# ```ruby
|
|
41
|
+
# allow_service(MyService)
|
|
42
|
+
# .succeeds(count: 1)
|
|
43
|
+
# .then_succeeds(count: 2)
|
|
44
|
+
# .then_fails(type: :base, message: "Error")
|
|
45
|
+
# ```
|
|
46
|
+
#
|
|
47
|
+
# With input matching:
|
|
48
|
+
#
|
|
49
|
+
# ```ruby
|
|
50
|
+
# allow_service(MyService)
|
|
51
|
+
# .with(user_id: 123)
|
|
52
|
+
# .succeeds(user: user)
|
|
53
|
+
#
|
|
54
|
+
# # Or order doesn't matter:
|
|
55
|
+
# allow_service(MyService)
|
|
56
|
+
# .succeeds(user: user)
|
|
57
|
+
# .with(user_id: 123)
|
|
58
|
+
# ```
|
|
59
|
+
#
|
|
60
|
+
# ## Features
|
|
61
|
+
#
|
|
62
|
+
# - **Fluent API** - chainable methods for readable test setup
|
|
63
|
+
# - **Success/Failure** - configure expected result type in one method
|
|
64
|
+
# - **Exception Handling** - auto-creates exceptions with type, message, meta
|
|
65
|
+
# - **Input Matching** - match specific service inputs with `.with()`
|
|
66
|
+
# - **Sequential Responses** - different results for consecutive calls
|
|
67
|
+
# - **Automatic Validation** - validates inputs and outputs against service definition
|
|
68
|
+
#
|
|
69
|
+
# ## Architecture
|
|
70
|
+
#
|
|
71
|
+
# Works with:
|
|
72
|
+
# - ServiceMockConfig - holds mock configuration state
|
|
73
|
+
# - MockExecutor - executes the actual RSpec stubbing
|
|
74
|
+
# - OutputValidator - validates outputs match service definition
|
|
75
|
+
# - InputValidator - validates inputs match service definition
|
|
76
|
+
class ServiceMockBuilder # rubocop:disable Metrics/ClassLength
|
|
77
|
+
include Concerns::ServiceClassValidation
|
|
78
|
+
include Concerns::ErrorMessages
|
|
79
|
+
|
|
80
|
+
# @return [Class] The Servactory service class being mocked
|
|
81
|
+
attr_reader :service_class
|
|
82
|
+
|
|
83
|
+
# @return [ServiceMockConfig] Current mock configuration
|
|
84
|
+
attr_reader :config
|
|
85
|
+
|
|
86
|
+
# Creates a new service mock builder.
|
|
87
|
+
#
|
|
88
|
+
# @param service_class [Class] The Servactory service class to mock
|
|
89
|
+
# @param method_type [Symbol] The method to stub (:call or :call!)
|
|
90
|
+
# @param rspec_context [Object] The RSpec example context for stubbing
|
|
91
|
+
def initialize(service_class, method_type:, rspec_context:)
|
|
92
|
+
validate_service_class!(service_class)
|
|
93
|
+
|
|
94
|
+
@service_class = service_class
|
|
95
|
+
@rspec_context = rspec_context
|
|
96
|
+
@config = ServiceMockConfig.new(service_class:)
|
|
97
|
+
@config.method_type = method_type
|
|
98
|
+
@sequential_configs = []
|
|
99
|
+
@executed = false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ============================================================
|
|
103
|
+
# Primary Fluent API
|
|
104
|
+
# ============================================================
|
|
105
|
+
|
|
106
|
+
# Configures the mock to return a successful result with outputs.
|
|
107
|
+
#
|
|
108
|
+
# Outputs are automatically validated against service definition.
|
|
109
|
+
#
|
|
110
|
+
# @param outputs_hash [Hash{Symbol => Object}] Output name-value pairs
|
|
111
|
+
# @return [ServiceMockBuilder] self for method chaining
|
|
112
|
+
#
|
|
113
|
+
# @example Basic success
|
|
114
|
+
# allow_service(PaymentService).succeeds(status: :completed)
|
|
115
|
+
#
|
|
116
|
+
# @example With input matching
|
|
117
|
+
# allow_service(PaymentService)
|
|
118
|
+
# .succeeds(transaction_id: "txn_123")
|
|
119
|
+
# .with(amount: 100)
|
|
120
|
+
#
|
|
121
|
+
# @raise [ArgumentError] if called after then_succeeds/then_fails
|
|
122
|
+
# @raise [OutputValidator::ValidationError] if outputs don't match service definition
|
|
123
|
+
def succeeds(outputs_hash = {})
|
|
124
|
+
validate_not_in_sequential_mode!(:succeeds)
|
|
125
|
+
validate_result_type_not_switched!(:succeeds)
|
|
126
|
+
|
|
127
|
+
validate_outputs!(outputs_hash)
|
|
128
|
+
@config.result_type = :success
|
|
129
|
+
@config.outputs = outputs_hash
|
|
130
|
+
execute_or_re_execute_mock
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Configures the mock to return a failure result with exception.
|
|
135
|
+
#
|
|
136
|
+
# @param exception_class [Class, nil] Exception class (default: service config.failure_class)
|
|
137
|
+
# @param type [Symbol] Error type (default: :base)
|
|
138
|
+
# @param message [String] Error message (required)
|
|
139
|
+
# @param meta [Object, nil] Optional metadata for the exception
|
|
140
|
+
# @return [ServiceMockBuilder] self for method chaining
|
|
141
|
+
#
|
|
142
|
+
# @example Minimal failure
|
|
143
|
+
# allow_service(PaymentService).fails(message: "Card declined")
|
|
144
|
+
#
|
|
145
|
+
# @example With type
|
|
146
|
+
# allow_service(PaymentService)
|
|
147
|
+
# .fails(type: :payment_declined, message: "Insufficient funds")
|
|
148
|
+
#
|
|
149
|
+
# @example With custom exception class
|
|
150
|
+
# allow_service(PaymentService)
|
|
151
|
+
# .fails(CustomException, type: :error, message: "Error")
|
|
152
|
+
#
|
|
153
|
+
# @raise [ArgumentError] if called after then_succeeds/then_fails
|
|
154
|
+
def fails(exception_class = nil, type: :base, message:, meta: nil) # rubocop:disable Style/KeywordParametersOrder
|
|
155
|
+
validate_not_in_sequential_mode!(:fails)
|
|
156
|
+
validate_result_type_not_switched!(:fails)
|
|
157
|
+
|
|
158
|
+
@config.result_type = :failure
|
|
159
|
+
@config.exception = build_exception(exception_class, type:, message:, meta:)
|
|
160
|
+
execute_or_re_execute_mock
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Configures input matching for the mock.
|
|
165
|
+
#
|
|
166
|
+
# Can be called at any position in the chain (before/after succeeds/fails,
|
|
167
|
+
# or after then_* methods). Applies to the entire mock chain.
|
|
168
|
+
#
|
|
169
|
+
# Inputs are automatically validated against service definition.
|
|
170
|
+
#
|
|
171
|
+
# @param inputs_hash_or_matcher [Hash, Object] Service inputs to match or RSpec matcher
|
|
172
|
+
# @return [ServiceMockBuilder] self for method chaining
|
|
173
|
+
#
|
|
174
|
+
# @example Exact match
|
|
175
|
+
# allow_service(S).with(amount: 100).succeeds(result: :ok)
|
|
176
|
+
#
|
|
177
|
+
# @example With matchers
|
|
178
|
+
# allow_service(S).with(including(amount: 100)).succeeds(result: :ok)
|
|
179
|
+
#
|
|
180
|
+
# @example Any position in chain
|
|
181
|
+
# allow_service(S).succeeds(result: :ok).with(amount: 100)
|
|
182
|
+
#
|
|
183
|
+
# @raise [InputValidator::ValidationError] if inputs don't match service definition
|
|
184
|
+
def with(inputs_hash_or_matcher)
|
|
185
|
+
validate_inputs!(inputs_hash_or_matcher)
|
|
186
|
+
@config.argument_matcher = inputs_hash_or_matcher
|
|
187
|
+
re_execute_mock if @executed
|
|
188
|
+
self
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# ============================================================
|
|
192
|
+
# Sequential Call API
|
|
193
|
+
# ============================================================
|
|
194
|
+
|
|
195
|
+
# Adds a successful result for sequential call handling.
|
|
196
|
+
#
|
|
197
|
+
# Use for testing code that calls the same service multiple times.
|
|
198
|
+
# Outputs are automatically validated against service definition.
|
|
199
|
+
#
|
|
200
|
+
# @param outputs_hash [Hash{Symbol => Object}] Output name-value pairs
|
|
201
|
+
# @return [ServiceMockBuilder] self for method chaining
|
|
202
|
+
#
|
|
203
|
+
# @example Multiple successes
|
|
204
|
+
# allow_service(RetryService)
|
|
205
|
+
# .succeeds(status: :pending)
|
|
206
|
+
# .then_succeeds(status: :completed)
|
|
207
|
+
#
|
|
208
|
+
# @raise [ArgumentError] if called without first calling succeeds/fails
|
|
209
|
+
# @raise [OutputValidator::ValidationError] if outputs don't match service definition
|
|
210
|
+
def then_succeeds(outputs_hash = {})
|
|
211
|
+
validate_result_type_defined!(:then_succeeds)
|
|
212
|
+
|
|
213
|
+
validate_outputs!(outputs_hash)
|
|
214
|
+
finalize_current_to_sequence
|
|
215
|
+
@config = ServiceMockConfig.new(service_class:)
|
|
216
|
+
@config.result_type = :success
|
|
217
|
+
@config.outputs = outputs_hash
|
|
218
|
+
@config.method_type = @sequential_configs.last&.method_type || :call
|
|
219
|
+
execute_sequential_mock
|
|
220
|
+
self
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Adds a failure result for sequential call handling.
|
|
224
|
+
#
|
|
225
|
+
# Use for testing code that calls the same service multiple times.
|
|
226
|
+
#
|
|
227
|
+
# @param exception_class [Class, nil] Exception class (default: service config.failure_class)
|
|
228
|
+
# @param type [Symbol] Error type (default: :base)
|
|
229
|
+
# @param message [String] Error message (required)
|
|
230
|
+
# @param meta [Object, nil] Optional metadata for the exception
|
|
231
|
+
# @return [ServiceMockBuilder] self for method chaining
|
|
232
|
+
#
|
|
233
|
+
# @example Success then failure
|
|
234
|
+
# allow_service(RetryService)
|
|
235
|
+
# .succeeds(status: :pending)
|
|
236
|
+
# .then_fails(type: :timeout, message: "Request timed out")
|
|
237
|
+
#
|
|
238
|
+
# @raise [ArgumentError] if called without first calling succeeds/fails
|
|
239
|
+
def then_fails(exception_class = nil, type: :base, message:, meta: nil) # rubocop:disable Style/KeywordParametersOrder
|
|
240
|
+
validate_result_type_defined!(:then_fails)
|
|
241
|
+
|
|
242
|
+
finalize_current_to_sequence
|
|
243
|
+
@config = ServiceMockConfig.new(service_class:)
|
|
244
|
+
@config.result_type = :failure
|
|
245
|
+
@config.exception = build_exception(exception_class, type:, message:, meta:)
|
|
246
|
+
@config.method_type = @sequential_configs.last&.method_type || :call
|
|
247
|
+
execute_sequential_mock
|
|
248
|
+
self
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
# ============================================================
|
|
254
|
+
# Validation
|
|
255
|
+
# ============================================================
|
|
256
|
+
|
|
257
|
+
# Validates that we're not in sequential mode.
|
|
258
|
+
#
|
|
259
|
+
# @param method_name [Symbol] The method being called
|
|
260
|
+
# @raise [ArgumentError] if in sequential mode
|
|
261
|
+
# @return [void]
|
|
262
|
+
def validate_not_in_sequential_mode!(method_name)
|
|
263
|
+
return if @sequential_configs.empty?
|
|
264
|
+
|
|
265
|
+
raise ArgumentError,
|
|
266
|
+
"Cannot call #{method_name}() after then_succeeds/then_fails. " \
|
|
267
|
+
"Use then_succeeds() or then_fails() to add sequential responses."
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Validates that result type is defined.
|
|
271
|
+
#
|
|
272
|
+
# @param method_name [Symbol] The method being called
|
|
273
|
+
# @raise [ArgumentError] if result type is not defined
|
|
274
|
+
# @return [void]
|
|
275
|
+
def validate_result_type_defined!(method_name)
|
|
276
|
+
return if @config.result_type_defined?
|
|
277
|
+
|
|
278
|
+
raise ArgumentError,
|
|
279
|
+
"Cannot call #{method_name}() without first calling succeeds() or fails()."
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Validates that result type is not being switched.
|
|
283
|
+
#
|
|
284
|
+
# Prevents accidental switching between succeeds() and fails().
|
|
285
|
+
#
|
|
286
|
+
# @param new_type [Symbol] :succeeds or :fails
|
|
287
|
+
# @raise [ArgumentError] if trying to switch result type
|
|
288
|
+
# @return [void]
|
|
289
|
+
def validate_result_type_not_switched!(new_type) # rubocop:disable Metrics/CyclomaticComplexity
|
|
290
|
+
return unless @config.result_type_defined?
|
|
291
|
+
|
|
292
|
+
current_type = @config.success? ? :succeeds : :fails
|
|
293
|
+
return if (new_type == :succeeds && current_type == :succeeds) ||
|
|
294
|
+
(new_type == :fails && current_type == :fails)
|
|
295
|
+
|
|
296
|
+
raise ArgumentError,
|
|
297
|
+
"Cannot call #{new_type}() after #{current_type}() was already called. " \
|
|
298
|
+
"#{new_type == :succeeds ? 'succeeds()' : 'fails()'} replaces the result type, " \
|
|
299
|
+
"which is likely a mistake. Create a new mock if you need different behavior."
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Validates outputs against service definition.
|
|
303
|
+
#
|
|
304
|
+
# @param outputs_hash [Hash] Outputs to validate
|
|
305
|
+
# @return [void]
|
|
306
|
+
# @raise [OutputValidator::ValidationError] If outputs don't match service definition
|
|
307
|
+
def validate_outputs!(outputs_hash)
|
|
308
|
+
return if outputs_hash.empty?
|
|
309
|
+
|
|
310
|
+
OutputValidator.validate!(
|
|
311
|
+
service_class:,
|
|
312
|
+
outputs: outputs_hash
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Validates inputs against service definition.
|
|
317
|
+
#
|
|
318
|
+
# @param inputs_matcher [Hash, Object] Inputs or matcher to validate
|
|
319
|
+
# @return [void]
|
|
320
|
+
# @raise [InputValidator::ValidationError] If inputs don't match service definition
|
|
321
|
+
def validate_inputs!(inputs_matcher)
|
|
322
|
+
InputValidator.validate!(
|
|
323
|
+
service_class:,
|
|
324
|
+
inputs_matcher:
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# ============================================================
|
|
329
|
+
# Exception Building
|
|
330
|
+
# ============================================================
|
|
331
|
+
|
|
332
|
+
# Builds exception instance for failure mocks.
|
|
333
|
+
#
|
|
334
|
+
# Uses service's configured failure_class if no exception_class provided.
|
|
335
|
+
#
|
|
336
|
+
# @param exception_class [Class, nil] Exception class to use
|
|
337
|
+
# @param type [Symbol] Error type
|
|
338
|
+
# @param message [String] Error message
|
|
339
|
+
# @param meta [Object, nil] Optional metadata
|
|
340
|
+
# @return [Exception] The constructed exception instance
|
|
341
|
+
def build_exception(exception_class, type:, message:, meta:)
|
|
342
|
+
klass = exception_class || default_failure_class
|
|
343
|
+
klass.new(type:, message:, meta:)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Gets the default failure class from service configuration.
|
|
347
|
+
#
|
|
348
|
+
# @return [Class] The service's configured failure class
|
|
349
|
+
def default_failure_class
|
|
350
|
+
service_class.config.failure_class
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# ============================================================
|
|
354
|
+
# Mock Execution
|
|
355
|
+
# ============================================================
|
|
356
|
+
|
|
357
|
+
# Executes or re-executes mock depending on current state.
|
|
358
|
+
#
|
|
359
|
+
# @return [void]
|
|
360
|
+
def execute_or_re_execute_mock
|
|
361
|
+
if @executed
|
|
362
|
+
re_execute_mock
|
|
363
|
+
else
|
|
364
|
+
execute_mock
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Saves current config to sequential list.
|
|
369
|
+
#
|
|
370
|
+
# @return [void]
|
|
371
|
+
def finalize_current_to_sequence
|
|
372
|
+
@sequential_configs << @config.dup
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Executes the mock for the first time.
|
|
376
|
+
#
|
|
377
|
+
# @return [void]
|
|
378
|
+
def execute_mock
|
|
379
|
+
return if @executed
|
|
380
|
+
|
|
381
|
+
@executed = true
|
|
382
|
+
MockExecutor.new(
|
|
383
|
+
service_class:,
|
|
384
|
+
configs: [@config],
|
|
385
|
+
rspec_context: @rspec_context
|
|
386
|
+
).execute
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Re-executes the mock after configuration changes.
|
|
390
|
+
#
|
|
391
|
+
# @return [void]
|
|
392
|
+
def re_execute_mock
|
|
393
|
+
return unless @executed
|
|
394
|
+
|
|
395
|
+
if @sequential_configs.any?
|
|
396
|
+
execute_sequential_mock
|
|
397
|
+
else
|
|
398
|
+
MockExecutor.new(
|
|
399
|
+
service_class:,
|
|
400
|
+
configs: [@config],
|
|
401
|
+
rspec_context: @rspec_context
|
|
402
|
+
).execute
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Executes the mock with all sequential configurations.
|
|
407
|
+
#
|
|
408
|
+
# @return [void]
|
|
409
|
+
def execute_sequential_mock
|
|
410
|
+
all_configs = @sequential_configs + [@config]
|
|
411
|
+
|
|
412
|
+
MockExecutor.new(
|
|
413
|
+
service_class:,
|
|
414
|
+
configs: all_configs,
|
|
415
|
+
rspec_context: @rspec_context
|
|
416
|
+
).execute
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Configuration object for a single service mock.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Holds all configuration for mocking a single service call, including
|
|
12
|
+
# result type (success/failure), method type (call/call!), outputs,
|
|
13
|
+
# exceptions, and argument matchers. Used by ServiceMockBuilder and
|
|
14
|
+
# MockExecutor.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# Usually created internally by ServiceMockBuilder:
|
|
19
|
+
#
|
|
20
|
+
# ```ruby
|
|
21
|
+
# config = ServiceMockConfig.new(service_class: MyService)
|
|
22
|
+
# config.result_type = :success
|
|
23
|
+
# config.outputs = { user: user }
|
|
24
|
+
# config.method_type = :call
|
|
25
|
+
# ```
|
|
26
|
+
#
|
|
27
|
+
# ## Attributes
|
|
28
|
+
#
|
|
29
|
+
# - `service_class` - The service class being mocked
|
|
30
|
+
# - `result_type` - :success or :failure
|
|
31
|
+
# - `method_type` - :call or :call!
|
|
32
|
+
# - `outputs` - Hash of output values
|
|
33
|
+
# - `exception` - Exception for failure mocks
|
|
34
|
+
# - `argument_matcher` - RSpec argument matcher or Hash
|
|
35
|
+
class ServiceMockConfig
|
|
36
|
+
attr_accessor :service_class,
|
|
37
|
+
:result_type,
|
|
38
|
+
:method_type,
|
|
39
|
+
:outputs,
|
|
40
|
+
:exception,
|
|
41
|
+
:argument_matcher
|
|
42
|
+
|
|
43
|
+
# Creates a new mock configuration.
|
|
44
|
+
#
|
|
45
|
+
# @param service_class [Class] The service class to mock
|
|
46
|
+
# @return [ServiceMockConfig] New config instance
|
|
47
|
+
def initialize(service_class:)
|
|
48
|
+
@service_class = service_class
|
|
49
|
+
@result_type = nil
|
|
50
|
+
@method_type = :call
|
|
51
|
+
@outputs = {}
|
|
52
|
+
@exception = nil
|
|
53
|
+
@argument_matcher = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Checks if this is a success mock.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] True if result_type is :success
|
|
59
|
+
def success?
|
|
60
|
+
result_type == :success
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Checks if this is a failure mock.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean] True if result_type is :failure
|
|
66
|
+
def failure?
|
|
67
|
+
result_type == :failure
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Checks if this mocks the .call! method.
|
|
71
|
+
#
|
|
72
|
+
# @return [Boolean] True if method_type is :call!
|
|
73
|
+
def bang_method?
|
|
74
|
+
method_type == :call!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Checks if result type has been set.
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean] True if result_type is not nil
|
|
80
|
+
def result_type_defined?
|
|
81
|
+
!result_type.nil?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Builds a Servactory::TestKit::Result from this config.
|
|
85
|
+
#
|
|
86
|
+
# @return [Servactory::TestKit::Result] Mock result object
|
|
87
|
+
def build_result
|
|
88
|
+
result_attrs = outputs.merge(service_class:)
|
|
89
|
+
|
|
90
|
+
if success?
|
|
91
|
+
Servactory::TestKit::Result.as_success(**result_attrs)
|
|
92
|
+
else
|
|
93
|
+
result_attrs[:exception] = exception if exception
|
|
94
|
+
Servactory::TestKit::Result.as_failure(**result_attrs)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Builds RSpec argument matcher from config.
|
|
99
|
+
#
|
|
100
|
+
# If no matcher specified, builds one from service input names.
|
|
101
|
+
#
|
|
102
|
+
# @param rspec_context [Object] The RSpec test context
|
|
103
|
+
# @return [Object] RSpec argument matcher
|
|
104
|
+
def build_argument_matcher(rspec_context)
|
|
105
|
+
return argument_matcher if argument_matcher.present?
|
|
106
|
+
|
|
107
|
+
input_names = service_class.info.inputs.keys
|
|
108
|
+
return rspec_context.no_args if input_names.empty?
|
|
109
|
+
|
|
110
|
+
input_names.to_h { |input_name| [input_name, rspec_context.anything] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Creates a deep copy of this config.
|
|
114
|
+
#
|
|
115
|
+
# @return [ServiceMockConfig] New config with copied values
|
|
116
|
+
def dup
|
|
117
|
+
copy = self.class.new(service_class:)
|
|
118
|
+
copy.result_type = result_type
|
|
119
|
+
copy.method_type = method_type
|
|
120
|
+
copy.outputs = outputs.dup
|
|
121
|
+
copy.exception = exception
|
|
122
|
+
copy.argument_matcher = argument_matcher
|
|
123
|
+
copy
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -1,93 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "helpers/concerns/error_messages"
|
|
4
|
+
require_relative "helpers/concerns/service_class_validation"
|
|
5
|
+
require_relative "helpers/service_mock_config"
|
|
6
|
+
require_relative "helpers/mock_executor"
|
|
7
|
+
require_relative "helpers/input_validator"
|
|
8
|
+
require_relative "helpers/output_validator"
|
|
9
|
+
require_relative "helpers/argument_matchers"
|
|
10
|
+
require_relative "helpers/service_mock_builder"
|
|
11
|
+
require_relative "helpers/fluent"
|
|
12
|
+
require_relative "helpers/legacy"
|
|
13
|
+
|
|
3
14
|
module Servactory
|
|
4
15
|
module TestKit
|
|
5
16
|
module Rspec
|
|
17
|
+
# RSpec helper methods for mocking Servactory services.
|
|
18
|
+
#
|
|
19
|
+
# ## Purpose
|
|
20
|
+
#
|
|
21
|
+
# Provides convenient helper methods for mocking Servactory service calls
|
|
22
|
+
# in RSpec tests. Supports both a modern fluent API and backward-compatible
|
|
23
|
+
# legacy methods.
|
|
24
|
+
#
|
|
25
|
+
# ## Usage
|
|
26
|
+
#
|
|
27
|
+
# Include in RSpec configuration:
|
|
28
|
+
#
|
|
29
|
+
# ```ruby
|
|
30
|
+
# RSpec.configure do |config|
|
|
31
|
+
# config.include Servactory::TestKit::Rspec::Helpers, type: :service
|
|
32
|
+
# end
|
|
33
|
+
# ```
|
|
34
|
+
#
|
|
35
|
+
# ## Available Helpers
|
|
36
|
+
#
|
|
37
|
+
# **Fluent API (recommended):**
|
|
38
|
+
# - `allow_service(ServiceClass)` - mock `.call` method (returns Result)
|
|
39
|
+
# - `allow_service!(ServiceClass)` - mock `.call!` method (raises on failure)
|
|
40
|
+
#
|
|
41
|
+
# **Backward-Compatible API:**
|
|
42
|
+
# - `allow_service_as_success!` / `allow_service_as_success`
|
|
43
|
+
# - `allow_service_as_failure!` / `allow_service_as_failure`
|
|
44
|
+
#
|
|
45
|
+
# **Argument Matchers:**
|
|
46
|
+
# - `including(hash)` - partial hash matching
|
|
47
|
+
# - `excluding(hash)` - exclusion matching
|
|
48
|
+
# - `any_inputs` - match any arguments
|
|
49
|
+
# - `no_inputs` - match no arguments
|
|
50
|
+
#
|
|
51
|
+
# @see Helpers::Fluent for fluent API documentation
|
|
52
|
+
# @see Helpers::Legacy for backward-compatible API documentation
|
|
53
|
+
# @see Helpers::ArgumentMatchers for argument matcher documentation
|
|
6
54
|
module Helpers
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def allow_service_as_success(service_class, with: nil, &block)
|
|
12
|
-
allow_service(service_class, :as_success, with:, &block)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def allow_service_as_failure!(service_class, with: nil, &block)
|
|
16
|
-
allow_service!(service_class, :as_failure, with:, &block)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def allow_service_as_failure(service_class, with: nil, &block)
|
|
20
|
-
allow_service(service_class, :as_failure, with:, &block)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
########################################################################
|
|
24
|
-
|
|
25
|
-
def allow_service!(service_class, result_type, with: nil, &block)
|
|
26
|
-
allow_servactory(service_class, :call!, result_type, with:, &block)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def allow_service(service_class, result_type, with: nil, &block)
|
|
30
|
-
allow_servactory(service_class, :call, result_type, with:, &block)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
########################################################################
|
|
34
|
-
|
|
35
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
36
|
-
def allow_servactory(service_class, method_call, result_type, with: nil)
|
|
37
|
-
method_call = method_call.to_sym
|
|
38
|
-
result_type = result_type.to_sym
|
|
39
|
-
|
|
40
|
-
unless %i[call! call].include?(method_call)
|
|
41
|
-
raise ArgumentError, "Invalid value for `method_call`. Must be `:call!` or `:call`."
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
unless %i[as_success as_failure].include?(result_type)
|
|
45
|
-
raise ArgumentError, "Invalid value for `result_type`. Must be `:as_success` or `:as_failure`."
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
as_success = result_type == :as_success
|
|
49
|
-
with_bang = method_call == :call!
|
|
50
|
-
|
|
51
|
-
if block_given? && !yield.is_a?(Hash) && as_success
|
|
52
|
-
raise ArgumentError, "Invalid value for block. Must be a Hash with attributes."
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
and_return_or_raise = with_bang && !as_success ? :and_raise : :and_return
|
|
56
|
-
|
|
57
|
-
result = if block_given?
|
|
58
|
-
if yield.is_a?(Hash)
|
|
59
|
-
yield
|
|
60
|
-
else
|
|
61
|
-
{ exception: yield }
|
|
62
|
-
end
|
|
63
|
-
else
|
|
64
|
-
{}
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
result[:service_class] = service_class
|
|
68
|
-
|
|
69
|
-
allow(service_class).to(
|
|
70
|
-
receive(method_call)
|
|
71
|
-
.with(
|
|
72
|
-
if with.present?
|
|
73
|
-
with
|
|
74
|
-
elsif (input_names = service_class.info.inputs.keys).present?
|
|
75
|
-
input_names.to_h { |input_name| [input_name, anything] }
|
|
76
|
-
else
|
|
77
|
-
no_args
|
|
78
|
-
end
|
|
79
|
-
)
|
|
80
|
-
.public_send(
|
|
81
|
-
and_return_or_raise,
|
|
82
|
-
if as_success || !with_bang
|
|
83
|
-
Servactory::TestKit::Result.public_send(result_type, **result)
|
|
84
|
-
else
|
|
85
|
-
result.fetch(:exception)
|
|
86
|
-
end
|
|
87
|
-
)
|
|
88
|
-
)
|
|
89
|
-
end
|
|
90
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
55
|
+
include Helpers::ArgumentMatchers
|
|
56
|
+
include Helpers::Fluent
|
|
57
|
+
include Helpers::Legacy
|
|
91
58
|
end
|
|
92
59
|
end
|
|
93
60
|
end
|