servactory 2.16.1 → 3.0.0.rc2
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/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 +248 -0
- data/lib/servactory/version.rb +4 -4
- data/lib/servactory.rb +6 -0
- metadata +57 -48
- 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,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Validates mock input values against service definitions.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Ensures that mocked input arguments match the service's input
|
|
12
|
+
# definitions in terms of names. Called automatically when using
|
|
13
|
+
# `.with()` on the fluent builder.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# Called automatically from ServiceMockBuilder:
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# allow_service(MyService)
|
|
21
|
+
# .with(user_id: 123)
|
|
22
|
+
# .succeeds(result: "ok")
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# Can also be called directly:
|
|
26
|
+
#
|
|
27
|
+
# ```ruby
|
|
28
|
+
# InputValidator.validate!(
|
|
29
|
+
# service_class: MyService,
|
|
30
|
+
# inputs_matcher: { user_id: 123 }
|
|
31
|
+
# )
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# ## Supported Matcher Types
|
|
35
|
+
#
|
|
36
|
+
# - Hash - validates all keys against service inputs
|
|
37
|
+
# - `including(hash)` - validates specified keys only
|
|
38
|
+
# - `any_inputs` - skips validation (accepts anything)
|
|
39
|
+
# - `no_inputs` - validates service has no required inputs
|
|
40
|
+
# - `excluding(hash)` - skips validation (cannot validate exclusion)
|
|
41
|
+
class InputValidator
|
|
42
|
+
include Concerns::ErrorMessages
|
|
43
|
+
|
|
44
|
+
# Error raised when validation fails
|
|
45
|
+
class ValidationError < StandardError; end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# Validates inputs and raises on failure.
|
|
49
|
+
#
|
|
50
|
+
# @param service_class [Class] The service class
|
|
51
|
+
# @param inputs_matcher [Hash, Object] Input values or matcher
|
|
52
|
+
# @raise [ValidationError] If validation fails
|
|
53
|
+
# @return [void]
|
|
54
|
+
def validate!(service_class:, inputs_matcher:)
|
|
55
|
+
new(service_class:, inputs_matcher:).validate!
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates a new validator instance.
|
|
60
|
+
#
|
|
61
|
+
# @param service_class [Class] The service class
|
|
62
|
+
# @param inputs_matcher [Hash, Object] Input values or matcher
|
|
63
|
+
# @return [InputValidator] New validator
|
|
64
|
+
def initialize(service_class:, inputs_matcher:)
|
|
65
|
+
@service_class = service_class
|
|
66
|
+
@inputs_matcher = inputs_matcher
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Runs validation based on matcher type.
|
|
70
|
+
#
|
|
71
|
+
# @raise [ValidationError] If validation fails
|
|
72
|
+
# @return [void]
|
|
73
|
+
def validate!
|
|
74
|
+
keys_to_validate = extract_keys_for_validation
|
|
75
|
+
|
|
76
|
+
return if keys_to_validate.nil?
|
|
77
|
+
|
|
78
|
+
if keys_to_validate == :no_args
|
|
79
|
+
validate_service_has_no_required_inputs!
|
|
80
|
+
else
|
|
81
|
+
validate_input_names!(keys_to_validate)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Extracts keys to validate based on matcher type.
|
|
88
|
+
#
|
|
89
|
+
# @return [Array<Symbol>, Symbol, nil] Keys to validate, :no_args marker, or nil to skip
|
|
90
|
+
def extract_keys_for_validation # rubocop:disable Metrics/MethodLength
|
|
91
|
+
case @inputs_matcher
|
|
92
|
+
when Hash
|
|
93
|
+
@inputs_matcher.keys
|
|
94
|
+
when RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
|
|
95
|
+
extract_hash_including_keys
|
|
96
|
+
when RSpec::Mocks::ArgumentMatchers::NoArgsMatcher
|
|
97
|
+
:no_args
|
|
98
|
+
when RSpec::Mocks::ArgumentMatchers::AnyArgMatcher,
|
|
99
|
+
RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Extracts keys from HashIncludingMatcher.
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<Symbol>, nil] Keys from the matcher or nil
|
|
107
|
+
def extract_hash_including_keys
|
|
108
|
+
@inputs_matcher.instance_variable_get(:@expected)&.keys
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Validates all input names are defined in service.
|
|
114
|
+
#
|
|
115
|
+
# @param provided_keys [Array<Symbol>] Keys to validate
|
|
116
|
+
# @raise [ValidationError] If unknown input names provided
|
|
117
|
+
# @return [void]
|
|
118
|
+
def validate_input_names!(provided_keys)
|
|
119
|
+
defined_inputs = @service_class.info.inputs.keys
|
|
120
|
+
unknown_inputs = provided_keys - defined_inputs
|
|
121
|
+
|
|
122
|
+
return if unknown_inputs.empty?
|
|
123
|
+
|
|
124
|
+
raise ValidationError, unknown_inputs_message(
|
|
125
|
+
service_class: @service_class,
|
|
126
|
+
unknown_inputs:,
|
|
127
|
+
defined_inputs:
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Validates service has no required inputs when no_inputs used.
|
|
132
|
+
#
|
|
133
|
+
# @raise [ValidationError] If service has required inputs
|
|
134
|
+
# @return [void]
|
|
135
|
+
def validate_service_has_no_required_inputs!
|
|
136
|
+
required_inputs = @service_class.info.inputs.reject { |_, v| v[:required] == false }.keys
|
|
137
|
+
|
|
138
|
+
return if required_inputs.empty?
|
|
139
|
+
|
|
140
|
+
raise ValidationError, no_inputs_but_required_message(
|
|
141
|
+
service_class: @service_class,
|
|
142
|
+
required_inputs:
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -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
|